Skip to content

Fix TypeScript never-narrowing in require-async-entrypoint-catch for setup/js lint path#43440

Merged
pelikhan merged 2 commits into
mainfrom
copilot/eslint-monster-fix-eslint-factory-build
Jul 5, 2026
Merged

Fix TypeScript never-narrowing in require-async-entrypoint-catch for setup/js lint path#43440
pelikhan merged 2 commits into
mainfrom
copilot/eslint-monster-fix-eslint-factory-build

Conversation

Copilot AI commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

eslint-factory was failing to build because require-async-entrypoint-catch relied on definition narrowing that now collapses to never under current TypeScript control-flow. This blocked lint:setup-js before ESLint could run on actions/setup/js.

  • Rule typing/narrowing cleanup

    • Replaced ad-hoc Definition intersections with @typescript-eslint scope definition types:
      • TSESLint.Scope.Definitions.FunctionNameDefinition
      • TSESLint.Scope.Definitions.VariableDefinition
    • Updated type guards to narrow against TSESLint.Scope.DefinitionType.*, eliminating never access paths.
  • AST declaration access fix

    • Switched variable entrypoint declaration lookup from definition.node.parent to typed definition.parent.
    • Preserved existing async entrypoint detection behavior while making the path type-safe.
  • Targeted regression coverage

    • Added a focused invalid-case test for a module-scope async entrypoint declared in a multi-declarator const, ensuring the corrected declaration path remains enforced.
type VariableDefinition = TSESLint.Scope.Definitions.VariableDefinition;

function isAsyncVariableEntrypoint(definition: VariableDefinition): boolean {
  const declaration = definition.parent;
  if (!declaration || !isModuleScopeVariableDeclaration(declaration)) return false;
  const init = definition.node.init;
  return (init?.type === AST_NODE_TYPES.FunctionExpression || init?.type === AST_NODE_TYPES.ArrowFunctionExpression) && init.async;
}

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix ESLint factory build for TypeScript narrowing errors Fix TypeScript never-narrowing in require-async-entrypoint-catch for setup/js lint path Jul 4, 2026
Copilot AI requested a review from pelikhan July 4, 2026 22:29
@github-actions

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

🤖 PR Triage — Run §28725134784

Field Value
Category bug — TypeScript narrowing fix in eslint-factory
Risk 🟢 Low
Score 49 / 100
Breakdown Impact 24 · Urgency 14 · Quality 11
Action batch_review — bundle with pr-batch:eslint-bugfix
CI No checks recorded yet (draft)
Batch pr-batch:eslint-bugfix with #43404

Notes: Small targeted fix (2 files, +31/-12). Adds a test covering the corrected narrowing. Undraft to enable CI and expedite batch review with #43404.

Generated by 🔧 PR Triage Agent · 74.9 AIC · ⌖ 8.42 AIC · ⊞ 5.5K ·

@github-actions

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Thanks for this clean fix to require-async-entrypoint-catch! 🎉

The change is tight and well-reasoned — replacing the ad-hoc Definition intersection types with the first-class TSESLint.Scope.Definitions.* types is the right call, and switching definition.node.parentdefinition.parent for the variable declaration lookup is a meaningful correctness fix that eliminates the never-collapse under current TypeScript control-flow analysis.

The new invalid-case test for the multi-declarator const scenario is exactly the regression coverage this path needed. Everything looks ready for review.

Generated by ✅ Contribution Check · 150.4 AIC · ⌖ 11.6 AIC · ⊞ 6.3K ·

@pelikhan pelikhan marked this pull request as ready for review July 5, 2026 03:11
Copilot AI review requested due to automatic review settings July 5, 2026 03:11

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a TypeScript control-flow narrowing regression in the require-async-entrypoint-catch ESLint rule that caused eslint-factory to fail to build (blocking lint:setup-js) by aligning scope-definition typing/narrowing with @typescript-eslint’s official definition types.

Changes:

  • Replaced ad-hoc TSESLint.Scope.Definition & { ... } intersections with TSESLint.Scope.Definitions.* types and narrowed via TSESLint.Scope.DefinitionType.*.
  • Switched variable entrypoint declaration lookup to the typed definition.parent path to avoid never-narrowing and preserve behavior.
  • Added a regression test covering a module-scope async entrypoint declared in a multi-declarator const.
Show a summary per file
File Description
eslint-factory/src/rules/require-async-entrypoint-catch.ts Updates scope-definition typing and declaration access to avoid TS never narrowing while keeping async entrypoint detection behavior.
eslint-factory/src/rules/require-async-entrypoint-catch.test.ts Adds a targeted invalid-case regression test for multi-declarator module-scope async entrypoints.

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

@github-actions

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

@github-actions

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Design Decision Gate 🏗️ completed the design decision gate check.

No ADR enforcement needed: PR #43440 does not have the 'implementation' label and has 0 new lines of code in business logic directories (threshold: 100).

@github-actions

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

@github-actions

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

@github-actions github-actions Bot mentioned this pull request Jul 5, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skills-Based Review 🧠

Applied /diagnosing-bugs, /tdd, and /codebase-design — leaving 3 comments, no blocking issues.

📋 Key Themes & Highlights

Key Themes

  • Test completeness (/tdd): The new regression test only exercises the invalid (firing) path. Adding a paired valid case would confirm the definition.parent fix doesn't over-flag the handled form.
  • Type consistency (/codebase-design): FunctionDeclarationDefinition carries a redundant { node: TSESTree.FunctionDeclaration } intersection that VariableDefinition correctly drops; worth checking if it's still needed.
  • Test naming (/tdd): Minor — the new test name describes the scenario but not the expected outcome, departing slightly from the spec-style naming used in the rest of the file.

Positive Highlights

  • ✅ Root cause squarely addressed: replacing ad-hoc string-literal type guards with TSESLint.Scope.DefinitionType.* enum values is the right fix, not just a workaround.
  • definition.parent swap is clean and makes the intent obvious — the typed accessor removes the need to manually re-check .type === AST_NODE_TYPES.VariableDeclaration.
  • ✅ Regression test added — the PR does not leave the fix unvalidated.
  • ✅ Small, focused diff: 6 lines added in the source, 25 in the test — easy to review and revert if needed.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · 48 AIC · ⌖ 6.07 AIC · ⊞ 6.6K
Comment /matt to run again


it("invalid: bare call to async function in multi-declarator module-scope const is flagged", () => {
cjsRuleTester.run("require-async-entrypoint-catch", requireAsyncEntrypointCatchRule, {
valid: [],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] The new test block only covers the invalid case — valid: [] leaves the mirror path untested. Adding a valid case (multi-declarator const with .catch()) would confirm the rule does not over-flag and that definition.parent works for both branches.

💡 Suggested addition
valid: [
  {
    code: `const helper = () => 1, main = async () => { return 42; };
main().catch(err => { console.error(err); process.exitCode = 1; });`,
  },
],

Without this, the test only checks that the rule fires, not that it correctly accepts the handled form after the definition.parent fix.

@copilot please address this.

type: "Variable";
node: TSESTree.VariableDeclarator;
};
type FunctionDeclarationDefinition = TSESLint.Scope.Definitions.FunctionNameDefinition & { node: TSESTree.FunctionDeclaration };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/codebase-design] FunctionDeclarationDefinition still intersects with an explicit { node: TSESTree.FunctionDeclaration } override while VariableDefinition uses the library type as-is. If TSESLint.Scope.Definitions.FunctionNameDefinition already types node as TSESTree.FunctionDeclaration, the extra intersection is redundant and will silently diverge if the upstream type ever changes.

💡 Investigate and simplify

Check whether FunctionNameDefinition.node is already TSESTree.FunctionDeclaration in the installed @typescript-eslint/utils version. If so, simplify to:

type FunctionDeclarationDefinition = TSESLint.Scope.Definitions.FunctionNameDefinition;

This mirrors how VariableDefinition is defined on line 8 and removes a maintenance hazard.

@copilot please address this.

});
});

it("invalid: bare call to async function in multi-declarator module-scope const is flagged", () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] The test name describes the fix trigger (multi-declarator const) but not the expected outcome. Renaming it to follow the Arrange/Act/Assert convention makes it self-documenting and easier to diagnose in CI output.

