Skip to content

Commit ca1ce09

Browse files
committed
feat: Require unique anchors during composition
1 parent 0f1f73e commit ca1ce09

File tree

4 files changed

+44
-2
lines changed

4 files changed

+44
-2
lines changed

src/compose/compose-doc.ts

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export function composeDoc(
2424
const opts = Object.assign({ _directives: directives }, options)
2525
const doc = new Document(undefined, opts) as Document.Parsed
2626
const ctx: ComposeContext = {
27+
anchors: options.version === 'next' ? new Map() : null,
2728
atRoot: true,
2829
directives: doc.directives,
2930
options: doc.options,

src/compose/compose-node.ts

+21-2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { resolveEnd } from './resolve-end.js'
1111
import { emptyScalarPosition } from './util-empty-scalar-position.js'
1212

1313
export interface ComposeContext {
14+
anchors: Map<string, ParsedNode> | null
1415
atRoot: boolean
1516
directives: Directives
1617
options: Readonly<Required<Omit<ParseOptions, 'lineCounter'>>>
@@ -52,13 +53,13 @@ export function composeNode(
5253
case 'double-quoted-scalar':
5354
case 'block-scalar':
5455
node = composeScalar(ctx, token, tag, onError)
55-
if (anchor) node.anchor = anchor.source.substring(1)
56+
if (anchor) setAnchor(ctx, node, anchor, onError)
5657
break
5758
case 'block-map':
5859
case 'block-seq':
5960
case 'flow-collection':
6061
node = composeCollection(CN, ctx, token, tag, onError)
61-
if (anchor) node.anchor = anchor.source.substring(1)
62+
if (anchor) setAnchor(ctx, node, anchor, onError)
6263
break
6364
default: {
6465
const message =
@@ -138,3 +139,21 @@ function composeAlias(
138139
if (re.comment) alias.comment = re.comment
139140
return alias as Alias.Parsed
140141
}
142+
143+
function setAnchor(
144+
{ anchors }: ComposeContext,
145+
node: ParsedNode,
146+
anchor: SourceToken,
147+
onError: ComposeErrorHandler
148+
) {
149+
const name = anchor.source.substring(1)
150+
if (anchors) {
151+
if (anchors.has(name)) {
152+
const msg = `Anchors must be unique, ${name} is repeated`
153+
onError(node.range, 'DUPLICATE_ANCHOR', msg)
154+
} else {
155+
anchors.set(name, node)
156+
}
157+
}
158+
node.anchor = name
159+
}

src/errors.ts

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export type ErrorCode =
1010
| 'BAD_SCALAR_START'
1111
| 'BLOCK_AS_IMPLICIT_KEY'
1212
| 'BLOCK_IN_FLOW'
13+
| 'DUPLICATE_ANCHOR'
1314
| 'DUPLICATE_KEY'
1415
| 'IMPOSSIBLE'
1516
| 'KEY_OVER_1024_CHARS'

tests/next.ts

+21
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,24 @@ describe('relative-path alias', () => {
5353
expect(() => doc.toJS()).toThrow(ReferenceError)
5454
})
5555
})
56+
57+
describe('unique anchors', () => {
58+
test('repeats are fine without flag', () => {
59+
const src = source`
60+
- &a 1
61+
- &a 2
62+
- *a
63+
`
64+
expect(parse(src)).toEqual([1, 2, 2])
65+
})
66+
67+
test("repeats are an error with 'next'", () => {
68+
const src = source`
69+
- &a 1
70+
- &a 2
71+
- *a
72+
`
73+
const doc = parseDocument(src, { version: 'next' })
74+
expect(doc.errors).toMatchObject([{ code: 'DUPLICATE_ANCHOR' }])
75+
})
76+
})

0 commit comments

Comments
 (0)