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