xref: /openbmc/openbmc-build-scripts/tools/config-clang-tidy (revision 037f933db93a5d8400266296704bd2eac6d69d4d)
1b516729dSPatrick Williams#!/usr/bin/env python3
2b516729dSPatrick Williamsimport argparse
3b516729dSPatrick Williamsimport os
4b516729dSPatrick Williamsimport re
5b516729dSPatrick Williamsfrom typing import Any, Dict, List, Tuple
6b516729dSPatrick Williams
7b516729dSPatrick Williamsimport yaml
8b516729dSPatrick Williams
9b516729dSPatrick Williams
10b516729dSPatrick Williamsdef main() -> None:
11b516729dSPatrick Williams    parser = argparse.ArgumentParser()
12b516729dSPatrick Williams
13b516729dSPatrick Williams    parser.add_argument("--repo", help="Path to the repository", default=".")
14b516729dSPatrick Williams    parser.add_argument(
15b516729dSPatrick Williams        "--commit",
16b516729dSPatrick Williams        help="Commit the changes",
17b516729dSPatrick Williams        default=False,
18b516729dSPatrick Williams        action="store_true",
19b516729dSPatrick Williams    )
20b516729dSPatrick Williams
21b516729dSPatrick Williams    subparsers = parser.add_subparsers()
22b516729dSPatrick Williams    subparsers.required = True
23b516729dSPatrick Williams
24b516729dSPatrick Williams    parser_merge = subparsers.add_parser(
25b516729dSPatrick Williams        "merge", help="Merge a reference clang-tidy config"
26b516729dSPatrick Williams    )
27b516729dSPatrick Williams    parser_merge.add_argument(
28b516729dSPatrick Williams        "--reference", help="Path to reference clang-tidy", required=True
29b516729dSPatrick Williams    )
30b516729dSPatrick Williams    parser_merge.set_defaults(func=subcmd_merge)
31b516729dSPatrick Williams
32aeb73bc5SPatrick Williams    parser_format = subparsers.add_parser(
33aeb73bc5SPatrick Williams        "format", help="Format a clang-tidy config"
34aeb73bc5SPatrick Williams    )
35aeb73bc5SPatrick Williams    parser_format.set_defaults(func=subcmd_merge)
36aeb73bc5SPatrick Williams
37b516729dSPatrick Williams    parser_enable = subparsers.add_parser(
38b516729dSPatrick Williams        "enable", help="Enable a rule in a reference clang-tidy config"
39b516729dSPatrick Williams    )
40b516729dSPatrick Williams    parser_enable.add_argument("check", help="Check to enable")
41b516729dSPatrick Williams    parser_enable.set_defaults(func=subcmd_enable)
42b516729dSPatrick Williams
43b516729dSPatrick Williams    parser_disable = subparsers.add_parser(
44b516729dSPatrick Williams        "disable", help="Enable a rule in a reference clang-tidy config"
45b516729dSPatrick Williams    )
46b516729dSPatrick Williams    parser_disable.add_argument("check", help="Check to disable")
47b516729dSPatrick Williams    parser_disable.add_argument(
48b516729dSPatrick Williams        "--drop", help="Delete the check from the config", action="store_true"
49b516729dSPatrick Williams    )
50b516729dSPatrick Williams    parser_disable.set_defaults(func=subcmd_disable)
51b516729dSPatrick Williams
52b516729dSPatrick Williams    args = parser.parse_args()
53b516729dSPatrick Williams    args.func(args)
54b516729dSPatrick Williams
55b516729dSPatrick Williams
56b516729dSPatrick Williamsdef subcmd_merge(args: argparse.Namespace) -> None:
57b516729dSPatrick Williams    repo_path, repo_config = load_config(args.repo)
58aeb73bc5SPatrick Williams    _, ref_config = (
59aeb73bc5SPatrick Williams        load_config(args.reference) if "reference" in args else ("", {})
60aeb73bc5SPatrick Williams    )
61b516729dSPatrick Williams
62b516729dSPatrick Williams    result = {}
63b516729dSPatrick Williams
64b516729dSPatrick Williams    all_keys_set = set(repo_config.keys()) | set(ref_config.keys())
65b516729dSPatrick Williams    special_keys = ["Checks", "CheckOptions"]
66b516729dSPatrick Williams
67b516729dSPatrick Williams    # Create ordered_keys: special keys first (if present, in their defined order),
68b516729dSPatrick Williams    # followed by the rest of the keys sorted alphabetically.
69b516729dSPatrick Williams    ordered_keys = [k for k in special_keys if k in all_keys_set] + sorted(
70b516729dSPatrick Williams        list(all_keys_set - set(special_keys))
71b516729dSPatrick Williams    )
72b516729dSPatrick Williams
73b516729dSPatrick Williams    for key in ordered_keys:
74b516729dSPatrick Williams        repo_value = repo_config.get(key)
75b516729dSPatrick Williams        ref_value = ref_config.get(key)
76b516729dSPatrick Williams
77b516729dSPatrick Williams        key_class = globals().get(f"Key_{key}")
78b516729dSPatrick Williams        if key_class and hasattr(key_class, "merge"):
79b516729dSPatrick Williams            result[key] = key_class.merge(repo_value, ref_value)
80b516729dSPatrick Williams        elif repo_value:
81b516729dSPatrick Williams            result[key] = repo_value
82b516729dSPatrick Williams        else:
83b516729dSPatrick Williams            result[key] = ref_value
84b516729dSPatrick Williams
85b516729dSPatrick Williams    with open(repo_path, "w") as f:
86b516729dSPatrick Williams        f.write(format_yaml_output(result))
87b516729dSPatrick Williams
88b516729dSPatrick Williams
89b516729dSPatrick Williamsdef subcmd_enable(args: argparse.Namespace) -> None:
90b516729dSPatrick Williams    repo_path, repo_config = load_config(args.repo)
91b516729dSPatrick Williams
92b516729dSPatrick Williams    if "Checks" in repo_config:
93b516729dSPatrick Williams        repo_config["Checks"] = Key_Checks.enable(
94b516729dSPatrick Williams            repo_config["Checks"], args.check
95b516729dSPatrick Williams        )
96b516729dSPatrick Williams
97b516729dSPatrick Williams    with open(repo_path, "w") as f:
98b516729dSPatrick Williams        f.write(format_yaml_output(repo_config))
99b516729dSPatrick Williams
100b516729dSPatrick Williams    pass
101b516729dSPatrick Williams
102b516729dSPatrick Williams
103b516729dSPatrick Williamsdef subcmd_disable(args: argparse.Namespace) -> None:
104b516729dSPatrick Williams    repo_path, repo_config = load_config(args.repo)
105b516729dSPatrick Williams
106b516729dSPatrick Williams    if "Checks" in repo_config:
107b516729dSPatrick Williams        repo_config["Checks"] = Key_Checks.disable(
108b516729dSPatrick Williams            repo_config["Checks"], args.check, args.drop
109b516729dSPatrick Williams        )
110b516729dSPatrick Williams
111*037f933dSPatrick Williams    if "CheckOptions" in repo_config:
112*037f933dSPatrick Williams        repo_config["CheckOptions"] = Key_CheckOptions.disable(
113*037f933dSPatrick Williams            repo_config["CheckOptions"], args.check, args.drop
114*037f933dSPatrick Williams        )
115*037f933dSPatrick Williams
116b516729dSPatrick Williams    with open(repo_path, "w") as f:
117b516729dSPatrick Williams        f.write(format_yaml_output(repo_config))
118b516729dSPatrick Williams
119b516729dSPatrick Williams    pass
120b516729dSPatrick Williams
121b516729dSPatrick Williams
122b516729dSPatrick Williamsclass Key_Checks:
123b516729dSPatrick Williams    @staticmethod
124b516729dSPatrick Williams    def merge(repo: str, ref: str) -> str:
125b516729dSPatrick Williams        repo_checks = Key_Checks._split(repo)
126b516729dSPatrick Williams        ref_checks = Key_Checks._split(ref)
127b516729dSPatrick Williams
128b516729dSPatrick Williams        result: Dict[str, bool] = {}
129b516729dSPatrick Williams
130b516729dSPatrick Williams        for k, v in repo_checks.items():
131b516729dSPatrick Williams            result[k] = v
132b516729dSPatrick Williams        for k, v in ref_checks.items():
133b516729dSPatrick Williams            if k not in result:
134b516729dSPatrick Williams                result[k] = False
135b516729dSPatrick Williams
136b516729dSPatrick Williams        return Key_Checks._join(result)
137b516729dSPatrick Williams
138b516729dSPatrick Williams    @staticmethod
139b516729dSPatrick Williams    def enable(repo: str, check: str) -> str:
140b516729dSPatrick Williams        repo_checks = Key_Checks._split(repo)
141b516729dSPatrick Williams        repo_checks[check] = True
142b516729dSPatrick Williams        return Key_Checks._join(repo_checks)
143b516729dSPatrick Williams
144b516729dSPatrick Williams    @staticmethod
145b516729dSPatrick Williams    def disable(repo: str, check: str, drop: bool) -> str:
146b516729dSPatrick Williams        repo_checks = Key_Checks._split(repo)
147b516729dSPatrick Williams        if drop:
148b516729dSPatrick Williams            repo_checks.pop(check, None)
149b516729dSPatrick Williams        else:
150b516729dSPatrick Williams            repo_checks[check] = False
151b516729dSPatrick Williams        return Key_Checks._join(repo_checks)
152b516729dSPatrick Williams
153b516729dSPatrick Williams    @staticmethod
154b516729dSPatrick Williams    def _split(s: str) -> Dict[str, bool]:
155b516729dSPatrick Williams        result: Dict[str, bool] = {}
156b516729dSPatrick Williams        if not s:
157b516729dSPatrick Williams            return result
158b516729dSPatrick Williams        for item in s.split():
159b516729dSPatrick Williams            item = item.replace(",", "")
160b8ce3818SPatrick Williams            # Ignore global wildcard because we handle that specifically.
161b8ce3818SPatrick Williams            if item.startswith("-*"):
162b8ce3818SPatrick Williams                continue
163b8ce3818SPatrick Williams            # Drop category wildcard disables since we already use a global wildcard.
164b8ce3818SPatrick Williams            if item.startswith("-") and "*" in item:
165b516729dSPatrick Williams                continue
166b516729dSPatrick Williams            if item.startswith("-"):
167b516729dSPatrick Williams                result[item[1:]] = False
168b516729dSPatrick Williams            else:
169b516729dSPatrick Williams                result[item] = True
170b516729dSPatrick Williams        return result
171b516729dSPatrick Williams
172b516729dSPatrick Williams    @staticmethod
173b516729dSPatrick Williams    def _join(data: Dict[str, bool]) -> str:
174b516729dSPatrick Williams        return (
175b516729dSPatrick Williams            ",\n".join(
176b516729dSPatrick Williams                ["-*"] + [k if v else f"-{k}" for k, v in sorted(data.items())]
177b516729dSPatrick Williams            )
178b516729dSPatrick Williams            + "\n"
179b516729dSPatrick Williams        )
180b516729dSPatrick Williams
181b516729dSPatrick Williams
182b516729dSPatrick Williamsclass Key_CheckOptions:
183b516729dSPatrick Williams    @staticmethod
184b516729dSPatrick Williams    def merge(
185b516729dSPatrick Williams        repo: List[Dict[str, str]], ref: List[Dict[str, str]]
186b516729dSPatrick Williams    ) -> List[Dict[str, str]]:
187*037f933dSPatrick Williams        unrolled_repo = Key_CheckOptions._unroll(repo)
188b516729dSPatrick Williams        for item in ref or []:
189b516729dSPatrick Williams            if item["key"] in unrolled_repo:
190b516729dSPatrick Williams                continue
191b516729dSPatrick Williams            unrolled_repo[item["key"]] = item["value"]
192b516729dSPatrick Williams
193*037f933dSPatrick Williams        return Key_CheckOptions._roll(unrolled_repo)
194*037f933dSPatrick Williams
195*037f933dSPatrick Williams    @staticmethod
196*037f933dSPatrick Williams    def disable(
197*037f933dSPatrick Williams        repo: List[Dict[str, str]], option: str, drop: bool
198*037f933dSPatrick Williams    ) -> List[Dict[str, str]]:
199*037f933dSPatrick Williams        if not drop:
200*037f933dSPatrick Williams            return repo
201*037f933dSPatrick Williams
202*037f933dSPatrick Williams        unrolled_repo = Key_CheckOptions._unroll(repo)
203*037f933dSPatrick Williams
204*037f933dSPatrick Williams        if option in unrolled_repo:
205*037f933dSPatrick Williams            unrolled_repo.pop(option, None)
206*037f933dSPatrick Williams
207*037f933dSPatrick Williams        return Key_CheckOptions._roll(unrolled_repo)
208*037f933dSPatrick Williams
209*037f933dSPatrick Williams    @staticmethod
210*037f933dSPatrick Williams    def _unroll(repo: List[Dict[str, str]]) -> Dict[str, str]:
211*037f933dSPatrick Williams        unrolled_repo: Dict[str, str] = {}
212*037f933dSPatrick Williams        for item in repo or []:
213*037f933dSPatrick Williams            unrolled_repo[item["key"]] = item["value"]
214*037f933dSPatrick Williams        return unrolled_repo
215*037f933dSPatrick Williams
216*037f933dSPatrick Williams    @staticmethod
217*037f933dSPatrick Williams    def _roll(data: Dict[str, str]) -> List[Dict[str, str]]:
218*037f933dSPatrick Williams        return [{"key": k, "value": v} for k, v in sorted(data.items())]
219b516729dSPatrick Williams
220b516729dSPatrick Williams
221b516729dSPatrick Williamsdef load_config(path: str) -> Tuple[str, Dict[str, Any]]:
222b516729dSPatrick Williams    if "clang-tidy" not in path:
223b516729dSPatrick Williams        path = os.path.join(path, ".clang-tidy")
224b516729dSPatrick Williams
225b516729dSPatrick Williams    if not os.path.exists(path):
226b516729dSPatrick Williams        return (path, {})
227b516729dSPatrick Williams
228b516729dSPatrick Williams    with open(path, "r") as f:
22923362867SPatrick Williams        data = "\n".join([x for x in f.readlines() if not x.startswith("#")])
23023362867SPatrick Williams        return (path, yaml.safe_load(data))
231b516729dSPatrick Williams
232b516729dSPatrick Williams
233b516729dSPatrick Williamsdef format_yaml_output(data: Dict[str, Any]) -> str:
234b516729dSPatrick Williams    """Convert to a prettier YAML string:
235b516729dSPatrick Williams    - filter out excess empty lines
236b516729dSPatrick Williams    - insert new lines between keys
237b516729dSPatrick Williams    """
238b516729dSPatrick Williams    yaml_string = yaml.dump(data, sort_keys=False, indent=4)
239b516729dSPatrick Williams    lines: List[str] = []
240b516729dSPatrick Williams    for line in yaml_string.split("\n"):
241b516729dSPatrick Williams        # Strip excess new lines.
242b516729dSPatrick Williams        if not line:
243b516729dSPatrick Williams            continue
244b516729dSPatrick Williams        # Add new line between keys.
245b516729dSPatrick Williams        if len(lines) and re.match("[a-zA-Z0-9]+:", line):
246b516729dSPatrick Williams            lines.append("")
247b516729dSPatrick Williams        lines.append(line)
248b516729dSPatrick Williams    lines.append("")
249b516729dSPatrick Williams
250b516729dSPatrick Williams    return "\n".join(lines)
251b516729dSPatrick Williams
252b516729dSPatrick Williams
253b516729dSPatrick Williamsif __name__ == "__main__":
254b516729dSPatrick Williams    main()
255