Skip to content

fix(eslint): detect dropped await on local core.summary aliases#43404

Merged
pelikhan merged 6 commits into
mainfrom
copilot/require-await-core-summary-write
Jul 5, 2026
Merged

fix(eslint): detect dropped await on local core.summary aliases#43404
pelikhan merged 6 commits into
mainfrom
copilot/require-await-core-summary-write

Conversation

Copilot AI commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

The require-await-core-summary-write rule had a false negative when core.summary was aliased to a local variable — getRootIdentifier on summary.write() returned "summary", which failed the CORE_ALIASES check and silently skipped the call. This pattern is live in two corpus files.

Changes

  • isCoreSummaryAlias(identifier) — new helper inside create() that uses ESLint scope analysis to check whether an identifier is a single-assignment local bound to core.summary. Two patterns are tracked:

    • const/let summary = core.summary
    • const { summary } = core / const { summary: alias } = core

    Re-assigned let bindings are rejected via ref.isWrite() && !ref.init; non-core initialisers are ignored.

  • rootsSummaryOrAlias(node) — replaces the direct rootsSummary call in ExpressionStatement. Delegates to rootsSummary first (fast path), then resolves Identifier nodes through alias lookup and handles chained calls (summary.addRaw(x).write()).

Now flagged

async function f() {
  const summary = core.summary;
  summary.write();                   // was: FN — now flagged ✓

  const { summary: s } = core;
  s.addRaw(x).write();               // was: FN — now flagged ✓
}

Still not flagged (zero false positives)

async function f() {
  let summary = core.summary;
  summary = other;
  summary.write();                   // re-assigned — source unknown, safe to skip
}

Copilot AI and others added 3 commits July 4, 2026 16:14
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
…ummary-write rule

Extends the rule to detect dropped `await` on local variable aliases of
`core.summary`, resolving the false negative described in #43324.

Two alias patterns are now recognised:
  - `const/let summary = core.summary;  summary.write()`
  - `const { summary } = core;  summary.addRaw(x).write()`

Re-assigned `let` bindings are conservatively rejected (their post-reassignment
source is unknown). Non-core initialisers (e.g. `fs.summary`) are also ignored.

Tests added: 5 new invalid cases (both corpus patterns + rename destructure +
non-async + let variant) and 3 new valid cases (awaited alias, re-assigned let,
non-core init).

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
…eview

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix false negative on local summary aliases of core.summary fix(eslint): detect dropped await on local core.summary aliases Jul 4, 2026
Copilot AI requested a review from pelikhan July 4, 2026 16:19
@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

🤖 PR Triage — Run §28715668077

Field Value
Category bug
Risk 🟢 Low
Score 54/100
Impact 26/50
Urgency 18/30
Quality 10/20
Action batch_review
Batch pr-batch:eslint-bugfix

Detects dropped await on local core.summary aliases. +80/-2 TS rule, +40 test. Low risk eslint bugfix.

Generated by 🔧 PR Triage Agent · 113.5 AIC · ⌖ 13 AIC · ⊞ 5.5K ·

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 false negative in the require-await-core-summary-write ESLint rule by detecting core.summary calls when core.summary is first assigned/destructured into a local alias, ensuring dropped await on summary.write() is correctly flagged across common corpus patterns.

Changes:

  • Added scope-based alias resolution (isCoreSummaryAlias) to identify single-assignment locals bound to core.summary / { summary } = core.
  • Extended root-tracing logic (rootsSummaryOrAlias) to recognize alias-rooted chains like summary.addRaw(x).write().
  • Added test coverage for alias and destructuring patterns, including suggestion output behavior.
Show a summary per file
File Description
eslint-factory/src/rules/require-await-core-summary-write.ts Adds scope-aware alias detection and extends summary-root tracing to include local aliases.
eslint-factory/src/rules/require-await-core-summary-write.test.ts Adds new valid/invalid cases covering aliasing, reassignment, destructuring, and suggestion behavior.

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: 2
  • Review effort level: Low

