Skip to content

Commit cd20ba6

Browse files
authored
Fix recursion bug in ast-conversion (#1076)
* Ensure recursion works as expected * fix test safety for when parse fails * use a cache to keep track of resolved aliases
1 parent fce910a commit cd20ba6

File tree

2 files changed

+90
-13
lines changed

2 files changed

+90
-13
lines changed

src/languageservice/parser/ast-converter.ts

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,18 @@ import {
3232

3333
type NodeRange = [number, number, number];
3434

35-
const maxRefCount = 1000;
36-
let refDepth = 0;
37-
const seenAlias = new Set<Alias>();
35+
// Exported for tests
36+
export const aliasDepth = {
37+
maxRefCount: 1000,
38+
currentRefDepth: 0,
39+
aliasResolutionCache: new Map<Alias, ASTNode>(),
40+
};
3841

3942
export function convertAST(parent: ASTNode, node: YamlNode, doc: Document, lineCounter: LineCounter): ASTNode | undefined {
4043
if (!parent) {
4144
// first invocation
42-
refDepth = 0;
45+
aliasDepth.currentRefDepth = 0;
46+
aliasDepth.aliasResolutionCache = new Map();
4347
}
4448

4549
if (!node) {
@@ -57,11 +61,8 @@ export function convertAST(parent: ASTNode, node: YamlNode, doc: Document, lineC
5761
if (isScalar(node)) {
5862
return convertScalar(node, parent);
5963
}
60-
if (isAlias(node) && !seenAlias.has(node) && refDepth < maxRefCount) {
61-
seenAlias.add(node);
62-
const converted = convertAlias(node, parent, doc, lineCounter);
63-
seenAlias.delete(node);
64-
return converted;
64+
if (isAlias(node) && aliasDepth.currentRefDepth < aliasDepth.maxRefCount) {
65+
return convertAlias(node, parent, doc, lineCounter);
6566
} else {
6667
return;
6768
}
@@ -154,15 +155,23 @@ function convertScalar(node: Scalar, parent: ASTNode): ASTNode {
154155
}
155156

156157
function convertAlias(node: Alias, parent: ASTNode, doc: Document, lineCounter: LineCounter): ASTNode {
157-
refDepth++;
158+
if (aliasDepth.aliasResolutionCache.has(node)) {
159+
return aliasDepth.aliasResolutionCache.get(node);
160+
}
161+
162+
aliasDepth.currentRefDepth++;
158163
const resolvedNode = node.resolve(doc);
164+
let ans: ASTNode;
159165
if (resolvedNode) {
160-
return convertAST(parent, resolvedNode, doc, lineCounter);
166+
ans = convertAST(parent, resolvedNode, doc, lineCounter);
161167
} else {
162168
const resultNode = new StringASTNodeImpl(parent, node, ...toOffsetLength(node.range));
163169
resultNode.value = node.source;
164-
return resultNode;
170+
ans = resultNode;
165171
}
172+
aliasDepth.currentRefDepth--;
173+
aliasDepth.aliasResolutionCache.set(node, ans);
174+
return ans;
166175
}
167176

168177
export function toOffsetLength(range: NodeRange): [number, number] {

test/yamlParser.test.ts

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
import * as assert from 'assert';
66
import { expect } from 'chai';
77
import { ArrayASTNode, ObjectASTNode, PropertyASTNode } from '../src/languageservice/jsonASTTypes';
8-
import { parse } from './../src/languageservice/parser/yamlParser07';
8+
import { parse, YAMLDocument } from './../src/languageservice/parser/yamlParser07';
9+
import { aliasDepth } from '../src/languageservice/parser/ast-converter';
910

1011
describe('YAML parser', () => {
1112
describe('YAML parser', function () {
@@ -243,6 +244,73 @@ describe('YAML parser', () => {
243244
);
244245
assert(parsedDocument.documents[0].errors.length === 0, JSON.stringify(parsedDocument.documents[0].errors));
245246
});
247+
248+
it('parse aliases up to a depth', () => {
249+
// If maxRefCount is set to 1, it will only resolve one layer of aliases, which means
250+
// `b` below will inherit `a`, but `c` will not inherit `a`.
251+
let parsedDocument: YAMLDocument;
252+
try {
253+
aliasDepth.maxRefCount = 1;
254+
parsedDocument = parse(`
255+
a: &a
256+
foo: "web"
257+
b: &b
258+
<<: *a
259+
c: &c
260+
<<: *b
261+
`);
262+
} finally {
263+
aliasDepth.maxRefCount = 1000;
264+
}
265+
266+
const anode: ObjectASTNode = (parsedDocument.documents[0].root.children[0] as PropertyASTNode).valueNode as ObjectASTNode;
267+
const aval = anode.properties[0].valueNode;
268+
269+
const bnode: ObjectASTNode = (parsedDocument.documents[0].root.children[1] as PropertyASTNode).valueNode as ObjectASTNode;
270+
const bvalprops: PropertyASTNode = (bnode.properties[0].valueNode as ObjectASTNode).properties[0];
271+
const bval = bvalprops.valueNode;
272+
273+
const cnode: ObjectASTNode = (parsedDocument.documents[0].root.children[2] as PropertyASTNode).valueNode as ObjectASTNode;
274+
const cvalprops: PropertyASTNode = (cnode.properties[0].valueNode as ObjectASTNode).properties[0];
275+
const cval = cvalprops.valueNode;
276+
277+
assert(aval?.value === 'web');
278+
assert(bval?.value === 'web');
279+
assert(cval?.value === undefined);
280+
});
281+
282+
it('parse aliases up to a depth for multiple objects', () => {
283+
// In the below configuration, `c` will not inherit `a` because of depth issues
284+
// but the following object `o` will still resolve correctly.
285+
let parsedDocument: YAMLDocument;
286+
try {
287+
aliasDepth.maxRefCount = 1;
288+
parsedDocument = parse(`
289+
a: &a
290+
foo: "web"
291+
b: &b
292+
<<: *a
293+
c: &c
294+
<<: *b
295+
296+
o: &o
297+
<<: *a
298+
`);
299+
} finally {
300+
aliasDepth.maxRefCount = 1000;
301+
}
302+
303+
const onode: ObjectASTNode = (parsedDocument.documents[0].root.children[3] as PropertyASTNode).valueNode as ObjectASTNode;
304+
const ovalprops: PropertyASTNode = (onode.properties[0].valueNode as ObjectASTNode).properties[0];
305+
const oval = ovalprops.valueNode;
306+
307+
const cnode: ObjectASTNode = (parsedDocument.documents[0].root.children[2] as PropertyASTNode).valueNode as ObjectASTNode;
308+
const cvalprops: PropertyASTNode = (cnode.properties[0].valueNode as ObjectASTNode).properties[0];
309+
const cval = cvalprops.valueNode;
310+
311+
assert(cval?.value === undefined);
312+
assert(oval?.value === 'web');
313+
});
246314
});
247315

248316
describe('YAML parser bugs', () => {

0 commit comments

Comments
 (0)