feat(web)!: rewrite the library in TypeScript

This commit is contained in:
Amaan Qureshi 2025-01-13 01:48:42 -05:00
parent 07a86b1729
commit 2cae67892e
39 changed files with 7856 additions and 3629 deletions

View file

@ -1,17 +0,0 @@
const Parser = require('..');
function languageURL(name) {
return require.resolve(`../../../target/release/tree-sitter-${name}.wasm`);
}
module.exports = Parser.init().then(async () => ({
Parser,
languageURL,
C: await Parser.Language.load(languageURL('c')),
EmbeddedTemplate: await Parser.Language.load(languageURL('embedded-template')),
HTML: await Parser.Language.load(languageURL('html')),
JavaScript: await Parser.Language.load(languageURL('javascript')),
JSON: await Parser.Language.load(languageURL('json')),
Python: await Parser.Language.load(languageURL('python')),
Rust: await Parser.Language.load(languageURL('rust')),
}));

View file

@ -0,0 +1,23 @@
import TSParser from "web-tree-sitter";
// @ts-ignore
const Parser: typeof TSParser = await import('..').then(m => m.default);
// https://github.com/tree-sitter/tree-sitter/blob/master/xtask/src/fetch.rs#L15
type LanguageName = "bash" | "c" | "cpp" | "embedded-template" | "go" | "html" | "java" | "javascript" | "jsdoc" | "json" | "php" | "python" | "ruby" | "rust" | "typescript";
function languageURL(name: LanguageName): string {
return new URL(`../../../target/release/tree-sitter-${name}.wasm`, import.meta.url).pathname;
}
export default Parser.init().then(async () => ({
Parser,
languageURL,
C: await Parser.Language.load(languageURL('c')),
EmbeddedTemplate: await Parser.Language.load(languageURL('embedded-template')),
HTML: await Parser.Language.load(languageURL('html')),
JavaScript: await Parser.Language.load(languageURL('javascript')),
JSON: await Parser.Language.load(languageURL('json')),
Python: await Parser.Language.load(languageURL('python')),
Rust: await Parser.Language.load(languageURL('rust')),
}));

View file

