feat(web)!: rewrite the library in TypeScript
This commit is contained in:
parent
07a86b1729
commit
2cae67892e
39 changed files with 7856 additions and 3629 deletions
|
|
@ -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')),
|
||||
}));
|
||||
23
lib/binding_web/test/helper.ts
Normal file
23
lib/binding_web/test/helper.ts
Normal 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')),
|
||||
}));
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
587
lib/binding_web/test/node.test.ts
Normal file
587
lib/binding_web/test/node.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
412
lib/binding_web/test/parser.test.ts
Normal file
412
lib/binding_web/test/parser.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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;
|
||||
|
|
@ -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);
|
||||
}
|
||||
443
lib/binding_web/test/tree.test.ts
Normal file
443
lib/binding_web/test/tree.test.ts
Normal 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);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue