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,59 +29,88 @@ 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 , 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
43
tablename = inflection .tableize (cls .name ),
43
- tableparams = [
44
- "this" ] + ["result" if p is prop else "_" for p in cls .properties if p .is_single ],
44
+ tableparams = ["this" ] + ["result" if p is prop else "_" for p in cls .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
50
tablename = inflection .tableize (f"{ cls .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
56
tablename = inflection .tableize (f"{ cls .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 ),
60
+ args .update (
61
+ singular = inflection .camelize (prop .name , uppercase_first_letter = False ),
66
62
tablename = inflection .underscore (f"{ cls .name } _{ prop .name } " ),
67
63
tableparams = ["this" ],
68
64
)
65
+ else :
66
+ raise ValueError (f"unknown property kind for { prop .name } from { cls .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
76
properties = [get_ql_property (cls , p ) for p in cls .properties ],
78
77
dir = cls .dir ,
78
+ ipa = bool (cls .ipa ),
79
79
** pragmas ,
80
80
)
81
81
82
82
83
+ def _to_db_type (x : str ) -> str :
84
+ if x [0 ].isupper ():
85
+ return "Raw::" + x
86
+ return x
87
+
88
+
89
+ _final_db_class_lookup = {}
90
+
91
+
92
+ def get_ql_ipa_class_db (name : str ) -> ql .Synth .FinalClassDb :
93
+ return _final_db_class_lookup .setdefault (name , ql .Synth .FinalClassDb (name = name ,
94
+ params = [
95
+ ql .Synth .Param ("id" , _to_db_type (name ))]))
96
+
97
+
98
+ def get_ql_ipa_class (cls : schema .Class ):
99
+ if cls .derived :
100
+ return ql .Synth .NonFinalClass (name = cls .name , derived = sorted (cls .derived ),
101
+ root = (cls .name == schema .root_class_name ))
102
+ if cls .ipa and cls .ipa .from_class is not None :
103
+ source = cls .ipa .from_class
104
+ get_ql_ipa_class_db (source ).subtract_type (cls .name )
105
+ return ql .Synth .FinalClassDerivedIpa (name = cls .name ,
106
+ params = [ql .Synth .Param ("id" , _to_db_type (source ))])
107
+ if cls .ipa and cls .ipa .on_arguments is not None :
108
+ return ql .Synth .FinalClassFreshIpa (name = cls .name ,
109
+ params = [ql .Synth .Param (k , _to_db_type (v ))
110
+ for k , v in cls .ipa .on_arguments .items ()])
111
+ return get_ql_ipa_class_db (cls .name )
112
+
113
+
83
114
def get_import (file : pathlib .Path , swift_dir : pathlib .Path ):
84
115
stem = file .relative_to (swift_dir / "ql/lib" ).with_suffix ("" )
85
116
return str (stem ).replace ("/" , "." )
@@ -96,10 +127,10 @@ def get_classes_used_by(cls: ql.Class):
96
127
return sorted (set (t for t in get_types_used_by (cls ) if t [0 ].isupper ()))
97
128
98
129
99
- _generated_stub_re = re .compile (r"private import .*\n\nclass \w+ extends \w+ \{[ \n]\}" , re .MULTILINE )
130
+ _generated_stub_re = re .compile (r"\n* private import .*\n+class \w+ extends \w+ \{[ \n]? \}" , re .MULTILINE )
100
131
101
132
102
- def _is_generated_stub (file ) :
133
+ def _is_generated_stub (file : pathlib . Path ) -> bool :
103
134
with open (file ) as contents :
104
135
for line in contents :
105
136
if not line .startswith ("// generated" ):
@@ -108,12 +139,14 @@ def _is_generated_stub(file):
108
139
else :
109
140
# no lines
110
141
return False
111
- # one line already read, if we can read 5 other we are past the normal stub generation
112
- line_threshold = 5
113
- first_lines = list (itertools .islice (contents , line_threshold ))
114
- if len (first_lines ) == line_threshold or not _generated_stub_re .match ("" .join (first_lines )):
115
- raise ModifiedStubMarkedAsGeneratedError (
116
- f"{ file .name } stub was modified but is still marked as generated" )
142
+ # we still do not detect modified synth constructors
143
+ if not file .name .endswith ("Constructor.qll" ):
144
+ # one line already read, if we can read 5 other we are past the normal stub generation
145
+ line_threshold = 5
146
+ first_lines = list (itertools .islice (contents , line_threshold ))
147
+ if len (first_lines ) == line_threshold or not _generated_stub_re .match ("" .join (first_lines )):
148
+ raise ModifiedStubMarkedAsGeneratedError (
149
+ f"{ file .name } stub was modified but is still marked as generated" )
117
150
return True
118
151
119
152
@@ -129,45 +162,53 @@ def format(codeql, files):
129
162
log .debug (line .strip ())
130
163
131
164
132
- def _get_all_properties (cls : ql .Class , lookup : typing .Dict [str , ql .Class ]) -> typing .Iterable [
133
- typing .Tuple [ql .Class , ql .Property ]]:
134
- for b in cls .bases :
165
+ def _get_all_properties (cls : schema .Class , lookup : typing .Dict [str , schema .Class ],
166
+ already_seen : typing .Optional [typing .Set [int ]] = None ) -> \
167
+ typing .Iterable [typing .Tuple [schema .Class , schema .Property ]]:
168
+ # deduplicate using ids
169
+ if already_seen is None :
170
+ already_seen = set ()
171
+ for b in sorted (cls .bases ):
135
172
base = lookup [b ]
136
- for item in _get_all_properties (base , lookup ):
173
+ for item in _get_all_properties (base , lookup , already_seen ):
137
174
yield item
138
175
for p in cls .properties :
139
- yield cls , p
176
+ if id (p ) not in already_seen :
177
+ already_seen .add (id (p ))
178
+ yield cls , p
140
179
141
180
142
- def _get_all_properties_to_be_tested (cls : ql .Class , lookup : typing .Dict [str , ql .Class ]) -> typing .Iterable [
143
- ql .PropertyForTest ]:
144
- # deduplicate using id
145
- already_seen = set ()
181
+ def _get_all_properties_to_be_tested (cls : schema .Class , lookup : typing .Dict [str , schema .Class ]) -> \
182
+ typing .Iterable [ql .PropertyForTest ]:
146
183
for c , p in _get_all_properties (cls , lookup ):
147
- if not (c .qltest_skip or p .qltest_skip or id (p ) in already_seen ):
148
- already_seen .add (id (p ))
184
+ if not ("qltest_skip" in c .pragmas or "qltest_skip" in p .pragmas ):
185
+ # TODO here operations are duplicated, but should be better if we split ql and qltest generation
186
+ p = get_ql_property (c , p )
149
187
yield ql .PropertyForTest (p .getter , p .type , p .is_single , p .is_predicate , p .is_repeated )
150
188
151
189
190
+ def _partition_iter (x , pred ):
191
+ x1 , x2 = itertools .tee (x )
192
+ return filter (pred , x1 ), itertools .filterfalse (pred , x2 )
193
+
194
+
152
195
def _partition (l , pred ):
153
196
""" partitions a list according to boolean predicate """
154
- res = ([], [])
155
- for x in l :
156
- res [not pred (x )].append (x )
157
- return res
197
+ return map (list , _partition_iter (l , pred ))
158
198
159
199
160
- def _is_in_qltest_collapsed_hierachy (cls : ql .Class , lookup : typing .Dict [str , ql .Class ]):
161
- return cls .qltest_collapse_hierarchy or _is_under_qltest_collapsed_hierachy (cls , lookup )
200
+ def _is_in_qltest_collapsed_hierachy (cls : schema .Class , lookup : typing .Dict [str , schema .Class ]):
201
+ return "qltest_collapse_hierarchy" in cls .pragmas or _is_under_qltest_collapsed_hierachy (cls , lookup )
162
202
163
203
164
- def _is_under_qltest_collapsed_hierachy (cls : ql .Class , lookup : typing .Dict [str , ql .Class ]):
165
- return not cls .qltest_uncollapse_hierarchy and any (
204
+ def _is_under_qltest_collapsed_hierachy (cls : schema .Class , lookup : typing .Dict [str , schema .Class ]):
205
+ return "qltest_uncollapse_hierarchy" not in cls .pragmas and any (
166
206
_is_in_qltest_collapsed_hierachy (lookup [b ], lookup ) for b in cls .bases )
167
207
168
208
169
- def _should_skip_qltest (cls : ql .Class , lookup : typing .Dict [str , ql .Class ]):
170
- return cls .qltest_skip or not (cls .final or cls .qltest_collapse_hierarchy ) or _is_under_qltest_collapsed_hierachy (
209
+ def _should_skip_qltest (cls : schema .Class , lookup : typing .Dict [str , schema .Class ]):
210
+ return "qltest_skip" in cls .pragmas or not (
211
+ cls .final or "qltest_collapse_hierarchy" in cls .pragmas ) or _is_under_qltest_collapsed_hierachy (
171
212
cls , lookup )
172
213
173
214
@@ -185,15 +226,18 @@ def generate(opts, renderer):
185
226
186
227
data = schema .load (input )
187
228
188
- classes = [get_ql_class (cls ) for cls in data .classes ]
189
- lookup = {cls .name : cls for cls in classes }
190
- classes .sort (key = lambda cls : (cls .dir , cls .name ))
229
+ classes = {name : get_ql_class (cls , data .classes ) for name , cls in data .classes .items ()}
191
230
imports = {}
192
231
193
- for c in classes :
232
+ inheritance_graph = {name : cls .bases for name , cls in data .classes .items ()}
233
+ db_classes = [classes [name ] for name in toposort_flatten (inheritance_graph ) if not classes [name ].ipa ]
234
+ renderer .render (ql .DbClasses (db_classes ), out / "Raw.qll" )
235
+
236
+ classes_by_dir_and_name = sorted (classes .values (), key = lambda cls : (cls .dir , cls .name ))
237
+ for c in classes_by_dir_and_name :
194
238
imports [c .name ] = get_import (stub_out / c .path , opts .swift_dir )
195
239
196
- for c in classes :
240
+ for c in classes . values () :
197
241
qll = out / c .path .with_suffix (".qll" )
198
242
c .imports = [imports [t ] for t in get_classes_used_by (c )]
199
243
renderer .render (c , qll )
@@ -207,27 +251,49 @@ def generate(opts, renderer):
207
251
include_file = stub_out .with_suffix (".qll" )
208
252
renderer .render (ql .ImportList (list (imports .values ())), include_file )
209
253
210
- renderer .render (ql .GetParentImplementation (
211
- classes ), out / 'GetImmediateParent.qll' )
254
+ renderer .render (ql .GetParentImplementation (classes_by_dir_and_name ), out / 'GetImmediateParent.qll' )
212
255
213
- for c in classes :
214
- if _should_skip_qltest (c , lookup ):
256
+ for c in data . classes . values () :
257
+ if _should_skip_qltest (c , data . classes ):
215
258
continue
216
- test_dir = test_out / c .path
259
+ test_dir = test_out / c .dir / c . name
217
260
test_dir .mkdir (parents = True , exist_ok = True )
218
261
if not any (test_dir .glob ("*.swift" )):
219
- log .warning (f"no test source in { c .path } " )
262
+ log .warning (f"no test source in { c .dir / c . name } " )
220
263
renderer .render (ql .MissingTestInstructions (),
221
264
test_dir / missing_test_source_filename )
222
265
continue
223
- total_props , partial_props = _partition (_get_all_properties_to_be_tested (c , lookup ),
266
+ total_props , partial_props = _partition (_get_all_properties_to_be_tested (c , data . classes ),
224
267
lambda p : p .is_single or p .is_predicate )
225
268
renderer .render (ql .ClassTester (class_name = c .name ,
226
269
properties = total_props ), test_dir / f"{ c .name } .ql" )
227
270
for p in partial_props :
228
271
renderer .render (ql .PropertyTester (class_name = c .name ,
229
272
property = p ), test_dir / f"{ c .name } _{ p .getter } .ql" )
230
273
274
+ final_ipa_types = []
275
+ non_final_ipa_types = []
276
+ constructor_imports = []
277
+ ipa_constructor_imports = []
278
+ for cls in sorted (data .classes .values (), key = lambda cls : (cls .dir , cls .name )):
279
+ ipa_type = get_ql_ipa_class (cls )
280
+ if ipa_type .is_final :
281
+ final_ipa_types .append (ipa_type )
282
+ if ipa_type .has_params :
283
+ stub_file = stub_out / cls .dir / f"{ cls .name } Constructor.qll"
284
+ if not stub_file .is_file () or _is_generated_stub (stub_file ):
285
+ renderer .render (ql .Synth .ConstructorStub (ipa_type ), stub_file )
286
+ constructor_import = get_import (stub_file , opts .swift_dir )
287
+ constructor_imports .append (constructor_import )
288
+ if ipa_type .is_ipa :
289
+ ipa_constructor_imports .append (constructor_import )
290
+ else :
291
+ non_final_ipa_types .append (ipa_type )
292
+
293
+ renderer .render (ql .Synth .Types (schema .root_class_name , final_ipa_types , non_final_ipa_types ), out / "Synth.qll" )
294
+ renderer .render (ql .ImportList (constructor_imports ), out / "SynthConstructors.qll" )
295
+ renderer .render (ql .ImportList (ipa_constructor_imports ), out / "PureSynthConstructors.qll" )
296
+
231
297
renderer .cleanup (existing )
232
298
if opts .ql_format :
233
299
format (opts .codeql_binary , renderer .written )
0 commit comments