diff --git a/CHANGELOG.md b/CHANGELOG.md index ec5e3a5..d193238 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/Sources/Parsing/Scanner.swift b/Sources/Parsing/Scanner.swift index e8a3475..5fc19eb 100644 --- a/Sources/Parsing/Scanner.swift +++ b/Sources/Parsing/Scanner.swift @@ -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 } } } diff --git a/Tests/TagHandleDiagnosticTests.swift b/Tests/TagHandleDiagnosticTests.swift new file mode 100644 index 0000000..eb575d3 --- /dev/null +++ b/Tests/TagHandleDiagnosticTests.swift @@ -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!"]) + } +}