xref: /openbmc/openbmc-build-scripts/tools/config-clang-tidy (revision 233628672f533bc26536ec1fc489eff2ba0f744d)
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(",", "")
148b516729dSPatrick Williams            if "-*" in item:
149b516729dSPatrick Williams                continue
150b516729dSPatrick Williams            if item.startswith("-"):
151b516729dSPatrick Williams                result[item[1:]] = False
152b516729dSPatrick Williams            else:
153b516729dSPatrick Williams                result[item] = True
154b516729dSPatrick Williams        return result
155b516729dSPatrick Williams
156b516729dSPatrick Williams    @staticmethod
157b516729dSPatrick Williams    def _join(data: Dict[str, bool]) -> str:
158b516729dSPatrick Williams        return (
159b516729dSPatrick Williams            ",\n".join(
160b516729dSPatrick Williams                ["-*"] + [k if v else f"-{k}" for k, v in sorted(data.items())]
161b516729dSPatrick Williams            )
162b516729dSPatrick Williams            + "\n"
163b516729dSPatrick Williams        )
164b516729dSPatrick Williams
165b516729dSPatrick Williams
166b516729dSPatrick Williamsclass Key_CheckOptions:
167b516729dSPatrick Williams    @staticmethod
168b516729dSPatrick Williams    def merge(
169b516729dSPatrick Williams        repo: List[Dict[str, str]], ref: List[Dict[str, str]]
170b516729dSPatrick Williams    ) -> List[Dict[str, str]]:
171b516729dSPatrick Williams        unrolled_repo: Dict[str, str] = {}
172b516729dSPatrick Williams        for item in repo or []:
173b516729dSPatrick Williams            unrolled_repo[item["key"]] = item["value"]
174b516729dSPatrick Williams        for item in ref or []:
175b516729dSPatrick Williams            if item["key"] in unrolled_repo:
176b516729dSPatrick Williams                continue
177b516729dSPatrick Williams            unrolled_repo[item["key"]] = item["value"]
178b516729dSPatrick Williams
179b516729dSPatrick Williams        return [
180b516729dSPatrick Williams            {"key": k, "value": v} for k, v in sorted(unrolled_repo.items())
181b516729dSPatrick Williams        ]
182b516729dSPatrick Williams
183b516729dSPatrick Williams
184b516729dSPatrick Williamsdef load_config(path: str) -> Tuple[str, Dict[str, Any]]:
185b516729dSPatrick Williams    if "clang-tidy" not in path:
186b516729dSPatrick Williams        path = os.path.join(path, ".clang-tidy")
187b516729dSPatrick Williams
188b516729dSPatrick Williams    if not os.path.exists(path):
189b516729dSPatrick Williams        return (path, {})
190b516729dSPatrick Williams
191b516729dSPatrick Williams    with open(path, "r") as f:
192*23362867SPatrick Williams        data = "\n".join([x for x in f.readlines() if not x.startswith("#")])
193*23362867SPatrick Williams        return (path, yaml.safe_load(data))
194b516729dSPatrick Williams
195b516729dSPatrick Williams
196b516729dSPatrick Williamsdef format_yaml_output(data: Dict[str, Any]) -> str:
197b516729dSPatrick Williams    """Convert to a prettier YAML string:
198b516729dSPatrick Williams    - filter out excess empty lines
199b516729dSPatrick Williams    - insert new lines between keys
200b516729dSPatrick Williams    """
201b516729dSPatrick Williams    yaml_string = yaml.dump(data, sort_keys=False, indent=4)
202b516729dSPatrick Williams    lines: List[str] = []
203b516729dSPatrick Williams    for line in yaml_string.split("\n"):
204b516729dSPatrick Williams        # Strip excess new lines.
205b516729dSPatrick Williams        if not line:
206b516729dSPatrick Williams            continue
207b516729dSPatrick Williams        # Add new line between keys.
208b516729dSPatrick Williams        if len(lines) and re.match("[a-zA-Z0-9]+:", line):
209b516729dSPatrick Williams            lines.append("")
210b516729dSPatrick Williams        lines.append(line)
211b516729dSPatrick Williams    lines.append("")
212b516729dSPatrick Williams
213b516729dSPatrick Williams    return "\n".join(lines)
214b516729dSPatrick Williams
215b516729dSPatrick Williams
216b516729dSPatrick Williamsif __name__ == "__main__":
217b516729dSPatrick Williams    main()
218