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