💡 Suggested rename
it("invalid: bare call to async entrypoint in multi-declarator const reports requireCatch", () => {

The pattern [valid|invalid]: <condition> [reports|does not report] <messageId> matches the spec-style naming used throughout the rest of the file.

@copilot please address this.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · 55.5 AIC · ⌖ 6.03 AIC · ⊞ 4.9K

type: "Variable";
node: TSESTree.VariableDeclarator;
};
type FunctionDeclarationDefinition = TSESLint.Scope.Definitions.FunctionNameDefinition & { node: TSESTree.FunctionDeclaration };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call using TSESLint.Scope.Definitions.FunctionNameDefinition directly. One note: the intersection & { node: TSESTree.FunctionDeclaration } is still needed here since FunctionNameDefinition.node is typed more broadly (TSESTree.FunctionDeclaration | TSESTree.TSDeclareFunction in some versions). If the upstream type already narrows to FunctionDeclaration, this intersection becomes a no-op — either way, no harm. But it is worth confirming with the installed version that the constraint is redundant rather than load-bearing.

@github-actions

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel Report

Test Quality Score: 90/100 - Excellent

Analyzed 1 test(s): 1 design, 0 implementation, 0 violation(s).

Metrics:

  • Analyzed: 1 (Go: 0, JS: 1)
  • Design tests: 1 (100%)
  • Implementation tests: 0 (0%)
  • Edge/error coverage: 1 (100%)
  • Duplicate clusters: 0
  • Inflation: YES (25 test additions vs 6 prod additions, 4.2:1)
  • Violations: 0

Test: invalid: bare call to async function in multi-declarator module-scope const is flagged in eslint-factory/src/rules/require-async-entrypoint-catch.test.ts:415 - Classification: design_test / high_value. Note: 4.2:1 inflation ratio, but this is an artifact of the ESLint RuleTester pattern requiring verbose errors/suggestions objects.

Verdict: PASSED. 0% implementation tests (threshold: 30%). The new test covers a previously-broken edge case (multi-declarator const statements) and validates both error detection and the auto-fix suggestion output.

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • awmgmcpg

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "awmgmcpg"

See Network Configuration for more information.

🧪 Test quality analysis by Test Quality Sentinel · 66.5 AIC · ⌖ 11.2 AIC · ⊞ 6.8K ·
Comment /review to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Test Quality Sentinel: 90/100. 0% implementation tests (threshold: 30%). The new test directly targets the fixed edge case and validates auto-fix output.

@pelikhan

pelikhan commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator

/souschef

@github-actions

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

PR Sous Chef finished PR sous-chef nudges.

processed=5; skipped_checks_running=0; skipped_last_comment_from_sous_chef=0; skipped_cooldown=3; nudged=2; branch_update_attempts=0; formatter_pushes=0; merge_main_scheduled=0

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking observations

The TypeScript narrowing fix is mechanically correct — replacing ad-hoc string literal intersection types with canonical TSESLint.Scope.Definitions.* types is the right approach, and switching definition.node.parentdefinition.parent correctly resolves the never-collapse bug. The targeted test for the multi-declarator case is a good addition.

Findings (3 non-blocking)
  1. Missing ESM test coverage (line 416, test file) — the new multi-declarator test only runs in CJS mode, leaving the ExportNamedDeclaration branch of isModuleScopeVariableDeclaration unverified for this scenario.

  2. Removed declaration type guard (line 59, source) — the declaration.type !== AST_NODE_TYPES.VariableDeclaration check was dropped. While the type alias now guarantees this statically, the downstream isModuleScopeVariableDeclaration accesses node.parent.type without guarding against a null/undefined parent. A defensive guard there would add robustness.

  3. Redundant type intersection (line 7, source) — FunctionNameDefinition & { node: TSESTree.FunctionDeclaration } duplicates a field already present in the upstream type. Either remove the intersection or add a comment explaining the intent.

🔎 Code quality review by PR Code Quality Reviewer · 159.2 AIC · ⌖ 8.38 AIC · ⊞ 5.4K
Comment /review to run again

});

