Skip to content

[eslint-miner] eslint-factory: add prefer-structured-clone rule for actions/setup/js - #50530

Merged
pelikhan merged 1 commit into
mainfrom
eslint-miner/prefer-structured-clone-1785922828-eff2984c6ef13d66
Aug 5, 2026
Merged

[eslint-miner] eslint-factory: add prefer-structured-clone rule for actions/setup/js#50530
pelikhan merged 1 commit into
mainfrom
eslint-miner/prefer-structured-clone-1785922828-eff2984c6ef13d66

Conversation

@github-actions

@github-actions github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Overview

Adds a new custom ESLint rule, prefer-structured-clone, to eslint-factory (the gh-aw-custom plugin) targeting actions/setup/js scripts. The rule flags the common JSON.parse(JSON.stringify(x)) deep-clone idiom and suggests replacing it with structuredClone(x).

Rationale

The JSON round-trip clone pattern:

  • is slower than structuredClone
  • silently drops undefined and function values
  • converts Date objects to strings
  • throws on circular references

structuredClone (available globally since Node ≥17, and in the Node 24 runtime targeted by actions/setup/js) clones plain/JSON-safe data correctly without these pitfalls.

Changes

File Type Summary
eslint-factory/eslint.config.cjs modified Registers gh-aw-custom/prefer-structured-clone at warn severity in the plugin's rule config.
eslint-factory/src/index.ts modified Imports and wires preferStructuredCloneRule into the plugin's rules map.
eslint-factory/src/rules/prefer-structured-clone.ts added New rule implementation (79 lines) built with @typescript-eslint/utils ESLintUtils.RuleCreator. Detects JSON.parse(JSON.stringify(x)) via both direct (JSON.parse) and computed (JSON["parse"]) member access. The stringify-side match requires exactly one argument, intentionally excluding replacer/indent variants that change round-trip semantics. Reports with an auto-fixable suggestion (hasSuggestions: true) that replaces the full expression with structuredClone(x).
eslint-factory/src/rules/prefer-structured-clone.test.ts added New RuleTester-based test suite (83 lines).

Test coverage

  • Valid (not flagged): unrelated JSON.parse/JSON.stringify calls, existing structuredClone() usage, JSON.parse(JSON.stringify(obj, null, 2)) (replacer/indent argument), JSON.parse(JSON.stringify(obj), reviver) (custom reviver).
  • Invalid (flagged + auto-fix suggested): JSON.parse(JSON.stringify(tool))structuredClone(tool); the same pattern inside an arrow-function/.map() callback; computed member access JSON["parse"](JSON["stringify"](tool))structuredClone(tool).

Impact

New lint rule addition only — no breaking changes. Existing code is not modified beyond wiring the new rule into the plugin config; violations will surface as warn-level lint findings for future review/fix.

Generated by PR Description Updater for #50530 · auto · 43.2 AIC · ⌖ 4.67 AIC · ⊞ 6.9K ·

Flags JSON.parse(JSON.stringify(x)) deep-clone round-trips in
actions/setup/js and suggests structuredClone(x) instead. The JSON
round-trip silently drops undefined/function values, converts Date
objects to strings, and throws on circular references, while
structuredClone (available globally on the Node 24 runtime this
action targets) clones plain/JSON-safe data directly and faster.

Found 3 existing occurrences of this pattern in actions/setup/js:
generate_safe_outputs_tools.cjs, pick_experiment.cjs, and
safe_outputs_tools_loader.cjs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions github-actions Bot added automation cookie Issue Monster Loves Cookies! eslint labels Aug 5, 2026
@pelikhan
pelikhan marked this pull request as ready for review August 5, 2026 12:56
Copilot AI balanced review requested due to automatic review settings August 5, 2026 12:56
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

PR Code Quality Reviewer completed the code quality review.

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

Test Quality Sentinel completed test quality analysis.

No test files were added or modified in this PR. Test Quality Sentinel skipped.

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

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

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

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

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

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

Adds a custom ESLint rule recommending structuredClone over JSON round-trip cloning in setup scripts.

Changes:

  • Implements and tests pattern detection and suggestions.
  • Registers and enables the rule for setup JavaScript linting.
  • Includes computed-property support.
Show a summary per file
File Description
eslint-factory/src/rules/prefer-structured-clone.ts Implements the rule and suggestion.
eslint-factory/src/rules/prefer-structured-clone.test.ts Tests accepted and reported patterns.
eslint-factory/src/index.ts Registers the rule.
eslint-factory/eslint.config.cjs Enables the rule as a warning.

Review details

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

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


const innerArg = node.arguments[0];
if (innerArg.type !== AST_NODE_TYPES.CallExpression) return;
if (!isPlainJsonStringifyCall(innerArg)) return;
const isDirectAccess = !callee.computed && property.type === AST_NODE_TYPES.Identifier && property.name === "stringify";
const isComputedAccess = callee.computed && property.type === AST_NODE_TYPES.Literal && property.value === "stringify";
if (!isDirectAccess && !isComputedAccess) return false;
return node.arguments.length === 1;
{
messageId: "replaceWithStructuredClone",
fix(fixer: TSESLint.RuleFixer) {
return fixer.replaceText(node, `structuredClone(${clonedExpressionText})`);

describe("prefer-structured-clone", () => {
it("uses the correct docs URL", () => {
expect(preferStructuredCloneRule.meta.docs.url).toBe("https://github.com/github/gh-aw/tree/main/eslint-factory#prefer-structured-clone");
Comment on lines +25 to +27
// Custom reviver on parse changes semantics too; still matched today since only the
// parse call itself is checked for a single stringify argument — but the inner
// stringify call must have exactly one argument, so this is out of scope here.

@github-actions github-actions Bot left a comment

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.

The rule implementation is correct and well-scoped. Logic for detecting JSON.parse(JSON.stringify(x)) with exactly one argument at each level is sound, suggestion fix is properly wired, test coverage includes computed-property access and edge cases, and the integration into index.ts / eslint.config.cjs follows existing patterns.

One minor nit: the comment on lines 27–29 of prefer-structured-clone.test.ts ("still matched today since only the parse call itself is checked for a single stringify argument") is slightly misleading. The real reason JSON.parse(JSON.stringify(obj), reviver) is excluded is the node.arguments.length !== 1 check on the outer parse call. The comment implies the inner-stringify check does the excluding, which is inaccurate. Non-blocking — the test result is correct.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · sonnet46 · 18.4 AIC · ⌖ 9.3 AIC · ⊞ 5.4K

@github-actions github-actions Bot left a comment

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.

Skills-Based Review 🧠

Applied /tdd and /grill-with-docs — one minor comment on a misleading test comment; otherwise the PR is solid.

📋 Key Themes & Highlights

Key Themes

  • One inaccurate inline comment in the test file (lines 28–30) implies the rule fires on the reviver case when it actually correctly skips it. See inline comment above.

Positive Highlights

  • ✅ Rule is narrowly scoped to the unambiguous JSON.parse(JSON.stringify(x)) shape — low false-positive risk
  • ✅ Computed-property access (JSON["parse"]/JSON["stringify"]) is correctly handled and tested
  • ✅ Replacer/indent and reviver edge cases are considered and excluded with clear rationale
  • ✅ Suggestion fixer produces clean structuredClone(expr) output
  • ✅ Test structure is clear with separate valid/invalid describe blocks
  • ✅ Rule description and error messages are accurate and actionable

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · sonnet46 · 29 AIC · ⌖ 7.99 AIC · ⊞ 7.1K
Comment /matt to run again

});

it("invalid: JSON.parse(JSON.stringify(x)) is flagged and suggests structuredClone(x)", () => {
cjsRuleTester.run("prefer-structured-clone", preferStructuredCloneRule, {

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.

[/tdd] The inline comment on lines 28–30 is misleading: it says "still matched today" but this case is in the valid array (i.e. it is not flagged). The phrase implies the rule fires on this input, which is the opposite of what happens — JSON.parse(JSON.stringify(obj), reviver) is correctly excluded because the outer parse call has 2 arguments.

💡 Suggested clarification
// Custom reviver: excluded because JSON.parse has 2 arguments —
// the node.arguments.length !== 1 guard in the rule rejects it cleanly.
`const clone = JSON.parse(JSON.stringify(obj), reviver);`,

@copilot please address this.

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

@github-actions github-actions Bot left a comment

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.

Verdict: COMMENT — narrow, low-risk rule with one real gap in autofix safety

Small, well-scoped addition (a new ESLint rule + tests + registration). The AST matching is deliberately conservative (bails on replacer/reviver args) and the tests cover direct and computed-property access.

💡 Themes / highlights
  • Autofix safety gap: the rule matches JSON purely by identifier name with no scope resolution, so a shadowed/aliased JSON would be falsely flagged and the suggest fixer would rewrite it to the real global structuredClone, changing behavior. Low likelihood in this codebase's .cjs scripts today, but worth hardening since this ships an automatic suggestion, not just a warning.
  • Minor test-comment inconsistency: one inline comment describing why the reviver-argument case is excluded is self-contradictory/misleading about the actual guard clause responsible.
  • No blocking correctness bugs in the primary detection path (JSON.parse(JSON.stringify(x)), single-arg constraint, computed-access handling) — the core rule logic is sound for its stated narrow scope.

🔎 Code quality review by PR Code Quality Reviewer · auto · 39.3 AIC · ⌖ 7.18 AIC · ⊞ 7.9K
Comment /review to run again

function isJsonParseCall(node: TSESTree.CallExpression): boolean {
const callee = node.callee;
if (callee.type !== AST_NODE_TYPES.MemberExpression) return false;
if (callee.object.type !== AST_NODE_TYPES.Identifier || callee.object.name !== "JSON") return false;

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.

This matches on the bare identifier name JSON with no scope/binding resolution, so a locally shadowed or aliased JSON will be falsely flagged and auto-fixed to structuredClone(...), silently changing runtime behavior.

💡 Details

isJsonParseCall/isPlainJsonStringifyCall only check callee.object.name === "JSON" textually, with no scope analysis. If any enclosing scope shadows the global JSON (e.g. a parameter, destructure, or test double named JSON), the rule still matches, and because this rule ships an auto-suggest fixer (not just a warning), applying it rewrites to the real global structuredClone, which is not equivalent to the shadowed value — actively breaking code rather than just warning about it.

Fix: resolve JSON via scope (e.g. check context.sourceCode.getScope(node) for a local binding named JSON and bail out) before reporting/fixing.

messageId: "replaceWithStructuredClone",
output: `const runs = state.runs.map(run => structuredClone(run));`,
},
],

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.

This comment is self-contradictory: it claims the reviver case is "still matched today" but the test lists it under valid (i.e., not flagged) — the opposite claim.

💡 Details

The outer JSON.parse(JSON.stringify(obj), reviver) call has two arguments, so node.arguments.length !== 1 triggers an early return in the rule — it is never matched, for a different reason than the comment implies. The comment focuses on the inner stringify call's argument count, but the actual disqualifier here is the outer parse call's second reviver argument. Clarify the comment to state that the parse call itself is excluded due to its own arg count, rather than saying it is "matched" and excluded downstream.

@pelikhan
pelikhan merged commit b60ebc0 into main Aug 5, 2026
50 checks passed
@pelikhan
pelikhan deleted the eslint-miner/prefer-structured-clone-1785922828-eff2984c6ef13d66 branch August 5, 2026 13:17
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

🎉 This pull request is included in a new release.

Release: v0.85.1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

automation cookie Issue Monster Loves Cookies! eslint

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants