Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
godotengine
GitHub Repository: godotengine/godot
Path: blob/master/misc/scripts/validate_includes.py
45997 views
1
#!/usr/bin/env python3
2
3
if __name__ != "__main__":
4
raise ImportError(f'Utility script "{__file__}" should not be used as a module!')
5
6
import os
7
from pathlib import Path
8
9
if Path(os.getcwd()).as_posix() != (ROOT := Path(__file__).parent.parent.parent).as_posix():
10
raise RuntimeError(f'Utility script "{__file__}" must be run from the repository root!')
11
12
import argparse
13
import re
14
from dataclasses import dataclass
15
16
BASE_FOLDER = Path(__file__).resolve().parent.parent.parent
17
RE_INCLUDES = re.compile(
18
r"^#(?P<keyword>include|import) " # Both `include` and `import` keywords valid.
19
r'(?P<sequence>[<"])' # Handle both styles of char wrappers. Does NOT handle macro expansion.
20
r'(?P<path>.+?)[>"]', # Resolve path of include itself. Can safely assume the sequence matches.
21
re.RegexFlag.MULTILINE,
22
)
23
24
25
@dataclass
26
class IncludeData:
27
path: str
28
is_angle: bool
29
is_import: bool
30
31
@staticmethod
32
def from_match(match: re.Match[str]):
33
items = match.groupdict()
34
return IncludeData(
35
items["path"],
36
items["sequence"] == "<",
37
items["keyword"] == "import",
38
)
39
40
def copy(self):
41
return IncludeData(self.path, self.is_angle, self.is_import)
42
43
def __str__(self):
44
return "#{keyword} {rbracket}{path}{lbracket}".format(
45
keyword="import" if self.is_import else "include",
46
rbracket="<" if self.is_angle else '"',
47
path=self.path,
48
lbracket=">" if self.is_angle else '"',
49
)
50
51
52
def validate_includes(path: Path) -> int:
53
ret = 0
54
content = path.read_text(encoding="utf-8")
55
56
for data in map(IncludeData.from_match, RE_INCLUDES.finditer(content)):
57
original_data = data.copy()
58
59
if "\\" in data.path:
60
data.path = data.path.replace("\\", "/")
61
62
if data.path.startswith("thirdparty/"):
63
data.is_angle = True
64
65
if (relative_path := path.parent / data.path).exists():
66
# Relative includes are only permitted under certain circumstances.
67
68
if relative_path.name.split(".")[0] == path.name.split(".")[0]:
69
# Identical leading names permitted
70
pass
71
72
elif ("modules" in relative_path.parts and "modules" in path.parts) or (
73
"platform" in relative_path.parts and "platform" in path.parts
74
):
75
# Modules and platforms can use relative includes if constrained to the module/platform itself.
76
pass
77
78
else:
79
data.path = relative_path.resolve().relative_to(BASE_FOLDER).as_posix()
80
81
if original_data != data:
82
content = content.replace(f"{original_data}", f"{data}")
83
ret += 1
84
85
if ret:
86
with open(path, "w", encoding="utf-8", newline="\n") as file:
87
file.write(content)
88
89
return ret
90
91
92
def main() -> int:
93
parser = argparse.ArgumentParser(description="Validate C/C++ includes, correcting if necessary")
94
parser.add_argument("files", nargs="+", help="A list of files to validate")
95
args = parser.parse_args()
96
97
ret = 0
98
99
for file in map(Path, args.files):
100
ret += validate_includes(file)
101
102
return ret
103
104
105
try:
106
raise SystemExit(main())
107
except KeyboardInterrupt:
108
import signal
109
110
signal.signal(signal.SIGINT, signal.SIG_DFL)
111
os.kill(os.getpid(), signal.SIGINT)
112
113