Skip to content

Commit

Permalink
feat: dereference circular anchor refs
Browse files Browse the repository at this point in the history
  • Loading branch information
P0lip committed Apr 17, 2019
1 parent 0c6f530 commit fb18576
Show file tree
Hide file tree
Showing 2 changed files with 75 additions and 2 deletions.
23 changes: 23 additions & 0 deletions src/__tests__/parseWithPointers.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,4 +140,27 @@ prop2: true
]);
});
});

test('dereferences circular anchor refs', () => {
const result = parseWithPointers(`definitions:
model: &ref
foo:
name: *ref
`);
expect(result.data).toEqual({
definitions: {
model: {
foo: {
name: {
foo: {
name: undefined,
},
},
},
},
},
});

expect(() => JSON.stringify(result.data)).not.toThrow();
});
});
54 changes: 52 additions & 2 deletions src/parseWithPointers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { DiagnosticSeverity, IDiagnostic, IParserResult } from '@stoplight/types';
import { Kind, load as loadAST, YAMLException, YamlMap, YAMLNode, YAMLSequence } from 'yaml-ast-parser';
import {
Kind,
load as loadAST,
YAMLAnchorReference,
YAMLException,
YamlMap,
YAMLNode,
YAMLSequence,
} from 'yaml-ast-parser';

export const parseWithPointers = <T>(value: string): IParserResult<T, YAMLNode, number[]> => {
const lineMap = computeLineMap(value);
Expand Down Expand Up @@ -41,7 +49,11 @@ const walk = (node: YAMLNode | null): unknown => {
case Kind.SCALAR:
return 'valueObject' in node ? node.valueObject : node.value;
case Kind.ANCHOR_REF:
return;
if (node.value !== undefined && isCircularAnchorRef(node as YAMLAnchorReference)) {
node.value = dereferenceAnchor(node.value, node);
}

return walk(node.value);
default:
return null;
}
Expand All @@ -50,6 +62,44 @@ const walk = (node: YAMLNode | null): unknown => {
return node;
};

const isCircularAnchorRef = (anchorRef: YAMLAnchorReference) => {
const { referencesAnchor } = anchorRef;
let node: YAMLNode | undefined = anchorRef;
// tslint:disable-next-line:no-conditional-assignment
while ((node = node.parent)) {
if ('anchorId' in node && node.anchorId === referencesAnchor) {
return true;
}
}

return false;
};

const dereferenceAnchor = (node: YAMLNode, parent: YAMLNode): YAMLNode | YAMLNode[] | void => {
if (!node) return node;
if (node === parent) return;

switch (node.kind) {
case Kind.MAP:
return {
...node,
mappings: (node as YamlMap).mappings.map(mapping => dereferenceAnchor(mapping, parent) as YAMLNode),
} as YamlMap;
case Kind.SEQ:
return {
...node,
items: (node as YAMLSequence).items.map(item => dereferenceAnchor(item, parent) as YAMLNode),
} as YAMLSequence;
case Kind.MAPPING:
return { ...node, value: dereferenceAnchor(node.value, parent) };
case Kind.SCALAR:
case Kind.ANCHOR_REF:
return { ...node };
default:
return node;
}
};

// builds up the line map, for use by linesForPosition
const computeLineMap = (input: string) => {
const lines = input.split(/\n/);
Expand Down

0 comments on commit fb18576

Please sign in to comment.