@ -1,13 +1,17 @@
const {assert} = require('chai');
let JavaScript;
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import helper from './helper';
import TSParser, { type LookaheadIterable, type Language } from 'web-tree-sitter';
let JavaScript: Language;
let Rust: Language;
describe('Language', () => {
before(async () => ({JavaScript, Rust} = await require('./helper')));
beforeAll(async () => ({ JavaScript, Rust } = await helper));
describe('.name, .version', () => {
it('returns the name and version of the language', () => {
assert.equal('javascript', JavaScript.name);
assert.equal(15, JavaScript.version);
expect(JavaScript.name).toBe('javascript');
expect(JavaScript.version).toBe(15);
});
});
@ -16,16 +20,16 @@ describe('Language', () => {
const nameId = JavaScript.fieldIdForName('name');
const bodyId = JavaScript.fieldIdForName('body');
assert.isBelow(nameId, JavaScript.fieldCount);
assert.isBelow(bodyId, JavaScript.fieldCount);
assert.equal('name', JavaScript.fieldNameForId(nameId));
assert.equal('body', JavaScript.fieldNameForId(bodyId));
expect(nameId).toBeLessThan(JavaScript.fieldCount);
expect(bodyId).toBeLessThan(JavaScript.fieldCount);
expect(JavaScript.fieldNameForId(nameId!)).toBe('name');
expect(JavaScript.fieldNameForId(bodyId!)).toBe('body');
});
it('handles invalid inputs', () => {
assert.equal(null, JavaScript.fieldIdForName('namezzz'));
assert.equal(null, JavaScript.fieldNameForId(-1));
assert.equal(null, JavaScript.fieldNameForId(10000));
expect(JavaScript.fieldIdForName('namezzz')).toBeNull();
expect(JavaScript.fieldNameForId(-3)).toBeNull();
expect(JavaScript.fieldNameForId(10000)).toBeNull();
});
});
@ -34,18 +38,18 @@ describe('Language', () => {
const exportStatementId = JavaScript.idForNodeType('export_statement', true);
const starId = JavaScript.idForNodeType('*', false);
assert.isBelow(exportStatementId, JavaScript.nodeTypeCount);
assert.isBelow(starId, JavaScript.nodeTypeCount);
assert.equal(true, JavaScript.nodeTypeIsNamed(exportStatementId));
assert.equal('export_statement', JavaScript.nodeTypeForId(exportStatementId));
assert.equal(false, JavaScript.nodeTypeIsNamed(starId));
assert.equal('*', JavaScript.nodeTypeForId(starId));
expect(exportStatementId).toBeLessThan(JavaScript.nodeTypeCount);
expect(starId).toBeLessThan(JavaScript.nodeTypeCount);
expect(JavaScript.nodeTypeIsNamed(exportStatementId!)).toBe(true);
expect(JavaScript.nodeTypeForId(exportStatementId!)).toBe('export_statement');
expect(JavaScript.nodeTypeIsNamed(starId!)).toBe(false);
expect(JavaScript.nodeTypeForId(starId!)).toBe('*');
});
it('handles invalid inputs', () => {
assert.equal(null, JavaScript.nodeTypeForId(-1));
assert.equal(null, JavaScript.nodeTypeForId(10000));
assert.equal(null, JavaScript.idForNodeType('export_statement', false));
expect(JavaScript.nodeTypeForId(-3)).toBeNull();
expect(JavaScript.nodeTypeForId(10000)).toBeNull();
expect(JavaScript.idForNodeType('export_statement', false)).toBeNull();
});
});
@ -53,19 +57,23 @@ describe('Language', () => {
it('gets the supertypes and subtypes of a parser', () => {
const supertypes = Rust.supertypes;
const names = supertypes.map((id) => Rust.nodeTypeForId(id));
assert.deepStrictEqual(
names,
['_expression', '_literal', '_literal_pattern', '_pattern', '_type'],
);
expect(names).toEqual([
'_expression',
'_literal',
'_literal_pattern',
'_pattern',
'_type'
]);
for (const id of supertypes) {
const name = Rust.nodeTypeForId(id);
const subtypes = Rust.subtypes(id);
let subtypeNames = subtypes.map((id) => Rust.nodeTypeForId(id));
subtypeNames = [...new Set(subtypeNames)].sort(); // Remove duplicates & sort
switch (name) {
case '_literal':
assert.deepStrictEqual(subtypeNames, [
expect(subtypeNames).toEqual([
'boolean_literal',
'char_literal',
'float_literal',
@ -75,7 +83,7 @@ describe('Language', () => {
]);
break;
case '_pattern':
assert.deepStrictEqual(subtypeNames, [
expect(subtypeNames).toEqual([
'_',
'_literal_pattern',
'captured_pattern',
@ -96,7 +104,7 @@ describe('Language', () => {
]);
break;
case '_type':
assert.deepStrictEqual(subtypeNames, [
expect(subtypeNames).toEqual([
'abstract_type',
'array_type',
'bounded_type',
@ -116,8 +124,6 @@ describe('Language', () => {
'unit_type',
]);
break;
default:
break;
}
}
});
@ -125,44 +131,45 @@ describe('Language', () => {
});
describe('Lookahead iterator', () => {
let lookahead;
let state;
before(async () => {
let Parser;
({JavaScript, Parser} = await require('./helper'));
const parser = new Parser().setLanguage(JavaScript);
let lookahead: LookaheadIterable;
let state: number;
beforeAll(async () => {
let Parser: typeof TSParser;
({ JavaScript, Parser } = await helper);
const parser = new Parser();
parser.setLanguage(JavaScript);
const tree = parser.parse('function fn() {}');
parser.delete();
const cursor = tree.walk();
assert(cursor.gotoFirstChild());
assert(cursor.gotoFirstChild());
expect(cursor.gotoFirstChild()).toBe(true);
expect(cursor.gotoFirstChild()).toBe(true);
state = cursor.currentNode.nextParseState;
lookahead = JavaScript.lookaheadIterator(state);
assert.exists(lookahead);
lookahead = JavaScript.lookaheadIterator(state)!;
expect(lookahead).toBeDefined();
});
after(() => {
lookahead.delete();
});
afterAll(() => lookahead.delete());
const expected = ['(', 'identifier', '*', 'formal_parameters', 'html_comment', 'comment'];
it('should iterate over valid symbols in the state', () => {
const symbols = Array.from(lookahead);
assert.includeMembers(symbols, expected);
assert.lengthOf(symbols, expected.length);
expect(symbols).toEqual(expect.arrayContaining(expected));
expect(symbols).toHaveLength(expected.length);
});
it('should reset to the initial state', () => {
assert(lookahead.resetState(state));
expect(lookahead.resetState(state)).toBe(true);
const symbols = Array.from(lookahead);
assert.includeMembers(symbols, expected);
assert.lengthOf(symbols, expected.length);
expect(symbols).toEqual(expect.arrayContaining(expected));
expect(symbols).toHaveLength(expected.length);
});
it('should reset', () => {
assert(lookahead.reset(JavaScript, state));
expect(lookahead.reset(JavaScript, state)).toBe(true);
const symbols = Array.from(lookahead);
assert.includeMembers(symbols, expected);
assert.lengthOf(symbols, expected.length);
expect(symbols).toEqual(expect.arrayContaining(expected));
expect(symbols).toHaveLength(expected.length);
});
});

View file

@ -1,693 +0,0 @@
const {assert} = require('chai');
let Parser; let C; 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, C, 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("ERROR", null, null)', () => {
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;
let descendants = errorNode.descendantsOfType('ERROR', null, null);
assert.deepEqual(
descendants.map((node) => node.startIndex),
[4],
);
});
});
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 contains or starts after 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 contains or starts after 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', () => {
parser.setLanguage(C);
tree = parser.parse('int w = x + /* y is special! */ y;');
const translationUnitNode = tree.rootNode;
const declarationNode = translationUnitNode.firstChild;
const binaryExpressionNode = declarationNode
.childForFieldName('declarator')
.childForFieldName('value');
// -------------------
// left: (identifier) 0
// operator: "+" _ <--- (not a named child)
// (comment) 1 <--- (is an extra)
// right: (identifier) 2
// -------------------
assert.equal(binaryExpressionNode.fieldNameForChild(0), 'left');
assert.equal(binaryExpressionNode.fieldNameForChild(1), 'operator');
// The comment should not have a field name, as it's just an extra
assert.equal(binaryExpressionNode.fieldNameForChild(2), null);
assert.equal(binaryExpressionNode.fieldNameForChild(3), 'right');
// Negative test - Not a valid child index
assert.equal(binaryExpressionNode.fieldNameForChild(4), null);
});
});
describe('.fieldNameForNamedChild(index)', () => {
it('returns the field of a named child or null', () => {
parser.setLanguage(C);
tree = parser.parse('int w = x + /* y is special! */ y;');
const translationUnitNode = tree.rootNode;
const declarationNode = translationUnitNode.firstNamedChild;
const binaryExpressionNode = declarationNode
.childForFieldName('declarator')
.childForFieldName('value');
// -------------------
// left: (identifier) 0
// operator: "+" _ <--- (not a named child)
// (comment) 1 <--- (is an extra)
// right: (identifier) 2
// -------------------
assert.equal(binaryExpressionNode.fieldNameForNamedChild(0), 'left');
// The comment should not have a field name, as it's just an extra
assert.equal(binaryExpressionNode.fieldNameForNamedChild(1), null);
// The operator is not a named child, so the named child at index 2 is the right child
assert.equal(binaryExpressionNode.fieldNameForNamedChild(2), 'right');
// Negative test - Not a valid child index
assert.equal(binaryExpressionNode.fieldNameForNamedChild(3), null);
});
});
});

View file

@ -0,0 +1,587 @@
import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest';
import TSParser, { type Language, type Tree, type SyntaxNode, type Point } from 'web-tree-sitter';
import helper from './helper';
let Parser: typeof TSParser;
let C: Language;
let JavaScript: Language;
let JSON: Language;
let EmbeddedTemplate: Language;
let Python: Language;
const JSON_EXAMPLE = `
[
123,
false,
{
"x": null
}
]
`;
function getAllNodes(tree: Tree): SyntaxNode[] {
const result: SyntaxNode[] = [];
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: TSParser;
let tree: Tree | null;
beforeAll(async () => {
({ Parser, C, EmbeddedTemplate, JavaScript, JSON, Python } = await helper);
});
beforeEach(() => {
tree = null;
parser = new Parser();
parser.setLanguage(JavaScript);
});
afterEach(() => {
parser.delete();
tree!.delete();
});
describe('.children', () => {
it('returns an array of child nodes', () => {
tree = parser.parse('x10 + 1000');
expect(tree.rootNode.children).toHaveLength(1);
const sumNode = tree.rootNode.firstChild!.firstChild;
expect(sumNode!.children.map(child => child.type)).toEqual([
'identifier',
'+',
'number'
]);
});
});
describe('.namedChildren', () => {
it('returns an array of named child nodes', () => {
tree = parser.parse('x10 + 1000');
const sumNode = tree.rootNode.firstChild!.firstChild;
expect(tree.rootNode.namedChildren).toHaveLength(1);
expect(sumNode!.namedChildren.map(child => child.type)).toEqual([
'identifier',
'number'
]);
});
});
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;
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);
});
expect(alternativeTexts).toEqual(['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;
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]);
});
});
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!;
expect(sumNode.type).toBe('binary_expression');
expect(sumNode.startPosition).toEqual({ row: 0, column: 0 });
expect(sumNode.endPosition).toEqual({ row: 0, column: 10 });
expect(sumNode.children.map((child) => child.startPosition)).toEqual([
{ row: 0, column: 0 },
{ row: 0, column: 4 },
{ row: 0, column: 6 },
]);
expect(sumNode.children.map((child) => child.endPosition)).toEqual([
{ row: 0, column: 3 },
{ row: 0, column: 5 },
{ row: 0, column: 10 },
]);
});
it('handles characters that occupy two UTF16 code units', () => {
tree = parser.parse('a👍👎1 /\n b👎c👎');
const sumNode = tree.rootNode.firstChild!.firstChild;
expect(sumNode!.children.map(child => [child.startPosition, child.endPosition])).toEqual([
[{ 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', () => {
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);
});
});
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;
expect(variableNode!.firstChild).toBeNull();
expect(variableNode!.lastChild).toBeNull();
expect(variableNode!.firstNamedChild).toBeNull();
expect(variableNode!.lastNamedChild).toBeNull();
expect(variableNode!.child(1)).toBeNull();
});
});
describe('.childForFieldName()', () => {
it('returns node for the given field name', () => {
tree = parser.parse('class A { b() {} }');
const classNode = tree.rootNode.firstChild;
expect(classNode!.type).toBe('class_declaration');
const classNameNode = classNode!.childForFieldName('name');
expect(classNameNode!.type).toBe('identifier');
expect(classNameNode!.text).toBe('A');
const bodyNode = classNode!.childForFieldName('body');
expect(bodyNode!.type).toBe('class_body');
expect(bodyNode!.text).toBe('{ b() {} }');
const methodNode = bodyNode!.firstNamedChild;
expect(methodNode!.type).toBe('method_definition');
expect(methodNode!.text).toBe('b() {}');
});
});
describe('.nextSibling and .previousSibling', () => {
it('returns the node\'s next and previous sibling', () => {
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);
});
});
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;
expect(sumNode!.namedChildren[1].id).toBe(sumNode!.namedChildren[0].nextNamedSibling!.id);
expect(sumNode!.namedChildren[0].id).toBe(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;
expect(sumNode!.descendantForIndex(1, 2)!.type).toBe('identifier');
expect(sumNode!.descendantForIndex(4, 4)!.type).toBe('+');
expect(() => {
sumNode!.descendantForIndex(1, {} as any);
}).toThrow('Arguments must be numbers');
expect(() => {
sumNode!.descendantForIndex(undefined as any);
}).toThrow('Arguments must be numbers');
});
});
describe('.namedDescendantForIndex', () => {
it('returns the smallest named node that spans the given range', () => {
tree = parser.parse('x10 + 1000');
const sumNode = tree.rootNode.firstChild;
expect(sumNode!.descendantForIndex(1, 2)!.type).toBe('identifier');
expect(sumNode!.descendantForIndex(4, 4)!.type).toBe('+');
});
});
describe('.descendantForPosition', () => {
it('returns the smallest node that spans the given range', () => {
tree = parser.parse('x10 + 1000');
const sumNode = tree.rootNode.firstChild;
expect(
sumNode!.descendantForPosition(
{ row: 0, column: 1 },
{ row: 0, column: 2 }
)!.type
).toBe('identifier');
expect(
sumNode!.descendantForPosition({ row: 0, column: 4 })!.type
).toBe('+');
expect(() => {
sumNode!.descendantForPosition(1 as any, {} as any);
}).toThrow('Arguments must be {row, column} objects');
expect(() => {
sumNode!.descendantForPosition(undefined as any);
}).toThrow('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!;
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');
});
});
describe('.hasError', () => {
it('returns true if the node contains an error', () => {
tree = parser.parse('1 + 2 * * 3');
const node = tree.rootNode;
expect(node.toString()).toBe(
'(program (expression_statement (binary_expression left: (number) right: (binary_expression left: (number) (ERROR) right: (number)))))'
);
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);
});
});
describe('.isError', () => {
it('returns true if the node is an error', () => {
tree = parser.parse('2 * * 3');
const node = tree.rootNode;
expect(node.toString()).toBe(
'(program (expression_statement (binary_expression left: (number) (ERROR) right: (number))))'
);
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);
});
});
describe('.isMissing', () => {
it('returns true if the node was inserted via error recovery', () => {
tree = parser.parse('(2 ||)');
const node = tree.rootNode;
expect(node.toString()).toBe(
'(program (expression_statement (parenthesized_expression (binary_expression left: (number) right: (MISSING identifier)))))'
);
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);
});
});
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);
expect(node.type).toBe('program');
expect(commentNode!.type).toBe('comment');
expect(node.isExtra).toBe(false);
expect(commentNode!.isExtra).toBe(true);
});
});
describe('.text', () => {
const text = 'α0 / b👎c👎';
Object.entries({
'.parse(String)': text,
'.parse(Function)': (offset: number) => text.slice(offset, offset + 4),
}).forEach(([method, _parse]) =>
it(`returns the text of a node generated by ${method}`, async () => {
const [numeratorSrc, denominatorSrc] = text.split(/\s*\/\s+/);
tree = parser.parse(_parse);
const quotientNode = tree.rootNode!.firstChild!.firstChild!;
const [numerator, slash, denominator] = quotientNode.children;
expect(tree.rootNode.text).toBe(text);
expect(denominator.text).toBe(denominatorSrc);
expect(quotientNode!.text).toBe(text);
expect(numerator.text).toBe(numeratorSrc);
expect(slash.text).toBe('/');
}),
);
});
describe('.descendantCount', () => {
it('returns the number of descendants', () => {
parser.setLanguage(JSON);
tree = parser.parse(JSON_EXAMPLE);
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);
tree = parser.parse('hello');
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', () => {
tree = parser.parse(' if (a) b');
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 });
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 });
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', () => {
tree = parser.parse(text);
const quotientNode = tree.rootNode.firstChild!.firstChild;
const [numerator, slash, denominator] = quotientNode!.children;
expect(tree.rootNode.parseState).toBe(0);
// parse states will change on any change to the grammar so test that it
// returns something instead
expect(numerator.parseState).toBeGreaterThan(0);
expect(slash.parseState).toBeGreaterThan(0);
expect(denominator.parseState).toBeGreaterThan(0);
});
it('returns next parse state equal to the language', () => {
tree = parser.parse(text);
const quotientNode = tree.rootNode.firstChild!.firstChild;
quotientNode!.children.forEach((node) => {
expect(node.nextParseState).toBe(
JavaScript.nextState(node.parseState, node.grammarId)
);
});
});
});
describe('.descendantsOfType', () => {
it('finds all descendants of a 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 });
expect(descendants.map(node => node.startIndex)).toEqual([4, 12]);
expect(descendants.map(node => node.endPosition)).toEqual([
{ row: 0, column: 5 },
{ row: 0, column: 13 },
]);
});
});
describe('.firstChildForIndex(index)', () => {
it('returns the first child that contains or starts after the given index', () => {
tree = parser.parse('x10 + 1000');
const sumNode = tree.rootNode.firstChild!.firstChild;
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');
});
});
describe('.firstNamedChildForIndex(index)', () => {
it('returns the first child that contains or starts after the given index', () => {
tree = parser.parse('x10 + 1000');
const sumNode = tree.rootNode.firstChild!.firstChild;
expect(sumNode!.firstNamedChildForIndex(0)!.type).toBe('identifier');
expect(sumNode!.firstNamedChildForIndex(1)!.type).toBe('identifier');
expect(sumNode!.firstNamedChildForIndex(3)!.type).toBe('number');
});
});
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;
expect(node1!.equals(node2!)).toBe(true);
});
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;
expect(node1!.equals(node2!)).toBe(false);
});
});
describe('.fieldNameForChild(index)', () => {
it('returns the field of a child or null', () => {
parser.setLanguage(C);
tree = parser.parse('int w = x + /* y is special! */ y;');
const translationUnitNode = tree.rootNode;
const declarationNode = translationUnitNode.firstChild;
const binaryExpressionNode = declarationNode!
.childForFieldName('declarator')!
.childForFieldName('value');
// -------------------
// left: (identifier) 0
// operator: "+" 1 <--- (not a named child)
// (comment) 2 <--- (is an extra)
// right: (identifier) 3
// -------------------
expect(binaryExpressionNode!.fieldNameForChild(0)).toBe('left');
expect(binaryExpressionNode!.fieldNameForChild(1)).toBe('operator');
// The comment should not have a field name, as it's just an extra
expect(binaryExpressionNode!.fieldNameForChild(2)).toBeNull();
expect(binaryExpressionNode!.fieldNameForChild(3)).toBe('right');
// Negative test - Not a valid child index
expect(binaryExpressionNode!.fieldNameForChild(4)).toBeNull();
});
});
describe('.fieldNameForNamedChild(index)', () => {
it('returns the field of a named child or null', () => {
parser.setLanguage(C);
tree = parser.parse('int w = x + /* y is special! */ y;');
const translationUnitNode = tree.rootNode;
const declarationNode = translationUnitNode.firstNamedChild;
const binaryExpressionNode = declarationNode!
.childForFieldName('declarator')!
.childForFieldName('value');
// -------------------
// left: (identifier) 0
// operator: "+" _ <--- (not a named child)
// (comment) 1 <--- (is an extra)
// right: (identifier) 2
// -------------------
expect(binaryExpressionNode!.fieldNameForNamedChild(0)).toBe('left');
// The comment should not have a field name, as it's just an extra
expect(binaryExpressionNode!.fieldNameForNamedChild(1)).toBeNull();
// The operator is not a named child, so the named child at index 2 is the right child
expect(binaryExpressionNode!.fieldNameForNamedChild(2)).toBe('right');
// Negative test - Not a valid child index
expect(binaryExpressionNode!.fieldNameForNamedChild(3)).toBeNull();
});
});
});

View file

