| #!/usr/bin/env python3 |
| # Copyright 2021 The Pigweed Authors |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| # use this file except in compliance with the License. You may obtain a copy of |
| # the License at |
| # |
| # https://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| # License for the specific language governing permissions and limitations under |
| # the License. |
| """Invoke clang-tidy. |
| |
| Implements additional features compared to directly calling |
| clang-tidy: |
| - add option `--source-exclude` to exclude matching sources from the |
| clang-tidy analysis. |
| - inputs the full compile command, with the cc binary name |
| - TODO: infer platform options from the full compile command |
| """ |
| |
| import argparse |
| import logging |
| from pathlib import Path |
| import re |
| import shlex |
| import subprocess |
| import sys |
| from typing import Iterable, List, Optional, Union |
| |
| _LOG = logging.getLogger(__name__) |
| |
| |
| def _parse_args() -> argparse.Namespace: |
| """Parses arguments for this script, splitting out the command to run.""" |
| |
| parser = argparse.ArgumentParser() |
| parser.add_argument('-v', |
| '--verbose', |
| action='store_true', |
| help='Run clang_tidy with extra debug output.') |
| |
| parser.add_argument('--clang-tidy', |
| default='clang-tidy', |
| help='Path to clang-tidy executable.') |
| |
| parser.add_argument( |
| '--source-file', |
| required=True, |
| type=Path, |
| help='Path to the source file to analyze with clang-tidy.') |
| parser.add_argument( |
| '--source-root', |
| required=True, |
| type=Path, |
| help=('Path to the root source directory.' |
| ' The relative path from the root directory is matched' |
| ' against source filter rather than the absolute path.')) |
| parser.add_argument( |
| '--export-fixes', |
| required=False, |
| type=Path, |
| help=('YAML file to store suggested fixes in. The ' |
| 'stored fixes can be applied to the input source ' |
| 'code with clang-apply-replacements.')) |
| |
| parser.add_argument('--source-exclude', |
| default=[], |
| action='append', |
| type=str, |
| help=('Regular expressions matching the paths of' |
| ' source files to be excluded from the' |
| ' analysis.')) |
| |
| parser.add_argument( |
| '--skip-include-path', |
| default=[], |
| action='append', |
| type=str, |
| help=('Exclude include paths ending in these paths from clang-tidy. ' |
| 'These paths are switched from -I to -isystem so clang-tidy ' |
| 'ignores them.')) |
| |
| # Add a silent placeholder arg for everything that was left over. |
| parser.add_argument('extra_args', |
| nargs=argparse.REMAINDER, |
| help=argparse.SUPPRESS) |
| |
| parsed_args = parser.parse_args() |
| |
| if parsed_args.extra_args[0] != '--': |
| parser.error('arguments not correctly split') |
| parsed_args.extra_args = parsed_args.extra_args[1:] |
| return parsed_args |
| |
| |
| def _filter_include_paths(args: Iterable[str], |
| skip_include_paths: Iterable[str]) -> Iterable[str]: |
| filters = [f.rstrip('/') for f in skip_include_paths] |
| |
| for arg in args: |
| if arg.startswith('-I'): |
| path = Path(arg[2:]).as_posix() |
| if any( |
| path.endswith(f) or re.match(f, str(path)) |
| for f in filters): |
| yield '-isystem' + arg[2:] |
| continue |
| if arg.startswith('--sysroot'): |
| path = Path(arg[9:]).as_posix() |
| if any( |
| path.endswith(f) or re.match(f, str(path)) |
| for f in filters): |
| yield '-isysroot' + arg[9:] |
| continue |
| |
| yield arg |
| |
| |
| def run_clang_tidy(clang_tidy: str, verbose: bool, source_file: Path, |
| export_fixes: Optional[Path], skip_include_path: List[str], |
| extra_args: List[str]) -> int: |
| """Executes clang_tidy via subprocess. Returns true if no failures.""" |
| command: List[Union[str, Path]] = [clang_tidy, source_file, '--use-color'] |
| |
| if not verbose: |
| command.append('--quiet') |
| |
| if export_fixes is not None: |
| command.extend(['--export-fixes', export_fixes]) |
| |
| # Append extra compilation flags. Extra args up to |
| # END_OF_INVOKER are skipped. |
| command.append('--') |
| end_of_invoker = extra_args.index('END_OF_INVOKER') |
| command.extend( |
| _filter_include_paths(extra_args[end_of_invoker + 1:], |
| skip_include_path)) |
| |
| process = subprocess.run( |
| command, |
| stdout=subprocess.PIPE, |
| # clang-tidy prints regular information on |
| # stderr, even with the option --quiet. |
| stderr=subprocess.PIPE) |
| if process.returncode != 0: |
| _LOG.warning('%s', ' '.join(shlex.quote(str(arg)) for arg in command)) |
| |
| if process.stdout: |
| _LOG.warning(process.stdout.decode().strip()) |
| |
| if process.stderr and process.returncode != 0: |
| _LOG.error(process.stderr.decode().strip()) |
| |
| return process.returncode |
| |
| |
| def main( |
| verbose: bool, |
| clang_tidy: str, |
| source_file: Path, |
| source_root: Path, |
| export_fixes: Optional[Path], |
| source_exclude: List[str], |
| skip_include_path: List[str], |
| extra_args: List[str], |
| ) -> int: |
| # Rebase the source file path on source_root. |
| # If source_file is not relative to source_root (which may be the case for |
| # generated files) stick with the original source_file. |
| try: |
| relative_source_file = source_file.relative_to(source_root) |
| except ValueError: |
| relative_source_file = source_file |
| |
| for pattern in source_exclude: |
| if re.match(pattern, str(relative_source_file)): |
| return 0 |
| |
| source_file_path = source_file.resolve() |
| export_fixes_path = (export_fixes.resolve() |
| if export_fixes is not None else None) |
| return run_clang_tidy(clang_tidy, verbose, source_file_path, |
| export_fixes_path, skip_include_path, extra_args) |
| |
| |
| if __name__ == '__main__': |
| sys.exit(main(**vars(_parse_args()))) |