Skip to content

🐛 Guard against orphaning an alias when deleting an anchor target#185

Merged
frenck merged 1 commit into
mainfrom
frenck/roundtrip-guard-dangling-alias
Jul 3, 2026
Merged

🐛 Guard against orphaning an alias when deleting an anchor target#185
frenck merged 1 commit into
mainfrom
frenck/roundtrip-guard-dangling-alias

Conversation

@frenck

@frenck frenck commented Jul 3, 2026

Copy link
Copy Markdown
Owner

Breaking change

None. This only rejects a delete that would produce YAML that no longer parses; it does not change the result of any delete that was already valid.

Proposed change

Deleting a node that carries an &anchor still referenced by a *alias elsewhere in the document left the alias dangling, so the re-emitted YAML no longer parsed. The mutation API allowed it, which turned a valid document into a broken one with no warning.

__delitem__ (both the top-level document and a node view) now snapshots the node tree before the delete, but only when the document actually references an alias, so the common no-anchor case stays allocation-free. After the delete it checks for a *alias with no matching &anchor; if one is found it restores the snapshot and raises a ValueError naming the anchor, leaving the document untouched. Removing the alias itself (rather than its target) stays allowed.

Type of change

  • Dependency or tooling upgrade
  • Bugfix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Deprecation (replaces or removes a feature, with a migration path)
  • Breaking change (a fix or feature that changes existing behavior)
  • Code quality, refactor, or test-only change
  • Documentation only

Additional information

  • This PR fixes or closes issue: None
  • This PR is related to: None
  • Link to a separate documentation pull request: None

Checklist

  • I have read the AI Policy, and this pull request was not created by an autonomous agent.
  • I fully understand the code in this pull request and can explain every line, including any AI-assisted changes.
  • The change is covered by tests, and uv run pytest passes locally. A pull request cannot be merged unless CI is green.
  • uv run ruff check . and uv run ruff format --check . pass.
  • cargo fmt --check and cargo clippy --all-targets -- -D warnings pass.
  • Round-trip fidelity is preserved: an unmodified document still re-emits byte-for-byte.
  • No commented-out or dead code is left in the pull request.

If the change is user-facing:

  • Documentation under docs/ is added or updated, and docs/verify_examples.py still passes.

Copilot AI review requested due to automatic review settings July 3, 2026 07:49
@frenck frenck added the bugfix Inconsistencies or issues which will cause a problem for users or implementers. label Jul 3, 2026
@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This change adds delete-time protection against orphaning YAML aliases in both the single-document and nested-view deletion paths of the round-trip document implementation. New internal helper functions detect aliases, compute dangling alias sets before and after a deletion, and roll back the deletion with a raised error if it would newly orphan an alias. Corresponding tests validate that deleting referenced anchor targets fails, deleting aliases themselves succeeds, nested anchor targets are protected, and unrelated deletions proceed despite pre-existing dangling aliases. No public signatures changed.

Sequence Diagram(s)

Included above in the hidden review stack artifact.

Possibly related PRs

  • frenck/YAMLRocks#59: Modifies the same __delitem__ deletion implementation in src/roundtrip/document.rs that this PR extends with alias-orphan protection.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main fix: preventing alias orphaning when deleting an anchor target.
Description check ✅ Passed The description matches the change by explaining the delete-time alias check and rollback behavior.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codspeed-hq

codspeed-hq Bot commented Jul 3, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 16 untouched benchmarks


Comparing frenck/roundtrip-guard-dangling-alias (a5c5270) with main (abc939b)

Open in CodSpeed

@codecov-commenter

codecov-commenter commented Jul 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.38710% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 92.34%. Comparing base (abc939b) to head (a5c5270).

Files with missing lines Patch % Lines
src/roundtrip/document.rs 98.38% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #185      +/-   ##
==========================================
+ Coverage   92.31%   92.34%   +0.03%     
==========================================
  Files          40       40              
  Lines       11444    11504      +60     
==========================================
+ Hits        10564    10623      +59     
- Misses        880      881       +1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copilot AI left a comment

Copy link
Copy Markdown

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 a mutation-API bug in the round-trip document layer (src/roundtrip/document.rs): deleting a node that carried an &anchor still referenced by a *alias elsewhere produced YAML that no longer parsed. Both YAMLRocksDocument.__delitem__ and YAMLRocksDocumentView.__delitem__ now snapshot the node tree before a delete (only when the document actually contains aliases, keeping the common no-alias case allocation-free), perform the delete, and roll back with a descriptive ValueError if the delete leaves a dangling alias. Deleting the alias itself remains allowed.

