2025-01-13 01:48:42 -05:00
|
|
|
|
import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest';
|
2025-01-20 03:12:52 -05:00
|
|
|
|
import type { Language, Tree, Node } from '../src';
|
|
|
|
|
|
import { Parser } from '../src';
|
2025-01-13 01:48:42 -05:00
|
|
|
|
import helper from './helper';
|
|
|
|
|
|
|
|
|
|
|
|
let C: Language;
|
|
|
|
|
|
let JavaScript: Language;
|
|
|
|
|
|
let JSON: Language;
|
|
|
|
|
|
let EmbeddedTemplate: Language;
|
|
|
|
|
|
let Python: Language;
|
|
|
|
|
|
|
|
|
|
|
|
const JSON_EXAMPLE = `
|
|
|
|
|
|
[
|
|
|
|
|
|
123,
|
|
|
|
|
|
false,
|
|
|
|
|
|
{
|
|
|
|
|
|
"x": null
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
2025-01-19 15:15:01 -05:00
|
|
|
|
function getAllNodes(tree: Tree): Node[] {
|
|
|
|
|
|
const result: Node[] = [];
|
2025-01-13 01:48:42 -05:00
|
|
|
|
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', () => {
|
2025-01-20 03:12:52 -05:00
|
|
|
|
let parser: Parser;
|
2025-01-13 01:48:42 -05:00
|
|
|
|
let tree: Tree | null;
|
|
|
|
|
|
|
|
|
|
|
|
beforeAll(async () => {
|
2025-01-20 03:12:52 -05:00
|
|
|
|
({ C, EmbeddedTemplate, JavaScript, JSON, Python } = await helper);
|
2025-01-13 01:48:42 -05:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
|
tree = null;
|
|
|
|
|
|
parser = new Parser();
|
|
|
|
|
|
parser.setLanguage(JavaScript);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
|
parser.delete();
|
2025-01-20 03:12:52 -05:00
|
|
|
|
tree!.delete();
|
2025-01-13 01:48:42 -05:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
describe('.children', () => {
|
|
|
|
|
|
it('returns an array of child nodes', () => {
|
2025-01-20 03:12:52 -05:00
|
|
|
|
tree = parser.parse('x10 + 1000')!;
|
2025-01-13 01:48:42 -05:00
|
|
|
|
expect(tree.rootNode.children).toHaveLength(1);
|
2025-01-20 03:12:52 -05:00
|
|
|
|
const sumNode = tree.rootNode.firstChild!.firstChild!;
|
2025-02-19 17:47:10 -05:00
|
|
|
|
expect(sumNode.children.map(child => child!.type)).toEqual(['identifier', '+', 'number']);
|
2025-01-13 01:48:42 -05:00
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
describe('.namedChildren', () => {
|
|
|
|
|
|
it('returns an array of named child nodes', () => {
|
2025-01-20 03:12:52 -05:00
|
|
|
|
tree = parser.parse('x10 + 1000')!;
|
|
|
|
|
|
const sumNode = tree.rootNode.firstChild!.firstChild!;
|
2025-01-13 01:48:42 -05:00
|
|
|
|
expect(tree.rootNode.namedChildren).toHaveLength(1);
|
2025-01-20 03:12:52 -05:00
|
|
|
|
expect(sumNode.namedChildren.map(child => child!.type)).toEqual(['identifier', 'number']);
|
2025-01-13 01:48:42 -05:00
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
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()`;
|
|
|
|
|
|
|
2025-01-20 03:12:52 -05:00
|
|
|
|
tree = parser.parse(source)!;
|
|
|
|
|
|
const node = tree.rootNode.firstChild!;
|
|
|
|
|
|
expect(node.type).toBe('if_statement');
|
|
|
|
|
|
const alternatives = node.childrenForFieldName('alternative');
|
|
|
|
|
|
const alternativeTexts = alternatives.map(n => {
|
|
|
|
|
|
const condition = n!.childForFieldName('condition')!;
|
|
|
|
|
|
return source.slice(condition.startIndex, condition.endIndex);
|
2025-01-13 01:48:42 -05:00
|
|
|
|
});
|
|
|
|
|
|
expect(alternativeTexts).toEqual(['two', 'three', 'four']);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
describe('.startIndex and .endIndex', () => {
|
|
|
|
|
|
it('returns the character index where the node starts/ends in the text', () => {
|
2025-01-20 03:12:52 -05:00
|
|
|
|
tree = parser.parse('a👍👎1 / b👎c👎')!;
|
|
|
|
|
|
const quotientNode = tree.rootNode.firstChild!.firstChild!;
|
2025-01-13 01:48:42 -05:00
|
|
|
|
|
2025-01-20 03:12:52 -05:00
|
|
|
|
expect(quotientNode.startIndex).toBe(0);
|
|
|
|
|
|
expect(quotientNode.endIndex).toBe(15);
|
|
|
|
|
|
expect(quotientNode.children.map(child => child!.startIndex)).toEqual([0, 7, 9]);
|
|
|
|
|
|
expect(quotientNode.children.map(child => child!.endIndex)).toEqual([6, 8, 15]);
|
2025-01-13 01:48:42 -05:00
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
describe('.startPosition and .endPosition', () => {
|
|
|
|
|
|
it('returns the row and column where the node starts/ends in the text', () => {
|
2025-01-20 03:12:52 -05:00
|
|
|
|
tree = parser.parse('x10 + 1000')!;
|
|
|
|
|
|
const sumNode = tree.rootNode.firstChild!.firstChild!;
|
|
|
|
|
|
expect(sumNode.type).toBe('binary_expression');
|
2025-01-13 01:48:42 -05:00
|
|
|
|
|
2025-01-20 03:12:52 -05:00
|
|
|
|
expect(sumNode.startPosition).toEqual({ row: 0, column: 0 });
|
|
|
|
|
|
expect(sumNode.endPosition).toEqual({ row: 0, column: 10 });
|
|
|
|
|
|
expect(sumNode.children.map((child) => child!.startPosition)).toEqual([
|
2025-01-13 01:48:42 -05:00
|
|
|
|
{ row: 0, column: 0 },
|
|
|
|
|
|
{ row: 0, column: 4 },
|
|
|
|
|
|
{ row: 0, column: 6 },
|
|
|
|
|
|
]);
|
2025-01-20 03:12:52 -05:00
|
|
|
|
expect(sumNode.children.map((child) => child!.endPosition)).toEqual([
|
2025-01-13 01:48:42 -05:00
|
|
|
|
{ row: 0, column: 3 },
|
|
|
|
|
|
{ row: 0, column: 5 },
|
|
|
|
|
|
{ row: 0, column: 10 },
|
|
|
|
|
|
]);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('handles characters that occupy two UTF16 code units', () => {
|
2025-01-20 03:12:52 -05:00
|
|
|
|
tree = parser.parse('a👍👎1 /\n b👎c👎')!;
|
|
|
|
|
|
const sumNode = tree.rootNode.firstChild!.firstChild!;
|
|
|
|
|
|
expect(sumNode.children.map(child => [child!.startPosition, child!.endPosition])).toEqual([
|
2025-01-13 01:48:42 -05:00
|
|
|
|
[{ row: 0, column: 0 }, { row: 0, column: 6 }],
|
|
|
|
|
|
[{ row: 0, column: 7 }, { row: 0, column: 8 }],
|
|
|
|
|
|
[{ row: 1, column: 1 }, { row: 1, column: 7 }]
|
|
|
|
|
|
]);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
describe('.parent', () => {
|
|
|
|
|
|
it('returns the node\'s parent', () => {
|
2025-01-20 03:12:52 -05:00
|
|
|
|
tree = parser.parse('x10 + 1000')!;
|
|
|
|
|
|
const sumNode = tree.rootNode.firstChild!;
|
|
|
|
|
|
const variableNode = sumNode.firstChild!;
|
|
|
|
|
|
expect(sumNode.id).not.toBe(variableNode.id);
|
|
|
|
|
|
expect(sumNode.id).toBe(variableNode.parent!.id);
|
|
|
|
|
|
expect(tree.rootNode.id).toBe(sumNode.parent!.id);
|
2025-01-13 01:48:42 -05:00
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
describe('.child(), .firstChild, .lastChild', () => {
|
|
|
|
|
|
it('returns null when the node has no children', () => {
|
2025-01-20 03:12:52 -05:00
|
|
|
|
tree = parser.parse('x10 + 1000')!;
|
|
|
|
|
|
const sumNode = tree.rootNode.firstChild!.firstChild!;
|
|
|
|
|
|
const variableNode = sumNode.firstChild!;
|
|
|
|
|
|
expect(variableNode.firstChild).toBeNull();
|
|
|
|
|
|
expect(variableNode.lastChild).toBeNull();
|
|
|
|
|
|
expect(variableNode.firstNamedChild).toBeNull();
|
|
|
|
|
|
expect(variableNode.lastNamedChild).toBeNull();
|
|
|
|
|
|
expect(variableNode.child(1)).toBeNull();
|
2025-01-13 01:48:42 -05:00
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
describe('.childForFieldName()', () => {
|
|
|
|
|
|
it('returns node for the given field name', () => {
|
2025-01-20 03:12:52 -05:00
|
|
|
|
tree = parser.parse('class A { b() {} }')!;
|
2025-01-13 01:48:42 -05:00
|
|
|
|
|
2025-01-20 03:12:52 -05:00
|
|
|
|
const classNode = tree.rootNode.firstChild!;
|
|
|
|
|
|
expect(classNode.type).toBe('class_declaration');
|
2025-01-13 01:48:42 -05:00
|
|
|
|
|
2025-01-20 03:12:52 -05:00
|
|
|
|
const classNameNode = classNode.childForFieldName('name')!;
|
|
|
|
|
|
expect(classNameNode.type).toBe('identifier');
|
|
|
|
|
|
expect(classNameNode.text).toBe('A');
|
2025-01-13 01:48:42 -05:00
|
|
|
|
|
2025-01-20 03:12:52 -05:00
|
|
|
|
const bodyNode = classNode.childForFieldName('body')!;
|
|
|
|
|
|
expect(bodyNode.type).toBe('class_body');
|
|
|
|
|
|
expect(bodyNode.text).toBe('{ b() {} }');
|
2025-01-13 01:48:42 -05:00
|
|
|
|
|
2025-01-20 03:12:52 -05:00
|
|
|
|
const methodNode = bodyNode.firstNamedChild!;
|
|
|
|
|
|
expect(methodNode.type).toBe('method_definition');
|
|
|
|
|
|
expect(methodNode.text).toBe('b() {}');
|
2025-01-13 01:48:42 -05:00
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-04-14 22:38:32 -07:00
|
|
|
|
describe('.childWithDescendant()', () => {
|
|
|
|
|
|
it('correctly retrieves immediate children', () => {
|
|
|
|
|
|
const sourceCode = 'let x = 1; console.log(x);';
|
|
|
|
|
|
tree = parser.parse(sourceCode)!;
|
|
|
|
|
|
const root = tree.rootNode
|
|
|
|
|
|
const child = root.children[0].children[0]
|
|
|
|
|
|
const a = root.childWithDescendant(child)
|
|
|
|
|
|
expect(a!.startIndex).toBe(0)
|
|
|
|
|
|
const b = a!.childWithDescendant(child)
|
|
|
|
|
|
expect(b).toEqual(child)
|
|
|
|
|
|
const c = b!.childWithDescendant(child)
|
|
|
|
|
|
expect(c).toBeNull()
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-01-13 01:48:42 -05:00
|
|
|
|
describe('.nextSibling and .previousSibling', () => {
|
|
|
|
|
|
it('returns the node\'s next and previous sibling', () => {
|
2025-01-20 03:12:52 -05:00
|
|
|
|
tree = parser.parse('x10 + 1000')!;
|
|
|
|
|
|
const sumNode = tree.rootNode.firstChild!.firstChild!;
|
|
|
|
|
|
expect(sumNode.children[1]!.id).toBe(sumNode.children[0]!.nextSibling!.id);
|
|
|
|
|
|
expect(sumNode.children[2]!.id).toBe(sumNode.children[1]!.nextSibling!.id);
|
|
|
|
|
|
expect(sumNode.children[0]!.id).toBe(sumNode.children[1]!.previousSibling!.id);
|
|
|
|
|
|
expect(sumNode.children[1]!.id).toBe(sumNode.children[2]!.previousSibling!.id);
|
2025-01-13 01:48:42 -05:00
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
describe('.nextNamedSibling and .previousNamedSibling', () => {
|
|
|
|
|
|
it('returns the node\'s next and previous named sibling', () => {
|
2025-01-20 03:12:52 -05:00
|
|
|
|
tree = parser.parse('x10 + 1000')!;
|
|
|
|
|
|
const sumNode = tree.rootNode.firstChild!.firstChild!;
|
|
|
|
|
|
expect(sumNode.namedChildren[1]!.id).toBe(sumNode.namedChildren[0]!.nextNamedSibling!.id);
|
|
|
|
|
|
expect(sumNode.namedChildren[0]!.id).toBe(sumNode.namedChildren[1]!.previousNamedSibling!.id);
|
2025-01-13 01:48:42 -05:00
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
describe('.descendantForIndex(min, max)', () => {
|
|
|
|
|
|
it('returns the smallest node that spans the given range', () => {
|
2025-01-20 03:12:52 -05:00
|
|
|
|
tree = parser.parse('x10 + 1000')!;
|
|
|
|
|
|
const sumNode = tree.rootNode.firstChild!.firstChild!;
|
|
|
|
|
|
expect(sumNode.descendantForIndex(1, 2)!.type).toBe('identifier');
|
|
|
|
|
|
expect(sumNode.descendantForIndex(4, 4)!.type).toBe('+');
|
2025-01-13 01:48:42 -05:00
|
|
|
|
|
|
|
|
|
|
expect(() => {
|
2025-01-16 01:10:54 -05:00
|
|
|
|
// @ts-expect-error Testing invalid arguments
|
2025-01-19 15:15:01 -05:00
|
|
|
|
sumNode.descendantForIndex(1, {});
|
2025-01-13 01:48:42 -05:00
|
|
|
|
}).toThrow('Arguments must be numbers');
|
|
|
|
|
|
|
|
|
|
|
|
expect(() => {
|
2025-01-16 01:10:54 -05:00
|
|
|
|
// @ts-expect-error Testing invalid arguments
|
2025-01-19 15:15:01 -05:00
|
|
|
|
sumNode.descendantForIndex(undefined);
|
2025-01-13 01:48:42 -05:00
|
|
|
|
}).toThrow('Arguments must be numbers');
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
describe('.namedDescendantForIndex', () => {
|
|
|
|
|
|
it('returns the smallest named node that spans the given range', () => {
|
2025-01-20 03:12:52 -05:00
|
|
|
|
tree = parser.parse('x10 + 1000')!;
|
2025-01-19 15:15:01 -05:00
|
|
|
|
const sumNode = tree.rootNode.firstChild!;
|
2025-01-20 03:12:52 -05:00
|
|
|
|
expect(sumNode.descendantForIndex(1, 2)!.type).toBe('identifier');
|
|
|
|
|
|
expect(sumNode.descendantForIndex(4, 4)!.type).toBe('+');
|
2025-01-13 01:48:42 -05:00
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
describe('.descendantForPosition', () => {
|
|
|
|
|
|
it('returns the smallest node that spans the given range', () => {
|
2025-01-20 03:12:52 -05:00
|
|
|
|
tree = parser.parse('x10 + 1000')!;
|
2025-01-19 15:15:01 -05:00
|
|
|
|
const sumNode = tree.rootNode.firstChild!;
|
2025-01-13 01:48:42 -05:00
|
|
|
|
|
2025-01-20 03:12:52 -05:00
|
|
|
|
expect(sumNode.descendantForPosition({ row: 0, column: 1 }, { row: 0, column: 2 })!.type).toBe('identifier');
|
|
|
|
|
|
expect(sumNode.descendantForPosition({ row: 0, column: 4 })!.type).toBe('+');
|
2025-01-13 01:48:42 -05:00
|
|
|
|
|
|
|
|
|
|
expect(() => {
|
2025-01-16 01:10:54 -05:00
|
|
|
|
// @ts-expect-error Testing invalid arguments
|
2025-01-19 15:15:01 -05:00
|
|
|
|
sumNode.descendantForPosition(1, {});
|
2025-01-13 01:48:42 -05:00
|
|
|
|
}).toThrow('Arguments must be {row, column} objects');
|
|
|
|
|
|
|
|
|
|
|
|
expect(() => {
|
2025-01-16 01:10:54 -05:00
|
|
|
|
// @ts-expect-error Testing invalid arguments
|
2025-01-19 15:15:01 -05:00
|
|
|
|
sumNode.descendantForPosition(undefined);
|
2025-01-13 01:48:42 -05:00
|
|
|
|
}).toThrow('Arguments must be {row, column} objects');
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
describe('.namedDescendantForPosition(min, max)', () => {
|
|
|
|
|
|
it('returns the smallest named node that spans the given range', () => {
|
2025-01-20 03:12:52 -05:00
|
|
|
|
tree = parser.parse('x10 + 1000')!;
|
2025-01-13 01:48:42 -05:00
|
|
|
|
const sumNode = tree.rootNode.firstChild!;
|
|
|
|
|
|
|
2025-01-20 03:12:52 -05:00
|
|
|
|
expect(sumNode.namedDescendantForPosition({ row: 0, column: 1 }, { row: 0, column: 2 })!.type).toBe('identifier')
|
|
|
|
|
|
expect(sumNode.namedDescendantForPosition({ row: 0, column: 4 })!.type).toBe('binary_expression');
|
2025-01-13 01:48:42 -05:00
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
describe('.hasError', () => {
|
|
|
|
|
|
it('returns true if the node contains an error', () => {
|
2025-01-20 03:12:52 -05:00
|
|
|
|
tree = parser.parse('1 + 2 * * 3')!;
|
2025-01-13 01:48:42 -05:00
|
|
|
|
const node = tree.rootNode;
|
|
|
|
|
|
expect(node.toString()).toBe(
|
|
|
|
|
|
'(program (expression_statement (binary_expression left: (number) right: (binary_expression left: (number) (ERROR) right: (number)))))'
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2025-01-20 03:12:52 -05:00
|
|
|
|
const sum = node.firstChild!.firstChild!;
|
|
|
|
|
|
expect(sum.hasError).toBe(true);
|
|
|
|
|
|
expect(sum.children[0]!.hasError).toBe(false);
|
|
|
|
|
|
expect(sum.children[1]!.hasError).toBe(false);
|
|
|
|
|
|
expect(sum.children[2]!.hasError).toBe(true);
|
2025-01-13 01:48:42 -05:00
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
describe('.isError', () => {
|
|
|
|
|
|
it('returns true if the node is an error', () => {
|
2025-01-20 03:12:52 -05:00
|
|
|
|
tree = parser.parse('2 * * 3')!;
|
2025-01-13 01:48:42 -05:00
|
|
|
|
const node = tree.rootNode;
|
|
|
|
|
|
expect(node.toString()).toBe(
|
|
|
|
|
|
'(program (expression_statement (binary_expression left: (number) (ERROR) right: (number))))'
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2025-01-20 03:12:52 -05:00
|
|
|
|
const multi = node.firstChild!.firstChild!;
|
|
|
|
|
|
expect(multi.hasError).toBe(true);
|
|
|
|
|
|
expect(multi.children[0]!.isError).toBe(false);
|
|
|
|
|
|
expect(multi.children[1]!.isError).toBe(false);
|
|
|
|
|
|
expect(multi.children[2]!.isError).toBe(true);
|
|
|
|
|
|
expect(multi.children[3]!.isError).toBe(false);
|
2025-01-13 01:48:42 -05:00
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
describe('.isMissing', () => {
|
|
|
|
|
|
it('returns true if the node was inserted via error recovery', () => {
|
2025-01-20 03:12:52 -05:00
|
|
|
|
tree = parser.parse('(2 ||)')!;
|
2025-01-13 01:48:42 -05:00
|
|
|
|
const node = tree.rootNode;
|
|
|
|
|
|
expect(node.toString()).toBe(
|
|
|
|
|
|
'(program (expression_statement (parenthesized_expression (binary_expression left: (number) right: (MISSING identifier)))))'
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2025-01-20 03:12:52 -05:00
|
|
|
|
const sum = node.firstChild!.firstChild!.firstNamedChild!;
|
|
|
|
|
|
expect(sum.type).toBe('binary_expression');
|
|
|
|
|
|
expect(sum.hasError).toBe(true);
|
|
|
|
|
|
expect(sum.children[0]!.isMissing).toBe(false);
|
|
|
|
|
|
expect(sum.children[1]!.isMissing).toBe(false);
|
|
|
|
|
|
expect(sum.children[2]!.isMissing).toBe(true);
|
2025-01-13 01:48:42 -05:00
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
describe('.isExtra', () => {
|
|
|
|
|
|
it('returns true if the node is an extra node like comments', () => {
|
2025-01-20 03:12:52 -05:00
|
|
|
|
tree = parser.parse('foo(/* hi */);')!;
|
2025-01-13 01:48:42 -05:00
|
|
|
|
const node = tree.rootNode;
|
2025-01-19 15:15:01 -05:00
|
|
|
|
const commentNode = node.descendantForIndex(7, 7)!;
|
2025-01-13 01:48:42 -05:00
|
|
|
|
|
|
|
|
|
|
expect(node.type).toBe('program');
|
2025-01-16 01:10:54 -05:00
|
|
|
|
expect(commentNode.type).toBe('comment');
|
2025-01-13 01:48:42 -05:00
|
|
|
|
expect(node.isExtra).toBe(false);
|
2025-01-16 01:10:54 -05:00
|
|
|
|
expect(commentNode.isExtra).toBe(true);
|
2025-01-13 01:48:42 -05:00
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
describe('.text', () => {
|
|
|
|
|
|
const text = 'α0 / b👎c👎';
|
|
|
|
|
|
|
|
|
|
|
|
Object.entries({
|
|
|
|
|
|
'.parse(String)': text,
|
|
|
|
|
|
'.parse(Function)': (offset: number) => text.slice(offset, offset + 4),
|
2025-01-16 01:10:54 -05:00
|
|
|
|
}).forEach(([method, _parse]) => {
|
|
|
|
|
|
it(`returns the text of a node generated by ${method}`, () => {
|
2025-01-13 01:48:42 -05:00
|
|
|
|
const [numeratorSrc, denominatorSrc] = text.split(/\s*\/\s+/);
|
2025-01-20 03:12:52 -05:00
|
|
|
|
tree = parser.parse(_parse)!;
|
|
|
|
|
|
const quotientNode = tree.rootNode.firstChild!.firstChild!;
|
|
|
|
|
|
const [numerator, slash, denominator] = quotientNode.children;
|
2025-01-13 01:48:42 -05:00
|
|
|
|
|
|
|
|
|
|
expect(tree.rootNode.text).toBe(text);
|
2025-01-20 03:12:52 -05:00
|
|
|
|
expect(denominator!.text).toBe(denominatorSrc);
|
|
|
|
|
|
expect(quotientNode.text).toBe(text);
|
|
|
|
|
|
expect(numerator!.text).toBe(numeratorSrc);
|
|
|
|
|
|
expect(slash!.text).toBe('/');
|
2025-01-16 01:10:54 -05:00
|
|
|
|
});
|
|
|
|
|
|
});
|
2025-01-13 01:48:42 -05:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
describe('.descendantCount', () => {
|
|
|
|
|
|
it('returns the number of descendants', () => {
|
|
|
|
|
|
parser.setLanguage(JSON);
|
2025-01-20 03:12:52 -05:00
|
|
|
|
tree = parser.parse(JSON_EXAMPLE)!;
|
2025-01-13 01:48:42 -05:00
|
|
|
|
const valueNode = tree.rootNode;
|
|
|
|
|
|
const allNodes = getAllNodes(tree);
|
|
|
|
|
|
|
|
|
|
|
|
expect(valueNode.descendantCount).toBe(allNodes.length);
|
|
|
|
|
|
|
|
|
|
|
|
const cursor = tree.walk();
|
|
|
|
|
|
for (let i = 0; i < allNodes.length; i++) {
|
|
|
|
|
|
const node = allNodes[i];
|
|
|
|
|
|
cursor.gotoDescendant(i);
|
|
|
|
|
|
expect(cursor.currentNode.id).toBe(node.id);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for (let i = allNodes.length - 1; i >= 0; i--) {
|
|
|
|
|
|
const node = allNodes[i];
|
|
|
|
|
|
cursor.gotoDescendant(i);
|
|
|
|
|
|
expect(cursor.currentNode.id).toBe(node.id);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('tests a single node tree', () => {
|
|
|
|
|
|
parser.setLanguage(EmbeddedTemplate);
|
2025-01-20 03:12:52 -05:00
|
|
|
|
tree = parser.parse('hello')!;
|
2025-01-13 01:48:42 -05:00
|
|
|
|
|
|
|
|
|
|
const nodes = getAllNodes(tree);
|
|
|
|
|
|
expect(nodes).toHaveLength(2);
|
|
|
|
|
|
expect(tree.rootNode.descendantCount).toBe(2);
|
|
|
|
|
|
|
|
|
|
|
|
const cursor = tree.walk();
|
|
|
|
|
|
|
|
|
|
|
|
cursor.gotoDescendant(0);
|
|
|
|
|
|
expect(cursor.currentDepth).toBe(0);
|
|
|
|
|
|
expect(cursor.currentNode.id).toBe(nodes[0].id);
|
|
|
|
|
|
|
|
|
|
|
|
cursor.gotoDescendant(1);
|
|
|
|
|
|
expect(cursor.currentDepth).toBe(1);
|
|
|
|
|
|
expect(cursor.currentNode.id).toBe(nodes[1].id);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
describe('.rootNodeWithOffset', () => {
|
|
|
|
|
|
it('returns the root node of the tree, offset by the given byte offset', () => {
|
2025-01-20 03:12:52 -05:00
|
|
|
|
tree = parser.parse(' if (a) b')!;
|
2025-01-13 01:48:42 -05:00
|
|
|
|
const node = tree.rootNodeWithOffset(6, { row: 2, column: 2 });
|
|
|
|
|
|
expect(node.startIndex).toBe(8);
|
|
|
|
|
|
expect(node.endIndex).toBe(16);
|
|
|
|
|
|
expect(node.startPosition).toEqual({ row: 2, column: 4 });
|
|
|
|
|
|
expect(node.endPosition).toEqual({ row: 2, column: 12 });
|
|
|
|
|
|
|
2025-01-20 03:12:52 -05:00
|
|
|
|
let child = node.firstChild!.child(2)!;
|
|
|
|
|
|
expect(child.type).toBe('expression_statement');
|
|
|
|
|
|
expect(child.startIndex).toBe(15);
|
|
|
|
|
|
expect(child.endIndex).toBe(16);
|
|
|
|
|
|
expect(child.startPosition).toEqual({ row: 2, column: 11 });
|
|
|
|
|
|
expect(child.endPosition).toEqual({ row: 2, column: 12 });
|
2025-01-13 01:48:42 -05:00
|
|
|
|
|
|
|
|
|
|
const cursor = node.walk();
|
|
|
|
|
|
cursor.gotoFirstChild();
|
|
|
|
|
|
cursor.gotoFirstChild();
|
|
|
|
|
|
cursor.gotoNextSibling();
|
|
|
|
|
|
child = cursor.currentNode;
|
|
|
|
|
|
expect(child.type).toBe('parenthesized_expression');
|
|
|
|
|
|
expect(child.startIndex).toBe(11);
|
|
|
|
|
|
expect(child.endIndex).toBe(14);
|
|
|
|
|
|
expect(child.startPosition).toEqual({ row: 2, column: 7 });
|
|
|
|
|
|
expect(child.endPosition).toEqual({ row: 2, column: 10 });
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
describe('.parseState, .nextParseState', () => {
|
|
|
|
|
|
const text = '10 / 5';
|
|
|
|
|
|
|
|
|
|
|
|
it('returns node parse state ids', () => {
|
2025-01-20 03:12:52 -05:00
|
|
|
|
tree = parser.parse(text)!;
|
|
|
|
|
|
const quotientNode = tree.rootNode.firstChild!.firstChild!;
|
|
|
|
|
|
const [numerator, slash, denominator] = quotientNode.children;
|
2025-01-13 01:48:42 -05:00
|
|
|
|
|
|
|
|
|
|
expect(tree.rootNode.parseState).toBe(0);
|
|
|
|
|
|
// parse states will change on any change to the grammar so test that it
|
|
|
|
|
|
// returns something instead
|
2025-01-20 03:12:52 -05:00
|
|
|
|
expect(numerator!.parseState).toBeGreaterThan(0);
|
|
|
|
|
|
expect(slash!.parseState).toBeGreaterThan(0);
|
|
|
|
|
|
expect(denominator!.parseState).toBeGreaterThan(0);
|
2025-01-13 01:48:42 -05:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('returns next parse state equal to the language', () => {
|
2025-01-20 03:12:52 -05:00
|
|
|
|
tree = parser.parse(text)!;
|
|
|
|
|
|
const quotientNode = tree.rootNode.firstChild!.firstChild!;
|
|
|
|
|
|
quotientNode.children.forEach((node) => {
|
|
|
|
|
|
expect(node!.nextParseState).toBe(JavaScript.nextState(node!.parseState, node!.grammarId));
|
2025-01-13 01:48:42 -05:00
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-02-19 17:47:10 -05:00
|
|
|
|
describe('.descendantsOfType("ERROR")', () => {
|
|
|
|
|
|
it('finds all of the descendants of an ERROR node', () => {
|
|
|
|
|
|
tree = parser.parse(
|
|
|
|
|
|
`if ({a: 'b'} {c: 'd'}) {
|
|
|
|
|
|
// ^ ERROR
|
|
|
|
|
|
x = function(a) { b; } function(c) { d; }
|
|
|
|
|
|
}`
|
|
|
|
|
|
)!;
|
|
|
|
|
|
const errorNode = tree.rootNode;
|
|
|
|
|
|
const descendants = errorNode.descendantsOfType('ERROR');
|
|
|
|
|
|
expect(
|
|
|
|
|
|
descendants.map((node) => node!.startIndex)
|
|
|
|
|
|
).toEqual(
|
|
|
|
|
|
[4]
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-01-13 01:48:42 -05:00
|
|
|
|
describe('.descendantsOfType', () => {
|
|
|
|
|
|
it('finds all descendants of a given type in the given range', () => {
|
2025-01-20 03:12:52 -05:00
|
|
|
|
tree = parser.parse('a + 1 * b * 2 + c + 3')!;
|
|
|
|
|
|
const outerSum = tree.rootNode.firstChild!.firstChild!;
|
2025-01-13 01:48:42 -05:00
|
|
|
|
|
2025-01-20 03:12:52 -05:00
|
|
|
|
const descendants = outerSum.descendantsOfType('number', { row: 0, column: 2 }, { row: 0, column: 15 });
|
|
|
|
|
|
expect(descendants.map(node => node!.startIndex)).toEqual([4, 12]);
|
|
|
|
|
|
expect(descendants.map(node => node!.endPosition)).toEqual([{ row: 0, column: 5 }, { row: 0, column: 13 }]);
|
2025-01-13 01:48:42 -05:00
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
describe('.firstChildForIndex(index)', () => {
|
|
|
|
|
|
it('returns the first child that contains or starts after the given index', () => {
|
2025-01-20 03:12:52 -05:00
|
|
|
|
tree = parser.parse('x10 + 1000')!;
|
|
|
|
|
|
const sumNode = tree.rootNode.firstChild!.firstChild!;
|
2025-01-13 01:48:42 -05:00
|
|
|
|
|
2025-01-20 03:12:52 -05:00
|
|
|
|
expect(sumNode.firstChildForIndex(0)!.type).toBe('identifier');
|
|
|
|
|
|
expect(sumNode.firstChildForIndex(1)!.type).toBe('identifier');
|
|
|
|
|
|
expect(sumNode.firstChildForIndex(3)!.type).toBe('+');
|
|
|
|
|
|
expect(sumNode.firstChildForIndex(5)!.type).toBe('number');
|
2025-01-13 01:48:42 -05:00
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
describe('.firstNamedChildForIndex(index)', () => {
|
|
|
|
|
|
it('returns the first child that contains or starts after the given index', () => {
|
2025-01-20 03:12:52 -05:00
|
|
|
|
tree = parser.parse('x10 + 1000')!;
|
|
|
|
|
|
const sumNode = tree.rootNode.firstChild!.firstChild!;
|
2025-01-13 01:48:42 -05:00
|
|
|
|
|
2025-01-20 03:12:52 -05:00
|
|
|
|
expect(sumNode.firstNamedChildForIndex(0)!.type).toBe('identifier');
|
|
|
|
|
|
expect(sumNode.firstNamedChildForIndex(1)!.type).toBe('identifier');
|
|
|
|
|
|
expect(sumNode.firstNamedChildForIndex(3)!.type).toBe('number');
|
2025-01-13 01:48:42 -05:00
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
describe('.equals(other)', () => {
|
|
|
|
|
|
it('returns true if the nodes are the same', () => {
|
2025-01-20 03:12:52 -05:00
|
|
|
|
tree = parser.parse('1 + 2')!;
|
2025-01-13 01:48:42 -05:00
|
|
|
|
|
2025-01-20 03:12:52 -05:00
|
|
|
|
const sumNode = tree.rootNode.firstChild!.firstChild!;
|
|
|
|
|
|
const node1 = sumNode.firstChild!;
|
|
|
|
|
|
const node2 = sumNode.firstChild!;
|
|
|
|
|
|
expect(node1.equals(node2)).toBe(true);
|
2025-01-13 01:48:42 -05:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('returns false if the nodes are not the same', () => {
|
2025-01-20 03:12:52 -05:00
|
|
|
|
tree = parser.parse('1 + 2')!;
|
2025-01-13 01:48:42 -05:00
|
|
|
|
|
2025-01-20 03:12:52 -05:00
|
|
|
|
const sumNode = tree.rootNode.firstChild!.firstChild!;
|
|
|
|
|
|
const node1 = sumNode.firstChild!;
|
|
|
|
|
|
const node2 = node1.nextSibling!;
|
|
|
|
|
|
expect(node1.equals(node2)).toBe(false);
|
2025-01-13 01:48:42 -05:00
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
describe('.fieldNameForChild(index)', () => {
|
|
|
|
|
|
it('returns the field of a child or null', () => {
|
|
|
|
|
|
parser.setLanguage(C);
|
2025-01-20 03:12:52 -05:00
|
|
|
|
tree = parser.parse('int w = x + /* y is special! */ y;')!;
|
2025-01-13 01:48:42 -05:00
|
|
|
|
|
|
|
|
|
|
const translationUnitNode = tree.rootNode;
|
|
|
|
|
|
const declarationNode = translationUnitNode.firstChild;
|
|
|
|
|
|
const binaryExpressionNode = declarationNode!
|
|
|
|
|
|
.childForFieldName('declarator')!
|
2025-01-20 03:12:52 -05:00
|
|
|
|
.childForFieldName('value')!;
|
2025-01-13 01:48:42 -05:00
|
|
|
|
|
|
|
|
|
|
// -------------------
|
|
|
|
|
|
// left: (identifier) 0
|
|
|
|
|
|
// operator: "+" 1 <--- (not a named child)
|
|
|
|
|
|
// (comment) 2 <--- (is an extra)
|
|
|
|
|
|
// right: (identifier) 3
|
|
|
|
|
|
// -------------------
|
|
|
|
|
|
|
2025-01-20 03:12:52 -05:00
|
|
|
|
expect(binaryExpressionNode.fieldNameForChild(0)).toBe('left');
|
|
|
|
|
|
expect(binaryExpressionNode.fieldNameForChild(1)).toBe('operator');
|
2025-01-13 01:48:42 -05:00
|
|
|
|
// The comment should not have a field name, as it's just an extra
|
2025-01-20 03:12:52 -05:00
|
|
|
|
expect(binaryExpressionNode.fieldNameForChild(2)).toBeNull();
|
|
|
|
|
|
expect(binaryExpressionNode.fieldNameForChild(3)).toBe('right');
|
2025-01-13 01:48:42 -05:00
|
|
|
|
// Negative test - Not a valid child index
|
2025-01-20 03:12:52 -05:00
|
|
|
|
expect(binaryExpressionNode.fieldNameForChild(4)).toBeNull();
|
2025-01-13 01:48:42 -05:00
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
describe('.fieldNameForNamedChild(index)', () => {
|
|
|
|
|
|
it('returns the field of a named child or null', () => {
|
|
|
|
|
|
parser.setLanguage(C);
|
2025-01-20 03:12:52 -05:00
|
|
|
|
tree = parser.parse('int w = x + /* y is special! */ y;')!;
|
2025-01-13 01:48:42 -05:00
|
|
|
|
|
|
|
|
|
|
const translationUnitNode = tree.rootNode;
|
|
|
|
|
|
const declarationNode = translationUnitNode.firstNamedChild;
|
|
|
|
|
|
const binaryExpressionNode = declarationNode!
|
|
|
|
|
|
.childForFieldName('declarator')!
|
2025-01-20 03:12:52 -05:00
|
|
|
|
.childForFieldName('value')!;
|
2025-01-13 01:48:42 -05:00
|
|
|
|
|
|
|
|
|
|
// -------------------
|
|
|
|
|
|
// left: (identifier) 0
|
|
|
|
|
|
// operator: "+" _ <--- (not a named child)
|
|
|
|
|
|
// (comment) 1 <--- (is an extra)
|
|
|
|
|
|
// right: (identifier) 2
|
|
|
|
|
|
// -------------------
|
|
|
|
|
|
|
2025-01-20 03:12:52 -05:00
|
|
|
|
expect(binaryExpressionNode.fieldNameForNamedChild(0)).toBe('left');
|
2025-01-13 01:48:42 -05:00
|
|
|
|
// The comment should not have a field name, as it's just an extra
|
2025-01-20 03:12:52 -05:00
|
|
|
|
expect(binaryExpressionNode.fieldNameForNamedChild(1)).toBeNull();
|
2025-01-13 01:48:42 -05:00
|
|
|
|
// The operator is not a named child, so the named child at index 2 is the right child
|
2025-01-20 03:12:52 -05:00
|
|
|
|
expect(binaryExpressionNode.fieldNameForNamedChild(2)).toBe('right');
|
2025-01-13 01:48:42 -05:00
|
|
|
|
// Negative test - Not a valid child index
|
2025-01-20 03:12:52 -05:00
|
|
|
|
expect(binaryExpressionNode.fieldNameForNamedChild(3)).toBeNull();
|
2025-01-13 01:48:42 -05:00
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|