22QL code generation
33
44`generate(opts, renderer)` will generate in the library directory:
5- * generated/Raw.qll with thin class wrappers around DB types
6- * generated/Synth.qll with the base algebraic datatypes for AST entities
7- * generated/<group>/<Class>.qll with generated properties for each class
8- * if not already modified, a elements/<group>/<Class>.qll stub to customize the above classes
9- * elements.qll importing all the above stubs
10- * if not already modified, a elements/<group>/<Class>Constructor.qll stub to customize the algebraic datatype
5+ * `generated/Raw.qll` with thin class wrappers around DB types
6+ * `generated/Synth.qll` with the base algebraic datatypes for AST entities
7+ * `generated/<group>/<Class>.qll` with generated properties for each class
8+ * if not already modified, an `elements/<group>/<Class>Impl.qll` stub to customize the above classes
9+ * `elements/<group>/<Class>.qll` that wraps the internal `<Class>Impl.qll` file in a public `final` class.
10+ * `elements.qll` importing all the above public classes
11+ * if not already modified, an `elements/<group>/<Class>Constructor.qll` stub to customize the algebraic datatype
1112 characteristic predicate
12- * generated/SynthConstructors.qll importing all the above constructor stubs
13- * generated/PureSynthConstructors.qll importing constructor stubs for pure synthesized types (that is, not
13+ * ` generated/SynthConstructors.qll` importing all the above constructor stubs
14+ * ` generated/PureSynthConstructors.qll` importing constructor stubs for pure synthesized types (that is, not
1415 corresponding to raw types)
1516Moreover in the test directory for each <Class> in <group> it will generate beneath the
16- extractor-tests/generated/<group>/<Class> directory either
17+ ` extractor-tests/generated/<group>/<Class>` directory either
1718 * a `MISSING_SOURCE.txt` explanation file if no source is present, or
1819 * one `<Class>.ql` test query for all single properties and on `<Class>_<property>.ql` test query for each optional or
1920 repeated property
@@ -164,6 +165,7 @@ def get_ql_class(cls: schema.Class, lookup: typing.Dict[str, schema.Class]) -> q
164165 return ql .Class (
165166 name = cls .name ,
166167 bases = cls .bases ,
168+ bases_impl = [base + "Impl::" + base for base in cls .bases ],
167169 final = not cls .derived ,
168170 properties = properties ,
169171 dir = pathlib .Path (cls .group or "" ),
@@ -210,15 +212,17 @@ def get_import(file: pathlib.Path, root_dir: pathlib.Path):
210212 return str (stem ).replace ("/" , "." )
211213
212214
213- def get_types_used_by (cls : ql .Class ) -> typing .Iterable [str ]:
215+ def get_types_used_by (cls : ql .Class , is_impl : bool ) -> typing .Iterable [str ]:
214216 for b in cls .bases :
215- yield b .base
217+ yield b .base + "Impl" if is_impl else b . base
216218 for p in cls .properties :
217219 yield p .type
220+ if cls .root :
221+ yield cls .name # used in `getResolveStep` and `resolve`
218222
219223
220- def get_classes_used_by (cls : ql .Class ) -> typing .List [str ]:
221- return sorted (set (t for t in get_types_used_by (cls ) if t [0 ].isupper () and t != cls .name ))
224+ def get_classes_used_by (cls : ql .Class , is_impl : bool ) -> typing .List [str ]:
225+ return sorted (set (t for t in get_types_used_by (cls , is_impl ) if t [0 ].isupper () and ( is_impl or t != cls .name ) ))
222226
223227
224228def format (codeql , files ):
@@ -239,6 +243,10 @@ def _get_path(cls: schema.Class) -> pathlib.Path:
239243 return pathlib .Path (cls .group or "" , cls .name ).with_suffix (".qll" )
240244
241245
246+ def _get_path_impl (cls : schema .Class ) -> pathlib .Path :
247+ return pathlib .Path (cls .group or "" , cls .name + "Impl" ).with_suffix (".qll" )
248+
249+
242250def _get_all_properties (cls : schema .Class , lookup : typing .Dict [str , schema .Class ],
243251 already_seen : typing .Optional [typing .Set [int ]] = None ) -> \
244252 typing .Iterable [typing .Tuple [schema .Class , schema .Property ]]:
@@ -315,11 +323,14 @@ def _get_stub(cls: schema.Class, base_import: str, generated_import_prefix: str)
315323 else :
316324 accessors = []
317325 return ql .Stub (name = cls .name , base_import = base_import , import_prefix = generated_import_prefix ,
318- doc = cls .doc , synth_accessors = accessors ,
319- internal = "ql_internal" in cls . pragmas )
326+ doc = cls .doc , synth_accessors = accessors )
327+
320328
329+ def _get_class_public (cls : schema .Class ) -> ql .ClassPublic :
330+ return ql .ClassPublic (name = cls .name , doc = cls .doc , internal = "ql_internal" in cls .pragmas )
321331
322- _stub_qldoc_header = "// the following QLdoc is generated: if you need to edit it, do it in the schema file\n "
332+
333+ _stub_qldoc_header = "// the following QLdoc is generated: if you need to edit it, do it in the schema file\n "
323334
324335_class_qldoc_re = re .compile (
325336 rf"(?P<qldoc>(?:{ re .escape (_stub_qldoc_header )} )?/\*\*.*?\*/\s*|^\s*)(?:class\s+(?P<class>\w+))?" ,
@@ -330,13 +341,13 @@ def _patch_class_qldoc(cls: str, qldoc: str, stub_file: pathlib.Path):
330341 """ Replace or insert `qldoc` as the QLdoc of class `cls` in `stub_file` """
331342 if not qldoc or not stub_file .exists ():
332343 return
333- qldoc = "\n " .join (l .rstrip () for l in qldoc .splitlines ())
344+ qldoc = "\n " .join (l .rstrip () for l in qldoc .splitlines ())
334345 with open (stub_file ) as input :
335346 contents = input .read ()
336347 for match in _class_qldoc_re .finditer (contents ):
337348 if match ["class" ] == cls :
338349 qldoc_start , qldoc_end = match .span ("qldoc" )
339- contents = f"{ contents [:qldoc_start ]} { _stub_qldoc_header } { qldoc } \n { contents [qldoc_end :]} "
350+ contents = f"{ contents [:qldoc_start ]} { _stub_qldoc_header } { qldoc } \n { contents [qldoc_end :]} "
340351 tmp = stub_file .with_suffix (f"{ stub_file .suffix } .bkp" )
341352 with open (tmp , "w" ) as out :
342353 out .write (contents )
@@ -370,6 +381,8 @@ def generate(opts, renderer):
370381 raise RootElementHasChildren (root )
371382
372383 imports = {}
384+ imports_impl = {}
385+ classes_used_by = {}
373386 generated_import_prefix = get_import (out , opts .root_dir )
374387 registry = opts .generated_registry or pathlib .Path (
375388 os .path .commonpath ((out , stub_out , test_out )), ".generated.list" )
@@ -382,24 +395,34 @@ def generate(opts, renderer):
382395
383396 classes_by_dir_and_name = sorted (classes .values (), key = lambda cls : (cls .dir , cls .name ))
384397 for c in classes_by_dir_and_name :
385- imports [c .name ] = get_import (stub_out / c .path , opts .root_dir )
398+ path = get_import (stub_out / c .path , opts .root_dir )
399+ imports [c .name ] = path
400+ imports_impl [c .name + "Impl" ] = path + "Impl"
386401
387402 for c in classes .values ():
388403 qll = out / c .path .with_suffix (".qll" )
389- c .imports = [imports [t ] for t in get_classes_used_by (c )]
404+ c .imports = [imports [t ] if t in imports else imports_impl [t ] +
405+ "::Impl as " + t for t in get_classes_used_by (c , is_impl = True )]
406+ classes_used_by [c .name ] = get_classes_used_by (c , is_impl = False )
390407 c .import_prefix = generated_import_prefix
391408 renderer .render (c , qll )
392409
393410 for c in data .classes .values ():
394411 path = _get_path (c )
395- stub_file = stub_out / path
412+ path_impl = _get_path_impl (c )
413+ stub_file = stub_out / path_impl
396414 base_import = get_import (out / path , opts .root_dir )
397415 stub = _get_stub (c , base_import , generated_import_prefix )
416+
398417 if not renderer .is_customized_stub (stub_file ):
399418 renderer .render (stub , stub_file )
400419 else :
401420 qldoc = renderer .render_str (stub , template = 'ql_stub_class_qldoc' )
402421 _patch_class_qldoc (c .name , qldoc , stub_file )
422+ class_public = _get_class_public (c )
423+ class_public_file = stub_out / path
424+ class_public .imports = [imports [t ] for t in classes_used_by [c .name ]]
425+ renderer .render (class_public , class_public_file )
403426
404427 # for example path/to/elements -> path/to/elements.qll
405428 renderer .render (ql .ImportList ([i for name , i in imports .items () if not classes [name ].internal ]),
0 commit comments