Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/Scripts/precommit.py
1 views
1
#!/usr/bin/env python3
2
3
import os
4
import sys
5
import subprocess
6
import argparse
7
from typing import Iterable
8
from pathlib import Path
9
from lint.util import EXTENSIONS_TO_CHECK
10
11
CLANG_FORMAT_EXTS = set([".m", ".mm", ".h"])
12
13
14
def sort_forward_decl_statement_block(text):
15
lines = text.split("\n")
16
lines = [line.strip() for line in lines if line.strip()]
17
lines = list(set(lines))
18
lines.sort()
19
return "\n" + "\n".join(lines) + "\n"
20
21
22
def find_matching_section(text, match_test):
23
lines = text.split("\n")
24
first_matching_line_index = None
25
for index, line in enumerate(lines):
26
if match_test(line):
27
first_matching_line_index = index
28
break
29
30
if first_matching_line_index is None:
31
return None
32
33
# Absorb any leading empty lines.
34
while first_matching_line_index > 0:
35
prev_line = lines[first_matching_line_index - 1]
36
if prev_line.strip() != "":
37
break
38
first_matching_line_index = first_matching_line_index - 1
39
40
first_non_matching_line_index = None
41
for index, line in enumerate(lines[first_matching_line_index:]):
42
if line.strip() == "":
43
# Absorb any trailing empty lines.
44
continue
45
if not match_test(line):
46
first_non_matching_line_index = index + first_matching_line_index
47
break
48
49
text0 = "\n".join(lines[:first_matching_line_index])
50
if first_non_matching_line_index is None:
51
text1 = "\n".join(lines[first_matching_line_index:])
52
text2 = None
53
else:
54
text1 = "\n".join(
55
lines[first_matching_line_index:first_non_matching_line_index]
56
)
57
text2 = "\n".join(lines[first_non_matching_line_index:])
58
59
return text0, text1, text2
60
61
62
def sort_matching_blocks(sort_name, filepath, text, match_func, sort_func):
63
unprocessed = text
64
processed = None
65
while True:
66
section = find_matching_section(unprocessed, match_func)
67
if not section:
68
if processed:
69
processed = "\n".join((processed, unprocessed))
70
else:
71
processed = unprocessed
72
break
73
74
text0, text1, text2 = section
75
76
if processed:
77
processed = "\n".join((processed, text0))
78
else:
79
processed = text0
80
81
text1 = sort_func(text1)
82
processed = "\n".join((processed, text1))
83
if text2:
84
unprocessed = text2
85
else:
86
break
87
88
if text != processed:
89
print(sort_name, filepath)
90
return processed
91
92
93
def find_forward_class_statement_section(text):
94
def is_forward_class_statement(line):
95
return line.strip().startswith("@class ")
96
97
return find_matching_section(text, is_forward_class_statement)
98
99
100
def find_forward_protocol_statement_section(text):
101
def is_forward_protocol_statement(line):
102
return line.strip().startswith("@protocol ") and line.strip().endswith(";")
103
104
return find_matching_section(text, is_forward_protocol_statement)
105
106
107
def sort_forward_class_statements(filepath, file_extension, text):
108
if file_extension not in (".h", ".m", ".mm"):
109
return text
110
return sort_matching_blocks(
111
"sort_class_statements",
112
filepath,
113
text,
114
find_forward_class_statement_section,
115
sort_forward_decl_statement_block,
116
)
117
118
119
def sort_forward_protocol_statements(filepath, file_extension, text):
120
if file_extension not in (".h", ".m", ".mm"):
121
return text
122
return sort_matching_blocks(
123
"sort_forward_protocol_statements",
124
filepath,
125
text,
126
find_forward_protocol_statement_section,
127
sort_forward_decl_statement_block,
128
)
129
130
131
def get_ext(file: str) -> str:
132
return os.path.splitext(file)[1]
133
134
135
def process(filepath):
136
file_ext = get_ext(filepath)
137
138
with open(filepath, "rt") as f:
139
text = f.read()
140
141
original_text = text
142
143
text = sort_forward_class_statements(filepath, file_ext, text)
144
text = sort_forward_protocol_statements(filepath, file_ext, text)
145
text = text.strip() + "\n"
146
147
if original_text == text:
148
return
149
150
with open(filepath, "wt") as f:
151
f.write(text)
152
153
154
def get_file_paths_for_commit(commit):
155
return (
156
subprocess.run(
157
["git", "diff", "--name-only", "--diff-filter=ACMR", commit],
158
check=True,
159
capture_output=True,
160
encoding="utf8",
161
)
162
.stdout.rstrip()
163
.split("\n")
164
)
165
166
167
def should_process_file(file_path: str) -> bool:
168
if get_ext(file_path) not in EXTENSIONS_TO_CHECK:
169
return False
170
171
for component in Path(file_path).parts:
172
if component.startswith("."):
173
return False
174
if component in ("Pods", "ThirdParty"):
175
return False
176
if component.startswith("MobileCoinExternal."):
177
return False
178
179
return True
180
181
182
def clang_format(file_paths):
183
file_paths = list(filter(lambda f: get_ext(f) in CLANG_FORMAT_EXTS, file_paths))
184
if len(file_paths) == 0:
185
return True
186
proc = subprocess.run(["clang-format", "-i", *file_paths])
187
return proc.returncode == 0
188
189
190
def swiftformat(file_paths):
191
file_paths = list(filter(lambda f: get_ext(f) == ".swift", file_paths))
192
if len(file_paths) == 0:
193
return True
194
195
proc = subprocess.run(["swiftformat", "--quiet", *file_paths])
196
return proc.returncode == 0
197
198
199
if __name__ == "__main__":
200
parser = argparse.ArgumentParser(description="lint & format files")
201
parser.add_argument("path", nargs="*", help="a path to process")
202
parser.add_argument(
203
"--ref",
204
metavar="commit-sha",
205
help="process paths that have changed since this commit",
206
)
207
parser.add_argument(
208
"--skip-xcode-sort",
209
action='store_true',
210
help="skip sorting the Xcode project",
211
)
212
ns = parser.parse_args()
213
214
if len(ns.path) > 0:
215
file_paths = ns.path
216
else:
217
file_paths = get_file_paths_for_commit(ns.ref or "HEAD")
218
file_paths = sorted(set(filter(should_process_file, file_paths)))
219
220
result = True
221
222
print("Checking license headers...", flush=True)
223
proc = subprocess.run(["Scripts/lint/lint-license-headers", *file_paths])
224
if proc.returncode != 0:
225
result = False
226
print("")
227
228
print("Sorting forward declarations...", flush=True)
229
for file_path in file_paths:
230
process(file_path)
231
print("")
232
233
if ns.skip_xcode_sort:
234
print("Skipping Xcode project sort!", flush=True)
235
else:
236
print("Sorting Xcode project...", flush=True)
237
proc = subprocess.run(["Scripts/sort-Xcode-project-file", "Signal.xcodeproj"])
238
if proc.returncode != 0:
239
result = False
240
print("")
241
242
print("Running swiftformat...", flush=True)
243
if not swiftformat(file_paths):
244
result = False
245
print("")
246
247
print("Running clang-format...", flush=True)
248
if not clang_format(file_paths):
249
result = False
250
print("")
251
252
if not result:
253
print("Some errors couldn't be fixed automatically.")
254
sys.exit(1)
255
256