33import logging
44import pathlib
55import subprocess
6+ import typing
67
78import inflection
89
1112log = logging .getLogger (__name__ )
1213
1314
15+ class FormatError (Exception ):
16+ pass
17+
18+
1419def get_ql_property (cls : schema .Class , prop : schema .Property ):
20+ common_args = dict (
21+ type = prop .type if not prop .is_predicate else "predicate" ,
22+ skip_qltest = "no_qltest" in prop .tags ,
23+ is_child = prop .is_child ,
24+ is_optional = prop .is_optional ,
25+ is_predicate = prop .is_predicate ,
26+ )
1527 if prop .is_single :
1628 return ql .Property (
29+ ** common_args ,
1730 singular = inflection .camelize (prop .name ),
18- type = prop .type ,
1931 tablename = inflection .tableize (cls .name ),
20- tableparams = ["this" ] + [ "result" if p is prop else "_" for p in cls . properties if p . is_single ],
21- is_child = prop . is_child ,
32+ tableparams = [
33+ "this" ] + [ "result" if p is prop else "_" for p in cls . properties if p . is_single ] ,
2234 )
2335 elif prop .is_repeated :
2436 return ql .Property (
37+ ** common_args ,
2538 singular = inflection .singularize (inflection .camelize (prop .name )),
2639 plural = inflection .pluralize (inflection .camelize (prop .name )),
27- type = prop .type ,
2840 tablename = inflection .tableize (f"{ cls .name } _{ prop .name } " ),
2941 tableparams = ["this" , "index" , "result" ],
30- is_optional = prop .is_optional ,
31- is_child = prop .is_child ,
3242 )
3343 elif prop .is_optional :
3444 return ql .Property (
45+ ** common_args ,
3546 singular = inflection .camelize (prop .name ),
36- type = prop .type ,
3747 tablename = inflection .tableize (f"{ cls .name } _{ prop .name } " ),
3848 tableparams = ["this" , "result" ],
39- is_optional = True ,
40- is_child = prop .is_child ,
4149 )
4250 elif prop .is_predicate :
4351 return ql .Property (
44- singular = inflection .camelize (prop .name , uppercase_first_letter = False ),
45- type = "predicate" ,
52+ ** common_args ,
53+ singular = inflection .camelize (
54+ prop .name , uppercase_first_letter = False ),
4655 tablename = inflection .underscore (f"{ cls .name } _{ prop .name } " ),
4756 tableparams = ["this" ],
48- is_predicate = True ,
4957 )
5058
5159
@@ -56,6 +64,7 @@ def get_ql_class(cls: schema.Class):
5664 final = not cls .derived ,
5765 properties = [get_ql_property (cls , p ) for p in cls .properties ],
5866 dir = cls .dir ,
67+ skip_qltest = "no_qltest" in cls .tags ,
5968 )
6069
6170
@@ -77,48 +86,106 @@ def get_classes_used_by(cls: ql.Class):
7786
7887def is_generated (file ):
7988 with open (file ) as contents :
80- return next (contents ).startswith ("// generated" )
89+ for line in contents :
90+ return line .startswith ("// generated" )
91+ return False
8192
8293
8394def format (codeql , files ):
8495 format_cmd = [codeql , "query" , "format" , "--in-place" , "--" ]
85- format_cmd .extend (str (f ) for f in files )
86- res = subprocess .run (format_cmd , check = True , stderr = subprocess .PIPE , text = True )
96+ format_cmd .extend (str (f ) for f in files if f .suffix in (".qll" , ".ql" ))
97+ res = subprocess .run (format_cmd , stderr = subprocess .PIPE , text = True )
98+ if res .returncode :
99+ for line in res .stderr .splitlines ():
100+ log .error (line .strip ())
101+ raise FormatError ("QL format failed" )
87102 for line in res .stderr .splitlines ():
88103 log .debug (line .strip ())
89104
90105
106+ def _get_all_properties (cls : ql .Class , lookup : typing .Dict [str , ql .Class ]) -> typing .Iterable [
107+ typing .Tuple [ql .Class , ql .Property ]]:
108+ for b in cls .bases :
109+ base = lookup [b ]
110+ for item in _get_all_properties (base , lookup ):
111+ yield item
112+ for p in cls .properties :
113+ yield cls , p
114+
115+
116+ def _get_all_properties_to_be_tested (cls : ql .Class , lookup : typing .Dict [str , ql .Class ]) -> typing .Iterable [
117+ ql .PropertyForTest ]:
118+ # deduplicate using id
119+ already_seen = set ()
120+ for c , p in _get_all_properties (cls , lookup ):
121+ if not (c .skip_qltest or p .skip_qltest or id (p ) in already_seen ):
122+ already_seen .add (id (p ))
123+ yield ql .PropertyForTest (p .getter , p .type , p .is_single , p .is_predicate , p .is_repeated )
124+
125+
126+ def _partition (l , pred ):
127+ """ partitions a list according to boolean predicate """
128+ res = ([], [])
129+ for x in l :
130+ res [not pred (x )].append (x )
131+ return res
132+
133+
91134def generate (opts , renderer ):
92135 input = opts .schema
93136 out = opts .ql_output
94137 stub_out = opts .ql_stub_output
138+ test_out = opts .ql_test_output
139+ missing_test_source_filename = "MISSING_SOURCE.txt"
95140 existing = {q for q in out .rglob ("*.qll" )}
96141 existing |= {q for q in stub_out .rglob ("*.qll" ) if is_generated (q )}
142+ existing |= {q for q in test_out .rglob ("*.ql" )}
143+ existing |= {q for q in test_out .rglob (missing_test_source_filename )}
97144
98145 data = schema .load (input )
99146
100147 classes = [get_ql_class (cls ) for cls in data .classes ]
101- classes .sort (key = lambda cls : cls .name )
148+ lookup = {cls .name : cls for cls in classes }
149+ classes .sort (key = lambda cls : (cls .dir , cls .name ))
102150 imports = {}
103151
104152 for c in classes :
105153 imports [c .name ] = get_import (stub_out / c .path , opts .swift_dir )
106154
107155 for c in classes :
108- qll = ( out / c .path ) .with_suffix (".qll" )
156+ qll = out / c .path .with_suffix (".qll" )
109157 c .imports = [imports [t ] for t in get_classes_used_by (c )]
110158 renderer .render (c , qll )
111- stub_file = ( stub_out / c .path ) .with_suffix (".qll" )
159+ stub_file = stub_out / c .path .with_suffix (".qll" )
112160 if not stub_file .is_file () or is_generated (stub_file ):
113- stub = ql .Stub (name = c .name , base_import = get_import (qll , opts .swift_dir ))
161+ stub = ql .Stub (
162+ name = c .name , base_import = get_import (qll , opts .swift_dir ))
114163 renderer .render (stub , stub_file )
115164
116165 # for example path/to/elements -> path/to/elements.qll
117166 include_file = stub_out .with_suffix (".qll" )
118- all_imports = ql .ImportList (list (sorted (imports .values ())))
119- renderer .render (all_imports , include_file )
167+ renderer .render (ql .ImportList (list (imports .values ())), include_file )
168+
169+ renderer .render (ql .GetParentImplementation (
170+ classes ), out / 'GetImmediateParent.qll' )
120171
121- renderer .render (ql .GetParentImplementation (classes ), out / 'GetImmediateParent.qll' )
172+ for c in classes :
173+ if not c .final or c .skip_qltest :
174+ continue
175+ test_dir = test_out / c .path
176+ test_dir .mkdir (parents = True , exist_ok = True )
177+ if not any (test_dir .glob ("*.swift" )):
178+ log .warning (f"no test source in { c .path } " )
179+ renderer .render (ql .MissingTestInstructions (),
180+ test_dir / missing_test_source_filename )
181+ continue
182+ total_props , partial_props = _partition (_get_all_properties_to_be_tested (c , lookup ),
183+ lambda p : p .is_single or p .is_predicate )
184+ renderer .render (ql .ClassTester (class_name = c .name ,
185+ properties = total_props ), test_dir / f"{ c .name } .ql" )
186+ for p in partial_props :
187+ renderer .render (ql .PropertyTester (class_name = c .name ,
188+ property = p ), test_dir / f"{ c .name } _{ p .getter } .ql" )
122189
123190 renderer .cleanup (existing )
124191 if opts .ql_format :
0 commit comments