Skip to content

Commit 295626d

Browse files
authored
Merge pull request #17918 from hvitved/rust/cfg-codegen
Rust: Add (auto-generated) CFG node wrapper classes
2 parents 6dc599c + 86a7c48 commit 295626d

File tree

29 files changed

+5008
-616
lines changed

29 files changed

+5008
-616
lines changed

misc/codegen/codegen.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ def _parse_args() -> argparse.Namespace:
5454
"generated qll file importing every class file"),
5555
p.add_argument("--ql-test-output",
5656
help="output directory for QL generated extractor test files"),
57+
p.add_argument("--ql-cfg-output",
58+
help="output directory for QL CFG layer (optional)."),
5759
p.add_argument("--cpp-output",
5860
help="output directory for generated C++ files, required if trap or cpp is provided to "
5961
"--generate"),

misc/codegen/generators/qlgen.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,8 @@ def get_ql_class(cls: schema.Class, lookup: typing.Dict[str, schema.Class]) -> q
160160
prop = get_ql_property(cls, p, lookup, prev_child)
161161
if prop.is_child:
162162
prev_child = prop.singular
163+
if prop.type in lookup and lookup[prop.type].cfg:
164+
prop.cfg = True
163165
properties.append(prop)
164166
return ql.Class(
165167
name=cls.name,
@@ -171,6 +173,16 @@ def get_ql_class(cls: schema.Class, lookup: typing.Dict[str, schema.Class]) -> q
171173
doc=cls.doc,
172174
hideable="ql_hideable" in cls.pragmas,
173175
internal="ql_internal" in cls.pragmas,
176+
cfg=cls.cfg,
177+
)
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,6 +416,8 @@ 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"
419+
if c.cfg:
420+
cfg_classes.append(get_ql_cfg_class(c, classes))
405421

406422
for c in classes.values():
407423
qll = out / c.path.with_suffix(".qll")
@@ -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=cfg_classes
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: 17 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,18 @@ class ConstructorStub:
333335

334336
cls: "Synth.FinalClass"
335337
import_prefix: str
338+
339+
340+
@dataclass
341+
class CfgClass:
342+
name: str
343+
bases: List[Base] = field(default_factory=list)
344+
properties: List[Property] = field(default_factory=list)
345+
doc: List[str] = field(default_factory=list)
346+
347+
348+
@dataclass
349+
class CfgClasses:
350+
template: ClassVar = 'ql_cfg_nodes'
351+
include_file_import: Optional[str] = None
352+
classes: List[CfgClass] = field(default_factory=list)

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

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)