refactor(parsing): share merge-key resolution between composers

The buffered and streaming event composers each carried an identical
copy of the merge-key (<<) override logic and its invalid-value error.
Extract both into one internal Parsing.MergeResolution helper so the
precedence rules (local keys win, earlier sources win, inherited then
local order) have a single source of truth. No behavior change; all
tests pass.
This commit is contained in:
Mihaela M 2026-06-26 05:50:28 +02:00
parent 87e3e5cbab
commit 9ae6c3baff
5 changed files with 63 additions and 78 deletions

View File

@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Changed
- Merge-key (`<<`) override resolution now lives in a single internal helper,
`Parsing.MergeResolution`, shared by the buffered and streaming event
composers. Both paths previously carried an identical copy of the
inherited-versus-local pair merge, so the subtle precedence rules (local keys
win, earlier sources win, inherited-then-local ordering) now have one source
of truth. No behavior change.
### Documentation
- Correct `SECURITY.md`: fix the reporting contact address and replace the

View File

@ -56,12 +56,12 @@ extension PureYAML.Parsing.EventComposer {
case let .sequence(values):
return try values.map { value in
guard case let .mapping(mapping) = value else {
throw invalidMergeValue(mark: mark)
throw PureYAML.Parsing.MergeResolution.invalidMergeValue(mark: mark)
}
return mapping.pairs
}
case .bool, .double, .int, .null, .string:
throw invalidMergeValue(mark: mark)
throw PureYAML.Parsing.MergeResolution.invalidMergeValue(mark: mark)
}
}
@ -70,42 +70,8 @@ extension PureYAML.Parsing.EventComposer {
mark: PureYAML.Parsing.Mark,
) throws -> [PureYAML.Model.Pair] {
guard case let .mapping(mapping) = value else {
throw invalidMergeValue(mark: mark)
throw PureYAML.Parsing.MergeResolution.invalidMergeValue(mark: mark)
}
return mapping.pairs
}
func mergedPairs(
from mergeSources: [[PureYAML.Model.Pair]],
localPairs: [PureYAML.Model.Pair],
) -> [PureYAML.Model.Pair] {
guard !mergeSources.isEmpty else {
return localPairs
}
let localKeys = Set(localPairs.map(\.keyNode))
var inheritedKeys = Set<PureYAML.Model.Key>()
var inheritedPairs: [PureYAML.Model.Pair] = []
for source in mergeSources {
var sourceKeys = Set<PureYAML.Model.Key>()
for pair in source {
guard !localKeys.contains(pair.keyNode),
!inheritedKeys.contains(pair.keyNode)
else {
continue
}
inheritedPairs.append(pair)
sourceKeys.insert(pair.keyNode)
}
inheritedKeys.formUnion(sourceKeys)
}
return inheritedPairs + localPairs
}
func invalidMergeValue(mark: PureYAML.Parsing.Mark) -> PureYAML.Parsing.ParseError {
PureYAML.Parsing.ParseError.invalidMergeValue(
line: mark.line,
column: mark.column,
)
}
}

View File

@ -149,7 +149,7 @@ extension PureYAML.Parsing.EventComposer {
}
}
_ = try expect("mapping end") { $0.isMappingEnd }
let pairs = mergedPairs(from: mergeSources, localPairs: localPairs)
let pairs = PureYAML.Parsing.MergeResolution.mergedPairs(from: mergeSources, localPairs: localPairs)
return .mapping(PureYAML.Model.Mapping(pairs))
}

View File

@ -182,7 +182,7 @@ private extension PureYAML.Parsing.EventStreamComposer {
guard container.pendingKey == nil else {
throw unexpectedEvent(expected: "mapping value", mark: mark)
}
let pairs = mergedPairs(
let pairs = PureYAML.Parsing.MergeResolution.mergedPairs(
from: container.mergeSources,
localPairs: container.localPairs,
)
@ -251,7 +251,7 @@ private extension PureYAML.Parsing.EventStreamComposer {
guard !node.sequenceItems.isEmpty else {
return try values.map { value in
guard case let .mapping(mapping) = value else {
throw invalidMergeValue(mark: node.mark)
throw PureYAML.Parsing.MergeResolution.invalidMergeValue(mark: node.mark)
}
return mapping.pairs
}
@ -259,51 +259,15 @@ private extension PureYAML.Parsing.EventStreamComposer {
return try values.enumerated().map { index, value in
let mark = node.sequenceItems[index].mark
guard case let .mapping(mapping) = value else {
throw invalidMergeValue(mark: mark)
throw PureYAML.Parsing.MergeResolution.invalidMergeValue(mark: mark)
}
return mapping.pairs
}
case .bool, .double, .int, .null, .string:
throw invalidMergeValue(mark: node.mark)
throw PureYAML.Parsing.MergeResolution.invalidMergeValue(mark: node.mark)
}
}
func mergedPairs(
from mergeSources: [[PureYAML.Model.Pair]],
localPairs: [PureYAML.Model.Pair],
) -> [PureYAML.Model.Pair] {
guard !mergeSources.isEmpty else {
return localPairs
}
let localKeys = Set(localPairs.map(\.keyNode))
var inheritedKeys = Set<PureYAML.Model.Key>()
var inheritedPairs: [PureYAML.Model.Pair] = []
for source in mergeSources {
var sourceKeys = Set<PureYAML.Model.Key>()
for pair in source {
guard !localKeys.contains(pair.keyNode),
!inheritedKeys.contains(pair.keyNode)
else {
continue
}
inheritedPairs.append(pair)
sourceKeys.insert(pair.keyNode)
}
inheritedKeys.formUnion(sourceKeys)
}
return inheritedPairs + localPairs
}
func invalidMergeValue(
mark: PureYAML.Parsing.Mark,
) -> PureYAML.Parsing.ParseError {
PureYAML.Parsing.ParseError.invalidMergeValue(
line: mark.line,
column: mark.column,
)
}
func unexpectedEvent(
expected: String,
mark: PureYAML.Parsing.Mark,

View File

@ -0,0 +1,46 @@
extension PureYAML.Parsing {
/// Shared merge-key (`<<`) resolution used by both the buffered and the
/// streaming event composers, so the override semantics live in one place.
enum MergeResolution {
/// Combine merge sources with local pairs.
///
/// Local keys win over inherited keys, and earlier merge sources win
/// over later ones. Insertion order is inherited pairs first, then the
/// document's own local pairs.
static func mergedPairs(
from mergeSources: [[PureYAML.Model.Pair]],
localPairs: [PureYAML.Model.Pair],
) -> [PureYAML.Model.Pair] {
guard !mergeSources.isEmpty else {
return localPairs
}
let localKeys = Set(localPairs.map(\.keyNode))
var inheritedKeys = Set<PureYAML.Model.Key>()
var inheritedPairs: [PureYAML.Model.Pair] = []
for source in mergeSources {
var sourceKeys = Set<PureYAML.Model.Key>()
for pair in source {
guard !localKeys.contains(pair.keyNode),
!inheritedKeys.contains(pair.keyNode)
else {
continue
}
inheritedPairs.append(pair)
sourceKeys.insert(pair.keyNode)
}
inheritedKeys.formUnion(sourceKeys)
}
return inheritedPairs + localPairs
}
/// The parse error raised when a merge value is neither a mapping nor a
/// sequence of mappings.
static func invalidMergeValue(mark: PureYAML.Parsing.Mark) -> PureYAML.Parsing.ParseError {
PureYAML.Parsing.ParseError.invalidMergeValue(
line: mark.line,
column: mark.column,
)
}
}
}