Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .github/impact-gate/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,22 @@
# CI installs it with `pip install --require-hashes --no-deps`, so nothing here
# resolves at install time and a transitive dependency cannot float.
#
# The top-level pins are impact-gate==0.4.1, lizard==1.24.1 (Lizard 1.24.0 is
# The top-level pins are impact-gate==0.4.1, lizard==1.24.3 (Lizard 1.24.0 is
# excluded by ImpactGate's own metadata) and pygments==2.21.0, which
# scripts/impact-gate.py imports directly. uv still annotates pygments "via
# lizard". To regenerate after changing them, from the repository root:
#
# printf 'impact-gate==0.4.1\nlizard==1.24.1\npygments==2.21.0\n' \
# printf 'impact-gate==0.4.1\nlizard==1.24.3\npygments==2.21.0\n' \
# | uv pip compile - --generate-hashes --universal --python-version 3.12 \
# --no-header -o .github/impact-gate/requirements.txt
#
# then restore this header. Any change here changes the baseline cache key.
impact-gate==0.4.1 \
--hash=sha256:9d9c8fb4c6f2fa56559d0530ff82569bc6d42b28ff5a77103e504e2470f198c6 \
--hash=sha256:e6dab6abec925151bd4dbffff92d4a40f742d561a6f8527b3fbe15dd68b25518
lizard==1.24.1 \
--hash=sha256:746060a3e9a375af7340b2d4c6aab307b8a8b4199339c36ddc7afc4188b53d1c \
--hash=sha256:c022cf1aac8170994b175e2a700f0535088f66da7e6bdf6c59fbd3b716cd10ac
lizard==1.24.3 \
--hash=sha256:7d2570ca711288388de1cf0f25512e97fbbc5e189f551c87b4e20d11d028b297 \
--hash=sha256:f7fccd909719170e323a240cc6927bfea65984d77a8f9c0e6405be582e9861db
# via impact-gate
pathspec==1.1.1 \
--hash=sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a \
Expand Down
11 changes: 5 additions & 6 deletions scripts/impact-gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,16 +157,15 @@ def report(args: argparse.Namespace, policy: str) -> None:
publish(mark_misread_spans(result, args.base, spans))


# lizard's TypeScript reader sometimes misses a function's closing brace (an
# arrow callback followed by a ternary returning an object literal is one
# trigger) and runs the span on into the next top-level declaration. Edits to
# lizard's TypeScript reader sometimes misses a function's closing brace (in
# lizard 1.24.3, a `{` in a regex literal is one trigger) and runs the span on
# into the next top-level declaration. Edits to
# that neighbour are then charged to a function the PR never touched. In
# formatted code nothing inside a function body starts at column 0, so a
# top-level declaration strictly inside a span is the tell. Pygments decides
# what is code: a column-0 `const` inside a template literal is string text.
# Limit: when the misread makes lizard drop functions instead (the same
# trigger inside a class method reports only an `(anonymous)` function), the
# edits land on a row that has no span to check, and nothing is marked.
# Limit: when the misread makes lizard drop functions instead, the edits land
# on a row that has no span to check, and nothing is marked.
SPAN_SUFFIXES = ('.ts', '.tsx', '.js', '.jsx')
DECLARATION = re.compile(r'@|(export|import|declare|abstract|async|function|const|let|var'
r'|class|interface|type|enum)\b')
Expand Down
53 changes: 29 additions & 24 deletions scripts/tests/test_impact_gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@
import unittest

DRIVER = Path(__file__).resolve().parents[1] / 'impact-gate.py'
# lizard misses this function's closing brace (a ternary returning an object
# literal after an arrow callback) and runs its span on into whatever follows.
PICK = ('export function pick(xs: readonly string[], hint?: string) {\n'
' const hinted = xs.find((c) => c === hint)\n'
" return hinted ? { hinted, via: 'hint' } : null\n"
# lizard 1.24.3 misses this function's closing brace (the `{` in the regex
# literal) and runs its span on to the next `function`, over any top-level code
# between them. 1.24.3 fixed the trigger these tests used before (a ternary
# returning an object literal); when lizard fixes this one too, the tests
# that use PICK fail and need a new trigger.
PICK = ('export function pick(s: string) {\n'
' return /\\{/.test(s)\n'
'}\n')


Expand Down Expand Up @@ -118,19 +120,22 @@ def test_pull_request_pin_refuses_a_checkout_that_is_not_the_pr_merge(self):
self.assertNotIn('sha=', result.stdout)

def test_marks_cost_drivers_whose_function_span_lizard_misread(self):
# lizard ends `pick` at the end of `after`: a ternary returning an
# object literal after an arrow callback loses the closing brace. Edits
# to `after` are then charged to `pick`, which the PR never touched.
# lizard runs `pick` on to `after` (see PICK), so the top-level code
# between them is charged to `pick`, which the PR never touched.
def branches(name, arg, result):
return (f'export function {name}({arg}: number): number {{\n'
+ ''.join(f' if ({arg} === {i}) return {i};\n' for i in range(200))
+ f' return {result}\n}}\n')

def checks(count):
return '\nexport const LIMIT = 10\n' + ''.join(
f'if (LIMIT === {i}) console.log({i})\n' for i in range(count))
after = '\nexport function after(x: number): number {\n return x + 1\n}\n'
self.commit('src/pick.ts', 'export function third(y: number): number {\n return y\n}\n\n'
+ PICK + '\nexport function after(x: number): number {\n return x + 1\n}\n')
+ PICK + checks(1) + after)
self.run_driver('prepare')
self.git('checkout', '-qb', 'feature')
self.commit('src/pick.ts', branches('third', 'y', 'y') + '\n' + PICK + '\n'
+ branches('after', 'x', 'x + 1'))
self.commit('src/pick.ts', branches('third', 'y', 'y') + '\n' + PICK + checks(200) + after)
# The grade multiplies cost by files changed; the drivers table is only
# printed above the warn percentile.
for index in range(6):
Expand Down Expand Up @@ -177,17 +182,15 @@ def test_unindented_declarations_inside_a_function_body_are_not_top_level(self):
self.assertEqual(self.load_driver().misread_functions([head], 'src/flat.ts'), set())

def test_a_span_ending_on_a_one_line_declaration_swallows_it(self):
# lizard ends `path` on the next declaration's first line. That is
# harmless when only a signature is there, but a one-line declaration
# is there whole, so edits to it are charged to `path`.
path = ('export function path(segments: string[]): string {\n'
' return `$.${segments.join(".")}`\n}\n\n')
self.commit('src/path.ts', path + 'export const LIMIT = 10\n')
self.commit('src/boundary.ts', path + 'export function next(x: number): number {\n'
# lizard ends `pick` (see PICK) on the next declaration's first line.
# That is harmless when only a signature is there, but a one-line
# declaration is there whole, so edits to it are charged to `pick`.
self.commit('src/path.ts', PICK + '\nexport const LIMIT = 10\n')
self.commit('src/boundary.ts', PICK + '\nexport function next(x: number): number {\n'
' return x\n}\n')
head = self.git('rev-parse', 'HEAD')
driver = self.load_driver()
self.assertEqual(driver.misread_functions([head], 'src/path.ts'), {('', 'path')})
self.assertEqual(driver.misread_functions([head], 'src/path.ts'), {('', 'pick')})
self.assertEqual(driver.misread_functions([head], 'src/boundary.ts'), set())

def test_a_declaration_continued_on_the_next_line_is_not_whole(self):
Expand All @@ -214,9 +217,9 @@ def test_a_declaration_continued_on_the_next_line_is_not_whole(self):
self.assertEqual(driver.declaration_lines('a.ts', source)[1], whole)

def test_marks_a_function_above_a_removed_declaration(self):
# At the base, `pick`'s span swallows `gone`; the PR deletes `gone`.
self.commit('src/pick.ts', PICK + '\nexport function gone(x: number): number {\n'
' return x\n}\n\nexport function kept(y: number): number {\n return y\n}\n')
# At the base, `pick`'s span swallows `GONE`; the PR deletes `GONE`.
self.commit('src/pick.ts', PICK + '\nexport const GONE = 1\n'
'\nexport function kept(y: number): number {\n return y\n}\n')
base = self.git('rev-parse', 'HEAD')
self.commit('src/pick.ts', PICK + '\nexport function kept(y: number): number {\n'
' return y + 1\n}\n')
Expand All @@ -225,12 +228,14 @@ def test_marks_a_function_above_a_removed_declaration(self):
self.assertNotIn(('', 'kept'), found)

def test_marks_a_function_above_appended_code(self):
# At the base `pick` ends the file; the PR appends after it.
# At the base `pick` ends the file; the PR appends a declaration and a
# function after it.
self.commit('src/pick.ts', 'export function edited(y: number): number {\n return y\n}\n\n'
+ PICK)
base = self.git('rev-parse', 'HEAD')
self.commit('src/pick.ts', 'export function edited(y: number): number {\n return y + 1\n}\n\n'
+ PICK + '\nexport function appended(x: number): number {\n return x\n}\n')
+ PICK + '\nexport const LIMIT = 10\n'
'\nexport function appended(x: number): number {\n return x\n}\n')
found = self.load_driver().misread_functions([base, 'HEAD'], 'src/pick.ts')
self.assertIn(('', 'pick'), found)
self.assertNotIn(('', 'edited'), found)
Expand Down
Loading