fix(parsing): keep a non-core-tagged scalar as a string
A node with an explicit tag is not subject to implicit plain-scalar resolution, but composeScalarValue fell through to it whenever the tag was not a recognized core type. So a local tag (`!foo`), the non-specific `!`, or an unknown verbatim URI still let `! 12` resolve to the integer 12 and `!Thing false` to a boolean. When a tag is present but not a core type, fix the node as the string of its content instead of resolving it. Recognized core tags and untagged scalars are unchanged. Two compatibility tests asserted the old behavior (a custom- tagged `true` as a boolean); they are corrected to expect the string, the behavior the yaml-test-suite case S4JQ requires. Raises yaml-test-suite value-level conformance to 188/194 (96.9%) with the parse-level and should-error rejection rates unchanged. 452 tests pass.
This commit is contained in:
parent
6c322fc5ed
commit
64f3f71211
@ -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 `#`
|
||||
|
||||
@ -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)
|
||||
|
||||
36
Tests/NonCoreTagScalarResolutionTests.swift
Normal file
36
Tests/NonCoreTagScalarResolutionTests.swift
Normal file
@ -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: !<tag:example.com,2026:thing> 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)]))
|
||||
}
|
||||
}
|
||||
@ -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(
|
||||
|
||||
@ -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())
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user