Skip to content

Commit ea07255

Browse files
committed
Swift: first IPA layer
1 parent e43755b commit ea07255

File tree

18 files changed

+693
-10
lines changed

18 files changed

+693
-10
lines changed

swift/codegen/generators/qlgen.py

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,28 @@ def get_ql_class(cls: schema.Class):
8080
)
8181

8282

83+
def _to_db_type(x: str) -> str:
84+
if x[0].isupper():
85+
return "@" + inflection.underscore(x)
86+
return x
87+
88+
89+
_final_db_class_lookup = {}
90+
91+
92+
def get_ql_ipa_class(cls: schema.Class):
93+
if cls.derived:
94+
return ql.Ipa.NonFinalClass(name=cls.name, derived=sorted(cls.derived))
95+
if cls.ipa and cls.ipa.from_class:
96+
source = cls.ipa.from_class
97+
_final_db_class_lookup.setdefault(source, ql.Ipa.FinalClassDb(source)).subtract_type(cls.name)
98+
return ql.Ipa.FinalClassIpaFrom(name=cls.name, type=_to_db_type(source))
99+
if cls.ipa and cls.ipa.on_arguments:
100+
return ql.Ipa.FinalClassIpaOn(name=cls.name,
101+
params=[ql.Ipa.Param(k, _to_db_type(v)) for k, v in cls.ipa.on_arguments.items()])
102+
return _final_db_class_lookup.setdefault(cls.name, ql.Ipa.FinalClassDb(cls.name))
103+
104+
83105
def get_import(file: pathlib.Path, swift_dir: pathlib.Path):
84106
stem = file.relative_to(swift_dir / "ql/lib").with_suffix("")
85107
return str(stem).replace("/", ".")
@@ -96,7 +118,9 @@ def get_classes_used_by(cls: ql.Class):
96118
return sorted(set(t for t in get_types_used_by(cls) if t[0].isupper()))
97119

98120

99-
_generated_stub_re = re.compile(r"private import .*\n\nclass \w+ extends \w+ \{[ \n]\}", re.MULTILINE)
121+
_generated_stub_re = re.compile(r"\n*private import .*\n+class \w+ extends \w+ \{[ \n]?\}"
122+
"|"
123+
r"\n*predicate construct\w+\(.*?\) \{ none\(\) \}", re.MULTILINE)
100124

101125

102126
def _is_generated_stub(file):
@@ -112,6 +136,7 @@ def _is_generated_stub(file):
112136
line_threshold = 5
113137
first_lines = list(itertools.islice(contents, line_threshold))
114138
if len(first_lines) == line_threshold or not _generated_stub_re.match("".join(first_lines)):
139+
print("".join(first_lines))
115140
raise ModifiedStubMarkedAsGeneratedError(
116141
f"{file.name} stub was modified but is still marked as generated")
117142
return True
@@ -149,12 +174,14 @@ def _get_all_properties_to_be_tested(cls: ql.Class, lookup: typing.Dict[str, ql.
149174
yield ql.PropertyForTest(p.getter, p.type, p.is_single, p.is_predicate, p.is_repeated)
150175

151176

177+
def _partition_iter(x, pred):
178+
x1, x2 = itertools.tee(x)
179+
return filter(pred, x1), itertools.filterfalse(pred, x2)
180+
181+
152182
def _partition(l, pred):
153183
""" partitions a list according to boolean predicate """
154-
res = ([], [])
155-
for x in l:
156-
res[not pred(x)].append(x)
157-
return res
184+
return map(list, _partition_iter(l, pred))
158185

159186

160187
def _is_in_qltest_collapsed_hierachy(cls: ql.Class, lookup: typing.Dict[str, ql.Class]):
@@ -184,10 +211,10 @@ def generate(opts, renderer):
184211
existing |= {q for q in test_out.rglob(missing_test_source_filename)}
185212

186213
data = schema.load(input)
214+
data.classes.sort(key=lambda cls: (cls.dir, cls.name))
187215

188216
classes = [get_ql_class(cls) for cls in data.classes]
189217
lookup = {cls.name: cls for cls in classes}
190-
classes.sort(key=lambda cls: (cls.dir, cls.name))
191218
imports = {}
192219

193220
for c in classes:
@@ -228,6 +255,24 @@ def generate(opts, renderer):
228255
renderer.render(ql.PropertyTester(class_name=c.name,
229256
property=p), test_dir / f"{c.name}_{p.getter}.ql")
230257

258+
final_ipa_types = []
259+
non_final_ipa_types = []
260+
constructor_imports = []
261+
for cls in data.classes:
262+
ipa_type = get_ql_ipa_class(cls)
263+
if ipa_type.is_final:
264+
final_ipa_types.append(ipa_type)
265+
if ipa_type.is_ipa:
266+
stub_file = stub_out / cls.dir / f"{cls.name}Constructor.qll"
267+
if not stub_file.is_file() or _is_generated_stub(stub_file):
268+
renderer.render(ql.Ipa.ConstructorStub(ipa_type), stub_file)
269+
constructor_imports.append(get_import(stub_file, opts.swift_dir))
270+
else:
271+
non_final_ipa_types.append(ipa_type)
272+
273+
renderer.render(ql.Ipa.Types(schema.root_class_name, final_ipa_types, non_final_ipa_types), out / "IpaTypes.qll")
274+
renderer.render(ql.ImportList(constructor_imports), out / "IpaConstructors.qll")
275+
231276
renderer.cleanup(existing)
232277
if opts.ql_format:
233278
format(opts.codeql_binary, renderer.written)

swift/codegen/lib/ql.py

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
import pathlib
1616
from dataclasses import dataclass, field
17-
from typing import List, ClassVar
17+
from typing import List, ClassVar, Union
1818

1919
import inflection
2020

@@ -151,3 +151,93 @@ class PropertyTester:
151151
@dataclass
152152
class MissingTestInstructions:
153153
template: ClassVar = 'ql_test_missing'
154+
155+
156+
class Ipa:
157+
@dataclass
158+
class Class:
159+
is_final: ClassVar = False
160+
161+
name: str
162+
first: bool = False
163+
164+
@dataclass
165+
class FinalClass(Class):
166+
is_final: ClassVar = True
167+
is_ipa_from: ClassVar = False
168+
is_ipa_on: ClassVar = False
169+
is_db: ClassVar = False
170+
171+
@property
172+
def is_ipa(self):
173+
return self.is_ipa_on or self.is_ipa_from
174+
175+
@dataclass
176+
class Param:
177+
param: str
178+
type: str
179+
first: bool = False
180+
181+
@dataclass
182+
class FinalClassIpaFrom(FinalClass):
183+
is_ipa_from: ClassVar = True
184+
185+
type: str = None
186+
187+
@property
188+
def params(self):
189+
return [Ipa.Param("id", self.type, first=True)]
190+
191+
@dataclass
192+
class FinalClassIpaOn(FinalClass):
193+
is_ipa_on: ClassVar = True
194+
195+
params: List["Ipa.Param"] = field(default_factory=list)
196+
197+
def __post_init__(self):
198+
if self.params:
199+
self.params[0].first = True
200+
201+
@dataclass
202+
class FinalClassDb(FinalClass):
203+
is_db: ClassVar = True
204+
205+
subtracted_ipa_types: List["Ipa.Class"] = field(default_factory=list)
206+
207+
def subtract_type(self, type: str):
208+
self.subtracted_ipa_types.append(Ipa.Class(type, first=not self.subtracted_ipa_types))
209+
210+
@property
211+
def has_subtracted_ipa_types(self):
212+
return bool(self.subtracted_ipa_types)
213+
214+
@property
215+
def db_id(self):
216+
return "@" + inflection.underscore(self.name)
217+
218+
@dataclass
219+
class NonFinalClass(Class):
220+
derived: List["Ipa.Class"] = field(default_factory=list)
221+
222+
def __post_init__(self):
223+
self.derived = [Ipa.Class(c) for c in self.derived]
224+
if self.derived:
225+
self.derived[0].first = True
226+
227+
@dataclass
228+
class Types:
229+
template: ClassVar = "ql_ipa_types"
230+
231+
root: str
232+
final_classes: List["Ipa.FinalClass"] = field(default_factory=list)
233+
non_final_classes: List["Ipa.NonFinalClass"] = field(default_factory=list)
234+
235+
def __post_init__(self):
236+
if self.final_classes:
237+
self.final_classes[0].first = True
238+
239+
@dataclass
240+
class ConstructorStub:
241+
template: ClassVar = "ql_ipa_constructor_stub"
242+
243+
cls: Union["Ipa.FinalClassIpaFrom", "Ipa.FinalClassIpaOn"]

swift/codegen/lib/schema.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import pathlib
44
import re
5-
import typing
65
from dataclasses import dataclass, field
76
from typing import List, Set, Union, Dict, ClassVar
87

@@ -57,6 +56,12 @@ class PredicateProperty(Property):
5756
is_predicate: ClassVar = True
5857

5958

59+
@dataclass
60+
class IpaInfo:
61+
from_class: str = None
62+
on_arguments: Dict[str, str] = None
63+
64+
6065
@dataclass
6166
class Class:
6267
name: str
@@ -65,6 +70,7 @@ class Class:
6570
properties: List[Property] = field(default_factory=list)
6671
dir: pathlib.Path = pathlib.Path()
6772
pragmas: List[str] = field(default_factory=list)
73+
ipa: IpaInfo = None
6874

6975

7076
@dataclass
@@ -107,6 +113,11 @@ def _parse_property(name: str, data: Union[str, Dict[str, _StrOrList]], is_child
107113
return SingleProperty(name, type, is_child=is_child, pragmas=pragmas)
108114

109115

116+
def _parse_ipa(data: Dict[str, Union[str, Dict[str, str]]]):
117+
return IpaInfo(from_class=data.get("from"),
118+
on_arguments=data.get(True)) # 'on' is parsed as boolean True in yaml
119+
120+
110121
class _DirSelector:
111122
""" Default output subdirectory selector for generated QL files, based on the `_directories` global field"""
112123

@@ -145,6 +156,8 @@ def load(path):
145156
cls.properties.extend(_parse_property(kk, vv, is_child=True) for kk, vv in v.items())
146157
elif k == "_pragma":
147158
cls.pragmas = _auto_list(v)
159+
elif k == "_ipa":
160+
cls.ipa = _parse_ipa(v)
148161
else:
149162
raise Error(f"unknown metadata {k} for class {name}")
150163
if not cls.bases and cls.name != root_class_name:

swift/codegen/schema.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -792,6 +792,12 @@ OptionalTryExpr:
792792
TryExpr:
793793
_extends: AnyTryExpr
794794

795+
MethodCallExpr:
796+
_extends: ApplyExpr
797+
_ipa:
798+
from: CallExpr
799+
qualifier: Expr
800+
795801
BinaryExpr:
796802
_extends: ApplyExpr
797803

swift/codegen/templates/ql_class.mustache

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
// generated by {{generator}}
2-
32
{{#imports}}
43
import {{.}}
54
{{/imports}}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// generated by {{generator}}, remove this comment if you wish to edit this file
2+
predicate construct{{cls.name}}({{#cls.params}}{{^first}}, {{/first}}{{type}} {{param}}{{/cls.params}}) { none() }
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
private import codeql.swift.elements.IpaConstructors
2+
3+
cached module Cached {
4+
cached newtype T{{root}} =
5+
{{#final_classes}}
6+
{{^first}}
7+
or
8+
{{/first}}
9+
{{#is_ipa_from}}
10+
T{{name}}({{type}} id) { construct{{name}}(id) }
11+
{{/is_ipa_from}}
12+
{{#is_ipa_on}}
13+
T{{name}}({{#params}}{{^first}}, {{/first}}{{type}} {{param}}{{/params}}) { construct{{name}}({{#params}}{{^first}}, {{/first}}{{param}}{{/params}}) }
14+
{{/is_ipa_on}}
15+
{{#is_db}}
16+
T{{name}}({{db_id}} id){{#has_subtracted_ipa_types}} { {{#subtracted_ipa_types}}{{^first}} and {{/first}}not construct{{name}}(id){{/subtracted_ipa_types}} }{{/has_subtracted_ipa_types}}
17+
{{/is_db}}
18+
{{/final_classes}}
19+
20+
{{#non_final_classes}}
21+
class T{{name}} = {{#derived}}{{^first}} or {{/first}}T{{name}}{{/derived}};
22+
{{/non_final_classes}}
23+
}
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
// generated by {{generator}}, remove this comment if you wish to edit this file
2-
32
private import {{base_import}}
43

54
class {{name}} extends {{name}}Base {}

swift/codegen/test/test_qlgen.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ def _filter_generated_classes(ret, output_test_files=False):
101101
str(f): ret[ql_test_output_path() / f]
102102
for f in test_files
103103
}
104+
base_files -= {pathlib.Path("IpaTypes.qll"), pathlib.Path("IpaConstructors.qll")}
104105
assert stub_files == base_files
105106
return {
106107
str(f): (ret[stub_path() / f], ret[ql_output_path() / f])
@@ -128,6 +129,8 @@ def test_empty(generate):
128129
assert generate([]) == {
129130
import_file(): ql.ImportList(),
130131
children_file(): ql.GetParentImplementation(),
132+
ql_output_path() / "IpaTypes.qll": ql.Ipa.Types(schema.root_class_name),
133+
ql_output_path() / "IpaConstructors.qll": ql.ImportList(),
131134
}
132135

133136

swift/codegen/test/test_schema.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,5 +303,34 @@ def test_class_with_unknown_metadata(load):
303303
""")
304304

305305

306+
def test_ipa_class_from(load):
307+
ret = load("""
308+
MyClass:
309+
_ipa:
310+
from: A
311+
""")
312+
assert ret.classes == [
313+
schema.Class(root_name, derived={'MyClass'}),
314+
schema.Class('MyClass', bases={root_name}, ipa=schema.IpaInfo(from_class="A")),
315+
]
316+
317+
318+
def test_ipa_class_on(load):
319+
ret = load("""
320+
MyClass:
321+
_ipa:
322+
on:
323+
x: A
324+
y: int
325+
""")
326+
assert ret.classes == [
327+
schema.Class(root_name, derived={'MyClass'}),
328+
schema.Class('MyClass', bases={root_name}, ipa=schema.IpaInfo(on_arguments={"x": "A", "y": "int"})),
329+
]
330+
331+
332+
# TODO rejection tests and implementation for malformed `_ipa` clauses
333+
334+
306335
if __name__ == '__main__':
307336
sys.exit(pytest.main([__file__] + sys.argv[1:]))

0 commit comments

Comments
 (0)