@ -1,413 +0,0 @@
const {assert} = require('chai');
let Parser; let JavaScript; let HTML; let languageURL; let JSON;
describe('Parser', () => {
let parser;
before(async () =>
({Parser, JavaScript, HTML, JSON, languageURL} = await require('./helper')),
);
beforeEach(() => {
parser = new Parser();
});
afterEach(() => {
parser.delete();
});
describe('.setLanguage', () => {
it('allows setting the language to null', () => {
assert.equal(parser.getLanguage(), null);
parser.setLanguage(JavaScript);
assert.equal(parser.getLanguage(), JavaScript);
parser.setLanguage(null);
assert.equal(parser.getLanguage(), null);
});
it('throws an exception when the given object is not a tree-sitter language', () => {
assert.throws(() => parser.setLanguage({}), /Argument must be a Language/);
assert.throws(() => parser.setLanguage(1), /Argument must be a Language/);
});
});
describe('.setLogger', () => {
beforeEach(() => {
parser.setLanguage(JavaScript);
});
it('calls the given callback for each parse event', () => {
const debugMessages = [];
parser.setLogger((message) => debugMessages.push(message));
parser.parse('a + b + c');
assert.includeMembers(debugMessages, [
'skip character:\' \'',
'consume character:\'b\'',
'reduce sym:program, child_count:1',
'accept',
]);
});
it('allows the callback to be retrieved later', () => {
const callback = () => {};
parser.setLogger(callback);
assert.equal(parser.getLogger(), callback);
parser.setLogger(false);
assert.equal(parser.getLogger(), null);
});
it('disables debugging when given a falsy value', () => {
const debugMessages = [];
parser.setLogger((message) => debugMessages.push(message));
parser.setLogger(false);
parser.parse('a + b * c');
assert.equal(debugMessages.length, 0);
});
it('throws an error when given a truthy value that isn\'t a function ', () => {
assert.throws(
() => parser.setLogger('5'),
'Logger callback must be a function',
);
});
it('rethrows errors thrown by the logging callback', () => {
const error = new Error('The error message');
parser.setLogger((_msg, _params) => {
throw error;
});
assert.throws(
() => parser.parse('ok;'),
'The error message',
);
});
});
describe('one included range', () => {
it('parses the text within a range', () => {
parser.setLanguage(HTML);
const sourceCode = '<span>hi</span><script>console.log(\'sup\');</script>';
const htmlTree = parser.parse(sourceCode);
const scriptContentNode = htmlTree.rootNode.child(1).child(1);
assert.equal(scriptContentNode.type, 'raw_text');
parser.setLanguage(JavaScript);
assert.deepEqual(parser.getIncludedRanges(), [{
startIndex: 0,
endIndex: 2147483647,
startPosition: {row: 0, column: 0},
endPosition: {row: 4294967295, column: 2147483647},
}]);
const ranges = [{
startIndex: scriptContentNode.startIndex,
endIndex: scriptContentNode.endIndex,
startPosition: scriptContentNode.startPosition,
endPosition: scriptContentNode.endPosition,
}];
const jsTree = parser.parse(
sourceCode,
null,
{includedRanges: ranges},
);
assert.deepEqual(parser.getIncludedRanges(), ranges);
assert.equal(
jsTree.rootNode.toString(),
'(program (expression_statement (call_expression ' +
'function: (member_expression object: (identifier) property: (property_identifier)) ' +
'arguments: (arguments (string (string_fragment))))))',
);
assert.deepEqual(jsTree.rootNode.startPosition, {row: 0, column: sourceCode.indexOf('console')});
});
});
describe('multiple included ranges', () => {
it('parses the text within multiple ranges', () => {
parser.setLanguage(JavaScript);
const sourceCode = 'html `<div>Hello, ${name.toUpperCase()}, it\'s <b>${now()}</b>.</div>`';
const jsTree = parser.parse(sourceCode);
const templateStringNode = jsTree.rootNode.descendantForIndex(sourceCode.indexOf('`<'), sourceCode.indexOf('>`'));
assert.equal(templateStringNode.type, 'template_string');
const openQuoteNode = templateStringNode.child(0);
const interpolationNode1 = templateStringNode.child(2);
const interpolationNode2 = templateStringNode.child(4);
const closeQuoteNode = templateStringNode.child(6);
parser.setLanguage(HTML);
const htmlRanges = [
{
startIndex: openQuoteNode.endIndex,
startPosition: openQuoteNode.endPosition,
endIndex: interpolationNode1.startIndex,
endPosition: interpolationNode1.startPosition,
},
{
startIndex: interpolationNode1.endIndex,
startPosition: interpolationNode1.endPosition,
endIndex: interpolationNode2.startIndex,
endPosition: interpolationNode2.startPosition,
},
{
startIndex: interpolationNode2.endIndex,
startPosition: interpolationNode2.endPosition,
endIndex: closeQuoteNode.startIndex,
endPosition: closeQuoteNode.startPosition,
},
];
const htmlTree = parser.parse(sourceCode, null, {includedRanges: htmlRanges});
assert.equal(
htmlTree.rootNode.toString(),
'(document (element' +
' (start_tag (tag_name))' +
' (text)' +
' (element (start_tag (tag_name)) (end_tag (tag_name)))' +
' (text)' +
' (end_tag (tag_name))))',
);
assert.deepEqual(htmlTree.getIncludedRanges(), htmlRanges);
const divElementNode = htmlTree.rootNode.child(0);
const helloTextNode = divElementNode.child(1);
const bElementNode = divElementNode.child(2);
const bStartTagNode = bElementNode.child(0);
const bEndTagNode = bElementNode.child(1);
assert.equal(helloTextNode.type, 'text');
assert.equal(helloTextNode.startIndex, sourceCode.indexOf('Hello'));
assert.equal(helloTextNode.endIndex, sourceCode.indexOf(' <b>'));
assert.equal(bStartTagNode.type, 'start_tag');
assert.equal(bStartTagNode.startIndex, sourceCode.indexOf('<b>'));
assert.equal(bStartTagNode.endIndex, sourceCode.indexOf('${now()}'));
assert.equal(bEndTagNode.type, 'end_tag');
assert.equal(bEndTagNode.startIndex, sourceCode.indexOf('</b>'));
assert.equal(bEndTagNode.endIndex, sourceCode.indexOf('.</div>'));
});
});
describe('an included range containing mismatched positions', () => {
it('parses the text within the range', () => {
const sourceCode = '<div>test</div>{_ignore_this_part_}';
parser.setLanguage(HTML);
const endIndex = sourceCode.indexOf('{_ignore_this_part_');
const rangeToParse = {
startIndex: 0,
startPosition: {row: 10, column: 12},
endIndex,
endPosition: {row: 10, column: 12 + endIndex},
};
const htmlTree = parser.parse(sourceCode, null, {includedRanges: [rangeToParse]});
assert.deepEqual(htmlTree.getIncludedRanges()[0], rangeToParse);
assert.equal(
htmlTree.rootNode.toString(),
'(document (element (start_tag (tag_name)) (text) (end_tag (tag_name))))',
);
});
});
describe('.parse', () => {
let tree;
beforeEach(() => {
tree = null;
parser.setLanguage(JavaScript);
});
afterEach(() => {
if (tree) tree.delete();
});
it('reads from the given input', () => {
const parts = ['first', '_', 'second', '_', 'third'];
tree = parser.parse(() => parts.shift());
assert.equal(tree.rootNode.toString(), '(program (expression_statement (identifier)))');
});
it('stops reading when the input callback return something that\'s not a string', () => {
const parts = ['abc', 'def', 'ghi', {}, {}, {}, 'second-word', ' '];
tree = parser.parse(() => parts.shift());
assert.equal(
tree.rootNode.toString(),
'(program (expression_statement (identifier)))',
);
assert.equal(tree.rootNode.endIndex, 9);
assert.equal(parts.length, 2);
});
it('throws an exception when the given input is not a function', () => {
assert.throws(() => parser.parse(null), 'Argument must be a string or a function');
assert.throws(() => parser.parse(5), 'Argument must be a string or a function');
assert.throws(() => parser.parse({}), 'Argument must be a string or a function');
});
it('handles long input strings', () => {
const repeatCount = 10000;
const inputString = `[${Array(repeatCount).fill('0').join(',')}]`;
tree = parser.parse(inputString);
assert.equal(tree.rootNode.type, 'program');
assert.equal(tree.rootNode.firstChild.firstChild.namedChildCount, repeatCount);
}).timeout(5000);
it('can use the bash parser', async () => {
parser.setLanguage(await Parser.Language.load(languageURL('bash')));
tree = parser.parse('FOO=bar echo <<EOF 2> err.txt > hello.txt \nhello${FOO}\nEOF');
assert.equal(
tree.rootNode.toString(),
'(program ' +
'(redirected_statement ' +
'body: (command ' +
'(variable_assignment name: (variable_name) value: (word)) ' +
'name: (command_name (word))) ' +
'redirect: (heredoc_redirect (heredoc_start) ' +
'redirect: (file_redirect descriptor: (file_descriptor) destination: (word)) ' +
'redirect: (file_redirect destination: (word)) ' +
'(heredoc_body ' +
'(expansion (variable_name)) (heredoc_content)) (heredoc_end))))',
);
}).timeout(5000);
it('can use the c++ parser', async () => {
parser.setLanguage(await Parser.Language.load(languageURL('cpp')));
tree = parser.parse('const char *s = R"EOF(HELLO WORLD)EOF";');
assert.equal(
tree.rootNode.toString(),
'(translation_unit (declaration ' +
'(type_qualifier) ' +
'type: (primitive_type) ' +
'declarator: (init_declarator ' +
'declarator: (pointer_declarator declarator: (identifier)) ' +
'value: (raw_string_literal delimiter: (raw_string_delimiter) (raw_string_content) (raw_string_delimiter)))))',
);
}).timeout(5000);
it('can use the HTML parser', async () => {
parser.setLanguage(await Parser.Language.load(languageURL('html')));
tree = parser.parse('<div><span><custom></custom></span></div>');
assert.equal(
tree.rootNode.toString(),
'(document (element (start_tag (tag_name)) (element (start_tag (tag_name)) (element (start_tag (tag_name)) (end_tag (tag_name))) (end_tag (tag_name))) (end_tag (tag_name))))',
);
}).timeout(5000);
it('can use the python parser', async () => {
parser.setLanguage(await Parser.Language.load(languageURL('python')));
tree = parser.parse('class A:\n def b():\n c()');
assert.equal(
tree.rootNode.toString(),
'(module (class_definition ' +
'name: (identifier) ' +
'body: (block ' +
'(function_definition ' +
'name: (identifier) ' +
'parameters: (parameters) ' +
'body: (block (expression_statement (call ' +
'function: (identifier) ' +
'arguments: (argument_list))))))))',
);
}).timeout(5000);
it('can use the rust parser', async () => {
parser.setLanguage(await Parser.Language.load(languageURL('rust')));
tree = parser.parse('const x: &\'static str = r###"hello"###;');
assert.equal(
tree.rootNode.toString(),
'(source_file (const_item ' +
'name: (identifier) ' +
'type: (reference_type (lifetime (identifier)) type: (primitive_type)) ' +
'value: (raw_string_literal (string_content))))',
);
}).timeout(5000);
it('can use the typescript parser', async () => {
parser.setLanguage(await Parser.Language.load(languageURL('typescript')));
tree = parser.parse('a()\nb()\n[c]');
assert.equal(
tree.rootNode.toString(),
'(program ' +
'(expression_statement (call_expression function: (identifier) arguments: (arguments))) ' +
'(expression_statement (subscript_expression ' +
'object: (call_expression ' +
'function: (identifier) ' +
'arguments: (arguments)) ' +
'index: (identifier))))',
);
}).timeout(5000);
it('can use the tsx parser', async () => {
parser.setLanguage(await Parser.Language.load(languageURL('tsx')));
tree = parser.parse('a()\nb()\n[c]');
assert.equal(
tree.rootNode.toString(),
'(program ' +
'(expression_statement (call_expression function: (identifier) arguments: (arguments))) ' +
'(expression_statement (subscript_expression ' +
'object: (call_expression ' +
'function: (identifier) ' +
'arguments: (arguments)) ' +
'index: (identifier))))',
);
}).timeout(5000);
it('parses only the text within the `includedRanges` if they are specified', () => {
const sourceCode = '<% foo() %> <% bar %>';
const start1 = sourceCode.indexOf('foo');
const end1 = start1 + 5;
const start2 = sourceCode.indexOf('bar');
const end2 = start2 + 3;
const tree = parser.parse(sourceCode, null, {
includedRanges: [
{
startIndex: start1,
endIndex: end1,
startPosition: {row: 0, column: start1},
endPosition: {row: 0, column: end1},
},
{
startIndex: start2,
endIndex: end2,
startPosition: {row: 0, column: start2},
endPosition: {row: 0, column: end2},
},
],
});
assert.equal(
tree.rootNode.toString(),
'(program (expression_statement (call_expression function: (identifier) arguments: (arguments))) (expression_statement (identifier)))',
);
});
it('parses with a timeout', () => {
parser.setLanguage(JSON);
const startTime = performance.now();
assert.throws(() => {
parser.parse(
(offset, _) => offset === 0 ? '[' : ',0',
null,
{
progressCallback: (_) => {
if (performance.now() - startTime > 1) {
return true;
}
return false;
},
},
);
},
);
}).timeout(5000);
});
});

