eslint-factory: flag core.error + core.setFailed(prefix + msg) as redundant - #49496
Conversation
… pattern Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
|
✅ Design Decision Gate 🏗️ completed the design decision gate check. No ADR enforcement needed: PR does not have the 'implementation' label and has ≤100 new lines of code in business logic directories. |
|
✅ Test Quality Sentinel completed test quality analysis. |
|
🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅ |
|
✅ PR Code Quality Reviewer completed the code quality review. |
There was a problem hiding this comment.
Pull request overview
Extends the ESLint rule to detect redundant core.error() calls when core.setFailed() prefixes the same template message.
Changes:
- Adds template-literal prefix matching.
- Adds prefix and suffix test cases.
- Regenerates one unrelated workflow lock file.
Show a summary per file
| File | Description |
|---|---|
eslint-factory/src/rules/no-core-error-then-setfailed.ts |
Implements prefixed-message detection. |
eslint-factory/src/rules/no-core-error-then-setfailed.test.ts |
Tests literal/expression prefixes and suffix exclusion. |
.github/workflows/daily-byok-ollama-test.lock.yml |
Adds generated model-routing and pricing configuration. |
Review details
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 3/3 changed files
- Comments generated: 3
- Review effort level: Balanced
| function isSetFailedArgPrefixedVersion(errorArg: TSESTree.Expression, setFailedArg: TSESTree.Expression, sourceCode: SourceCode): boolean { | ||
| if (errorArg.type !== AST_NODE_TYPES.TemplateLiteral) return false; | ||
| if (setFailedArg.type !== AST_NODE_TYPES.TemplateLiteral) return false; |
| (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" |
| const exactMatch = sourceCode.getText(errorArg) === sourceCode.getText(setFailedArg); | ||
| if (!exactMatch && !isSetFailedArgPrefixedVersion(errorArg, setFailedArg, sourceCode)) continue; |
There was a problem hiding this comment.
The isSetFailedArgPrefixedVersion implementation is correct and well-guarded. The null checks for cooked values, expression count guards, and suffix/junction quasi matching all look sound. Test cases properly cover literal prefix, expression prefix, and the suffix non-match case. No blocking issues.
🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · sonnet46 · 12.5 AIC · ⌖ 7.15 AIC · ⊞ 5.3K
🧪 Test Quality Sentinel Report✅ Test Quality Score: 100/100 — Excellent
📊 Metrics (2 tests)
Verdict
Highlights:
|
There was a problem hiding this comment.
✅ Test Quality Sentinel: 100/100. 0% implementation tests (threshold: 30%). Both new test cases verify the core ESLint rule logic for detecting prefixed template literals with exact output assertions. Real-world coverage mirrors add_reaction.cjs pattern.
There was a problem hiding this comment.
Skills-Based Review 🧠
Applied /diagnosing-bugs and /tdd — requesting changes for two targeted issues.
📋 Key Themes & Highlights
Issues
- Null guard gap (
no-core-error-then-setfailed.ts:75): intermediate quasis are compared with!==without guarding forcooked === null. Sincenull !== nullisfalse, two quasis whose escape sequences are both invalid would be incorrectly treated as equal, creating false-positive lint reports. The junction-quasi check at lines 81–82 already does the right thing — apply the same pattern to the inner loop. - Missing test (
no-core-error-then-setfailed.test.ts): the expression-firsterrorArgpath (${msg}with no leading text) is not covered. SinceendsWith('')is alwaystrue, the empty-leading-quasi edge case relies entirely on theprefixExprCount > 0guard — a test would lock in this behaviour.
Positive Highlights
- ✅ Algorithm design is sound: suffix additions are correctly excluded, expression alignment via source-text comparison is robust
- ✅ Comprehensive JSDoc comment clearly lays out all six match conditions
- ✅ New tests mirror an actual codebase pattern (
add_reaction.cjs:184-185), making the intent concrete
🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · sonnet46 · 36.8 AIC · ⌖ 7.61 AIC · ⊞ 7K
Comment /matt to run again
|
|
||
| // Quasis at offsets [prefixExprCount+1 .. end] of sfTpl must equal errTpl quasis [1 .. end]. | ||
| for (let i = 1; i < errTpl.quasis.length; i++) { | ||
| if (sfTpl.quasis[prefixExprCount + i].value.cooked !== errTpl.quasis[i].value.cooked) return false; |
There was a problem hiding this comment.
[/diagnosing-bugs] Null guard missing for intermediate quasis — when a template literal contains an invalid escape sequence, cooked is null, and null !== null evaluates to false, causing two structurally different null quasis to be treated as equal (false positive).
💡 Suggested fix
const sfCooked = sfTpl.quasis[prefixExprCount + i].value.cooked;
const errCooked = errTpl.quasis[i].value.cooked;
if (sfCooked === null || errCooked === null || sfCooked !== errCooked) return false;This brings the inner-quasi check in line with the null guard already applied at lines 81–82 for the junction quasi.
@copilot please address this.
| }, | ||
| ], | ||
| }); | ||
| }); |
There was a problem hiding this comment.
[/tdd] Missing edge-case: errorArg starts with an expression (empty leading quasi). When errTpl.quasis[0].value.cooked === '' and the junction quasi ends with '', endsWith('') is always true, so the prefix guard at line 85 becomes the only safeguard. Add a test to document the expected behaviour:
💡 Suggested test case
// Expression-first error arg with expression prefix in setFailed
{
code: 'core.error(`${msg}`); core.setFailed(`ERR: ${msg}`);',
errors: [{ messageId: 'noCoreErrorThenSetFailed', ... }],
},This exercises the empty-leading-quasi path and confirms that prefixExprCount > 0 correctly drives the non-empty-prefix guard when errFirstCooked === ''.
@copilot please address this.
There was a problem hiding this comment.
Verdict: Comment (no blocking issues)
Solid, well-tested implementation of a legitimate rule improvement. Findings below are non-blocking hardening/test-coverage suggestions.
💡 Themes
- The new prefix-match heuristic in is textually-based (source-text equality of expressions, cooked-value equality of quasis). This is consistent with the existing exact-match approach in the rule but widens the matching surface, so false positives (flagging genuinely non-redundant pairs) are somewhat more likely than before, though the risk is mitigated by this only producing a suggestion, not an autofix.
- Test coverage is good for the primary new cases (literal prefix, expression prefix, suffix-not-flagged) but misses a couple of boundary conditions in the new function: zero-expression error templates with a prefix expression, and the null-guard branch.
- No correctness bugs found in the array-indexing logic itself (quasis/expressions loop bounds were checked and are consistent with invariants).
🔎 Code quality review by PR Code Quality Reviewer · auto · 64.8 AIC · ⌖ 10.6 AIC · ⊞ 7.8K
Comment /review to run again
|
|
||
| // Last errTpl.expressions.length expressions of sfTpl must match errTpl's expressions. | ||
| for (let i = 0; i < errTpl.expressions.length; i++) { | ||
| const sfExpr = sfTpl.expressions[prefixExprCount + i] as TSESTree.Expression; |
There was a problem hiding this comment.
Expression-prefix matching relies purely on textual source comparison, which can be fooled by non-obvious cases.
💡 Details
Both exactMatch and isSetFailedArgPrefixedVersion compare sourceCode.getText(expr) for equality. This was already the approach for the exact-match case, but the new prefix-matching path widens the blast radius: it now also compares multiple expressions per call pair (all of errTpl.expressions) and glues cooked quasi text together, so more real-world call sites will be auto-matched and their core.error() calls silently removed by the removeErrorCall suggestion.
Textual equality does not guarantee semantic equality — e.g. a shadowed err in nested scope, or an expression whose evaluation has a side effect visible only through equality-of-text but not equality-of-runtime-value (rare, but the existing isSideEffectFree check only protects against side effects in the removed call, not against divergent runtime values between the two call sites).
Suggested hardening: since this is a suggestion (not autofix), reviewers already vet it manually, so the risk is moderate rather than critical. Still worth documenting this limitation explicitly in the function doc comment so future maintainers don't assume textual equality implies semantic equality.
| @@ -88,6 +79,26 @@ describe("no-core-error-then-setfailed", () => { | |||
| }, | |||
There was a problem hiding this comment.
New prefix-matching branches lack tests for zero-expression error templates and null-cooked quasis, leaving two guard conditions unverified.
💡 Details
isSetFailedArgPrefixedVersion has at least two branches with no covering test case:
errTpl.expressions.length === 0(a plain literal-onlycore.error(\Failed`)) paired with asetFailedtemplate that has a prefix expression, e.g.core.setFailed(`${ERR_CODE}: Failed`). This exercisesprefixExprCount > 0with the loop overerrTpl.expressionsnever running — a distinct code path from the two invalid tests added, which both useerrTpl.expressions.length === 1`.- The
cooked === nullguard (if (junctionCooked === null || errFirstCooked === null) return false;) — reachable when a template literal contains an invalid escape sequence — has no test, so a regression that inverts or removes this null check would go undetected.
Adding two more invalid/valid cases would close this gap and lock in the intended behavior for both edge cases.
PR Triage
Note: ESLint tooling rule addition, has tests, CI green
|
|
@copilot please address the remaining review feedback, refresh the branch if GitHub allows it, and run the Current maintainer-facing blockers:
Please summarize which comments are fully addressed, whether any stale blocking reviews remain, and what still needs maintainer action.
|
…e-case tests Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
Addressed all remaining review feedback in commit Changes made:
Lock file note: The Stale blocking reviews: The Matt Pocock Skills Reviewer blocking review on null-cooked quasi guarding and expression-first edge cases is now fully addressed. No other unresolved reviewer comments require maintainer action. |
|
🎉 This pull request is included in a new release. Release: |
The
no-core-error-then-setfailedrule only fired on exact source-text matches, missing the codebase's dominant pattern wheresetFailedprepends an error code to the same message — 9 live sites, zero flagged.Changes
no-core-error-then-setfailed.ts— addsisSetFailedArgPrefixedVersion: recognises whensetFailedArgis aTemplateLiteralwhose content ends with the full content oferrorArg(also aTemplateLiteral), with only a non-empty prefix in front. The prefix may include template expressions (e.g.${ERR_API}:). Suffix/interleaved additions remain unflagged.no-core-error-then-setfailed.test.ts— moves the former valid prefix-pattern case to invalid; adds two new invalid cases (literal prefix, expression prefix); adds a suffix valid case.Now flagged (previously missed)
Remains valid (suffix adds distinct context)
Run: https://github.com/github/gh-aw/actions/runs/30689325959