fix(eslint): gate replaceWithGetErrorMessage suggestion on hasResolvableLocalBinding in no-err-stack-then-string-fallback - #49501
Conversation
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
…-string-fallback rule Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
PR Triage
Note: Draft, only lock.yml regenerated so far
|
There was a problem hiding this comment.
Pull request overview
Gates an ESLint autofix on a resolvable getErrorMessage binding to prevent runtime failures.
Changes:
- Adds scope-aware suggestion gating.
- Adds binding/no-binding tests.
- Regenerates an unrelated workflow lock file.
Show a summary per file
| File | Description |
|---|---|
no-err-stack-then-string-fallback.ts |
Adds binding resolution logic. |
no-err-stack-then-string-fallback.test.ts |
Tests suggestion availability. |
daily-byok-ollama-test.lock.yml |
Changes generated model-routing configuration. |
Review details
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Suppressed comments (1)
eslint-factory/src/rules/no-err-stack-then-string-fallback.ts:82
- When the current scope contains a later same-name lexical declaration, this loop skips its unavailable definition and continues to an outer
getErrorMessage. The inner declaration still shadows the outer binding throughout the block, so the suggested call would hit the inner binding's temporal dead zone and throw. Stop at the first scope that defines the name and return whether that definition is available; add a nested-shadow regression case.
const variable = scope.set.get(name);
if (variable && variable.defs.some(def => isDefinitionAvailableAtNode(def, node))) {
- Files reviewed: 3/3 changed files
- Comments generated: 2
- Review effort level: Balanced
| const definitionNode = definition.name ?? definition.node; | ||
| if (!definitionNode?.range || !node.range) return false; | ||
| return definitionNode.range[0] < node.range[0]; |
| (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) | ||
| GH_AW_MAX_AI_CREDITS="${{ vars.GH_AW_DEFAULT_DETECTION_MAX_AI_CREDITS || '400' }}" | ||
| printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.43/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"api.business.githubcopilot.com\",\"api.enterprise.githubcopilot.com\",\"api.github.com\",\"api.githubcopilot.com\",\"api.individual.githubcopilot.com\",\"github.com\",\"host.docker.internal\",\"raw.githubusercontent.com\",\"registry.npmjs.org\",\"telemetry.enterprise.githubcopilot.com\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"targets\":{\"copilot\":{\"host\":\"host.docker.internal:11434\"}}},\"container\":{\"imageTag\":\"0.27.43,squid=sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d,agent=sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6,api-proxy=sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1,cli-proxy=sha256:65c45ea2967984d0024f3df61bc71335658a77ede96c8d9665da7a5f33a795ab\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" | ||
| printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.43/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"api.business.githubcopilot.com\",\"api.enterprise.githubcopilot.com\",\"api.github.com\",\"api.githubcopilot.com\",\"api.individual.githubcopilot.com\",\"github.com\",\"host.docker.internal\",\"raw.githubusercontent.com\",\"registry.npmjs.org\",\"telemetry.enterprise.githubcopilot.com\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"defaultAiCreditsPricing\":{\"input\":0.000001,\"output\":0.000001},\"targets\":{\"copilot\":{\"host\":\"host.docker.internal:11434\"}},\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.43,squid=sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d,agent=sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6,api-proxy=sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1,cli-proxy=sha256:65c45ea2967984d0024f3df61bc71335658a77ede96c8d9665da7a5f33a795ab\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" |
|
✅ Design Decision Gate 🏗️ completed the design decision gate check. No ADR enforcement needed: PR #49501 does not have the 'implementation' label and has 0 new lines of code in business logic directories (threshold: 100). |
|
🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅ |
|
✅ Test Quality Sentinel completed test quality analysis. |
|
|
There was a problem hiding this comment.
Skills-Based Review 🧠
Applied /diagnosing-bugs and /tdd — no blocking issues; one gap worth addressing.
📋 Key Themes & Highlights
Key Themes
- Root cause well addressed: gating the suggestion on
hasResolvableLocalBindingmatches the pattern already used in three sibling rules — consistent and correct. - Regression test added: the new
suggestions: []case coveringcopilot_sdk_driver.cjsdirectly proves the fix. - ESM branch untested:
isDefinitionAvailableAtNodereturns early forImportBindingwithout a test — see inline comment.
Positive Highlights
- ✅ Identical guard pattern to sibling rules — easy to audit and maintain
- ✅ Existing tests updated to include the import so suggestion coverage is preserved
- ✅ Diagnostic still fires unconditionally; only the unsafe autofix is suppressed
🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · sonnet46 · 24.5 AIC · ⌖ 12.5 AIC · ⊞ 7K
Comment /matt to run again
| ], | ||
| }); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
[/tdd] Tests only cover the CJS require path — the ESM ImportBinding branch of isDefinitionAvailableAtNode is untested.
isDefinitionAvailableAtNode returns true immediately for ImportBinding, so the suggestion will fire for ESM imports too. Adding one test case with import { getErrorMessage } from '...' would confirm that branch and prevent a silent regression if the early-return is removed.
💡 Suggested test snippet
{
code: `import { getErrorMessage } from './error_helpers.cjs'; const msg = err instanceof Error ? err.stack : String(err);`,
errors: [{
messageId: 'preferGetErrorMessage',
data: { errorVar: 'err' },
suggestions: [{ messageId: 'replaceWithGetErrorMessage', data: { errorVar: 'err' }, output: `import { getErrorMessage } from './error_helpers.cjs'; const msg = getErrorMessage(err);` }],
}],
}Use an ESM rule tester instance with { parserOptions: { sourceType: 'module' } }.
@copilot please address this.
There was a problem hiding this comment.
Review: no-err-stack-then-string-fallback ESLint rule fix
The core logic — gating the replaceWithGetErrorMessage suggestion on hasResolvableLocalBinding — is a sound correctness fix. Tests are comprehensive.
Two prior comments flag issues:
- Self-referential binding edge case (line 52): the range check may consider a
const getErrorMessage = ...binding available during its own initializer — a narrow but real edge case. - Lock file churn: the generated lock file includes a large unrelated model-routing policy update alongside the ESLint fix.
Overall the change is correct and non-blocking.
🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · sonnet46 · 21.6 AIC · ⌖ 9.79 AIC · ⊞ 5.3K
🧪 Test Quality Sentinel Report✅ Test Quality Score: 92/100 — Excellent
📊 Metrics (8 tests)
📋 Test Classifications
🎯 Analysis SummaryPR Fix: Gates the Test Coverage:
Why This Matters:
Verdict
|
There was a problem hiding this comment.
✅ Test Quality Sentinel: 92/100. 0% implementation tests (threshold: 30%). All 8 tests are design tests verifying behavioral contracts. The new test at line 195 is a critical regression safeguard for the PR's core fix—it validates that suggestions are only offered when getErrorMessage is resolvable in scope.
|
@copilot run pr-finisher skill |
…bleAtNode A binding is inside its own variable declarator's initializer when the reported node's range falls within definition.node's range. Guard against this so that `const getErrorMessage = err instanceof Error ? err.stack : String(err)` does not receive a replaceWithGetErrorMessage suggestion that would produce `const getErrorMessage = getErrorMessage(err)` (TDZ ReferenceError). Add regression test covering the TDZ case. Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Done. Here's what was addressed: TDZ fix ( Lock file ( Commit:
|
|
@copilot quick triage for this PR:
|
|
🎉 This pull request is included in a new release. Release: |
no-err-stack-then-string-fallbackunconditionally offered areplaceWithGetErrorMessageautofix suggestion regardless of whethergetErrorMessagewas imported in the file. Accepting the suggestion in a file without the import (e.g.copilot_sdk_driver.cjs) would silently replace the last-resort error handler withgetErrorMessage(err), throwingReferenceErrorat runtime. Three sibling rules already guard against this withhasResolvableLocalBinding.Changes
no-err-stack-then-string-fallback.ts: AddisDefinitionAvailableAtNode+hasResolvableLocalBinding(identical pattern tono-json-stringify-error,prefer-get-error-message-over-string,no-caught-error-interpolation). Thesuggestarray is now[]whengetErrorMessageis not in scope; diagnostic still fires unconditionally.no-err-stack-then-string-fallback.test.ts:const { getErrorMessage } = require("./error_helpers.cjs")so suggestion coverage is preserved.suggestions: []whengetErrorMessageis not in scope, including the exactcopilot_sdk_driver.cjspattern from the issue.