Commit | Line | Data |
---|---|---|
270aa17a SM |
1 | #!/usr/bin/env python3 |
2 | ||
3 | # Usage: check-include-guard.py [--fix] FILE | |
4 | # | |
5 | # Checks (and optionally tries to fix) the C/C++ header include guard | |
6 | # names and format of `FILE`. | |
7 | # | |
8 | # With `--fix`, this script fixes the include guard names in place. | |
9 | ||
10 | import re | |
11 | import sys | |
12 | import pathlib | |
13 | import argparse | |
14 | ||
15 | ||
16 | class _Oops(Exception): | |
17 | def __init__(self, msg: str): | |
18 | self._msg = msg | |
19 | ||
20 | @property | |
21 | def msg(self): | |
22 | return self._msg | |
23 | ||
24 | ||
25 | def _make_expected_ig_name(filename: pathlib.Path): | |
26 | # Normalize `filename` (e.g. remove `..`) and make it relative to | |
27 | # the root of the source tree. | |
28 | root = pathlib.Path(__file__).parent.parent.resolve(strict=True) | |
29 | filename = filename.absolute().resolve(strict=True).relative_to(root) | |
30 | ||
31 | expected_ig_name = re.sub(r"[/.-]", "_", str(filename)).upper() | |
32 | expected_ig_name = re.sub(r"^SRC_", "", expected_ig_name) | |
33 | return "BABELTRACE_" + expected_ig_name | |
34 | ||
35 | ||
36 | def _check_file(filename: pathlib.Path, fix: bool): | |
37 | with open(filename) as f: | |
38 | contents = f.read() | |
39 | ||
40 | write_file = False | |
41 | ||
42 | # Top part | |
43 | top_re = re.compile(r"^(/\*.+?\*/)\n\n#ifndef (\w+)\n#define (\w+)\n", re.DOTALL) | |
44 | top_m = top_re.match(contents) | |
45 | ||
46 | if not top_m: | |
47 | raise _Oops( | |
48 | "Top of the file doesn't have the expected form: block comment, empty line, and then two include guard lines" | |
49 | ) | |
50 | ||
51 | expected_ig_name = _make_expected_ig_name(filename) | |
52 | ||
53 | if fix: | |
54 | contents = top_re.sub( | |
55 | rf"\1\n\n#ifndef {expected_ig_name}\n#define {expected_ig_name}\n", | |
56 | contents, | |
57 | ) | |
58 | write_file = True | |
59 | else: | |
60 | if top_m.group(2) != expected_ig_name: | |
61 | raise _Oops( | |
62 | f"In `#ifndef {top_m.group(2)}` include guard line: expecting `#ifndef {expected_ig_name}`" | |
63 | ) | |
64 | ||
65 | if top_m.group(3) != expected_ig_name: | |
66 | raise _Oops( | |
67 | f"In `#define {top_m.group(3)}` include guard line: expecting `#define {expected_ig_name}`" | |
68 | ) | |
69 | ||
70 | # Bottom part | |
71 | bottom_re = re.compile(r"\n\n#endif(?: /\* (\w+) \*/)?\n$") | |
72 | bottom_m = bottom_re.search(contents) | |
73 | ||
74 | if not bottom_m: | |
75 | raise _Oops("Missing final `#endif` include guard line and trailing empty line") | |
76 | ||
77 | if fix: | |
78 | contents = bottom_re.sub(f"\n\n#endif /* {expected_ig_name} */\n", contents) | |
79 | write_file = True | |
80 | else: | |
81 | if bottom_m.group(1) != expected_ig_name: | |
82 | raise _Oops( | |
83 | f"In bottom `#endif` include guard line: expecting `#endif /* {expected_ig_name} */`" | |
84 | ) | |
85 | ||
86 | if write_file: | |
87 | with open(filename, "w") as f: | |
88 | f.write(contents) | |
89 | ||
90 | ||
91 | def _main(): | |
92 | argparser = argparse.ArgumentParser() | |
93 | argparser.add_argument( | |
94 | "-f", | |
95 | "--fix", | |
96 | action="store_true", | |
97 | help="attempt to fix the include guards of FILE", | |
98 | ) | |
99 | argparser.add_argument("FILE") | |
100 | args = argparser.parse_args() | |
101 | filename = pathlib.Path(args.FILE) | |
102 | ||
103 | try: | |
104 | _check_file(filename, args.fix) | |
105 | except _Oops as exc: | |
106 | print(f"{filename}: {exc}", file=sys.stderr) | |
107 | sys.exit(1) | |
108 | ||
109 | ||
110 | if __name__ == "__main__": | |
111 | _main() |