Comment on lines +125 to +130
// Pattern 1: const/let summaryVar = core.summary;
// rootsSummary covers both `core.summary` and chained inits like
// `core.summary.addRaw(x)` (which still returns a Summary object).
if (declarator.id.type === "Identifier") {
return rootsSummary(declarator.init);
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 72d11d2. Pattern 1 now explicitly rejects .write() call-expressions before calling rootsSummary: const s = core.summary.write() is no longer treated as a Summary alias. A valid test was added to codify this: const s = core.summary.write(); s.write() is not flagged.

Comment on lines +158 to +163
// Destructure with rename: const { summary: s } = core; s.write();
{
code: `async function f() { const { summary: s } = core; s.write(); }`,
errors: [{ messageId: "requireAwait", suggestions: [{ messageId: "addAwait", output: `async function f() { const { summary: s } = core; await s.write(); }` }] }],
},
// Outside async: flagged, no suggestion

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 72d11d2. Added two new invalid test cases for coreObj: direct alias (const summary = coreObj.summary; summary.write()) and destructuring (const { summary } = coreObj; summary.write()).

@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 does not have the 'implementation' label and has ≤100 new lines of code in business logic directories (default_business_additions: 0).

@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

⚠️ PR Code Quality Reviewer failed during code quality review.

@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 commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

⚠️ Test Quality Score: 78/100 — Acceptable

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

📊 Metrics (4 new tests)
Metric Value
Analyzed 4 (Go: 0, JS/TS: 4)
✅ Design 3 (75%)
⚠️ Implementation 1 (25%)
Edge/error coverage 3 (75%)
Duplicate clusters 1
Inflation No (test:prod added lines = 40:80 → 0.50)
🚨 Violations 0

Note: Test file is TypeScript (.test.ts), analyzed as JS-class vitest tests with ESLint RuleTester. No Go test files changed.

Test File Classification Issues
valid: identifiers outside CORE_ALIASES are not flagged .test.ts:101 ✅ design_test / high_value None
invalid: flagged outside async function — no suggestion offered .test.ts:110 ✅ design_test / high_value None
invalid: local alias const summary = core.summary is flagged .test.ts:132 ✅ design_test / high_value None
suggestion: inserts 'await' before the expression .test.ts:172 ⚠️ implementation_test / duplicated Repeats patterns from tests 4–6
⚠️ Flagged Tests (1)

suggestion: inserts 'await ' before the expression (.test.ts:172) — implementation_test / duplicated.

This test validates the exact fixer output strings for core.summary.write(), core.summary.addRaw(summary).write(), coreObj.summary.write(), and an IIFE form. However, these four cases are already fully exercised — with output: checks — in the earlier invalid: bare core.summary.write() is flagged, invalid: chained ..., and invalid: coreObj alias ... tests. The test adds no new behavioral scenarios and duplicates assertion patterns. Consider replacing it with a case not covered elsewhere (e.g. a deeply chained alias path) or removing it.

Verdict

Passed. 25% implementation tests (threshold: 30%). No violations.

Scoring breakdown: design coverage (3/4 × 40 = 30) + edge coverage (3/4 × 30 = 22.5) + duplication penalty (20 − 5 = 15) + no inflation (10) = 77.5 → 78.

New tests summary:

  • CORE_ALIASES tightening invariant (test valid: identifiers outside CORE_ALIASES...) — enforces that objects merely prefixed with core (e.g. coreCache, coreData, coreference) are never flagged; directly codifies the contract change from the broad /^core/i heuristic. High value.
  • Outside-async / no suggestion (test invalid: flagged outside async function...) — covers three shapes (top-level, non-async fn, non-async arrow) and correctly asserts suggestions: [], preventing a footgun where applying await outside async would produce a syntax error. High value.
  • Local alias detection (test invalid: local alias ...) — the primary new feature; excellent edge coverage: 5 invalid patterns (const, let, destructure { summary }, rename { summary: s }, non-async fn) plus 3 valid escape hatches (awaited alias, reassigned let, non-core initializer). Inline comments cite corpus patterns, aiding maintainability. High value.

References:

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 · 44.5 AIC · ⌖ 14.4 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: 78/100. 25% implementation tests (threshold: 30%). No violations.

@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 and /tdd — approving with non-blocking suggestions.

📋 Key Themes & Highlights

Key Themes

  • Scope-walk test gap: The while (currentScope.upper) path in isCoreSummaryAlias is never exercised by the new tests — all test cases use single-scope setups. A cross-scope alias (alias declared in outer function, used in inner) should be added.
  • Unbounded recursion in rootsSummaryOrAlias: The recursive call on deeply chained CallExpressions has no depth guard. Not a practical risk, but a depth cap or iterative rewrite would remove the theoretical concern.
  • Undocumented chained-init subtlety: isCoreSummaryAlias calls rootsSummary(declarator.init), which also matches core.summary.addRaw(x) as valid initialisers. This is intentional but undocumented, making it easy to misread on future maintenance.
  • Missing valid-case test for alias without .write(): summary.addRaw(x) with no chained .write() should not be flagged; no test guards against a future false positive here.

Positive Highlights

  • ✅ Excellent PR description: root cause, two corpus patterns, and zero-false-positive guarantee all clearly documented.
  • ✅ Conservative re-assignment guard (ref.isWrite() && !ref.init) is precise and well-commented.
  • ✅ Good use of the fast-path in rootsSummaryOrAlias — existing behaviour unchanged, new code is purely additive.
  • ✅ Test structure cleanly separates valid and invalid cases for each new pattern.
  • defs.length !== 1 guard prevents false positives on merged/duplicate bindings.

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

code: `function f() { const summary = core.summary; summary.write(); }`,
errors: [{ messageId: "requireAwait", suggestions: [] }],
},
],

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] No test for alias used across a nested function scope — the scope-walk (currentScope.upper) path in isCoreSummaryAlias is never exercised.

