Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/Scripts/sds_codegen/sds_parse_objc.py
1 views
1
#!/usr/bin/env python3
2
3
import os
4
import subprocess
5
import argparse
6
import re
7
import json
8
import sds_common
9
from sds_common import fail
10
import tempfile
11
import shutil
12
13
git_repo_path = sds_common.git_repo_path
14
15
16
def ows_getoutput(cmd: list[str]) -> tuple[int, str, str]:
17
proc = subprocess.Popen(
18
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
19
)
20
stdout, stderr = proc.communicate()
21
22
return proc.returncode, stdout, stderr
23
24
25
class LineProcessor:
26
def __init__(self, text):
27
self.lines = text.split("\n")
28
29
def hasNext(self):
30
return len(self.lines) > 0
31
32
def next(self, should_pop=False):
33
if len(self.lines) == 0:
34
return None
35
line = self.lines[0]
36
if should_pop:
37
self.lines = self.lines[1:]
38
return line
39
40
def popNext(self):
41
return self.next(should_pop=True)
42
43
44
counter = 0
45
46
47
def next_counter():
48
global counter
49
counter = counter + 1
50
return counter
51
52
53
class ParsedClass:
54
def __init__(self, name):
55
self.name = name
56
self.is_implemented = False
57
self.property_map = {}
58
self.super_class_name = None
59
self.counter = next_counter()
60
self.finalize_method_name = None
61
self.namespace = None
62
self.protocol_names = []
63
64
def get_property(self, name):
65
result = self.property_map.get(name)
66
if result is None:
67
result = self.get_inherited_property(name)
68
return result
69
70
def add_property(self, property):
71
self.property_map[property.name] = property
72
73
def properties(self):
74
result = []
75
for name in sorted(self.property_map.keys()):
76
result.append(self.property_map[name])
77
return result
78
79
def property_names(self):
80
return sorted(self.property_map.keys())
81
82
def inherit_from_protocol(self, namespace, protocol_name):
83
self.namespace = namespace
84
self.protocol_names.append(protocol_name)
85
86
def get_inherited_property(self, name):
87
for protocol in self.class_protocols():
88
result = protocol.get_property(name)
89
if result is not None:
90
return result
91
return None
92
93
def all_properties(self):
94
result = self.properties()
95
# We need to include any properties synthesized by this class
96
# but declared in a protocol.
97
for protocol in self.class_protocols():
98
result.extend(protocol.all_properties())
99
return result
100
101
def class_protocols(self):
102
result = []
103
for protocol_name in self.protocol_names:
104
if protocol_name == self.name:
105
# There are classes that implement a protocol of the same name, e.g. MTLModel
106
# Ignore them.
107
continue
108
109
protocol = self.namespace.find_class(protocol_name)
110
if protocol is None:
111
if (
112
protocol_name.startswith("NS")
113
or protocol_name.startswith("AV")
114
or protocol_name.startswith("UI")
115
or protocol_name.startswith("MF")
116
or protocol_name.startswith("UN")
117
or protocol_name.startswith("CN")
118
):
119
# Ignore built in protocols.
120
continue
121
print("clazz:", self.name)
122
print("file_path:", file_path)
123
fail("Missing protocol:", protocol_name)
124
125
result.append(protocol)
126
127
return result
128
129
130
class ParsedProperty:
131
def __init__(self, name, objc_type, is_optional):
132
self.name = name
133
self.objc_type = objc_type
134
self.is_optional = is_optional
135
self.is_not_readonly = False
136
self.is_synthesized = False
137
138
139
class Namespace:
140
def __init__(self):
141
self.class_map = {}
142
143
def upsert_class(self, class_name):
144
clazz = self.class_map.get(class_name)
145
if clazz is None:
146
clazz = ParsedClass(class_name)
147
self.class_map[class_name] = clazz
148
return clazz
149
150
def find_class(self, class_name):
151
clazz = self.class_map.get(class_name)
152
return clazz
153
154
def class_names(self):
155
return sorted(self.class_map.keys())
156
157
158
split_objc_ast_prefix_regex = re.compile(r"^([ |\-`]*)(.+)$")
159
160
161
# The AST emitted by clang uses punctuation to indicate the AST hierarchy.
162
# This function strips that out.
163
def split_objc_ast_prefix(line):
164
match = split_objc_ast_prefix_regex.search(line)
165
if match is None:
166
fail("Could not match line:", line)
167
prefix = match.group(1)
168
remainder = match.group(2)
169
return prefix, remainder
170
171
172
def process_objc_ast(namespace: Namespace, file_path: str, raw_ast: str) -> None:
173
m_filename = os.path.basename(file_path)
174
file_base, file_extension = os.path.splitext(m_filename)
175
if file_extension != ".m":
176
fail("Bad file extension:", file_extension)
177
h_filename = file_base + ".h"
178
179
# TODO: Remove
180
lines = raw_ast.split("\n")
181
raw_ast = "\n".join(lines)
182
183
lines = LineProcessor(raw_ast)
184
while lines.hasNext():
185
line = lines.popNext()
186
prefix, remainder = split_objc_ast_prefix(line)
187
188
if remainder.startswith("ObjCInterfaceDecl "):
189
# |-ObjCInterfaceDecl 0x112510490 <SignalDataStoreCommon/ObjCMessage.h:14:1, line:25:2> line:14:12 ObjCMessage
190
process_objc_interface(namespace, file_path, lines, prefix, remainder)
191
elif remainder.startswith("ObjCCategoryDecl "):
192
# |-ObjCCategoryDecl 0x112510d58 <SignalDataStoreCommon/ObjCMessage.m:18:1, line:22:2> line:18:12
193
process_objc_category(namespace, file_path, lines, prefix, remainder)
194
elif remainder.startswith("ObjCImplementationDecl "):
195
# `-ObjCImplementationDecl 0x112510f20 <line:24:1, line:87:1> line:24:17 ObjCMessage
196
process_objc_implementation(namespace, file_path, lines, prefix, remainder)
197
elif remainder.startswith("ObjCProtocolDecl "):
198
# `-ObjCImplementationDecl 0x112510f20 <line:24:1, line:87:1> line:24:17 ObjCMessage
199
process_objc_protocol_decl(namespace, file_path, lines, prefix, remainder)
200
# TODO: Category impl.
201
elif remainder.startswith("TypedefDecl "):
202
# `-ObjCImplementationDecl 0x112510f20 <line:24:1, line:87:1> line:24:17 ObjCMessage
203
process_objc_type_declaration(
204
namespace, file_path, lines, prefix, remainder
205
)
206
elif remainder.startswith("EnumDecl "):
207
# `-ObjCImplementationDecl 0x112510f20 <line:24:1, line:87:1> line:24:17 ObjCMessage
208
process_objc_enum_declaration(
209
namespace, file_path, lines, prefix, remainder
210
)
211
212
213
# |-EnumDecl 0x7fd576047310 </Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS12.2.sdk/System/Library/Frameworks/CoreFoundation.framework/Headers/CFAvailability.h:127:43, /Users/matthew/code/workspace/ows/Signal-iOS-2/SignalServiceKit/src/Messages/TSCall.h:12:29> col:29 RPRecentCallType 'NSUInteger':'unsigned long'
214
# | `-EnumExtensibilityAttr 0x7fd5760473f0 </Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS12.2.sdk/System/Library/Frameworks/CoreFoundation.framework/Headers/CFAvailability.h:116:45, col:68> Open
215
# |-TypedefDecl 0x7fd576047488 </Users/matthew/code/workspace/ows/Signal-iOS-2/SignalServiceKit/src/Messages/TSCall.h:12:1, col:29> col:29 referenced RPRecentCallType 'enum RPRecentCallType':'enum RPRecentCallType'
216
# | `-ElaboratedType 0x7fd576047430 'enum RPRecentCallType' sugar
217
# | `-EnumType 0x7fd5760473d0 'enum RPRecentCallType'
218
# | `-Enum 0x7fd576047518 'RPRecentCallType'
219
# |-EnumDecl 0x7fd576047518 prev 0x7fd576047310 </Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS12.2.sdk/System/Library/Frameworks/CoreFoundation.framework/Headers/CFAvailability.h:127:90,
220
process_objc_enum_declaration_regex = re.compile(r"^.+? ([^ ]+) '([^']+)':'([^']+)'$")
221
# process_objc_enum_declaration_regex = re.compile(r"^.+?'([^']+)'(:'([^']+)')?$")
222
223
enum_type_map = {}
224
225
226
def process_objc_enum_declaration(namespace, file_path, lines, prefix, remainder):
227
match = process_objc_enum_declaration_regex.search(remainder)
228
if match is None:
229
print("file_path:", file_path)
230
print("Could not match line:", remainder)
231
return
232
type1 = get_match_group(match, 1)
233
type2 = get_match_group(match, 2)
234
type3 = get_match_group(match, 3)
235
236
if type1.startswith("line:"):
237
return
238
if type1 in enum_type_map:
239
return
240
enum_type_map[type1] = type2
241
242
243
# |-TypedefDecl 0x7f8d8fb44748 <line:12:1, line:22:3> col:3 referenced RPRecentCallType 'enum RPRecentCallType':'RPRecentCallType'
244
process_objc_type_declaration_regex = re.compile(r"^.+?'([^']+)'(:'([^']+)')?$")
245
246
247
def process_objc_type_declaration(namespace, file_path, lines, prefix, remainder):
248
match = process_objc_type_declaration_regex.search(remainder)
249
if match is None:
250
print("file_path:", file_path)
251
fail("Could not match line:", remainder)
252
type1 = get_match_group(match, 1)
253
type2 = get_match_group(match, 2)
254
type3 = get_match_group(match, 3)
255
256
if type1 is None or type3 is None:
257
return
258
is_enum = type1 == "enum " + type3
259
if not is_enum:
260
return
261
262
if type3.startswith("line:"):
263
print("Ignoring invalid enum(2):", type1, type2, type3)
264
return
265
if type3 not in enum_type_map:
266
print("Enum has unknown type:", type3)
267
enum_type = "NSUInteger"
268
else:
269
enum_type = enum_type_map[type3]
270
enum_type_map[type3] = enum_type
271
272
273
# |-ObjCInterfaceDecl 0x10f5d2b60 <SignalDataStoreCommon/ObjCBaseModel.h:15:1, col:8> col:8 SDSDataStore
274
# |-ObjCInterfaceDecl 0x10f5d2c10 <line:17:1, line:29:2> line:17:12 ObjCBaseModel
275
# | |-ObjCPropertyDecl 0x10f5d2d40 <line:19:1, col:43> col:43 uniqueId 'NSString * _Nonnull':'NSString *' readonly nonatomic
276
# ...
277
# |-ObjCInterfaceDecl 0x10f5d3490 <SignalDataStoreCommon/ObjCMessage.h:14:1, line:25:2> line:14:12 ObjCMessage
278
# | |-super ObjCInterface 0x10f5d2c10 'ObjCBaseModel'
279
# | |-ObjCImplementation 0x10f5d3f20 'ObjCMessage'
280
# | |-ObjCPropertyDecl 0x10f5d35d0 <line:16:1, col:43> col:43 body 'NSString * _Nonnull':'NSString *' readonly nonatomic strong
281
def process_objc_interface(
282
namespace: Namespace, file_path: str, lines, decl_prefix, decl_remainder
283
):
284
# |-ObjCInterfaceDecl 0x10ab2fd58 </Users/matthew/code/workspace/ows/Signal-iOS-2/SignalDataStore/SignalDataStoreCommon/ObjCMessageWAuthor.h:13:1, line:26:2> line:13:12 ObjCMessageWAuthor
285
# | |-super ObjCInterface 0x10ab2f490 'ObjCMessage'
286
287
super_class_name = None
288
if lines.hasNext():
289
line = lines.next()
290
prefix, remainder = split_objc_ast_prefix(line)
291
if len(prefix) > len(decl_prefix):
292
splits = remainder.split(" ")
293
if len(splits) >= 2 and splits[0] == "super":
294
super_class_name = splits[-1].strip()
295
if super_class_name.startswith("'") and super_class_name.endswith("'"):
296
super_class_name = super_class_name[1:-1]
297
298
process_objc_class(
299
namespace,
300
file_path,
301
lines,
302
decl_prefix,
303
decl_remainder,
304
super_class_name=super_class_name,
305
)
306
307
308
# |-ObjCCategoryDecl 0x10f5d3d58 <SignalDataStoreCommon/ObjCMessage.m:18:1, line:22:2> line:18:12
309
# | |-ObjCInterface 0x10f5d3490 'ObjCMessage'
310
# | |-ObjCPropertyDecl 0x10f5d3e20 <line:20:1, col:43> col:43 ignore 'NSString * _Nonnull':'NSString *' readonly nonatomic strong
311
# | `-ObjCMethodDecl 0x10f5d3e98 <col:43> col:43 implicit - ignore 'NSString * _Nonnull':'NSString *'
312
def process_objc_category(namespace, file_path, lines, decl_prefix, decl_remainder):
313
# |-ObjCCategoryDecl 0x1092f8440 <line:76:1, line:81:2> line:76:12 SomeCategory
314
# | |-ObjCInterface 0x1092f5d58 'ObjCMessageWAuthor'
315
# | |-ObjCCategoryImpl 0x1092f8608 'SomeCategory'
316
# | |-ObjCPropertyDecl 0x1092f8508 <line:79:1, col:53> col:53 fakeProperty2 'NSString * _Nullable':'NSString *' readonly nonatomic
317
# | `-ObjCMethodDecl 0x1092f8580 <col:53> col:53 implicit - fakeProperty2 'NSString * _Nullable':'NSString *'
318
if not lines.hasNext():
319
fail("Category missing interface.")
320
line = lines.next()
321
prefix, remainder = split_objc_ast_prefix(line)
322
if len(prefix) <= len(decl_prefix):
323
fail("Category missing interface.")
324
class_name = remainder.split(" ")[-1]
325
if class_name.startswith("'") and class_name.endswith("'"):
326
class_name = class_name[1:-1]
327
328
process_objc_class(
329
namespace,
330
file_path,
331
lines,
332
decl_prefix,
333
decl_remainder,
334
custom_class_name=class_name,
335
)
336
337
338
# |-ObjCCategoryDecl 0x10f5d3d58 <SignalDataStoreCommon/ObjCMessage.m:18:1, line:22:2> line:18:12
339
# | |-ObjCInterface 0x10f5d3490 'ObjCMessage'
340
# | |-ObjCPropertyDecl 0x10f5d3e20 <line:20:1, col:43> col:43 ignore 'NSString * _Nonnull':'NSString *' readonly nonatomic strong
341
# | `-ObjCMethodDecl 0x10f5d3e98 <col:43> col:43 implicit - ignore 'NSString * _Nonnull':'NSString *'
342
def process_objc_implementation(
343
namespace, file_path, lines, decl_prefix, decl_remainder
344
):
345
clazz = process_objc_class(namespace, file_path, lines, decl_prefix, decl_remainder)
346
if clazz is not None:
347
clazz.is_implemented = True
348
349
350
def process_objc_protocol_decl(
351
namespace, file_path, lines, decl_prefix, decl_remainder
352
):
353
clazz = process_objc_class(namespace, file_path, lines, decl_prefix, decl_remainder)
354
if clazz is not None:
355
clazz.is_implemented = True
356
357
358
# |-ObjCCategoryDecl 0x10f5d3d58 <SignalDataStoreCommon/ObjCMessage.m:18:1, line:22:2> line:18:12
359
# | |-ObjCInterface 0x10f5d3490 'ObjCMessage'
360
# | |-ObjCPropertyDecl 0x10f5d3e20 <line:20:1, col:43> col:43 ignore 'NSString * _Nonnull':'NSString *' readonly nonatomic strong
361
# | `-ObjCMethodDecl 0x10f5d3e98 <col:43> col:43 implicit - ignore 'NSString * _Nonnull':'NSString *'
362
def process_objc_class(
363
namespace,
364
file_path,
365
lines,
366
decl_prefix,
367
decl_remainder,
368
custom_class_name=None,
369
super_class_name=None,
370
):
371
if custom_class_name is not None:
372
class_name = custom_class_name
373
else:
374
class_name = decl_remainder.split(" ")[-1]
375
376
clazz = namespace.upsert_class(class_name)
377
378
if super_class_name is not None:
379
if clazz.super_class_name is None:
380
clazz.super_class_name = super_class_name
381
elif clazz.super_class_name != super_class_name:
382
fail(
383
"super_class_name does not match:",
384
clazz.super_class_name,
385
super_class_name,
386
)
387
388
while lines.hasNext():
389
line = lines.next()
390
prefix, remainder = split_objc_ast_prefix(line)
391
if len(prefix) <= len(decl_prefix):
392
# Declaration is over.
393
return clazz
394
395
line = lines.popNext()
396
397
# | |-ObjCPropertyDecl 0x10f5d2d40 <line:19:1, col:43> col:43 uniqueId 'NSString * _Nonnull':'NSString *' readonly nonatomic
398
399
"""
400
TODO: We face interesting choices about how to process:
401
402
* properties
403
* private properties
404
* properties with renamed ivars
405
* ivars without properties
406
* properties not backed by ivars (e.g. actually accessors).
407
"""
408
if remainder.startswith("ObjCPropertyDecl "):
409
process_objc_property(clazz, prefix, file_path, line, remainder)
410
elif remainder.startswith("ObjCPropertyImplDecl "):
411
process_objc_property_impl(clazz, prefix, file_path, line, remainder)
412
elif remainder.startswith("ObjCMethodDecl "):
413
process_objc_method_decl(clazz, prefix, file_path, line, remainder)
414
elif remainder.startswith("ObjCProtocol "):
415
process_objc_protocol(namespace, clazz, prefix, file_path, line, remainder)
416
417
return clazz
418
419
420
process_objc_method_decl_regex = re.compile(r" - (sdsFinalize[^ ]*?) 'void'$")
421
422
423
def process_objc_method_decl(clazz, prefix, file_path, line, remainder):
424
match = process_objc_method_decl_regex.search(remainder)
425
if match is None:
426
return
427
method_name = match.group(1).strip()
428
clazz.finalize_method_name = method_name
429
430
431
# | |-ObjCProtocol 0x7f879888b8a8 'AppContext'
432
process_objc_protocol_regex = re.compile(r" '([^']+)'$")
433
434
435
def process_objc_protocol(namespace, clazz, prefix, file_path, line, remainder):
436
match = process_objc_protocol_regex.search(remainder)
437
if match is None:
438
return
439
protocol_name = match.group(1).strip()
440
clazz.inherit_from_protocol(namespace, protocol_name)
441
442
443
# | |-ObjCPropertyImplDecl 0x1092f6d68 <col:1, col:13> col:13 someSynthesizedProperty synthesize
444
# | |-ObjCPropertyImplDecl 0x1092f6f18 <col:1, col:35> col:13 someRenamedProperty synthesize
445
# | |-ObjCPropertyImplDecl 0x1092f7698 <<invalid sloc>, col:53> <invalid sloc> author synthesize
446
# | `-ObjCPropertyImplDecl 0x1092f77f8 <<invalid sloc>, col:53> <invalid sloc> somePrivateOptionalString synthesize
447
#
448
# ObjCPropertyDecl 0x7fc37e08f800 <line:37:1, col:28> col:28 shouldThreadBeVisible 'int' assign readwrite nonatomic unsafe_unretained
449
process_objc_property_impl_regex = re.compile(r"^.+ ([^ ]+) synthesize$")
450
451
452
def process_objc_property_impl(clazz, prefix, file_path, line, remainder):
453
match = process_objc_property_impl_regex.search(remainder)
454
if match is None:
455
print("file_path:", file_path)
456
fail("Could not match line:", line)
457
property_name = match.group(1).strip()
458
property = clazz.get_property(property_name)
459
if property is None:
460
if clazz.name == "AppDelegate" and property_name == "window":
461
# We can't parse properties synthesized locally but
462
# declared in a protocol defined in the iOS frameworks.
463
# So, special case these propert(y/ies) - we don't need
464
# to handle them.
465
return
466
467
print("file_path:", file_path)
468
print("line:", line)
469
print("\t", "clazz", clazz.name, clazz.counter)
470
print("\t", "property_name", property_name)
471
for name in clazz.property_names():
472
print("\t\t", name)
473
fail("Can't find property:", property_name)
474
else:
475
property.is_synthesized = True
476
477
478
# | |-ObjCPropertyDecl 0x11250fd40 <line:19:1, col:43> col:43 uniqueId 'NSString * _Nonnull':'NSString *' readonly nonatomic
479
# | |-ObjCPropertyDecl 0x116afde80 <line:15:1, col:38> col:38 isUnread 'BOOL':'signed char' readonly nonatomic
480
#
481
# | |-ObjCPropertyDecl 0x7f8157089a00 <line:37:1, col:28> col:28 shouldThreadBeVisible 'int' assign readwrite nonatomic unsafe_unretained
482
#
483
# | |-ObjCPropertyDecl 0x7faf139af8e0 <line:37:1, col:28> col:28 shouldThreadBeVisible 'BOOL':'bool' assign readwrite nonatomic unsafe_unretained
484
# | |-ObjCPropertyDecl 0x7f879889f460 <line:46:1, col:40> col:40 mainWindow 'UIWindow * _Nullable':'UIWindow *' readwrite atomic strong
485
process_objc_property_regex = re.compile(r"^.+<.+> col:\d+(.+?)'(.+?)'(:'(.+)')?(.+)$")
486
487
488
# This convenience function handles None results and strips.
489
def get_match_group(match, index):
490
group = match.group(index)
491
if group is None:
492
return ""
493
return group.strip()
494
495
496
def process_objc_property(clazz, prefix, file_path, line, remainder):
497
498
match = process_objc_property_regex.search(remainder)
499
if match is None:
500
print("file_path:", file_path)
501
print("remainder:", remainder)
502
fail("Could not match line:", line)
503
property_name = match.group(1).strip()
504
property_type_1 = get_match_group(match, 2)
505
property_type_2 = get_match_group(match, 4)
506
property_keywords = match.group(5).strip().split(" ")
507
508
is_optional = (property_type_2 + " _Nullable") == property_type_1
509
is_readonly = "readonly" in property_keywords
510
511
property_type = property_type_2
512
if len(property_type_2) < 1:
513
property_type = property_type_1
514
515
primitive_types = ("BOOL", "NSInteger", "NSUInteger", "uint64_t", "int64_t")
516
if property_type_1 in primitive_types:
517
property_type = property_type_1
518
519
property = clazz.get_property(property_name)
520
if property is None:
521
522
property = ParsedProperty(property_name, property_type, is_optional)
523
clazz.add_property(property)
524
else:
525
if property.name != property_name:
526
fail("Property names don't match", property.name, property_name)
527
if property.is_optional != is_optional:
528
if clazz.name.startswith("DD"):
529
# CocoaLumberjack has nullability consistency issues.
530
# Ignore them.
531
return
532
print("file_path:", file_path)
533
print("clazz:", clazz.name)
534
fail("Property is_optional don't match", property_name)
535
if property.objc_type != property_type:
536
# There's a common pattern of using a mutable private property
537
# and exposing a non-mutable public property to prevent
538
# external modification of the property.
539
if (
540
property_type.startswith("NSMutable")
541
and property.objc_type == "NS" + property_type[len("NSMutable") :]
542
):
543
property.objc_type = property_type
544
else:
545
print("file_path:", file_path)
546
print("remainder:", remainder)
547
print("property.objc_type:", property.objc_type)
548
print("property_type:", property_type)
549
print("property_name:", property_name)
550
fail("Property types don't match", property.objc_type, property_type)
551
552
if not is_readonly:
553
property.is_not_readonly = True
554
555
556
def emit_output(file_path, namespace):
557
classes = []
558
for class_name in namespace.class_names():
559
clazz = namespace.upsert_class(class_name)
560
if not clazz.is_implemented:
561
if not class_name.startswith("NS"):
562
pass
563
continue
564
565
properties = []
566
567
for property in clazz.all_properties():
568
if not property.is_synthesized:
569
continue
570
571
property_dict = {
572
"name": property.name,
573
"objc_type": property.objc_type,
574
"is_optional": property.is_optional,
575
"class_name": class_name,
576
# This might not be necessary, thanks to is_synthesized
577
# 'is_readonly': (not property.is_not_readonly),
578
}
579
580
properties.append(property_dict)
581
582
class_dict = {
583
"name": class_name,
584
"properties": properties,
585
"filepath": sds_common.sds_to_relative_path(file_path),
586
"finalize_method_name": clazz.finalize_method_name,
587
}
588
if clazz.super_class_name is not None:
589
class_dict["super_class_name"] = clazz.super_class_name
590
classes.append(class_dict)
591
592
enums = enum_type_map
593
594
root = {
595
"classes": classes,
596
"enums": enums,
597
}
598
599
return json.dumps(root, sort_keys=True, indent=4)
600
601
602
# We need to include search paths for every
603
# non-framework header.
604
def find_header_include_paths(include_path):
605
result = []
606
607
def add_dir_if_has_header(dir_path):
608
# Only include subdirectories with header files.
609
for filename in os.listdir(dir_path):
610
if filename.endswith(".h"):
611
result.append("-I" + dir_path)
612
break
613
614
# Add root if necessary.
615
add_dir_if_has_header(include_path)
616
617
for rootdir, dirnames, filenames in os.walk(include_path):
618
for dirname in dirnames:
619
dir_path = os.path.abspath(os.path.join(rootdir, dirname))
620
add_dir_if_has_header(dir_path)
621
622
return result
623
624
625
# --- Modules
626
627
# Framework compilation gathers all framework headers
628
# in an include directory with the framework name, so
629
# that headers can be included like so:
630
#
631
# #import <framework_name/header_name.h>
632
#
633
# For example:
634
#
635
# #import <SignalServiceKit/OWSFailedAttachmentDownloadsJob.h>
636
#
637
# To simulate this, we walk the Pods directory and copy
638
# headers into per-framework directories.
639
640
641
def copy_module_headers(src_dir_path, module_name, module_header_dir_path):
642
dst_dir_path = os.path.join(module_header_dir_path, module_name)
643
os.mkdir(dst_dir_path)
644
645
for rootdir, dirnames, filenames in os.walk(src_dir_path):
646
for filename in filenames:
647
if not filename.endswith(".h"):
648
continue
649
src_file_path = os.path.abspath(os.path.join(rootdir, filename))
650
dst_file_path = os.path.abspath(os.path.join(dst_dir_path, filename))
651
shutil.copyfile(src_file_path, dst_file_path)
652
653
654
def gather_pod_headers(pods_dir_path, module_header_dir_path):
655
656
for dirname in os.listdir(pods_dir_path):
657
src_dir_path = os.path.join(pods_dir_path, dirname)
658
if not os.path.isdir(src_dir_path):
659
continue
660
661
copy_module_headers(src_dir_path, dirname, module_header_dir_path)
662
663
664
def gather_module_headers(pods_dir_path):
665
# Make a temp directory to gather framework headers in.
666
module_header_dir_path = tempfile.mkdtemp()
667
668
gather_pod_headers(pods_dir_path, module_header_dir_path)
669
670
for project_name in (
671
"SignalServiceKit",
672
"Signal",
673
):
674
src_dir_path = os.path.join(git_repo_path, project_name)
675
copy_module_headers(src_dir_path, project_name, module_header_dir_path)
676
677
return module_header_dir_path
678
679
680
# --- PCH
681
682
683
def get_pch_include(file_path):
684
ssk_path = os.path.join(git_repo_path, "SignalServiceKit") + os.sep
685
s_path = os.path.join(git_repo_path, "Signal") + os.sep
686
sae_path = os.path.join(git_repo_path, "SignalShareExtension") + os.sep
687
if file_path.startswith(ssk_path):
688
return os.path.join(
689
git_repo_path, "SignalServiceKit/SignalServiceKit-prefix.pch"
690
)
691
elif file_path.startswith(s_path):
692
return os.path.join(git_repo_path, "Signal/Signal-Prefix.pch")
693
elif file_path.startswith(sae_path):
694
return os.path.join(
695
git_repo_path, "SignalShareExtension/SignalShareExtension-Prefix.pch"
696
)
697
else:
698
fail("Couldn't determine .pch for file:", file_path)
699
700
701
# --- Processing
702
703
704
def process_objc(
705
file_path: str,
706
iphoneos_sdk_path: str,
707
swift_bridging_path: str,
708
module_header_dir_path: str,
709
header_include_paths: list[str],
710
) -> None:
711
pch_include = get_pch_include(file_path)
712
713
# These clang args can be found by building our workspace and looking at how XCode invokes clang.
714
clang_args = "-arch arm64 -fmessage-length=0 -fdiagnostics-show-note-include-stack -fmacro-backtrace-limit=0 -std=gnu11 -fobjc-arc -fobjc-weak -fmodules -gmodules -fmodules-prune-interval=86400 -fmodules-prune-after=345600 -Wnon-modular-include-in-framework-module -Werror=non-modular-include-in-framework-module -fapplication-extension -Wno-trigraphs -fpascal-strings -O0 -fno-common -Wno-missing-field-initializers -Wno-missing-prototypes -Werror=return-type -Wdocumentation -Wunreachable-code -Wno-implicit-atomic-properties -Werror=deprecated-objc-isa-usage -Wno-objc-interface-ivars -Werror=objc-root-class -Wno-arc-repeated-use-of-weak -Wimplicit-retain-self -Wduplicate-method-match -Wno-missing-braces -Wparentheses -Wswitch -Wunused-function -Wno-unused-label -Wno-unused-parameter -Wunused-variable -Wunused-value -Wempty-body -Wuninitialized -Wconditional-uninitialized -Wno-unknown-pragmas -Wno-shadow -Wno-four-char-constants -Wno-conversion -Wconstant-conversion -Wint-conversion -Wbool-conversion -Wenum-conversion -Wno-float-conversion -Wnon-literal-null-conversion -Wobjc-literal-conversion -Wshorten-64-to-32 -Wpointer-sign -Wno-newline-eof -Wno-selector -Wno-strict-selector-match -Wundeclared-selector -Wdeprecated-implementations".split(
715
" "
716
)
717
718
# TODO: We'll never repro the correct search paths, so clang will always emit errors.
719
# We'll want to ignore these errors without silently failing.
720
command = (
721
[
722
"clang",
723
"-x",
724
"objective-c",
725
"-Xclang",
726
"-ast-dump",
727
"-fobjc-arc",
728
]
729
+ clang_args
730
+ [
731
"-isysroot",
732
iphoneos_sdk_path,
733
]
734
+ header_include_paths
735
+ [
736
("-I" + module_header_dir_path),
737
("-I" + swift_bridging_path),
738
"-include",
739
pch_include,
740
file_path,
741
]
742
)
743
744
exit_code, output, error_output = ows_getoutput(command)
745
746
output = output.strip()
747
raw_ast = output
748
749
namespace = Namespace()
750
751
process_objc_ast(namespace, file_path, raw_ast)
752
753
output = emit_output(file_path, namespace)
754
755
parsed_file_path = file_path + sds_common.SDS_JSON_FILE_EXTENSION
756
with open(parsed_file_path, "wt") as f:
757
f.write(output)
758
759
760
def process_file(
761
file_path,
762
iphoneos_sdk_path,
763
swift_bridging_path,
764
module_header_dir_path,
765
header_include_paths,
766
):
767
filename = os.path.basename(file_path)
768
769
# TODO: Fix this file
770
if filename == "OWSDisappearingMessageFinderTest.m":
771
return
772
773
_, file_extension = os.path.splitext(filename)
774
if file_extension == ".m":
775
process_objc(
776
file_path,
777
iphoneos_sdk_path,
778
swift_bridging_path,
779
module_header_dir_path,
780
header_include_paths,
781
)
782
783
784
# ---
785
786
if __name__ == "__main__":
787
parser = argparse.ArgumentParser(description="Parse Objective-C AST.")
788
parser.add_argument(
789
"--src-path", required=True, help="used to specify a path to process."
790
)
791
parser.add_argument(
792
"--swift-bridging-path",
793
required=True,
794
help="used to specify a path to process.",
795
)
796
args = parser.parse_args()
797
798
src_path = os.path.abspath(args.src_path)
799
swift_bridging_path = os.path.abspath(args.swift_bridging_path)
800
module_header_dir_path = gather_module_headers("Pods")
801
802
command = [
803
"xcrun",
804
"--show-sdk-path",
805
"--sdk",
806
"iphoneos",
807
]
808
exit_code, output, error_output = ows_getoutput(command)
809
if int(exit_code) != 0:
810
fail("Could not find iOS SDK.")
811
iphoneos_sdk_path = output.strip()
812
813
header_include_paths = []
814
header_include_paths.extend(find_header_include_paths("SignalServiceKit"))
815
816
# SDS code generation uses clang to parse the AST of Objective-C files.
817
# We're parsing these files outside the context of an XCode workspace,
818
# so many things won't work - unless do some legwork.
819
#
820
# * Compiling of dependencies.
821
# * Workspace include and framework search paths.
822
# * Auto-generated files, like -Swift.h bridging headers.
823
# * .pch files.
824
825
print(f"Parsing Obj-C files in {src_path}...")
826
if os.path.isfile(src_path):
827
process_file(
828
src_path, iphoneos_sdk_path, swift_bridging_path, module_header_dir_path
829
)
830
else:
831
# First clear out existing .sdsjson files.
832
for rootdir, dirnames, filenames in os.walk(src_path):
833
for filename in filenames:
834
if filename.endswith(sds_common.SDS_JSON_FILE_EXTENSION):
835
file_path = os.path.abspath(os.path.join(rootdir, filename))
836
os.remove(file_path)
837
838
for rootdir, dirnames, filenames in os.walk(src_path):
839
for filename in filenames:
840
file_path = os.path.abspath(os.path.join(rootdir, filename))
841
process_file(
842
file_path,
843
iphoneos_sdk_path,
844
swift_bridging_path,
845
module_header_dir_path,
846
header_include_paths,
847
)
848
849
850
# TODO: We can't access ivars from Swift without public property accessors.
851
# TODO: We can't access private properties from Swift without public property accessors.
852
# We could generate "SDS Private" headers that exposes these properties, but only if they're backed by an ivar.
853
# TODO: Preprocessor macros & directives won't work properly with this AST parser.
854
855