fix(parsing): keep tag-handle diagnostics across a scan-aborting error

The diagnostic accessor ran the scan through `try?`, which discarded the whole
scanner state, and the tag-handle issues collected on it, on any error: the
comment promised the issues found before a scan-aborting error were returned,
but the code returned none. Split the run into a state-capturing `collect` that
returns the partial state with the aborting error, so `scan` still throws while
`tagHandleIssues` reports the diagnostics gathered before the abort. A
regression test pins an undeclared handle followed by an unterminated quote.
This commit is contained in:
Mihaela M 2026-06-29 21:43:25 +02:00
parent e64bff7cdf
commit ebf2e2477c
3 changed files with 51 additions and 6 deletions

View File

@ -94,6 +94,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Tag-handle diagnostics gathered before a scan-aborting error are now reported rather than dropped.
The diagnostic accessor ran the scan through `try?`, which discarded the whole scanner state, and
the tag-handle issues collected on it, on any error; a document with an undeclared `!handle!` and a
later fatal error reported nothing. The scanner now returns its partial state alongside the error so
the diagnostics survive the abort.
- A repeated document-end marker (`...` immediately followed by another `...`) is now accepted.
- A root-level block scalar (`|`/`>` as the whole document) now ends at a following document marker
(`---`/`...`) rather than folding the marker into its content.

View File

@ -3,17 +3,34 @@ extension PureYAML.Parsing {
init() {}
func scan(_ yaml: String) throws -> [Token] {
try run(yaml).tokens
let outcome = collect(yaml)
if let error = outcome.error {
throw error
}
return outcome.state.tokens
}
/// The tag-handle issues found while scanning, for the diagnostic layer. Issues collected
/// before a scan-aborting error are still returned; a clean scan returns them all.
/// The tag-handle issues found while scanning, for the diagnostic layer. A scan-aborting error
/// still leaves the issues collected before it on the returned state, so they are reported; a
/// clean scan returns the complete set.
func tagHandleIssues(_ yaml: String) -> [PureYAML.Parsing.TagHandleIssue] {
(try? run(yaml))?.tagHandleIssues ?? []
collect(yaml).state.tagHandleIssues
}
private func run(_ yaml: String) throws -> State {
/// Runs the scan and returns the resulting state with the error that aborted it, if any.
/// Returning the partial state keeps the diagnostics gathered before an abort, which a `try?`
/// over a throwing run would discard.
private func collect(_ yaml: String) -> (state: State, error: (any Error)?) {
var state = State(reader: Reader(yaml))
do {
try scanAll(&state)
return (state, nil)
} catch {
return (state, error)
}
}
private func scanAll(_ state: inout State) throws {
state.append(.streamStart)
while !state.reader.isAtEnd {
@ -48,7 +65,6 @@ extension PureYAML.Parsing {
closeIndentation(&state)
try enforceDirectiveHasDocument(state)
state.append(.streamEnd)
return state
}
}
}

View File

@ -0,0 +1,24 @@
@testable import PureYAML
import Testing
@Suite("Tag-handle diagnostics survive a scan abort")
struct TagHandleDiagnosticTests {
@Test("An undeclared named tag handle is recorded as a diagnostic")
func test_undeclaredHandleRecorded() {
let issues = PureYAML.Parsing.Scanner().tagHandleIssues("key: !foo!bar\n")
#expect(issues.map(\.handle) == ["!foo!"])
}
@Test("Diagnostics collected before a scan-aborting error are still returned")
func test_diagnosticsSurviveScanAbort() throws {
// The undeclared `!foo!` handle records a diagnostic on the first line; the unterminated
// quote aborts the scan on the second. The accessor must still report the diagnostic gathered
// before the abort rather than dropping it, which a `try?` over the throwing scan once did.
let yaml = "key: !foo!bar\nnext: \"unterminated\n"
#expect(throws: PureYAML.Parsing.ParseError.self) {
_ = try PureYAML.Parsing.Scanner().scan(yaml)
}
let issues = PureYAML.Parsing.Scanner().tagHandleIssues(yaml)
#expect(issues.map(\.handle) == ["!foo!"])
}
}