Changes:

  • Added a snapshot-and-rollback guard to the single-document top-level delete and the nested-view delete paths.
  • Added four helper functions (collect_alias_names, document_has_aliases, first_dangling_alias, dangling_alias_error) to detect orphaned aliases and build the error.
  • Added Python tests covering rejection of deleting a referenced anchor target and allowing deletion of the alias itself.

Reviewed changes

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

File Description
src/roundtrip/document.rs Adds the dangling-alias guard to both delete paths plus the supporting helper functions and error message.
tests/core/test_anchors.py Adds tests for the top-level delete guard (target rejected, alias deletion allowed).

Notes from the review:

  • The rollback correctly preserves round-trip fidelity, since the snapshot is cloned before del_child calls mark_modified, so the restored nodes keep their original modified flags.
  • The post-delete check is absolute (any dangling alias) rather than differential, which can misattribute a pre-existing dangling alias (round-trip loading does not validate aliases) to an unrelated delete and emit a misleading error.
  • The nested-view guard path is new but untested; only the top-level path has coverage.

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

Comment thread src/roundtrip/document.rs Outdated
Comment thread src/roundtrip/document.rs
Deleting a node that carries an `&anchor` still referenced by a `*alias`
elsewhere in the document left the alias dangling, so the re-emitted YAML no
longer parsed: silent corruption produced by a mutation the API allowed.

`__delitem__` (both the top-level document and a node view) now snapshots the
node tree before the delete, but only when the document actually references an
alias, so the common no-anchor case stays allocation-free. The check is
differential: it records which aliases were already dangling before the delete
(the round-trip/compose path does not validate alias targets at load time, so a
document may already carry one) and rejects only an alias the delete *newly*
orphans, restoring the snapshot and raising a `ValueError` that names the anchor.
Removing the alias itself (rather than its target) stays allowed.
@frenck frenck force-pushed the frenck/roundtrip-guard-dangling-alias branch from 96182d6 to a5c5270 Compare July 3, 2026 09:10

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
src/roundtrip/document.rs (2)

174-188: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Guard logic is correct. Snapshotting before del_child (pre-mark_modified) means rollback restores a fully clean state, so to_yaml() replays the original bytes verbatim — matching the test that asserts byte-for-byte preservation on a blocked delete.

Note: this snapshot/rollback block is duplicated almost verbatim in YAMLRocksDocumentView.__delitem__ (Lines 613-629). Consider extracting a small helper (e.g. taking &mut Vec<YamlNode> plus a deletion closure) so the two guards can't drift out of sync.


1492-1501: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

document_has_aliases fully traverses and allocates a HashSet just to test emptiness, which undercuts the doc comment's claim of keeping the no-alias case cheap. A short-circuiting existence check avoids both the allocation and the full walk on the common path.

♻️ Short-circuiting existence check
-fn document_has_aliases(nodes: &[YamlNode]) -> bool {
-    let mut aliases = std::collections::HashSet::new();
-    for node in nodes {
-        collect_alias_names(node, &mut aliases);
-    }
-    !aliases.is_empty()
-}
+fn document_has_aliases(nodes: &[YamlNode]) -> bool {
+    nodes.iter().any(node_contains_alias)
+}
+
+fn node_contains_alias(node: &YamlNode) -> bool {
+    match &node.kind {
+        YamlNodeKind::Alias(_) => true,
+        YamlNodeKind::Mapping(pairs) => pairs
+            .iter()
+            .any(|(k, v)| node_contains_alias(k) || node_contains_alias(v)),
+        YamlNodeKind::Sequence(items) => items.iter().any(node_contains_alias),
+        _ => false,
+    }
+}

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 49442fc8-83a6-4ba4-990f-0d35e3f72e2d

📥 Commits

Reviewing files that changed from the base of the PR and between abc939b and a5c5270.

📒 Files selected for processing (2)
  • src/roundtrip/document.rs
  • tests/core/test_anchors.py

@frenck frenck merged commit a5823bc into main Jul 3, 2026
76 of 114 checks passed
@frenck frenck deleted the frenck/roundtrip-guard-dangling-alias branch July 3, 2026 09:37
@github-actions github-actions Bot locked as resolved and limited conversation to collaborators Jul 4, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

bugfix Inconsistencies or issues which will cause a problem for users or implementers.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants