Skip to content

Commit 6f52fe8

Browse files
authored
Merge pull request github#11331 from github/redsun82/swift-codegen-skip-unchanged
Swift: skip QL code generation on untouched files
2 parents b6dd388 + aaa96b2 commit 6f52fe8

File tree

10 files changed

+1323
-213
lines changed

10 files changed

+1323
-213
lines changed

.github/workflows/swift.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ jobs:
3939
- 'swift/ql/lib/codeql/swift/elements/**'
4040
- 'swift/ql/lib/codeql/swift/generated/**'
4141
- 'swift/ql/test/extractor-tests/generated/**'
42+
- 'swift/ql/.generated.list'
4243
ql:
4344
- 'github/workflows/swift.yml'
4445
- 'swift/**/*.ql'

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ repos:
4444

4545
- id: swift-codegen
4646
name: Run Swift checked in code generation
47-
files: ^swift/(schema.py$|codegen/|.*/generated/|ql/lib/(swift\.dbscheme$|codeql/swift/elements))
47+
files: ^swift/(schema.py$|codegen/|.*/generated/|ql/lib/(swift\.dbscheme$|codeql/swift/elements)|ql/\.generated.list)
4848
language: system
4949
entry: bazel run //swift/codegen -- --quiet
5050
pass_filenames: false

swift/actions/setup-env/action.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,4 @@ runs:
2121
shell: bash
2222
run: |
2323
echo build --repository_cache=~/.cache/bazel-repository-cache --disk_cache=~/.cache/bazel-disk-cache > ~/.bazelrc
24+
echo test --test_output=errors >> ~/.bazelrc

swift/codegen/codegen.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,13 @@ def _parse_args() -> argparse.Namespace:
4040
p.add_argument("--codeql-binary", default="codeql", help="command to use for QL formatting (default %(default)s)")
4141
p.add_argument("--cpp-output", type=_abspath,
4242
help="output directory for generated C++ files, required if trap or cpp is provided to --generate")
43+
p.add_argument("--generated-registry", type=_abspath, default=paths.swift_dir / "ql/.generated.list",
44+
help="registry file containing information about checked-in generated code")
4345
return p.parse_args()
4446

4547

46-
def _abspath(x: str) -> pathlib.Path:
47-
return pathlib.Path(x).resolve()
48+
def _abspath(x: str) -> typing.Optional[pathlib.Path]:
49+
return pathlib.Path(x).resolve() if x else None
4850

4951

5052
def run():
@@ -56,9 +58,8 @@ def run():
5658
else:
5759
log_level = logging.INFO
5860
logging.basicConfig(format="{levelname} {message}", style='{', level=log_level)
59-
exe_path = paths.exe_file.relative_to(opts.swift_dir)
6061
for target in opts.generate:
61-
generate(target, opts, render.Renderer(exe_path))
62+
generate(target, opts, render.Renderer(opts.swift_dir))
6263

6364

6465
if __name__ == "__main__":

swift/codegen/generators/qlgen.py

Lines changed: 83 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,6 @@ class FormatError(Error):
4343
pass
4444

4545

46-
class ModifiedStubMarkedAsGeneratedError(Error):
47-
pass
48-
49-
5046
class RootElementHasChildren(Error):
5147
pass
5248

@@ -216,32 +212,11 @@ def get_classes_used_by(cls: ql.Class) -> typing.List[str]:
216212
return sorted(set(t for t in get_types_used_by(cls) if t[0].isupper() and t != cls.name))
217213

218214

219-
_generated_stub_re = re.compile(r"\n*private import .*\n+class \w+ extends Generated::\w+ \{[ \n]?\}", re.MULTILINE)
220-
221-
222-
def _is_generated_stub(file: pathlib.Path) -> bool:
223-
with open(file) as contents:
224-
for line in contents:
225-
if not line.startswith("// generated"):
226-
return False
227-
break
228-
else:
229-
# no lines
230-
return False
231-
# we still do not detect modified synth constructors
232-
if not file.name.endswith("Constructor.qll"):
233-
# one line already read, if we can read 5 other we are past the normal stub generation
234-
line_threshold = 5
235-
first_lines = list(itertools.islice(contents, line_threshold))
236-
if len(first_lines) == line_threshold or not _generated_stub_re.match("".join(first_lines)):
237-
raise ModifiedStubMarkedAsGeneratedError(
238-
f"{file.name} stub was modified but is still marked as generated")
239-
return True
240-
241-
242215
def format(codeql, files):
243-
format_cmd = [codeql, "query", "format", "--in-place", "--"]
244-
format_cmd.extend(str(f) for f in files if f.suffix in (".qll", ".ql"))
216+
ql_files = [str(f) for f in files if f.suffix in (".qll", ".ql")]
217+
if not ql_files:
218+
return
219+
format_cmd = [codeql, "query", "format", "--in-place", "--"] + ql_files
245220
res = subprocess.run(format_cmd, stderr=subprocess.PIPE, text=True)
246221
if res.returncode:
247222
for line in res.stderr.splitlines():
@@ -307,11 +282,14 @@ def generate(opts, renderer):
307282
stub_out = opts.ql_stub_output
308283
test_out = opts.ql_test_output
309284
missing_test_source_filename = "MISSING_SOURCE.txt"
285+
include_file = stub_out.with_suffix(".qll")
286+
287+
generated = {q for q in out.rglob("*.qll")}
288+
generated.add(include_file)
289+
generated.update(q for q in test_out.rglob("*.ql"))
290+
generated.update(q for q in test_out.rglob(missing_test_source_filename))
310291

