xref: /openbmc/openbmc-build-scripts/tools/config-clang-tidy (revision aeb73bc59870f220a5a45ebe4b2435a99cb6cb83)
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
32*aeb73bc5SPatrick Williams    parser_format = subparsers.add_parser(
33*aeb73bc5SPatrick Williams        "format", help="Format a clang-tidy config"
34*aeb73bc5SPatrick Williams    )
35*aeb73bc5SPatrick Williams    parser_format.set_defaults(func=subcmd_merge)
36*aeb73bc5SPatrick 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)
58*aeb73bc5SPatrick Williams    _, ref_config = (
59*aeb73bc5SPatrick Williams        load_config(args.reference) if "reference" in args else ("", {})
60*aeb73bc5SPatrick 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
111b516729dSPatrick Williams    with open(repo_path, "w") as f:
112b516729dSPatrick Williams        f.write(format_yaml_output(repo_config))
113b516729dSPatrick Williams
114b516729dSPatrick Williams    pass
115b516729dSPatrick Williams
116b516729dSPatrick Williams
117b516729dSPatrick Williamsclass Key_Checks:
118b516729dSPatrick Williams    @staticmethod
119b516729dSPatrick Williams    def merge(repo: str, ref: str) -> str:
120b516729dSPatrick Williams        repo_checks = Key_Checks._split(repo)
121b516729dSPatrick Williams        ref_checks = Key_Checks._split(ref)
122b516729dSPatrick Williams
123b516729dSPatrick Williams        result: Dict[str, bool] = {}
124b516729dSPatrick Williams
125b516729dSPatrick Williams        for k, v in repo_checks.items():
126b516729dSPatrick Williams            result[k] = v
127b516729dSPatrick Williams        for k, v in ref_checks.items():
128b516729dSPatrick Williams            if k not in result:
129b516729dSPatrick Williams                result[k] = False
130b516729dSPatrick Williams
131b516729dSPatrick Williams        return Key_Checks._join(result)
132b516729dSPatrick Williams
133b516729dSPatrick Williams    @staticmethod
134b516729dSPatrick Williams    def enable(repo: str, check: str) -> str:
135b516729dSPatrick Williams        repo_checks = Key_Checks._split(repo)
136b516729dSPatrick Williams        repo_checks[check] = True
137b516729dSPatrick Williams        return Key_Checks._join(repo_checks)
138b516729dSPatrick Williams
139b516729dSPatrick Williams    @staticmethod
140b516729dSPatrick Williams    def disable(repo: str, check: str, drop: bool) -> str:
141b516729dSPatrick Williams        repo_checks = Key_Checks._split(repo)
142b516729dSPatrick Williams        if drop:
143b516729dSPatrick Williams            repo_checks.pop(check, None)
144b516729dSPatrick Williams        else:
145b516729dSPatrick Williams            repo_checks[check] = False
146b516729dSPatrick Williams        return Key_Checks._join(repo_checks)
147b516729dSPatrick Williams
148b516729dSPatrick Williams    @staticmethod
149b516729dSPatrick Williams    def _split(s: str) -> Dict[str, bool]:
150b516729dSPatrick Williams        result: Dict[str, bool] = {}
151b516729dSPatrick Williams        if not s:
152b516729dSPatrick Williams            return result
153b516729dSPatrick Williams        for item in s.split():
154b516729dSPatrick Williams            item = item.replace(",", "")
155b8ce3818SPatrick Williams            # Ignore global wildcard because we handle that specifically.
156b8ce3818SPatrick Williams            if item.startswith("-*"):
157b8ce3818SPatrick Williams                continue
158b8ce3818SPatrick Williams            # Drop category wildcard disables since we already use a global wildcard.
159b8ce3818SPatrick Williams            if item.startswith("-") and "*" in item:
160b516729dSPatrick Williams                continue
161b516729dSPatrick Williams            if item.startswith("-"):
162b516729dSPatrick Williams                result[item[1:]] = False
163b516729dSPatrick Williams            else:
164b516729dSPatrick Williams                result[item] = True
165b516729dSPatrick Williams        return result
166b516729dSPatrick Williams
167b516729dSPatrick Williams    @staticmethod
168b516729dSPatrick Williams    def _join(data: Dict[str, bool]) -> str:
169b516729dSPatrick Williams        return (
170b516729dSPatrick Williams            ",\n".join(
171b516729dSPatrick Williams                ["-*"] + [k if v else f"-{k}" for k, v in sorted(data.items())]
172b516729dSPatrick Williams            )
173b516729dSPatrick Williams            + "\n"
174b516729dSPatrick Williams        )
175b516729dSPatrick Williams
176b516729dSPatrick Williams
177b516729dSPatrick Williamsclass Key_CheckOptions:
178b516729dSPatrick Williams    @staticmethod
179b516729dSPatrick Williams    def merge(
180b516729dSPatrick Williams        repo: List[Dict[str, str]], ref: List[Dict[str, str]]
181b516729dSPatrick Williams    ) -> List[Dict[str, str]]:
182b516729dSPatrick Williams        unrolled_repo: Dict[str, str] = {}
183b516729dSPatrick Williams        for item in repo or []:
184b516729dSPatrick Williams            unrolled_repo[item["key"]] = item["value"]
185b516729dSPatrick Williams        for item in ref or []:
186b516729dSPatrick Williams            if item["key"] in unrolled_repo:
187b516729dSPatrick Williams                continue
188b516729dSPatrick Williams            unrolled_repo[item["key"]] = item["value"]
189b516729dSPatrick Williams
190b516729dSPatrick Williams        return [
191b516729dSPatrick Williams            {"key": k, "value": v} for k, v in sorted(unrolled_repo.items())
192b516729dSPatrick Williams        ]
193b516729dSPatrick Williams
194b516729dSPatrick Williams
195b516729dSPatrick Williamsdef load_config(path: str) -> Tuple[str, Dict[str, Any]]:
196b516729dSPatrick Williams    if "clang-tidy" not in path:
197b516729dSPatrick Williams        path = os.path.join(path, ".clang-tidy")
198b516729dSPatrick Williams
199b516729dSPatrick Williams    if not os.path.exists(path):
200b516729dSPatrick Williams        return (path, {})
201b516729dSPatrick Williams
202b516729dSPatrick Williams    with open(path, "r") as f:
20323362867SPatrick Williams        data = "\n".join([x for x in f.readlines() if not x.startswith("#")])
20423362867SPatrick Williams        return (path, yaml.safe_load(data))
205b516729dSPatrick Williams
206b516729dSPatrick Williams
207b516729dSPatrick Williamsdef format_yaml_output(data: Dict[str, Any]) -> str:
208b516729dSPatrick Williams    """Convert to a prettier YAML string:
209b516729dSPatrick Williams    - filter out excess empty lines
210b516729dSPatrick Williams    - insert new lines between keys
211b516729dSPatrick Williams    """
212b516729dSPatrick Williams    yaml_string = yaml.dump(data, sort_keys=False, indent=4)
213b516729dSPatrick Williams    lines: List[str] = []
214b516729dSPatrick Williams    for line in yaml_string.split("\n"):
215b516729dSPatrick Williams        # Strip excess new lines.
216b516729dSPatrick Williams        if not line:
217b516729dSPatrick Williams            continue
218b516729dSPatrick Williams        # Add new line between keys.
219b516729dSPatrick Williams        if len(lines) and re.match("[a-zA-Z0-9]+:", line):
220b516729dSPatrick Williams            lines.append("")
221b516729dSPatrick Williams        lines.append(line)
222b516729dSPatrick Williams    lines.append("")
223b516729dSPatrick Williams
224b516729dSPatrick Williams    return "\n".join(lines)
225b516729dSPatrick Williams
226b516729dSPatrick Williams
227b516729dSPatrick Williamsif __name__ == "__main__":
228b516729dSPatrick Williams    main()
229