const {assert} = require('chai'); let Parser; let JavaScript; let JSON; let EmbeddedTemplate; let Python; const JSON_EXAMPLE = ` [ 123, false, { "x": null } ] `; function getAllNodes(tree) { const result = []; let visitedChildren = false; const cursor = tree.walk(); while (true) { if (!visitedChildren) { result.push(cursor.currentNode); if (!cursor.gotoFirstChild()) { visitedChildren = true; } } else if (cursor.gotoNextSibling()) { visitedChildren = false; } else if (!cursor.gotoParent()) { break; } } return result; } describe('Node', () => { let parser; let tree; before(async () => ({Parser, EmbeddedTemplate, JavaScript, JSON, Python} = await require('./helper')), ); beforeEach(() => { tree = null; parser = new Parser().setLanguage(JavaScript); }); afterEach(() => { parser.delete(); tree.delete(); }); describe('.children', () => { it('returns an array of child nodes', () => { tree = parser.parse('x10 + 1000'); assert.equal(1, tree.rootNode.children.length); const sumNode = tree.rootNode.firstChild.firstChild; assert.deepEqual( sumNode.children.map((child) => child.type), ['identifier', '+', 'number'], ); }); }); describe('.namedChildren', () => { it('returns an array of named child nodes', () => { tree = parser.parse('x10 + 1000'); const sumNode = tree.rootNode.firstChild.firstChild; assert.equal(1, tree.rootNode.namedChildren.length); assert.deepEqual( ['identifier', 'number'], sumNode.namedChildren.map((child) => child.type), ); }); }); describe('.childrenForFieldName', () => { it('returns an array of child nodes for the given field name', () => { parser.setLanguage(Python); const source = ` if one: a() elif two: b() elif three: c() elif four: d()`; tree = parser.parse(source); const node = tree.rootNode.firstChild; assert.equal(node.type, 'if_statement'); const alternatives = node.childrenForFieldName('alternative'); const alternativeTexts = alternatives.map((n) => { const condition = n.childForFieldName('condition'); return source.slice(condition.startIndex, condition.endIndex); }); assert.deepEqual(alternativeTexts, ['two', 'three', 'four']); }); }); describe('.startIndex and .endIndex', () => { it('returns the character index where the node starts/ends in the text', () => { tree = parser.parse('a👍👎1 / b👎c👎'); const quotientNode = tree.rootNode.firstChild.firstChild; assert.equal(0, quotientNode.startIndex); assert.equal(15, quotientNode.endIndex); assert.deepEqual( [0, 7, 9], quotientNode.children.map((child) => child.startIndex), ); assert.deepEqual( [6, 8, 15], quotientNode.children.map((child) => child.endIndex), ); }); }); describe('.startPosition and .endPosition', () => { it('returns the row and column where the node starts/ends in the text', () => { tree = parser.parse('x10 + 1000'); const sumNode = tree.rootNode.firstChild.firstChild; assert.equal('binary_expression', sumNode.type); assert.deepEqual({row: 0, column: 0}, sumNode.startPosition); assert.deepEqual({row: 0, column: 10}, sumNode.endPosition); assert.deepEqual( [{row: 0, column: 0}, {row: 0, column: 4}, {row: 0, column: 6}], sumNode.children.map((child) => child.startPosition), ); assert.deepEqual( [{row: 0, column: 3}, {row: 0, column: 5}, {row: 0, column: 10}], sumNode.children.map((child) => child.endPosition), ); }); it('handles characters that occupy two UTF16 code units', () => { tree = parser.parse('a👍👎1 /\n b👎c👎'); const sumNode = tree.rootNode.firstChild.firstChild; assert.deepEqual( [ [{row: 0, column: 0}, {row: 0, column: 6}], [{row: 0, column: 7}, {row: 0, column: 8}], [{row: 1, column: 1}, {row: 1, column: 7}], ], sumNode.children.map((child) => [child.startPosition, child.endPosition]), ); }); }); describe('.parent', () => { it('returns the node\'s parent', () => { tree = parser.parse('x10 + 1000'); const sumNode = tree.rootNode.firstChild; const variableNode = sumNode.firstChild; assert.notEqual(sumNode.id, variableNode.id); assert.equal(sumNode.id, variableNode.parent.id); assert.equal(tree.rootNode.id, sumNode.parent.id); }); }); describe('.child(), .firstChild, .lastChild', () => { it('returns null when the node has no children', () => { tree = parser.parse('x10 + 1000'); const sumNode = tree.rootNode.firstChild.firstChild; const variableNode = sumNode.firstChild; assert.equal(variableNode.firstChild, null); assert.equal(variableNode.lastChild, null); assert.equal(variableNode.firstNamedChild, null); assert.equal(variableNode.lastNamedChild, null); assert.equal(variableNode.child(1), null); }); }); describe('.childForFieldName()', () => { it('returns null when the node has no children', () => { tree = parser.parse('class A { b() {} }'); const classNode = tree.rootNode.firstChild; assert.equal(classNode.type, 'class_declaration'); const classNameNode = classNode.childForFieldName('name'); assert.equal(classNameNode.type, 'identifier'); assert.equal(classNameNode.text, 'A'); const bodyNode = classNode.childForFieldName('body'); assert.equal(bodyNode.type, 'class_body'); assert.equal(bodyNode.text, '{ b() {} }'); const methodNode = bodyNode.firstNamedChild; assert.equal(methodNode.type, 'method_definition'); assert.equal(methodNode.text, 'b() {}'); const methodNameNode = methodNode.childForFieldName('name'); assert.equal(methodNameNode.type, 'property_identifier'); assert.equal(methodNameNode.text, 'b'); const paramsNode = methodNode.childForFieldName('parameters'); assert.equal(paramsNode.type, 'formal_parameters'); assert.equal(paramsNode.text, '()'); }); }); describe('.nextSibling and .previousSibling', () => { it('returns the node\'s next and previous sibling', () => { tree = parser.parse('x10 + 1000'); const sumNode = tree.rootNode.firstChild.firstChild; assert.equal(sumNode.children[1].id, sumNode.children[0].nextSibling.id); assert.equal(sumNode.children[2].id, sumNode.children[1].nextSibling.id); assert.equal( sumNode.children[0].id, sumNode.children[1].previousSibling.id, ); assert.equal( sumNode.children[1].id, sumNode.children[2].previousSibling.id, ); }); }); describe('.nextNamedSibling and .previousNamedSibling', () => { it('returns the node\'s next and previous named sibling', () => { tree = parser.parse('x10 + 1000'); const sumNode = tree.rootNode.firstChild.firstChild; assert.equal( sumNode.namedChildren[1].id, sumNode.namedChildren[0].nextNamedSibling.id, ); assert.equal( sumNode.namedChildren[0].id, sumNode.namedChildren[1].previousNamedSibling.id, ); }); }); describe('.descendantForIndex(min, max)', () => { it('returns the smallest node that spans the given range', () => { tree = parser.parse('x10 + 1000'); const sumNode = tree.rootNode.firstChild.firstChild; assert.equal('identifier', sumNode.descendantForIndex(1, 2).type); assert.equal('+', sumNode.descendantForIndex(4, 4).type); assert.throws(() => { sumNode.descendantForIndex(1, {}); }, 'Arguments must be numbers'); assert.throws(() => { sumNode.descendantForIndex(); }, 'Arguments must be numbers'); }); }); describe('.namedDescendantForIndex', () => { it('returns the smallest node that spans the given range', () => { tree = parser.parse('x10 + 1000'); const sumNode = tree.rootNode.firstChild; assert.equal('identifier', sumNode.descendantForIndex(1, 2).type); assert.equal('+', sumNode.descendantForIndex(4, 4).type); }); }); describe('.descendantForPosition(min, max)', () => { it('returns the smallest node that spans the given range', () => { tree = parser.parse('x10 + 1000'); const sumNode = tree.rootNode.firstChild; assert.equal( 'identifier', sumNode.descendantForPosition( {row: 0, column: 1}, {row: 0, column: 2}, ).type, ); assert.equal( '+', sumNode.descendantForPosition({row: 0, column: 4}).type, ); assert.throws(() => { sumNode.descendantForPosition(1, {}); }, 'Arguments must be {row, column} objects'); assert.throws(() => { sumNode.descendantForPosition(); }, 'Arguments must be {row, column} objects'); }); }); describe('.namedDescendantForPosition(min, max)', () => { it('returns the smallest named node that spans the given range', () => { tree = parser.parse('x10 + 1000'); const sumNode = tree.rootNode.firstChild; assert.equal( sumNode.namedDescendantForPosition( {row: 0, column: 1}, {row: 0, column: 2}, ).type, 'identifier', ); assert.equal( sumNode.namedDescendantForPosition({row: 0, column: 4}).type, 'binary_expression', ); }); }); describe('.hasError', () => { it('returns true if the node contains an error', () => { tree = parser.parse('1 + 2 * * 3'); const node = tree.rootNode; assert.equal( node.toString(), '(program (expression_statement (binary_expression left: (number) right: (binary_expression left: (number) (ERROR) right: (number)))))', ); const sum = node.firstChild.firstChild; assert(sum.hasError); assert(!sum.children[0].hasError); assert(!sum.children[1].hasError); assert(sum.children[2].hasError); }); }); describe('.isError', () => { it('returns true if the node is an error', () => { tree = parser.parse('2 * * 3'); const node = tree.rootNode; assert.equal( node.toString(), '(program (expression_statement (binary_expression left: (number) (ERROR) right: (number))))', ); const multi = node.firstChild.firstChild; assert(multi.hasError); assert(!multi.children[0].isError); assert(!multi.children[1].isError); assert(multi.children[2].isError); assert(!multi.children[3].isError); }); }); describe('.isMissing', () => { it('returns true if the node is missing from the source and was inserted via error recovery', () => { tree = parser.parse('(2 ||)'); const node = tree.rootNode; assert.equal( node.toString(), '(program (expression_statement (parenthesized_expression (binary_expression left: (number) right: (MISSING identifier)))))', ); const sum = node.firstChild.firstChild.firstNamedChild; assert.equal(sum.type, 'binary_expression'); assert(sum.hasError); assert(!sum.children[0].isMissing); assert(!sum.children[1].isMissing); assert(sum.children[2].isMissing); }); }); describe('.isExtra', () => { it('returns true if the node is an extra node like comments', () => { tree = parser.parse('foo(/* hi */);'); const node = tree.rootNode; const commentNode = node.descendantForIndex(7, 7); assert.equal(node.type, 'program'); assert.equal(commentNode.type, 'comment'); assert(!node.isExtra); assert(commentNode.isExtra); }); }); describe('.text', () => { const text = 'α0 / b👎c👎'; Object.entries({ '.parse(String)': text, '.parse(Function)': (offset) => text.slice(offset, 4), }).forEach(([method, _parse]) => it(`returns the text of a node generated by ${method}`, async () => { const [numeratorSrc, denominatorSrc] = text.split(/\s*\/\s+/); tree = await parser.parse(text); const quotientNode = tree.rootNode.firstChild.firstChild; const [numerator, slash, denominator] = quotientNode.children; assert.equal(text, tree.rootNode.text, 'root node text'); assert.equal(denominatorSrc, denominator.text, 'denominator text'); assert.equal(text, quotientNode.text, 'quotient text'); assert.equal(numeratorSrc, numerator.text, 'numerator text'); assert.equal('/', slash.text, '"/" text'); }), ); }); describe('.descendantCount', () => { it('returns the number of descendants', () => { parser.setLanguage(JSON); tree = parser.parse(JSON_EXAMPLE); const valueNode = tree.rootNode; const allNodes = getAllNodes(tree); assert.equal(valueNode.descendantCount, allNodes.length); const cursor = tree.walk(); for (let i = 0; i < allNodes.length; i++) { const node = allNodes[i]; cursor.gotoDescendant(i); assert.equal(cursor.currentNode.id, node.id, `index ${i}`); } for (let i = allNodes.length - 1; i >= 0; i--) { const node = allNodes[i]; cursor.gotoDescendant(i); assert.equal(cursor.currentNode.id, node.id, `rev index ${i}`); } }); it('tests a single node tree', () => { parser.setLanguage(EmbeddedTemplate); tree = parser.parse('hello'); const nodes = getAllNodes(tree); assert.equal(nodes.length, 2); assert.equal(tree.rootNode.descendantCount, 2); const cursor = tree.walk(); cursor.gotoDescendant(0); assert.equal(cursor.currentDepth, 0); assert.equal(cursor.currentNode.id, nodes[0].id); cursor.gotoDescendant(1); assert.equal(cursor.currentDepth, 1); assert.equal(cursor.currentNode.id, nodes[1].id); }); }); describe('.rootNodeWithOffset', () => { it('returns the root node of the tree, offset by the given byte offset', () => { tree = parser.parse(' if (a) b'); const node = tree.rootNodeWithOffset(6, {row: 2, column: 2}); assert.equal(node.startIndex, 8); assert.equal(node.endIndex, 16); assert.deepEqual(node.startPosition, {row: 2, column: 4}); assert.deepEqual(node.endPosition, {row: 2, column: 12}); let child = node.firstChild.child(2); assert.equal(child.type, 'expression_statement'); assert.equal(child.startIndex, 15); assert.equal(child.endIndex, 16); assert.deepEqual(child.startPosition, {row: 2, column: 11}); assert.deepEqual(child.endPosition, {row: 2, column: 12}); const cursor = node.walk(); cursor.gotoFirstChild(); cursor.gotoFirstChild(); cursor.gotoNextSibling(); child = cursor.currentNode; assert.equal(child.type, 'parenthesized_expression'); assert.equal(child.startIndex, 11); assert.equal(child.endIndex, 14); assert.deepEqual(child.startPosition, {row: 2, column: 7}); assert.deepEqual(child.endPosition, {row: 2, column: 10}); }); }); describe('.parseState, .nextParseState', () => { const text = '10 / 5'; it('returns node parse state ids', async () => { tree = await parser.parse(text); const quotientNode = tree.rootNode.firstChild.firstChild; const [numerator, slash, denominator] = quotientNode.children; assert.equal(tree.rootNode.parseState, 0); // parse states will change on any change to the grammar so test that it // returns something instead assert.isAbove(numerator.parseState, 0); assert.isAbove(slash.parseState, 0); assert.isAbove(denominator.parseState, 0); }); it('returns next parse state equal to the language', async () => { tree = await parser.parse(text); const quotientNode = tree.rootNode.firstChild.firstChild; quotientNode.children.forEach((node) => { assert.equal( node.nextParseState, JavaScript.nextState(node.parseState, node.grammarId), ); }); }); }); describe('.descendantsOfType(type, min, max)', () => { it('finds all of the descendants of the given type in the given range', () => { tree = parser.parse('a + 1 * b * 2 + c + 3'); const outerSum = tree.rootNode.firstChild.firstChild; let descendants = outerSum.descendantsOfType('number', {row: 0, column: 2}, {row: 0, column: 15}); assert.deepEqual( descendants.map((node) => node.startIndex), [4, 12], ); assert.deepEqual( descendants.map((node) => node.endPosition), [{row: 0, column: 5}, {row: 0, column: 13}], ); descendants = outerSum.descendantsOfType('identifier', {row: 0, column: 2}, {row: 0, column: 15}); assert.deepEqual( descendants.map((node) => node.startIndex), [8], ); descendants = outerSum.descendantsOfType('identifier', {row: 0, column: 0}, {row: 0, column: 30}); assert.deepEqual( descendants.map((node) => node.startIndex), [0, 8, 16], ); descendants = outerSum.descendantsOfType('number', {row: 0, column: 0}, {row: 0, column: 30}); assert.deepEqual( descendants.map((node) => node.startIndex), [4, 12, 20], ); descendants = outerSum.descendantsOfType( ['identifier', 'number'], {row: 0, column: 0}, {row: 0, column: 30}, ); assert.deepEqual( descendants.map((node) => node.startIndex), [0, 4, 8, 12, 16, 20], ); descendants = outerSum.descendantsOfType('number'); assert.deepEqual( descendants.map((node) => node.startIndex), [4, 12, 20], ); descendants = outerSum.firstChild.descendantsOfType('number', {row: 0, column: 0}, {row: 0, column: 30}); assert.deepEqual( descendants.map((node) => node.startIndex), [4, 12], ); }); }); describe.skip('.closest(type)', () => { it('returns the closest ancestor of the given type', () => { tree = parser.parse('a(b + -d.e)'); const property = tree.rootNode.descendantForIndex('a(b + -d.'.length); assert.equal(property.type, 'property_identifier'); const unary = property.closest('unary_expression'); assert.equal(unary.type, 'unary_expression'); assert.equal(unary.startIndex, 'a(b + '.length); assert.equal(unary.endIndex, 'a(b + -d.e'.length); const sum = property.closest(['binary_expression', 'call_expression']); assert.equal(sum.type, 'binary_expression'); assert.equal(sum.startIndex, 2); assert.equal(sum.endIndex, 'a(b + -d.e'.length); }); it('throws an exception when an invalid argument is given', () => { tree = parser.parse('a + 1 * b * 2 + c + 3'); const number = tree.rootNode.descendantForIndex(4); assert.throws(() => number.closest({a: 1}), /Argument must be a string or array of strings/); }); }); describe('.firstChildForIndex(index)', () => { it('returns the first child that extends beyond the given index', () => { tree = parser.parse('x10 + 1000'); const sumNode = tree.rootNode.firstChild.firstChild; assert.equal('identifier', sumNode.firstChildForIndex(0).type); assert.equal('identifier', sumNode.firstChildForIndex(1).type); assert.equal('+', sumNode.firstChildForIndex(3).type); assert.equal('number', sumNode.firstChildForIndex(5).type); }); }); describe('.firstNamedChildForIndex(index)', () => { it('returns the first child that extends beyond the given index', () => { tree = parser.parse('x10 + 1000'); const sumNode = tree.rootNode.firstChild.firstChild; assert.equal('identifier', sumNode.firstNamedChildForIndex(0).type); assert.equal('identifier', sumNode.firstNamedChildForIndex(1).type); assert.equal('number', sumNode.firstNamedChildForIndex(3).type); }); }); describe('.equals(other)', () => { it('returns true if the nodes are the same', () => { tree = parser.parse('1 + 2'); const sumNode = tree.rootNode.firstChild.firstChild; const node1 = sumNode.firstChild; const node2 = sumNode.firstChild; assert(node1.equals(node2)); }); it('returns false if the nodes are not the same', () => { tree = parser.parse('1 + 2'); const sumNode = tree.rootNode.firstChild.firstChild; const node1 = sumNode.firstChild; const node2 = node1.nextSibling; assert(!node1.equals(node2)); }); }); describe('.fieldNameForChild(index)', () => { it('returns the field of a child or null', () => { tree = parser.parse('let a = 5'); const noField = tree.rootNode.fieldNameForChild(0); const name = tree.rootNode.firstChild.children[1].fieldNameForChild(0); const value = tree.rootNode.firstChild.children[1].fieldNameForChild(2); const overflow = tree.rootNode.firstChild.children[1].fieldNameForChild(3); assert.equal(noField, null); assert.equal(name, 'name'); assert.equal(value, 'value'); assert.equal(overflow, null); }); }); });