11#!/usr/bin/env python3
2+ # TODO this should probably be split in different generators now: ql, qltest, maybe qlipa
23
34import logging
45import pathlib
89import itertools
910
1011import inflection
12+ from toposort import toposort_flatten
1113
1214from swift .codegen .lib import schema , ql
1315
@@ -27,59 +29,88 @@ class ModifiedStubMarkedAsGeneratedError(Error):
2729 pass
2830
2931
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 (
3234 type = prop .type if not prop .is_predicate else "predicate" ,
3335 qltest_skip = "qltest_skip" in prop .pragmas ,
3436 is_child = prop .is_child ,
3537 is_optional = prop .is_optional ,
3638 is_predicate = prop .is_predicate ,
3739 )
3840 if prop .is_single :
39- return ql .Property (
40- ** common_args ,
41+ args .update (
4142 singular = inflection .camelize (prop .name ),
4243 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 ],
4545 )
4646 elif prop .is_repeated :
47- return ql .Property (
48- ** common_args ,
47+ args .update (
4948 singular = inflection .singularize (inflection .camelize (prop .name )),
5049 plural = inflection .pluralize (inflection .camelize (prop .name )),
5150 tablename = inflection .tableize (f"{ cls .name } _{ prop .name } " ),
5251 tableparams = ["this" , "index" , "result" ],
5352 )
5453 elif prop .is_optional :
55- return ql .Property (
56- ** common_args ,
54+ args .update (
5755 singular = inflection .camelize (prop .name ),
5856 tablename = inflection .tableize (f"{ cls .name } _{ prop .name } " ),
5957 tableparams = ["this" , "result" ],
6058 )
6159 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 ),
6662 tablename = inflection .underscore (f"{ cls .name } _{ prop .name } " ),
6763 tableparams = ["this" ],
6864 )
65+ else :
66+ raise ValueError (f"unknown property kind for { prop .name } from { cls .name } " )
67+ return ql .Property (** args )
6968
7069
71- def get_ql_class (cls : schema .Class ):
70+ def get_ql_class (cls : schema .Class , lookup : typing . Dict [ str , schema . Class ] ):
7271 pragmas = {k : True for k in cls .pragmas if k .startswith ("ql" )}
7372 return ql .Class (
7473 name = cls .name ,
7574 bases = cls .bases ,
7675 final = not cls .derived ,
7776 properties = [get_ql_property (cls , p ) for p in cls .properties ],
7877 dir = cls .dir ,
78+ ipa = bool (cls .ipa ),
7979 ** pragmas ,
8080 )
8181
8282
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+
83114def get_import (file : pathlib .Path , swift_dir : pathlib .Path ):
84115 stem = file .relative_to (swift_dir / "ql/lib" ).with_suffix ("" )
85116 return str (stem ).replace ("/" , "." )
@@ -96,10 +127,10 @@ def get_classes_used_by(cls: ql.Class):
96127 return sorted (set (t for t in get_types_used_by (cls ) if t [0 ].isupper ()))
97128
98129
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 )
100131
101132
102- def _is_generated_stub (file ) :
133+ def _is_generated_stub (file : pathlib . Path ) -> bool :
103134 with open (file ) as contents :
104135 for line in contents :
105136 if not line .startswith ("// generated" ):
@@ -108,12 +139,14 @@ def _is_generated_stub(file):
108139 else :
109140 # no lines
110141 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" )
117150 return True
118151
119152
@@ -129,45 +162,53 @@ def format(codeql, files):
129162 log .debug (line .strip ())
130163
131164
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 ):
135172 base = lookup [b ]
136- for item in _get_all_properties (base , lookup ):
173+ for item in _get_all_properties (base , lookup , already_seen ):
137174 yield item
138175 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
140179
141180
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 ]:
146183 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 )
149187 yield ql .PropertyForTest (p .getter , p .type , p .is_single , p .is_predicate , p .is_repeated )
150188
151189
190+ def _partition_iter (x , pred ):
191+ x1 , x2 = itertools .tee (x )
192+ return filter (pred , x1 ), itertools .filterfalse (pred , x2 )
193+
194+
152195def _partition (l , pred ):
153196 """ 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 ))
158198
159199
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 )
162202
163203
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 (
166206 _is_in_qltest_collapsed_hierachy (lookup [b ], lookup ) for b in cls .bases )
167207
168208
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 (
171212 cls , lookup )
172213
173214
@@ -185,15 +226,18 @@ def generate(opts, renderer):
185226
186227 data = schema .load (input )
187228
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 ()}
191230 imports = {}
192231
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 :
194238 imports [c .name ] = get_import (stub_out / c .path , opts .swift_dir )
195239
196- for c in classes :
240+ for c in classes . values () :
197241 qll = out / c .path .with_suffix (".qll" )
198242 c .imports = [imports [t ] for t in get_classes_used_by (c )]
199243 renderer .render (c , qll )
@@ -207,27 +251,49 @@ def generate(opts, renderer):
207251 include_file = stub_out .with_suffix (".qll" )
208252 renderer .render (ql .ImportList (list (imports .values ())), include_file )
209253
210- renderer .render (ql .GetParentImplementation (
211- classes ), out / 'GetImmediateParent.qll' )
254+ renderer .render (ql .GetParentImplementation (classes_by_dir_and_name ), out / 'GetImmediateParent.qll' )
212255
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 ):
215258 continue
216- test_dir = test_out / c .path
259+ test_dir = test_out / c .dir / c . name
217260 test_dir .mkdir (parents = True , exist_ok = True )
218261 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 } " )
220263 renderer .render (ql .MissingTestInstructions (),
221264 test_dir / missing_test_source_filename )
222265 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 ),
224267 lambda p : p .is_single or p .is_predicate )
225268 renderer .render (ql .ClassTester (class_name = c .name ,
226269 properties = total_props ), test_dir / f"{ c .name } .ql" )
227270 for p in partial_props :
228271 renderer .render (ql .PropertyTester (class_name = c .name ,
229272 property = p ), test_dir / f"{ c .name } _{ p .getter } .ql" )
230273
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+
231297 renderer .cleanup (existing )
232298 if opts .ql_format :
233299 format (opts .codeql_binary , renderer .written )
0 commit comments