1
1
#!/usr/bin/env python3
2
+ # TODO this should probably be split in different generators now: ql, qltest, maybe qlipa
2
3
3
4
import logging
4
5
import pathlib
8
9
import itertools
9
10
10
11
import inflection
12
+ from toposort import toposort_flatten
11
13
12
14
from swift .codegen .lib import schema , ql
13
15
@@ -27,55 +29,55 @@ class ModifiedStubMarkedAsGeneratedError(Error):
27
29
pass
28
30
29
31
30
- def get_ql_property (cls : schema .Class , prop : schema .Property ):
31
- common_args = dict (
32
+ def get_ql_property (cls : schema .Class , source : schema . Class , prop : schema .Property ) -> ql . Property :
33
+ args = dict (
32
34
type = prop .type if not prop .is_predicate else "predicate" ,
33
35
qltest_skip = "qltest_skip" in prop .pragmas ,
34
36
is_child = prop .is_child ,
35
37
is_optional = prop .is_optional ,
36
38
is_predicate = prop .is_predicate ,
37
39
)
38
40
if prop .is_single :
39
- return ql .Property (
40
- ** common_args ,
41
+ args .update (
41
42
singular = inflection .camelize (prop .name ),
42
- tablename = inflection .tableize (cls .name ),
43
- tableparams = [
44
- "this" ] + ["result" if p is prop else "_" for p in cls .properties if p .is_single ],
43
+ tablename = inflection .tableize (source .name ),
44
+ tableparams = ["this" ] + ["result" if p is prop else "_" for p in source .properties if p .is_single ],
45
45
)
46
46
elif prop .is_repeated :
47
- return ql .Property (
48
- ** common_args ,
47
+ args .update (
49
48
singular = inflection .singularize (inflection .camelize (prop .name )),
50
49
plural = inflection .pluralize (inflection .camelize (prop .name )),
51
- tablename = inflection .tableize (f"{ cls .name } _{ prop .name } " ),
50
+ tablename = inflection .tableize (f"{ source .name } _{ prop .name } " ),
52
51
tableparams = ["this" , "index" , "result" ],
53
52
)
54
53
elif prop .is_optional :
55
- return ql .Property (
56
- ** common_args ,
54
+ args .update (
57
55
singular = inflection .camelize (prop .name ),
58
- tablename = inflection .tableize (f"{ cls .name } _{ prop .name } " ),
56
+ tablename = inflection .tableize (f"{ source .name } _{ prop .name } " ),
59
57
tableparams = ["this" , "result" ],
60
58
)
61
59
elif prop .is_predicate :
62
- return ql .Property (
63
- ** common_args ,
64
- singular = inflection .camelize (
65
- prop .name , uppercase_first_letter = False ),
66
- tablename = inflection .underscore (f"{ cls .name } _{ prop .name } " ),
60
+ args .update (
61
+ singular = inflection .camelize (prop .name , uppercase_first_letter = False ),
62
+ tablename = inflection .underscore (f"{ source .name } _{ prop .name } " ),
67
63
tableparams = ["this" ],
68
64
)
65
+ else :
66
+ raise ValueError (f"unknown property kind for { prop .name } from { source .name } " )
67
+ return ql .Property (** args )
69
68
70
69
71
- def get_ql_class (cls : schema .Class ):
70
+ def get_ql_class (cls : schema .Class , lookup : typing . Dict [ str , schema . Class ] ):
72
71
pragmas = {k : True for k in cls .pragmas if k .startswith ("ql" )}
73
72
return ql .Class (
74
73
name = cls .name ,
75
74
bases = cls .bases ,
76
75
final = not cls .derived ,
77
- properties = [get_ql_property (cls , p ) for p in cls .properties ],
76
+
77
+ properties = [get_ql_property (cls , cls , p ) for p in cls .properties ],
78
78
dir = cls .dir ,
79
+ has_db_id = not cls .ipa or cls .ipa .from_class ,
80
+ ipa = bool (cls .ipa ),
79
81
** pragmas ,
80
82
)
81
83
@@ -92,11 +94,11 @@ def _to_db_type(x: str) -> str:
92
94
def get_ql_ipa_class (cls : schema .Class ):
93
95
if cls .derived :
94
96
return ql .Ipa .NonFinalClass (name = cls .name , derived = sorted (cls .derived ))
95
- if cls .ipa and cls .ipa .from_class :
97
+ if cls .ipa and cls .ipa .from_class is not None :
96
98
source = cls .ipa .from_class
97
99
_final_db_class_lookup .setdefault (source , ql .Ipa .FinalClassDb (source )).subtract_type (cls .name )
98
100
return ql .Ipa .FinalClassIpaFrom (name = cls .name , type = _to_db_type (source ))
99
- if cls .ipa and cls .ipa .on_arguments :
101
+ if cls .ipa and cls .ipa .on_arguments is not None :
100
102
return ql .Ipa .FinalClassIpaOn (name = cls .name ,
101
103
params = [ql .Ipa .Param (k , _to_db_type (v )) for k , v in cls .ipa .on_arguments .items ()])
102
104
return _final_db_class_lookup .setdefault (cls .name , ql .Ipa .FinalClassDb (cls .name ))
@@ -136,7 +138,6 @@ def _is_generated_stub(file):
136
138
line_threshold = 5
137
139
first_lines = list (itertools .islice (contents , line_threshold ))
138
140
if len (first_lines ) == line_threshold or not _generated_stub_re .match ("" .join (first_lines )):
139
- print ("" .join (first_lines ))
140
141
raise ModifiedStubMarkedAsGeneratedError (
141
142
f"{ file .name } stub was modified but is still marked as generated" )
142
143
return True
@@ -154,23 +155,28 @@ def format(codeql, files):
154
155
log .debug (line .strip ())
155
156
156
157
157
- def _get_all_properties (cls : ql .Class , lookup : typing .Dict [str , ql .Class ]) -> typing .Iterable [
158
- typing .Tuple [ql .Class , ql .Property ]]:
159
- for b in cls .bases :
158
+ def _get_all_properties (cls : schema .Class , lookup : typing .Dict [str , schema .Class ],
159
+ already_seen : typing .Optional [typing .Set [int ]] = None ) -> \
160
+ typing .Iterable [typing .Tuple [schema .Class , schema .Property ]]:
161
+ # deduplicate using ids
162
+ if already_seen is None :
163
+ already_seen = set ()
164
+ for b in sorted (cls .bases ):
160
165
base = lookup [b ]
161
- for item in _get_all_properties (base , lookup ):
166
+ for item in _get_all_properties (base , lookup , already_seen ):
162
167
yield item
163
168
for p in cls .properties :
164
- yield cls , p
169
+ if id (p ) not in already_seen :
170
+ already_seen .add (id (p ))
171
+ yield cls , p
165
172
166
173
167
- def _get_all_properties_to_be_tested (cls : ql .Class , lookup : typing .Dict [str , ql .Class ]) -> typing .Iterable [
168
- ql .PropertyForTest ]:
169
- # deduplicate using id
170
- already_seen = set ()
174
+ def _get_all_properties_to_be_tested (cls : schema .Class , lookup : typing .Dict [str , schema .Class ]) -> \
175
+ typing .Iterable [ql .PropertyForTest ]:
171
176
for c , p in _get_all_properties (cls , lookup ):
172
- if not (c .qltest_skip or p .qltest_skip or id (p ) in already_seen ):
173
- already_seen .add (id (p ))
177
+ if not ("qltest_skip" in c .pragmas or "qltest_skip" in p .pragmas ):
178
+ # TODO here operations are duplicated, but should be better if we split ql and qltest generation
179
+ p = get_ql_property (cls , c , p )
174
180
yield ql .PropertyForTest (p .getter , p .type , p .is_single , p .is_predicate , p .is_repeated )
175
181
176
182
@@ -184,17 +190,18 @@ def _partition(l, pred):
184
190
return map (list , _partition_iter (l , pred ))
185
191
186
192
187
- def _is_in_qltest_collapsed_hierachy (cls : ql .Class , lookup : typing .Dict [str , ql .Class ]):
188
- return cls .qltest_collapse_hierarchy or _is_under_qltest_collapsed_hierachy (cls , lookup )
193
+ def _is_in_qltest_collapsed_hierachy (cls : schema .Class , lookup : typing .Dict [str , schema .Class ]):
194
+ return "qltest_collapse_hierarchy" in cls .pragmas or _is_under_qltest_collapsed_hierachy (cls , lookup )
189
195
190
196
191
- def _is_under_qltest_collapsed_hierachy (cls : ql .Class , lookup : typing .Dict [str , ql .Class ]):
192
- return not cls .qltest_uncollapse_hierarchy and any (
197
+ def _is_under_qltest_collapsed_hierachy (cls : schema .Class , lookup : typing .Dict [str , schema .Class ]):
198
+ return "qltest_uncollapse_hierarchy" not in cls .pragmas and any (
193
199
_is_in_qltest_collapsed_hierachy (lookup [b ], lookup ) for b in cls .bases )
194
200
195
201
196
- def _should_skip_qltest (cls : ql .Class , lookup : typing .Dict [str , ql .Class ]):
197
- return cls .qltest_skip or not (cls .final or cls .qltest_collapse_hierarchy ) or _is_under_qltest_collapsed_hierachy (
202
+ def _should_skip_qltest (cls : schema .Class , lookup : typing .Dict [str , schema .Class ]):
203
+ return "qltest_skip" in cls .pragmas or not (
204
+ cls .final or "qltest_collapse_hierarchy" in cls .pragmas ) or _is_under_qltest_collapsed_hierachy (
198
205
cls , lookup )
199
206
200
207
@@ -211,12 +218,14 @@ def generate(opts, renderer):
211
218
existing |= {q for q in test_out .rglob (missing_test_source_filename )}
212
219
213
220
data = schema .load (input )
214
- data .classes .sort (key = lambda cls : (cls .dir , cls .name ))
221
+ inheritance_graph = {name : cls .bases for name , cls in data .classes .items ()}
222
+ input_classes = [data .classes [name ] for name in toposort_flatten (inheritance_graph )]
215
223
216
- classes = [get_ql_class (cls ) for cls in data .classes ]
217
- lookup = {cls .name : cls for cls in classes }
224
+ classes = [get_ql_class (cls , data .classes ) for cls in input_classes ]
218
225
imports = {}
219
226
227
+ renderer .render (ql .DbClasses (cls for cls in classes if not cls .ipa ), out / "Db.qll" )
228
+
220
229
for c in classes :
221
230
imports [c .name ] = get_import (stub_out / c .path , opts .swift_dir )
222
231
@@ -237,17 +246,17 @@ def generate(opts, renderer):
237
246
renderer .render (ql .GetParentImplementation (
238
247
classes ), out / 'GetImmediateParent.qll' )
239
248
240
- for c in classes :
241
- if _should_skip_qltest (c , lookup ):
249
+ for c in input_classes :
250
+ if _should_skip_qltest (c , data . classes ):
242
251
continue
243
- test_dir = test_out / c .path
252
+ test_dir = test_out / c .dir / c . name
244
253
test_dir .mkdir (parents = True , exist_ok = True )
245
254
if not any (test_dir .glob ("*.swift" )):
246
- log .warning (f"no test source in { c .path } " )
255
+ log .warning (f"no test source in { c .dir / c . name } " )
247
256
renderer .render (ql .MissingTestInstructions (),
248
257
test_dir / missing_test_source_filename )
249
258
continue
250
- total_props , partial_props = _partition (_get_all_properties_to_be_tested (c , lookup ),
259
+ total_props , partial_props = _partition (_get_all_properties_to_be_tested (c , data . classes ),
251
260
lambda p : p .is_single or p .is_predicate )
252
261
renderer .render (ql .ClassTester (class_name = c .name ,
253
262
properties = total_props ), test_dir / f"{ c .name } .ql" )
@@ -258,16 +267,16 @@ def generate(opts, renderer):
258
267
final_ipa_types = []
259
268
non_final_ipa_types = []
260
269
constructor_imports = []
261
- for cls in data . classes :
270
+ for cls in input_classes :
262
271
ipa_type = get_ql_ipa_class (cls )
263
272
if ipa_type .is_final :
264
273
final_ipa_types .append (ipa_type )
265
- if ipa_type .is_ipa :
274
+ if ipa_type .is_ipa_from or ( ipa_type . is_ipa_on and ipa_type . has_params ) :
266
275
stub_file = stub_out / cls .dir / f"{ cls .name } Constructor.qll"
267
276
if not stub_file .is_file () or _is_generated_stub (stub_file ):
268
277
renderer .render (ql .Ipa .ConstructorStub (ipa_type ), stub_file )
269
278
constructor_imports .append (get_import (stub_file , opts .swift_dir ))
270
- else :
279
+ elif cls . name != schema . root_class_name :
271
280
non_final_ipa_types .append (ipa_type )
272
281
273
282
renderer .render (ql .Ipa .Types (schema .root_class_name , final_ipa_types , non_final_ipa_types ), out / "IpaTypes.qll" )
0 commit comments