💡 Suggested test case to add inside the valid/invalid blocks
// Cross-scope: alias declared in outer, used in inner async — should be flagged
{
  code: `async function outer() {
  const summary = core.summary;
  return (async function inner() { summary.write(); })();
}`,
  errors: [{ messageId: "requireAwait", suggestions: [{ messageId: "addAwait", output: `async function outer() {
  const summary = core.summary;
  return (async function inner() { await summary.write(); })();
}` }] }],
},

Without this, the while (currentScope !== null) { currentScope = currentScope.upper; } loop is only tested with a single-scope setup and a regression here would go undetected.

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 72d11d2. Added a cross-scope invalid test: alias declared in the outer async function, write() called inside a nested inner async function. The scope-walk in isCoreSummaryAlias finds the variable via currentScope.upper and the call is correctly flagged with the await suggestion.


// Chained alias: `summary.addRaw(x).write()` — callee.object is a CallExpression
// rooted on the alias identifier.
if (node.type === "CallExpression" && node.callee.type === "MemberExpression") {

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.

[/diagnosing-bugs] rootsSummaryOrAlias has unbounded recursion on deeply nested CallExpression chains — e.g. summary.addRaw(a).addRaw(b).addRaw(c).write() calls itself once per call-chain depth.

💡 Note on risk and mitigation

In practice, ESLint AST depths for real source code are shallow (< ~20 levels), so a stack overflow is not a real-world risk. However:

  • The recursive call passes the same function, so there is no progress guarantee.
  • A dedicated while-loop or a depth guard (e.g. depth > 32 → return false) would remove the theoretical concern and make the intent explicit.
function rootsSummaryOrAlias(node: TSESTree.Node, depth = 0): boolean {
  if (depth > 32) return false; // guard against extreme nesting
  if (rootsSummary(node)) return true;
  if (node.type === "Identifier") return isCoreSummaryAlias(node);
  if (node.type === "CallExpression" && node.callee.type === "MemberExpression") {
    return rootsSummaryOrAlias(node.callee.object, depth + 1);
  }
  return false;
}

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 72d11d2. rootsSummaryOrAlias now accepts a depth parameter (default 0) and returns false when depth > 32. The recursive call passes depth + 1. Real source code is well below this limit; the guard removes the theoretical concern.

// rootsSummary covers both `core.summary` and chained inits like
// `core.summary.addRaw(x)` (which still returns a Summary object).
if (declarator.id.type === "Identifier") {
return rootsSummary(declarator.init);

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.

[/diagnosing-bugs] Pattern 1 in isCoreSummaryAlias calls rootsSummary(declarator.init), which accepts chained initialisers like core.summary.addRaw(x). That means const s = core.summary.addRaw(x) would be treated as a core.summary alias — but addRaw returns Summary, not a Promise, so this is intentional and correct. Consider adding a brief comment or a test case to make the intent explicit, since this subtlety is easy to miss in future maintenance.

💡 Suggested clarifying comment + optional test

Add to the existing comment block:

// Note: rootsSummary also accepts chained initialisers like `core.summary.addRaw(x)`
// because addRaw returns Summary — the alias still wraps a Summary object.

Optional extra valid-case test:

`async function f() { const s = core.summary.addRaw("x"); await s.write(); }`

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added in 72d11d2. Pattern 1 now has a comment block explicitly documenting that addRaw returns Summary so chained inits are valid, and that .write() is explicitly rejected because it returns Promise<Summary>. A valid test const s = core.summary.addRaw("x"); await s.write() was added to codify the chained-init intent.

code: `function f() { const summary = core.summary; summary.write(); }`,
errors: [{ messageId: "requireAwait", suggestions: [] }],
},
],

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] No test verifies what happens when the destructured alias is called directly without .write() — e.g. summary.addRaw(x) alone (no chained .write()). This is a valid case and should be in the valid block to prevent future false positives.

💡 Suggested test
// Using alias for addRaw only (no write) — must NOT be flagged
`async function f() { const { summary } = core; summary.addRaw("x"); }`

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 72d11d2. Added a valid test: async function f() { const { summary } = core; summary.addRaw("x"); } — using the alias for addRaw only (no .write()) is not flagged.

@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 · 77.1 AIC · ⌖ 5.87 AIC · ⊞ 4.8K

errors: [{ messageId: "requireAwait", suggestions: [] }],
},
],
});

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.

The new test block covers core.* aliases but not the coreObj variant. Since coreObj is in CORE_ALIASES and the fix advertises coverage for any CORE_ALIASES member, a test case like:

{
  code: `async function f() { const summary = coreObj.summary; summary.write(); }`,
  errors: [{ messageId: "requireAwait", suggestions: [{ messageId: "addAwait", output: `async function f() { const summary = coreObj.summary; await summary.write(); }` }] }],
},

would guard against a future regression there.

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 72d11d2. Added an invalid test for the coreObj direct-alias pattern: const summary = coreObj.summary; summary.write() is now flagged with the await suggestion.

@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=4; skipped_cooldown=1; nudged=0; branch_update_attempts=0; formatter_pushes=0; merge_main_scheduled=0

@gh-aw-bot

Copy link
Copy Markdown
Collaborator

pr-sous-chef
Sous-chef was invoked and will triage this PR.

Generated by 👨‍🍳 PR Sous Chef · 10.7 AIC · ⌖ 3.7 AIC · ⊞ 3.4K ·
Comment /souschef to run again

@pelikhan

pelikhan commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator

@copilot run pr-finisher skill

Copilot AI and others added 2 commits July 5, 2026 05:17
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
- Reject .write() call-expressions as alias initializers in Pattern 1
  (const s = core.summary.write() returns Promise, not Summary)
- Add depth guard (> 32) to rootsSummaryOrAlias to prevent unbounded
  recursion on deeply chained CallExpression trees
- Add clarifying comment explaining addRaw chained inits are intentionally
  accepted (addRaw returns Summary, not Promise)
- Tests: alias without .write() (no false positive)
- Tests: chained init core.summary.addRaw(x) — awaited alias is valid
- Tests: .write() as initializer is rejected — not treated as alias
- Tests: coreObj alias const summary = coreObj.summary; summary.write()
- Tests: coreObj destructuring const { summary } = coreObj; summary.write()
- Tests: cross-scope alias (declared in outer, used in inner async)

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
@pelikhan pelikhan merged commit bd0c249 into main Jul 5, 2026
6 checks passed
@pelikhan pelikhan deleted the copilot/require-await-core-summary-write branch July 5, 2026 05:26
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

🎉 This pull request is included in a new release.

Release: v0.82.3

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.

require-await-core-summary-write: false negative on local summary aliases of core.summary

4 participants