Fix for x86_64 build fail
[platform/upstream/connectedhomeip.git] / third_party / pigweed / repo / pw_module / py / pw_module / check.py
1 # Copyright 2020 The Pigweed Authors
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License"); you may not
4 # use this file except in compliance with the License. You may obtain a copy of
5 # the License at
6 #
7 #     https://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12 # License for the specific language governing permissions and limitations under
13 # the License.
14 """Checks a Pigweed module's format and structure."""
15
16 import argparse
17 import logging
18 import pathlib
19 import glob
20 from enum import Enum
21 from typing import Callable, NamedTuple, Sequence
22
23 _LOG = logging.getLogger(__name__)
24
25 CheckerFunction = Callable[[str], None]
26
27
28 def check_modules(modules: Sequence[str]) -> int:
29     if len(modules) > 1:
30         _LOG.info('Checking %d modules', len(modules))
31
32     passed = 0
33
34     for path in modules:
35         if len(modules) > 1:
36             print()
37             print(f' {path} '.center(80, '='))
38
39         passed += check_module(path)
40
41     if len(modules) > 1:
42         _LOG.info('%d of %d modules passed', passed, len(modules))
43
44     return 0 if passed == len(modules) else 1
45
46
47 def check_module(module) -> bool:
48     """Runs module checks on one module; returns True if the module passes."""
49
50     if not pathlib.Path(module).is_dir():
51         _LOG.error('No directory found: %s', module)
52         return False
53
54     found_any_warnings = False
55     found_any_errors = False
56
57     _LOG.info('Checking module: %s', module)
58     # Run each checker.
59     for check in _checkers:
60         _LOG.debug(
61             'Running checker: %s - %s',
62             check.name,
63             check.description,
64         )
65         issues = list(check.run(module))
66
67         # Log any issues found
68         for issue in issues:
69             if issue.severity == Severity.ERROR:
70                 log_level = logging.ERROR
71                 found_any_errors = True
72             elif issue.severity == Severity.WARNING:
73                 log_level = logging.WARNING
74                 found_any_warnings = True
75
76             # Try to make an error message that will help editors open the part
77             # of the module in question (e.g. vim's 'cerr' functionality).
78             components = [
79                 x for x in (
80                     issue.file,
81                     issue.line_number,
82                     issue.line_contents,
83                 ) if x
84             ]
85             editor_error_line = ':'.join(components)
86             if editor_error_line:
87                 _LOG.log(log_level, '%s', check.name)
88                 print(editor_error_line, issue.message)
89             else:
90                 # No per-file error to put in a "cerr" list, so just log.
91                 _LOG.log(log_level, '%s: %s', check.name, issue.message)
92
93         if issues:
94             _LOG.debug('Done running checker: %s (issues found)', check.name)
95         else:
96             _LOG.debug('Done running checker: %s (OK)', check.name)
97
98     # TODO(keir): Give this a proper ASCII art treatment.
99     if not found_any_warnings and not found_any_errors:
100         _LOG.info('OK: Module %s looks good; no errors or warnings found',
101                   module)
102     if found_any_errors:
103         _LOG.error('FAIL: Found errors when checking module %s', module)
104         return False
105
106     return True
107
108
109 class Checker(NamedTuple):
110     name: str
111     description: str
112     run: CheckerFunction
113
114
115 class Severity(Enum):
116     ERROR = 1
117     WARNING = 2
118
119
120 class Issue(NamedTuple):
121     message: str
122     file: str = ''
123     line_number: str = ''
124     line_contents: str = ''
125     severity: Severity = Severity.ERROR
126
127
128 _checkers = []
129
130
131 def checker(pwck_id, description):
132     def inner_decorator(function):
133         _checkers.append(Checker(pwck_id, description, function))
134         return function
135
136     return inner_decorator
137
138
139 @checker('PWCK001', 'If there is Python code, there is a setup.py')
140 def check_python_proper_module(directory):
141     module_python_files = glob.glob(f'{directory}/**/*.py', recursive=True)
142     module_setup_py = glob.glob(f'{directory}/**/setup.py', recursive=True)
143     if module_python_files and not module_setup_py:
144         yield Issue('Python code present but no setup.py.')
145
146
147 @checker('PWCK002', 'If there are C++ files, there are C++ tests')
148 def check_have_cc_tests(directory):
149     module_cc_files = glob.glob(f'{directory}/**/*.cc', recursive=True)
150     module_cc_test_files = glob.glob(f'{directory}/**/*test.cc',
151                                      recursive=True)
152     if module_cc_files and not module_cc_test_files:
153         yield Issue('C++ code present but no tests at all (you monster).')
154
155
156 @checker('PWCK003', 'If there are Python files, there are Python tests')
157 def check_have_python_tests(directory):
158     module_py_files = glob.glob(f'{directory}/**/*.py', recursive=True)
159     module_py_test_files = glob.glob(f'{directory}/**/*test*.py',
160                                      recursive=True)
161     if module_py_files and not module_py_test_files:
162         yield Issue('Python code present but no tests (you monster).')
163
164
165 @checker('PWCK004', 'There is a README.md')
166 def check_has_readme(directory):
167     if not glob.glob(f'{directory}/README.md'):
168         yield Issue('Missing module top-level README.md')
169
170
171 @checker('PWCK005', 'There is ReST documentation (*.rst)')
172 def check_has_rst_docs(directory):
173     if not glob.glob(f'{directory}/**/*.rst', recursive=True):
174         yield Issue(
175             'Missing ReST documentation; need at least e.g. "docs.rst"')
176
177
178 @checker('PWCK006', 'If C++, have <mod>/public/<mod>/*.h or '
179          '<mod>/public_override/*.h')
180 def check_has_public_or_override_headers(directory):
181     # TODO: Should likely have a decorator to check for C++ in a checker, or
182     # other more useful and cachable mechanisms.
183     if (not glob.glob(f'{directory}/**/*.cc', recursive=True)
184             and not glob.glob(f'{directory}/**/*.h', recursive=True)):
185         # No C++ files.
186         return
187
188     module_name = pathlib.Path(directory).name
189
190     has_public_cpp_headers = glob.glob(f'{directory}/public/{module_name}/*.h')
191     has_public_cpp_override_headers = glob.glob(
192         f'{directory}/public_overrides/**/*.h')
193
194     if not has_public_cpp_headers and not has_public_cpp_override_headers:
195         yield Issue(f'Have C++ code but no public/{module_name}/*.h '
196                     'found and no public_overrides/ found')
197
198     multiple_public_directories = glob.glob(f'{directory}/public/*')
199     if len(multiple_public_directories) != 1:
200         yield Issue(f'Have multiple directories under public/; there should '
201                     f'only be a single directory: "public/{module_name}". '
202                     'Perhaps you were looking for public_overrides/?.')
203
204
205 def main() -> None:
206     """Check that a module matches Pigweed's module guidelines."""
207     parser = argparse.ArgumentParser(description=__doc__)
208     parser.add_argument('modules', nargs='+', help='The module to check')
209     check_modules(**vars(parser.parse_args()))