xref: /aosp_15_r20/external/pigweed/pw_toolchain/py/pw_toolchain/clang_apply_replacements.py (revision 61c4878ac05f98d0ceed94b57d316916de578985)
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