1 # Copyright 2020 The Pigweed Authors
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
7 # https://www.apache.org/licenses/LICENSE-2.0
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
14 """Checks a Pigweed module's format and structure."""
21 from typing import Callable, NamedTuple, Sequence
23 _LOG = logging.getLogger(__name__)
25 CheckerFunction = Callable[[str], None]
28 def check_modules(modules: Sequence[str]) -> int:
30 _LOG.info('Checking %d modules', len(modules))
37 print(f' {path} '.center(80, '='))
39 passed += check_module(path)
42 _LOG.info('%d of %d modules passed', passed, len(modules))
44 return 0 if passed == len(modules) else 1
47 def check_module(module) -> bool:
48 """Runs module checks on one module; returns True if the module passes."""
50 if not pathlib.Path(module).is_dir():
51 _LOG.error('No directory found: %s', module)
54 found_any_warnings = False
55 found_any_errors = False
57 _LOG.info('Checking module: %s', module)
59 for check in _checkers:
61 'Running checker: %s - %s',
65 issues = list(check.run(module))
67 # Log any issues found
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
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).
85 editor_error_line = ':'.join(components)
87 _LOG.log(log_level, '%s', check.name)
88 print(editor_error_line, issue.message)
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)
94 _LOG.debug('Done running checker: %s (issues found)', check.name)
96 _LOG.debug('Done running checker: %s (OK)', check.name)
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',
103 _LOG.error('FAIL: Found errors when checking module %s', module)
109 class Checker(NamedTuple):
115 class Severity(Enum):
120 class Issue(NamedTuple):
123 line_number: str = ''
124 line_contents: str = ''
125 severity: Severity = Severity.ERROR
131 def checker(pwck_id, description):
132 def inner_decorator(function):
133 _checkers.append(Checker(pwck_id, description, function))
136 return inner_decorator
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.')
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',
152 if module_cc_files and not module_cc_test_files:
153 yield Issue('C++ code present but no tests at all (you monster).')
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',
161 if module_py_files and not module_py_test_files:
162 yield Issue('Python code present but no tests (you monster).')
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')
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):
175 'Missing ReST documentation; need at least e.g. "docs.rst"')
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)):
188 module_name = pathlib.Path(directory).name
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')
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')
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/?.')
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()))