xref: /openbmc/openbmc-build-scripts/tools/config-clang-tidy (revision b8ce3818c2c73196857daf9c0e6e3ee18938f6cc)
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
32b516729dSPatrick Williams    parser_enable = subparsers.add_parser(
33b516729dSPatrick Williams        "enable", help="Enable a rule in a reference clang-tidy config"
34b516729dSPatrick Williams    )
35b516729dSPatrick Williams    parser_enable.add_argument("check", help="Check to enable")
36b516729dSPatrick Williams    parser_enable.set_defaults(func=subcmd_enable)
37b516729dSPatrick Williams
38b516729dSPatrick Williams    parser_disable = subparsers.add_parser(
39b516729dSPatrick Williams        "disable", help="Enable a rule in a reference clang-tidy config"
40b516729dSPatrick Williams    )
41b516729dSPatrick Williams    parser_disable.add_argument("check", help="Check to disable")
42b516729dSPatrick Williams    parser_disable.add_argument(
43b516729dSPatrick Williams        "--drop", help="Delete the check from the config", action="store_true"
44b516729dSPatrick Williams    )
45b516729dSPatrick Williams    parser_disable.set_defaults(func=subcmd_disable)
46b516729dSPatrick Williams
47b516729dSPatrick Williams    args = parser.parse_args()
48b516729dSPatrick Williams    args.func(args)
49b516729dSPatrick Williams
50b516729dSPatrick Williams
51b516729dSPatrick Williamsdef subcmd_merge(args: argparse.Namespace) -> None:
52b516729dSPatrick Williams    repo_path, repo_config = load_config(args.repo)
53b516729dSPatrick Williams    ref_path, ref_config = load_config(args.reference)
54b516729dSPatrick Williams
55b516729dSPatrick Williams    result = {}
56b516729dSPatrick Williams
57b516729dSPatrick Williams    all_keys_set = set(repo_config.keys()) | set(ref_config.keys())
58b516729dSPatrick Williams    special_keys = ["Checks", "CheckOptions"]
59b516729dSPatrick Williams
60b516729dSPatrick Williams    # Create ordered_keys: special keys first (if present, in their defined order),
61b516729dSPatrick Williams    # followed by the rest of the keys sorted alphabetically.
62b516729dSPatrick Williams    ordered_keys = [k for k in special_keys if k in all_keys_set] + sorted(
63b516729dSPatrick Williams        list(all_keys_set - set(special_keys))
64b516729dSPatrick Williams    )
65b516729dSPatrick Williams
66b516729dSPatrick Williams    for key in ordered_keys:
67b516729dSPatrick Williams        repo_value = repo_config.get(key)
68b516729dSPatrick Williams        ref_value = ref_config.get(key)
69b516729dSPatrick Williams
70b516729dSPatrick Williams        key_class = globals().get(f"Key_{key}")
71b516729dSPatrick Williams        if key_class and hasattr(key_class, "merge"):
72b516729dSPatrick Williams            result[key] = key_class.merge(repo_value, ref_value)
73b516729dSPatrick Williams        elif repo_value:
74b516729dSPatrick Williams            result[key] = repo_value
75b516729dSPatrick Williams        else:
76b516729dSPatrick Williams            result[key] = ref_value
77b516729dSPatrick Williams
78b516729dSPatrick Williams    with open(repo_path, "w") as f:
79b516729dSPatrick Williams        f.write(format_yaml_output(result))
80b516729dSPatrick Williams
81b516729dSPatrick Williams
82b516729dSPatrick Williamsdef subcmd_enable(args: argparse.Namespace) -> None:
83b516729dSPatrick Williams    repo_path, repo_config = load_config(args.repo)
84b516729dSPatrick Williams
85b516729dSPatrick Williams    if "Checks" in repo_config:
86b516729dSPatrick Williams        repo_config["Checks"] = Key_Checks.enable(
87b516729dSPatrick Williams            repo_config["Checks"], args.check
88b516729dSPatrick Williams        )
89b516729dSPatrick Williams
90b516729dSPatrick Williams    with open(repo_path, "w") as f:
91b516729dSPatrick Williams        f.write(format_yaml_output(repo_config))
92b516729dSPatrick Williams
93b516729dSPatrick Williams    pass
94b516729dSPatrick Williams
95b516729dSPatrick Williams
96b516729dSPatrick Williamsdef subcmd_disable(args: argparse.Namespace) -> None:
97b516729dSPatrick Williams    repo_path, repo_config = load_config(args.repo)
98b516729dSPatrick Williams
99b516729dSPatrick Williams    if "Checks" in repo_config:
100b516729dSPatrick Williams        repo_config["Checks"] = Key_Checks.disable(
101b516729dSPatrick Williams            repo_config["Checks"], args.check, args.drop
102b516729dSPatrick Williams        )
103b516729dSPatrick Williams
104b516729dSPatrick Williams    with open(repo_path, "w") as f:
105b516729dSPatrick Williams        f.write(format_yaml_output(repo_config))
106b516729dSPatrick Williams
107b516729dSPatrick Williams    pass
108b516729dSPatrick Williams
109b516729dSPatrick Williams
110b516729dSPatrick Williamsclass Key_Checks:
111b516729dSPatrick Williams    @staticmethod
112b516729dSPatrick Williams    def merge(repo: str, ref: str) -> str:
113b516729dSPatrick Williams        repo_checks = Key_Checks._split(repo)
114b516729dSPatrick Williams        ref_checks = Key_Checks._split(ref)
115b516729dSPatrick Williams
116b516729dSPatrick Williams        result: Dict[str, bool] = {}
117b516729dSPatrick Williams
118b516729dSPatrick Williams        for k, v in repo_checks.items():
119b516729dSPatrick Williams            result[k] = v
120b516729dSPatrick Williams        for k, v in ref_checks.items():
121b516729dSPatrick Williams            if k not in result:
122b516729dSPatrick Williams                result[k] = False
123b516729dSPatrick Williams
124b516729dSPatrick Williams        return Key_Checks._join(result)
125b516729dSPatrick Williams
126b516729dSPatrick Williams    @staticmethod
127b516729dSPatrick Williams    def enable(repo: str, check: str) -> str:
128b516729dSPatrick Williams        repo_checks = Key_Checks._split(repo)
129b516729dSPatrick Williams        repo_checks[check] = True
130b516729dSPatrick Williams        return Key_Checks._join(repo_checks)
131b516729dSPatrick Williams
132b516729dSPatrick Williams    @staticmethod
133b516729dSPatrick Williams    def disable(repo: str, check: str, drop: bool) -> str:
134b516729dSPatrick Williams        repo_checks = Key_Checks._split(repo)
135b516729dSPatrick Williams        if drop:
136b516729dSPatrick Williams            repo_checks.pop(check, None)
137b516729dSPatrick Williams        else:
138b516729dSPatrick Williams            repo_checks[check] = False
139b516729dSPatrick Williams        return Key_Checks._join(repo_checks)
140b516729dSPatrick Williams
141b516729dSPatrick Williams    @staticmethod
142b516729dSPatrick Williams    def _split(s: str) -> Dict[str, bool]:
143b516729dSPatrick Williams        result: Dict[str, bool] = {}
144b516729dSPatrick Williams        if not s:
145b516729dSPatrick Williams            return result
146b516729dSPatrick Williams        for item in s.split():
147b516729dSPatrick Williams            item = item.replace(",", "")
148*b8ce3818SPatrick Williams            # Ignore global wildcard because we handle that specifically.
149*b8ce3818SPatrick Williams            if item.startswith("-*"):
150*b8ce3818SPatrick Williams                continue
151*b8ce3818SPatrick Williams            # Drop category wildcard disables since we already use a global wildcard.
152*b8ce3818SPatrick Williams            if item.startswith("-") and "*" in item:
153b516729dSPatrick Williams                continue
154b516729dSPatrick Williams            if item.startswith("-"):
155b516729dSPatrick Williams                result[item[1:]] = False
156b516729dSPatrick Williams            else:
157b516729dSPatrick Williams                result[item] = True
158b516729dSPatrick Williams        return result
159b516729dSPatrick Williams
160b516729dSPatrick Williams    @staticmethod
161b516729dSPatrick Williams    def _join(data: Dict[str, bool]) -> str:
162b516729dSPatrick Williams        return (
163b516729dSPatrick Williams            ",\n".join(
164b516729dSPatrick Williams                ["-*"] + [k if v else f"-{k}" for k, v in sorted(data.items())]
165b516729dSPatrick Williams            )
166b516729dSPatrick Williams            + "\n"
167b516729dSPatrick Williams        )
168b516729dSPatrick Williams
169b516729dSPatrick Williams
170b516729dSPatrick Williamsclass Key_CheckOptions:
171b516729dSPatrick Williams    @staticmethod
172b516729dSPatrick Williams    def merge(
173b516729dSPatrick Williams        repo: List[Dict[str, str]], ref: List[Dict[str, str]]
174b516729dSPatrick Williams    ) -> List[Dict[str, str]]:
175b516729dSPatrick Williams        unrolled_repo: Dict[str, str] = {}
176b516729dSPatrick Williams        for item in repo or []:
177b516729dSPatrick Williams            unrolled_repo[item["key"]] = item["value"]
178b516729dSPatrick Williams        for item in ref or []:
179b516729dSPatrick Williams            if item["key"] in unrolled_repo:
180b516729dSPatrick Williams                continue
181b516729dSPatrick Williams            unrolled_repo[item["key"]] = item["value"]
182b516729dSPatrick Williams
183b516729dSPatrick Williams        return [
184b516729dSPatrick Williams            {"key": k, "value": v} for k, v in sorted(unrolled_repo.items())
185b516729dSPatrick Williams        ]
186b516729dSPatrick Williams
187b516729dSPatrick Williams
188b516729dSPatrick Williamsdef load_config(path: str) -> Tuple[str, Dict[str, Any]]:
189b516729dSPatrick Williams    if "clang-tidy" not in path:
190b516729dSPatrick Williams        path = os.path.join(path, ".clang-tidy")
191b516729dSPatrick Williams
192b516729dSPatrick Williams    if not os.path.exists(path):
193b516729dSPatrick Williams        return (path, {})
194b516729dSPatrick Williams
195b516729dSPatrick Williams    with open(path, "r") as f:
19623362867SPatrick Williams        data = "\n".join([x for x in f.readlines() if not x.startswith("#")])
19723362867SPatrick Williams        return (path, yaml.safe_load(data))
198b516729dSPatrick Williams
199b516729dSPatrick Williams
200b516729dSPatrick Williamsdef format_yaml_output(data: Dict[str, Any]) -> str:
201b516729dSPatrick Williams    """Convert to a prettier YAML string:
202b516729dSPatrick Williams    - filter out excess empty lines
203b516729dSPatrick Williams    - insert new lines between keys
204b516729dSPatrick Williams    """
205b516729dSPatrick Williams    yaml_string = yaml.dump(data, sort_keys=False, indent=4)
206b516729dSPatrick Williams    lines: List[str] = []
207b516729dSPatrick Williams    for line in yaml_string.split("\n"):
208b516729dSPatrick Williams        # Strip excess new lines.
209b516729dSPatrick Williams        if not line:
210b516729dSPatrick Williams            continue
211b516729dSPatrick Williams        # Add new line between keys.
212b516729dSPatrick Williams        if len(lines) and re.match("[a-zA-Z0-9]+:", line):
213b516729dSPatrick Williams            lines.append("")
214b516729dSPatrick Williams        lines.append(line)
215b516729dSPatrick Williams    lines.append("")
216b516729dSPatrick Williams
217b516729dSPatrick Williams    return "\n".join(lines)
218b516729dSPatrick Williams
219b516729dSPatrick Williams
220b516729dSPatrick Williamsif __name__ == "__main__":
221b516729dSPatrick Williams    main()
222