Skip to content

fix(eslint): harden require-await-core-summary-write for compound expression forms#43507

Open
pelikhan with Copilot wants to merge 3 commits into
mainfrom
copilot/require-await-core-summary-write
Open

fix(eslint): harden require-await-core-summary-write for compound expression forms#43507
pelikhan with Copilot wants to merge 3 commits into
mainfrom
copilot/require-await-core-summary-write

Conversation

Copilot AI commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

The require-await-core-summary-write rule only inspected direct ExpressionStatement → CallExpression nodes, silently missing Promise<Summary> drops in compound expression forms:

async function f() {
  cond && core.summary.write();           // LogicalExpression  — FN
  cond ? core.summary.write() : noop();   // ConditionalExpression — FN
  (log(), core.summary.write());          // SequenceExpression — FN
  core.summary.write().then(() => {});    // .then() chain — FN
}

Changes

  • collectBareWriteCalls(expr) — new recursive helper inside create() that replaces the inline guard. Unwraps:

    • LogicalExpression → right operand only
    • ConditionalExpression → both branches
    • SequenceExpression → last element (with defensive empty-array guard)
    • UnaryExpression[void] → immediately returns [] (deliberate-discard, exempt)
    • .then()/.catch()/.finally() chains on a write() call → flags the outer expression; restricted to known Promise method names to avoid false positives on arbitrary chained calls
  • ExpressionStatement visitor — refactored to call collectBareWriteCalls and loop over results; the await-insertion suggestion continues to work correctly for each flagged node within its compound context.

  • Tests — new it blocks for void exemption (valid) and all four compound forms (invalid), including a double-error case where both branches of a conditional call write().

…ression forms

- Add collectBareWriteCalls() helper to unwrap LogicalExpression (right
  operand), ConditionalExpression (both branches), and SequenceExpression
  (last element) before the CallExpression check
- Detect .then()/.catch()/.finally() chains on write() calls and flag the
  outer expression (restricted to known Promise methods to avoid FPs)
- Exempt void core.summary.write() as the idiomatic deliberate-discard marker
- Add invalid tests for Logical / Conditional / Sequence / .then() forms
- Add valid test for void exemption

Closes #43326

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix require-await rule to detect discarded promises in expressions fix(eslint): harden require-await-core-summary-write for compound expression forms Jul 5, 2026
Copilot AI requested a review from pelikhan July 5, 2026 08:19
@pelikhan pelikhan marked this pull request as ready for review July 5, 2026 08:25
Copilot AI review requested due to automatic review settings July 5, 2026 08:25

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

This PR strengthens the require-await-core-summary-write ESLint rule so it detects dropped Promise<Summary> values from core.summary.write() even when the call is embedded in compound expressions (logical/conditional/sequence expressions and Promise chain methods), and adds tests for these previously missed forms.

Changes:

  • Add collectBareWriteCalls(expr) helper to recursively find bare write() calls inside several compound expression shapes.
  • Refactor the ExpressionStatement visitor to report (and suggest await for) each matched call expression.
  • Add new tests covering compound-expression false negatives and void deliberate-discard exemption.
Show a summary per file
File Description
eslint-factory/src/rules/require-await-core-summary-write.ts Adds recursive collector and refactors reporting/suggestions to catch compound-expression dropped write() promises.
eslint-factory/src/rules/require-await-core-summary-write.test.ts Adds new valid/invalid cases for compound expressions and void exemption.

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

Comment on lines +199 to +202
// Logical short-circuit: only the right operand can be the dropped promise
if (expr.type === "LogicalExpression") {
return collectBareWriteCalls(expr.right);
}
Comment on lines +209 to +217
// Sequence: only the last expression is the "result" value.
// SequenceExpression always has ≥2 elements per the ECMAScript spec, but
// guard defensively since the type signature allows an empty array.
if (expr.type === "SequenceExpression") {
const { expressions } = expr;
if (expressions.length === 0) return [];
const last = expressions[expressions.length - 1];
return collectBareWriteCalls(last);
}
Comment on lines +204 to +209
// LogicalExpression: right operand is the dropped promise
{
code: `async function f() { cond && core.summary.write(); }`,
errors: [{ messageId: "requireAwait", suggestions: [{ messageId: "addAwait", output: `async function f() { cond && await core.summary.write(); }` }] }],
},
// ConditionalExpression: the write() branch is dropped
Comment on lines +222 to +227
// SequenceExpression: last element is the dropped promise
{
code: `async function f() { (log(), core.summary.write()); }`,
errors: [{ messageId: "requireAwait", suggestions: [{ messageId: "addAwait", output: `async function f() { (log(), await core.summary.write()); }` }] }],
},
// .then() chain: outer promise is also dropped
@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

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 (0 additions detected).