View file

@ -0,0 +1,412 @@
import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest';
import helper from './helper';
import TSParser, { type Language } from 'web-tree-sitter';
let Parser: typeof TSParser;
let JavaScript: Language;
let HTML: Language;
let JSON: Language;
let languageURL: (name: string) => string;
describe('Parser', () => {
let parser: TSParser;
beforeAll(async () => {
({ Parser, JavaScript, HTML, JSON, languageURL } = await helper);
});
beforeEach(() => {
parser = new Parser();
});
afterEach(() => {
parser.delete();
});
describe('.setLanguage', () => {
it('allows setting the language to null', () => {
expect(parser.getLanguage()).toBeUndefined();
parser.setLanguage(JavaScript);
expect(parser.getLanguage()).toBe(JavaScript);
parser.setLanguage(null);
expect(parser.getLanguage()).toBeNull();
});
it('throws an exception when the given object is not a tree-sitter language', () => {
expect(() => parser.setLanguage({} as any)).toThrow(/Argument must be a Language/);
expect(() => parser.setLanguage(1 as any)).toThrow(/Argument must be a Language/);
});
});
describe('.setLogger', () => {
beforeEach(() => {
parser.setLanguage(JavaScript);
});
it('calls the given callback for each parse event', () => {
const debugMessages: string[] = [];
parser.setLogger((message) => debugMessages.push(message));
parser.parse('a + b + c');
expect(debugMessages).toEqual(expect.arrayContaining([
'skip character:\' \'',
'consume character:\'b\'',
'reduce sym:program, child_count:1',
'accept'
]));
});
it('allows the callback to be retrieved later', () => {
const callback = () => { };
parser.setLogger(callback);
expect(parser.getLogger()).toBe(callback);
parser.setLogger(false);
expect(parser.getLogger()).toBeNull();
});
it('disables debugging when given a falsy value', () => {
const debugMessages: string[] = [];
parser.setLogger((message) => debugMessages.push(message));
parser.setLogger(false);
parser.parse('a + b * c');
expect(debugMessages).toHaveLength(0);
});
it('throws an error when given a truthy value that isn\'t a function', () => {
expect(() => parser.setLogger('5' as any)).toThrow('Logger callback must be a function');
});
it('rethrows errors thrown by the logging callback', () => {
const error = new Error('The error message');
parser.setLogger((_msg) => {
throw error;
});
expect(() => parser.parse('ok;')).toThrow('The error message');
});
});
describe('one included range', () => {
it('parses the text within a range', () => {
parser.setLanguage(HTML);
const sourceCode = '<span>hi</span><script>console.log(\'sup\');</script>';
const htmlTree = parser.parse(sourceCode);
const scriptContentNode = htmlTree.rootNode.child(1)!.child(1)!;
expect(scriptContentNode.type).toBe('raw_text');
parser.setLanguage(JavaScript);
expect(parser.getIncludedRanges()).toEqual([{
startIndex: 0,
endIndex: 2147483647,
startPosition: { row: 0, column: 0 },
endPosition: { row: 4294967295, column: 2147483647 }
}]);
const ranges = [{
startIndex: scriptContentNode.startIndex,
endIndex: scriptContentNode.endIndex,
startPosition: scriptContentNode.startPosition,
endPosition: scriptContentNode.endPosition,
}];
const jsTree = parser.parse(
sourceCode,
null,
{ includedRanges: ranges }
);
expect(parser.getIncludedRanges()).toEqual(ranges);
expect(jsTree.rootNode.toString()).toBe(
'(program (expression_statement (call_expression ' +
'function: (member_expression object: (identifier) property: (property_identifier)) ' +
'arguments: (arguments (string (string_fragment))))))'
);
expect(jsTree.rootNode.startPosition).toEqual({ row: 0, column: sourceCode.indexOf('console') });
});
});
describe('multiple included ranges', () => {
it('parses the text within multiple ranges', () => {
parser.setLanguage(JavaScript);
const sourceCode = 'html `<div>Hello, ${name.toUpperCase()}, it\'s <b>${now()}</b>.</div>`';
const jsTree = parser.parse(sourceCode);
const templateStringNode = jsTree.rootNode.descendantForIndex(
sourceCode.indexOf('`<'),
sourceCode.indexOf('>`')
);
expect(templateStringNode.type).toBe('template_string');
const openQuoteNode = templateStringNode.child(0)!;
const interpolationNode1 = templateStringNode.child(2)!;
const interpolationNode2 = templateStringNode.child(4)!;
const closeQuoteNode = templateStringNode.child(6)!;
parser.setLanguage(HTML);
const htmlRanges = [
{
startIndex: openQuoteNode.endIndex,
startPosition: openQuoteNode.endPosition,
endIndex: interpolationNode1.startIndex,
endPosition: interpolationNode1.startPosition,
},
{
startIndex: interpolationNode1.endIndex,
startPosition: interpolationNode1.endPosition,
endIndex: interpolationNode2.startIndex,
endPosition: interpolationNode2.startPosition,
},
{
startIndex: interpolationNode2.endIndex,
startPosition: interpolationNode2.endPosition,
endIndex: closeQuoteNode.startIndex,
endPosition: closeQuoteNode.startPosition,
},
];
const htmlTree = parser.parse(sourceCode, null, { includedRanges: htmlRanges });
expect(htmlTree.rootNode.toString()).toBe(
'(document (element' +
' (start_tag (tag_name))' +
' (text)' +
' (element (start_tag (tag_name)) (end_tag (tag_name)))' +
' (text)' +
' (end_tag (tag_name))))'
);
expect(htmlTree.getIncludedRanges()).toEqual(htmlRanges);
const divElementNode = htmlTree.rootNode.child(0)!;
const helloTextNode = divElementNode.child(1)!;
const bElementNode = divElementNode.child(2)!;
const bStartTagNode = bElementNode.child(0)!;
const bEndTagNode = bElementNode.child(1)!;
expect(helloTextNode.type).toBe('text');
expect(helloTextNode.startIndex).toBe(sourceCode.indexOf('Hello'));
expect(helloTextNode.endIndex).toBe(sourceCode.indexOf(' <b>'));
expect(bStartTagNode.type).toBe('start_tag');
expect(bStartTagNode.startIndex).toBe(sourceCode.indexOf('<b>'));
expect(bStartTagNode.endIndex).toBe(sourceCode.indexOf('${now()}'));
expect(bEndTagNode.type).toBe('end_tag');
expect(bEndTagNode.startIndex).toBe(sourceCode.indexOf('</b>'));
expect(bEndTagNode.endIndex).toBe(sourceCode.indexOf('.</div>'));
});
});
describe('an included range containing mismatched positions', () => {
it('parses the text within the range', () => {
const sourceCode = '<div>test</div>{_ignore_this_part_}';
parser.setLanguage(HTML);
const endIndex = sourceCode.indexOf('{_ignore_this_part_');
const rangeToParse = {
startIndex: 0,
startPosition: { row: 10, column: 12 },
endIndex,
endPosition: { row: 10, column: 12 + endIndex },
};
const htmlTree = parser.parse(sourceCode, null, { includedRanges: [rangeToParse] });
expect(htmlTree.getIncludedRanges()[0]).toEqual(rangeToParse);
expect(htmlTree.rootNode.toString()).toBe(
'(document (element (start_tag (tag_name)) (text) (end_tag (tag_name))))'
);
});
});
describe('.parse', () => {
let tree: TSParser.Tree | null;
beforeEach(() => {
tree = null;
parser.setLanguage(JavaScript);
});
afterEach(() => {
if (tree) tree.delete();
});
it('reads from the given input', () => {
const parts = ['first', '_', 'second', '_', 'third'];
tree = parser.parse(() => parts.shift());
expect(tree.rootNode.toString()).toBe('(program (expression_statement (identifier)))');
});
it('stops reading when the input callback returns something that\'s not a string', () => {
const parts = ['abc', 'def', 'ghi', {}, {}, {}, 'second-word', ' '];
tree = parser.parse(() => parts.shift() as string);
expect(tree.rootNode.toString()).toBe('(program (expression_statement (identifier)))');
expect(tree.rootNode.endIndex).toBe(9);
expect(parts).toHaveLength(2);
});
it('throws an exception when the given input is not a function', () => {
expect(() => parser.parse(null as any)).toThrow('Argument must be a string or a function');
expect(() => parser.parse(5 as any)).toThrow('Argument must be a string or a function');
expect(() => parser.parse({} as any)).toThrow('Argument must be a string or a function');
});
it('handles long input strings', { timeout: 5000 }, () => {
const repeatCount = 10000;
const inputString = `[${Array(repeatCount).fill('0').join(',')}]`;
tree = parser.parse(inputString);
expect(tree.rootNode.type).toBe('program');
expect(tree.rootNode.firstChild!.firstChild!.namedChildCount).toBe(repeatCount);
});
it('can use the bash parser', async () => {
parser.setLanguage(await Parser.Language.load(languageURL('bash')));
tree = parser.parse('FOO=bar echo <<EOF 2> err.txt > hello.txt \nhello${FOO}\nEOF');
expect(tree.rootNode.toString()).toBe(
'(program ' +
'(redirected_statement ' +
'body: (command ' +
'(variable_assignment name: (variable_name) value: (word)) ' +
'name: (command_name (word))) ' +
'redirect: (heredoc_redirect (heredoc_start) ' +
'redirect: (file_redirect descriptor: (file_descriptor) destination: (word)) ' +
'redirect: (file_redirect destination: (word)) ' +
'(heredoc_body ' +
'(expansion (variable_name)) (heredoc_content)) (heredoc_end))))'
);
}, { timeout: 5000 });
it('can use the c++ parser', { timeout: 5000 }, async () => {
parser.setLanguage(await Parser.Language.load(languageURL('cpp')));
tree = parser.parse('const char *s = R"EOF(HELLO WORLD)EOF";');
expect(tree.rootNode.toString()).toBe(
'(translation_unit (declaration ' +
'(type_qualifier) ' +
'type: (primitive_type) ' +
'declarator: (init_declarator ' +
'declarator: (pointer_declarator declarator: (identifier)) ' +
'value: (raw_string_literal delimiter: (raw_string_delimiter) (raw_string_content) (raw_string_delimiter)))))'
);
});
it('can use the HTML parser', { timeout: 5000 }, async () => {
parser.setLanguage(await Parser.Language.load(languageURL('html')));
tree = parser.parse('<div><span><custom></custom></span></div>');
expect(tree.rootNode.toString()).toBe(
'(document (element (start_tag (tag_name)) (element (start_tag (tag_name)) ' +
'(element (start_tag (tag_name)) (end_tag (tag_name))) (end_tag (tag_name))) (end_tag (tag_name))))'
);
});
it('can use the python parser', { timeout: 5000 }, async () => {
parser.setLanguage(await Parser.Language.load(languageURL('python')));
tree = parser.parse('class A:\n def b():\n c()');
expect(tree.rootNode.toString()).toBe(
'(module (class_definition ' +
'name: (identifier) ' +
'body: (block ' +
'(function_definition ' +
'name: (identifier) ' +
'parameters: (parameters) ' +
'body: (block (expression_statement (call ' +
'function: (identifier) ' +
'arguments: (argument_list))))))))'
);
});
it('can use the rust parser', { timeout: 5000 }, async () => {
parser.setLanguage(await Parser.Language.load(languageURL('rust')));
tree = parser.parse('const x: &\'static str = r###"hello"###;');
expect(tree.rootNode.toString()).toBe(
'(source_file (const_item ' +
'name: (identifier) ' +
'type: (reference_type (lifetime (identifier)) type: (primitive_type)) ' +
'value: (raw_string_literal (string_content))))'
);
});
it('can use the typescript parser', { timeout: 5000 }, async () => {
parser.setLanguage(await Parser.Language.load(languageURL('typescript')));
tree = parser.parse('a()\nb()\n[c]');
expect(tree.rootNode.toString()).toBe(
'(program ' +
'(expression_statement (call_expression function: (identifier) arguments: (arguments))) ' +
'(expression_statement (subscript_expression ' +
'object: (call_expression ' +
'function: (identifier) ' +
'arguments: (arguments)) ' +
'index: (identifier))))'
);
});
it('can use the tsx parser', { timeout: 5000 }, async () => {
parser.setLanguage(await Parser.Language.load(languageURL('tsx')));
tree = parser.parse('a()\nb()\n[c]');
expect(tree.rootNode.toString()).toBe(
'(program ' +
'(expression_statement (call_expression function: (identifier) arguments: (arguments))) ' +
'(expression_statement (subscript_expression ' +
'object: (call_expression ' +
'function: (identifier) ' +
'arguments: (arguments)) ' +
'index: (identifier))))',
);
});
it('parses only the text within the `includedRanges` if they are specified', () => {
const sourceCode = '<% foo() %> <% bar %>';
const start1 = sourceCode.indexOf('foo');
const end1 = start1 + 5;
const start2 = sourceCode.indexOf('bar');
const end2 = start2 + 3;
const tree = parser.parse(sourceCode, null, {
includedRanges: [
{
startIndex: start1,
endIndex: end1,
startPosition: { row: 0, column: start1 },
endPosition: { row: 0, column: end1 },
},
{
startIndex: start2,
endIndex: end2,
startPosition: { row: 0, column: start2 },
endPosition: { row: 0, column: end2 },
},
],
});
expect(tree.rootNode.toString()).toBe(
'(program ' +
'(expression_statement (call_expression function: (identifier) arguments: (arguments))) ' +
'(expression_statement (identifier)))'
);
});
it('parses with a timeout', { timeout: 5000 }, () => {
parser.setLanguage(JSON);
const startTime = performance.now();
let currentByteOffset = 0;
const progressCallback = (state: TSParser.State) => {
expect(state.currentOffset).toBeGreaterThanOrEqual(currentByteOffset);
currentByteOffset = state.currentOffset;
if (performance.now() - startTime > 1) {
return true;
}
return false;
};
expect(() => parser.parse(
(offset, _) => offset === 0 ? '[' : ',0',
null,
{ progressCallback },
)
).toThrowError();
});
});
});

