) => boolean;
+
+export class Query {
+ private [0]: number; // Internal handle for WASM
+ private exceededMatchLimit: boolean;
+ private textPredicates: TextPredicate[][];
+
+ readonly captureNames: string[];
+ readonly captureQuantifiers: number[][];
+ readonly predicates: Predicate[][];
+ readonly setProperties: Properties[];
+ readonly assertedProperties: Properties[];
+ readonly refutedProperties: Properties[];
+ matchLimit?: number;
+
+ constructor(
+ internal: Internal,
+ address: number,
+ captureNames: string[],
+ captureQuantifiers: number[][],
+ textPredicates: TextPredicate[][],
+ predicates: Predicate[][],
+ setProperties: Properties[],
+ assertedProperties: Properties[],
+ refutedProperties: Properties[],
+ ) {
+ assertInternal(internal);
+ this[0] = address;
+ this.captureNames = captureNames;
+ this.captureQuantifiers = captureQuantifiers;
+ this.textPredicates = textPredicates;
+ this.predicates = predicates;
+ this.setProperties = setProperties;
+ this.assertedProperties = assertedProperties;
+ this.refutedProperties = refutedProperties;
+ this.exceededMatchLimit = false;
+ }
+
+ delete(): void {
+ C._ts_query_delete(this[0]);
+ this[0] = 0;
+ }
+
+ matches(
+ node: Node,
+ options: QueryOptions = {}
+ ): QueryMatch[] {
+ const startPosition = options.startPosition || ZERO_POINT;
+ const endPosition = options.endPosition || ZERO_POINT;
+ const startIndex = options.startIndex || 0;
+ const endIndex = options.endIndex || 0;
+ const matchLimit = options.matchLimit || 0xFFFFFFFF;
+ const maxStartDepth = options.maxStartDepth || 0xFFFFFFFF;
+ const timeoutMicros = options.timeoutMicros || 0;
+ const progressCallback = options.progressCallback;
+
+ if (typeof matchLimit !== 'number') {
+ throw new Error('Arguments must be numbers');
+ }
+ this.matchLimit = matchLimit;
+
+ if (endIndex !== 0 && startIndex > endIndex) {
+ throw new Error('`startIndex` cannot be greater than `endIndex`');
+ }
+
+ if (endPosition !== ZERO_POINT && (
+ startPosition.row > endPosition.row ||
+ (startPosition.row === endPosition.row && startPosition.column > endPosition.column)
+ )) {
+ throw new Error('`startPosition` cannot be greater than `endPosition`');
+ }
+
+ if (progressCallback) {
+ currentQueryProgressCallback = progressCallback;
+ }
+
+ marshalNode(node);
+
+ C._ts_query_matches_wasm(
+ this[0],
+ node.tree[0],
+ startPosition.row,
+ startPosition.column,
+ endPosition.row,
+ endPosition.column,
+ startIndex,
+ endIndex,
+ matchLimit,
+ maxStartDepth,
+ timeoutMicros,
+ );
+
+ const rawCount = getValue(TRANSFER_BUFFER, 'i32');
+ const startAddress = getValue(TRANSFER_BUFFER + SIZE_OF_INT, 'i32');
+ const didExceedMatchLimit = getValue(TRANSFER_BUFFER + 2 * SIZE_OF_INT, 'i32');
+ const result = new Array(rawCount);
+ this.exceededMatchLimit = Boolean(didExceedMatchLimit);
+
+ let filteredCount = 0;
+ let address = startAddress;
+ for (let i = 0; i < rawCount; i++) {
+ const pattern = getValue(address, 'i32');
+ address += SIZE_OF_INT;
+ const captureCount = getValue(address, 'i32');
+ address += SIZE_OF_INT;
+
+ const captures: Capture[] = new Array(captureCount);
+ address = unmarshalCaptures(this, node.tree, address, captures);
+
+ if (this.textPredicates[pattern].every((p) => p(captures))) {
+ result[filteredCount] = { pattern, captures };
+ const setProperties = this.setProperties[pattern];
+ if (setProperties) result[filteredCount].setProperties = setProperties;
+ const assertedProperties = this.assertedProperties[pattern];
+ if (assertedProperties) result[filteredCount].assertedProperties = assertedProperties;
+ const refutedProperties = this.refutedProperties[pattern];
+ if (refutedProperties) result[filteredCount].refutedProperties = refutedProperties;
+ filteredCount++;
+ }
+ }
+ result.length = filteredCount;
+
+ C._free(startAddress);
+ currentQueryProgressCallback = null;
+ return result;
+ }
+
+ captures(
+ node: Node,
+ options: QueryOptions = {}
+ ): Capture[] {
+ const startPosition = options.startPosition || ZERO_POINT;
+ const endPosition = options.endPosition || ZERO_POINT;
+ const startIndex = options.startIndex || 0;
+ const endIndex = options.endIndex || 0;
+ const matchLimit = options.matchLimit || 0xFFFFFFFF;
+ const maxStartDepth = options.maxStartDepth || 0xFFFFFFFF;
+ const timeoutMicros = options.timeoutMicros || 0;
+ const progressCallback = options.progressCallback;
+
+ if (typeof matchLimit !== 'number') {
+ throw new Error('Arguments must be numbers');
+ }
+ this.matchLimit = matchLimit;
+
+ if (endIndex !== 0 && startIndex > endIndex) {
+ throw new Error('`startIndex` cannot be greater than `endIndex`');
+ }
+
+ if (endPosition !== ZERO_POINT && (
+ startPosition.row > endPosition.row ||
+ (startPosition.row === endPosition.row && startPosition.column > endPosition.column)
+ )) {
+ throw new Error('`startPosition` cannot be greater than `endPosition`');
+ }
+
+ if (progressCallback) {
+ currentQueryProgressCallback = progressCallback;
+ }
+
+ marshalNode(node);
+
+ C._ts_query_captures_wasm(
+ this[0],
+ node.tree[0],
+ startPosition.row,
+ startPosition.column,
+ endPosition.row,
+ endPosition.column,
+ startIndex,
+ endIndex,
+ matchLimit,
+ maxStartDepth,
+ timeoutMicros,
+ );
+
+ const count = getValue(TRANSFER_BUFFER, 'i32');
+ const startAddress = getValue(TRANSFER_BUFFER + SIZE_OF_INT, 'i32');
+ const didExceedMatchLimit = getValue(TRANSFER_BUFFER + 2 * SIZE_OF_INT, 'i32');
+ const result: Capture[] = [];
+ this.exceededMatchLimit = Boolean(didExceedMatchLimit);
+
+ const captures: Capture[] = [];
+ let address = startAddress;
+ for (let i = 0; i < count; i++) {
+ const pattern = getValue(address, 'i32');
+ address += SIZE_OF_INT;
+ const captureCount = getValue(address, 'i32');
+ address += SIZE_OF_INT;
+ const captureIndex = getValue(address, 'i32');
+ address += SIZE_OF_INT;
+
+ captures.length = captureCount;
+ address = unmarshalCaptures(this, node.tree, address, captures);
+
+ if (this.textPredicates[pattern].every((p) => p(captures))) {
+ const capture = captures[captureIndex];
+ const setProperties = this.setProperties[pattern];
+ if (setProperties) capture.setProperties = setProperties;
+ const assertedProperties = this.assertedProperties[pattern];
+ if (assertedProperties) capture.assertedProperties = assertedProperties;
+ const refutedProperties = this.refutedProperties[pattern];
+ if (refutedProperties) capture.refutedProperties = refutedProperties;
+ result.push(capture);
+ }
+ }
+
+ C._free(startAddress);
+ currentQueryProgressCallback = null;
+ return result;
+ }
+
+ predicatesForPattern(patternIndex: number): Predicate[] {
+ return this.predicates[patternIndex];
+ }
+
+ disableCapture(captureName: string): void {
+ const captureNameLength = lengthBytesUTF8(captureName);
+ const captureNameAddress = C._malloc(captureNameLength + 1);
+ stringToUTF8(captureName, captureNameAddress, captureNameLength + 1);
+ C._ts_query_disable_capture(this[0], captureNameAddress, captureNameLength);
+ C._free(captureNameAddress);
+ }
+
+ disablePattern(patternIndex: number): void {
+ if (patternIndex >= this.predicates.length) {
+ throw new Error(
+ `Pattern index is ${patternIndex} but the pattern count is ${this.predicates.length}`
+ );
+ }
+ C._ts_query_disable_pattern(this[0], patternIndex);
+ }
+
+ didExceedMatchLimit(): boolean {
+ return this.exceededMatchLimit;
+ }
+
+ startIndexForPattern(patternIndex: number): number {
+ if (patternIndex >= this.predicates.length) {
+ throw new Error(
+ `Pattern index is ${patternIndex} but the pattern count is ${this.predicates.length}`
+ );
+ }
+ return C._ts_query_start_byte_for_pattern(this[0], patternIndex);
+ }
+
+ endIndexForPattern(patternIndex: number): number {
+ if (patternIndex >= this.predicates.length) {
+ throw new Error(
+ `Pattern index is ${patternIndex} but the pattern count is ${this.predicates.length}`
+ );
+ }
+ return C._ts_query_end_byte_for_pattern(this[0], patternIndex);
+ }
+
+ isPatternNonLocal(patternIndex: number): boolean {
+ return C._ts_query_is_pattern_non_local(this[0], patternIndex) === 1;
+ }
+
+ isPatternRooted(patternIndex: number): boolean {
+ return C._ts_query_is_pattern_rooted(this[0], patternIndex) === 1;
+ }
+
+ isPatternGuaranteedAtStep(patternIndex: number, stepIndex: number): boolean {
+ return C._ts_query_is_pattern_guaranteed_at_step(
+ this[0],
+ patternIndex,
+ stepIndex
+ ) === 1;
+ }
+}
diff --git a/lib/binding_web/src/tree.ts b/lib/binding_web/src/tree.ts
new file mode 100644
index 00000000..e6e97907
--- /dev/null
+++ b/lib/binding_web/src/tree.ts
@@ -0,0 +1,115 @@
+import { INTERNAL, Internal, assertInternal, ParseCallback, Point, Range, Edit, SIZE_OF_NODE, SIZE_OF_INT, SIZE_OF_RANGE, C } from './constants';
+import { Language } from './language';
+import { Node } from './node';
+import { TreeCursor } from './tree_cursor';
+import { marshalEdit, marshalPoint, unmarshalNode, unmarshalRange } from './marshal';
+import { TRANSFER_BUFFER } from './parser';
+
+export function getText(tree: Tree, startIndex: number, endIndex: number, startPosition: Point): string {
+ const length = endIndex - startIndex;
+ let result = tree.textCallback(startIndex, startPosition);
+ if (result) {
+ startIndex += result.length;
+ while (startIndex < endIndex) {
+ const string = tree.textCallback(startIndex, startPosition);
+ if (string && string.length > 0) {
+ startIndex += string.length;
+ result += string;
+ } else {
+ break;
+ }
+ }
+ if (startIndex > endIndex) {
+ result = result.slice(0, length);
+ }
+ }
+ return result || '';
+}
+
+export class Tree {
+ private [0]: number; // Internal handle for WASM
+
+ textCallback: ParseCallback;
+ language: Language;
+
+ constructor(internal: Internal, address: number, language: Language, textCallback: ParseCallback) {
+ assertInternal(internal);
+ this[0] = address;
+ this.language = language;
+ this.textCallback = textCallback;
+ }
+
+ copy(): Tree {
+ const address = C._ts_tree_copy(this[0]);
+ return new Tree(INTERNAL, address, this.language, this.textCallback);
+ }
+
+ delete(): void {
+ C._ts_tree_delete(this[0]);
+ this[0] = 0;
+ }
+
+ edit(edit: Edit): void {
+ marshalEdit(edit);
+ C._ts_tree_edit_wasm(this[0]);
+ }
+
+ get rootNode(): Node {
+ C._ts_tree_root_node_wasm(this[0]);
+ return unmarshalNode(this)!;
+ }
+
+ rootNodeWithOffset(offsetBytes: number, offsetExtent: Point): Node {
+ const address = TRANSFER_BUFFER + SIZE_OF_NODE;
+ setValue(address, offsetBytes, 'i32');
+ marshalPoint(address + SIZE_OF_INT, offsetExtent);
+ C._ts_tree_root_node_with_offset_wasm(this[0]);
+ return unmarshalNode(this)!;
+ }
+
+ getLanguage(): Language {
+ return this.language;
+ }
+
+ walk(): TreeCursor {
+ return this.rootNode.walk();
+ }
+
+ getChangedRanges(other: Tree): Range[] {
+ if (!(other instanceof Tree)) {
+ throw new TypeError('Argument must be a Tree');
+ }
+
+ C._ts_tree_get_changed_ranges_wasm(this[0], other[0]);
+ const count = getValue(TRANSFER_BUFFER, 'i32');
+ const buffer = getValue(TRANSFER_BUFFER + SIZE_OF_INT, 'i32');
+ const result = new Array(count);
+
+ if (count > 0) {
+ let address = buffer;
+ for (let i = 0; i < count; i++) {
+ result[i] = unmarshalRange(address);
+ address += SIZE_OF_RANGE;
+ }
+ C._free(buffer);
+ }
+ return result;
+ }
+
+ getIncludedRanges(): Range[] {
+ C._ts_tree_included_ranges_wasm(this[0]);
+ const count = getValue(TRANSFER_BUFFER, 'i32');
+ const buffer = getValue(TRANSFER_BUFFER + SIZE_OF_INT, 'i32');
+ const result = new Array(count);
+
+ if (count > 0) {
+ let address = buffer;
+ for (let i = 0; i < count; i++) {
+ result[i] = unmarshalRange(address);
+ address += SIZE_OF_RANGE;
+ }
+ C._free(buffer);
+ }
+ return result;
+ }
+}
diff --git a/lib/binding_web/src/tree_cursor.ts b/lib/binding_web/src/tree_cursor.ts
new file mode 100644
index 00000000..8de0ce96
--- /dev/null
+++ b/lib/binding_web/src/tree_cursor.ts
@@ -0,0 +1,193 @@
+import { INTERNAL, Internal, assertInternal, Point, SIZE_OF_NODE, SIZE_OF_CURSOR, C } from './constants';
+import { marshalNode, marshalPoint, marshalTreeCursor, unmarshalNode, unmarshalPoint, unmarshalTreeCursor } from './marshal';
+import { Node } from './node';
+import { TRANSFER_BUFFER } from './parser';
+import { getText, Tree } from './tree';
+
+export class TreeCursor {
+ // @ts-ignore
+ private [0]: number; // Internal handle for WASM
+ // @ts-ignore
+ private [1]: number; // Internal handle for WASM
+ // @ts-ignore
+ private [2]: number; // Internal handle for WASM
+ // @ts-ignore
+ private [3]: number; // Internal handle for WASM
+
+ private tree: Tree;
+
+ constructor(internal: Internal, tree: Tree) {
+ assertInternal(internal);
+ this.tree = tree;
+ unmarshalTreeCursor(this);
+ }
+
+ copy(): TreeCursor {
+ const copy = new TreeCursor(INTERNAL, this.tree);
+ C._ts_tree_cursor_copy_wasm(this.tree[0]);
+ unmarshalTreeCursor(copy);
+ return copy;
+ }
+
+ delete(): void {
+ marshalTreeCursor(this);
+ C._ts_tree_cursor_delete_wasm(this.tree[0]);
+ this[0] = this[1] = this[2] = 0;
+ }
+
+ reset(node: Node): void {
+ marshalNode(node);
+ marshalTreeCursor(this, TRANSFER_BUFFER + SIZE_OF_NODE);
+ C._ts_tree_cursor_reset_wasm(this.tree[0]);
+ unmarshalTreeCursor(this);
+ }
+
+ resetTo(cursor: TreeCursor): void {
+ marshalTreeCursor(this, TRANSFER_BUFFER);
+ marshalTreeCursor(cursor, TRANSFER_BUFFER + SIZE_OF_CURSOR);
+ C._ts_tree_cursor_reset_to_wasm(this.tree[0], cursor.tree[0]);
+ unmarshalTreeCursor(this);
+ }
+
+ get nodeType(): string {
+ return this.tree.language.types[this.nodeTypeId] || 'ERROR';
+ }
+
+ get nodeTypeId(): number {
+ marshalTreeCursor(this);
+ return C._ts_tree_cursor_current_node_type_id_wasm(this.tree[0]);
+ }
+
+ get nodeStateId(): number {
+ marshalTreeCursor(this);
+ return C._ts_tree_cursor_current_node_state_id_wasm(this.tree[0]);
+ }
+
+ get nodeId(): number {
+ marshalTreeCursor(this);
+ return C._ts_tree_cursor_current_node_id_wasm(this.tree[0]);
+ }
+
+ get nodeIsNamed(): boolean {
+ marshalTreeCursor(this);
+ return C._ts_tree_cursor_current_node_is_named_wasm(this.tree[0]) === 1;
+ }
+
+ get nodeIsMissing(): boolean {
+ marshalTreeCursor(this);
+ return C._ts_tree_cursor_current_node_is_missing_wasm(this.tree[0]) === 1;
+ }
+
+ get nodeText(): string {
+ marshalTreeCursor(this);
+ const startIndex = C._ts_tree_cursor_start_index_wasm(this.tree[0]);
+ const endIndex = C._ts_tree_cursor_end_index_wasm(this.tree[0]);
+ C._ts_tree_cursor_start_position_wasm(this.tree[0]);
+ const startPosition = unmarshalPoint(TRANSFER_BUFFER);
+ return getText(this.tree, startIndex, endIndex, startPosition);
+ }
+
+ get startPosition(): Point {
+ marshalTreeCursor(this);
+ C._ts_tree_cursor_start_position_wasm(this.tree[0]);
+ return unmarshalPoint(TRANSFER_BUFFER);
+ }
+
+ get endPosition(): Point {
+ marshalTreeCursor(this);
+ C._ts_tree_cursor_end_position_wasm(this.tree[0]);
+ return unmarshalPoint(TRANSFER_BUFFER);
+ }
+
+ get startIndex(): number {
+ marshalTreeCursor(this);
+ return C._ts_tree_cursor_start_index_wasm(this.tree[0]);
+ }
+
+ get endIndex(): number {
+ marshalTreeCursor(this);
+ return C._ts_tree_cursor_end_index_wasm(this.tree[0]);
+ }
+
+ get currentNode(): Node | null {
+ marshalTreeCursor(this);
+ C._ts_tree_cursor_current_node_wasm(this.tree[0]);
+ return unmarshalNode(this.tree);
+ }
+
+ get currentFieldId(): number {
+ marshalTreeCursor(this);
+ return C._ts_tree_cursor_current_field_id_wasm(this.tree[0]);
+ }
+
+ get currentFieldName(): string | null {
+ return this.tree.language.fields[this.currentFieldId];
+ }
+
+ get currentDepth(): number {
+ marshalTreeCursor(this);
+ return C._ts_tree_cursor_current_depth_wasm(this.tree[0]);
+ }
+
+ get currentDescendantIndex(): number {
+ marshalTreeCursor(this);
+ return C._ts_tree_cursor_current_descendant_index_wasm(this.tree[0]);
+ }
+
+ gotoFirstChild(): boolean {
+ marshalTreeCursor(this);
+ const result = C._ts_tree_cursor_goto_first_child_wasm(this.tree[0]);
+ unmarshalTreeCursor(this);
+ return result === 1;
+ }
+
+ gotoLastChild(): boolean {
+ marshalTreeCursor(this);
+ const result = C._ts_tree_cursor_goto_last_child_wasm(this.tree[0]);
+ unmarshalTreeCursor(this);
+ return result === 1;
+ }
+
+ gotoFirstChildForIndex(goalIndex: number): boolean {
+ marshalTreeCursor(this);
+ setValue(TRANSFER_BUFFER + SIZE_OF_CURSOR, goalIndex, 'i32');
+ const result = C._ts_tree_cursor_goto_first_child_for_index_wasm(this.tree[0]);
+ unmarshalTreeCursor(this);
+ return result === 1;
+ }
+
+ gotoFirstChildForPosition(goalPosition: Point): boolean {
+ marshalTreeCursor(this);
+ marshalPoint(TRANSFER_BUFFER + SIZE_OF_CURSOR, goalPosition);
+ const result = C._ts_tree_cursor_goto_first_child_for_position_wasm(this.tree[0]);
+ unmarshalTreeCursor(this);
+ return result === 1;
+ }
+
+ gotoNextSibling(): boolean {
+ marshalTreeCursor(this);
+ const result = C._ts_tree_cursor_goto_next_sibling_wasm(this.tree[0]);
+ unmarshalTreeCursor(this);
+ return result === 1;
+ }
+
+ gotoPreviousSibling(): boolean {
+ marshalTreeCursor(this);
+ const result = C._ts_tree_cursor_goto_previous_sibling_wasm(this.tree[0]);
+ unmarshalTreeCursor(this);
+ return result === 1;
+ }
+
+ gotoDescendant(goalDescendantIndex: number): void {
+ marshalTreeCursor(this);
+ C._ts_tree_cursor_goto_descendant_wasm(this.tree[0], goalDescendantIndex);
+ unmarshalTreeCursor(this);
+ }
+
+ gotoParent(): boolean {
+ marshalTreeCursor(this);
+ const result = C._ts_tree_cursor_goto_parent_wasm(this.tree[0]);
+ unmarshalTreeCursor(this);
+ return result === 1;
+ }
+}
diff --git a/lib/binding_web/test/helper.js b/lib/binding_web/test/helper.js
deleted file mode 100644
index c70ed7f7..00000000
--- a/lib/binding_web/test/helper.js
+++ /dev/null
@@ -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')),
-}));
diff --git a/lib/binding_web/test/helper.ts b/lib/binding_web/test/helper.ts
new file mode 100644
index 00000000..31287f19
--- /dev/null
+++ b/lib/binding_web/test/helper.ts
@@ -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')),
+}));
diff --git a/lib/binding_web/test/language-test.js b/lib/binding_web/test/language.test.ts
similarity index 56%
rename from lib/binding_web/test/language-test.js
rename to lib/binding_web/test/language.test.ts
index e2f40e88..a7c5cc7d 100644
--- a/lib/binding_web/test/language-test.js
+++ b/lib/binding_web/test/language.test.ts
@@ -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);
});
});
diff --git a/lib/binding_web/test/node-test.js b/lib/binding_web/test/node-test.js
deleted file mode 100644
index aa2e4bde..00000000
--- a/lib/binding_web/test/node-test.js
+++ /dev/null
@@ -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);
- });
- });
-});
diff --git a/lib/binding_web/test/node.test.ts b/lib/binding_web/test/node.test.ts
new file mode 100644
index 00000000..0ace6534
--- /dev/null
+++ b/lib/binding_web/test/node.test.ts
@@ -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();
+ });
+ });
+});
diff --git a/lib/binding_web/test/parser-test.js b/lib/binding_web/test/parser-test.js
deleted file mode 100644
index 37ddb939..00000000
--- a/lib/binding_web/test/parser-test.js
+++ /dev/null
@@ -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 = 'hi';
- 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 `Hello, ${name.toUpperCase()}, it\'s ${now()}.
`';
- 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(' '));
-
- assert.equal(bStartTagNode.type, 'start_tag');
- assert.equal(bStartTagNode.startIndex, sourceCode.indexOf(''));
- assert.equal(bStartTagNode.endIndex, sourceCode.indexOf('${now()}'));
-
- assert.equal(bEndTagNode.type, 'end_tag');
- assert.equal(bEndTagNode.startIndex, sourceCode.indexOf(''));
- assert.equal(bEndTagNode.endIndex, sourceCode.indexOf('.'));
- });
- });
-
- describe('an included range containing mismatched positions', () => {
- it('parses the text within the range', () => {
- const sourceCode = 'test
{_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 < 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('
');
- 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);
- });
-});
diff --git a/lib/binding_web/test/parser.test.ts b/lib/binding_web/test/parser.test.ts
new file mode 100644
index 00000000..d7adc31a
--- /dev/null
+++ b/lib/binding_web/test/parser.test.ts
@@ -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 = 'hi';
+ 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 `Hello, ${name.toUpperCase()}, it\'s ${now()}.
`';
+ 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(' '));
+
+ expect(bStartTagNode.type).toBe('start_tag');
+ expect(bStartTagNode.startIndex).toBe(sourceCode.indexOf(''));
+ expect(bStartTagNode.endIndex).toBe(sourceCode.indexOf('${now()}'));
+
+ expect(bEndTagNode.type).toBe('end_tag');
+ expect(bEndTagNode.startIndex).toBe(sourceCode.indexOf(''));
+ expect(bEndTagNode.endIndex).toBe(sourceCode.indexOf('.'));
+ });
+ });
+
+ describe('an included range containing mismatched positions', () => {
+ it('parses the text within the range', () => {
+ const sourceCode = 'test
{_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 < 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('
');
+ 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();
+ });
+ });
+});
diff --git a/lib/binding_web/test/query-test.js b/lib/binding_web/test/query.test.ts
similarity index 61%
rename from lib/binding_web/test/query-test.js
rename to lib/binding_web/test/query.test.ts
index 1a3e3a44..50195ff5 100644
--- a/lib/binding_web/test/query-test.js
+++ b/lib/binding_web/test/query.test.ts
@@ -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;
diff --git a/lib/binding_web/test/tree-test.js b/lib/binding_web/test/tree-test.js
deleted file mode 100644
index a79da588..00000000
--- a/lib/binding_web/test/tree-test.js
+++ /dev/null
@@ -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);
-}
diff --git a/lib/binding_web/test/tree.test.ts b/lib/binding_web/test/tree.test.ts
new file mode 100644
index 00000000..2f3568c8
--- /dev/null
+++ b/lib/binding_web/test/tree.test.ts
@@ -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);
+}
diff --git a/lib/binding_web/tree-sitter-web.d.ts b/lib/binding_web/tree-sitter-web.d.ts
index 18e17bbf..74db2ba3 100644
--- a/lib/binding_web/tree-sitter-web.d.ts
+++ b/lib/binding_web/tree-sitter-web.d.ts
@@ -8,7 +8,7 @@ declare module "web-tree-sitter" {
delete(): void;
parse(
input: string | Parser.Input,
- oldTree?: Parser.Tree,
+ oldTree?: Parser.Tree | null,
options?: Parser.Options,
): Parser.Tree;
getIncludedRanges(): Parser.Range[];
@@ -59,7 +59,7 @@ declare module "web-tree-sitter" {
) => void;
export interface Input {
- (index: number, position?: Point): string | null;
+ (index: number, position?: Point): string | null | undefined;
}
export interface SyntaxNode {
@@ -104,6 +104,7 @@ declare module "web-tree-sitter" {
childForFieldName(fieldName: string): SyntaxNode | null;
childForFieldId(fieldId: number): SyntaxNode | null;
fieldNameForChild(childIndex: number): string | null;
+ fieldNameForNamedChild(childIndex: number): string | null;
childrenForFieldName(fieldName: string): Array;
childrenForFieldId(fieldId: number): Array;
firstChildForIndex(index: number): SyntaxNode | null;
@@ -203,9 +204,19 @@ declare module "web-tree-sitter" {
matchLimit?: number;
maxStartDepth?: number;
timeoutMicros?: number;
- progressCallback: (state: QueryState) => boolean;
+ progressCallback?: (state: QueryState) => boolean;
};
+ export interface Predicate {
+ operator: string;
+ operands: PredicateStep[];
+ }
+
+ type PredicateStep =
+ | { type: 'string'; value: string }
+ | { type: 'capture'; name: string };
+
+
export interface PredicateResult {
operator: string;
operands: { name: string; type: string }[];
@@ -220,13 +231,13 @@ declare module "web-tree-sitter" {
}
export class Query {
- captureNames: string[];
- captureQuantifiers: CaptureQuantifier[];
+ readonly captureNames: string[];
+ readonly captureQuantifiers: CaptureQuantifier[];
readonly predicates: { [name: string]: Function }[];
readonly setProperties: any[];
readonly assertedProperties: any[];
readonly refutedProperties: any[];
- readonly matchLimit: number;
+ readonly matchLimit?: number;
delete(): void;
captures(node: SyntaxNode, options?: QueryOptions): QueryCapture[];
@@ -245,17 +256,21 @@ declare module "web-tree-sitter" {
class Language {
static load(input: string | Uint8Array): Promise;
+ readonly name: string | null;
readonly version: number;
readonly fieldCount: number;
readonly stateCount: number;
readonly nodeTypeCount: number;
+
fieldNameForId(fieldId: number): string | null;
fieldIdForName(fieldName: string): number | null;
idForNodeType(type: string, named: boolean): number;
nodeTypeForId(typeId: number): string | null;
nodeTypeIsNamed(typeId: number): boolean;
nodeTypeIsVisible(typeId: number): boolean;
+ get supertypes(): number[];
+ subtypes(supertype: number): number[];
nextState(stateId: number, typeId: number): number;
query(source: string): Query;
lookaheadIterator(stateId: number): LookaheadIterable | null;
diff --git a/lib/binding_web/tsconfig.json b/lib/binding_web/tsconfig.json
new file mode 100644
index 00000000..5d74457b
--- /dev/null
+++ b/lib/binding_web/tsconfig.json
@@ -0,0 +1,37 @@
+{
+ "compilerOptions": {
+ "target": "es2022",
+ "module": "es2022",
+ "lib": [
+ "es2022",
+ "dom"
+ ],
+ "declaration": true,
+ "declarationMap": true,
+ "sourceMap": true,
+ "rootDir": "./",
+ "outDir": "./dist",
+ "strict": true,
+ "noImplicitAny": true,
+ "strictNullChecks": true,
+ "strictFunctionTypes": true,
+ "strictPropertyInitialization": true,
+ "noImplicitThis": true,
+ "alwaysStrict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noImplicitReturns": true,
+ "moduleResolution": "node",
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true,
+ "skipLibCheck": true,
+ },
+ "include": [
+ "src/**/*",
+ "test/**/*"
+ ],
+ "exclude": [
+ "node_modules",
+ "dist",
+ ]
+}
diff --git a/lib/binding_web/vitest.config.ts b/lib/binding_web/vitest.config.ts
new file mode 100644
index 00000000..11067a36
--- /dev/null
+++ b/lib/binding_web/vitest.config.ts
@@ -0,0 +1,19 @@
+import { defineConfig } from 'vitest/config'
+
+export default defineConfig({
+ test: {
+ globals: true,
+ environment: 'node',
+ coverage: {
+ include: [
+ 'tree-sitter.js',
+ ],
+ exclude: [
+ 'test/**',
+ 'dist/**',
+ 'lib/**',
+ 'wasm/**'
+ ],
+ },
+ }
+})
diff --git a/lib/binding_web/exports.txt b/lib/binding_web/wasm/exports.txt
similarity index 100%
rename from lib/binding_web/exports.txt
rename to lib/binding_web/wasm/exports.txt
diff --git a/lib/binding_web/imports.js b/lib/binding_web/wasm/imports.js
similarity index 100%
rename from lib/binding_web/imports.js
rename to lib/binding_web/wasm/imports.js
diff --git a/lib/binding_web/prefix.js b/lib/binding_web/wasm/prefix.js
similarity index 100%
rename from lib/binding_web/prefix.js
rename to lib/binding_web/wasm/prefix.js
diff --git a/lib/binding_web/suffix.js b/lib/binding_web/wasm/suffix.js
similarity index 100%
rename from lib/binding_web/suffix.js
rename to lib/binding_web/wasm/suffix.js
diff --git a/xtask/src/build_wasm.rs b/xtask/src/build_wasm.rs
index 77a198b9..37c18e9f 100644
--- a/xtask/src/build_wasm.rs
+++ b/xtask/src/build_wasm.rs
@@ -106,7 +106,7 @@ pub fn run_wasm(args: &BuildWasm) -> Result<()> {
let exported_functions = format!(
"{}{}",
fs::read_to_string("lib/src/wasm/stdlib-symbols.txt")?,
- fs::read_to_string("lib/binding_web/exports.txt")?
+ fs::read_to_string("lib/binding_web/wasm/exports.txt")?
)
.replace('"', "")
.lines()
@@ -120,53 +120,37 @@ pub fn run_wasm(args: &BuildWasm) -> Result<()> {
let exported_functions = format!("EXPORTED_FUNCTIONS={exported_functions}");
let exported_runtime_methods = "EXPORTED_RUNTIME_METHODS=stringToUTF16,AsciiToString";
+ std::env::set_var("EMCC_DEBUG_SAVE", "1");
+
+ #[rustfmt::skip]
emscripten_flags.extend([
- "-s",
- "WASM=1",
- "-s",
- "INITIAL_MEMORY=33554432",
- "-s",
- "ALLOW_MEMORY_GROWTH=1",
- "-s",
- "SUPPORT_BIG_ENDIAN=1",
- "-s",
- "MAIN_MODULE=2",
- "-s",
- "FILESYSTEM=0",
- "-s",
- "NODEJS_CATCH_EXIT=0",
- "-s",
- "NODEJS_CATCH_REJECTION=0",
- "-s",
- &exported_functions,
- "-s",
- exported_runtime_methods,
+ "-gsource-map",
+ "--source-map-base", ".",
+ "-s", "WASM=1",
+ "-s", "INITIAL_MEMORY=33554432",
+ "-s", "ALLOW_MEMORY_GROWTH=1",
+ "-s", "SUPPORT_BIG_ENDIAN=1",
+ "-s", "MAIN_MODULE=2",
+ "-s", "FILESYSTEM=0",
+ "-s", "NODEJS_CATCH_EXIT=0",
+ "-s", "NODEJS_CATCH_REJECTION=0",
+ "-s", &exported_functions,
+ "-s", exported_runtime_methods,
"-fno-exceptions",
"-std=c11",
- "-D",
- "fprintf(...)=",
- "-D",
- "NDEBUG=",
- "-D",
- "_POSIX_C_SOURCE=200112L",
- "-D",
- "_DEFAULT_SOURCE=",
- "-I",
- "lib/src",
- "-I",
- "lib/include",
- "--js-library",
- "lib/binding_web/imports.js",
- "--pre-js",
- "lib/binding_web/prefix.js",
- "--post-js",
- "lib/binding_web/binding.js",
- "--post-js",
- "lib/binding_web/suffix.js",
+ "-D", "fprintf(...)=",
+ "-D", "NDEBUG=",
+ "-D", "_POSIX_C_SOURCE=200112L",
+ "-D", "_DEFAULT_SOURCE=",
+ "-I", "lib/src",
+ "-I", "lib/include",
+ "--js-library", "lib/binding_web/wasm/imports.js",
+ "--pre-js", "lib/binding_web/wasm/prefix.js",
+ "--post-js", "lib/binding_web/dist/tree-sitter.js",
+ "--post-js", "lib/binding_web/wasm/suffix.js",
+ "-o", "target/scratch/tree-sitter.js",
"lib/src/lib.c",
- "lib/binding_web/binding.c",
- "-o",
- "target/scratch/tree-sitter.js",
+ "lib/binding_web/lib/tree-sitter.c",
]);
let command = command.args(&emscripten_flags);
@@ -195,6 +179,11 @@ fn build_wasm(cmd: &mut Command) -> Result<()> {
"lib/binding_web/tree-sitter.wasm",
)?;
+ fs::rename(
+ "target/scratch/tree-sitter.wasm.map",
+ "lib/binding_web/tree-sitter.wasm.map",
+ )?;
+
Ok(())
}