@github-actions

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 100/100 — Excellent

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

📊 Metrics (2 tests)
Metric Value
Analyzed 2 (Go: 0, JS/TS: 2)
✅ Design 2 (100%)
⚠️ Implementation 0 (0%)
Edge/error coverage 2 (100%)
Duplicate clusters 0
Inflation No (0.49:1 — well under 2:1 threshold)
🚨 Violations 0
Test File Classification Issues
valid: void core.summary.write() is not flagged require-await-core-summary-write.test.ts:193 ✅ design_test · high_value None
invalid: write() in compound expression forms is flagged require-await-core-summary-write.test.ts:200 ✅ design_test · high_value None

Verdict

Passed. 0% implementation tests (threshold: 30%). Both new tests enforce genuine behavioral contracts with multiple edge-case scenarios.

valid: void core.summary.write() is not flagged — Design test codifying that void expr is the idiomatic deliberate-discard marker and must not be flagged. Covers both direct and chained (void core.summary.addRaw(x).write()) forms. Prevents false positives for intentional usage.

invalid: write() in compound expression forms is flagged — Design test covering five distinct compound expression patterns that silently drop the returned Promise<Summary>: LogicalExpression (cond && write()), ConditionalExpression (one branch), ConditionalExpression (both branches — two errors emitted), SequenceExpression ((log(), write())), and .then() chain. The two-errors case for the both-branches conditional is a non-trivial edge that proves the rule correctly walks all promise-dropping positions in a single statement.

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 · 51.8 AIC · ⌖ 11.8 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: 100/100. 0% implementation tests (threshold: 30%). Both new tests are high-value design tests with thorough edge-case coverage.

@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 — overall a clean, well-structured fix with good coverage. Left a few suggestions around edge case documentation and missing test variants.

📋 Key Themes & Highlights

Key Themes

  • Missing ||/?? test coverage: The LogicalExpression handler covers all three operators but only && is tested. Adding || and ?? cases would lock in the behaviour.
  • Multi-level .then().catch() chain: A doubly-chained form like write().then(f).catch(e) is not flagged by the current logic. Whether that's intentional (to avoid FPs) or an oversight should be documented or tested.
  • void compound form: void (cond && write()) is already exempt by the implementation, but no test locks in this behaviour.
  • ESLint suggestion independence: The double-error ConditionalExpression case is tested correctly; a brief inline comment would make the single-at-a-time assumption explicit for future maintainers.

Positive Highlights

  • ✅ Excellent recursive design for collectBareWriteCalls — clean separation from the visitor
  • void exemption is well-motivated and properly handled as a first-class escape hatch
  • ✅ The double-error conditional test is a great regression anchor for multiple-suggestion scenarios
  • ✅ Defensive empty-array guard on SequenceExpression is a nice touch given the type signature
  • ✅ Promise chain restriction to then/catch/finally avoids false positives on arbitrary chained methods

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


// Logical short-circuit: only the right operand can be the dropped promise
if (expr.type === "LogicalExpression") {
return collectBareWriteCalls(expr.right);

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] The comment says "right operand only (the potential dropped value)" but doesn't explain why ||/?? left operands are safe to skip. For cond || core.summary.write() the right operand is only evaluated when cond is falsy, which is still a dropped Promise worth flagging — yet for cond ?? write() the left would never be a write call, so right-only is always correct. A brief comment clarifying the reasoning would prevent future readers from second-guessing this choice.

💡 Suggested clarification
// For all logical operators (&&, ||, ??), the right operand is the value that
// can be produced and immediately discarded as a bare ExpressionStatement.
// The left operand is always evaluated for its truthiness — it cannot itself
// be silently dropped — so right-only recursion is the correct conservative policy.

@copilot please address this.

const chainProperty = callee.property;
const isPromiseChainMethod =
!callee.computed && chainProperty.type === "Identifier" && (chainProperty.name === "then" || chainProperty.name === "catch" || chainProperty.name === "finally");
if (isPromiseChainMethod && callee.object.type === "CallExpression") {

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 .then() chain detection checks callee.object.type === "CallExpression", but collectBareWriteCalls is not called recursively on the chain callee object — it calls it and only uses the result to decide whether to flag the outer expression. This means a doubly-chained case like write().then(f).catch(e) would NOT be flagged: catch's callee object is write().then(f), not a raw write(). Consider adding a test case to document whether this is intentional or a missed case.

💡 Missing test case
// Should this be flagged? Currently it isn't:
{
  code: `async function f() { core.summary.write().then(() => {}).catch(e => {}); }`,
  errors: [...],
}

If intentional (to avoid FPs on arbitrary chains), document it in the comment. If unintentional, the fix is to recursively call collectBareWriteCalls(callee.object) instead of only checking callee.object.type === "CallExpression".

@copilot please address this.

invalid: [
// LogicalExpression: right operand is the dropped promise
{
code: `async function f() { cond && core.summary.write(); }`,

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 LogicalExpression test only covers &&. It's worth adding || and ?? cases to confirm right-operand-only recursion works correctly for all three logical operators, since the PR description doesn't explicitly state that ||/?? forms were considered.

💡 Suggested additional test cases
// || form: right side dropped when left is falsy
{
  code: `async function f() { cond || core.summary.write(); }`,
  errors: [{ messageId: "requireAwait", suggestions: [{ messageId: "addAwait", output: `async function f() { cond || await core.summary.write(); }` }] }],
},
// ?? form: right side dropped when left is nullish
{
  code: `async function f() { val ?? core.summary.write(); }`,
  errors: [{ messageId: "requireAwait", suggestions: [{ messageId: "addAwait", output: `async function f() { val ?? await core.summary.write(); }` }] }],
},

@copilot please address this.


it("valid: void core.summary.write() is not flagged (deliberate-discard marker)", () => {
cjsRuleTester.run("require-await-core-summary-write", requireAwaitCoreSummaryWriteRule, {
valid: [`async function f() { void core.summary.write(); }`, `async function f() { void core.summary.addRaw(x).write(); }`],

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 void exemption test validates two patterns but neither covers a compound form like void (cond && core.summary.write()). If someone wraps a LogicalExpression in void, the current implementation still recurses into the logical and would flag it — but maybe the intent is to let void at the outer level exempt everything inside. A test verifying the edge case would clarify the intended semantics.

💡 Suggested edge case test
// void wrapping a compound — should it be exempt or still flagged?
valid: [
  `async function f() { void (cond && core.summary.write()); }`,
  // ^ currently: the ExpressionStatement expr IS the UnaryExpression,
  // so collectBareWriteCalls returns [] and this is already exempt.
  // Add as a valid case to lock in the behaviour.
]

@copilot please address this.

{
messageId: "addAwait" as const,
fix(fixer: TSESLint.RuleFixer) {
return fixer.insertTextBefore(callExpr, "await ");

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] When collectBareWriteCalls returns multiple call expressions (e.g., both branches of a ConditionalExpression), each one gets its own insertTextBefore fix. However, if a user applies both suggestions sequentially, the second fix might produce invalid output because the offsets shift after the first insertion. ESLint suggestions are meant to be applied one at a time (not composed), but it's worth confirming that the test for the two-error conditional case (cond ? write() : write()) validates that applying each suggestion independently produces the correct output — the existing test does verify this, which is good. A comment in the code confirming the single-at-a-time assumption would be a helpful guard.

💡 Suggested defensive comment
// Note: suggestions are applied one at a time by the ESLint editor;
// each fix operates on the original source independently.
// Applying both suggestions from a double-write conditional produces
// two separate corrected forms, not a single doubly-awaited form.

@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.

Review: fix(eslint): harden require-await-core-summary-write for compound expression forms

The implementation is correct and well-reasoned. The new collectBareWriteCalls helper cleanly handles all four compound expression forms (logical, conditional, sequence, promise chains) and the tests thoroughly cover both the valid (void-exemption) and invalid cases including the multi-error conditional.

Highlights:

  • void exemption is correctly handled as the outermost guard — deliberate-discard pattern won't produce false positives
  • Reporting against callExpr (not node) gives more precise location and makes the await-insertion suggestion target the right node
  • Promise chain detection is conservatively restricted to then/catch/finally — no false-positive risk on arbitrary method chains
  • The double-error test for cond ? write() : write() validates independent suggestion application correctly

Non-blocking suggestions (inline comments):

  1. Line 201 — inline comment says "only the right operand can be the dropped promise" which is slightly misleading (left-side write() in write() && cond is an acknowledged gap, not because the right is the only possible drop). Suggest wording the comment to make the gap explicit.
  2. Line 195collectBareWriteCalls has no depth guard, unlike rootsSummaryOrAlias. A simple depth > 32 guard would make it consistent and prevent hypothetical deep-recursion in malformed ASTs.
  3. Line 212 — Non-last sequence elements with write() calls are silently ignored. The behavior is defensible, but a comment acknowledging this as intentional would improve readability.

All suggestions are non-blocking. The change is a net improvement in linter coverage. ✅

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · 94.7 AIC · ⌖ 6.23 AIC · ⊞ 4.9K


// Logical short-circuit: only the right operand can be the dropped promise
if (expr.type === "LogicalExpression") {
return collectBareWriteCalls(expr.right);

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 comment says "only the right operand can be the dropped promise", but the left operand of && is also evaluated and its Promise is discarded when it is a bare write call:

core.summary.write() && doSomething()  // left-side write() Promise silently dropped — not flagged

This is a known gap (the PR description focuses on right-operand / result-value drops), but the inline comment is misleading. Consider rewording to:

// Logical: only the right operand is unwrapped (it carries the statement's result value).
// Left-side write() drops (e.g. write() && cond) are a known out-of-scope gap.

Non-blocking suggestion. @copilot please address this.

* also dropped; the outer call expression is flagged (and the suggestion wraps
* the entire chain in `await`).
*/
function collectBareWriteCalls(expr: TSESTree.Expression): TSESTree.CallExpression[] {

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.

Unlike rootsSummaryOrAlias (which has an explicit depth > 32 guard), collectBareWriteCalls has no recursion depth guard. AST nodes like deeply-nested ternaries or &&-chains could in theory recurse hundreds of levels deep:

cond1 ? cond2 ? cond3 ? ... ? core.summary.write() : x : y : z

In practice this is unlikely to cause a stack overflow with real source code, but a simple depth guard (e.g. if (depth > 32) return []) would make this explicit and consistent with the rest of the file.

Non-blocking suggestion. @copilot please address this.

// Sequence: only the last expression is the "result" value.
// SequenceExpression always has ≥2 elements per the ECMAScript spec, but
// guard defensively since the type signature allows an empty array.
if (expr.type === "SequenceExpression") {

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 comment says "SequenceExpression always has ≥2 elements per the ECMAScript spec" — this is correct per spec, but the type definition (TSESTree.SequenceExpression.expressions: TSESTree.Expression[]) admits an empty array. The guard is the right call.

One additional edge: non-last elements of a sequence that are bare write calls are silently ignored:

(core.summary.write(), log())  // write() Promise dropped — not flagged (only last checked)

This is arguably correct (the caller may have intended fire-and-forget here), but is worth a comment acknowledging the gap:

// Note: non-last sequence elements are intentionally not unwrapped; their
// Promise drops are treated as deliberate (they cannot be the statement result).

Non-blocking suggestion. @copilot please address this.

@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot please run the pr-finisher skill, review the remaining feedback, ensure the branch is current, and rerun checks.

Generated by 👨‍🍳 PR Sous Chef · 16.1 AIC · ⌖ 6.23 AIC · ⊞ 4.6K ·
Comment /souschef to run again

…xpression elements

Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>

Copilot AI commented Jul 5, 2026

Copy link
Copy Markdown
Contributor Author

@copilot please run the pr-finisher skill, review the remaining feedback, ensure the branch is current, and rerun checks.

...

Done. Both issues from the review are addressed in commit eeb9b7d:

  • LogicalExpression: now inspects both left and right operands — write() && cond is correctly flagged.
  • SequenceExpression: now inspects all elements via flatMap(core.summary.write(), log()) is correctly flagged.
  • Tests added for both new cases.

All 129 eslint-factory tests pass. The only test-unit failure (TestPRSousChefWorkflowAddCommentTargetContract) is pre-existing and unrelated to these changes.

Copilot AI requested a review from gh-aw-bot July 5, 2026 09:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

require-await-core-summary-write: misses discarded promise in non-bare-call statement positions

4 participants