Skip to content

Commit 10d8f77

Browse files
committed
Rust: Auto-generate CfgNodes.qll
1 parent fc8d8bb commit 10d8f77

File tree

16 files changed

+3629
-60
lines changed

16 files changed

+3629
-60
lines changed

misc/codegen/codegen.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ 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)."),
6769
]
6870
p.add_argument("--script-name",
6971
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
@@ -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 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,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.append(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=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: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
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+
predicate normalEvaluation(AstNode n);
24+
}
25+
26+
/**
27+
* Given a `CfgNode` implementation, provides the module `Nodes` that
28+
* contains wrappers around `CfgNode` for relevant classes.
29+
*/
30+
module MakeCfgNodes<LocationSig Loc, InputSig<Loc> Input> {
31+
private import Input
32+
33+
final private class AstNodeFinal = AstNode;
34+
35+
final private class CfgNodeFinal = CfgNode;
36+
37+
/**
38+
* INTERNAL: Do not expose.
39+
*/
40+
abstract class ChildMapping extends AstNodeFinal {
41+
/**
42+
* Holds if `child` is a (possibly nested) child of this AST node
43+
* for which we would like to find a matching CFG child.
44+
*/
45+
abstract predicate relevantChild(AstNode child);
46+
47+
/**
48+
* Holds if there is a control-flow path from `cfn` to `cfnChild`, where `cfn`
49+
* is a control-flow node for this AST node, and `cfnChild` is a control-flow
50+
* node for `child`.
51+
*
52+
* This predicate should be implemented at the place where `MakeCfgNodes` is
53+
* invoked.
54+
*/
55+
cached
56+
predicate hasCfgChild(AstNode child, CfgNode cfn, CfgNode cfnChild) { none() }
57+
}
58+
59+
/** Provides sub classes of `CfgNode`. */
60+
module Nodes {
61+
{{#classes}}
62+
private final class {{name}}ChildMapping extends ChildMapping, {{name}} {
63+
override predicate relevantChild(AstNode child) {
64+
none()
65+
{{#properties}}
66+
{{#cfg}}
67+
or
68+
child = this.{{getter}}({{#is_indexed}}_{{/is_indexed}})
69+
{{/cfg}}
70+
{{/properties}}
71+
}
72+
}
73+
74+
/**
75+
{{#doc}}
76+
* {{.}}
77+
{{/doc}}
78+
*/
79+
final class {{name}}CfgNode extends CfgNodeFinal{{#bases}}, {{.}}CfgNode{{/bases}} {
80+
private {{name}}ChildMapping node;
81+
82+
{{name}}CfgNode() {
83+
node = this.getAstNode()
84+
}
85+
86+
/** Gets the underlying `{{name}}`. */
87+
{{name}} get{{name}}() { result = node }
88+
89+
{{#properties}}
90+
/**
91+
* {{>ql_property_doc}} *
92+
{{#description}}
93+
* {{.}}
94+
{{/description}}
95+
{{#internal}}
96+
* INTERNAL: Do not use.
97+
{{/internal}}
98+
*/
99+
{{type}}{{#cfg}}CfgNode{{/cfg}} {{getter}}({{#is_indexed}}int index{{/is_indexed}}) {
100+
{{#cfg}}
101+
node.hasCfgChild(node.{{getter}}({{#is_indexed}}index{{/is_indexed}}), this, result)
102+
{{/cfg}}
103+
{{^cfg}}
104+
{{^is_predicate}}result = {{/is_predicate}}node.{{getter}}({{#is_indexed}}index{{/is_indexed}})
105+
{{/cfg}}
106+
}
107+
108+
{{#is_optional}}
109+
/**
110+
* Holds if `{{getter}}({{#is_repeated}}index{{/is_repeated}})` exists.
111+
{{#internal}}
112+
* INTERNAL: Do not use.
113+
{{/internal}}
114+
*/
115+
predicate has{{singular}}({{#is_repeated}}int index{{/is_repeated}}) {
116+
exists(this.{{getter}}({{#is_repeated}}index{{/is_repeated}}))
117+
}
118+
{{/is_optional}}
119+
{{#is_indexed}}
120+
121+
/**
122+
* Gets any of the {{doc_plural}}.
123+
{{#internal}}
124+
* INTERNAL: Do not use.
125+
{{/internal}}
126+
*/
127+
{{type}}{{#cfg}}CfgNode{{/cfg}} {{indefinite_getter}}() {
128+
result = this.{{getter}}(_)
129+
}
130+
{{^is_optional}}
131+
132+
/**
133+
* Gets the number of {{doc_plural}}.
134+
{{#internal}}
135+
* INTERNAL: Do not use.
136+
{{/internal}}
137+
*/
138+
int getNumberOf{{plural}}() {
139+
result = count(int i | exists(this.{{getter}}(i)))
140+
}
141+
{{/is_optional}}
142+
{{/is_indexed}}
143+
{{#is_unordered}}
144+
/**
145+
* Gets the number of {{doc_plural}}.
146+
{{#internal}}
147+
* INTERNAL: Do not use.
148+
{{/internal}}
149+
*/
150+
int getNumberOf{{plural}}() {
151+
result = count(this.{{getter}}())
152+
}
153+
{{/is_unordered}}
154+
{{/properties}}
155+
}
156+
{{/classes}}
157+
}
158+
159+
module Consistency {
160+
query predicate missingCfgChild(CfgNode parent, string pred, int child) {
161+
none()
162+
{{#classes}}
163+
{{#properties}}
164+
{{#cfg}}
165+
or
166+
pred = "{{getter}}" and
167+
parent = any(Nodes::{{name}}CfgNode cfgNode, {{name}} astNode, {{type}} res |
168+
astNode = cfgNode.get{{name}}() and
169+
res = getDesugared(astNode.{{getter}}({{#is_indexed}}child{{/is_indexed}}))
170+
{{^is_indexed}}and child = -1{{/is_indexed}} and
171+
normalEvaluation(res) and
172+
not res = cfgNode.{{getter}}({{#is_indexed}}child{{/is_indexed}}).getAstNode()
173+
|
174+
cfgNode
175+
)
176+
{{/cfg}}
177+
{{/properties}}
178+
{{/classes}}
179+
}
180+
}
181+
}

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)