Skip to content

[yaml_edit] Add YAML Merge Key (<<) support under AliasBehavior - #2586

Draft
sigurdm wants to merge 3 commits into
handle-aliasesfrom
merge-keys
Draft

sigurdm wants to merge 3 commits into
handle-aliasesfrom
merge-keys

Conversation

@sigurdm

@sigurdm sigurdm commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Stacked on top of #2528.

This PR adds support for YAML 1.1 Merge Keys (<<: *anchor) to package:yaml_edit across parseAt, update, and remove, honoring the configured AliasBehavior.

Changes

  • Key Origin Resolution (_findMergedOrigin): Resolves merged properties through single merges (<<: *defaults), multi-merges (<<: [*m1, *m2]) with first-map precedence, and chained merges (service -> mid -> base).
  • AliasBehavior Integration:
    • disallow: Throws AliasException when attempting to update or remove a property originating from a merge key, while allowing explicit local keys.
    • reference: Redirects merged property updates and removals to the anchor definition in-place, while updating explicit overrides locally.
    • copyOnWrite: Inserts explicit override keys directly into the target map without mutating the template. Materializes intermediate sub-maps for nested paths (['service', 'db', 'port']). Restores inherited anchor values upon removal of explicit overrides.
  • Unit Tests: Added 30 comprehensive unit tests in test/merge_keys_test.dart covering single, multi, chained, deep (3+ levels), flow style, and malformed merge keys.

@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 support for YAML merge keys (<<) in YamlEditor, allowing keys to be resolved through single, multi, and chained merges under different AliasBehavior configurations (disallow, reference, and copyOnWrite). The changes include updating path resolution, traversal, and mutation logic, along with adding a comprehensive test suite. The review feedback suggests several improvements: simplifying the remove method by eliminating the redundant isRemove flag, correcting a redundant map lookup where map.nodes['<<'] is used instead of retrieving the entry via getYamlMapEntry, and adding a unit test to verify nested merged key removal under copyOnWrite behavior.

Comment on lines 1085 to +1086
YamlNode remove(Iterable<Object?> path) {
path = _resolvePath(path);
path = _resolvePath(path, isRemove: true);

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.

critical

Update the remove method to call _resolvePath without the obsolete isRemove parameter.

  YamlNode remove(Iterable<Object?> path) {
    path = _resolvePath(path);
References
  1. Avoid mutually exclusive or redundant boolean flags in API design. Simplify the API by removing redundant flags to make invalid states unrepresentable.

final visitedMaps = visited ?? Set<YamlMap>.identity();
if (!visitedMaps.add(map)) return null;

final mergeNode = map.nodes['<<'] ?? getYamlMapEntry(map, '<<').valueNode;

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.

medium

In package:yaml, the keys of YamlMap.nodes are always YamlNode objects (such as YamlScalar wrapping '<<'), rather than raw Strings. Therefore, map.nodes['<<'] will always evaluate to null, making the left-hand side of the null-coalescing operator redundant and misleading. We can directly retrieve the value node using getYamlMapEntry.

    final mergeNode = getYamlMapEntry(map, '<<').valueNode;

''', aliasBehavior: AliasBehavior.copyOnWrite);

expect(() => doc.remove(['service', 'timeout']), throwsPathError);
expect(doc.parseAt(['service', 'timeout']).value, equals(30));

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.

medium

Add a comprehensive unit test to verify that removing a nested merged key under copyOnWrite correctly materializes the parent map and does not mutate the shared template or other inheriting services.

    });

    test('removing nested merged key materializes parent and does not mutate template', () {
      final doc = YamlEditor('''
defaults: &defaults
  db:
    port: 5432
    host: localhost
service1:
  <<: *defaults
service2:
  <<: *defaults
''', aliasBehavior: AliasBehavior.copyOnWrite);

      doc.remove(['service1', 'db', 'port']);
      expect(doc.parseAt(['service1', 'db', 'host']).value, equals('localhost'));
      expect(() => doc.parseAt(['service1', 'db', 'port']), throwsPathError);

      // Ensure template and other services are untouched
      expect(doc.parseAt(['defaults', 'db', 'port']).value, equals(5432));
      expect(doc.parseAt(['service2', 'db', 'port']).value, equals(5432));
    });

@sigurdm
sigurdm force-pushed the merge-keys branch 3 times, most recently from 8e71085 to 8cfd98f Compare September 9, 2026 08:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant