1#!/usr/bin/env python3 2 3# Copyright 2024 The Pigweed Authors 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); you may not 6# use this file except in compliance with the License. You may obtain a copy of 7# the License at 8# 9# https://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14# License for the specific language governing permissions and limitations under 15# the License. 16"""Applies clang-tidy suggested fixes. 17 18This uses the clang-apply-replacements tool to apply code changes described in 19a YAML file generated by clang-tidy. 20""" 21 22import argparse 23import logging 24import re 25from pathlib import Path 26from pw_cli.tool_runner import BasicSubprocessRunner 27 28_LOG: logging.Logger = logging.getLogger(__name__) 29_REMOVE_CHANGE_FILES_FLAG = "--remove-change-desc-files" 30_IGNORE_INSERT_CONFLICT_FLAG = "--ignore-insert-conflict" 31_CLANG_TIDY_SUGGESTED_FIX_FILE_REGEX = "*.o.yaml" 32_PIGWEED_INCLUDE_FIX_REGEX = re.compile(r'ReplacementText: "#include <(pw_.*)>') 33_PIGWEED_INCLUDE_REPLACEMENT_REGEX = r'ReplacementText: "#include \"\1\"' 34 35 36def apply_replacements( 37 root: Path, remove_change_desc_files: bool, raise_insert_conflict: bool 38) -> int: 39 """Runs the clang-apply-replacements tool to apply clang-tidy fixes. 40 41 Args: 42 root: The directory under which clang-apply-replacements will search 43 for YAML fix files generated by clang-tidy. 44 remove_change_desc_files: Whether or not to remove the change 45 description files regardless of successful. 46 raise_insert_conflict: Whether or not to ignore insert conflicts. 47 48 Returns: 49 The return code of the clang-apply-replacements tool. 50 """ 51 # Search under root for YAML files generated by clang-tidy that contain 52 # suggested fixes 53 for filepath in root.rglob(_CLANG_TIDY_SUGGESTED_FIX_FILE_REGEX): 54 clang_tidy_fixes = filepath.read_text() 55 56 # Change suggested pigweed includes from <pw_.*> to "pw_.*" 57 if clang_tidy_fixes: 58 new_clang_tidy_fixes = _PIGWEED_INCLUDE_FIX_REGEX.sub( 59 _PIGWEED_INCLUDE_REPLACEMENT_REGEX, clang_tidy_fixes 60 ) 61 filepath.write_text(new_clang_tidy_fixes) 62 63 # Add flags for the clang-apply-replacements tool 64 flags = [] 65 if remove_change_desc_files: 66 flags.append(_REMOVE_CHANGE_FILES_FLAG) 67 if raise_insert_conflict: 68 flags.append(_IGNORE_INSERT_CONFLICT_FLAG) 69 70 # Use the clang-apply-replacements tool via a subprocess 71 _LOG.info('Applying clang-tidy fixes') 72 run_tool = BasicSubprocessRunner() 73 process_result = run_tool( 74 'clang-apply-replacements', 75 [str(root)] + flags, 76 check=True, 77 ) 78 return process_result.returncode 79 80 81def arguments() -> argparse.ArgumentParser: 82 """Creates an argument parser for clang-apply-replacements tool.""" 83 84 parser = argparse.ArgumentParser(description=__doc__) 85 86 def existing_path(arg: str) -> Path: 87 path = Path(arg) 88 if not path.is_dir(): 89 raise argparse.ArgumentTypeError( 90 f'{arg} is not a path to a directory' 91 ) 92 93 return path 94 95 parser.add_argument( 96 'root', 97 type=existing_path, 98 help=( 99 'Root directory that clang-apply-replacements will recursively ' 100 'search under for the YAML fix files generated by clang-tidy.' 101 ), 102 ) 103 104 parser.add_argument( 105 '--remove-change-desc-files', 106 action=argparse.BooleanOptionalAction, 107 default=True, 108 help=( 109 'Remove the change description files regardless of successful ' 110 'merging/replacing.' 111 ), 112 ) 113 114 parser.add_argument( 115 '--raise-insert-conflict', 116 action=argparse.BooleanOptionalAction, 117 default=True, 118 help='Do not ignore insert conflicts.', 119 ) 120 121 return parser 122 123 124def main() -> int: 125 """Check and fix formatting for source files.""" 126 return apply_replacements(**vars(arguments().parse_args())) 127