it("invalid: bare call to async function in multi-declarator module-scope const is flagged", () => {
cjsRuleTester.run("require-async-entrypoint-catch", requireAsyncEntrypointCatchRule, {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing ESM regression coverage: the multi-declarator fix is only tested in CJS mode, leaving the ExportNamedDeclaration branch of isModuleScopeVariableDeclaration unexercised for this new case.

💡 Add an ESM test variant

isModuleScopeVariableDeclaration has two branches:

return node.parent.type === AST_NODE_TYPES.Program ||
  (node.parent.type === AST_NODE_TYPES.ExportNamedDeclaration &&
   node.parent.parent.type === AST_NODE_TYPES.Program);

The new test only exercises the first (parent.type === Program) via CJS. A regression in the ExportNamedDeclaration path for multi-declarator export const helper = ..., main = async () => {} would not be caught. Add an ESM variant using esmRuleTester:

it("invalid: bare call in multi-declarator exported const is flagged (ESM)", () => {
  esmRuleTester.run("require-async-entrypoint-catch", requireAsyncEntrypointCatchRule, {
    valid: [],
    invalid: [
      {
        code: `export const helper = () => 1, main = async () => { return 42; };\nmain();`,
        errors: [{ messageId: "requireCatch", data: { name: "main" } }],
      },
    ],
  });
});

const declaration = definition.node.parent;
if (!declaration || declaration.type !== AST_NODE_TYPES.VariableDeclaration || !isModuleScopeVariableDeclaration(declaration)) return false;
const declaration = definition.parent;
if (!declaration || !isModuleScopeVariableDeclaration(declaration)) return false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Silent implicit type change in definition.parent: switching from definition.node.parent to definition.parent relies on the upstream VariableDefinition.parent type always being a VariableDeclaration, but this invariant is not verified and the type is not checked at runtime.

💡 Why this matters

The old code had an explicit guard:

if (!declaration || declaration.type !== AST_NODE_TYPES.VariableDeclaration || ...)

The new code drops the declaration.type !== AST_NODE_TYPES.VariableDeclaration check entirely:

const declaration = definition.parent;
if (!declaration || !isModuleScopeVariableDeclaration(declaration)) return false;

TSESLint.Scope.Definitions.VariableDefinition.parent is typed as TSESTree.VariableDeclaration in the library, so TypeScript won't complain — but isModuleScopeVariableDeclaration calls node.parent.type without any null check on node.parent:

function isModuleScopeVariableDeclaration(node: TSESTree.VariableDeclaration): boolean {
  return node.parent.type === AST_NODE_TYPES.Program || ...
}

If node.parent is ever null or undefined (e.g., detached AST nodes in edge-case rule scenarios), this throws at runtime instead of returning false. The removed declaration.type check was redundant given the new type, but the node.parent null guard was providing defensive depth that's now removed one layer up. At minimum, isModuleScopeVariableDeclaration itself should guard against a missing parent.

type: "Variable";
node: TSESTree.VariableDeclarator;
};
type FunctionDeclarationDefinition = TSESLint.Scope.Definitions.FunctionNameDefinition & { node: TSESTree.FunctionDeclaration };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant node intersection on FunctionDeclarationDefinition: FunctionNameDefinition already types node as TSESTree.FunctionDeclaration, so the intersection & { node: TSESTree.FunctionDeclaration } is a no-op and creates confusion about what value the intersection actually adds.

💡 Simplify the type alias

TSESLint.Scope.Definitions.FunctionNameDefinition already declares node: TSESTree.FunctionDeclaration. Intersecting it with { node: TSESTree.FunctionDeclaration } doesn't narrow anything further — it duplicates the same field. Simplify to:

type FunctionDeclarationDefinition = TSESLint.Scope.Definitions.FunctionNameDefinition;

If the purpose of keeping the intersection is to be explicit or guard against future upstream type drift, add a comment explaining the intent. Otherwise the redundancy is misleading — a reader will wonder what additional narrowing is happening here.


function isVariableDefinition(definition: TSESLint.Scope.Definition): definition is VariableDefinition {
return definition.type === "Variable" && definition.node.type === AST_NODE_TYPES.VariableDeclarator;
return definition.type === TSESLint.Scope.DefinitionType.Variable && definition.node.type === AST_NODE_TYPES.VariableDeclarator;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dead runtime check in isVariableDefinition: after switching to TSESLint.Scope.Definitions.VariableDefinition, definition.node is already typed as TSESTree.VariableDeclarator, so definition.node.type === AST_NODE_TYPES.VariableDeclarator is always true when the first condition passes — this check is unreachable dead code.

💡 Remove or explain the redundant check

Before the type alias change, the explicit runtime check provided a meaningful runtime narrowing guard. Now that VariableDefinition.node is typed as TSESTree.VariableDeclarator by the upstream library, it cannot be anything else. Simplify to:

function isVariableDefinition(definition: TSESLint.Scope.Definition): definition is VariableDefinition {
  return definition.type === TSESLint.Scope.DefinitionType.Variable;
}

The type-guard cast to VariableDefinition will already ensure callers see the narrowed type correctly.

});
});

it("invalid: bare call to async function in multi-declarator module-scope const is flagged", () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New test missing valid-case for non-async sibling declarator: the test only asserts main() is flagged, but doesn't verify that helper() — the non-async sibling in the same const statement — is not incorrectly flagged.

💡 Add a valid case to fully exercise the per-declarator discrimination

Without a valid entry for helper(), the test doesn't confirm that isAsyncVariableEntrypoint correctly ignores the sync sibling. Add:

valid: [
  {
    code: `const helper = () => 1, main = async () => { return 42; };
helper();`,
  },
],

This ensures the rule doesn't accidentally flag all declarators in a multi-declarator const rather than only the async ones.

@pelikhan

pelikhan commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator

@copilot run pr-finisher skill

@pelikhan pelikhan merged commit 9d8eb29 into main Jul 5, 2026
56 of 57 checks passed
@pelikhan pelikhan deleted the copilot/eslint-monster-fix-eslint-factory-build branch July 5, 2026 04:50
Copilot stopped work on behalf of pelikhan due to an error July 5, 2026 04:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[eslint-monster] ESLint factory build fix: resolve TypeScript never-narrowing errors in require-async-entrypoint-catch for actions/setup/js lint

3 participants