diff --git a/src/compose/compose-doc.ts b/src/compose/compose-doc.ts index cc65702e..e55c6cd3 100644 --- a/src/compose/compose-doc.ts +++ b/src/compose/compose-doc.ts @@ -24,6 +24,7 @@ export function composeDoc( const opts = Object.assign({ directives }, options) const doc = new Document(undefined, opts) as Document.Parsed const ctx: ComposeContext = { + anchors: options.version === 'next' ? new Map() : null, atRoot: true, directives: doc.directives, options: doc.options, diff --git a/src/compose/compose-node.ts b/src/compose/compose-node.ts index 6785777d..071f45a1 100644 --- a/src/compose/compose-node.ts +++ b/src/compose/compose-node.ts @@ -11,6 +11,7 @@ import { resolveEnd } from './resolve-end.js' import { emptyScalarPosition } from './util-empty-scalar-position.js' export interface ComposeContext { + anchors: Map | null atRoot: boolean directives: Directives options: Readonly>> @@ -51,13 +52,13 @@ export function composeNode( case 'double-quoted-scalar': case 'block-scalar': node = composeScalar(ctx, token, tag, onError) - if (anchor) node.anchor = anchor.source.substring(1) + if (anchor) setAnchor(ctx, node, anchor, onError) break case 'block-map': case 'block-seq': case 'flow-collection': node = composeCollection(CN, ctx, token, tag, onError) - if (anchor) node.anchor = anchor.source.substring(1) + if (anchor) setAnchor(ctx, node, anchor, onError) break default: { const message = @@ -134,3 +135,21 @@ function composeAlias( if (re.comment) alias.comment = re.comment return alias as Alias.Parsed } + +function setAnchor( + { anchors }: ComposeContext, + node: ParsedNode, + anchor: SourceToken, + onError: ComposeErrorHandler +) { + const name = anchor.source.substring(1) + if (anchors) { + if (anchors.has(name)) { + const msg = `Anchors must be unique, ${name} is repeated` + onError(node.range, 'DUPLICATE_ANCHOR', msg) + } else { + anchors.set(name, node) + } + } + node.anchor = name +} diff --git a/src/errors.ts b/src/errors.ts index a1db8a9c..84551e1b 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -10,6 +10,7 @@ export type ErrorCode = | 'BAD_SCALAR_START' | 'BLOCK_AS_IMPLICIT_KEY' | 'BLOCK_IN_FLOW' + | 'DUPLICATE_ANCHOR' | 'DUPLICATE_KEY' | 'IMPOSSIBLE' | 'KEY_OVER_1024_CHARS' diff --git a/tests/next.ts b/tests/next.ts index 1ee46d9c..f4e4bf7e 100644 --- a/tests/next.ts +++ b/tests/next.ts @@ -53,3 +53,24 @@ describe('relative-path alias', () => { expect(() => doc.toJS()).toThrow(ReferenceError) }) }) + +describe('unique anchors', () => { + test('repeats are fine without flag', () => { + const src = source` + - &a 1 + - &a 2 + - *a + ` + expect(parse(src)).toEqual([1, 2, 2]) + }) + + test("repeats are an error with 'next'", () => { + const src = source` + - &a 1 + - &a 2 + - *a + ` + const doc = parseDocument(src, { version: 'next' }) + expect(doc.errors).toMatchObject([{ code: 'DUPLICATE_ANCHOR' }]) + }) +})