Resolve require-async-entrypoint-catch by binding, not bare name#43349
Conversation
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
require-async-entrypoint-catch by binding, not bare name
|
|
|
✅ Test Quality Sentinel completed test quality analysis. |
|
🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅ |
|
✅ Design Decision Gate 🏗️ completed the design decision gate check. No ADR enforcement needed: PR #43349 does not have the 'implementation' label and has 0 new lines of code in business logic directories (threshold: 100). |
There was a problem hiding this comment.
Pull request overview
This PR makes the require-async-entrypoint-catch ESLint rule scope-aware by resolving identifier callees to their actual bindings via ESLint scope lookup, preventing false positives/negatives when a module-scope async function name is shadowed by a local binding.
Changes:
- Replace module-wide name tracking with scope/binding resolution so
Identifiercallees are matched to the correctFunctionDeclaration. - Only report when the resolved binding is a module-scope async
FunctionDeclaration, stopping at the first shadowing binding. - Add RuleTester coverage for shadowed local sync and shadowed local async bindings with the same name.
Show a summary per file
| File | Description |
|---|---|
| eslint-factory/src/rules/require-async-entrypoint-catch.ts | Switches entrypoint detection from name-based tracking to scope-resolved binding checks. |
| eslint-factory/src/rules/require-async-entrypoint-catch.test.ts | Adds valid test cases ensuring shadowed locals don’t trigger module-scope entrypoint reports. |
Review details
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 2/2 changed files
- Comments generated: 0
- Review effort level: Low
🧪 Test Quality Sentinel Report✅ Test Quality Score: 100/100 — Excellent
📊 Metrics (2 tests)
Both new test cases are valid-case scenarios added inside an existing
Verdict
References: Warning Firewall blocked 1 domainThe following domain was blocked by the firewall during workflow execution:
network:
allowed:
- defaults
- "awmgmcpg"See Network Configuration for more information.
|
There was a problem hiding this comment.
Skills-Based Review 🧠
Applied /diagnosing-bugs, /tdd, and /codebase-design — no blocking issues, but a few targeted improvements are worth addressing.
📋 Key Themes & Highlights
Key Themes
- False-negative coverage gap: The two new valid tests cover the shadowing cases, but there is no paired invalid test confirming that a module-scope
report()call is still flagged when a sibling function shadows the name. Adding this completes the regression story. defs[0]assumption:getResolvedFunctionDeclarationreads only the first definition; in the presence ofvarre-declarations or merged TypeScript declarations, a later def that is a FunctionDeclaration would be missed. UsingArray.findover all defs is safer and explicit.SourceCodeScopetype locality: The inferred type alias is declared insidecreate(), hiding it from callers and tests. A module-level alias (or the publicTSESLint.Scope.Scope) is easier to navigate and reuse.
Positive Highlights
- ✅ Root cause correctly identified and addressed — replacing a global name-set with scope-aware binding resolution eliminates the entire class of false positives.
- ✅ The scope-walk logic is clean and short; early-exit on the first shadowing definition prevents unnecessary traversal.
- ✅ The two new valid test cases are well-commented and directly mirror the motivating example in the PR description.
- ✅ Removing the two-pass approach (collect then check) simplifies the rule to a single-pass visitor.
🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · 52.5 AIC · ⌖ 5.61 AIC · ⊞ 6.6K
Comment /matt to run again
| function buildThing() { | ||
| async function report() { return 1; } | ||
| report(); | ||
| }`, |
There was a problem hiding this comment.
[/tdd] Missing regression test: no case verifies that a module-scope report() call is still flagged when a same-name shadow exists in a sibling function. Without this, a future reader cannot tell whether the scope-walk might accidentally suppress real positives.
💡 Suggested invalid test to add
{
// Module-scope async is still flagged even though another function shadows the name locally.
code: `async function report() { return 42; }
function buildThing() {
const report = () => { return 1; };
}
report();`,
errors: [{ messageId: "requireCatch", data: { name: "report" } }],
},This is the complementary case to the two valid tests added here: the module-scope entrypoint should still produce an error when called from the module scope alongside a sibling that shadows the name. This guards against false negatives.
@copilot please address this.
There was a problem hiding this comment.
Fixed in 0fd21c5: added the sibling-shadow invalid regression so the module-scope report() call is still flagged when a sibling function shadows the name locally.
| const variable = scope.set.get(identifier.name); | ||
| const definition = variable?.defs[0]; | ||
|
|
||
| if (definition?.type === "FunctionName" && definition.node.type === AST_NODE_TYPES.FunctionDeclaration) { |
There was a problem hiding this comment.
[/diagnosing-bugs] variable?.defs[0] silently ignores multiply-defined bindings — if a variable is declared more than once (e.g. var re-declarations or merged declaration files in TypeScript), only the first def is examined. The subsequent if (variable?.defs.length) early-exit also fires for length === 0 on an empty array (though in practice ESLint populates defs), so the logic is subtler than it looks.
💡 Suggested hardening
Consider iterating over all defs before concluding the binding is not a FunctionDeclaration:
const hasDefs = variable && variable.defs.length > 0;
const funcDef = variable?.defs.find(
d => d.type === "FunctionName" && d.node.type === AST_NODE_TYPES.FunctionDeclaration
);
if (funcDef) return funcDef.node as TSESTree.FunctionDeclaration;
if (hasDefs) return null; // shadowed by a non-function bindingThis makes the intent explicit: stop at the first scope that knows the name, whether or not it is a function.
@copilot please address this.
There was a problem hiding this comment.
Fixed in 0fd21c5: the scope lookup now uses a FunctionDeclaration-specific predicate across variable.defs instead of only inspecting defs[0], while still stopping at the first scope that binds the name.
|
|
||
| // Names of async functions declared in this module. | ||
| const asyncFunctionNames = new Set<string>(); | ||
| type SourceCodeScope = ReturnType<typeof sourceCode.getScope>; |
There was a problem hiding this comment.
[/codebase-design] type SourceCodeScope is declared inside create(), making it invisible to tests and other helpers. Moving it to module scope (or using the public TSESLint.Scope.Scope type from @typescript-eslint/utils) would make the type reusable and self-documenting.
💡 Suggestion
import { TSESLint } from "`@typescript-eslint/utils`";
// module-level alias, visible to tests and future helpers
type SourceCodeScope = TSESLint.Scope.Scope;If the inferred type from sourceCode.getScope differs from the public API type, document why so future maintainers understand the constraint.
@copilot please address this.
There was a problem hiding this comment.
Fixed in 0fd21c5: SourceCodeScope now lives at module scope and uses TSESLint.Scope.Scope so the alias is reusable outside create().
There was a problem hiding this comment.
Review
The scope-aware binding resolution is the correct ESLint pattern and the implementation is sound.
What was checked
getResolvedFunctionDeclarationlogic — starts at call-site scope, walks up, stops at first shadowing binding (any non-FunctionNamedef), returnsnullfor shadowed names. ✅- Reporter guard (line 74) — reports only when the resolved declaration is
asyncAND its parent isProgram. Correctly handles nested helpers and any shadowed declaration. ✅ - Fixer —
insertTextAfteron theCallExpressionnode correctly producesfn().catch(...)with the original;unchanged. ✅ - New tests — both shadowing cases (local
constand nestedasync functionwith the same name) cover the core bug being fixed. ✅ - Existing invalid cases — still pass since module-scope
FunctionDeclarationresolves correctly through the scope chain. ✅
No bugs, no security issues, no regressions identified. Clean fix.
🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · 63 AIC · ⌖ 5.78 AIC · ⊞ 4.9K
|
@copilot please run the
|
Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
Addressed in 0fd21c5: the sibling-shadow invalid regression is in place, and |
require-async-entrypoint-catchwas matching bare calls by identifier text alone, so shadowed same-name locals could be reported as module-scope async entrypoints. This made the rule unsound in both directions: false positives for local sync bindings and missed distinctions between separate same-name bindings.Scope-aware entrypoint matching
FunctionDeclaration.const, function, or other definitions do not fall through to an outer same-name entrypoint.Rule coverage for shadowing
RuleTestercases for a shadowed local synchronous binding with the same name.RuleTestercases for a shadowed nested async binding with the same name.main().Behavioral effect