diff --git a/CHANGELOG.md b/CHANGELOG.md index c073da6..51ff6c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -84,6 +84,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- A scalar carrying an explicit non-core tag now stays a string instead of being implicitly resolved. + A node with a tag is not subject to plain-scalar resolution, so a local tag (`!foo`), the + non-specific `!`, or an unknown verbatim URI fixes the node as the string of its content: `! 12` is + the string `12`, not the integer, and `!Thing false` is the string `false`, not a boolean. + Recognized core tags (`!!int`, `!!bool`, ...) and untagged scalars still resolve. Two compatibility + tests that asserted the previous behavior were corrected. Raises yaml-test-suite value-level + conformance to 188/194 (96.9%), with the parse and should-error rates unchanged. + - A tab before a `#` now ends a plain scalar and begins a trailing comment, as a space already did, since a comment opens after any white space. `c: d\t#X` is the value `d` with a comment, not the literal `d\t#X`. A tab inside a plain scalar with no `#` after it stays part of the value, and a `#` diff --git a/Sources/Parsing/Parser.ScalarComposition.swift b/Sources/Parsing/Parser.ScalarComposition.swift index a74d68f..84b973d 100644 --- a/Sources/Parsing/Parser.ScalarComposition.swift +++ b/Sources/Parsing/Parser.ScalarComposition.swift @@ -9,6 +9,13 @@ extension PureYAML.Parsing.Parser { return tagged } + // An explicit tag that is not a recognized core type, a local tag like `!foo` or the + // non-specific `!`, still fixes the node as a string: a tagged scalar is not subject to the + // implicit plain-scalar resolution that would otherwise read `! 12` as the integer 12. + if normalizedTag(tag) != nil { + return .string(value) + } + switch style { case .folded, .literal: return .string(value) diff --git a/Tests/NonCoreTagScalarResolutionTests.swift b/Tests/NonCoreTagScalarResolutionTests.swift new file mode 100644 index 0000000..b9550e4 --- /dev/null +++ b/Tests/NonCoreTagScalarResolutionTests.swift @@ -0,0 +1,36 @@ +@testable import PureYAML +import Testing + +/// A scalar carrying an explicit tag is not subject to implicit plain-scalar resolution. A recognized +/// core tag (`!!int`, `!!bool`, ...) resolves to that type, but a local tag (`!foo`), the +/// non-specific `!`, or an unknown verbatim URI fixes the node as the string of its content: `! 12` +/// is the string `12`, not the integer. +@Suite("Non-core tag scalar resolution") +struct NonCoreTagScalarResolutionTests { + @Test("The non-specific ! tag keeps a number as a string") + func test_nonSpecificTag() throws { + #expect(try PureYAML.parse("- ! 12\n") == .sequence([.string("12")])) + } + + @Test("A local tag keeps a scalar as a string") + func test_localTag() throws { + #expect(try PureYAML.parse("- !foo 12\n") == .sequence([.string("12")])) + #expect(try PureYAML.parse("flag: !custom true\n") == .mapping(.init([ + .init(key: "flag", value: .string("true")), + ]))) + } + + @Test("An unknown verbatim URI tag keeps a scalar as a string") + func test_unknownVerbatimTag() throws { + #expect(try PureYAML.parse("x: ! true\n") == .mapping(.init([ + .init(key: "x", value: .string("true")), + ]))) + } + + @Test("Recognized core tags and untagged scalars still resolve") + func test_coreTagsStillResolve() throws { + #expect(try PureYAML.parse("- !!int 12\n") == .sequence([.int(12)])) + #expect(try PureYAML.parse("- !!bool true\n") == .sequence([.bool(true)])) + #expect(try PureYAML.parse("- 12\n") == .sequence([.int(12)])) + } +} diff --git a/Tests/ParsingCompatibilityTests.swift b/Tests/ParsingCompatibilityTests.swift index b2e9f0f..5903735 100644 --- a/Tests/ParsingCompatibilityTests.swift +++ b/Tests/ParsingCompatibilityTests.swift @@ -220,7 +220,9 @@ struct ParsingCompatibilityTests { #expect(root?["ratio"] == .double(3.5)) #expect(root?["enabled"] == .bool(false)) #expect(root?["missing"] == .null) - #expect(root?["custom"] == .bool(true)) + // A custom (non-core) tag excludes the node from implicit resolution, so the scalar content + // stays a string: `true` is not read as a boolean under an application-specific tag. + #expect(root?["custom"] == .string("true")) } private func expectScalar( diff --git a/Tests/TaggedCompatibilityTests.swift b/Tests/TaggedCompatibilityTests.swift index a849c0b..d690da9 100644 --- a/Tests/TaggedCompatibilityTests.swift +++ b/Tests/TaggedCompatibilityTests.swift @@ -25,7 +25,9 @@ struct TaggedCompatibilityTests { #expect(firstItem?.tag == .string) #expect(firstItem?.value == .string("true")) #expect(secondItem?.tag == .init("!Thing")) - #expect(secondItem?.value == .bool(false)) + // The `!Thing` local tag is preserved, and it excludes the scalar from implicit resolution, + // so its value stays the string `false` rather than being read as a boolean. + #expect(secondItem?.value == .string("false")) #expect(PureYAML.Tagged.Validator().collect(.mapping(root ?? .init(pairs: []))) == .init()) }