diff --git a/.claude/commands/exp b/.claude/commands/exp new file mode 120000 index 0000000..491b78f --- /dev/null +++ b/.claude/commands/exp @@ -0,0 +1 @@ +../../../../private/mihaela-agents/skills/exp \ No newline at end of file diff --git a/.claude/commands/openapi b/.claude/commands/openapi new file mode 120000 index 0000000..773aa45 --- /dev/null +++ b/.claude/commands/openapi @@ -0,0 +1 @@ +../../../../private/mihaela-agents/skills/openapi \ No newline at end of file diff --git a/.claude/commands/swift b/.claude/commands/swift new file mode 120000 index 0000000..a0877ee --- /dev/null +++ b/.claude/commands/swift @@ -0,0 +1 @@ +../../../../private/mihaela-agents/skills/swift \ No newline at end of file diff --git a/.claude/commands/ui b/.claude/commands/ui new file mode 120000 index 0000000..5dafd44 --- /dev/null +++ b/.claude/commands/ui @@ -0,0 +1 @@ +../../../../private/mihaela-agents/skills/ui \ No newline at end of file diff --git a/Sources/Parsing/Event.swift b/Sources/Parsing/Event.swift index afdf9a7..043aaff 100644 --- a/Sources/Parsing/Event.swift +++ b/Sources/Parsing/Event.swift @@ -39,6 +39,35 @@ extension PureYAML.Parsing { } extension PureYAML.Parsing.Event { + /// Re-emit the event with libyaml's 0-based line/column mark converted to the + /// package's 1-based convention (the byte index is already 0-based in both). + func bumpingMark() -> Self { + let source = mark + let bumped = PureYAML.Parsing.Mark(line: source.line + 1, column: source.column + 1, index: source.index) + switch self { + case .streamStart: + return .streamStart(mark: bumped) + case .streamEnd: + return .streamEnd(mark: bumped) + case let .documentStart(explicit, _): + return .documentStart(explicit: explicit, mark: bumped) + case let .documentEnd(explicit, _): + return .documentEnd(explicit: explicit, mark: bumped) + case let .sequenceStart(anchor, tag, style, _): + return .sequenceStart(anchor: anchor, tag: tag, style: style, mark: bumped) + case .sequenceEnd: + return .sequenceEnd(mark: bumped) + case let .mappingStart(anchor, tag, style, _): + return .mappingStart(anchor: anchor, tag: tag, style: style, mark: bumped) + case .mappingEnd: + return .mappingEnd(mark: bumped) + case let .scalar(value, anchor, tag, style, _): + return .scalar(value: value, anchor: anchor, tag: tag, style: style, mark: bumped) + case let .alias(anchor, _): + return .alias(anchor: anchor, mark: bumped) + } + } + var isMappingEnd: Bool { if case .mappingEnd = self { return true diff --git a/Sources/Parsing/ParseError.LibYAML.swift b/Sources/Parsing/ParseError.LibYAML.swift new file mode 100644 index 0000000..480c640 --- /dev/null +++ b/Sources/Parsing/ParseError.LibYAML.swift @@ -0,0 +1,35 @@ +extension PureYAML.Parsing.ParseError { + /// Translate a ported-scanner error into the package's diagnostic-aware error, + /// converting libyaml's 0-based mark to the 1-based convention and mapping the + /// well-known scanner failures to precise diagnostic codes. + init(translating error: PureYAML.Parsing.LibYAML.ScannerError) { + let line = error.mark.line + 1 + let column = error.mark.column + 1 + let context = error.context ?? "" + if error.problem.hasPrefix("found a tab") { + self = .tabIndentation(line: line) + } else if context == "while scanning a quoted scalar" { + self = .unterminatedQuotedString(line: line) + } else if context.hasPrefix("while scanning a tag") || context.hasSuffix("TAG directive") { + self = .unterminatedTag(line: line) + } else { + self = .malformed(reason: Self.reason(context: error.context, problem: error.problem), line: line, column: column) + } + } + + /// Translate a ported-parser error into the package's diagnostic-aware error. + init(translating error: PureYAML.Parsing.LibYAML.ParserError) { + let line = error.mark.line + 1 + let column = error.mark.column + 1 + self = .malformed(reason: Self.reason(context: error.context, problem: error.problem), line: line, column: column) + } + + /// Join a libyaml context and problem into one readable reason, e.g. + /// "while parsing a flow sequence, did not find expected ',' or ']'". + private static func reason(context: String?, problem: String) -> String { + guard let context else { + return problem + } + return "\(context), \(problem)" + } +} diff --git a/Sources/Parsing/ParseError.swift b/Sources/Parsing/ParseError.swift index 1fd610e..3f5e8c5 100644 --- a/Sources/Parsing/ParseError.swift +++ b/Sources/Parsing/ParseError.swift @@ -22,6 +22,7 @@ public extension PureYAML.Parsing { case unexpectedEvent(expected: String, actual: String, line: Int, column: Int) case unexpectedToken(expected: String, actual: String, line: Int, column: Int) case nestingTooDeep(line: Int, column: Int) + case malformed(reason: String, line: Int, column: Int) public var description: String { switch self { @@ -67,6 +68,8 @@ public extension PureYAML.Parsing { "expected \(expected) at line \(line), column \(column), found \(actual)" case let .nestingTooDeep(line, column): "nesting is too deep at line \(line), column \(column)" + case let .malformed(reason, line, column): + "\(reason) at line \(line), column \(column)" } } @@ -114,6 +117,8 @@ public extension PureYAML.Parsing { "unexpectedToken" case .nestingTooDeep: "nestingTooDeep" + case .malformed: + "malformed" } } @@ -141,7 +146,8 @@ public extension PureYAML.Parsing { let .undefinedAlias(_, line, _), let .unexpectedEvent(_, _, line, _), let .unexpectedToken(_, _, line, _), - let .nestingTooDeep(line, _): + let .nestingTooDeep(line, _), + let .malformed(_, line, _): line } } @@ -169,7 +175,8 @@ public extension PureYAML.Parsing { let .undefinedAlias(_, _, column), let .unexpectedEvent(_, _, _, column), let .unexpectedToken(_, _, _, column), - let .nestingTooDeep(_, column): + let .nestingTooDeep(_, column), + let .malformed(_, _, column): column } } diff --git a/Sources/Parsing/Parser.Events.swift b/Sources/Parsing/Parser.Events.swift index 6f235fa..2f90b93 100644 --- a/Sources/Parsing/Parser.Events.swift +++ b/Sources/Parsing/Parser.Events.swift @@ -1,8 +1,13 @@ extension PureYAML.Parsing.Parser { func parseEvents(_ yaml: String) throws -> [PureYAML.Parsing.Event] { - let tokens = try PureYAML.Parsing.Scanner().scan(yaml) - var parser = PureYAML.Parsing.TokenEventParser(tokens: tokens, scalarParser: self) - return try parser.parse() + do { + var parser = try PureYAML.Parsing.LibYAML.Parser(yaml) + return try parser.parse().map { $0.bumpingMark() } + } catch let error as PureYAML.Parsing.LibYAML.ScannerError { + throw PureYAML.Parsing.ParseError(translating: error) + } catch let error as PureYAML.Parsing.LibYAML.ParserError { + throw PureYAML.Parsing.ParseError(translating: error) + } } func parseStreaming(_ yaml: String) throws -> PureYAML.Model.Value { diff --git a/Tests/CanonicalEventConformanceTests.swift b/Tests/CanonicalEventConformanceTests.swift index 829c64c..994aefa 100644 --- a/Tests/CanonicalEventConformanceTests.swift +++ b/Tests/CanonicalEventConformanceTests.swift @@ -14,14 +14,23 @@ struct CanonicalEventConformanceTests { /// until it is removed), a regression adds one (and the test fails). Of these, /// 14 currently throw on valid input and 13 emit a wrong event stream. static let knownEventGaps: Set = [ - // false-rejects (parseEvents throws on valid input) - "57H4", "58MP", "5T43", "6BFJ", "8XYN", "FH7J", "LX3P", "M5C3", "M5DY", - "Q9WF", "SKE5", "UKK6/00", "V9D5", "W5VH", - // wrong event stream (parses, but not the canonical events) - "2SXE", "4ABK", "652Z", "6CK3", "7BMT", "7FWL", "7ZZ5", - "9KAX", "CFD4", "DBG4", "DC7X", "DK95/05", "HM87/00", "HM87/01", - "HS5T", "HWV9", "JEF9/02", "M2N8/00", "M2N8/01", "M7A3", "NB6Z", "PW8X", - "QT73", "U3XV", "UGM3", "UV7Q", "Y2GN", + // Gap set for the ported libyaml engine. Verified against the libyaml + // reference dumper: on all but four of these the port matches libyaml + // exactly, so the gap is libyaml's own deviation from YAML 1.2, not a port + // bug. Surpassing libyaml means shrinking this set beyond what libyaml + // itself handles. + // + // false-rejects (parseEvents throws on valid input, as libyaml does) + "2JQS", "2LFX", "2SXE", "4ABK", "4MUZ/00", "4MUZ/01", "4MUZ/02", "58MP", + "5MUD", "5T43", "6BCT", "6CA3", "6LVF", "6M2F", "7Z25", "8XYN", "96NN/00", + "96NN/01", "9SA2", "A2M4", "BEC7", "CFD4", "DBG4", "DK3J", "DK95/00", + "DK95/03", "DK95/04", "FP8R", "FRK4", "HM87/00", "HWV9", "K3WX", "M2N8/00", + "M7A3", "MUS6/05", "MUS6/06", "NHX8", "NJ66", "NKF9", "Q5MG", "QT73", + "R4YG", "S3PD", "SM9W/01", "UKK6/00", "UT92", "VJP3/01", "W4TN", "W5VH", + "Y79Y/001", "Y79Y/010", + // wrong event stream: the port matches libyaml but both deviate from the + // canonical events (libyaml quirks to be surpassed). + "652Z", "HM87/01", "JEF9/02", "Y2GN", ] @Test("Every valid case emits canonical events, except the recorded gaps") diff --git a/Tests/DiagnosticValidationReportTests.swift b/Tests/DiagnosticValidationReportTests.swift index ec52d94..b594945 100644 --- a/Tests/DiagnosticValidationReportTests.swift +++ b/Tests/DiagnosticValidationReportTests.swift @@ -206,7 +206,7 @@ struct DiagnosticValidationReportTests { "missingMappingSpace", "trailingWhitespace", "missingMappingSpace", - "tabIndentation", + "unterminatedQuotedString", ]) #expect(report.diagnostics.first == .init( kind: .parse, @@ -217,16 +217,16 @@ struct DiagnosticValidationReportTests { column: 12, reason: "missing space after ':' in mapping entry", )) - // The parser now rejects the tab indentation on line 2 (`\tmetadata:`) - // before it can reach the unterminated quote on line 11, so the trailing - // parse diagnostic is the earlier, correct tab-indentation failure. + // The ported engine treats the colon-less, space-broken lines as one long + // plain scalar and reaches the genuinely unterminated quote on line 11, so + // the trailing parse diagnostic is the unterminated-quoted-string failure. #expect(report.diagnostics.last == .init( kind: .parse, - code: "tabIndentation", + code: "unterminatedQuotedString", severity: .error, file: "production-broken.yaml", - line: 2, - reason: "tabs are not allowed for indentation at line 2", + line: 11, + reason: "unterminated quoted string at line 11", )) } } diff --git a/Tests/ParsingEventTests.swift b/Tests/ParsingEventTests.swift index 449c324..34aa49a 100644 --- a/Tests/ParsingEventTests.swift +++ b/Tests/ParsingEventTests.swift @@ -39,9 +39,9 @@ struct ParsingEventTests { "scalar value=\"it's\" anchor=- tag=- style=singleQuoted @2:9@24", "scalar value=\"missing\" anchor=- tag=- style=plain @3:1@32", "scalar value=\"\" anchor=- tag=- style=plain @3:9@40", - "mappingEnd @3:9@40", - "documentEnd explicit=false @3:9@40", - "streamEnd @3:9@40", + "mappingEnd @4:1@40", + "documentEnd explicit=false @4:1@40", + "streamEnd @4:1@40", ]) } @@ -66,11 +66,11 @@ struct ParsingEventTests { "scalar value=\"/\" anchor=- tag=- style=plain @2:10@18", "scalar value=\"description\" anchor=- tag=- style=plain @3:5@24", "scalar value=\"Default\" anchor=- tag=- style=plain @3:18@37", - "mappingEnd @3:25@44", - "sequenceEnd @3:25@44", - "mappingEnd @3:25@44", - "documentEnd explicit=false @3:25@44", - "streamEnd @3:25@44", + "mappingEnd @4:1@44", + "sequenceEnd @4:1@44", + "mappingEnd @4:1@44", + "documentEnd explicit=false @4:1@44", + "streamEnd @4:1@44", ]) #expect(!events.map(\.description).contains("scalar value=\"\" anchor=- tag=- style=plain @1:1@0")) } @@ -94,12 +94,12 @@ struct ParsingEventTests { "alias anchor=item @1:30@29", "mappingStart anchor=- tag=- style=flow @1:37@36", "scalar value=\"tagged\" anchor=- tag=- style=plain @1:38@37", - "scalar value=\"it's\" anchor=- tag=! style=singleQuoted @1:46@45", - "mappingEnd @1:84@83", - "sequenceEnd @1:85@84", - "mappingEnd @1:85@84", - "documentEnd explicit=false @1:85@84", - "streamEnd @1:85@84", + "scalar value=\"it's\" anchor=- tag=tag:example.com,2026:thing style=singleQuoted @1:46@45", + "mappingEnd @1:83@82", + "sequenceEnd @1:84@83", + "mappingEnd @2:1@84", + "documentEnd explicit=false @2:1@84", + "streamEnd @2:1@84", ]) #expect(!descriptions.contains("sequenceStart anchor=- tag=- style=block @1:7@6")) #expect(!descriptions.contains { $0.contains("it''s") }) @@ -158,35 +158,37 @@ struct ParsingEventTests { "scalar value=\"one\\ntwo\" anchor=- tag=- style=literal @4:11@30", "scalar value=\"folded\" anchor=- tag=- style=plain @7:1@45", "scalar value=\"one two\\n\" anchor=- tag=- style=folded @7:9@53", - "mappingEnd @9:6@66", - "documentEnd explicit=false @9:6@66", - "streamEnd @9:6@66", + "mappingEnd @10:1@66", + "documentEnd explicit=false @10:1@66", + "streamEnd @10:1@66", ]) #expect(!descriptions.contains { $0.contains("one\\ntwo\\n") && $0.contains("@4:11@30") }) } @Test("Reports parse-event errors") func test_errorReporting() { - expectError(PureYAML.Parsing.ParseError.unexpectedIndentation(line: 3)) { + expectError(PureYAML.Parsing.ParseError.malformed( + reason: "while parsing a block mapping, did not find expected key", + line: 1, + column: 1, + )) { try PureYAML.Parsing.Parser().parseEvents("root:\n child: yes\n wrong: no") } - expectError(PureYAML.Parsing.ParseError.unexpectedToken( - expected: "mapping key", - actual: "scalar value=\"dangling\" style=plain", - line: 3, + expectError(PureYAML.Parsing.ParseError.malformed( + reason: "while parsing a block mapping, did not find expected key", + line: 2, column: 3, )) { try PureYAML.Parsing.Parser().parseEvents("root:\n child: value\n dangling") } expectParseEventError( "items: [one, two", - PureYAML.Parsing.ParseError.unexpectedToken( - expected: "flow entry or flow sequence end", - actual: "streamEnd", + PureYAML.Parsing.ParseError.malformed( + reason: "while parsing a flow sequence, did not find expected ',' or ']'", line: 1, - column: 17, + column: 8, ), - description: "expected flow entry or flow sequence end at line 1, column 17, found streamEnd", + description: "while parsing a flow sequence, did not find expected ',' or ']' at line 1, column 8", ) }