Skip to content

Commit beb0472

Browse files
committed
Swift: fix ParentChild generation
There was an issue in case multiple inheritance from classes with children was involved, where indexes would overlap. The generated code structure has been reshuffled a bit, with `Impl::getImmediateChildOf<Class>` predicates giving 0-based children for a given class, including those coming from bases, and the final `Impl::getImmediateChild` disjuncting the above on final classes only. This removes the need of `getMaximumChildrenIndex<Class>`, and also removes the code scanning alerts. Also, comments were fixed addressing the review.
1 parent 3f4a330 commit beb0472

File tree

7 files changed

+4548
-1542
lines changed

7 files changed

+4548
-1542
lines changed

swift/codegen/generators/qlgen.py

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

3131

32+
class RootElementHasChildren(Error):
33+
pass
34+
35+
3236
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",
@@ -123,14 +127,14 @@ def get_import(file: pathlib.Path, swift_dir: pathlib.Path):
123127
return str(stem).replace("/", ".")
124128

125129

126-
def get_types_used_by(cls: ql.Class):
130+
def get_types_used_by(cls: ql.Class) -> typing.Iterable[str]:
127131
for b in cls.bases:
128-
yield b
132+
yield b.base
129133
for p in cls.properties:
130134
yield p.type
131135

132136

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

136140

@@ -234,6 +238,10 @@ def generate(opts, renderer):
234238
data = schema.load(input)
235239

236240
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+
237245
imports = {}
238246

239247
inheritance_graph = {name: cls.bases for name, cls in data.classes.items()}

swift/codegen/lib/ql.py

Lines changed: 19 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
@@ -70,12 +71,21 @@ def is_child(self):
7071
return self.prev_child is not None
7172

7273

74+
@dataclass
75+
class Base:
76+
base: str
77+
prev: str = ""
78+
79+
def __str__(self):
80+
return self.base
81+
82+
7383
@dataclass
7484
class Class:
7585
template: ClassVar = 'ql_class'
7686

7787
name: str
78-
bases: List[str] = field(default_factory=list)
88+
bases: List[Base] = field(default_factory=list)
7989
final: bool = False
8090
properties: List[Property] = field(default_factory=list)
8191
dir: pathlib.Path = pathlib.Path()
@@ -86,7 +96,8 @@ class Class:
8696
ipa: bool = False
8797

8898
def __post_init__(self):
89-
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))]
90101
if self.properties:
91102
self.properties[0].first = True
92103

@@ -99,13 +110,17 @@ def path(self) -> pathlib.Path:
99110
return self.dir / self.name
100111

101112
@property
102-
def db_id(self):
113+
def db_id(self) -> str:
103114
return "@" + inflection.underscore(self.name)
104115

105116
@property
106-
def has_children(self):
117+
def has_children(self) -> bool:
107118
return any(p.is_child for p in self.properties)
108119

120+
@property
121+
def last_base(self) -> str:
122+
return self.bases[-1].base if self.bases else ""
123+
109124

110125
@dataclass
111126
class Stub:

swift/codegen/templates/ql_parent.mustache

Lines changed: 64 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -4,80 +4,86 @@ import codeql.swift.elements
44

55
private module Impl {
66
{{#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}}
21-
/**
22-
* Gets any of the "immediate" children of `e`. "Immediate" means not taking into account node resolution: for example
23-
* if the AST child is the first of a series of conversions that would normally be hidden away, this will select the
24-
* next conversion down the hidden AST tree instead of the corresponding fully uncoverted node at the bottom.
25-
* Outside this module this file is mainly intended to be used to test uniqueness of parents.
26-
*/
27-
cached
28-
Element getImmediateChild(Element e, int index, string partialAccessor) {
29-
// why does this look more complicated than it should?
30-
// * none() simplifies generation, as we can append `or ...` without a special case for the first item
31-
none()
32-
{{#classes}}
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}}
7+
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 offsets 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}}
3723
{{#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}}
4524
{{#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}}
4634
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}}
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}}
5346
{{/is_child}}
54-
{{/properties}}
55-
))
56-
{{/has_children}}
57-
{{/classes}}
47+
{{/properties}}
48+
))
49+
{{/root}}
50+
}
51+
52+
{{/classes}}
53+
cached
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}}
5864
}
5965
}
6066

6167
/**
62-
* Gets the "immediate" parent of `e`. "Immediate" means not taking into account node resolution: for example
63-
* if `e` has conversions, `getImmediateParent(e)` will give the bottom conversion in the hidden AST.
64-
*/
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+
*/
6571
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)
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)
6975
}
7076

7177
/**
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.
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.
7379
*/
7480
Element getImmediateChildAndAccessor(Element e, int index, string accessor) {
75-
exists(string partialAccessor | result = Impl::getImmediateChild(e, index, partialAccessor) and accessor = "getImmediate" + partialAccessor)
81+
exists(string partialAccessor | result = Impl::getImmediateChild(e, index, partialAccessor) and accessor = "getImmediate" + partialAccessor)
7682
}
7783

7884
/**
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-
*/
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+
*/
8187
Element getChildAndAccessor(Element e, int index, string accessor) {
82-
exists(string partialAccessor | result = Impl::getImmediateChild(e, index, partialAccessor).resolve() and accessor = "get" + partialAccessor)
88+
exists(string partialAccessor | result = Impl::getImmediateChild(e, index, partialAccessor).resolve() and accessor = "get" + partialAccessor)
8389
}

swift/codegen/test/test_ql.py

Lines changed: 2 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

swift/codegen/test/test_qlgen.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,13 @@ def test_class_dir(generate_classes):
357357
}
358358

359359

360+
def test_root_element_cannot_have_children(generate_classes):
361+
with pytest.raises(qlgen.RootElementHasChildren):
362+
generate_classes([
363+
schema.Class(schema.root_class_name, properties=[schema.SingleProperty("x", is_child=True)])
364+
])
365+
366+
360367
def test_class_dir_imports(generate_import_list):
361368
dir = pathlib.Path("another/rel/path")
362369
assert generate_import_list([

0 commit comments

Comments
 (0)