311-
existing = {q for q in out.rglob("*.qll")}
312-
existing |= {q for q in stub_out.rglob("*.qll") if _is_generated_stub(q)}
313-
existing |= {q for q in test_out.rglob("*.ql")}
314-
existing |= {q for q in test_out.rglob(missing_test_source_filename)}
292+
stubs = {q for q in stub_out.rglob("*.qll")}
315293

316294
data = schema.load_file(input)
317295

@@ -324,77 +302,75 @@ def generate(opts, renderer):
324302

325303
imports = {}
326304

327-
db_classes = [cls for cls in classes.values() if not cls.ipa]
328-
renderer.render(ql.DbClasses(db_classes), out / "Raw.qll")
329-
330-
classes_by_dir_and_name = sorted(classes.values(), key=lambda cls: (cls.dir, cls.name))
331-
for c in classes_by_dir_and_name:
332-
imports[c.name] = get_import(stub_out / c.path, opts.swift_dir)
333-
334-
for c in classes.values():
335-
qll = out / c.path.with_suffix(".qll")
336-
c.imports = [imports[t] for t in get_classes_used_by(c)]
337-
renderer.render(c, qll)
338-
stub_file = stub_out / c.path.with_suffix(".qll")
339-
if not stub_file.is_file() or _is_generated_stub(stub_file):
340-
stub = ql.Stub(
341-
name=c.name, base_import=get_import(qll, opts.swift_dir))
342-
renderer.render(stub, stub_file)
343-
344-
# for example path/to/elements -> path/to/elements.qll
345-
include_file = stub_out.with_suffix(".qll")
346-
renderer.render(ql.ImportList(list(imports.values())), include_file)
347-
348-
renderer.render(ql.GetParentImplementation(list(classes.values())), out / 'ParentChild.qll')
349-
350-
for c in data.classes.values():
351-
if _should_skip_qltest(c, data.classes):
352-
continue
353-
test_dir = test_out / c.group / c.name
354-
test_dir.mkdir(parents=True, exist_ok=True)
355-
if not any(test_dir.glob("*.swift")):
356-
log.warning(f"no test source in {test_dir.relative_to(test_out)}")
357-
renderer.render(ql.MissingTestInstructions(),
358-
test_dir / missing_test_source_filename)
359-
continue
360-
total_props, partial_props = _partition(_get_all_properties_to_be_tested(c, data.classes),
361-
lambda p: p.is_single or p.is_predicate)
362-
renderer.render(ql.ClassTester(class_name=c.name,
363-
properties=total_props,
364-
# in case of collapsed hierarchies we want to see the actual QL class in results
365-
show_ql_class="qltest_collapse_hierarchy" in c.pragmas),
366-
test_dir / f"{c.name}.ql")
367-
for p in partial_props:
368-
renderer.render(ql.PropertyTester(class_name=c.name,
369-
property=p), test_dir / f"{c.name}_{p.getter}.ql")
370-
371-
final_ipa_types = []
372-
non_final_ipa_types = []
373-
constructor_imports = []
374-
ipa_constructor_imports = []
375-
stubs = {}
376-
for cls in sorted(data.classes.values(), key=lambda cls: (cls.group, cls.name)):
377-
ipa_type = get_ql_ipa_class(cls)
378-
if ipa_type.is_final:
379-
final_ipa_types.append(ipa_type)
380-
if ipa_type.has_params:
381-
stub_file = stub_out / cls.group / f"{cls.name}Constructor.qll"
382-
if not stub_file.is_file() or _is_generated_stub(stub_file):
383-
# stub rendering must be postponed as we might not have yet all subtracted ipa types in `ipa_type`
384-
stubs[stub_file] = ql.Synth.ConstructorStub(ipa_type)
385-
constructor_import = get_import(stub_file, opts.swift_dir)
386-
constructor_imports.append(constructor_import)
387-
if ipa_type.is_ipa:
388-
ipa_constructor_imports.append(constructor_import)
389-
else:
390-
non_final_ipa_types.append(ipa_type)
391-
392-
for stub_file, data in stubs.items():
393-
renderer.render(data, stub_file)
394-
renderer.render(ql.Synth.Types(root.name, final_ipa_types, non_final_ipa_types), out / "Synth.qll")
395-
renderer.render(ql.ImportList(constructor_imports), out / "SynthConstructors.qll")
396-
renderer.render(ql.ImportList(ipa_constructor_imports), out / "PureSynthConstructors.qll")
397-
398-
renderer.cleanup(existing)
399-
if opts.ql_format:
400-
format(opts.codeql_binary, renderer.written)
305+
with renderer.manage(generated=generated, stubs=stubs, registry=opts.generated_registry) as renderer:
306+
307+
db_classes = [cls for cls in classes.values() if not cls.ipa]
308+
renderer.render(ql.DbClasses(db_classes), out / "Raw.qll")
309+
310+
classes_by_dir_and_name = sorted(classes.values(), key=lambda cls: (cls.dir, cls.name))
311+
for c in classes_by_dir_and_name:
312+
imports[c.name] = get_import(stub_out / c.path, opts.swift_dir)
313+
314+
for c in classes.values():
315+
qll = out / c.path.with_suffix(".qll")
316+
c.imports = [imports[t] for t in get_classes_used_by(c)]
317+
renderer.render(c, qll)
318+
stub_file = stub_out / c.path.with_suffix(".qll")
319+
if not renderer.is_customized_stub(stub_file):
320+
stub = ql.Stub(name=c.name, base_import=get_import(qll, opts.swift_dir))
321+
renderer.render(stub, stub_file)
322+
323+
# for example path/to/elements -> path/to/elements.qll
324+
renderer.render(ql.ImportList(list(imports.values())), include_file)
325+
326+
renderer.render(ql.GetParentImplementation(list(classes.values())), out / 'ParentChild.qll')
327+
328+
for c in data.classes.values():
329+
if _should_skip_qltest(c, data.classes):
330+
continue
331+
test_dir = test_out / c.group / c.name
332+
test_dir.mkdir(parents=True, exist_ok=True)
333+
if not any(test_dir.glob("*.swift")):
334+
log.warning(f"no test source in {test_dir.relative_to(test_out)}")
335+
renderer.render(ql.MissingTestInstructions(),
336+
test_dir / missing_test_source_filename)
337+
continue
338+
total_props, partial_props = _partition(_get_all_properties_to_be_tested(c, data.classes),
339+
lambda p: p.is_single or p.is_predicate)
340+
renderer.render(ql.ClassTester(class_name=c.name,
341+
properties=total_props,
342+
# in case of collapsed hierarchies we want to see the actual QL class in results
343+
show_ql_class="qltest_collapse_hierarchy" in c.pragmas),
344+
test_dir / f"{c.name}.ql")
345+
for p in partial_props:
346+
renderer.render(ql.PropertyTester(class_name=c.name,
347+
property=p), test_dir / f"{c.name}_{p.getter}.ql")
348+
349+
final_ipa_types = []
350+
non_final_ipa_types = []
351+
constructor_imports = []
352+
ipa_constructor_imports = []
353+
stubs = {}
354+
for cls in sorted(data.classes.values(), key=lambda cls: (cls.group, cls.name)):
355+
ipa_type = get_ql_ipa_class(cls)
356+
if ipa_type.is_final:
357+
final_ipa_types.append(ipa_type)
358+
if ipa_type.has_params:
359+
stub_file = stub_out / cls.group / f"{cls.name}Constructor.qll"
360+
if not renderer.is_customized_stub(stub_file):
361+
# stub rendering must be postponed as we might not have yet all subtracted ipa types in `ipa_type`
362+
stubs[stub_file] = ql.Synth.ConstructorStub(ipa_type)
363+
constructor_import = get_import(stub_file, opts.swift_dir)
364+
constructor_imports.append(constructor_import)
365+
if ipa_type.is_ipa:
366+
ipa_constructor_imports.append(constructor_import)
367+
else:
368+
non_final_ipa_types.append(ipa_type)
369+
370+
for stub_file, data in stubs.items():
371+
renderer.render(data, stub_file)
372+
renderer.render(ql.Synth.Types(root.name, final_ipa_types, non_final_ipa_types), out / "Synth.qll")
373+
renderer.render(ql.ImportList(constructor_imports), out / "SynthConstructors.qll")
374+
renderer.render(ql.ImportList(ipa_constructor_imports), out / "PureSynthConstructors.qll")
375+
if opts.ql_format:
376+
format(opts.codeql_binary, renderer.written)

0 commit comments

Comments
 (0)