View file

@ -1,13 +1,22 @@
const {assert} = require('chai');
let Parser; let JavaScript;
import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest';
import TSParser, { type Language, type Tree, type Query, type QueryCapture, type QueryMatch } from 'web-tree-sitter';
import helper from './helper';
let Parser: typeof TSParser;
let JavaScript: Language;
describe('Query', () => {
let parser; let tree; let query;
let parser: TSParser;
let tree: Tree | null;
let query: Query | null;
before(async () => ({Parser, JavaScript} = await require('./helper')));
beforeAll(async () => {
({ Parser, JavaScript } = await helper);
});
beforeEach(() => {
parser = new Parser().setLanguage(JavaScript);
parser = new Parser();
parser.setLanguage(JavaScript);
});
afterEach(() => {
@ -18,36 +27,39 @@ describe('Query', () => {
describe('construction', () => {
it('throws an error on invalid patterns', () => {
assert.throws(() => {
expect(() => {
JavaScript.query('(function_declaration wat)');
}, 'Bad syntax at offset 22: \'wat)\'...');
assert.throws(() => {
}).toThrow('Bad syntax at offset 22: \'wat)\'...');
expect(() => {
JavaScript.query('(non_existent)');
}, 'Bad node name \'non_existent\'');
assert.throws(() => {
}).toThrow('Bad node name \'non_existent\'');
expect(() => {
JavaScript.query('(a)');
}, 'Bad node name \'a\'');
assert.throws(() => {
}).toThrow('Bad node name \'a\'');
expect(() => {
JavaScript.query('(function_declaration non_existent:(identifier))');
}, 'Bad field name \'non_existent\'');
assert.throws(() => {
}).toThrow('Bad field name \'non_existent\'');
expect(() => {
JavaScript.query('(function_declaration name:(statement_block))');
}, 'Bad pattern structure at offset 22: \'name:(statement_block))\'');
}).toThrow('Bad pattern structure at offset 22: \'name:(statement_block))\'');
});
it('throws an error on invalid predicates', () => {
assert.throws(() => {
expect(() => {
JavaScript.query('((identifier) @abc (#eq? @ab hi))');
}, 'Bad capture name @ab');
assert.throws(() => {
JavaScript.query('((identifier) @abc (#eq? @ab hi))');
}, 'Bad capture name @ab');
assert.throws(() => {
}).toThrow('Bad capture name @ab');
expect(() => {
JavaScript.query('((identifier) @abc (#eq?))');
}, 'Wrong number of arguments to `#eq?` predicate. Expected 2, got 0');
assert.throws(() => {
}).toThrow('Wrong number of arguments to `#eq?` predicate. Expected 2, got 0');
expect(() => {
JavaScript.query('((identifier) @a (#eq? @a @a @a))');
}, 'Wrong number of arguments to `#eq?` predicate. Expected 2, got 3');
}).toThrow('Wrong number of arguments to `#eq?` predicate. Expected 2, got 3');
});
});
@ -59,28 +71,28 @@ describe('Query', () => {
(call_expression function: (identifier) @fn-ref)
`);
const matches = query.matches(tree.rootNode);
assert.deepEqual(formatMatches(matches), [
{pattern: 0, captures: [{name: 'fn-def', text: 'one'}]},
{pattern: 1, captures: [{name: 'fn-ref', text: 'two'}]},
{pattern: 0, captures: [{name: 'fn-def', text: 'three'}]},
expect(formatMatches(matches)).toEqual([
{ pattern: 0, captures: [{ name: 'fn-def', text: 'one' }] },
{ pattern: 1, captures: [{ name: 'fn-ref', text: 'two' }] },
{ pattern: 0, captures: [{ name: 'fn-def', text: 'three' }] },
]);
});
it('can search in a specified ranges', () => {
it('can search in specified ranges', () => {
tree = parser.parse('[a, b,\nc, d,\ne, f,\ng, h]');
query = JavaScript.query('(identifier) @element');
const matches = query.matches(
tree.rootNode,
{
startPosition: {row: 1, column: 1},
endPosition: {row: 3, column: 1},
},
startPosition: { row: 1, column: 1 },
endPosition: { row: 3, column: 1 },
}
);
assert.deepEqual(formatMatches(matches), [
{pattern: 0, captures: [{name: 'element', text: 'd'}]},
{pattern: 0, captures: [{name: 'element', text: 'e'}]},
{pattern: 0, captures: [{name: 'element', text: 'f'}]},
{pattern: 0, captures: [{name: 'element', text: 'g'}]},
expect(formatMatches(matches)).toEqual([
{ pattern: 0, captures: [{ name: 'element', text: 'd' }] },
{ pattern: 0, captures: [{ name: 'element', text: 'e' }] },
{ pattern: 0, captures: [{ name: 'element', text: 'f' }] },
{ pattern: 0, captures: [{ name: 'element', text: 'g' }] },
]);
});
@ -104,9 +116,9 @@ describe('Query', () => {
`);
const matches = query.matches(tree.rootNode);
assert.deepEqual(formatMatches(matches), [
{pattern: 0, captures: [{name: 'name', text: 'giraffe'}]},
{pattern: 0, captures: [{name: 'name', text: 'gross'}]},
expect(formatMatches(matches)).toEqual([
{ pattern: 0, captures: [{ name: 'name', text: 'giraffe' }] },
{ pattern: 0, captures: [{ name: 'name', text: 'gross' }] },
]);
});
@ -122,8 +134,8 @@ describe('Query', () => {
`);
const matches = query.matches(tree.rootNode);
assert.deepEqual(formatMatches(matches), [
{pattern: 0, captures: [{name: 'variable.builtin', text: 'window'}]},
expect(formatMatches(matches)).toEqual([
{ pattern: 0, captures: [{ name: 'variable.builtin', text: 'window' }] },
]);
});
});
@ -156,19 +168,19 @@ describe('Query', () => {
`);
const captures = query.captures(tree.rootNode);
assert.deepEqual(formatCaptures(captures), [
{name: 'method.def', text: 'bc'},
{name: 'delimiter', text: ':'},
{name: 'method.alias', text: 'de'},
{name: 'function.def', text: 'fg'},
{name: 'operator', text: '='},
{name: 'function.alias', text: 'hi'},
{name: 'method.def', text: 'jk'},
{name: 'delimiter', text: ':'},
{name: 'method.alias', text: 'lm'},
{name: 'function.def', text: 'no'},
{name: 'operator', text: '='},
{name: 'function.alias', text: 'pq'},
expect(formatCaptures(captures)).toEqual([
{ name: 'method.def', text: 'bc' },
{ name: 'delimiter', text: ':' },
{ name: 'method.alias', text: 'de' },
{ name: 'function.def', text: 'fg' },
{ name: 'operator', text: '=' },
{ name: 'function.alias', text: 'hi' },
{ name: 'method.def', text: 'jk' },
{ name: 'delimiter', text: ':' },
{ name: 'method.alias', text: 'lm' },
{ name: 'function.def', text: 'no' },
{ name: 'operator', text: '=' },
{ name: 'function.alias', text: 'pq' },
]);
});
@ -197,21 +209,21 @@ describe('Query', () => {
`);
const captures = query.captures(tree.rootNode);
assert.deepEqual(formatCaptures(captures), [
{name: 'variable', text: 'panda'},
{name: 'variable', text: 'toad'},
{name: 'variable', text: 'ab'},
{name: 'variable', text: 'require'},
{name: 'function.builtin', text: 'require'},
{name: 'variable', text: 'Cd'},
{name: 'constructor', text: 'Cd'},
{name: 'variable', text: 'EF'},
{name: 'constructor', text: 'EF'},
{name: 'constant', text: 'EF'},
expect(formatCaptures(captures)).toEqual([
{ name: 'variable', text: 'panda' },
{ name: 'variable', text: 'toad' },
{ name: 'variable', text: 'ab' },
{ name: 'variable', text: 'require' },
{ name: 'function.builtin', text: 'require' },
{ name: 'variable', text: 'Cd' },
{ name: 'constructor', text: 'Cd' },
{ name: 'variable', text: 'EF' },
{ name: 'constructor', text: 'EF' },
{ name: 'constant', text: 'EF' },
]);
});
it('handles conditions that compare the text of capture to each other', () => {
it('handles conditions that compare the text of captures to each other', () => {
tree = parser.parse(`
ab = abc + 1;
def = de + 1;
@ -229,9 +241,9 @@ describe('Query', () => {
`);
const captures = query.captures(tree.rootNode);
assert.deepEqual(formatCaptures(captures), [
{name: 'id1', text: 'ghi'},
{name: 'id2', text: 'ghi'},
expect(formatCaptures(captures)).toEqual([
{ name: 'id1', text: 'ghi' },
{ name: 'id2', text: 'ghi' },
]);
});
@ -248,16 +260,20 @@ describe('Query', () => {
`);
const captures = query.captures(tree.rootNode);
assert.deepEqual(formatCaptures(captures), [
{name: 'func', text: 'a', setProperties: {foo: null, bar: 'baz'}},
expect(formatCaptures(captures)).toEqual([
{
name: 'func',
text: 'a',
setProperties: { foo: null, bar: 'baz' }
},
{
name: 'prop',
text: 'c',
assertedProperties: {foo: null},
refutedProperties: {bar: 'baz'},
assertedProperties: { foo: null },
refutedProperties: { bar: 'baz' },
},
]);
assert.ok(!query.didExceedMatchLimit());
expect(query.didExceedMatchLimit()).toBe(false);
});
it('detects queries with too many permutations to track', () => {
@ -275,90 +291,81 @@ describe('Query', () => {
(array (identifier) @pre (identifier) @post)
`);
query.captures(tree.rootNode, {matchLimit: 32});
assert.ok(query.didExceedMatchLimit());
query.captures(tree.rootNode, { matchLimit: 32 });
expect(query.didExceedMatchLimit()).toBe(true);
});
it('handles quantified captures properly', () => {
let captures;
tree = parser.parse(`
/// foo
/// bar
/// baz
`);
query = JavaScript.query(`
(
(comment)+ @foo
(#any-eq? @foo "/// foo")
)
`);
const expectCount = (tree, queryText, expectedCount) => {
const expectCount = (tree: Tree, queryText: string, expectedCount: number) => {
query = JavaScript.query(queryText);
captures = query.captures(tree.rootNode);
assert.equal(captures.length, expectedCount);
const captures = query.captures(tree.rootNode);
expect(captures).toHaveLength(expectedCount);
};
expectCount(
tree,
`((comment)+ @foo (#any-eq? @foo "/// foo"))`,
3,
3
);
expectCount(
tree,
`((comment)+ @foo (#eq? @foo "/// foo"))`,
0,
0
);
expectCount(
tree,
`((comment)+ @foo (#any-not-eq? @foo "/// foo"))`,
3,
3
);
expectCount(
tree,
`((comment)+ @foo (#not-eq? @foo "/// foo"))`,
0,
0
);
expectCount(
tree,
`((comment)+ @foo (#match? @foo "^/// foo"))`,
0,
0
);
expectCount(
tree,
`((comment)+ @foo (#any-match? @foo "^/// foo"))`,
3,
3
);
expectCount(
tree,
`((comment)+ @foo (#not-match? @foo "^/// foo"))`,
0,
0
);
expectCount(
tree,
`((comment)+ @foo (#not-match? @foo "fsdfsdafdfs"))`,
3,
3
);
expectCount(
tree,
`((comment)+ @foo (#any-not-match? @foo "^///"))`,
0,
0
);
expectCount(
tree,
`((comment)+ @foo (#any-not-match? @foo "^/// foo"))`,
3,
3
);
});
});
@ -381,37 +388,39 @@ describe('Query', () => {
"if" @d
`);
assert.deepEqual(query.predicatesForPattern(0), [
expect(query.predicatesForPattern(0)).toStrictEqual([
{
operator: 'something?',
operands: [
{type: 'capture', name: 'a'},
{type: 'capture', name: 'b'},
{ type: 'capture', name: 'a' },
{ type: 'capture', name: 'b' },
],
},
{
operator: 'something-else?',
operands: [
{type: 'capture', name: 'a'},
{type: 'string', value: 'A'},
{type: 'capture', name: 'b'},
{type: 'string', value: 'B'},
{ type: 'capture', name: 'a' },
{ type: 'string', value: 'A' },
{ type: 'capture', name: 'b' },
{ type: 'string', value: 'B' },
],
},
]);
assert.deepEqual(query.predicatesForPattern(1), [
expect(query.predicatesForPattern(1)).toStrictEqual([
{
operator: 'hello!',
operands: [{type: 'capture', name: 'c'}],
operands: [{ type: 'capture', name: 'c' }],
},
]);
assert.deepEqual(query.predicatesForPattern(2), []);
expect(query.predicatesForPattern(2)).toEqual([]);
});
});
describe('.disableCapture', () => {
it('disables a capture', () => {
const query = JavaScript.query(`
query = JavaScript.query(`
(function_declaration
(identifier) @name1 @name2 @name3
(statement_block) @body1 @body2)
@ -421,15 +430,15 @@ describe('Query', () => {
const tree = parser.parse(source);
let matches = query.matches(tree.rootNode);
assert.deepEqual(formatMatches(matches), [
expect(formatMatches(matches)).toEqual([
{
pattern: 0,
captures: [
{name: 'name1', text: 'foo'},
{name: 'name2', text: 'foo'},
{name: 'name3', text: 'foo'},
{name: 'body1', text: '{ return 1; }'},
{name: 'body2', text: '{ return 1; }'},
{ name: 'name1', text: 'foo' },
{ name: 'name2', text: 'foo' },
{ name: 'name3', text: 'foo' },
{ name: 'body1', text: '{ return 1; }' },
{ name: 'body2', text: '{ return 1; }' },
],
},
]);
@ -438,31 +447,32 @@ describe('Query', () => {
// single node.
query.disableCapture('name2');
matches = query.matches(tree.rootNode);
assert.deepEqual(formatMatches(matches), [
expect(formatMatches(matches)).toEqual([
{
pattern: 0,
captures: [
{name: 'name1', text: 'foo'},
{name: 'name3', text: 'foo'},
{name: 'body1', text: '{ return 1; }'},
{name: 'body2', text: '{ return 1; }'},
{ name: 'name1', text: 'foo' },
{ name: 'name3', text: 'foo' },
{ name: 'body1', text: '{ return 1; }' },
{ name: 'body2', text: '{ return 1; }' },
],
},
]);
});
});
describe('Set a timeout', () =>
describe('Set a timeout', () => {
it('returns less than the expected matches', () => {
tree = parser.parse('function foo() while (true) { } }\n'.repeat(1000));
query = JavaScript.query(
'(function_declaration name: (identifier) @function)',
'(function_declaration name: (identifier) @function)'
);
const matches = query.matches(tree.rootNode, {timeoutMicros: 1000});
assert.isBelow(matches.length, 1000);
const matches2 = query.matches(tree.rootNode, {timeoutMicros: 0});
assert.equal(matches2.length, 1000);
}));
const matches = query.matches(tree.rootNode, { timeoutMicros: 1000 });
expect(matches.length).toBeLessThan(1000);
const matches2 = query.matches(tree.rootNode, { timeoutMicros: 0 });
expect(matches2).toHaveLength(1000);
});
});
describe('Start and end indices for patterns', () => {
it('Returns the start and end indices for a pattern', () => {
@ -489,22 +499,17 @@ describe('Query', () => {
const query = JavaScript.query(source);
assert.equal(query.startIndexForPattern(0), 0);
assert.equal(query.endIndexForPattern(0), '"+" @operator\n'.length);
assert.equal(query.startIndexForPattern(5), patterns1.length);
assert.equal(
query.endIndexForPattern(5),
patterns1.length + '(identifier) @a\n'.length,
expect(query.startIndexForPattern(0)).toBe(0);
expect(query.endIndexForPattern(0)).toBe('"+" @operator\n'.length);
expect(query.startIndexForPattern(5)).toBe(patterns1.length);
expect(query.endIndexForPattern(5)).toBe(
patterns1.length + '(identifier) @a\n'.length
);
assert.equal(
query.startIndexForPattern(7),
patterns1.length + patterns2.length,
);
assert.equal(
query.endIndexForPattern(7),
expect(query.startIndexForPattern(7)).toBe(patterns1.length + patterns2.length);
expect(query.endIndexForPattern(7)).toBe(
patterns1.length +
patterns2.length +
'((identifier) @b (#match? @b i))\n'.length,
patterns2.length +
'((identifier) @b (#match? @b i))\n'.length
);
});
});
@ -525,12 +530,12 @@ describe('Query', () => {
const source = 'class A { constructor() {} } function b() { return 1; }';
tree = parser.parse(source);
const matches = query.matches(tree.rootNode);
assert.deepEqual(formatMatches(matches), [
expect(formatMatches(matches)).toEqual([
{
pattern: 3,
captures: [{name: 'body', text: '{ constructor() {} }'}],
captures: [{ name: 'body', text: '{ constructor() {} }' }],
},
{pattern: 1, captures: [{name: 'body', text: '{ return 1; }'}]},
{ pattern: 1, captures: [{ name: 'body', text: '{ return 1; }' }] },
]);
});
});
@ -539,7 +544,7 @@ describe('Query', () => {
it('Returns less than the expected matches', () => {
tree = parser.parse('function foo() while (true) { } }\n'.repeat(1000));
query = JavaScript.query(
'(function_declaration) @function',
'(function_declaration) @function'
);
const startTime = performance.now();
@ -553,24 +558,25 @@ describe('Query', () => {
}
return false;
},
},
}
);
assert.isBelow(matches.length, 1000);
expect(matches.length).toBeLessThan(1000);
const matches2 = query.matches(tree.rootNode);
assert.equal(matches2.length, 1000);
expect(matches2).toHaveLength(1000);
});
});
});
function formatMatches(matches) {
return matches.map(({pattern, captures}) => ({
// Helper functions
function formatMatches(matches: any[]): QueryMatch[] {
return matches.map(({ pattern, captures }) => ({
pattern,
captures: formatCaptures(captures),
}));
}
function formatCaptures(captures) {
function formatCaptures(captures: any[]): QueryCapture[] {
return captures.map((c) => {
const node = c.node;
delete c.node;

View file

@ -1,426 +0,0 @@
const {assert} = require('chai');
let Parser; let JavaScript;
describe('Tree', () => {
let parser; let tree;
before(async () =>
({Parser, JavaScript} = await require('./helper')),
);
beforeEach(() => {
parser = new Parser().setLanguage(JavaScript);
});
afterEach(() => {
parser.delete();
tree.delete();
});
describe('.edit', () => {
let input; let edit;
it('updates the positions of nodes', () => {
input = 'abc + cde';
tree = parser.parse(input);
assert.equal(
tree.rootNode.toString(),
'(program (expression_statement (binary_expression left: (identifier) right: (identifier))))',
);
let sumNode = tree.rootNode.firstChild.firstChild;
let variableNode1 = sumNode.firstChild;
let variableNode2 = sumNode.lastChild;
assert.equal(variableNode1.startIndex, 0);
assert.equal(variableNode1.endIndex, 3);
assert.equal(variableNode2.startIndex, 6);
assert.equal(variableNode2.endIndex, 9);
([input, edit] = spliceInput(input, input.indexOf('bc'), 0, ' * '));
assert.equal(input, 'a * bc + cde');
tree.edit(edit);
sumNode = tree.rootNode.firstChild.firstChild;
variableNode1 = sumNode.firstChild;
variableNode2 = sumNode.lastChild;
assert.equal(variableNode1.startIndex, 0);
assert.equal(variableNode1.endIndex, 6);
assert.equal(variableNode2.startIndex, 9);
assert.equal(variableNode2.endIndex, 12);
tree = parser.parse(input, tree);
assert.equal(
tree.rootNode.toString(),
'(program (expression_statement (binary_expression left: (binary_expression left: (identifier) right: (identifier)) right: (identifier))))',
);
});
it('handles non-ascii characters', () => {
input = 'αβδ + cde';
tree = parser.parse(input);
assert.equal(
tree.rootNode.toString(),
'(program (expression_statement (binary_expression left: (identifier) right: (identifier))))',
);
let variableNode = tree.rootNode.firstChild.firstChild.lastChild;
([input, edit] = spliceInput(input, input.indexOf('δ'), 0, '👍 * '));
assert.equal(input, 'αβ👍 * δ + cde');
tree.edit(edit);
variableNode = tree.rootNode.firstChild.firstChild.lastChild;
assert.equal(variableNode.startIndex, input.indexOf('cde'));
tree = parser.parse(input, tree);
assert.equal(
tree.rootNode.toString(),
'(program (expression_statement (binary_expression left: (binary_expression left: (identifier) right: (identifier)) right: (identifier))))',
);
});
});
describe('.getChangedRanges(previous)', () => {
it('reports the ranges of text whose syntactic meaning has changed', () => {
let sourceCode = 'abcdefg + hij';
tree = parser.parse(sourceCode);
assert.equal(
tree.rootNode.toString(),
'(program (expression_statement (binary_expression left: (identifier) right: (identifier))))',
);
sourceCode = 'abc + defg + hij';
tree.edit({
startIndex: 2,
oldEndIndex: 2,
newEndIndex: 5,
startPosition: {row: 0, column: 2},
oldEndPosition: {row: 0, column: 2},
newEndPosition: {row: 0, column: 5},
});
const tree2 = parser.parse(sourceCode, tree);
assert.equal(
tree2.rootNode.toString(),
'(program (expression_statement (binary_expression left: (binary_expression left: (identifier) right: (identifier)) right: (identifier))))',
);
const ranges = tree.getChangedRanges(tree2);
assert.deepEqual(ranges, [
{
startIndex: 0,
endIndex: 'abc + defg'.length,
startPosition: {row: 0, column: 0},
endPosition: {row: 0, column: 'abc + defg'.length},
},
]);
tree2.delete();
});
it('throws an exception if the argument is not a tree', () => {
tree = parser.parse('abcdefg + hij');
assert.throws(() => {
tree.getChangedRanges({});
}, /Argument must be a Tree/);
});
});
describe('.walk()', () => {
let cursor;
afterEach(() => {
cursor.delete();
});
it('returns a cursor that can be used to walk the tree', () => {
tree = parser.parse('a * b + c / d');
cursor = tree.walk();
assertCursorState(cursor, {
nodeType: 'program',
nodeIsNamed: true,
startPosition: {row: 0, column: 0},
endPosition: {row: 0, column: 13},
startIndex: 0,
endIndex: 13,
});
assert(cursor.gotoFirstChild());
assertCursorState(cursor, {
nodeType: 'expression_statement',
nodeIsNamed: true,
startPosition: {row: 0, column: 0},
endPosition: {row: 0, column: 13},
startIndex: 0,
endIndex: 13,
});
assert(cursor.gotoFirstChild());
assertCursorState(cursor, {
nodeType: 'binary_expression',
nodeIsNamed: true,
startPosition: {row: 0, column: 0},
endPosition: {row: 0, column: 13},
startIndex: 0,
endIndex: 13,
});
assert(cursor.gotoFirstChild());
assertCursorState(cursor, {
nodeType: 'binary_expression',
nodeIsNamed: true,
startPosition: {row: 0, column: 0},
endPosition: {row: 0, column: 5},
startIndex: 0,
endIndex: 5,
});
assert(cursor.gotoFirstChild());
assert.equal(cursor.nodeText, 'a');
assertCursorState(cursor, {
nodeType: 'identifier',
nodeIsNamed: true,
startPosition: {row: 0, column: 0},
endPosition: {row: 0, column: 1},
startIndex: 0,
endIndex: 1,
});
assert(!cursor.gotoFirstChild());
assert(cursor.gotoNextSibling());
assert.equal(cursor.nodeText, '*');
assertCursorState(cursor, {
nodeType: '*',
nodeIsNamed: false,
startPosition: {row: 0, column: 2},
endPosition: {row: 0, column: 3},
startIndex: 2,
endIndex: 3,
});
assert(cursor.gotoNextSibling());
assert.equal(cursor.nodeText, 'b');
assertCursorState(cursor, {
nodeType: 'identifier',
nodeIsNamed: true,
startPosition: {row: 0, column: 4},
endPosition: {row: 0, column: 5},
startIndex: 4,
endIndex: 5,
});
assert(!cursor.gotoNextSibling());
assert(cursor.gotoParent());
assertCursorState(cursor, {
nodeType: 'binary_expression',
nodeIsNamed: true,
startPosition: {row: 0, column: 0},
endPosition: {row: 0, column: 5},
startIndex: 0,
endIndex: 5,
});
assert(cursor.gotoNextSibling());
assertCursorState(cursor, {
nodeType: '+',
nodeIsNamed: false,
startPosition: {row: 0, column: 6},
endPosition: {row: 0, column: 7},
startIndex: 6,
endIndex: 7,
});
assert(cursor.gotoNextSibling());
assertCursorState(cursor, {
nodeType: 'binary_expression',
nodeIsNamed: true,
startPosition: {row: 0, column: 8},
endPosition: {row: 0, column: 13},
startIndex: 8,
endIndex: 13,
});
const copy = tree.walk();
copy.resetTo(cursor);
assert(copy.gotoPreviousSibling());
assertCursorState(copy, {
nodeType: '+',
nodeIsNamed: false,
startPosition: {row: 0, column: 6},
endPosition: {row: 0, column: 7},
startIndex: 6,
endIndex: 7,
});
assert(copy.gotoPreviousSibling());
assertCursorState(copy, {
nodeType: 'binary_expression',
nodeIsNamed: true,
startPosition: {row: 0, column: 0},
endPosition: {row: 0, column: 5},
startIndex: 0,
endIndex: 5,
});
assert(copy.gotoLastChild());
assertCursorState(copy, {
nodeType: 'identifier',
nodeIsNamed: true,
startPosition: {row: 0, column: 4},
endPosition: {row: 0, column: 5},
startIndex: 4,
endIndex: 5,
});
assert(copy.gotoParent());
assert(copy.gotoParent());
assert.equal(copy.nodeType, 'binary_expression');
assert(copy.gotoParent());
assert.equal(copy.nodeType, 'expression_statement');
assert(copy.gotoParent());
assert.equal(copy.nodeType, 'program');
assert(!copy.gotoParent());
assert(cursor.gotoParent());
assert.equal(cursor.nodeType, 'binary_expression');
assert(cursor.gotoParent());
assert.equal(cursor.nodeType, 'expression_statement');
assert(cursor.gotoParent());
assert.equal(cursor.nodeType, 'program');
assert(!cursor.gotoParent());
});
it('keeps track of the field name associated with each node', () => {
tree = parser.parse('a.b();');
cursor = tree.walk();
cursor.gotoFirstChild();
cursor.gotoFirstChild();
assert.equal(cursor.currentNode.type, 'call_expression');
assert.equal(cursor.currentFieldName, null);
cursor.gotoFirstChild();
assert.equal(cursor.currentNode.type, 'member_expression');
assert.equal(cursor.currentFieldName, 'function');
cursor.gotoFirstChild();
assert.equal(cursor.currentNode.type, 'identifier');
assert.equal(cursor.currentFieldName, 'object');
cursor.gotoNextSibling();
cursor.gotoNextSibling();
assert.equal(cursor.currentNode.type, 'property_identifier');
assert.equal(cursor.currentFieldName, 'property');
cursor.gotoParent();
cursor.gotoNextSibling();
assert.equal(cursor.currentNode.type, 'arguments');
assert.equal(cursor.currentFieldName, 'arguments');
});
it('returns a cursor that can be reset anywhere in the tree', () => {
tree = parser.parse('a * b + c / d');
cursor = tree.walk();
const root = tree.rootNode.firstChild;
cursor.reset(root.firstChild.firstChild);
assertCursorState(cursor, {
nodeType: 'binary_expression',
nodeIsNamed: true,
startPosition: {row: 0, column: 0},
endPosition: {row: 0, column: 5},
startIndex: 0,
endIndex: 5,
});
cursor.gotoFirstChild();
assertCursorState(cursor, {
nodeType: 'identifier',
nodeIsNamed: true,
startPosition: {row: 0, column: 0},
endPosition: {row: 0, column: 1},
startIndex: 0,
endIndex: 1,
});
assert(cursor.gotoParent());
assert(!cursor.gotoParent());
});
});
describe('.copy', () => {
it('creates another tree that remains stable if the original tree is edited', () => {
input = 'abc + cde';
tree = parser.parse(input);
assert.equal(
tree.rootNode.toString(),
'(program (expression_statement (binary_expression left: (identifier) right: (identifier))))',
);
const tree2 = tree.copy();
[input, edit] = spliceInput(input, 3, 0, '123');
assert.equal(input, 'abc123 + cde');
tree.edit(edit);
const leftNode = tree.rootNode.firstChild.firstChild.firstChild;
const leftNode2 = tree2.rootNode.firstChild.firstChild.firstChild;
const rightNode = tree.rootNode.firstChild.firstChild.lastChild;
const rightNode2 = tree2.rootNode.firstChild.firstChild.lastChild;
assert.equal(leftNode.endIndex, 6);
assert.equal(leftNode2.endIndex, 3);
assert.equal(rightNode.startIndex, 9);
assert.equal(rightNode2.startIndex, 6);
});
});
});
function spliceInput(input, startIndex, lengthRemoved, newText) {
const oldEndIndex = startIndex + lengthRemoved;
const newEndIndex = startIndex + newText.length;
const startPosition = getExtent(input.slice(0, startIndex));
const oldEndPosition = getExtent(input.slice(0, oldEndIndex));
input = input.slice(0, startIndex) + newText + input.slice(oldEndIndex);
const newEndPosition = getExtent(input.slice(0, newEndIndex));
return [
input,
{
startIndex, startPosition,
oldEndIndex, oldEndPosition,
newEndIndex, newEndPosition,
},
];
}
// Gets the extent of the text in terms of zero-based row and column numbers.
function getExtent(text) {
let row = 0;
let index = -1;
let lastIndex = 0;
while ((index = text.indexOf('\n', index + 1)) !== -1) {
row++;
lastIndex = index + 1;
}
return {row, column: text.length - lastIndex};
}
function assertCursorState(cursor, params) {
assert.equal(cursor.nodeType, params.nodeType);
assert.equal(cursor.nodeIsNamed, params.nodeIsNamed);
assert.deepEqual(cursor.startPosition, params.startPosition);
assert.deepEqual(cursor.endPosition, params.endPosition);
assert.deepEqual(cursor.startIndex, params.startIndex);
assert.deepEqual(cursor.endIndex, params.endIndex);
const node = cursor.currentNode;
assert.equal(node.type, params.nodeType);
assert.equal(node.isNamed, params.nodeIsNamed);
assert.deepEqual(node.startPosition, params.startPosition);
assert.deepEqual(node.endPosition, params.endPosition);
assert.deepEqual(node.startIndex, params.startIndex);
assert.deepEqual(node.endIndex, params.endIndex);
}

View file

@ -0,0 +1,443 @@
import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest';
import TSParser, { type Language, type Tree, type TreeCursor, type Edit, Point } from 'web-tree-sitter';
import helper from './helper';
let Parser: typeof TSParser;
let JavaScript: Language;
interface CursorState {
nodeType: string;
nodeIsNamed: boolean;
startPosition: Point;
endPosition: Point;
startIndex: number;
endIndex: number;
}
describe('Tree', () => {
let parser: TSParser;
let tree: Tree;
beforeAll(async () => {
({ Parser, JavaScript } = await helper);
});
beforeEach(() => {
parser = new Parser();
parser.setLanguage(JavaScript);
});
afterEach(() => {
parser.delete();
tree.delete();
});
describe('.edit', () => {
let input: string;
let edit: Edit;
it('updates the positions of nodes', () => {
input = 'abc + cde';
tree = parser.parse(input);
expect(tree.rootNode.toString()).toBe(
'(program (expression_statement (binary_expression left: (identifier) right: (identifier))))'
);
let sumNode = tree.rootNode.firstChild!.firstChild;
let variableNode1 = sumNode!.firstChild;
let variableNode2 = sumNode!.lastChild;
expect(variableNode1!.startIndex).toBe(0);
expect(variableNode1!.endIndex).toBe(3);
expect(variableNode2!.startIndex).toBe(6);
expect(variableNode2!.endIndex).toBe(9);
[input, edit] = spliceInput(input, input.indexOf('bc'), 0, ' * ');
expect(input).toBe('a * bc + cde');
tree.edit(edit);
sumNode = tree.rootNode.firstChild!.firstChild;
variableNode1 = sumNode!.firstChild;
variableNode2 = sumNode!.lastChild;
expect(variableNode1!.startIndex).toBe(0);
expect(variableNode1!.endIndex).toBe(6);
expect(variableNode2!.startIndex).toBe(9);
expect(variableNode2!.endIndex).toBe(12);
tree = parser.parse(input, tree);
expect(tree.rootNode.toString()).toBe(
'(program (expression_statement (binary_expression left: (binary_expression left: (identifier) right: (identifier)) right: (identifier))))'
);
});
it('handles non-ascii characters', () => {
input = 'αβδ + cde';
tree = parser.parse(input);
expect(tree.rootNode.toString()).toBe(
'(program (expression_statement (binary_expression left: (identifier) right: (identifier))))'
);
let variableNode = tree.rootNode.firstChild!.firstChild!.lastChild;
[input, edit] = spliceInput(input, input.indexOf('δ'), 0, '👍 * ');
expect(input).toBe('αβ👍 * δ + cde');
tree.edit(edit);
variableNode = tree.rootNode.firstChild!.firstChild!.lastChild;
expect(variableNode!.startIndex).toBe(input.indexOf('cde'));
tree = parser.parse(input, tree);
expect(tree.rootNode.toString()).toBe(
'(program (expression_statement (binary_expression left: (binary_expression left: (identifier) right: (identifier)) right: (identifier))))'
);
});
});
describe('.getChangedRanges(previous)', () => {
it('reports the ranges of text whose syntactic meaning has changed', () => {
let sourceCode = 'abcdefg + hij';
tree = parser.parse(sourceCode);
expect(tree.rootNode.toString()).toBe(
'(program (expression_statement (binary_expression left: (identifier) right: (identifier))))'
);
sourceCode = 'abc + defg + hij';
tree.edit({
startIndex: 2,
oldEndIndex: 2,
newEndIndex: 5,
startPosition: { row: 0, column: 2 },
oldEndPosition: { row: 0, column: 2 },
newEndPosition: { row: 0, column: 5 },
});
const tree2 = parser.parse(sourceCode, tree);
expect(tree2.rootNode.toString()).toBe(
'(program (expression_statement (binary_expression left: (binary_expression left: (identifier) right: (identifier)) right: (identifier))))'
);
const ranges = tree.getChangedRanges(tree2);
expect(ranges).toEqual([
{
startIndex: 0,
endIndex: 'abc + defg'.length,
startPosition: { row: 0, column: 0 },
endPosition: { row: 0, column: 'abc + defg'.length },
},
]);
tree2.delete();
});
it('throws an exception if the argument is not a tree', () => {
tree = parser.parse('abcdefg + hij');
expect(() => {
tree.getChangedRanges({} as Tree);
}).toThrow(/Argument must be a Tree/);
});
});
describe('.walk()', () => {
let cursor: TreeCursor;
afterEach(() => {
cursor.delete();
});
it('returns a cursor that can be used to walk the tree', () => {
tree = parser.parse('a * b + c / d');
cursor = tree.walk();
assertCursorState(cursor, {
nodeType: 'program',
nodeIsNamed: true,
startPosition: { row: 0, column: 0 },
endPosition: { row: 0, column: 13 },
startIndex: 0,
endIndex: 13,
});
expect(cursor.gotoFirstChild()).toBe(true);
assertCursorState(cursor, {
nodeType: 'expression_statement',
nodeIsNamed: true,
startPosition: { row: 0, column: 0 },
endPosition: { row: 0, column: 13 },
startIndex: 0,
endIndex: 13,
});
expect(cursor.gotoFirstChild()).toBe(true);
assertCursorState(cursor, {
nodeType: 'binary_expression',
nodeIsNamed: true,
startPosition: { row: 0, column: 0 },
endPosition: { row: 0, column: 13 },
startIndex: 0,
endIndex: 13,
});
expect(cursor.gotoFirstChild()).toBe(true);
assertCursorState(cursor, {
nodeType: 'binary_expression',
nodeIsNamed: true,
startPosition: { row: 0, column: 0 },
endPosition: { row: 0, column: 5 },
startIndex: 0,
endIndex: 5,
});
expect(cursor.gotoFirstChild()).toBe(true);
expect(cursor.nodeText).toBe('a');
assertCursorState(cursor, {
nodeType: 'identifier',
nodeIsNamed: true,
startPosition: { row: 0, column: 0 },
endPosition: { row: 0, column: 1 },
startIndex: 0,
endIndex: 1,
});
expect(cursor.gotoFirstChild()).toBe(false);
expect(cursor.gotoNextSibling()).toBe(true);
expect(cursor.nodeText).toBe('*');
assertCursorState(cursor, {
nodeType: '*',
nodeIsNamed: false,
startPosition: { row: 0, column: 2 },
endPosition: { row: 0, column: 3 },
startIndex: 2,
endIndex: 3,
});
expect(cursor.gotoNextSibling()).toBe(true);
expect(cursor.nodeText).toBe('b');
assertCursorState(cursor, {
nodeType: 'identifier',
nodeIsNamed: true,
startPosition: { row: 0, column: 4 },
endPosition: { row: 0, column: 5 },
startIndex: 4,
endIndex: 5,
});
expect(cursor.gotoNextSibling()).toBe(false);
expect(cursor.gotoParent()).toBe(true);
assertCursorState(cursor, {
nodeType: 'binary_expression',
nodeIsNamed: true,
startPosition: { row: 0, column: 0 },
endPosition: { row: 0, column: 5 },
startIndex: 0,
endIndex: 5,
});
expect(cursor.gotoNextSibling()).toBe(true);
assertCursorState(cursor, {
nodeType: '+',
nodeIsNamed: false,
startPosition: { row: 0, column: 6 },
endPosition: { row: 0, column: 7 },
startIndex: 6,
endIndex: 7,
});
expect(cursor.gotoNextSibling()).toBe(true);
assertCursorState(cursor, {
nodeType: 'binary_expression',
nodeIsNamed: true,
startPosition: { row: 0, column: 8 },
endPosition: { row: 0, column: 13 },
startIndex: 8,
endIndex: 13,
});
const copy = tree.walk();
copy.resetTo(cursor);
expect(copy.gotoPreviousSibling()).toBe(true);
assertCursorState(copy, {
nodeType: '+',
nodeIsNamed: false,
startPosition: { row: 0, column: 6 },
endPosition: { row: 0, column: 7 },
startIndex: 6,
endIndex: 7,
});
expect(copy.gotoPreviousSibling()).toBe(true);
assertCursorState(copy, {
nodeType: 'binary_expression',
nodeIsNamed: true,
startPosition: { row: 0, column: 0 },
endPosition: { row: 0, column: 5 },
startIndex: 0,
endIndex: 5,
});
expect(copy.gotoLastChild()).toBe(true);
assertCursorState(copy, {
nodeType: 'identifier',
nodeIsNamed: true,
startPosition: { row: 0, column: 4 },
endPosition: { row: 0, column: 5 },
startIndex: 4,
endIndex: 5,
});
expect(copy.gotoParent()).toBe(true);
expect(copy.gotoParent()).toBe(true);
expect(copy.nodeType).toBe('binary_expression');
expect(copy.gotoParent()).toBe(true);
expect(copy.nodeType).toBe('expression_statement');
expect(copy.gotoParent()).toBe(true);
expect(copy.nodeType).toBe('program');
expect(copy.gotoParent()).toBe(false);
copy.delete();
expect(cursor.gotoParent()).toBe(true);
expect(cursor.nodeType).toBe('binary_expression');
expect(cursor.gotoParent()).toBe(true);
expect(cursor.nodeType).toBe('expression_statement');
expect(cursor.gotoParent()).toBe(true);
expect(cursor.nodeType).toBe('program');
});
it('keeps track of the field name associated with each node', () => {
tree = parser.parse('a.b();');
cursor = tree.walk();
cursor.gotoFirstChild();
cursor.gotoFirstChild();
expect(cursor.currentNode.type).toBe('call_expression');
expect(cursor.currentFieldName).toBeNull();
cursor.gotoFirstChild();
expect(cursor.currentNode.type).toBe('member_expression');
expect(cursor.currentFieldName).toBe('function');
cursor.gotoFirstChild();
expect(cursor.currentNode.type).toBe('identifier');
expect(cursor.currentFieldName).toBe('object');
cursor.gotoNextSibling();
cursor.gotoNextSibling();
expect(cursor.currentNode.type).toBe('property_identifier');
expect(cursor.currentFieldName).toBe('property');
cursor.gotoParent();
cursor.gotoNextSibling();
expect(cursor.currentNode.type).toBe('arguments');
expect(cursor.currentFieldName).toBe('arguments');
});
it('returns a cursor that can be reset anywhere in the tree', () => {
tree = parser.parse('a * b + c / d');
cursor = tree.walk();
const root = tree.rootNode.firstChild;
cursor.reset(root!.firstChild!.firstChild!);
assertCursorState(cursor, {
nodeType: 'binary_expression',
nodeIsNamed: true,
startPosition: { row: 0, column: 0 },
endPosition: { row: 0, column: 5 },
startIndex: 0,
endIndex: 5,
});
cursor.gotoFirstChild();
assertCursorState(cursor, {
nodeType: 'identifier',
nodeIsNamed: true,
startPosition: { row: 0, column: 0 },
endPosition: { row: 0, column: 1 },
startIndex: 0,
endIndex: 1,
});
expect(cursor.gotoParent()).toBe(true);
expect(cursor.gotoParent()).toBe(false);
});
});
describe('.copy', () => {
let input: string;
let edit: Edit;
it('creates another tree that remains stable if the original tree is edited', () => {
input = 'abc + cde';
tree = parser.parse(input);
expect(tree.rootNode.toString()).toBe(
'(program (expression_statement (binary_expression left: (identifier) right: (identifier))))'
);
const tree2 = tree.copy();
[input, edit] = spliceInput(input, 3, 0, '123');
expect(input).toBe('abc123 + cde');
tree.edit(edit);
const leftNode = tree.rootNode.firstChild!.firstChild!.firstChild;
const leftNode2 = tree2.rootNode.firstChild!.firstChild!.firstChild;
const rightNode = tree.rootNode.firstChild!.firstChild!.lastChild;
const rightNode2 = tree2.rootNode.firstChild!.firstChild!.lastChild;
expect(leftNode!.endIndex).toBe(6);
expect(leftNode2!.endIndex).toBe(3);
expect(rightNode!.startIndex).toBe(9);
expect(rightNode2!.startIndex).toBe(6);
tree2.delete();
});
});
});
function spliceInput(input: string, startIndex: number, lengthRemoved: number, newText: string): [string, Edit] {
const oldEndIndex = startIndex + lengthRemoved;
const newEndIndex = startIndex + newText.length;
const startPosition = getExtent(input.slice(0, startIndex));
const oldEndPosition = getExtent(input.slice(0, oldEndIndex));
input = input.slice(0, startIndex) + newText + input.slice(oldEndIndex);
const newEndPosition = getExtent(input.slice(0, newEndIndex));
return [
input,
{
startIndex,
startPosition,
oldEndIndex,
oldEndPosition,
newEndIndex,
newEndPosition,
},
];
}
// Gets the extent of the text in terms of zero-based row and column numbers.
function getExtent(text: string): Point {
let row = 0;
let index = -1;
let lastIndex = 0;
while ((index = text.indexOf('\n', index + 1)) !== -1) {
row++;
lastIndex = index + 1;
}
return { row, column: text.length - lastIndex };
}
function assertCursorState(cursor: TreeCursor, params: CursorState): void {
expect(cursor.nodeType).toBe(params.nodeType);
expect(cursor.nodeIsNamed).toBe(params.nodeIsNamed);
expect(cursor.startPosition).toEqual(params.startPosition);
expect(cursor.endPosition).toEqual(params.endPosition);
expect(cursor.startIndex).toEqual(params.startIndex);
expect(cursor.endIndex).toEqual(params.endIndex);
const node = cursor.currentNode;
expect(node.type).toBe(params.nodeType);
expect(node.isNamed).toBe(params.nodeIsNamed);
expect(node.startPosition).toEqual(params.startPosition);
expect(node.endPosition).toEqual(params.endPosition);
expect(node.startIndex).toEqual(params.startIndex);
expect(node.endIndex).toEqual(params.endIndex);
}