Skip to content

fix(cli): guard customDeepMerge against circular references#28349

Closed
vedhakoushik wants to merge 4 commits into
google-gemini:mainfrom
vedhakoushik:fix/deepmerge-circular-refs
Closed

fix(cli): guard customDeepMerge against circular references#28349
vedhakoushik wants to merge 4 commits into
google-gemini:mainfrom
vedhakoushik:fix/deepmerge-circular-refs

Conversation

@vedhakoushik

Copy link
Copy Markdown

What

Fixes #28270. customDeepMerge's mergeRecursively recursed into nested plain objects with no cycle tracking, so a settings object containing a circular reference (e.g. obj.self = obj) caused unbounded recursion and crashed the settings manager with RangeError: Maximum call stack size exceeded, breaking CLI initialization until the user manually scrubbed their config.

The repo already documented this limitation with a test in settings.test.ts that asserted the crash (.toThrow(/Maximum call stack size exceeded/)).

Fix

packages/cli/src/utils/deepMerge.ts — track the chain of source objects currently being merged in a WeakSet, added on entry and removed on exit so it represents only the current ancestor path (not already-processed siblings). When a source value is a circular reference back to an ancestor, assign it by reference instead of recursing.

This is deliberately scoped so that shared-but-non-circular references (DAGs) still merge normally — only true cycles are short-circuited.

Tests

  • deepMerge.test.ts: added regression tests for self-reference (obj.self = obj), mutual/indirect cycles (a → b → a), and a shared non-circular reference (DAG) to guard against over-eager short-circuiting.
  • settings.test.ts: flipped the existing circular-reference test from asserting the crash to asserting it no longer throws.

Verification

  • deepMerge.test.ts — 20/20 pass (4 new)
  • settings.test.ts circular-reference test — passes
  • eslint, tsc --noEmit, and prettier --check all clean on the changed files

🤖 Authored with Claude Code. First-time contributor here — happy to adjust the cycle-handling behavior (e.g. omit vs. assign-by-reference) to match maintainer preference.

customDeepMerge's mergeRecursively recursed into nested plain objects with
no cycle tracking, so a settings object containing a circular reference
(e.g. `obj.self = obj`) caused unbounded recursion and crashed the settings
manager with `RangeError: Maximum call stack size exceeded` — breaking CLI
initialization until the user manually scrubbed their config. The repo already
documented this with a test asserting the crash.

Track the chain of source objects currently being merged in a WeakSet (added
on entry, removed on exit so it represents only the current ancestor path).
When a source value is a circular reference back to an ancestor, assign it by
reference instead of recursing. Shared-but-non-circular references (DAGs) are
unaffected and still merge normally.

- Add regression tests for self, mutual (a->b->a), and shared-reference cases.
- Flip the settings.test.ts circular-reference test from asserting the crash
  to asserting it no longer throws.

Fixes google-gemini#28270

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Bellam vedha kousik <bellam.vedhakoushik@gmail.com>
@vedhakoushik vedhakoushik requested a review from a team as a code owner July 10, 2026 07:30
@github-actions github-actions Bot added the size/m A medium sized PR label Jul 10, 2026
@github-actions

github-actions Bot commented Jul 10, 2026

Copy link
Copy Markdown

📊 PR Size: size/M

  • Lines changed: 85
  • Additions: +79
  • Deletions: -6
  • Files changed: 3

@google-cla

google-cla Bot commented Jul 10, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@vedhakoushik

Copy link
Copy Markdown
Author

@googlebot I signed it!

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses a critical issue where circular references in configuration objects caused unbounded recursion and stack overflow crashes in the CLI's settings manager. By introducing a cycle-tracking mechanism using a WeakSet during the deep merge process, the fix allows the system to gracefully handle circular structures by assigning them by reference instead of recursing, while maintaining correct behavior for non-circular shared references.

