Skip to content

Commit a2b7fcc

Browse files
committed
Rust: Auto-generate CfgNodes.qll
1 parent f8058e4 commit a2b7fcc

File tree

15 files changed

+3582
-59
lines changed

15 files changed

+3582
-59
lines changed

misc/codegen/codegen.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ def _parse_args() -> argparse.Namespace:
6464
help="registry file containing information about checked-in generated code. A .gitattributes"
6565
"file is generated besides it to mark those files with linguist-generated=true. Must"
6666
"be in a directory containing all generated code."),
67+
p.add_argument("--ql-cfg-output",
68+
help="output directory for QL CFG layer (optional)."),
69+
6770
]
6871
p.add_argument("--script-name",
6972
help="script name to put in header comments of generated files. By default, the path of this "

misc/codegen/generators/qlgen.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ def get_ql_property(cls: schema.Class, prop: schema.Property, lookup: typing.Dic
117117
synth=bool(cls.synth) or prop.synth,
118118
type_is_hideable="ql_hideable" in lookup[prop.type].pragmas if prop.type in lookup else False,
119119
internal="ql_internal" in prop.pragmas,
120+
# is_child=prop.is_child,
120121
)
121122
if prop.is_single:
122123
args.update(
@@ -160,6 +161,8 @@ def get_ql_class(cls: schema.Class, lookup: typing.Dict[str, schema.Class]) -> q
160161
prop = get_ql_property(cls, p, lookup, prev_child)
161162
if prop.is_child:
162163
prev_child = prop.singular
164+
if lookup[prop.type].cfg:
165+
prop.cfg = True
163166
properties.append(prop)
164167
return ql.Class(
165168
name=cls.name,
@@ -171,6 +174,15 @@ def get_ql_class(cls: schema.Class, lookup: typing.Dict[str, schema.Class]) -> q
171174
doc=cls.doc,
172175
hideable="ql_hideable" in cls.pragmas,
173176
internal="ql_internal" in cls.pragmas,
177+
cfg=cls.cfg,
178+
)
179+
180+
def get_ql_cfg_class(cls: schema.Class, lookup: typing.Dict[str, ql.Class]) -> ql.CfgClass:
181+
return ql.CfgClass(
182+
name=cls.name,
183+
bases=[base for base in cls.bases if lookup[base.base].cfg],
184+
properties=cls.properties,
185+
doc=cls.doc
174186
)
175187

176188

@@ -361,6 +373,7 @@ def generate(opts, renderer):
361373
input = opts.schema
362374
out = opts.ql_output
363375
stub_out = opts.ql_stub_output
376+
cfg_out = opts.ql_cfg_output
364377
test_out = opts.ql_test_output
365378
missing_test_source_filename = "MISSING_SOURCE.txt"
366379
include_file = stub_out.with_suffix(".qll")
@@ -385,6 +398,7 @@ def generate(opts, renderer):
385398
imports = {}
386399
imports_impl = {}
387400
classes_used_by = {}
401+
cfg_classes = {}
388402
generated_import_prefix = get_import(out, opts.root_dir)
389403
registry = opts.generated_registry or pathlib.Path(
390404
os.path.commonpath((out, stub_out, test_out)), ".generated.list")
@@ -402,7 +416,9 @@ def generate(opts, renderer):
402416
imports[c.name] = path
403417
path_impl = get_import(stub_out / c.dir / "internal" / c.name, opts.root_dir)
404418
imports_impl[c.name + "Impl"] = path_impl + "Impl"
405-
419+
if c.cfg:
420+
cfg_classes[c.name] = get_ql_cfg_class(c, classes)
421+
406422
for c in classes.values():
407423
qll = out / c.path.with_suffix(".qll")
408424
c.imports = [imports[t] if t in imports else imports_impl[t] +
@@ -411,6 +427,14 @@ def generate(opts, renderer):
411427
c.import_prefix = generated_import_prefix
412428
renderer.render(c, qll)
413429

430+
if cfg_out:
431+
cfg_classes_val = ql.CfgClasses(
432+
include_file_import = get_import(include_file, opts.root_dir),
433+
classes=list(cfg_classes.values())
434+
)
435+
cfg_qll = cfg_out / "CfgNodes.qll"
436+
renderer.render(cfg_classes_val, cfg_qll)
437+
414438
for c in data.classes.values():
415439
path = _get_path(c)
416440
path_impl = _get_path_impl(c)

misc/codegen/lib/ql.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ class Property:
4545
synth: bool = False
4646
type_is_hideable: bool = False
4747
internal: bool = False
48+
cfg: bool = False
4849

4950
def __post_init__(self):
5051
if self.tableparams:
@@ -110,6 +111,7 @@ class Class:
110111
internal: bool = False
111112
doc: List[str] = field(default_factory=list)
112113
hideable: bool = False
114+
cfg: bool = False
113115

114116
def __post_init__(self):
115117
def get_bases(bases): return [Base(str(b), str(prev)) for b, prev in zip(bases, itertools.chain([""], bases))]
@@ -333,3 +335,53 @@ class ConstructorStub:
333335

334336
cls: "Synth.FinalClass"
335337
import_prefix: str
338+
339+
@dataclass
340+
class CfgClass:
341+
name: str
342+
bases: List[Base] = field(default_factory=list)
343+
# bases_impl: List[Base] = field(default_factory=list)
344+
# final: bool = False
345+
properties: List[Property] = field(default_factory=list)
346+
# dir: pathlib.Path = pathlib.Path()
347+
# imports: List[str] = field(default_factory=list)
348+
# import_prefix: Optional[str] = None
349+
# internal: bool = False
350+
doc: List[str] = field(default_factory=list)
351+
# hideable: bool = False
352+
353+
def __post_init__(self):
354+
def get_bases(bases): return [Base(str(b), str(prev)) for b, prev in zip(bases, itertools.chain([""], bases))]
355+
self.bases = get_bases(self.bases)
356+
# self.bases_impl = get_bases(self.bases_impl)
357+
if self.properties:
358+
self.properties[0].first = True
359+
360+
@property
361+
def root(self) -> bool:
362+
return not self.bases
363+
364+
# @property
365+
# def path(self) -> pathlib.Path:
366+
# return self.dir / self.name
367+
368+
# @property
369+
# def db_id(self) -> str:
370+
# return "@" + inflection.underscore(self.name)
371+
372+
# @property
373+
# def has_children(self) -> bool:
374+
# return any(p.is_child for p in self.properties)
375+
376+
@property
377+
def last_base(self) -> str:
378+
return self.bases[-1].base if self.bases else ""
379+
380+
@dataclass
381+
class CfgClasses:
382+
template: ClassVar = 'ql_cfg_nodes'
383+
384+
include_file_import: Optional[str] = None
385+
386+
classes: List[CfgClass] = field(default_factory=list)
387+

misc/codegen/lib/schema.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ class Class:
9494
properties: List[Property] = field(default_factory=list)
9595
pragmas: List[str] | Dict[str, object] = field(default_factory=dict)
9696
doc: List[str] = field(default_factory=list)
97+
cfg: bool = False
9798

9899
def __post_init__(self):
99100
if not isinstance(self.pragmas, dict):

misc/codegen/lib/schemadefs.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,7 @@ def __or__(self, other: _schema.PropertyModifier):
279279
drop = object()
280280

281281

282-
def annotate(annotated_cls: type, add_bases: _Iterable[type] | None = None, replace_bases: _Dict[type, type] | None = None) -> _Callable[[type], _PropertyAnnotation]:
282+
def annotate(annotated_cls: type, add_bases: _Iterable[type] | None = None, replace_bases: _Dict[type, type] | None = None, cfg: bool = False) -> _Callable[[type], _PropertyAnnotation]:
283283
"""
284284
Add or modify schema annotations after a class has been defined previously.
285285
@@ -298,6 +298,7 @@ def decorator(cls: type) -> _PropertyAnnotation:
298298
annotated_cls.__bases__ = tuple(replace_bases.get(b, b) for b in annotated_cls.__bases__)
299299
if add_bases:
300300
annotated_cls.__bases__ += tuple(add_bases)
301+
annotated_cls.__cfg__ = cfg
301302
for a in dir(cls):
302303
if a.startswith(_schema.inheritable_pragma_prefix):
303304
setattr(annotated_cls, a, getattr(cls, a))

misc/codegen/loaders/schemaloader.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,14 @@ def _get_class(cls: type) -> schema.Class:
4949
derived = {d.__name__ for d in cls.__subclasses__()}
5050
if "null" in pragmas and derived:
5151
raise schema.Error(f"Null class cannot be derived")
52+
cfg = False
53+
if hasattr(cls, "__cfg__"):
54+
cfg = cls.__cfg__
5255
return schema.Class(name=cls.__name__,
5356
bases=[b.__name__ for b in cls.__bases__ if b is not object],
5457
derived=derived,
5558
pragmas=pragmas,
59+
cfg=cfg, # todo?
5660
# in the following we don't use `getattr` to avoid inheriting
5761
properties=[
5862
a | _PropertyNamer(n)
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
// generated by {{generator}}, do not edit
2+
/**
3+
* This module provides generated wrappers around the `CfgNode` type.
4+
*
5+
* INTERNAL: Do not import directly.
6+
*/
7+
8+
private import codeql.util.Location
9+
private import {{include_file_import}}
10+
11+
/** Provides the input to `MakeCfgNodes` */
12+
signature module InputSig<LocationSig Loc> {
13+
class CfgNode {
14+
AstNode getAstNode();
15+
16+
string toString();
17+
18+
Loc getLocation();
19+
}
20+
21+
AstNode getDesugared(AstNode n);
22+
}
23+
24+
/**
25+
* Given a `CfgNode` implementation, provides the module `Nodes` that
26+
* contains wrappers around `CfgNode` for relevant classes.
27+
*/
28+
module MakeCfgNodes<LocationSig Loc, InputSig<Loc> Input> {
29+
private import Input
30+
31+
final private class AstNodeFinal = AstNode;
32+
33+
final private class CfgNodeFinal = CfgNode;
34+
35+
/**
36+
* INTERNAL: Do not expose.
37+
*/
38+
abstract class ChildMapping extends AstNodeFinal {
39+
/**
40+
* Holds if `child` is a (possibly nested) child of this AST node
41+
* for which we would like to find a matching CFG child.
42+
*/
43+
abstract predicate relevantChild(AstNode child);
44+
45+
/**
46+
* Holds if there is a control-flow path from `cfn` to `cfnChild`, where `cfn`
47+
* is a control-flow node for this AST node, and `cfnChild` is a control-flow
48+
* node for `child`.
49+
*
50+
* This predicate should be implemented at the place where `MakeCfgNodes` is
51+
* invoked.
52+
*/
53+
cached
54+
predicate hasCfgChild(AstNode child, CfgNode cfn, CfgNode cfnChild) { none() }
55+
}
56+
57+
/** Provides sub classes of `CfgNode`. */
58+
module Nodes {
59+
{{#classes}}
60+
private final class {{name}}ChildMapping extends ChildMapping, {{name}} {
61+
override predicate relevantChild(AstNode child) {
62+
none()
63+
{{#properties}}
64+
{{#cfg}}
65+
or
66+
child = this.{{getter}}({{#is_indexed}}_{{/is_indexed}})
67+
{{/cfg}}
68+
{{/properties}}
69+
}
70+
}
71+
72+
/**
73+
{{#doc}}
74+
* {{.}}
75+
{{/doc}}
76+
*/
77+
final class {{name}}CfgNode extends CfgNodeFinal{{#bases}}, {{.}}CfgNode{{/bases}} {
78+
private {{name}}ChildMapping node;
79+
80+
{{name}}CfgNode() {
81+
node = this.getAstNode()
82+
}
83+
84+
/** Gets the underlying `{{name}}`. */
85+
{{name}} get{{name}}() { result = node }
86+
87+
{{#properties}}
88+
/**
89+
* {{>ql_property_doc}} *
90+
{{#description}}
91+
* {{.}}
92+
{{/description}}
93+
{{#internal}}
94+
* INTERNAL: Do not use.
95+
{{/internal}}
96+
*/
97+
{{type}}{{#cfg}}CfgNode{{/cfg}} {{getter}}({{#is_indexed}}int index{{/is_indexed}}) {
98+
{{#cfg}}
99+
node.hasCfgChild(node.{{getter}}({{#is_indexed}}index{{/is_indexed}}), this, result)
100+
{{/cfg}}
101+
{{^cfg}}
102+
{{^is_predicate}}result = {{/is_predicate}}node.{{getter}}({{#is_indexed}}index{{/is_indexed}})
103+
{{/cfg}}
104+
}
105+
106+
{{#is_optional}}
107+
/**
108+
* Holds if `{{getter}}({{#is_repeated}}index{{/is_repeated}})` exists.
109+
{{#internal}}
110+
* INTERNAL: Do not use.
111+
{{/internal}}
112+
*/
113+
predicate has{{singular}}({{#is_repeated}}int index{{/is_repeated}}) {
114+
exists(this.{{getter}}({{#is_repeated}}index{{/is_repeated}}))
115+
}
116+
{{/is_optional}}
117+
{{#is_indexed}}
118+
119+
/**
120+
* Gets any of the {{doc_plural}}.
121+
{{#internal}}
122+
* INTERNAL: Do not use.
123+
{{/internal}}
124+
*/
125+
{{type}}{{#cfg}}CfgNode{{/cfg}} {{indefinite_getter}}() {
126+
result = this.{{getter}}(_)
127+
}
128+
{{^is_optional}}
129+
130+
/**
131+
* Gets the number of {{doc_plural}}.
132+
{{#internal}}
133+
* INTERNAL: Do not use.
134+
{{/internal}}
135+
*/
136+
int getNumberOf{{plural}}() {
137+
result = count(int i | exists(this.{{getter}}(i)))
138+
}
139+
{{/is_optional}}
140+
{{/is_indexed}}
141+
{{#is_unordered}}
142+
/**
143+
* Gets the number of {{doc_plural}}.
144+
{{#internal}}
145+
* INTERNAL: Do not use.
146+
{{/internal}}
147+
*/
148+
int getNumberOf{{plural}}() {
149+
result = count(this.{{getter}}())
150+
}
151+
{{/is_unordered}}
152+
{{/properties}}
153+
}
154+
{{/classes}}
155+
}
156+
157+
module Consistency {
158+
private predicate hasCfgNode(AstNode astNode) {
159+
astNode = any(CfgNode cfgNode).getAstNode()
160+
}
161+
162+
query predicate missingCfgChild(CfgNode parent, string pred, int child) {
163+
none()
164+
{{#classes}}
165+
{{#properties}}
166+
{{#cfg}}
167+
or
168+
pred = "{{getter}}" and
169+
parent = any(Nodes::{{name}}CfgNode cfgNode, {{name}} astNode, {{type}} res |
170+
astNode = cfgNode.get{{name}}() and
171+
res = getDesugared(astNode.{{getter}}({{#is_indexed}}child{{/is_indexed}}))
172+
{{^is_indexed}}and child = -1{{/is_indexed}} and
173+
hasCfgNode(res) and
174+
not res = cfgNode.{{getter}}({{#is_indexed}}child{{/is_indexed}}).getAstNode()
175+
|
176+
cfgNode
177+
)
178+
{{/cfg}}
179+
{{/properties}}
180+
{{/classes}}
181+
}
182+
}
183+
}

rust/codegen.conf

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
--dbscheme=ql/lib/rust.dbscheme
55
--ql-output=ql/lib/codeql/rust/elements/internal/generated
66
--ql-stub-output=ql/lib/codeql/rust/elements
7+
--ql-cfg-output=ql/lib/codeql/rust/controlflow/internal/generated
78
--ql-test-output=ql/test/extractor-tests/generated
89
--rust-output=extractor/src/generated
910
--script-name=codegen

rust/ql/.generated.list

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rust/ql/.gitattributes

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)