feat(web)!: rewrite the library in TypeScript

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

1
.gitignore vendored
View file

@ -12,7 +12,6 @@ test/fuzz/out
test/fixtures/grammars/*
!test/fixtures/grammars/.gitkeep
package-lock.json
node_modules
docs/assets/js/tree-sitter.js

View file

@ -1,22 +0,0 @@
module.exports = {
'env': {
'commonjs': true,
'es2021': true,
},
'extends': 'google',
'overrides': [
],
'parserOptions': {
'ecmaVersion': 'latest',
'sourceType': 'module',
},
'rules': {
'indent': ['error', 2, {'SwitchCase': 1}],
'max-len': [
'error',
{'code': 120, 'ignoreComments': true, 'ignoreUrls': true, 'ignoreStrings': true, 'ignoreTemplateLiterals': true},
],
'require-jsdoc': 0,
'new-cap': 0,
},
};

View file

@ -1,6 +1,8 @@
dist/
/tree-sitter.js
/tree-sitter.js.map
/tree-sitter.wasm
package-lock.json
/tree-sitter.wasm.map
node_modules
*.tgz
LICENSE

View file

@ -50,6 +50,14 @@ await Parser.init();
// the library is ready
```
To install a debug version of the library, pass in `--debug` when running `npm install`:
```sh
npm install web-tree-sitter --debug
```
This will load the debug version of the `.wasm` file, which includes sourcemaps for both the JS and WASM files, debug symbols, and assertions.
### Basic Usage
First, create a parser:

File diff suppressed because it is too large Load diff

View file

@ -16,10 +16,16 @@ const void *TRANSFER_BUFFER[12] = {
NULL, NULL, NULL, NULL,
};
static const int SIZE_OF_CURSOR = 4;
static const int SIZE_OF_NODE = 5;
static const int SIZE_OF_POINT = 2;
static const int SIZE_OF_RANGE = 2 + (2 * SIZE_OF_POINT);
static const int SIZE_OF_CAPTURE = 1 + SIZE_OF_NODE;
void *ts_init() {
TRANSFER_BUFFER[0] = (const void *)TREE_SITTER_LANGUAGE_VERSION;
TRANSFER_BUFFER[1] = (const void *)TREE_SITTER_MIN_COMPATIBLE_LANGUAGE_VERSION;
return TRANSFER_BUFFER;
return (void*)TRANSFER_BUFFER;
}
static uint32_t code_unit_to_byte(uint32_t unit) {
@ -95,9 +101,9 @@ static void unmarshal_range(TSRange *range) {
static TSInputEdit unmarshal_edit() {
TSInputEdit edit;
const void **address = TRANSFER_BUFFER;
edit.start_point = unmarshal_point(address); address += 2;
edit.old_end_point = unmarshal_point(address); address += 2;
edit.new_end_point = unmarshal_point(address); address += 2;
edit.start_point = unmarshal_point(address); address += SIZE_OF_POINT;
edit.old_end_point = unmarshal_point(address); address += SIZE_OF_POINT;
edit.new_end_point = unmarshal_point(address); address += SIZE_OF_POINT;
edit.start_byte = code_unit_to_byte((uint32_t)*address); address += 1;
edit.old_end_byte = code_unit_to_byte((uint32_t)*address); address += 1;
edit.new_end_byte = code_unit_to_byte((uint32_t)*address); address += 1;
@ -260,7 +266,7 @@ void ts_tree_root_node_wasm(const TSTree *tree) {
void ts_tree_root_node_with_offset_wasm(const TSTree *tree) {
// read int and point from transfer buffer
const void **address = TRANSFER_BUFFER + 5;
const void **address = TRANSFER_BUFFER + SIZE_OF_NODE;
uint32_t offset = code_unit_to_byte((uint32_t)address[0]);
TSPoint extent = unmarshal_point(address + 1);
TSNode node = ts_tree_root_node_with_offset(tree, offset, extent);
@ -315,14 +321,14 @@ void ts_tree_cursor_delete_wasm(const TSTree *tree) {
void ts_tree_cursor_reset_wasm(const TSTree *tree) {
TSNode node = unmarshal_node(tree);
TSTreeCursor cursor = unmarshal_cursor(&TRANSFER_BUFFER[5], tree);
TSTreeCursor cursor = unmarshal_cursor(&TRANSFER_BUFFER[SIZE_OF_NODE], tree);
ts_tree_cursor_reset(&cursor, node);
marshal_cursor(&cursor);
}
void ts_tree_cursor_reset_to_wasm(const TSTree *_dst, const TSTree *_src) {
TSTreeCursor cursor = unmarshal_cursor(TRANSFER_BUFFER, _dst);
TSTreeCursor src = unmarshal_cursor(&TRANSFER_BUFFER[4], _src);
TSTreeCursor src = unmarshal_cursor(&TRANSFER_BUFFER[SIZE_OF_CURSOR], _src);
ts_tree_cursor_reset_to(&cursor, &src);
marshal_cursor(&cursor);
}
@ -508,25 +514,25 @@ void ts_node_children_by_field_id_wasm(const TSTree *tree, uint32_t field_id) {
if (!ts_tree_cursor_goto_next_sibling(&cursor)) {
done = true;
}
array_grow_by(&result, 5);
marshal_node(result.contents + result.size - 5, result_node);
array_grow_by(&result, SIZE_OF_NODE);
marshal_node(result.contents + result.size - SIZE_OF_NODE, result_node);
}
ts_tree_cursor_delete(&cursor);
TRANSFER_BUFFER[0] = (const void*)(result.size / 5);
TRANSFER_BUFFER[1] = result.contents;
TRANSFER_BUFFER[0] = (const void*)(result.size / SIZE_OF_NODE);
TRANSFER_BUFFER[1] = (const void*)result.contents;
}
void ts_node_first_child_for_byte_wasm(const TSTree *tree) {
TSNode node = unmarshal_node(tree);
const void** address = TRANSFER_BUFFER + 5;
const void** address = TRANSFER_BUFFER + SIZE_OF_NODE;
uint32_t byte = code_unit_to_byte((uint32_t)address[0]);
marshal_node(TRANSFER_BUFFER, ts_node_first_child_for_byte(node, byte));
}
void ts_node_first_named_child_for_byte_wasm(const TSTree *tree) {
TSNode node = unmarshal_node(tree);
const void** address = TRANSFER_BUFFER + 5;
const void** address = TRANSFER_BUFFER + SIZE_OF_NODE;
uint32_t byte = code_unit_to_byte((uint32_t)address[0]);
marshal_node(TRANSFER_BUFFER, ts_node_first_named_child_for_byte(node, byte));
}
@ -593,7 +599,7 @@ void ts_node_parent_wasm(const TSTree *tree) {
void ts_node_descendant_for_index_wasm(const TSTree *tree) {
TSNode node = unmarshal_node(tree);
const void **address = TRANSFER_BUFFER + 5;
const void **address = TRANSFER_BUFFER + SIZE_OF_NODE;
uint32_t start = code_unit_to_byte((uint32_t)address[0]);
uint32_t end = code_unit_to_byte((uint32_t)address[1]);
marshal_node(TRANSFER_BUFFER, ts_node_descendant_for_byte_range(node, start, end));
@ -601,7 +607,7 @@ void ts_node_descendant_for_index_wasm(const TSTree *tree) {
void ts_node_named_descendant_for_index_wasm(const TSTree *tree) {
TSNode node = unmarshal_node(tree);
const void **address = TRANSFER_BUFFER + 5;
const void **address = TRANSFER_BUFFER + SIZE_OF_NODE;
uint32_t start = code_unit_to_byte((uint32_t)address[0]);
uint32_t end = code_unit_to_byte((uint32_t)address[1]);
marshal_node(TRANSFER_BUFFER, ts_node_named_descendant_for_byte_range(node, start, end));
@ -609,16 +615,16 @@ void ts_node_named_descendant_for_index_wasm(const TSTree *tree) {
void ts_node_descendant_for_position_wasm(const TSTree *tree) {
TSNode node = unmarshal_node(tree);
const void **address = TRANSFER_BUFFER + 5;
TSPoint start = unmarshal_point(address); address += 2;
const void **address = TRANSFER_BUFFER + SIZE_OF_NODE;
TSPoint start = unmarshal_point(address); address += SIZE_OF_POINT;
TSPoint end = unmarshal_point(address);
marshal_node(TRANSFER_BUFFER, ts_node_descendant_for_point_range(node, start, end));
}
void ts_node_named_descendant_for_position_wasm(const TSTree *tree) {
TSNode node = unmarshal_node(tree);
const void **address = TRANSFER_BUFFER + 5;
TSPoint start = unmarshal_point(address); address += 2;
const void **address = TRANSFER_BUFFER + SIZE_OF_NODE;
TSPoint start = unmarshal_point(address); address += SIZE_OF_POINT;
TSPoint end = unmarshal_point(address);
marshal_node(TRANSFER_BUFFER, ts_node_named_descendant_for_point_range(node, start, end));
}
@ -653,20 +659,20 @@ void ts_node_children_wasm(const TSTree *tree) {
uint32_t count = ts_node_child_count(node);
const void **result = NULL;
if (count > 0) {
result = calloc(sizeof(void *), 5 * count);
result = (const void**)calloc(sizeof(void *), SIZE_OF_NODE * count);
const void **address = result;
ts_tree_cursor_reset(&scratch_cursor, node);
ts_tree_cursor_goto_first_child(&scratch_cursor);
marshal_node(address, ts_tree_cursor_current_node(&scratch_cursor));
for (uint32_t i = 1; i < count; i++) {
address += 5;
address += SIZE_OF_NODE;
ts_tree_cursor_goto_next_sibling(&scratch_cursor);
TSNode child = ts_tree_cursor_current_node(&scratch_cursor);
marshal_node(address, child);
}
}
TRANSFER_BUFFER[0] = (const void *)count;
TRANSFER_BUFFER[1] = result;
TRANSFER_BUFFER[1] = (const void *)result;
}
void ts_node_named_children_wasm(const TSTree *tree) {
@ -674,7 +680,7 @@ void ts_node_named_children_wasm(const TSTree *tree) {
uint32_t count = ts_node_named_child_count(node);
const void **result = NULL;
if (count > 0) {
result = calloc(sizeof(void *), 5 * count);
result = (const void**)calloc(sizeof(void *), SIZE_OF_NODE * count);
const void **address = result;
ts_tree_cursor_reset(&scratch_cursor, node);
ts_tree_cursor_goto_first_child(&scratch_cursor);
@ -683,7 +689,7 @@ void ts_node_named_children_wasm(const TSTree *tree) {
TSNode child = ts_tree_cursor_current_node(&scratch_cursor);
if (ts_node_is_named(child)) {
marshal_node(address, child);
address += 5;
address += SIZE_OF_NODE;
i++;
if (i == count) {
break;
@ -695,7 +701,7 @@ void ts_node_named_children_wasm(const TSTree *tree) {
}
}
TRANSFER_BUFFER[0] = (const void *)count;
TRANSFER_BUFFER[1] = result;
TRANSFER_BUFFER[1] = (const void *)result;
}
bool symbols_contain(const uint32_t *set, uint32_t length, uint32_t value) {
@ -757,8 +763,8 @@ void ts_node_descendants_of_type_wasm(
// Add the node to the result if its type matches one of the given
// node types.
if (symbols_contain(symbols, symbol_count, ts_node_symbol(descendant))) {
array_grow_by(&result, 5);
marshal_node(result.contents + result.size - 5, descendant);
array_grow_by(&result, SIZE_OF_NODE);
marshal_node(result.contents + result.size - SIZE_OF_NODE, descendant);
}
// Continue walking.
@ -783,8 +789,8 @@ void ts_node_descendants_of_type_wasm(
}
}
TRANSFER_BUFFER[0] = (const void *)(result.size / 5);
TRANSFER_BUFFER[1] = result.contents;
TRANSFER_BUFFER[0] = (const void *)(result.size / SIZE_OF_NODE);
TRANSFER_BUFFER[1] = (const void *)result.contents;
}
int ts_node_is_named_wasm(const TSTree *tree) {
@ -873,21 +879,21 @@ void ts_query_matches_wasm(
TSQueryMatch match;
while (ts_query_cursor_next_match(scratch_query_cursor, &match)) {
match_count++;
array_grow_by(&result, 2 + 6 * match.capture_count);
array_grow_by(&result, 2 + (SIZE_OF_CAPTURE * match.capture_count));
result.contents[index++] = (const void *)(uint32_t)match.pattern_index;
result.contents[index++] = (const void *)(uint32_t)match.capture_count;
for (unsigned i = 0; i < match.capture_count; i++) {
const TSQueryCapture *capture = &match.captures[i];
result.contents[index++] = (const void *)capture->index;
marshal_node(result.contents + index, capture->node);
index += 5;
index += SIZE_OF_NODE;
}
}
bool did_exceed_match_limit =
ts_query_cursor_did_exceed_match_limit(scratch_query_cursor);
TRANSFER_BUFFER[0] = (const void *)(match_count);
TRANSFER_BUFFER[1] = result.contents;
TRANSFER_BUFFER[1] = (const void *)result.contents;
TRANSFER_BUFFER[2] = (const void *)(did_exceed_match_limit);
}
@ -933,7 +939,7 @@ void ts_query_captures_wasm(
)) {
capture_count++;
array_grow_by(&result, 3 + 6 * match.capture_count);
array_grow_by(&result, 3 + (SIZE_OF_CAPTURE * match.capture_count));
result.contents[index++] = (const void *)(uint32_t)match.pattern_index;
result.contents[index++] = (const void *)(uint32_t)match.capture_count;
result.contents[index++] = (const void *)capture_index;
@ -941,13 +947,13 @@ void ts_query_captures_wasm(
const TSQueryCapture *capture = &match.captures[i];
result.contents[index++] = (const void *)capture->index;
marshal_node(result.contents + index, capture->node);
index += 5;
index += SIZE_OF_NODE;
}
}
bool did_exceed_match_limit =
ts_query_cursor_did_exceed_match_limit(scratch_query_cursor);
TRANSFER_BUFFER[0] = (const void *)(capture_count);
TRANSFER_BUFFER[1] = result.contents;
TRANSFER_BUFFER[1] = (const void *)result.contents;
TRANSFER_BUFFER[2] = (const void *)(did_exceed_match_limit);
}

3711
lib/binding_web/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -2,41 +2,56 @@
"name": "web-tree-sitter",
"version": "0.25.0",
"description": "Tree-sitter bindings for the web",
"main": "tree-sitter.js",
"types": "tree-sitter-web.d.ts",
"directories": {
"test": "test"
"repository": "https://github.com/tree-sitter/tree-sitter",
"homepage": "https://github.com/tree-sitter/tree-sitter/tree/master/lib/binding_web",
"license": "MIT",
"author": {
"name": "Max Brunsfeld",
"email": "maxbrunsfeld@gmail.com"
},
"maintainers": [
{
"name": "Amaan Qureshi",
"email": "amaanq12@gmail.com"
}
],
"main": "tree-sitter.js",
"type": "module",
"types": "tree-sitter-web.d.ts",
"keywords": [
"incremental",
"parsing",
"tree-sitter",
"wasm"
],
"files": [
"README.md",
"tree-sitter.js",
"tree-sitter.js.map",
"tree-sitter.wasm",
"tree-sitter.wasm.map",
"tree-sitter-web.d.ts"
],
"scripts": {
"test": "mocha",
"prepack": "cp ../../LICENSE .",
"prepublishOnly": "node check-artifacts-fresh.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/tree-sitter/tree-sitter.git"
},
"keywords": [
"incremental",
"parsing"
],
"author": "Max Brunsfeld",
"license": "MIT",
"bugs": {
"url": "https://github.com/tree-sitter/tree-sitter/issues"
},
"homepage": "https://github.com/tree-sitter/tree-sitter/tree/master/lib/binding_web",
"devDependencies": {
"@types/emscripten": "^1.39.10",
"chai": "^4.3.7",
"eslint": ">=8.56.0",
"eslint-config-google": "^0.14.0",
"mocha": "^10.2.0"
"@types/emscripten": "^1.39.13",
"@types/node": "^22.10.7",
"@vitest/coverage-v8": "^2.1.8",
"esbuild": "^0.24.2",
"eslint": ">=9.18.0",
"source-map": "^0.7.4",
"typescript": "^5.7.3",
"vitest": "^2.1.8"
},
"scripts": {
"build:ts": "esbuild src/index.ts --bundle --platform=neutral --format=cjs --global-name=TreeSitterImpl --outfile=dist/tree-sitter.js --external:fs/* --external:fs/promises --sourcemap --sources-content=true --keep-names",
"build:wasm": "cd ../../ && cargo xtask build-wasm",
"build:sourcemap": "node script/build-sourcemap.js",
"build": "npm run build:ts && npm run build:wasm && npm run build:sourcemap",
"build:debug": "npm run build:ts && npm run build:wasm:debug && mkdir -p debug && mv tree-sitter.* debug/",
"test": "vitest run",
"test:watch": "vitest",
"prepack": "cp ../../LICENSE .",
"prepublishOnly": "node script/check-artifacts-fresh.js",
"postinstall": "node scripts/postinstall.js"
}
}

View file

@ -0,0 +1,62 @@
import { readFileSync, writeFileSync } from 'fs';
import { SourceMapGenerator, SourceMapConsumer } from 'source-map';
async function fixSourceMap() {
const distMap = JSON.parse(readFileSync('dist/tree-sitter.js.map', 'utf8'));
const distJs = readFileSync('dist/tree-sitter.js', 'utf8').split('\n');
const finalJs = readFileSync('tree-sitter.js', 'utf8').split('\n');
const lineMap = new Map();
for (let distLine = 0; distLine < distJs.length; distLine++) {
const line = distJs[distLine].trim();
if (!line) continue;
for (let finalLine = 0; finalLine < finalJs.length; finalLine++) {
if (finalJs[finalLine].trim() === line) {
lineMap.set(distLine + 1, finalLine + 1);
break;
}
}
}
const consumer = await new SourceMapConsumer(distMap);
const generator = new SourceMapGenerator({
file: 'tree-sitter.js',
sourceRoot: ''
});
consumer.eachMapping(mapping => {
const finalLine = lineMap.get(mapping.generatedLine);
if (finalLine) {
generator.addMapping({
generated: {
line: finalLine,
column: mapping.generatedColumn
},
original: {
line: mapping.originalLine,
column: mapping.originalColumn
},
// Fix the source path to be relative to binding_web
source: `src/${mapping.source.split('/').pop()}`,
name: mapping.name
});
}
});
for (const source of consumer.sources) {
const content = consumer.sourceContentFor(source);
if (content) {
generator.setSourceContent(
`src/${source.split('/').pop()}`,
content
);
}
}
consumer.destroy();
writeFileSync('tree-sitter.js.map', generator.toString());
}
fixSourceMap().catch(console.error);

View file

@ -1,13 +1,11 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const inputFiles = [
'binding.c',
'lib/binding.c',
'binding.js',
'exports.txt',
'imports.js',
'wasm/exports.txt',
'wasm/imports.js',
'prefix.js',
...list('../include/tree_sitter'),
...list('../src'),

View file

@ -0,0 +1,25 @@
import fs from 'fs';
import path from 'path';
const isDebug = process.env.npm_config_web_tree_sitter_debug === 'true';
if (isDebug) {
// Copy debug versions to root
fs.copyFileSync(
path.join(__dirname, '../debug/tree-sitter.js'),
path.join(__dirname, '../tree-sitter.js')
);
fs.copyFileSync(
path.join(__dirname, '../debug/tree-sitter.wasm'),
path.join(__dirname, '../tree-sitter.wasm')
);
// Copy sourcemaps too
fs.copyFileSync(
path.join(__dirname, '../debug/tree-sitter.js.map'),
path.join(__dirname, '../tree-sitter.js.map')
);
fs.copyFileSync(
path.join(__dirname, '../debug/tree-sitter.wasm.map'),
path.join(__dirname, '../tree-sitter.wasm.map')
);
}

View file

@ -0,0 +1,238 @@
import { CaptureQuantifier } from "./query";
export interface Point {
row: number;
column: number;
}
export interface Range {
startPosition: Point;
endPosition: Point;
startIndex: number;
endIndex: number;
}
export interface Edit {
startPosition: Point;
oldEndPosition: Point;
newEndPosition: Point;
startIndex: number;
oldEndIndex: number;
newEndIndex: number;
}
export interface ParserOptions {
includedRanges?: Range[];
progressCallback?: (progress: { currentOffset: number }) => boolean;
}
export const SIZE_OF_SHORT = 2;
export const SIZE_OF_INT = 4;
export const SIZE_OF_CURSOR = 4 * SIZE_OF_INT;
export const SIZE_OF_NODE = 5 * SIZE_OF_INT;
export const SIZE_OF_POINT = 2 * SIZE_OF_INT;
export const SIZE_OF_RANGE = 2 * SIZE_OF_INT + 2 * SIZE_OF_POINT;
export const ZERO_POINT: Point = { row: 0, column: 0 };
// Types for callbacks
export type ParseCallback = (index: number, position: Point) => string | null;
export type ProgressCallback = (progress: { currentOffset: number }) => boolean;
export type LogCallback = (message: string, isLex: boolean) => void;
// Helper type for internal use
export const INTERNAL = Symbol('INTERNAL');
export type Internal = typeof INTERNAL;
// Helper functions for type checking
export function assertInternal(x: unknown): asserts x is Internal {
if (x !== INTERNAL) throw new Error('Illegal constructor');
}
export function isPoint(point: Point): point is Point {
return (
!!point &&
typeof (point as Point).row === 'number' &&
typeof (point as Point).column === 'number'
);
}
export const C: EmscriptenModule & {
// Global
_ts_init(): number;
// Libc
_malloc(size: number): number;
_calloc(count: number, size: number): number;
_free(ptr: number): void;
// Parser
_ts_parser_new_wasm(): void;
_ts_parser_delete(address: number): void;
_ts_parser_set_language(parserAddress: number, languageAddress: number): void;
_ts_language_version(address: number): number;
_ts_parser_enable_logger_wasm(address: number, enabled: number): void;
_ts_parser_parse_wasm(
address: number,
payload: number,
oldTreeAddress: number,
rangeAddress: number,
rangeCount: number
): number;
_ts_parser_reset(address: number): void;
_ts_parser_timeout_micros(address: number): number;
_ts_parser_set_timeout_micros(address: number, timeout: number): void;
_ts_parser_included_ranges_wasm(address: number): void;
// Language
_ts_language_symbol_count(address: number): number;
_ts_language_symbol_name(address: number, index: number): number;
_ts_language_symbol_type(address: number, index: number): number;
_ts_language_field_count(address: number): number;
_ts_language_field_name_for_id(address: number, id: number): number;
_ts_language_name(address: number): number;
_ts_language_version(address: number): number;
_ts_language_state_count(address: number): number;
_ts_language_symbol_for_name(address: number, typeAddress: number, typeLength: number, named: boolean): number;
_ts_language_type_is_named_wasm(address: number, typeId: number): number;
_ts_language_type_is_visible_wasm(address: number, typeId: number): number;
_ts_language_next_state(address: number, stateId: number, typeId: number): number;
_ts_language_supertypes_wasm(address: number): void;
_ts_language_subtypes_wasm(address: number, supertype: number): void;
// Tree
_ts_tree_copy(tree: number): number;
_ts_tree_delete(tree: number): void;
_ts_tree_edit_wasm(tree: number): void;
_ts_tree_root_node_wasm(tree: number): void;
_ts_tree_root_node_with_offset_wasm(tree: number): void;
_ts_tree_get_changed_ranges_wasm(self: number, other: number): void;
_ts_tree_included_ranges_wasm(self: number): void;
// Node
_ts_node_symbol_wasm(tree: number): number;
_ts_node_grammar_symbol_wasm(tree: number): number;
_ts_node_end_point_wasm(tree: number): void;
_ts_node_end_index_wasm(tree: number): number;
_ts_node_parse_state_wasm(tree: number): number;
_ts_node_next_parse_state_wasm(tree: number): number;
_ts_node_is_named_wasm(tree: number): number;
_ts_node_has_error_wasm(tree: number): number;
_ts_node_has_changes_wasm(tree: number): number;
_ts_node_is_error_wasm(tree: number): number;
_ts_node_is_missing_wasm(tree: number): number;
_ts_node_is_extra_wasm(tree: number): number;
_ts_node_child_wasm(tree: number, index: number): void;
_ts_node_named_child_wasm(tree: number, index: number): void;
_ts_node_child_by_field_id_wasm(tree: number, fieldId: number): void;
_ts_node_field_name_for_child_wasm(tree: number, index: number): number;
_ts_node_field_name_for_named_child_wasm(tree: number, index: number): number;
_ts_node_children_by_field_id_wasm(tree: number, fieldId: number): void;
_ts_node_first_child_for_byte_wasm(tree: number): void;
_ts_node_first_named_child_for_byte_wasm(tree: number): void;
_ts_node_child_count_wasm(tree: number): number;
_ts_node_named_child_count_wasm(tree: number): number;
_ts_node_children_wasm(tree: number): void;
_ts_node_named_children_wasm(tree: number): void;
_ts_node_descendants_of_type_wasm(
tree: number,
symbolsAddress: number,
symbolCount: number,
startRow: number,
startColumn: number,
endRow: number,
endColumn: number
): void;
_ts_node_next_sibling_wasm(tree: number): void;
_ts_node_prev_sibling_wasm(tree: number): void;
_ts_node_next_named_sibling_wasm(tree: number): void;
_ts_node_prev_named_sibling_wasm(tree: number): void;
_ts_node_descendant_count_wasm(tree: number): number;
_ts_node_parent_wasm(tree: number): void;
_ts_node_descendant_for_index_wasm(tree: number): void;
_ts_node_named_descendant_for_index_wasm(tree: number): void;
_ts_node_descendant_for_position_wasm(tree: number): void;
_ts_node_named_descendant_for_position_wasm(tree: number): void;
_ts_tree_cursor_new_wasm(tree: number): void;
_ts_node_to_string_wasm(tree: number): number;
// TreeCursor
_ts_tree_cursor_copy_wasm(cursor: number): void;
_ts_tree_cursor_delete_wasm(cursor: number): void;
_ts_tree_cursor_reset_wasm(cursor: number): void;
_ts_tree_cursor_reset_to_wasm(cursor: number, other: number): void;
_ts_tree_cursor_current_node_type_id_wasm(cursor: number): number;
_ts_tree_cursor_current_node_state_id_wasm(cursor: number): number;
_ts_tree_cursor_current_node_id_wasm(cursor: number): number;
_ts_tree_cursor_current_node_is_named_wasm(cursor: number): number;
_ts_tree_cursor_current_node_is_missing_wasm(cursor: number): number;
_ts_tree_cursor_start_index_wasm(cursor: number): number;
_ts_tree_cursor_end_index_wasm(cursor: number): number;
_ts_tree_cursor_start_position_wasm(cursor: number): void;
_ts_tree_cursor_end_position_wasm(cursor: number): void;
_ts_tree_cursor_current_node_wasm(cursor: number): void;
_ts_tree_cursor_current_field_id_wasm(cursor: number): number;
_ts_tree_cursor_current_depth_wasm(cursor: number): number;
_ts_tree_cursor_current_descendant_index_wasm(cursor: number): number;
_ts_tree_cursor_goto_first_child_wasm(cursor: number): number;
_ts_tree_cursor_goto_last_child_wasm(cursor: number): number;
_ts_tree_cursor_goto_first_child_for_index_wasm(cursor: number): number;
_ts_tree_cursor_goto_first_child_for_position_wasm(cursor: number): number;
_ts_tree_cursor_goto_next_sibling_wasm(cursor: number): number;
_ts_tree_cursor_goto_previous_sibling_wasm(cursor: number): number;
_ts_tree_cursor_goto_descendant_wasm(cursor: number, index: number): void;
_ts_tree_cursor_goto_parent_wasm(cursor: number): number;
// Query
_ts_query_new(languageAddress: number, sourceAddress: number, sourceLength: number, errorOffset: number, errorType: number): number;
_ts_query_string_count(address: number): number;
_ts_query_capture_count(address: number): number;
_ts_query_pattern_count(address: number): number;
_ts_query_capture_name_for_id(address: number, id: number, buffer: number): number;
_ts_query_capture_quantifier_for_id(address: number, patternId: number, captureId: number): CaptureQuantifier;
_ts_query_string_value_for_id(address: number, id: number, buffer: number): number;
_ts_query_predicates_for_pattern(address: number, patternId: number, buffer: number): number;
_ts_query_delete(address: number): void;
_ts_query_matches_wasm(
address: number,
treeAddress: number,
startRow: number,
startColumn: number,
endRow: number,
endColumn: number,
startIndex: number,
endIndex: number,
matchLimit: number,
maxStartDepth: number,
timeoutMicros: number
): void;
_ts_query_captures_wasm(
address: number,
treeAddress: number,
startRow: number,
startColumn: number,
endRow: number,
endColumn: number,
startIndex: number,
endIndex: number,
matchLimit: number,
maxStartDepth: number,
timeoutMicros: number
): void;
_ts_query_disable_capture(address: number, nameAddress: number, nameLength: number): void;
_ts_query_disable_pattern(address: number, patternIndex: number): void;
_ts_query_start_byte_for_pattern(address: number, patternIndex: number): number;
_ts_query_end_byte_for_pattern(address: number, patternIndex: number): number;
_ts_query_is_pattern_non_local(address: number, patternIndex: number): number;
_ts_query_is_pattern_rooted(address: number, patternIndex: number): number;
_ts_query_is_pattern_guaranteed_at_step(address: number, patternIndex: number, stepIndex: number): number;
// LookaheadIterator
_ts_lookahead_iterator_new(address: number, stateId: number): number;
_ts_lookahead_iterator_current_symbol(address: number): number;
_ts_lookahead_iterator_delete(address: number): void;
_ts_lookahead_iterator_reset_state(address: number, stateId: number): boolean;
_ts_lookahead_iterator_reset(address: number, languageAddress: number, stateId: number): boolean;
_ts_lookahead_iterator_next(address: number): boolean;
// @ts-ignore
} = Module;

View file

@ -0,0 +1,9 @@
export * from './constants';
export * from './marshal';
export * from './node';
export * from './tree';
export * from './tree_cursor';
export * from './lookahead_iterator';
export * from './query';
export * from './language';
export * from './parser';

View file

@ -0,0 +1,491 @@
import { INTERNAL, Internal, assertInternal, SIZE_OF_INT, SIZE_OF_SHORT, C } from './constants';
import { LookaheadIterator } from './lookahead_iterator';
import { Node } from './node';
import { TRANSFER_BUFFER } from './parser';
import { CaptureQuantifier, Predicate, PredicateStep, Properties, Query, TextPredicate } from './query';
declare const UTF8ToString: (ptr: number, maxBytesToRead?: number) => string;
declare const lengthBytesUTF8: (str: string) => number;
declare const stringToUTF8: (str: string, outPtr: number, maxBytesToRead: number) => void;
declare const getValue: (ptr: number, type: string) => any;
declare const loadWebAssemblyModule: (bytes: Uint8Array, options: { loadAsync: boolean }) => Promise<any>;
const PREDICATE_STEP_TYPE_CAPTURE = 1;
const PREDICATE_STEP_TYPE_STRING = 2;
const QUERY_WORD_REGEX = /[\w-]+/g;
const LANGUAGE_FUNCTION_REGEX = /^tree_sitter_\w+$/;
export class Language {
private [0]: number; // Internal handle for WASM
public types: string[];
public fields: (string | null)[];
constructor(internal: Internal, address: number) {
assertInternal(internal);
this[0] = address;
this.types = new Array(C._ts_language_symbol_count(this[0]));
for (let i = 0, n = this.types.length; i < n; i++) {
if (C._ts_language_symbol_type(this[0], i) < 2) {
this.types[i] = UTF8ToString(C._ts_language_symbol_name(this[0], i));
}
}
this.fields = new Array(C._ts_language_field_count(this[0]) + 1);
for (let i = 0, n = this.fields.length; i < n; i++) {
const fieldName = C._ts_language_field_name_for_id(this[0], i);
if (fieldName !== 0) {
this.fields[i] = UTF8ToString(fieldName);
} else {
this.fields[i] = null;
}
}
}
get name(): string | null {
const ptr = C._ts_language_name(this[0]);
if (ptr === 0) return null;
return UTF8ToString(ptr);
}
get version(): number {
return C._ts_language_version(this[0]);
}
get fieldCount(): number {
return this.fields.length - 1;
}
get stateCount(): number {
return C._ts_language_state_count(this[0]);
}
fieldIdForName(fieldName: string): number | null {
const result = this.fields.indexOf(fieldName);
return result !== -1 ? result : null;
}
fieldNameForId(fieldId: number): string | null {
return this.fields[fieldId] || null;
}
idForNodeType(type: string, named: boolean): number | null {
const typeLength = lengthBytesUTF8(type);
const typeAddress = C._malloc(typeLength + 1);
stringToUTF8(type, typeAddress, typeLength + 1);
const result = C._ts_language_symbol_for_name(this[0], typeAddress, typeLength, named);
C._free(typeAddress);
return result || null;
}
get nodeTypeCount(): number {
return C._ts_language_symbol_count(this[0]);
}
nodeTypeForId(typeId: number): string | null {
const name = C._ts_language_symbol_name(this[0], typeId);
return name ? UTF8ToString(name) : null;
}
nodeTypeIsNamed(typeId: number): boolean {
return C._ts_language_type_is_named_wasm(this[0], typeId) ? true : false;
}
nodeTypeIsVisible(typeId: number): boolean {
return C._ts_language_type_is_visible_wasm(this[0], typeId) ? true : false;
}
get supertypes(): number[] {
C._ts_language_supertypes_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] = getValue(address, 'i16');
address += SIZE_OF_SHORT;
}
}
return result;
}
subtypes(supertype: number): number[] {
C._ts_language_subtypes_wasm(this[0], supertype);
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] = getValue(address, 'i16');
address += SIZE_OF_SHORT;
}
}
return result;
}
nextState(stateId: number, typeId: number): number {
return C._ts_language_next_state(this[0], stateId, typeId);
}
lookaheadIterator(stateId: number): LookaheadIterator | null {
const address = C._ts_lookahead_iterator_new(this[0], stateId);
if (address) return new LookaheadIterator(INTERNAL, address, this);
return null;
}
query(source: string): Query {
const sourceLength = lengthBytesUTF8(source);
const sourceAddress = C._malloc(sourceLength + 1);
stringToUTF8(source, sourceAddress, sourceLength + 1);
const address = C._ts_query_new(
this[0],
sourceAddress,
sourceLength,
TRANSFER_BUFFER,
TRANSFER_BUFFER + SIZE_OF_INT
);
if (!address) {
const errorId = getValue(TRANSFER_BUFFER + SIZE_OF_INT, 'i32');
const errorByte = getValue(TRANSFER_BUFFER, 'i32');
const errorIndex = UTF8ToString(sourceAddress, errorByte).length;
const suffix = source.slice(errorIndex, errorIndex + 100).split('\n')[0];
let word = suffix.match(QUERY_WORD_REGEX)?.[0] || '';
let error: Error;
switch (errorId) {
case 2:
error = new RangeError(`Bad node name '${word}'`);
break;
case 3:
error = new RangeError(`Bad field name '${word}'`);
break;
case 4:
error = new RangeError(`Bad capture name @${word}`);
break;
case 5:
error = new TypeError(`Bad pattern structure at offset ${errorIndex}: '${suffix}'...`);
word = '';
break;
default:
error = new SyntaxError(`Bad syntax at offset ${errorIndex}: '${suffix}'...`);
word = '';
break;
}
(error as any).index = errorIndex;
(error as any).length = word.length;
C._free(sourceAddress);
throw error;
}
const stringCount = C._ts_query_string_count(address);
const captureCount = C._ts_query_capture_count(address);
const patternCount = C._ts_query_pattern_count(address);
const captureNames = new Array<string>(captureCount);
const captureQuantifiers = new Array<Array<CaptureQuantifier>>(patternCount);
const stringValues = new Array<string>(stringCount);
for (let i = 0; i < captureCount; i++) {
const nameAddress = C._ts_query_capture_name_for_id(
address,
i,
TRANSFER_BUFFER
);
const nameLength = getValue(TRANSFER_BUFFER, 'i32');
captureNames[i] = UTF8ToString(nameAddress, nameLength);
}
for (let i = 0; i < patternCount; i++) {
const captureQuantifiersArray = new Array<CaptureQuantifier>(captureCount);
for (let j = 0; j < captureCount; j++) {
const quantifier = C._ts_query_capture_quantifier_for_id(address, i, j);
captureQuantifiersArray[j] = quantifier;
}
captureQuantifiers[i] = captureQuantifiersArray;
}
for (let i = 0; i < stringCount; i++) {
const valueAddress = C._ts_query_string_value_for_id(
address,
i,
TRANSFER_BUFFER
);
const nameLength = getValue(TRANSFER_BUFFER, 'i32');
stringValues[i] = UTF8ToString(valueAddress, nameLength);
}
const setProperties = new Array<Properties>(patternCount);
const assertedProperties = new Array<Properties>(patternCount);
const refutedProperties = new Array<Properties>(patternCount);
const predicates = new Array<Array<Predicate>>(patternCount);
const textPredicates = new Array<Array<TextPredicate>>(patternCount);
for (let i = 0; i < patternCount; i++) {
const predicatesAddress = C._ts_query_predicates_for_pattern(
address,
i,
TRANSFER_BUFFER
);
const stepCount = getValue(TRANSFER_BUFFER, 'i32');
predicates[i] = [];
textPredicates[i] = [];
const steps: PredicateStep[] = [];
let stepAddress = predicatesAddress;
for (let j = 0; j < stepCount; j++) {
const stepType = getValue(stepAddress, 'i32');
stepAddress += SIZE_OF_INT;
const stepValueId: number = getValue(stepAddress, 'i32');
stepAddress += SIZE_OF_INT;
if (stepType === PREDICATE_STEP_TYPE_CAPTURE) {
const name = captureNames[stepValueId];
steps.push({ type: 'capture', name });
} else if (stepType === PREDICATE_STEP_TYPE_STRING) {
steps.push({ type: 'string', value: stringValues[stepValueId] });
} else if (steps.length > 0) {
if (steps[0].type !== 'string') {
throw new Error('Predicates must begin with a literal value');
}
const operator = steps[0].value!;
let isPositive = true;
let matchAll = true;
let captureName: string | undefined;
switch (operator) {
case 'any-not-eq?':
case 'not-eq?':
isPositive = false;
case 'any-eq?':
case 'eq?': {
if (steps.length !== 3) {
throw new Error(
`Wrong number of arguments to \`#${operator}\` predicate. Expected 2, got ${steps.length - 1}`
);
}
if (steps[1].type !== 'capture') {
throw new Error(
`First argument of \`#${operator}\` predicate must be a capture. Got "${steps[1].value}"`
);
}
matchAll = !operator.startsWith('any-');
if (steps[2].type === 'capture') {
const captureName1 = steps[1].name!;
const captureName2 = steps[2].name!;
textPredicates[i].push((captures) => {
const nodes1: Node[] = [];
const nodes2: Node[] = [];
for (const c of captures) {
if (c.name === captureName1) nodes1.push(c.node);
if (c.name === captureName2) nodes2.push(c.node);
}
const compare = (n1: { text: string }, n2: { text: string }, positive: boolean) => {
return positive ? n1.text === n2.text : n1.text !== n2.text;
};
return matchAll
? nodes1.every((n1) => nodes2.some((n2) => compare(n1, n2, isPositive)))
: nodes1.some((n1) => nodes2.some((n2) => compare(n1, n2, isPositive)));
});
} else {
captureName = steps[1].name;
const stringValue = steps[2].value;
const matches = (n: Node) => n.text === stringValue;
const doesNotMatch = (n: Node) => n.text !== stringValue;
textPredicates[i].push((captures) => {
const nodes = [];
for (const c of captures) {
if (c.name === captureName) nodes.push(c.node);
}
const test = isPositive ? matches : doesNotMatch;
return matchAll ?
nodes.every(test) :
nodes.some(test);
});
}
break;
}
case 'any-not-match?':
case 'not-match?':
isPositive = false;
case 'any-match?':
case 'match?': {
if (steps.length !== 3) {
throw new Error(
`Wrong number of arguments to \`#${operator}\` predicate. Expected 2, got ${steps.length - 1}.`,
);
}
if (steps[1].type !== 'capture') {
throw new Error(
`First argument of \`#${operator}\` predicate must be a capture. Got "${steps[1].value}".`,
);
}
if (steps[2].type !== 'string') {
throw new Error(
`Second argument of \`#${operator}\` predicate must be a string. Got @${steps[2].value}.`,
);
}
captureName = steps[1].name;
const regex = new RegExp(steps[2].value);
matchAll = !operator.startsWith('any-');
textPredicates[i].push((captures) => {
const nodes = [];
for (const c of captures) {
if (c.name === captureName) nodes.push(c.node.text);
}
const test = (text: string, positive: boolean) => {
return positive ?
regex.test(text) :
!regex.test(text);
};
if (nodes.length === 0) return !isPositive;
return matchAll ?
nodes.every((text) => test(text, isPositive)) :
nodes.some((text) => test(text, isPositive));
});
break;
}
case 'set!': {
if (steps.length < 2 || steps.length > 3) {
throw new Error(
`Wrong number of arguments to \`#set!\` predicate. Expected 1 or 2. Got ${steps.length - 1}.`,
);
}
if (steps.some((s) => s.type !== 'string')) {
throw new Error(
`Arguments to \`#set!\` predicate must be a strings.".`,
);
}
if (!setProperties[i]) setProperties[i] = {};
setProperties[i][steps[1].value!] = steps[2]?.value || null;
break;
}
case 'is?':
case 'is-not?': {
if (steps.length < 2 || steps.length > 3) {
throw new Error(
`Wrong number of arguments to \`#${operator}\` predicate. Expected 1 or 2. Got ${steps.length - 1}.`,
);
}
if (steps.some((s) => s.type !== 'string')) {
throw new Error(
`Arguments to \`#${operator}\` predicate must be a strings.".`,
);
}
const properties = operator === 'is?' ? assertedProperties : refutedProperties;
if (!properties[i]) properties[i] = {};
properties[i][steps[1].value!] = steps[2]?.value || null;
break;
}
case 'not-any-of?':
isPositive = false;
case 'any-of?': {
if (steps.length < 2) {
throw new Error(
`Wrong number of arguments to \`#${operator}\` predicate. Expected at least 1. Got ${steps.length - 1}.`,
);
}
if (steps[1].type !== 'capture') {
throw new Error(
`First argument of \`#${operator}\` predicate must be a capture. Got "${steps[1].value}".`,
);
}
for (let i = 2; i < steps.length; i++) {
if (steps[i].type !== 'string') {
throw new Error(
`Arguments to \`#${operator}\` predicate must be a strings.".`,
);
}
}
captureName = steps[1].name;
const values = steps.slice(2).map((s) => s.value);
textPredicates[i].push((captures) => {
const nodes = [];
for (const c of captures) {
if (c.name === captureName) nodes.push(c.node.text);
}
if (nodes.length === 0) return !isPositive;
return nodes.every((text) => values.includes(text)) === isPositive;
});
break;
}
default:
predicates[i].push({ operator, operands: steps.slice(1) });
}
steps.length = 0;
}
}
Object.freeze(setProperties[i]);
Object.freeze(assertedProperties[i]);
Object.freeze(refutedProperties[i]);
}
C._free(sourceAddress);
return new Query(
INTERNAL,
address,
captureNames,
captureQuantifiers,
textPredicates,
predicates,
setProperties,
assertedProperties,
refutedProperties,
);
}
static load(input: string | Uint8Array): Promise<Language> {
let bytes: Promise<Uint8Array>;
if (input instanceof Uint8Array) {
bytes = Promise.resolve(input);
} else {
// @ts-ignore
if (globalThis.process?.versions?.node) {
// @ts-ignore
const fs = require('fs/promises');
bytes = fs.readFile(input);
} else {
bytes = fetch(input)
.then((response) => response.arrayBuffer()
.then((buffer) => {
if (response.ok) {
return new Uint8Array(buffer);
} else {
const body = new TextDecoder('utf-8').decode(buffer);
throw new Error(`Language.load failed with status ${response.status}.\n\n${body}`);
}
}));
}
}
return bytes
.then((bytes) => loadWebAssemblyModule(bytes, { loadAsync: true }))
.then((mod) => {
const symbolNames = Object.keys(mod);
const functionName = symbolNames.find((key) =>
LANGUAGE_FUNCTION_REGEX.test(key) &&
!key.includes('external_scanner_'),
);
if (!functionName) {
console.log(`Couldn't find language function in WASM file. Symbols:\n${JSON.stringify(symbolNames, null, 2)}`);
throw new Error('Language.load failed: no language function found in WASM file');
}
const languageAddress = mod[functionName]();
return new Language(INTERNAL, languageAddress);
});
}
}

View file

@ -0,0 +1,50 @@
import { C, Internal, assertInternal } from './constants';
import { Language } from './language';
export class LookaheadIterator implements Iterable<string> {
private [0]: number; // Internal handle for WASM
private language: Language;
constructor(internal: Internal, address: number, language: Language) {
assertInternal(internal);
this[0] = address;
this.language = language;
}
get currentTypeId(): number {
return C._ts_lookahead_iterator_current_symbol(this[0]);
}
get currentType(): string {
return this.language.types[this.currentTypeId] || 'ERROR';
}
delete(): void {
C._ts_lookahead_iterator_delete(this[0]);
this[0] = 0;
}
resetState(stateId: number): boolean {
return Boolean(C._ts_lookahead_iterator_reset_state(this[0], stateId));
}
reset(language: Language, stateId: number): boolean {
if (C._ts_lookahead_iterator_reset(this[0], language[0], stateId)) {
this.language = language;
return true;
}
return false;
}
[Symbol.iterator](): Iterator<string> {
const self = this;
return {
next(): IteratorResult<string> {
if (C._ts_lookahead_iterator_next(self[0])) {
return { done: false, value: self.currentType };
}
return { done: true, value: '' };
}
};
}
}

View file

@ -0,0 +1,106 @@
import { Edit, INTERNAL, Point, Range, SIZE_OF_INT, SIZE_OF_NODE, SIZE_OF_POINT } from "./constants";
import { Node } from "./node";
import { Tree } from "./tree";
import { Query } from "./query";
import { TreeCursor } from "./tree_cursor";
import { TRANSFER_BUFFER } from "./parser";
export function unmarshalCaptures(query: Query, tree: Tree, address: number, result: Array<{name: string, node: Node}>) {
for (let i = 0, n = result.length; i < n; i++) {
const captureIndex = getValue(address, 'i32');
address += SIZE_OF_INT;
const node = unmarshalNode(tree, address)!;
address += SIZE_OF_NODE;
result[i] = {name: query.captureNames[captureIndex], node};
}
return address;
}
export function marshalNode(node: Node) {
let address = TRANSFER_BUFFER;
setValue(address, node.id, 'i32');
address += SIZE_OF_INT;
setValue(address, node.startIndex, 'i32');
address += SIZE_OF_INT;
setValue(address, node.startPosition.row, 'i32');
address += SIZE_OF_INT;
setValue(address, node.startPosition.column, 'i32');
address += SIZE_OF_INT;
setValue(address, node[0], 'i32');
}
export function unmarshalNode(tree: Tree, address = TRANSFER_BUFFER): Node | null {
const id = getValue(address, 'i32');
address += SIZE_OF_INT;
if (id === 0) return null;
const index = getValue(address, 'i32');
address += SIZE_OF_INT;
const row = getValue(address, 'i32');
address += SIZE_OF_INT;
const column = getValue(address, 'i32');
address += SIZE_OF_INT;
const other = getValue(address, 'i32');
const result = new Node(INTERNAL, {
id,
tree,
startIndex: index,
startPosition: {row, column},
other,
});
return result;
}
export function marshalTreeCursor(cursor: TreeCursor, address = TRANSFER_BUFFER) {
setValue(address + 0 * SIZE_OF_INT, cursor[0], 'i32');
setValue(address + 1 * SIZE_OF_INT, cursor[1], 'i32');
setValue(address + 2 * SIZE_OF_INT, cursor[2], 'i32');
setValue(address + 3 * SIZE_OF_INT, cursor[3], 'i32');
}
export function unmarshalTreeCursor(cursor: TreeCursor) {
cursor[0] = getValue(TRANSFER_BUFFER + 0 * SIZE_OF_INT, 'i32');
cursor[1] = getValue(TRANSFER_BUFFER + 1 * SIZE_OF_INT, 'i32');
cursor[2] = getValue(TRANSFER_BUFFER + 2 * SIZE_OF_INT, 'i32');
cursor[3] = getValue(TRANSFER_BUFFER + 3 * SIZE_OF_INT, 'i32');
}
export function marshalPoint(address: number, point: Point): void {
setValue(address, point.row, 'i32');
setValue(address + SIZE_OF_INT, point.column, 'i32');
}
export function unmarshalPoint(address: number): Point {
const result = {
row: getValue(address, 'i32') >>> 0,
column: getValue(address + SIZE_OF_INT, 'i32') >>> 0,
};
return result;
}
export function marshalRange(address: number, range: Range): void {
marshalPoint(address, range.startPosition); address += SIZE_OF_POINT;
marshalPoint(address, range.endPosition); address += SIZE_OF_POINT;
setValue(address, range.startIndex, 'i32'); address += SIZE_OF_INT;
setValue(address, range.endIndex, 'i32'); address += SIZE_OF_INT;
}
export function unmarshalRange(address: number): Range {
const result = {} as Range;
result.startPosition = unmarshalPoint(address); address += SIZE_OF_POINT;
result.endPosition = unmarshalPoint(address); address += SIZE_OF_POINT;
result.startIndex = getValue(address, 'i32') >>> 0; address += SIZE_OF_INT;
result.endIndex = getValue(address, 'i32') >>> 0;
return result;
}
export function marshalEdit(edit: Edit, address = TRANSFER_BUFFER) {
marshalPoint(address, edit.startPosition); address += SIZE_OF_POINT;
marshalPoint(address, edit.oldEndPosition); address += SIZE_OF_POINT;
marshalPoint(address, edit.newEndPosition); address += SIZE_OF_POINT;
setValue(address, edit.startIndex, 'i32'); address += SIZE_OF_INT;
setValue(address, edit.oldEndIndex, 'i32'); address += SIZE_OF_INT;
setValue(address, edit.newEndIndex, 'i32'); address += SIZE_OF_INT;
}

445
lib/binding_web/src/node.ts Normal file
View file

@ -0,0 +1,445 @@
import { INTERNAL, Internal, assertInternal, Point, Edit, SIZE_OF_INT, SIZE_OF_NODE, SIZE_OF_POINT, ZERO_POINT, isPoint, C } from './constants';
import { getText, Tree } from './tree';
import { TreeCursor } from './tree_cursor';
import { marshalNode, marshalPoint, unmarshalNode, unmarshalPoint } from './marshal';
import { TRANSFER_BUFFER } from './parser';
declare const AsciiToString: (ptr: number) => string;
export class Node {
// @ts-ignore
private [0]: number; // Internal handle for WASM
private _children?: (Node | null)[];
private _namedChildren?: (Node | null)[];
id!: number;
startIndex!: number;
startPosition!: Point;
tree: Tree;
constructor(
internal: Internal,
{
id,
tree,
startIndex,
startPosition,
other,
}: {
id: number;
tree: Tree;
startIndex: number;
startPosition: Point;
other: number;
}
) {
assertInternal(internal);
this[0] = other;
this.id = id;
this.tree = tree;
this.startIndex = startIndex;
this.startPosition = startPosition;
}
get typeId(): number {
marshalNode(this);
return C._ts_node_symbol_wasm(this.tree[0]);
}
get grammarId(): number {
marshalNode(this);
return C._ts_node_grammar_symbol_wasm(this.tree[0]);
}
get type(): string {
return this.tree.language.types[this.typeId] || 'ERROR';
}
get grammarType(): string {
return this.tree.language.types[this.grammarId] || 'ERROR';
}
get endPosition(): Point {
marshalNode(this);
C._ts_node_end_point_wasm(this.tree[0]);
return unmarshalPoint(TRANSFER_BUFFER);
}
get endIndex(): number {
marshalNode(this);
return C._ts_node_end_index_wasm(this.tree[0]);
}
get text(): string {
return getText(this.tree, this.startIndex, this.endIndex, this.startPosition);
}
get parseState(): number {
marshalNode(this);
return C._ts_node_parse_state_wasm(this.tree[0]);
}
get nextParseState(): number {
marshalNode(this);
return C._ts_node_next_parse_state_wasm(this.tree[0]);
}
get isNamed(): boolean {
marshalNode(this);
return C._ts_node_is_named_wasm(this.tree[0]) === 1;
}
get hasError(): boolean {
marshalNode(this);
return C._ts_node_has_error_wasm(this.tree[0]) === 1;
}
get hasChanges(): boolean {
marshalNode(this);
return C._ts_node_has_changes_wasm(this.tree[0]) === 1;
}
get isError(): boolean {
marshalNode(this);
return C._ts_node_is_error_wasm(this.tree[0]) === 1;
}
get isMissing(): boolean {
marshalNode(this);
return C._ts_node_is_missing_wasm(this.tree[0]) === 1;
}
get isExtra(): boolean {
marshalNode(this);
return C._ts_node_is_extra_wasm(this.tree[0]) === 1;
}
equals(other: Node): boolean {
return this.tree === other.tree && this.id === other.id;
}
child(index: number): Node | null {
marshalNode(this);
C._ts_node_child_wasm(this.tree[0], index);
return unmarshalNode(this.tree);
}
namedChild(index: number): Node | null {
marshalNode(this);
C._ts_node_named_child_wasm(this.tree[0], index);
return unmarshalNode(this.tree);
}
childForFieldId(fieldId: number): Node | null {
marshalNode(this);
C._ts_node_child_by_field_id_wasm(this.tree[0], fieldId);
return unmarshalNode(this.tree);
}
childForFieldName(fieldName: string): Node | null {
const fieldId = this.tree.language.fields.indexOf(fieldName);
if (fieldId !== -1) return this.childForFieldId(fieldId);
return null;
}
fieldNameForChild(index: number): string | null {
marshalNode(this);
const address = C._ts_node_field_name_for_child_wasm(this.tree[0], index);
if (!address) return null;
return AsciiToString(address);
}
fieldNameForNamedChild(index: number): string | null {
marshalNode(this);
const address = C._ts_node_field_name_for_named_child_wasm(this.tree[0], index);
if (!address) return null;
return AsciiToString(address);
}
childrenForFieldName(fieldName: string): (Node | null)[] {
const fieldId = this.tree.language.fields.indexOf(fieldName);
if (fieldId !== -1 && fieldId !== 0) return this.childrenForFieldId(fieldId);
return [];
}
childrenForFieldId(fieldId: number): (Node | null)[] {
marshalNode(this);
C._ts_node_children_by_field_id_wasm(this.tree[0], fieldId);
const count = getValue(TRANSFER_BUFFER, 'i32');
const buffer = getValue(TRANSFER_BUFFER + SIZE_OF_INT, 'i32');
const result = new Array<Node | null>(count);
if (count > 0) {
let address = buffer;
for (let i = 0; i < count; i++) {
result[i] = unmarshalNode(this.tree, address);
address += SIZE_OF_NODE;
}
C._free(buffer);
}
return result;
}
firstChildForIndex(index: number): Node | null {
marshalNode(this);
const address = TRANSFER_BUFFER + SIZE_OF_NODE;
setValue(address, index, 'i32');
C._ts_node_first_child_for_byte_wasm(this.tree[0]);
return unmarshalNode(this.tree);
}
firstNamedChildForIndex(index: number): Node | null {
marshalNode(this);
const address = TRANSFER_BUFFER + SIZE_OF_NODE;
setValue(address, index, 'i32');
C._ts_node_first_named_child_for_byte_wasm(this.tree[0]);
return unmarshalNode(this.tree);
}
get childCount(): number {
marshalNode(this);
return C._ts_node_child_count_wasm(this.tree[0]);
}
get namedChildCount(): number {
marshalNode(this);
return C._ts_node_named_child_count_wasm(this.tree[0]);
}
get firstChild(): Node | null {
return this.child(0);
}
get firstNamedChild(): Node | null {
return this.namedChild(0);
}
get lastChild(): Node | null {
return this.child(this.childCount - 1);
}
get lastNamedChild(): Node | null {
return this.namedChild(this.namedChildCount - 1);
}
get children(): (Node | null)[] {
if (!this._children) {
marshalNode(this);
C._ts_node_children_wasm(this.tree[0]);
const count = getValue(TRANSFER_BUFFER, 'i32');
const buffer = getValue(TRANSFER_BUFFER + SIZE_OF_INT, 'i32');
this._children = new Array<Node>(count);
if (count > 0) {
let address = buffer;
for (let i = 0; i < count; i++) {
this._children[i] = unmarshalNode(this.tree, address);
address += SIZE_OF_NODE;
}
C._free(buffer);
}
}
return this._children;
}
get namedChildren(): (Node | null)[] {
if (!this._namedChildren) {
marshalNode(this);
C._ts_node_named_children_wasm(this.tree[0]);
const count = getValue(TRANSFER_BUFFER, 'i32');
const buffer = getValue(TRANSFER_BUFFER + SIZE_OF_INT, 'i32');
this._namedChildren = new Array<Node>(count);
if (count > 0) {
let address = buffer;
for (let i = 0; i < count; i++) {
this._namedChildren[i] = unmarshalNode(this.tree, address);
address += SIZE_OF_NODE;
}
C._free(buffer);
}
}
return this._namedChildren;
}
descendantsOfType(
types: string | string[],
startPosition: Point = ZERO_POINT,
endPosition: Point = ZERO_POINT
): (Node | null)[] {
if (!Array.isArray(types)) types = [types];
// Convert the type strings to numeric type symbols
const symbols: number[] = [];
const typesBySymbol = this.tree.language.types;
for (let i = 0, n = typesBySymbol.length; i < n; i++) {
if (types.includes(typesBySymbol[i])) {
symbols.push(i);
}
}
// Copy the array of symbols to the WASM heap
const symbolsAddress = C._malloc(SIZE_OF_INT * symbols.length);
for (let i = 0, n = symbols.length; i < n; i++) {
setValue(symbolsAddress + i * SIZE_OF_INT, symbols[i], 'i32');
}
// Call the C API to compute the descendants
marshalNode(this);
C._ts_node_descendants_of_type_wasm(
this.tree[0],
symbolsAddress,
symbols.length,
startPosition.row,
startPosition.column,
endPosition.row,
endPosition.column
);
// Instantiate the nodes based on the data returned
const descendantCount = getValue(TRANSFER_BUFFER, 'i32');
const descendantAddress = getValue(TRANSFER_BUFFER + SIZE_OF_INT, 'i32');
const result = new Array<Node | null>(descendantCount);
if (descendantCount > 0) {
let address = descendantAddress;
for (let i = 0; i < descendantCount; i++) {
result[i] = unmarshalNode(this.tree, address);
address += SIZE_OF_NODE;
}
}
// Free the intermediate buffers
C._free(descendantAddress);
C._free(symbolsAddress);
return result;
}
get nextSibling(): Node | null {
marshalNode(this);
C._ts_node_next_sibling_wasm(this.tree[0]);
return unmarshalNode(this.tree);
}
get previousSibling(): Node | null {
marshalNode(this);
C._ts_node_prev_sibling_wasm(this.tree[0]);
return unmarshalNode(this.tree);
}
get nextNamedSibling(): Node | null {
marshalNode(this);
C._ts_node_next_named_sibling_wasm(this.tree[0]);
return unmarshalNode(this.tree);
}
get previousNamedSibling(): Node | null {
marshalNode(this);
C._ts_node_prev_named_sibling_wasm(this.tree[0]);
return unmarshalNode(this.tree);
}
get descendantCount(): number {
marshalNode(this);
return C._ts_node_descendant_count_wasm(this.tree[0]);
}
get parent(): Node | null {
marshalNode(this);
C._ts_node_parent_wasm(this.tree[0]);
return unmarshalNode(this.tree);
}
descendantForIndex(start: number, end: number = start): Node | null {
if (typeof start !== 'number' || typeof end !== 'number') {
throw new Error('Arguments must be numbers');
}
marshalNode(this);
const address = TRANSFER_BUFFER + SIZE_OF_NODE;
setValue(address, start, 'i32');
setValue(address + SIZE_OF_INT, end, 'i32');
C._ts_node_descendant_for_index_wasm(this.tree[0]);
return unmarshalNode(this.tree);
}
namedDescendantForIndex(start: number, end: number = start): Node | null {
if (typeof start !== 'number' || typeof end !== 'number') {
throw new Error('Arguments must be numbers');
}
marshalNode(this);
const address = TRANSFER_BUFFER + SIZE_OF_NODE;
setValue(address, start, 'i32');
setValue(address + SIZE_OF_INT, end, 'i32');
C._ts_node_named_descendant_for_index_wasm(this.tree[0]);
return unmarshalNode(this.tree);
}
descendantForPosition(start: Point, end: Point = start) {
if (!isPoint(start) || !isPoint(end)) {
throw new Error('Arguments must be {row, column} objects');
}
marshalNode(this);
const address = TRANSFER_BUFFER + SIZE_OF_NODE;
marshalPoint(address, start);
marshalPoint(address + SIZE_OF_POINT, end);
C._ts_node_descendant_for_position_wasm(this.tree[0]);
return unmarshalNode(this.tree);
}
namedDescendantForPosition(start: Point, end: Point = start) {
if (!isPoint(start) || !isPoint(end)) {
throw new Error('Arguments must be {row, column} objects');
}
marshalNode(this);
const address = TRANSFER_BUFFER + SIZE_OF_NODE;
marshalPoint(address, start);
marshalPoint(address + SIZE_OF_POINT, end);
C._ts_node_named_descendant_for_position_wasm(this.tree[0]);
return unmarshalNode(this.tree);
}
walk(): TreeCursor {
marshalNode(this);
C._ts_tree_cursor_new_wasm(this.tree[0]);
return new TreeCursor(INTERNAL, this.tree);
}
edit(edit: Edit) {
if (this.startIndex >= edit.oldEndIndex) {
this.startIndex = edit.newEndIndex + (this.startIndex - edit.oldEndIndex);
let subbedPointRow;
let subbedPointColumn;
if (this.startPosition.row > edit.oldEndPosition.row) {
subbedPointRow = this.startPosition.row - edit.oldEndPosition.row;
subbedPointColumn = this.startPosition.column;
} else {
subbedPointRow = 0;
subbedPointColumn = this.startPosition.column;
if (this.startPosition.column >= edit.oldEndPosition.column) {
subbedPointColumn =
this.startPosition.column - edit.oldEndPosition.column;
}
}
if (subbedPointRow > 0) {
this.startPosition.row += subbedPointRow;
this.startPosition.column = subbedPointColumn;
} else {
this.startPosition.column += subbedPointColumn;
}
} else if (this.startIndex > edit.startIndex) {
this.startIndex = edit.newEndIndex;
this.startPosition.row = edit.newEndPosition.row;
this.startPosition.column = edit.newEndPosition.column;
}
}
toString() {
marshalNode(this);
const address = C._ts_node_to_string_wasm(this.tree[0]);
const result = AsciiToString(address);
C._free(address);
return result;
}
}

View file

@ -0,0 +1,193 @@
import { C, INTERNAL, Point, Range, SIZE_OF_INT, SIZE_OF_RANGE } from './constants';
import { Language } from './language';
import { marshalRange, unmarshalRange } from './marshal';
import { Tree } from './tree';
declare const getValue: (ptr: number, type: string) => any;
declare const Module: { [key: string]: any };
declare let initPromise: Promise<void>;
interface ParseOptions {
includedRanges?: Range[];
progressCallback?: (percent: number) => void;
}
type ParseCallback = ((index: number, position: Point) => string) | string;
type LogCallback = ((message: string, type: number, row: number, column: number) => void) | null;
// Global variable for transferring data across the FFI boundary
export let TRANSFER_BUFFER: number;
let VERSION: number;
let MIN_COMPATIBLE_VERSION: number;
// @ts-ignore
let currentParseCallback: ((index: number, position: Point) => string) | null = null;
// @ts-ignore
let currentLogCallback: LogCallback = null;
// @ts-ignore
let currentProgressCallback: ((percent: number) => void) | null = null;
export class ParserImpl {
protected [0]: number = 0;
protected [1]: number = 0;
protected language: Language | null = null;
protected logCallback: LogCallback = null;
static Language: typeof Language;
static init() {
TRANSFER_BUFFER = C._ts_init();
VERSION = getValue(TRANSFER_BUFFER, 'i32');
MIN_COMPATIBLE_VERSION = getValue(TRANSFER_BUFFER + SIZE_OF_INT, 'i32');
}
initialize() {
C._ts_parser_new_wasm();
this[0] = getValue(TRANSFER_BUFFER, 'i32');
this[1] = getValue(TRANSFER_BUFFER + SIZE_OF_INT, 'i32');
}
delete() {
C._ts_parser_delete(this[0]);
C._free(this[1]);
this[0] = 0;
this[1] = 0;
}
setLanguage(language: Language | null): this {
let address: number;
if (!language) {
address = 0;
this.language = null;
} else if (language.constructor === Language) {
address = language[0];
const version = C._ts_language_version(address);
if (version < MIN_COMPATIBLE_VERSION || VERSION < version) {
throw new Error(
`Incompatible language version ${version}. ` +
`Compatibility range ${MIN_COMPATIBLE_VERSION} through ${VERSION}.`
);
}
this.language = language;
} else {
throw new Error('Argument must be a Language');
}
C._ts_parser_set_language(this[0], address);
return this;
}
getLanguage(): Language | null {
return this.language;
}
parse(
callback: ParseCallback,
oldTree?: Tree | null,
options: ParseOptions = {}
): Tree {
if (typeof callback === 'string') {
currentParseCallback = (index: number) => callback.slice(index);
} else if (typeof callback === 'function') {
currentParseCallback = callback;
} else {
throw new Error('Argument must be a string or a function');
}
if (options?.progressCallback) {
currentProgressCallback = options.progressCallback;
} else {
currentProgressCallback = null;
}
if (this.logCallback) {
currentLogCallback = this.logCallback;
C._ts_parser_enable_logger_wasm(this[0], 1);
} else {
currentLogCallback = null;
C._ts_parser_enable_logger_wasm(this[0], 0);
}
let rangeCount = 0;
let rangeAddress = 0;
if (options?.includedRanges) {
rangeCount = options.includedRanges.length;
rangeAddress = C._calloc(rangeCount, SIZE_OF_RANGE);
let address = rangeAddress;
for (let i = 0; i < rangeCount; i++) {
marshalRange(address, options.includedRanges[i]);
address += SIZE_OF_RANGE;
}
}
const treeAddress = C._ts_parser_parse_wasm(
this[0],
this[1],
oldTree ? oldTree[0] : 0,
rangeAddress,
rangeCount
);
if (!treeAddress) {
currentParseCallback = null;
currentLogCallback = null;
currentProgressCallback = null;
throw new Error('Parsing failed');
}
if (!this.language) {
throw new Error('Parser must have a language to parse');
}
const result = new Tree(INTERNAL, treeAddress, this.language, currentParseCallback);
currentParseCallback = null;
currentLogCallback = null;
currentProgressCallback = null;
return result;
}
reset(): void {
C._ts_parser_reset(this[0]);
}
getIncludedRanges(): Range[] {
C._ts_parser_included_ranges_wasm(this[0]);
const count = getValue(TRANSFER_BUFFER, 'i32');
const buffer = getValue(TRANSFER_BUFFER + SIZE_OF_INT, 'i32');
const result: Range[] = 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;
}
getTimeoutMicros(): number {
return C._ts_parser_timeout_micros(this[0]);
}
setTimeoutMicros(timeout: number): void {
C._ts_parser_set_timeout_micros(this[0], timeout);
}
setLogger(callback: LogCallback): this {
if (!callback) {
this.logCallback = null;
} else if (typeof callback !== 'function') {
throw new Error('Logger callback must be a function');
} else {
this.logCallback = callback;
}
return this;
}
getLogger(): LogCallback {
return this.logCallback;
}
}

View file

@ -0,0 +1,329 @@
import { Internal, assertInternal, Point, ZERO_POINT, SIZE_OF_INT, C } from './constants';
import { Node } from './node';
import { marshalNode, unmarshalCaptures } from './marshal';
import { TRANSFER_BUFFER } from './parser';
// @ts-ignore
let currentQueryProgressCallback: ((percent: number) => void) | null = null;
interface QueryOptions {
startPosition?: Point;
endPosition?: Point;
startIndex?: number;
endIndex?: number;
matchLimit?: number;
maxStartDepth?: number;
timeoutMicros?: number;
progressCallback?: (percent: number) => void;
}
export interface Properties {
[key: string]: string | null;
}
export interface Predicate {
operator: string;
operands: PredicateStep[];
}
export interface Capture {
name: string;
node: Node;
setProperties?: Properties;
assertedProperties?: Properties;
refutedProperties?: Properties;
}
export const CaptureQuantifier = {
Zero: 0,
ZeroOrOne: 1,
ZeroOrMore: 2,
One: 3,
OneOrMore: 4
} as const;
export type CaptureQuantifier = typeof CaptureQuantifier[keyof typeof CaptureQuantifier];
export interface QueryMatch {
pattern: number;
captures: Capture[];
setProperties?: Properties;
assertedProperties?: Properties;
refutedProperties?: Properties;
}
export type PredicateStep =
| { type: 'string'; value: string }
| { type: 'capture'; value?: string, name: string };
export type TextPredicate = (captures: Array<Capture>) => 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;
}
}

115
lib/binding_web/src/tree.ts Normal file
View file

@ -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;
}
}

View file

@ -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;
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<SyntaxNode>;
childrenForFieldId(fieldId: number): Array<SyntaxNode>;
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<Language>;
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;

View file

@ -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",
]
}

View file

@ -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/**'
],
},
}
})

View file

@ -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(())
}