Skip to content

Commit ad38cf2

Browse files
committed
Swift: prevent accidental revert of modified stub
If one modifies a QL stub but forgets to remove the `// generated` header comment, codegen will now abort with an error rather than silently reverting the change. This is based on the rough heuristic of just counting the lines. If any change is done to the stub class, the number of lines is bound to be 5 or more.
1 parent a6ae6cf commit ad38cf2

File tree

2 files changed

+43
-9
lines changed

2 files changed

+43
-9
lines changed

swift/codegen/generators/qlgen.py

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,16 @@
1212
log = logging.getLogger(__name__)
1313

1414

15-
class FormatError(Exception):
15+
class Error(Exception):
16+
def __str__(self):
17+
return self.args[0]
18+
19+
20+
class FormatError(Error):
21+
pass
22+
23+
24+
class ModifiedStubMarkedAsGeneratedError(Error):
1625
pass
1726

1827

@@ -84,11 +93,22 @@ def get_classes_used_by(cls: ql.Class):
8493
return sorted(set(t for t in get_types_used_by(cls) if t[0].isupper()))
8594

8695

87-
def is_generated(file):
96+
def _is_generated_stub(file, check_modification=False):
8897
with open(file) as contents:
8998
for line in contents:
90-
return line.startswith("// generated")
91-
return False
99+
if not line.startswith("// generated"):
100+
return False
101+
break
102+
else:
103+
# no lines
104+
return False
105+
if check_modification:
106+
# one line already read, if we can read 4 other we are past the normal stub generation
107+
line_threshold = 4
108+
if sum(1 for _ in zip(range(line_threshold), contents)) == line_threshold:
109+
raise ModifiedStubMarkedAsGeneratedError(
110+
f"{file.name} stub was modified but is still marked as generated")
111+
return True
92112

93113

94114
def format(codeql, files):
@@ -138,7 +158,7 @@ def generate(opts, renderer):
138158
test_out = opts.ql_test_output
139159
missing_test_source_filename = "MISSING_SOURCE.txt"
140160
existing = {q for q in out.rglob("*.qll")}
141-
existing |= {q for q in stub_out.rglob("*.qll") if is_generated(q)}
161+
existing |= {q for q in stub_out.rglob("*.qll") if _is_generated_stub(q, check_modification=True)}
142162
existing |= {q for q in test_out.rglob("*.ql")}
143163
existing |= {q for q in test_out.rglob(missing_test_source_filename)}
144164

@@ -157,7 +177,7 @@ def generate(opts, renderer):
157177
c.imports = [imports[t] for t in get_classes_used_by(c)]
158178
renderer.render(c, qll)
159179
stub_file = stub_out / c.path.with_suffix(".qll")
160-
if not stub_file.is_file() or is_generated(stub_file):
180+
if not stub_file.is_file() or _is_generated_stub(stub_file):
161181
stub = ql.Stub(
162182
name=c.name, base_import=get_import(qll, opts.swift_dir))
163183
renderer.render(stub, stub_file)

swift/codegen/test/test_qlgen.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
import subprocess
33
import sys
44

5+
import pytest
6+
57
from swift.codegen.generators import qlgen
68
from swift.codegen.lib import ql
79
from swift.codegen.test.utils import *
@@ -36,17 +38,22 @@ def children_file(): return ql_output_path() / "GetImmediateParent.qll"
3638

3739

3840
@pytest.fixture
39-
def generate(input, opts, renderer):
41+
def qlgen_opts(opts):
4042
opts.ql_stub_output = stub_path()
4143
opts.ql_output = ql_output_path()
4244
opts.ql_test_output = ql_test_output_path()
4345
opts.ql_format = True
4446
opts.swift_dir = paths.swift_dir
47+
return opts
48+
49+
50+
@pytest.fixture
51+
def generate(input, qlgen_opts, renderer):
4552
renderer.written = []
4653

4754
def func(classes):
4855
input.classes = classes
49-
return run_generation(qlgen.generate, opts, renderer)
56+
return run_generation(qlgen.generate, qlgen_opts, renderer)
5057

5158
return func
5259

@@ -349,7 +356,7 @@ def test_empty_cleanup(generate, renderer):
349356
assert renderer.mock_calls[-1] == mock.call.cleanup(set())
350357

351358

352-
def test_non_empty_cleanup(opts, generate, renderer, tmp_path):
359+
def test_non_empty_cleanup(opts, generate, renderer):
353360
ql_a = opts.ql_output / "A.qll"
354361
ql_b = opts.ql_output / "B.qll"
355362
stub_a = opts.ql_stub_output / "A.qll"
@@ -369,6 +376,13 @@ def test_non_empty_cleanup(opts, generate, renderer, tmp_path):
369376
{ql_a, ql_b, stub_a, test_a, test_b})
370377

371378

379+
def test_modified_stub_still_generated(qlgen_opts, renderer):
380+
stub = qlgen_opts.ql_stub_output / "A.qll"
381+
write(stub, "// generated\n\n\n\nsomething\n")
382+
with pytest.raises(qlgen.ModifiedStubMarkedAsGeneratedError):
383+
run_generation(qlgen.generate, qlgen_opts, renderer)
384+
385+
372386
def test_test_missing_source(generate_tests):
373387
generate_tests([
374388
schema.Class("A"),

0 commit comments

Comments
 (0)