blob: 3b7387e2741df25389e21e9e50f8ba683e4699ee [file] [log] [blame]
#!/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-filter` to select which sources to run the
clang-tidy on. Similar to `--header-filter` implemented by
clang-tidy.
- inputs the full compile command, with the cc binary name
- TODO: infer platform options from the full compile command
"""
import argparse
import logging
import pathlib
import re
import subprocess
import sys
from pathlib import Path
from typing import List, Optional
_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,
help='Source file to analyze with clang-tidy.')
parser.add_argument('--export-fixes',
required=False,
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-filter',
default='.*',
help='Regular expression matching the names of' +
' the source files to output diagnostics from')
# 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()
assert parsed_args.extra_args[0] == '--', 'arguments not correctly split'
parsed_args.extra_args = parsed_args.extra_args[1:]
return parsed_args
def run_clang_tidy(clang_tidy: str, verbose: bool, source_file: Path,
export_fixes: Optional[Path], extra_args: List[str]) -> int:
"""Executes clang_tidy via subprocess. Returns true if no failures."""
command = [clang_tidy, str(source_file)]
if not verbose:
command.append('--quiet')
if export_fixes is not None:
command.extend(['--export-fixes', str(export_fixes)])
# Append extra compilation flags. extra_args[0] is skipped as it contains
# the compiler binary name.
command.append('--')
command.extend(extra_args[1:])
process = subprocess.run(command,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
if process.stdout:
_LOG.warning(process.stdout.decode('utf-8').strip())
if process.stderr and process.returncode != 0:
_LOG.error(process.stderr.decode('utf-8').strip())
return process.returncode
def main(
verbose: bool,
clang_tidy: str,
source_file: str,
export_fixes: Optional[str],
source_filter: str,
extra_args: List[str],
) -> int:
if not re.search(source_filter, source_file):
return 0
source_file_path = pathlib.Path(source_file).resolve()
export_fixes_path = pathlib.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, extra_args)
if __name__ == '__main__':
sys.exit(main(**vars(_parse_args())))