Skip to content

eslint-factory: resolve destructured bindings precisely in resolveInitializer - #53960

Merged
pelikhan merged 3 commits into
mainfrom
copilot/fix-eslint-factory-resolve-initializer
Aug 19, 2026
Merged

eslint-factory: resolve destructured bindings precisely in resolveInitializer#53960
pelikhan merged 3 commits into
mainfrom
copilot/fix-eslint-factory-resolve-initializer

Conversation

Copilot AI commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

resolveInitializer read declarator.init without checking the shape of declarator.id, so a destructured binding resolved to the entire right-hand side rather than the value actually bound. Since it backs resolveWriteOnceInitializerChain / getDynamicCommandKind, dynamic commands and routes were silently missed by no-child-process-interpolated-command, no-exec-interpolated-command, and no-github-request-interpolated-route.

function run(danger) {
  const template = `git checkout ${danger}`;
  const [cmd] = [template];  // resolved to the ArrayExpression, not `template`
  execSync(cmd);             // not flagged before this change
}

Changes

  • resolveInitializer now dispatches on declarator.id:
    • Identifier — unchanged.
    • ArrayPattern — resolves to the array-literal element at the matching index.
    • ObjectPattern — resolves to the matching object-literal property value.
    • anything else — null.
  • Precision guards (return null rather than a wrong sub-expression): non-literal RHS, rest element before the binding, spread at/before the position, array holes, any spread in an object literal, computed or accessor properties, defaults, and nested patterns.
  • Regression tests in all three consuming rule test files: array- and object-destructured dynamic commands/routes are flagged; statically destructured values (const [cmd] = ["git status"]) and unresolvable ones (const [cmd] = parts) remain unflagged.

The issue proposed simply returning null for destructured ids, but that would leave the two "should now be flagged" acceptance criteria unmet — resolving the bound value satisfies both those and the no-false-positive criterion.

Copilot AI and others added 2 commits August 19, 2026 10:27
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix eslint-factory: handle destructured bindings correctly in resolveInitializer eslint-factory: resolve destructured bindings precisely in resolveInitializer Aug 19, 2026
Copilot AI requested a review from pelikhan August 19, 2026 10:29
@pelikhan
pelikhan marked this pull request as ready for review August 19, 2026 11:17
Copilot AI balanced review requested due to automatic review settings August 19, 2026 11:17
@github-actions

github-actions Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Ponytail Reviewer completed successfully!

Ponytail review: reviewed diff for over-engineering only. New helper functions (resolveArrayPatternElement, resolveObjectPatternProperty, getStaticPropertyName, asExpression) each encode a single required precision guard (rest/spread/hole/computed-key/default rejection) mandated by the linked issue acceptance criteria, not speculative abstraction. No reinvented stdlib, no unneeded deps, no dead code, no unused flexibility. Test additions are necessary regression coverage. Lean already. Ship.

Generated by Ponytail Reviewer for #53960

@github-actions

github-actions Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

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

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

🏗️ ADR gate enforced by Design Decision Gate 🏗️

@github-actions

github-actions Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

🧪 Test quality analysis by Test Quality Sentinel

@github-actions

github-actions Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

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

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer

@github-actions

github-actions Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

🔎 Code quality review by PR Code Quality Reviewer

@github-actions

Copy link
Copy Markdown
Contributor

Comment Memory

reviewed_at: 2026-08-19T00:00:00Z
review_event: COMMENT
top_themes:
  - destructured binding resolution logic looks sound
  - regression coverage added for array/object destructuring
files_reviewed:
  - eslint-factory/src/rules/command-initializer-utils.ts
  - eslint-factory/src/rules/no-child-process-interpolated-command.test.ts
  - eslint-factory/src/rules/no-exec-interpolated-command.test.ts
  - eslint-factory/src/rules/no-github-request-interpolated-route.test.ts
comment_count: 0

Note

This comment is managed by comment memory.

It stores persistent context for this thread in the code block at the top of this comment.
Edit only the text inside the backtick fences; workflow metadata and the footer are regenerated automatically.

Learn more about comment memory

🔎 Code quality review by PR Code Quality Reviewer · gpt54 · 4.82 AIC · ⌖ 9.04 AIC · ⊞ 7K ·
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.

Verdict

No actionable blocking issues found in the changed lines.

Review notes

I checked the new destructuring resolution logic and the added regression coverage for the affected rules. The implementation is intentionally conservative around spreads, defaults, computed properties, and non-literal right-hand sides, which avoids introducing false precision while fixing the missed dynamic-command cases this PR targets.

🔎 Code quality review by PR Code Quality Reviewer · gpt54 · 4.82 AIC · ⌖ 9.04 AIC · ⊞ 7K
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.

The implementation correctly resolves destructured bindings in resolveInitializer for both array and object patterns. Key correctness observations:

  • Array patterns: Rest elements and spread elements in the RHS literal both cause early null returns, preserving soundness. Holes are handled. Default values in the pattern are correctly rejected (the element won't be an Identifier, so the loop skips it).
  • Object patterns: Any spread in the object literal causes an immediate null return (conservative but sound). Duplicate keys: the loop overwrites resolved for each match, so the last property wins — matching JS runtime semantics.
  • asExpression: Correctly rejects AssignmentPattern and other non-expression property values from slipping through.
  • Tests: Valid (no-flag) and invalid (must-flag) cases cover the new code paths across all three rule test files.

No blocking issues found. ✅

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · sonnet46 · 18.8 AIC · ⌖ 8.83 AIC · ⊞ 5.7K

@github-actions github-actions Bot mentioned this pull request Aug 19, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 100/100 — Excellent

Analyzed 10 test scenario(s) across 3 TypeScript test files: 10 design, 0 implementation, 0 violation(s).

Note: Changed test files are TypeScript (.test.ts). Pre-fetch scripts target .test.cjs/_test.go and returned empty; files were read directly.

📊 Metrics (10 test scenarios)
Metric Value
Analyzed 10 (Go: 0, JS/TS: 10)
✅ Design 10 (100%)
⚠️ Implementation 0 (0%)
Edge/error coverage 10 (100%)
Duplicate clusters 0
Inflation No (test +59 lines / prod +90 lines ≈ 0.66:1)
🚨 Violations 0
Test File Classification Issues
invalid: array-destructured [cmd] from dynamic array no-child-process-interpolated-command.test.ts behavioral_contract / design_test / high_value None
invalid: object-destructured { cmd } from dynamic object no-child-process-interpolated-command.test.ts behavioral_contract / design_test / high_value None
invalid: array-destructured [cmd] from dynamic array no-exec-interpolated-command.test.ts behavioral_contract / design_test / high_value None
invalid: object-destructured { cmd } from dynamic object no-exec-interpolated-command.test.ts behavioral_contract / design_test / high_value None
invalid: identifier-bound interpolated route (template literal) no-github-request-interpolated-route.test.ts behavioral_contract / design_test / high_value None
invalid: identifier-bound interpolated route (concatenation) no-github-request-interpolated-route.test.ts behavioral_contract / design_test / high_value None
valid: fully-static ternary route is not flagged no-github-request-interpolated-route.test.ts behavioral_contract / design_test / high_value None
valid: reassigned identifier is not resolved no-github-request-interpolated-route.test.ts behavioral_contract / design_test / high_value None
valid+invalid: array/object destructured route bindings (4 cases) no-github-request-interpolated-route.test.ts behavioral_contract / design_test / high_value None

Verdict

Passed. 0% implementation tests (threshold: 30%). All new tests verify behavioral contracts of the ESLint rule — both positive (interpolated commands/routes through destructured bindings are flagged) and negative (static strings and unresolvable destructures are not flagged). Test inflation is within acceptable bounds (0.66:1).

🧪 Test quality analysis by Test Quality Sentinel · sonnet46 · 43.4 AIC · ⌖ 10.1 AIC · ⊞ 8.1K ·
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%).

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

Improves initializer resolution for array/object destructuring used by security-focused ESLint rules.

Changes:

  • Resolves literal destructured values precisely.
  • Adds conservative guards for ambiguous patterns.
  • Adds regression coverage across three consuming rules.
Show a summary per file
File Description
command-initializer-utils.ts Implements destructured initializer resolution.
no-child-process-interpolated-command.test.ts Tests destructured child-process commands.
no-exec-interpolated-command.test.ts Tests destructured exec commands.
no-github-request-interpolated-route.test.ts Tests destructured GitHub routes.

Review details

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

  • Files reviewed: 4/4 changed files
  • Comments generated: 3
  • Review effort level: Balanced

// A spread element at or before this position shifts the value positions.
if (init.elements.slice(0, index + 1).some(value => value !== null && value.type === AST_NODE_TYPES.SpreadElement)) return null;
const value = init.elements[index];
if (value === null || value.type === AST_NODE_TYPES.SpreadElement) return null;
Comment on lines +76 to +77
if (property.type !== AST_NODE_TYPES.Property || property.computed || property.kind !== "init") continue;
if (getStaticPropertyName(property.key) !== key) continue;
if (property.type !== AST_NODE_TYPES.Property || property.computed || property.kind !== "init") continue;
if (getStaticPropertyName(property.key) !== key) continue;
// Later properties win over earlier duplicates.
resolved = asExpression(property.value);

@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 — commenting with a few test-coverage gaps and one minor performance note. No correctness bugs found; the fix and its guards are sound.

📋 Key Themes & Highlights

Key Themes

  • Missing edge-case tests: three patterns (default values, duplicate object keys, nested/aliased destructuring) are handled correctly by the implementation but have no corresponding test entries — future refactors could silently break them.
  • Minor O(n2) loop: the spread-before-index check in resolveArrayPatternElement re-scans already-visited elements on each iteration; a single-pass flag would be cleaner.

Positive Highlights

  • ✅ Precision guards are comprehensive: rest elements, spread elements, array holes, computed props, accessor props, and defaults are all handled defensively.
  • ✅ Duplicate object property semantics (last-value-wins) are correctly implemented.
  • ✅ Regression tests cover all three consuming rules with valid, invalid, and unresolvable cases.
  • asExpression cleanly narrows property values, keeping the object-pattern resolver readable.
  • ✅ Root cause (dispatching on declarator.id.type) is addressed directly rather than papering over the symptom.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · sonnet46 · 45.9 AIC · ⌖ 10.3 AIC · ⊞ 7.8K
Comment /matt to run again

if (init.elements.slice(0, index + 1).some(value => value !== null && value.type === AST_NODE_TYPES.SpreadElement)) return null;
const value = init.elements[index];
if (value === null || value.type === AST_NODE_TYPES.SpreadElement) return null;
return value;

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 spread guard (slice(0, index + 1).some(...)) is correct but re-scans previously visited elements on every loop iteration, making the function O(n2) for wide array patterns.

💡 Suggested fix — single-pass spread tracking

Track whether a spread element has been seen as you iterate:

let spreadSeen = false;
for (let index = 0; index < pattern.elements.length; index++) {
  const element = pattern.elements[index];
  if (element !== null && element.type === AST_NODE_TYPES.RestElement) return null;
  const initEl = init.elements[index];
  if (initEl !== null && initEl.type === AST_NODE_TYPES.SpreadElement) spreadSeen = true;
  if (element === null || element.type !== AST_NODE_TYPES.Identifier || element.name !== name) continue;
  if (spreadSeen) return null;
  const value = init.elements[index];
  if (value === null || value.type === AST_NODE_TYPES.SpreadElement) return null;
  return value;
}

Not a correctness issue, but improves readability and avoids redundant work for patterns with many elements.

@copilot please address this.

if (init.type !== AST_NODE_TYPES.ObjectExpression) return null;
// A spread can override any property, so the literal is no longer authoritative.
if (init.properties.some(property => property.type === AST_NODE_TYPES.SpreadElement)) return null;

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] asExpression rejects AssignmentPattern (default values), which is correct for object shorthand defaults — but there's no test for const { cmd = fallback } = { cmd: template }. Without coverage, a future reader may not know this is intentionally unsupported.

💡 Suggested test

Add a valid-case entry in each rule's test suite:

// Default value in destructuring — unresolvable, must not be flagged
{ code: `const { execSync } = require("child_process"); const { cmd = "default" } = { cmd: \`git checkout \${branch}\` }; execSync(cmd);` }

If the intent is to be conservative (return null), the test documents that; if the intent should be to resolve through defaults, the missing logic becomes visible.

@copilot please address this.

resolved = asExpression(property.value);
}
return resolved;
}

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 object-pattern resolver handles duplicate property keys by returning the last one (matching JS runtime semantics). This edge case is not covered by any test, so a future refactor could break it silently.

💡 Suggested test
// Duplicate property — last value wins; if it is interpolated it must be flagged
{
  code: `const { execSync } = require("child_process"); const { cmd } = { cmd: "git status", cmd: \`git checkout \${branch}\` }; execSync(cmd);`,
  errors: [{ messageId: "interpolatedCommand", ... }],
}

And the reverse — last value is safe even though the first is not:

{ code: `... const { cmd } = { cmd: \`git checkout \${branch}\`, cmd: "git status" }; execSync(cmd);` }

@copilot please address this.

code: `const { execSync } = require("child_process"); const [cmd] = [\`git checkout \${branch}\`]; execSync(cmd);`,
errors: [{ messageId: "interpolatedCommand", data: { kind: "interpolated template literal", method: "execSync" } }],
},
// Object-destructured dynamic command must resolve to the destructured property

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 invalid test cases don't cover nested array destructuring (e.g., const [[cmd]] = [[template]]) or aliased object keys (e.g., const { cmd: alias } = { cmd: template }). The implementation returns null for these (correct conservative behaviour), but without valid-case tests a reader cannot verify that they are intentionally unsupported.

💡 Suggested additions to valid cases
// Nested destructuring — unresolvable, must not be flagged
{ code: `const { execSync } = require("child_process"); const [[cmd]] = [[\`git checkout \${branch}\`]]; execSync(cmd);` },
// Aliased object key — verify alias is resolved, not the original key
{ code: `const { execSync } = require("child_process"); const { cmd: alias } = { cmd: \`git checkout \${branch}\` }; execSync(alias);` },

Even if these remain unresolvable by design, explicit tests act as executable documentation.

@copilot please address this.

@pelikhan
pelikhan merged commit 4bb738f into main Aug 19, 2026
47 checks passed
@pelikhan
pelikhan deleted the copilot/fix-eslint-factory-resolve-initializer branch August 19, 2026 11:29
@github-actions

Copy link
Copy Markdown
Contributor

🎉 This pull request is included in a new release.

Release: v0.87.2

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.

eslint-factory: resolveInitializer mishandles destructured bindings, silently misses dynamic commands/routes across 3 rules

3 participants