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:
parent
e64bff7cdf
commit
ebf2e2477c
@ -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.
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
24
Tests/TagHandleDiagnosticTests.swift
Normal file
24
Tests/TagHandleDiagnosticTests.swift
Normal 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!"])
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user