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 ),
2032 tableparams = ["this" ] + ["result" if p is prop else "_" for p in cls .properties if p .is_single ],
21- is_child = prop .is_child ,
2233 )
2334 elif prop .is_repeated :
2435 return ql .Property (
36+ ** common_args ,
2537 singular = inflection .singularize (inflection .camelize (prop .name )),
2638 plural = inflection .pluralize (inflection .camelize (prop .name )),
27- type = prop .type ,
2839 tablename = inflection .tableize (f"{ cls .name } _{ prop .name } " ),
2940 tableparams = ["this" , "index" , "result" ],
30- is_optional = prop .is_optional ,
31- is_child = prop .is_child ,
3241 )
3342 elif prop .is_optional :
3443 return ql .Property (
44+ ** common_args ,
3545 singular = inflection .camelize (prop .name ),
36- type = prop .type ,
3746 tablename = inflection .tableize (f"{ cls .name } _{ prop .name } " ),
3847 tableparams = ["this" , "result" ],
39- is_optional = True ,
40- is_child = prop .is_child ,
4148 )
4249 elif prop .is_predicate :
4350 return ql .Property (
51+ ** common_args ,
4452 singular = inflection .camelize (prop .name , uppercase_first_letter = False ),
45- type = "predicate" ,
4653 tablename = inflection .underscore (f"{ cls .name } _{ prop .name } " ),
4754 tableparams = ["this" ],
48- is_predicate = True ,
4955 )
5056
5157
@@ -77,38 +83,75 @@ def get_classes_used_by(cls: ql.Class):
7783
7884def is_generated (file ):
7985 with open (file ) as contents :
80- return next (contents ).startswith ("// generated" )
86+ for line in contents :
87+ return line .startswith ("// generated" )
88+ return False
8189
8290
8391def format (codeql , files ):
8492 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 )
93+ format_cmd .extend (str (f ) for f in files if f .suffix in (".qll" , ".ql" ))
94+ res = subprocess .run (format_cmd , stderr = subprocess .PIPE , text = True )
95+ if res .returncode :
96+ for line in res .stderr .splitlines ():
97+ log .error (line .strip ())
98+ raise FormatError ("QL format failed" )
8799 for line in res .stderr .splitlines ():
88100 log .debug (line .strip ())
89101
90102
103+ def _get_all_properties (cls : ql .Class , lookup : typing .Dict [str , ql .Class ]) -> typing .Iterable [ql .Property ]:
104+ for b in cls .bases :
105+ for p in _get_all_properties (lookup [b ], lookup ):
106+ yield p
107+ for p in cls .properties :
108+ yield p
109+
110+
111+ def _get_all_properties_to_be_tested (cls : ql .Class , lookup : typing .Dict [str , ql .Class ]) -> typing .Iterable [
112+ ql .PropertyForTest ]:
113+ # deduplicate using id
114+ already_seen = set ()
115+ for p in _get_all_properties (cls , lookup ):
116+ if not p .skip_qltest and id (p ) not in already_seen :
117+ already_seen .add (id (p ))
118+ yield ql .PropertyForTest (p .getter , p .type , p .is_single , p .is_predicate , p .is_repeated )
119+
120+
121+ def _partition (l , pred ):
122+ """ partitions a list according to boolean predicate """
123+ res = ([], [])
124+ for x in l :
125+ res [not pred (x )].append (x )
126+ return res
127+
128+
91129def generate (opts , renderer ):
92130 input = opts .schema
93131 out = opts .ql_output
94132 stub_out = opts .ql_stub_output
133+ test_out = opts .ql_test_output
134+ missing_test_source_filename = "MISSING_SOURCE.txt"
95135 existing = {q for q in out .rglob ("*.qll" )}
96136 existing |= {q for q in stub_out .rglob ("*.qll" ) if is_generated (q )}
137+ existing |= {q for q in test_out .rglob ("*.ql" )}
138+ existing |= {q for q in test_out .rglob (missing_test_source_filename )}
97139
98140 data = schema .load (input )
99141
100142 classes = [get_ql_class (cls ) for cls in data .classes ]
101- classes .sort (key = lambda cls : cls .name )
143+ lookup = {cls .name : cls for cls in classes }
144+ classes .sort (key = lambda cls : (cls .dir , cls .name ))
102145 imports = {}
103146
104147 for c in classes :
105148 imports [c .name ] = get_import (stub_out / c .path , opts .swift_dir )
106149
107150 for c in classes :
108- qll = ( out / c .path ) .with_suffix (".qll" )
151+ qll = out / c .path .with_suffix (".qll" )
109152 c .imports = [imports [t ] for t in get_classes_used_by (c )]
110153 renderer .render (c , qll )
111- stub_file = ( stub_out / c .path ) .with_suffix (".qll" )
154+ stub_file = stub_out / c .path .with_suffix (".qll" )
112155 if not stub_file .is_file () or is_generated (stub_file ):
113156 stub = ql .Stub (name = c .name , base_import = get_import (qll , opts .swift_dir ))
114157 renderer .render (stub , stub_file )
@@ -120,6 +163,21 @@ def generate(opts, renderer):
120163
121164 renderer .render (ql .GetParentImplementation (classes ), out / 'GetImmediateParent.qll' )
122165
166+ for c in classes :
167+ if not c .final :
168+ continue
169+ test_dir = test_out / c .path
170+ test_dir .mkdir (parents = True , exist_ok = True )
171+ if not any (test_dir .glob ("*.swift" )):
172+ log .warning (f"no test source in { c .path } " )
173+ renderer .render (ql .MissingTestInstructions (), test_dir / missing_test_source_filename )
174+ continue
175+ total_props , partial_props = _partition (_get_all_properties_to_be_tested (c , lookup ),
176+ lambda p : p .is_single or p .is_predicate )
177+ renderer .render (ql .ClassTester (class_name = c .name , properties = total_props ), test_dir / f"{ c .name } .ql" )
178+ for p in partial_props :
179+ renderer .render (ql .PropertyTester (class_name = c .name , property = p ), test_dir / f"{ c .name } _{ p .getter } .ql" )
180+
123181 renderer .cleanup (existing )
124182 if opts .ql_format :
125183 format (opts .codeql_binary , renderer .written )
0 commit comments