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