Skip to content

Commit fae37b8

Browse files
committed
fix: << outside a mapping key no longer leaks the internal merge symbol
1 parent b62ef83 commit fae37b8

5 files changed

Lines changed: 41 additions & 13 deletions

File tree

‎src/index.ts‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ export {
99

1010
export {
1111
NOT_RESOLVED,
12-
MERGE_KEY,
1312
defineScalarTag,
1413
defineSequenceTag,
1514
defineMappingTag,

‎src/parser/constructor.ts‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import {
1010
import { getScalarValue } from './parser_scalar.ts'
1111
import { CORE_SCHEMA, type Schema } from '../schema.ts'
1212
import {
13-
MERGE_KEY,
1413
NOT_RESOLVED,
1514
type MappingTagDefinition,
1615
type ScalarTagDefinition,
@@ -21,6 +20,8 @@ import { tagNameFull } from '../common/tagname.ts'
2120

2221
const NO_RANGE = -1
2322

23+
const MERGE_TAG_NAME = 'tag:yaml.org,2002:merge'
24+
2425
interface DocumentFrame {
2526
kind: 'document'
2627
position: number
@@ -46,6 +47,8 @@ interface MappingFrame {
4647
key: unknown
4748
keyPosition: number
4849
hasKey: boolean
50+
// The key slot drops its tag, but `<<` is recognized by tag, not by value.
51+
keyIsMerge: boolean
4952
// Keys brought in by a merge that an explicit pair is still allowed to
5053
// override. Lazily allocated: stays null for mappings without `<<`.
5154
overridable: Set<unknown> | null
@@ -276,7 +279,7 @@ function addMappingValue (state: ConstructorState, frame: MappingFrame, key: unk
276279
state.position = frame.keyPosition
277280

278281
// `<<` is intercepted before dedup, so a repeated merge key is allowed.
279-
if (key === MERGE_KEY) {
282+
if (frame.keyIsMerge) {
280283
mergeSource(state, frame, value, tag)
281284
return
282285
}
@@ -311,6 +314,7 @@ function addValue (state: ConstructorState, value: unknown, tag: AnyTag) {
311314
frame.key = value
312315
frame.keyPosition = state.position
313316
frame.hasKey = true
317+
frame.keyIsMerge = tag.tagName === MERGE_TAG_NAME
314318
}
315319
}
316320

@@ -409,6 +413,7 @@ function constructFromEvents (events: Event[], options: ConstructorOptions): unk
409413
key: undefined,
410414
keyPosition: state.position,
411415
hasKey: false,
416+
keyIsMerge: false,
412417
overridable: null
413418
})
414419
break

‎src/tag.ts‎

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,6 @@
55
*/
66
const NOT_RESOLVED: unique symbol = Symbol('NOT_RESOLVED')
77

8-
/**
9-
* Constructed value of the YAML merge key (`<<`).
10-
*
11-
* @category Tags
12-
*/
13-
const MERGE_KEY: unique symbol = Symbol('MERGE_KEY')
14-
158
/**
169
* Options for {@link defineScalarTag}.
1710
*
@@ -273,7 +266,6 @@ function defineMappingTag<Carrier, Result = Carrier> (tagName: string, options:
273266

274267
export {
275268
NOT_RESOLVED,
276-
MERGE_KEY,
277269
defineScalarTag,
278270
defineSequenceTag,
279271
defineMappingTag,

‎src/tag/scalar/merge.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { defineScalarTag, MERGE_KEY, NOT_RESOLVED } from '../../tag.ts'
1+
import { defineScalarTag, NOT_RESOLVED } from '../../tag.ts'
22

33
/**
44
* Enables merge keys in {@link CORE_SCHEMA} when added with
@@ -10,8 +10,10 @@ const mergeTag = defineScalarTag('tag:yaml.org,2002:merge', {
1010
implicit: true,
1111
// source.charAt(0) over matched implicit inputs: '<' ('<<').
1212
implicitFirstChars: ['<'],
13+
// Merge semantics live in the tag, not in the value: the constructor acts on
14+
// a key tagged `!!merge`, so `<<` anywhere else is just this string.
1315
resolve: (source, isExplicit) => {
14-
if (source === '<<' || (isExplicit && source === '')) return MERGE_KEY
16+
if (source === '<<' || (isExplicit && source === '')) return '<<'
1517
return NOT_RESOLVED
1618
},
1719
identify: () => false

‎test/core/tags/merge.test.mjs‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,36 @@ a: &x [[p, q]]
126126
assert.deepStrictEqual(load(src, { schema: YAML11_SCHEMA }), new Set(['a']))
127127
})
128128

129+
describe('`<<` outside of a mapping key', () => {
130+
it('as a mapping value', () => {
131+
assert.deepStrictEqual(load('foo: <<\n', { schema: YAML11_SCHEMA }), { foo: '<<' })
132+
})
133+
134+
it('as a sequence item', () => {
135+
assert.deepStrictEqual(load('- <<\n', { schema: YAML11_SCHEMA }), ['<<'])
136+
})
137+
138+
it('as a document root', () => {
139+
assert.deepStrictEqual(load('<<\n', { schema: YAML11_SCHEMA }), '<<')
140+
})
141+
142+
it('inside a nested mapping', () => {
143+
assert.deepStrictEqual(load('a: {b: <<}\n', { schema: YAML11_SCHEMA }), { a: { b: '<<' } })
144+
})
145+
146+
it('as an explicit !!merge tag on an empty node', () => {
147+
assert.deepStrictEqual(load('foo: !!merge\n', { schema: YAML11_SCHEMA }), { foo: '<<' })
148+
})
149+
150+
it('quoted, never a merge key', () => {
151+
assert.deepStrictEqual(load('foo: "<<"\n', { schema: YAML11_SCHEMA }), { foo: '<<' })
152+
})
153+
154+
it('still merges in the key position', () => {
155+
assert.deepStrictEqual(load('<<: {p: 1}\n', { schema: YAML11_SCHEMA }), { p: 1 })
156+
})
157+
})
158+
129159
it('Resolving explicit !!merge on empty node', () => {
130160
assert.doesNotThrow(() => load('? !!merge\n: []', { schema: CORE_SCHEMA.withTags(mergeTag) }))
131161
})

0 commit comments

Comments
 (0)