Skip to content

Commit b5aad04

Browse files
committed
Swift: add indexes to generated parent-child library
1 parent 7b50c95 commit b5aad04

File tree

7 files changed

+2336
-265
lines changed

7 files changed

+2336
-265
lines changed

swift/codegen/generators/qlgen.py

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

3131

32-
def get_ql_property(cls: schema.Class, prop: schema.Property) -> ql.Property:
32+
def get_ql_property(cls: schema.Class, prop: schema.Property, prev_child: str = "") -> ql.Property:
3333
args = dict(
3434
type=prop.type if not prop.is_predicate else "predicate",
3535
qltest_skip="qltest_skip" in prop.pragmas,
36-
is_child=prop.is_child,
36+
prev_child=prev_child if prop.is_child else None,
3737
is_optional=prop.is_optional,
3838
is_predicate=prop.is_predicate,
3939
)
@@ -69,11 +69,18 @@ def get_ql_property(cls: schema.Class, prop: schema.Property) -> ql.Property:
6969

7070
def get_ql_class(cls: schema.Class, lookup: typing.Dict[str, schema.Class]):
7171
pragmas = {k: True for k in cls.pragmas if k.startswith("ql")}
72+
prev_child = ""
73+
properties = []
74+
for p in cls.properties:
75+
prop = get_ql_property(cls, p, prev_child)
76+
if prop.is_child:
77+
prev_child = prop.singular
78+
properties.append(prop)
7279
return ql.Class(
7380
name=cls.name,
7481
bases=cls.bases,
7582
final=not cls.derived,
76-
properties=[get_ql_property(cls, p) for p in cls.properties],
83+
properties=properties,
7784
dir=cls.dir,
7885
ipa=bool(cls.ipa),
7986
**pragmas,
@@ -230,7 +237,8 @@ def generate(opts, renderer):
230237
imports = {}
231238

232239
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]
240+
toposorted_names = toposort_flatten(inheritance_graph)
241+
db_classes = [classes[name] for name in toposorted_names if not classes[name].ipa]
234242
renderer.render(ql.DbClasses(db_classes), out / "Raw.qll")
235243

236244
classes_by_dir_and_name = sorted(classes.values(), key=lambda cls: (cls.dir, cls.name))
@@ -251,7 +259,8 @@ def generate(opts, renderer):
251259
include_file = stub_out.with_suffix(".qll")
252260
renderer.render(ql.ImportList(list(imports.values())), include_file)
253261

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

256265
for c in data.classes.values():
257266
if _should_skip_qltest(c, data.classes):

swift/codegen/lib/ql.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class Property:
3535
first: bool = False
3636
is_optional: bool = False
3737
is_predicate: bool = False
38-
is_child: bool = False
38+
prev_child: Optional[str] = None
3939
qltest_skip: bool = False
4040

4141
def __post_init__(self):
@@ -65,6 +65,10 @@ def is_repeated(self):
6565
def is_single(self):
6666
return not (self.is_optional or self.is_repeated or self.is_predicate)
6767

68+
@property
69+
def is_child(self):
70+
return self.prev_child is not None
71+
6872

6973
@dataclass
7074
class Class:
@@ -98,6 +102,10 @@ def path(self) -> pathlib.Path:
98102
def db_id(self):
99103
return "@" + inflection.underscore(self.name)
100104

105+
@property
106+
def has_children(self):
107+
return any(p.is_child for p in self.properties)
108+
101109

102110
@dataclass
103111
class Stub:

swift/codegen/templates/ql_parent.mustache

Lines changed: 60 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,33 +2,82 @@
22

33
import codeql.swift.elements
44

5+
module Impl {
6+
{{#classes}}
7+
int getMaximumChildrenIndex{{name}}({{name}} e) {
8+
{{#root}}e = e and{{/root}}
9+
result = 0
10+
{{#bases}}
11+
+ getMaximumChildrenIndex{{.}}(e)
12+
{{/bases}}
13+
{{#properties}}
14+
{{#is_child}}
15+
+ 1{{#is_repeated}}+ max(int i | exists(e.getImmediate{{singular}}(i)) | i){{/is_repeated}}
16+
{{/is_child}}
17+
{{/properties}}
18+
}
19+
20+
{{/classes}}
521
/**
622
* Gets any of the "immediate" children of `e`. "Immediate" means not taking into account node resolution: for example
723
* if the AST child is the first of a series of conversions that would normally be hidden away, this will select the
824
* next conversion down the hidden AST tree instead of the corresponding fully uncoverted node at the bottom.
925
* Outside this module this file is mainly intended to be used to test uniqueness of parents.
1026
*/
1127
cached
12-
Element getAnImmediateChild(Element e) {
28+
Element getImmediateChild(Element e, int index, string partialAccessor) {
1329
// why does this look more complicated than it should?
1430
// * none() simplifies generation, as we can append `or ...` without a special case for the first item
1531
none()
1632
{{#classes}}
17-
{{#properties}}
18-
{{#is_child}}
19-
or
20-
result = e.({{name}}).getImmediate{{singular}}({{#is_repeated}}_{{/is_repeated}})
21-
{{/is_child}}
22-
{{/properties}}
33+
{{#has_children}}
34+
or
35+
exists(int n{{#properties}}{{#is_child}}, int n{{singular}}{{/is_child}}{{/properties}} |
36+
n = 0{{#bases}} + getMaximumChildrenIndex{{.}}(e){{/bases}}
37+
{{#properties}}
38+
{{#is_child}}
39+
and n{{singular}} = n{{prev_child}} + 1{{#is_repeated}} + max(int i | i = 0 or exists(e.({{name}}).getImmediate{{singular}}(i)) | i){{/is_repeated}}
40+
{{/is_child}}
41+
{{/properties}}
42+
and (
43+
none()
44+
{{#properties}}
45+
{{#is_child}}
46+
or
47+
{{#is_repeated}}
48+
result = e.({{name}}).getImmediate{{singular}}(index - n{{prev_child}}) and partialAccessor = "{{singular}}(" + (index - n{{prev_child}}).toString() + ")"
49+
{{/is_repeated}}
50+
{{^is_repeated}}
51+
index = n{{prev_child}} and result = e.({{name}}).getImmediate{{singular}}() and partialAccessor = "{{singular}}()"
52+
{{/is_repeated}}
53+
{{/is_child}}
54+
{{/properties}}
55+
))
56+
{{/has_children}}
2357
{{/classes}}
2458
}
59+
}
2560

2661
/**
2762
* Gets the "immediate" parent of `e`. "Immediate" means not taking into account node resolution: for example
2863
* if `e` has conversions, `getImmediateParent(e)` will give the bottom conversion in the hidden AST.
2964
*/
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)
65+
Element getImmediateParent(Element e) {
66+
// `unique` is used here to tell the optimizer that there is in fact only one result
67+
// this is tested by the `library-tests/parent/no_double_parents.ql` test
68+
result = unique(Element x | e = Impl::getImmediateChild(x, _, _) | x)
69+
}
70+
71+
/**
72+
* Gets the immediate child indexed at `index`. Indexes are not guaranteed to be contiguous, but are guaranteed to be distinct. `accessor` is bound the the method giving the given child.
73+
*/
74+
Element getImmediateChildAndAccessor(Element e, int index, string accessor) {
75+
exists(string partialAccessor | result = Impl::getImmediateChild(e, index, partialAccessor) and accessor = "getImmediate" + partialAccessor)
76+
}
77+
78+
/**
79+
* Gets the child indexed at `index`. Indexes are not guaranteed to be contiguous, but are guaranteed to be distinct. `accessor` is bound the the method giving the given child. Node resolution is carried out.
80+
*/
81+
Element getChildAndAccessor(Element e, int index, string accessor) {
82+
exists(string partialAccessor | result = Impl::getImmediateChild(e, index, partialAccessor).resolve() and accessor = "get" + partialAccessor)
3483
}

swift/codegen/test/test_ql.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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:]))

swift/codegen/test/test_qlgen.py

Lines changed: 49 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def ql_test_output_path(): return paths.swift_dir / "ql/test/path"
2929
def import_file(): return stub_path().with_suffix(".qll")
3030

3131

32-
def children_file(): return ql_output_path() / "GetImmediateParent.qll"
32+
def children_file(): return ql_output_path() / "ParentChild.qll"
3333

3434

3535
stub_import_prefix = "stub.path."
@@ -207,6 +207,42 @@ def test_single_property(generate_classes):
207207
}
208208

209209

210+
def test_children(generate_classes):
211+
assert generate_classes([
212+
schema.Class("MyObject", properties=[
213+
schema.SingleProperty("a", "int"),
214+
schema.SingleProperty("child1", "int", is_child=True),
215+
schema.RepeatedProperty("b", "int"),
216+
schema.RepeatedProperty("child2", "int", is_child=True),
217+
schema.OptionalProperty("c", "int"),
218+
schema.OptionalProperty("child3", "int", is_child=True),
219+
schema.RepeatedOptionalProperty("d", "int"),
220+
schema.RepeatedOptionalProperty("child4", "int", is_child=True),
221+
]),
222+
]) == {
223+
"MyObject.qll": (ql.Stub(name="MyObject", base_import=gen_import_prefix + "MyObject"),
224+
ql.Class(name="MyObject", final=True,
225+
properties=[
226+
ql.Property(singular="A", type="int", tablename="my_objects",
227+
tableparams=["this", "result", "_"]),
228+
ql.Property(singular="Child1", type="int", tablename="my_objects",
229+
tableparams=["this", "_", "result"], prev_child=""),
230+
ql.Property(singular="B", plural="Bs", type="int", tablename="my_object_bs",
231+
tableparams=["this", "index", "result"]),
232+
ql.Property(singular="Child2", plural="Child2s", type="int", tablename="my_object_child2s",
233+
tableparams=["this", "index", "result"], prev_child="Child1"),
234+
ql.Property(singular="C", type="int", tablename="my_object_cs",
235+
tableparams=["this", "result"], is_optional=True),
236+
ql.Property(singular="Child3", type="int", tablename="my_object_child3s",
237+
tableparams=["this", "result"], is_optional=True, prev_child="Child2"),
238+
ql.Property(singular="D", plural="Ds", type="int", tablename="my_object_ds",
239+
tableparams=["this", "index", "result"], is_optional=True),
240+
ql.Property(singular="Child4", plural="Child4s", type="int", tablename="my_object_child4s",
241+
tableparams=["this", "index", "result"], is_optional=True, prev_child="Child3"),
242+
])),
243+
}
244+
245+
210246
def test_single_properties(generate_classes):
211247
assert generate_classes([
212248
schema.Class("MyObject", properties=[
@@ -228,8 +264,8 @@ def test_single_properties(generate_classes):
228264
}
229265

230266

231-
@pytest.mark.parametrize("is_child", [False, True])
232-
def test_optional_property(generate_classes, is_child):
267+
@pytest.mark.parametrize("is_child,prev_child", [(False, None), (True, "")])
268+
def test_optional_property(generate_classes, is_child, prev_child):
233269
assert generate_classes([
234270
schema.Class("MyObject", properties=[
235271
schema.OptionalProperty("foo", "bar", is_child=is_child)]),
@@ -238,27 +274,27 @@ def test_optional_property(generate_classes, is_child):
238274
ql.Class(name="MyObject", final=True, properties=[
239275
ql.Property(singular="Foo", type="bar", tablename="my_object_foos",
240276
tableparams=["this", "result"],
241-
is_optional=True, is_child=is_child),
277+
is_optional=True, prev_child=prev_child),
242278
])),
243279
}
244280

245281

246-
@pytest.mark.parametrize("is_child", [False, True])
247-
def test_repeated_property(generate_classes, is_child):
282+
@pytest.mark.parametrize("is_child,prev_child", [(False, None), (True, "")])
283+
def test_repeated_property(generate_classes, is_child, prev_child):
248284
assert generate_classes([
249285
schema.Class("MyObject", properties=[
250286
schema.RepeatedProperty("foo", "bar", is_child=is_child)]),
251287
]) == {
252288
"MyObject.qll": (ql.Stub(name="MyObject", base_import=gen_import_prefix + "MyObject"),
253289
ql.Class(name="MyObject", final=True, properties=[
254290
ql.Property(singular="Foo", plural="Foos", type="bar", tablename="my_object_foos",
255-
tableparams=["this", "index", "result"], is_child=is_child),
291+
tableparams=["this", "index", "result"], prev_child=prev_child),
256292
])),
257293
}
258294

259295

260-
@pytest.mark.parametrize("is_child", [False, True])
261-
def test_repeated_optional_property(generate_classes, is_child):
296+
@pytest.mark.parametrize("is_child,prev_child", [(False, None), (True, "")])
297+
def test_repeated_optional_property(generate_classes, is_child, prev_child):
262298
assert generate_classes([
263299
schema.Class("MyObject", properties=[
264300
schema.RepeatedOptionalProperty("foo", "bar", is_child=is_child)]),
@@ -267,7 +303,7 @@ def test_repeated_optional_property(generate_classes, is_child):
267303
ql.Class(name="MyObject", final=True, properties=[
268304
ql.Property(singular="Foo", plural="Foos", type="bar", tablename="my_object_foos",
269305
tableparams=["this", "index", "result"], is_optional=True,
270-
is_child=is_child),
306+
prev_child=prev_child),
271307
])),
272308
}
273309

@@ -286,8 +322,8 @@ def test_predicate_property(generate_classes):
286322
}
287323

288324

289-
@pytest.mark.parametrize("is_child", [False, True])
290-
def test_single_class_property(generate_classes, is_child):
325+
@pytest.mark.parametrize("is_child,prev_child", [(False, None), (True, "")])
326+
def test_single_class_property(generate_classes, is_child, prev_child):
291327
assert generate_classes([
292328
schema.Class("MyObject", properties=[
293329
schema.SingleProperty("foo", "Bar", is_child=is_child)]),
@@ -299,7 +335,7 @@ def test_single_class_property(generate_classes, is_child):
299335
ql.Property(singular="Foo", type="Bar", tablename="my_objects",
300336
tableparams=[
301337
"this", "result"],
302-
is_child=is_child),
338+
prev_child=prev_child),
303339
],
304340
)),
305341
"Bar.qll": (ql.Stub(name="Bar", base_import=gen_import_prefix + "Bar"),

0 commit comments

Comments
 (0)