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