Skip to content

fix(mergedeep): preserve array shape when comparing empty targets#1030

Open
tdabasinskas wants to merge 2 commits into
github-community-projects:main-enterprisefrom
datolabs-io:fix/comparedeep-empty-target-array-shape
Open

fix(mergedeep): preserve array shape when comparing empty targets#1030
tdabasinskas wants to merge 2 commits into
github-community-projects:main-enterprisefrom
datolabs-io:fix/comparedeep-empty-target-array-shape

Conversation

@tdabasinskas

Copy link
Copy Markdown

MergeDeep.compareDeep reports the same kind of change in two different JSON shapes depending on whether the target is empty. Comparing rulesets (any top-level array config works the same):

// repo already has a ruleset: additions is an array
compareDeep([{ name: 'other', ... }], [{ name: 'other', ... }, { name: 'new', ... }])
// => { additions: [ { name: 'new', ... } ], ... }

// repo receives its FIRST ruleset: additions is an index-keyed object
compareDeep([], [{ name: 'new', ... }])
// => { additions: { "0": { name: 'new', ... } }, ... }

Two details combine to cause this. On first invocation, top-level arrays are wrapped into { __array: [...] } before the accumulator-type check runs, so Array.isArray(source) is always false and additions initializes as {} (the array branch of that check is effectively dead code). The non-empty-target path recovers because the __array key is processed as a nested array and unwound before returning. But the empty-target early return does Object.assign(additions, s) — spreading an array into a plain object produces the { "0": ..., "1": ... } shape — and returns without the unwind. That early return also omits deletions from its result, unlike every other return path.

This hits anything consuming plan/diff JSON — dry-run reports, PR comments, or tooling parsing the NopCommand actions. A repo receiving its first ruleset reports index-keyed attribute soup instead of one added item, while the second ruleset onward reports a clean array. (Possibly related to the inconsistent-diff reports in #693).

When `compareDeep` encounters an empty target and a source array, it now returns additions as an array rather than an index-keyed object. This ensures consistent behavior whether the target is empty or contains existing entries.

Previously, comparing `[]` with `[item]` would produce `additions: { 0: item }`, while comparing `[existing]` with `[existing, item]` would correctly produce `additions: [item]`. This inconsistency caused issues when applying diffs to repositories receiving their first array-based configuration.

Also ensures the `deletions` property is always included in the result object for consistency.
Copilot AI review requested due to automatic review settings July 15, 2026 06:32

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 fixes MergeDeep.compareDeep so diffs for top-level array configs (e.g. rulesets) are consistent when comparing against an empty target, and ensures the compare result always includes deletions—improving the stability of plan/diff JSON consumed by dry-run reports and PR comments.

Changes:

  • Adjusts the empty-target early return in compareDeep to preserve top-level array diff shape for additions, and to always return deletions.
  • Adds unit tests covering top-level array shape consistency and presence of deletions in the result.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
lib/mergeDeep.js Fixes empty-target diff behavior to preserve top-level array shape and include deletions.
test/unit/lib/mergeDeep.test.js Adds regression tests for consistent top-level array diff shape and inclusion of deletions.

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

Comment thread lib/mergeDeep.js
Comment thread lib/mergeDeep.js Outdated
When comparing an empty target object against a source, the code now explicitly copies keys instead of using Object.assign. This prevents prototype pollution vectors like `__proto__` and `constructor` from being copied into the additions object.

The fix mirrors the existing protection in the comparison loop and ensures that malicious keys parsed from JSON cannot pollute the prototype chain through the additions reference.

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

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

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.

2 participants