Skip to content

Commit f5471e4

Browse files
authored
Merge pull request #10211 from github/redsun82/swift-generate-printast-first-step
Swift: generate indexed parent/child relationship
2 parents 8f0b999 + 9102cb5 commit f5471e4

File tree

17 files changed

+6844
-296
lines changed

17 files changed

+6844
-296
lines changed

swift/codegen/generators/qlgen.py

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,15 @@ class ModifiedStubMarkedAsGeneratedError(Error):
2929
pass
3030

3131

32-
def get_ql_property(cls: schema.Class, prop: schema.Property) -> ql.Property:
32+
class RootElementHasChildren(Error):
33+
pass
34+
35+
36+
def get_ql_property(cls: schema.Class, prop: schema.Property, prev_child: str = "") -> ql.Property:
3337
args = dict(
3438
type=prop.type if not prop.is_predicate else "predicate",
3539
qltest_skip="qltest_skip" in prop.pragmas,
36-
is_child=prop.is_child,
40+
prev_child=prev_child if prop.is_child else None,
3741
is_optional=prop.is_optional,
3842
is_predicate=prop.is_predicate,
3943
)
@@ -69,11 +73,18 @@ def get_ql_property(cls: schema.Class, prop: schema.Property) -> ql.Property:
6973

7074
def get_ql_class(cls: schema.Class, lookup: typing.Dict[str, schema.Class]):
7175
pragmas = {k: True for k in cls.pragmas if k.startswith("ql")}
76+
prev_child = ""
77+
properties = []
78+
for p in cls.properties:
79+
prop = get_ql_property(cls, p, prev_child)
80+
if prop.is_child:
81+
prev_child = prop.singular
82+
properties.append(prop)
7283
return ql.Class(
7384
name=cls.name,
7485
bases=cls.bases,
7586
final=not cls.derived,
76-
properties=[get_ql_property(cls, p) for p in cls.properties],
87+
properties=properties,
7788
dir=cls.dir,
7889
ipa=bool(cls.ipa),
7990
**pragmas,
@@ -116,14 +127,14 @@ def get_import(file: pathlib.Path, swift_dir: pathlib.Path):
116127
return str(stem).replace("/", ".")
117128

118129

119-
def get_types_used_by(cls: ql.Class):
130+
def get_types_used_by(cls: ql.Class) -> typing.Iterable[str]:
120131
for b in cls.bases:
121-
yield b
132+
yield b.base
122133
for p in cls.properties:
123134
yield p.type
124135

125136

126-
def get_classes_used_by(cls: ql.Class):
137+
def get_classes_used_by(cls: ql.Class) -> typing.List[str]:
127138
return sorted(set(t for t in get_types_used_by(cls) if t[0].isupper()))
128139

129140

@@ -227,10 +238,15 @@ def generate(opts, renderer):
227238
data = schema.load(input)
228239

229240
classes = {name: get_ql_class(cls, data.classes) for name, cls in data.classes.items()}
241+
# element root is absent in tests
242+
if schema.root_class_name in classes and classes[schema.root_class_name].has_children:
243+
raise RootElementHasChildren
244+
230245
imports = {}
231246

232247
inheritance_graph = {name: cls.bases for name, cls in data.classes.items()}
233-
db_classes = [classes[name] for name in toposort_flatten(inheritance_graph) if not classes[name].ipa]
248+
toposorted_names = toposort_flatten(inheritance_graph)
249+
db_classes = [classes[name] for name in toposorted_names if not classes[name].ipa]
234250
renderer.render(ql.DbClasses(db_classes), out / "Raw.qll")
235251

236252
classes_by_dir_and_name = sorted(classes.values(), key=lambda cls: (cls.dir, cls.name))
@@ -251,7 +267,8 @@ def generate(opts, renderer):
251267
include_file = stub_out.with_suffix(".qll")
252268
renderer.render(ql.ImportList(list(imports.values())), include_file)
253269

254-
renderer.render(ql.GetParentImplementation(classes_by_dir_and_name), out / 'GetImmediateParent.qll')
270+
toposorted_classes = [classes[name] for name in toposorted_names]
271+
renderer.render(ql.GetParentImplementation(toposorted_classes), out / 'ParentChild.qll')
255272

256273
for c in data.classes.values():
257274
if _should_skip_qltest(c, data.classes):

swift/codegen/lib/ql.py

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import pathlib
1616
from dataclasses import dataclass, field
17+
import itertools
1718
from typing import List, ClassVar, Union, Optional
1819

1920
import inflection
@@ -35,7 +36,7 @@ class Property:
3536
first: bool = False
3637
is_optional: bool = False
3738
is_predicate: bool = False
38-
is_child: bool = False
39+
prev_child: Optional[str] = None
3940
qltest_skip: bool = False
4041

4142
def __post_init__(self):
@@ -65,13 +66,26 @@ def is_repeated(self):
6566
def is_single(self):
6667
return not (self.is_optional or self.is_repeated or self.is_predicate)
6768

69+
@property
70+
def is_child(self):
71+
return self.prev_child is not None
72+
73+
74+
@dataclass
75+
class Base:
76+
base: str
77+
prev: str = ""
78+
79+
def __str__(self):
80+
return self.base
81+
6882

6983
@dataclass
7084
class Class:
7185
template: ClassVar = 'ql_class'
7286

7387
name: str
74-
bases: List[str] = field(default_factory=list)
88+
bases: List[Base] = field(default_factory=list)
7589
final: bool = False
7690
properties: List[Property] = field(default_factory=list)
7791
dir: pathlib.Path = pathlib.Path()
@@ -82,7 +96,8 @@ class Class:
8296
ipa: bool = False
8397

8498
def __post_init__(self):
85-
self.bases = sorted(self.bases)
99+
bases = sorted(str(b) for b in self.bases)
100+
self.bases = [Base(str(b), prev) for b, prev in zip(bases, itertools.chain([""], bases))]
86101
if self.properties:
87102
self.properties[0].first = True
88103

@@ -95,9 +110,17 @@ def path(self) -> pathlib.Path:
95110
return self.dir / self.name
96111

97112
@property
98-
def db_id(self):
113+
def db_id(self) -> str:
99114
return "@" + inflection.underscore(self.name)
100115

116+
@property
117+
def has_children(self) -> bool:
118+
return any(p.is_child for p in self.properties)
119+
120+
@property
121+
def last_base(self) -> str:
122+
return self.bases[-1].base if self.bases else ""
123+
101124

102125
@dataclass
103126
class Stub:

swift/codegen/templates/ql_parent.mustache

Lines changed: 80 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,33 +2,88 @@
22

33
import codeql.swift.elements
44

5-
/**
6-
* Gets any of the "immediate" children of `e`. "Immediate" means not taking into account node resolution: for example
7-
* if the AST child is the first of a series of conversions that would normally be hidden away, this will select the
8-
* next conversion down the hidden AST tree instead of the corresponding fully uncoverted node at the bottom.
9-
* Outside this module this file is mainly intended to be used to test uniqueness of parents.
10-
*/
5+
private module Impl {
6+
{{#classes}}
7+
private Element getImmediateChildOf{{name}}({{name}} e, int index, string partialPredicateCall) {
8+
{{! avoid unused argument warnings on root element, assuming the root element has no children }}
9+
{{#root}}none(){{/root}}
10+
{{^root}}
11+
{{! b is the base offset 0, for ease of generation }}
12+
{{! b<base> is constructed to be strictly greater than the indexes required for children coming from <base> }}
13+
{{! n is the base offset for direct children, equal to the last base offset from above }}
14+
{{! n<child> is constructed to be strictly greater than the indexes for <child> children }}
15+
exists(int b{{#bases}}, int b{{.}}{{/bases}}, int n{{#properties}}{{#is_child}}, int n{{singular}}{{/is_child}}{{/properties}} |
16+
b = 0
17+
{{#bases}}
18+
and
19+
b{{.}} = b{{prev}} + 1 + max(int i | i = -1 or exists(getImmediateChildOf{{.}}(e, i, _)) | i)
20+
{{/bases}}
21+
and
22+
n = b{{last_base}}
23+
{{#properties}}
24+
{{#is_child}}
25+
{{! n<child> is defined on top of the previous definition }}
26+
{{! for single and optional properties it adds 1 (regardless of whether the optional property exists) }}
27+
{{! for repeated it adds 1 + the maximum index (which works for repeated optional as well) }}
28+
and
29+
n{{singular}} = n{{prev_child}} + 1{{#is_repeated}}+ max(int i | i = -1 or exists(e.getImmediate{{singular}}(i)) | i){{/is_repeated}}
30+
{{/is_child}}
31+
{{/properties}} and (
32+
none()
33+
{{#bases}}
34+
or
35+
result = getImmediateChildOf{{.}}(e, index - b{{prev}}, partialPredicateCall)
36+
{{/bases}}
37+
{{#properties}}
38+
{{#is_child}}
39+
or
40+
{{#is_repeated}}
41+
result = e.getImmediate{{singular}}(index - n{{prev_child}}) and partialPredicateCall = "{{singular}}(" + (index - n{{prev_child}}).toString() + ")"
42+
{{/is_repeated}}
43+
{{^is_repeated}}
44+
index = n{{prev_child}} and result = e.getImmediate{{singular}}() and partialPredicateCall = "{{singular}}()"
45+
{{/is_repeated}}
46+
{{/is_child}}
47+
{{/properties}}
48+
))
49+
{{/root}}
50+
}
51+
52+
{{/classes}}
1153
cached
12-
Element getAnImmediateChild(Element e) {
13-
// why does this look more complicated than it should?
14-
// * none() simplifies generation, as we can append `or ...` without a special case for the first item
15-
none()
16-
{{#classes}}
17-
{{#properties}}
18-
{{#is_child}}
19-
or
20-
result = e.({{name}}).getImmediate{{singular}}({{#is_repeated}}_{{/is_repeated}})
21-
{{/is_child}}
22-
{{/properties}}
23-
{{/classes}}
54+
Element getImmediateChild(Element e, int index, string partialAccessor) {
55+
// why does this look more complicated than it should?
56+
// * none() simplifies generation, as we can append `or ...` without a special case for the first item
57+
none()
58+
{{#classes}}
59+
{{#final}}
60+
or
61+
result = getImmediateChildOf{{name}}(e, index, partialAccessor)
62+
{{/final}}
63+
{{/classes}}
64+
}
65+
}
66+
67+
/**
68+
* Gets the "immediate" parent of `e`. "Immediate" means not taking into account node resolution: for example
69+
* if `e` has conversions, `getImmediateParent(e)` will give the innermost conversion in the hidden AST.
70+
*/
71+
Element getImmediateParent(Element e) {
72+
// `unique` is used here to tell the optimizer that there is in fact only one result
73+
// this is tested by the `library-tests/parent/no_double_parents.ql` test
74+
result = unique(Element x | e = Impl::getImmediateChild(x, _, _) | x)
75+
}
76+
77+
/**
78+
* Gets the immediate child indexed at `index`. Indexes are not guaranteed to be contiguous, but are guaranteed to be distinct. `accessor` is bound the member predicate call resulting in the given child.
79+
*/
80+
Element getImmediateChildAndAccessor(Element e, int index, string accessor) {
81+
exists(string partialAccessor | result = Impl::getImmediateChild(e, index, partialAccessor) and accessor = "getImmediate" + partialAccessor)
2482
}
2583

2684
/**
27-
* Gets the "immediate" parent of `e`. "Immediate" means not taking into account node resolution: for example
28-
* if `e` has conversions, `getImmediateParent(e)` will give the bottom conversion in the hidden AST.
29-
*/
30-
Element getImmediateParent(Element e) {
31-
// `unique` is used here to tell the optimizer that there is in fact only one result
32-
// this is tested by the `library-tests/parent/no_double_parents.ql` test
33-
result = unique(Element x | e = getAnImmediateChild(x) | x)
85+
* Gets the child indexed at `index`. Indexes are not guaranteed to be contiguous, but are guaranteed to be distinct. `accessor` is bound the member predicate call resulting in the given child.
86+
*/
87+
Element getChildAndAccessor(Element e, int index, string accessor) {
88+
exists(string partialAccessor | result = Impl::getImmediateChild(e, index, partialAccessor).resolve() and accessor = "get" + partialAccessor)
3489
}

swift/codegen/test/test_ql.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,9 @@ def test_property_predicate_getter():
7878
assert prop.getter == "prop"
7979

8080

81-
def test_class_sorts_bases():
81+
def test_class_processes_bases():
8282
bases = ["B", "Ab", "C", "Aa"]
83-
expected = ["Aa", "Ab", "B", "C"]
83+
expected = [ql.Base("Aa"), ql.Base("Ab", prev="Aa"), ql.Base("B", prev="Ab"), ql.Base("C", prev="B")]
8484
cls = ql.Class("Foo", bases=bases)
8585
assert cls.bases == expected
8686

@@ -105,5 +105,27 @@ def test_non_root_class():
105105
assert not cls.root
106106

107107

108+
@pytest.mark.parametrize("prev_child,is_child", [(None, False), ("", True), ("x", True)])
109+
def test_is_child(prev_child, is_child):
110+
p = ql.Property("Foo", "int", prev_child=prev_child)
111+
assert p.is_child is is_child
112+
113+
114+
def test_empty_class_no_children():
115+
cls = ql.Class("Class", properties=[])
116+
assert cls.has_children is False
117+
118+
119+
def test_class_no_children():
120+
cls = ql.Class("Class", properties=[ql.Property("Foo", "int"), ql.Property("Bar", "string")])
121+
assert cls.has_children is False
122+
123+
124+
def test_class_with_children():
125+
cls = ql.Class("Class", properties=[ql.Property("Foo", "int"), ql.Property("Child", "x", prev_child=""),
126+
ql.Property("Bar", "string")])
127+
assert cls.has_children is True
128+
129+
108130
if __name__ == '__main__':
109131
sys.exit(pytest.main([__file__] + sys.argv[1:]))

0 commit comments

Comments
 (0)