PureYAML/Sources/Parsing/EventStreamComposer.Located.swift
Mihaela Mihaljevic 3f3ce310b1 feat(parsing): opt-in located parse with source spans on every node
Add parseLocated / parseStreamLocated, returning a Model.LocatedValue
tree parallel in shape to Value, where every node carries its source
span (1-based line/column) and located children. Collections span from
their opening to closing event; scalars carry a start-anchored span;
aliases and merge-inherited pairs honestly point at their reference site.

The plain Value, its equality, and the conformance oracle are untouched:
locations are built only when the streaming composer is asked for them
(tracksLocations), behind new entry points, so the default value path is
byte-identical. A whole-suite test proves the located projection equals
a plain parse for every passing case (402/402), including through merge
keys and aliases, and the canonical-event and gap gates stay green.
2026-06-26 00:57:55 +02:00

64 lines
2.6 KiB
Swift

extension PureYAML.Parsing.EventStreamComposer {
func range(at mark: PureYAML.Parsing.Mark) -> PureYAML.Model.SourceRange {
.init(at: .init(line: mark.line, column: mark.column))
}
func range(start: PureYAML.Parsing.Mark, end: PureYAML.Parsing.Mark) -> PureYAML.Model.SourceRange {
.init(start: .init(line: start.line, column: start.column), end: .init(line: end.line, column: end.column))
}
/// The node's located projection, falling back to a synthesized one at the node's mark if
/// the located field was somehow not populated (never expected while tracking locations).
func locatedNode(_ node: PureYAML.Parsing.EventStreamComposedNode) -> PureYAML.Model.LocatedValue {
node.located ?? .synthesized(from: node.value, at: range(at: node.mark))
}
/// The located pairs a merge value (`<<`) contributes, mirroring ``mergeSources(from:)``.
/// The value-side merge validation has already run, so the located shape is a mapping or a
/// sequence of mappings; anything else contributes nothing.
func locatedMergeSources(from node: PureYAML.Parsing.EventStreamComposedNode) -> [[PureYAML.Model.LocatedPair]] {
switch locatedNode(node) {
case let .mapping(pairs, _):
[pairs]
case let .sequence(items, _):
items.compactMap { item in
guard case let .mapping(pairs, _) = item else {
return nil
}
return pairs
}
case .scalar:
[]
}
}
/// The final located pairs, mirroring ``mergedPairs(from:localPairs:)`` exactly (same dedup,
/// same order) so the located mapping has the identical shape to the value mapping.
func mergedLocatedPairs(
from mergeSources: [[PureYAML.Model.LocatedPair]],
localPairs: [PureYAML.Model.LocatedPair],
) -> [PureYAML.Model.LocatedPair] {
guard !mergeSources.isEmpty else {
return localPairs
}
let localKeys = Set(localPairs.map(\.keyNode))
var inheritedKeys = Set<PureYAML.Model.Key>()
var inheritedPairs: [PureYAML.Model.LocatedPair] = []
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
}
}