Highlights

  • Circular Reference Handling: Implemented a WeakSet-based tracking mechanism in customDeepMerge to detect and handle circular references during object merging, preventing stack overflow errors.
  • Regression Testing: Added comprehensive test cases for self-referential objects, mutual circular references, and DAGs to ensure robust handling and prevent regressions.
  • Settings Manager Stability: Updated existing settings tests to verify that circular references in configuration objects no longer cause the CLI to crash.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces circular reference handling to customDeepMerge to prevent stack overflow errors when merging self-referential or mutually circular objects, using a WeakSet to track visited source objects during recursion. The review feedback suggests using a clones map instead of a WeakSet to ensure that circular references are preserved within the newly cloned structure rather than pointing back to the original source objects, and provides corresponding updates to the test assertions.

Comment thread packages/cli/src/utils/deepMerge.ts Outdated
Comment on lines +73 to +78
if (isPlainObject(srcValue) && seen.has(srcValue)) {
// Circular reference back to an ancestor source object: assign the
// reference directly instead of recursing to avoid infinite recursion.
target[key] = srcValue;
continue;
}

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.

high

Retrieve the cloned ancestor from the clones map instead of assigning the original srcValue directly.

Suggested change
if (isPlainObject(srcValue) && seen.has(srcValue)) {
// Circular reference back to an ancestor source object: assign the
// reference directly instead of recursing to avoid infinite recursion.
target[key] = srcValue;
continue;
}
if (isPlainObject(srcValue) && clones.has(srcValue)) {
// Circular reference back to an ancestor source object: assign the
// cloned reference directly instead of recursing to avoid infinite recursion
// and reference sharing with the original source object.
target[key] = clones.get(srcValue)!;
continue;
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done in 11948e9. Switched from a WeakSet of visited sources to a Map<source, clone>, and on a circular reference now assign the ancestor's clone so the merge produces a fully independent clone (result.self === result) rather than embedding a reference to the original source. Used a lint-safe ancestorClone !== undefined check instead of a non-null assertion.

Comment thread packages/cli/src/utils/deepMerge.ts Outdated
target[key] = srcValue;
}
}
seen.delete(source);

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.

high

Delete the source from the clones map on exit.

Suggested change
seen.delete(source);
clones.delete(source);

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done in 11948e9clones.delete(source) on exit.

Comment on lines +250 to +253
const result = customDeepMerge(getMergeStrategy, {}, circular);
expect(result['a']).toBe(1);
// The cycle is preserved by reference rather than recursed into.
expect(result['self']).toBe(circular);

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.

high

Update the test assertion to verify that the circular reference is preserved within the cloned structure, rather than pointing back to the original source object.

Suggested change
const result = customDeepMerge(getMergeStrategy, {}, circular);
expect(result['a']).toBe(1);
// The cycle is preserved by reference rather than recursed into.
expect(result['self']).toBe(circular);
const result = customDeepMerge(getMergeStrategy, {}, circular);
expect(result['a']).toBe(1);
// The cycle is preserved within the cloned structure rather than pointing back to the source.
expect(result['self']).toBe(result);

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done in 11948e9. The assertion now checks expect(result['self']).toBe(result) (plus .not.toBe(circular)), and I strengthened the mutual-cycle test to assert result.b.a === result.

Address Gemini Code Assist review: track source->clone in a Map instead of a
WeakSet of visited sources. On a circular reference, assign the ancestor's
*clone* rather than the original source object, so the merged result is a fully
independent clone (result.self === result) and never embeds a live reference
back into the caller's input (which risked cross-mutation of settings).

Strengthen tests to assert the cycle is reproduced within the clone for both
self-referential and mutual (a -> b -> a) cases.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Bellam vedha kousik <bellam.vedhakoushik@gmail.com>
@vedhakoushik

Copy link
Copy Markdown
Author

@googlebot I signed it!

@vedhakoushik

Copy link
Copy Markdown
Author

Superseded by #28387 — same fix, clean re-submission without the Co-Authored-By trailer that was blocking the CLA check.

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

Labels

area/core Issues related to User Interface, OS Support, Core Functionality priority/p2 Important but can be addressed in a future release. size/m A medium sized PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant