From 9a8d15635ba2c815958cf81b8a3708205278d9eb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Nov 2026 01:46:04 +0000 Subject: [PATCH 1/2] chore(deps): bump lizard Bumps the impact-gate-minor-patch group in /.github/impact-gate with 1 update: [lizard](https://github.com/terryyin/lizard). Updates `lizard` from 1.24.1 to 1.24.3 - [Release notes](https://github.com/terryyin/lizard/releases) - [Changelog](https://github.com/terryyin/lizard/blob/master/CHANGELOG.md) - [Commits](https://github.com/terryyin/lizard/compare/1.24.1...1.24.3) --- updated-dependencies: - dependency-name: lizard dependency-version: 1.24.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: impact-gate-minor-patch ... Signed-off-by: dependabot[bot] --- .github/impact-gate/requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/impact-gate/requirements.txt b/.github/impact-gate/requirements.txt index f57dfb3ca..520a4b1bf 100644 --- a/.github/impact-gate/requirements.txt +++ b/.github/impact-gate/requirements.txt @@ -2,7 +2,7 @@ # 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: @@ -15,7 +15,7 @@ impact-gate==0.4.1 \ --hash=sha256:9d9c8fb4c6f2fa56559d0530ff82569bc6d42b28ff5a77103e504e2470f198c6 \ --hash=sha256:e6dab6abec925151bd4dbffff92d4a40f742d561a6f8527b3fbe15dd68b25518 -lizard==1.24.1 \ +lizard==1.24.3 \ --hash=sha256:746060a3e9a375af7340b2d4c6aab307b8a8b4199339c36ddc7afc4188b53d1c \ --hash=sha256:c022cf1aac8170994b175e2a700f0535088f66da7e6bdf6c59fbd3b716cd10ac # via impact-gate From 22f993258f5708e6b057ee6b75d0290822accd0f Mon Sep 17 00:00:00 2001 From: James Sadler Date: Mon, 2 Nov 2026 12:45:01 +1100 Subject: [PATCH 2/2] fix(impact): hash lizard 1.24.3 and move the misread fixtures to a live trigger Dependabot moved the lizard pin to 1.24.3 but kept the 1.24.1 hashes, so `pip install --require-hashes` refused the wheel. The new hashes are the ones PyPI lists for lizard-1.24.3 (wheel and sdist), from the command in the file's header, which now names 1.24.3 too. No other package moves. lizard 1.24.3 fixes the TypeScript misread the span tests used as their trigger (a ternary returning an object literal, lizard issue #523), so four tests failed: the fixture spans were read correctly. A `{` in a regex literal still makes 1.24.3 run a span on to the next `function`, over any top-level code between them. The four fixtures now use that trigger, and the driver comment names it. --- .github/impact-gate/requirements.txt | 6 ++-- scripts/impact-gate.py | 11 +++--- scripts/tests/test_impact_gate.py | 53 +++++++++++++++------------- 3 files changed, 37 insertions(+), 33 deletions(-) diff --git a/.github/impact-gate/requirements.txt b/.github/impact-gate/requirements.txt index 520a4b1bf..a99980784 100644 --- a/.github/impact-gate/requirements.txt +++ b/.github/impact-gate/requirements.txt @@ -7,7 +7,7 @@ # 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 # @@ -16,8 +16,8 @@ impact-gate==0.4.1 \ --hash=sha256:9d9c8fb4c6f2fa56559d0530ff82569bc6d42b28ff5a77103e504e2470f198c6 \ --hash=sha256:e6dab6abec925151bd4dbffff92d4a40f742d561a6f8527b3fbe15dd68b25518 lizard==1.24.3 \ - --hash=sha256:746060a3e9a375af7340b2d4c6aab307b8a8b4199339c36ddc7afc4188b53d1c \ - --hash=sha256:c022cf1aac8170994b175e2a700f0535088f66da7e6bdf6c59fbd3b716cd10ac + --hash=sha256:7d2570ca711288388de1cf0f25512e97fbbc5e189f551c87b4e20d11d028b297 \ + --hash=sha256:f7fccd909719170e323a240cc6927bfea65984d77a8f9c0e6405be582e9861db # via impact-gate pathspec==1.1.1 \ --hash=sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a \ diff --git a/scripts/impact-gate.py b/scripts/impact-gate.py index c25688bc1..345a24c13 100644 --- a/scripts/impact-gate.py +++ b/scripts/impact-gate.py @@ -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') diff --git a/scripts/tests/test_impact_gate.py b/scripts/tests/test_impact_gate.py index 15c501051..e8f3ea85b 100644 --- a/scripts/tests/test_impact_gate.py +++ b/scripts/tests/test_impact_gate.py @@ -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') @@ -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): @@ -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): @@ -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') @@ -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)