Compare commits

..

1 commit

Author SHA1 Message Date
WillLillis
047de9bf96 fix(lib): pass raw duped fd to _fdopen
Attempting to call `_get_osfhandle(fd)` leads to a stack overflow
2025-09-21 17:21:27 -04:00
164 changed files with 4555 additions and 8032 deletions

View file

@ -1,2 +1,6 @@
[alias] [alias]
xtask = "run --package xtask --" xtask = "run --package xtask --"
[env]
# See: https://github.com/rust-lang/cargo/issues/3946#issuecomment-973132993
CARGO_WORKSPACE_DIR = { value = "", relative = true }

View file

@ -4,8 +4,6 @@ updates:
directory: "/" directory: "/"
schedule: schedule:
interval: "weekly" interval: "weekly"
cooldown:
default-days: 3
commit-message: commit-message:
prefix: "build(deps)" prefix: "build(deps)"
labels: labels:
@ -14,16 +12,10 @@ updates:
groups: groups:
cargo: cargo:
patterns: ["*"] patterns: ["*"]
ignore:
- dependency-name: "*"
update-types: ["version-update:semver-major", "version-update:semver-minor"]
- package-ecosystem: "github-actions" - package-ecosystem: "github-actions"
directory: "/" directory: "/"
schedule: schedule:
interval: "weekly" interval: "weekly"
cooldown:
default-days: 3
commit-message: commit-message:
prefix: "ci" prefix: "ci"
labels: labels:
@ -32,17 +24,13 @@ updates:
groups: groups:
actions: actions:
patterns: ["*"] patterns: ["*"]
- package-ecosystem: "npm" - package-ecosystem: "npm"
versioning-strategy: increase
directories: directories:
- "/crates/npm" - "/crates/npm"
- "/crates/eslint" - "/crates/eslint"
- "/lib/binding_web" - "/lib/binding_web"
schedule: schedule:
interval: "weekly" interval: "weekly"
cooldown:
default-days: 3
commit-message: commit-message:
prefix: "build(deps)" prefix: "build(deps)"
labels: labels:

View file

@ -1,25 +0,0 @@
module.exports = async ({ github, context, core }) => {
if (context.eventName !== 'pull_request') return;
const prNumber = context.payload.pull_request.number;
const owner = context.repo.owner;
const repo = context.repo.repo;
const { data: files } = await github.rest.pulls.listFiles({
owner,
repo,
pull_number: prNumber
});
const changedFiles = files.map(file => file.filename);
const wasmStdLibSrc = 'crates/language/wasm/';
const dirChanged = changedFiles.some(file => file.startsWith(wasmStdLibSrc));
if (!dirChanged) return;
const wasmStdLibHeader = 'lib/src/wasm/wasm-stdlib.h';
const requiredChanged = changedFiles.includes(wasmStdLibHeader);
if (!requiredChanged) core.setFailed(`Changes detected in ${wasmStdLibSrc} but ${wasmStdLibHeader} was not modified.`);
};

View file

@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v6 uses: actions/checkout@v5
- name: Create app token - name: Create app token
uses: actions/create-github-app-token@v2 uses: actions/create-github-app-token@v2
@ -24,7 +24,7 @@ jobs:
private-key: ${{ secrets.BACKPORT_KEY }} private-key: ${{ secrets.BACKPORT_KEY }}
- name: Create backport PR - name: Create backport PR
uses: korthout/backport-action@v4 uses: korthout/backport-action@v3
with: with:
pull_title: "${pull_title}" pull_title: "${pull_title}"
label_pattern: "^ci:backport ([^ ]+)$" label_pattern: "^ci:backport ([^ ]+)$"

View file

@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v6 uses: actions/checkout@v5
- name: Set up stable Rust toolchain - name: Set up stable Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1 uses: actions-rust-lang/setup-rust-toolchain@v1

View file

@ -41,7 +41,7 @@ jobs:
- { platform: windows-x64 , target: x86_64-pc-windows-msvc , os: windows-2025 } - { platform: windows-x64 , target: x86_64-pc-windows-msvc , os: windows-2025 }
- { platform: windows-x86 , target: i686-pc-windows-msvc , os: windows-2025 } - { platform: windows-x86 , target: i686-pc-windows-msvc , os: windows-2025 }
- { platform: macos-arm64 , target: aarch64-apple-darwin , os: macos-15 } - { platform: macos-arm64 , target: aarch64-apple-darwin , os: macos-15 }
- { platform: macos-x64 , target: x86_64-apple-darwin , os: macos-15-intel } - { platform: macos-x64 , target: x86_64-apple-darwin , os: macos-13 }
- { platform: wasm32 , target: wasm32-unknown-unknown , os: ubuntu-24.04 } - { platform: wasm32 , target: wasm32-unknown-unknown , os: ubuntu-24.04 }
# Extra features # Extra features
@ -68,25 +68,27 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v6 uses: actions/checkout@v5
- name: Set up cross-compilation - name: Set up environment
if: matrix.cross
run: | run: |
for target in armv7-unknown-linux-gnueabihf i686-unknown-linux-gnu powerpc64-unknown-linux-gnu; do printf 'EMSCRIPTEN_VERSION=%s\n' "$(<crates/loader/emscripten-version)" >> $GITHUB_ENV
camel_target=${target//-/_}; target_cc=${target/-unknown/}
printf 'CC_%s=%s\n' "$camel_target" "${target_cc/v7/}-gcc"
printf 'AR_%s=%s\n' "$camel_target" "${target_cc/v7/}-ar"
printf 'CARGO_TARGET_%s_LINKER=%s\n' "${camel_target^^}" "${target_cc/v7/}-gcc"
done >> $GITHUB_ENV
{
printf 'CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_RUNNER=qemu-arm -L /usr/arm-linux-gnueabihf\n'
printf 'CARGO_TARGET_POWERPC64_UNKNOWN_LINUX_GNU_RUNNER=qemu-ppc64 -L /usr/powerpc64-linux-gnu\n'
} >> $GITHUB_ENV
- name: Get emscripten version if [[ '${{ matrix.platform }}' =~ ^windows ]]; then
if: contains(matrix.features, 'wasm') # Prevent race condition (see #2041)
run: printf 'EMSCRIPTEN_VERSION=%s\n' "$(<crates/loader/emscripten-version)" >> $GITHUB_ENV printf 'RUST_TEST_THREADS=1\n' >> $GITHUB_ENV
elif [[ '${{ matrix.cross }}' == true ]]; then
for target in armv7-unknown-linux-gnueabihf i686-unknown-linux-gnu powerpc64-unknown-linux-gnu; do
camel_target=${target//-/_}; target_cc=${target/-unknown/}
printf 'CC_%s=%s\n' "$camel_target" "${target_cc/v7/}-gcc"
printf 'AR_%s=%s\n' "$camel_target" "${target_cc/v7/}-ar"
printf 'CARGO_TARGET_%s_LINKER=%s\n' "${camel_target^^}" "${target_cc/v7/}-gcc"
done >> $GITHUB_ENV
{
printf 'CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_RUNNER=qemu-arm -L /usr/arm-linux-gnueabihf\n'
printf 'CARGO_TARGET_POWERPC64_UNKNOWN_LINUX_GNU_RUNNER=qemu-ppc64 -L /usr/powerpc64-linux-gnu\n'
} >> $GITHUB_ENV
fi
- name: Install Emscripten - name: Install Emscripten
if: contains(matrix.features, 'wasm') if: contains(matrix.features, 'wasm')
@ -278,7 +280,7 @@ jobs:
- name: Upload CLI artifact - name: Upload CLI artifact
if: "!matrix.no-run" if: "!matrix.no-run"
uses: actions/upload-artifact@v6 uses: actions/upload-artifact@v4
with: with:
name: tree-sitter.${{ matrix.platform }} name: tree-sitter.${{ matrix.platform }}
path: target/${{ matrix.target }}/release/tree-sitter${{ contains(matrix.target, 'windows') && '.exe' || '' }} path: target/${{ matrix.target }}/release/tree-sitter${{ contains(matrix.target, 'windows') && '.exe' || '' }}
@ -287,7 +289,7 @@ jobs:
- name: Upload Wasm artifacts - name: Upload Wasm artifacts
if: matrix.platform == 'linux-x64' if: matrix.platform == 'linux-x64'
uses: actions/upload-artifact@v6 uses: actions/upload-artifact@v4
with: with:
name: tree-sitter.wasm name: tree-sitter.wasm
path: | path: |

View file

@ -26,7 +26,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v6 uses: actions/checkout@v5
- name: Set up stable Rust toolchain - name: Set up stable Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1 uses: actions-rust-lang/setup-rust-toolchain@v1
@ -44,6 +44,3 @@ jobs:
build: build:
uses: ./.github/workflows/build.yml uses: ./.github/workflows/build.yml
check-wasm-stdlib:
uses: ./.github/workflows/wasm_stdlib.yml

View file

@ -3,7 +3,6 @@ on:
push: push:
branches: [master] branches: [master]
paths: [docs/**] paths: [docs/**]
workflow_dispatch:
jobs: jobs:
deploy-docs: deploy-docs:
@ -16,7 +15,7 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v6 uses: actions/checkout@v5
- name: Set up Rust - name: Set up Rust
uses: actions-rust-lang/setup-rust-toolchain@v1 uses: actions-rust-lang/setup-rust-toolchain@v1
@ -26,7 +25,7 @@ jobs:
GH_TOKEN: ${{ github.token }} GH_TOKEN: ${{ github.token }}
run: | run: |
jq_expr='.assets[] | select(.name | contains("x86_64-unknown-linux-gnu")) | .browser_download_url' jq_expr='.assets[] | select(.name | contains("x86_64-unknown-linux-gnu")) | .browser_download_url'
url=$(gh api repos/rust-lang/mdbook/releases/tags/v0.4.52 --jq "$jq_expr") url=$(gh api repos/rust-lang/mdbook/releases/latest --jq "$jq_expr")
mkdir mdbook mkdir mdbook
curl -sSL "$url" | tar -xz -C mdbook curl -sSL "$url" | tar -xz -C mdbook
printf '%s/mdbook\n' "$PWD" >> "$GITHUB_PATH" printf '%s/mdbook\n' "$PWD" >> "$GITHUB_PATH"

View file

@ -28,9 +28,9 @@ jobs:
NVIM: ${{ matrix.os == 'windows-latest' && 'nvim-win64\\bin\\nvim.exe' || 'nvim' }} NVIM: ${{ matrix.os == 'windows-latest' && 'nvim-win64\\bin\\nvim.exe' || 'nvim' }}
NVIM_TS_DIR: nvim-treesitter NVIM_TS_DIR: nvim-treesitter
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v5
- uses: actions/checkout@v6 - uses: actions/checkout@v5
with: with:
repository: nvim-treesitter/nvim-treesitter repository: nvim-treesitter/nvim-treesitter
path: ${{ env.NVIM_TS_DIR }} path: ${{ env.NVIM_TS_DIR }}

View file

@ -17,15 +17,13 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: build needs: build
permissions: permissions:
id-token: write
attestations: write
contents: write contents: write
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v6 uses: actions/checkout@v5
- name: Download build artifacts - name: Download build artifacts
uses: actions/download-artifact@v7 uses: actions/download-artifact@v5
with: with:
path: artifacts path: artifacts
@ -49,16 +47,9 @@ jobs:
rm -rf artifacts rm -rf artifacts
ls -l target/ ls -l target/
- name: Generate attestations
uses: actions/attest-build-provenance@v3
with:
subject-path: |
target/tree-sitter-*.gz
target/web-tree-sitter.tar.gz
- name: Create release - name: Create release
run: |- run: |-
gh release create $GITHUB_REF_NAME \ gh release create ${{ github.ref_name }} \
target/tree-sitter-*.gz \ target/tree-sitter-*.gz \
target/web-tree-sitter.tar.gz target/web-tree-sitter.tar.gz
env: env:
@ -67,47 +58,35 @@ jobs:
crates_io: crates_io:
name: Publish packages to Crates.io name: Publish packages to Crates.io
runs-on: ubuntu-latest runs-on: ubuntu-latest
environment: crates
permissions:
id-token: write
contents: read
needs: release needs: release
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v6 uses: actions/checkout@v5
- name: Set up Rust - name: Set up Rust
uses: actions-rust-lang/setup-rust-toolchain@v1 uses: actions-rust-lang/setup-rust-toolchain@v1
- name: Set up registry token
id: auth
uses: rust-lang/crates-io-auth-action@v1
- name: Publish crates to Crates.io - name: Publish crates to Crates.io
uses: katyo/publish-crates@v2 uses: katyo/publish-crates@v2
with: with:
registry-token: ${{ steps.auth.outputs.token }} registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }}
npm: npm:
name: Publish packages to npmjs.com name: Publish packages to npmjs.com
runs-on: ubuntu-latest runs-on: ubuntu-latest
environment: npm
permissions:
id-token: write
contents: read
needs: release needs: release
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
directory: [crates/cli/npm, lib/binding_web] directory: [cli/npm, lib/binding_web]
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v6 uses: actions/checkout@v5
- name: Set up Node - name: Set up Node
uses: actions/setup-node@v6 uses: actions/setup-node@v5
with: with:
node-version: 24 node-version: 20
registry-url: https://registry.npmjs.org registry-url: https://registry.npmjs.org
- name: Set up Rust - name: Set up Rust
@ -127,3 +106,5 @@ jobs:
- name: Publish to npmjs.com - name: Publish to npmjs.com
working-directory: ${{ matrix.directory }} working-directory: ${{ matrix.directory }}
run: npm publish run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View file

@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout script - name: Checkout script
uses: actions/checkout@v6 uses: actions/checkout@v5
with: with:
sparse-checkout: .github/scripts/close_unresponsive.js sparse-checkout: .github/scripts/close_unresponsive.js
sparse-checkout-cone-mode: false sparse-checkout-cone-mode: false
@ -35,7 +35,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout script - name: Checkout script
uses: actions/checkout@v6 uses: actions/checkout@v5
with: with:
sparse-checkout: .github/scripts/remove_response_label.js sparse-checkout: .github/scripts/remove_response_label.js
sparse-checkout-cone-mode: false sparse-checkout-cone-mode: false

View file

@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout script - name: Checkout script
uses: actions/checkout@v6 uses: actions/checkout@v5
with: with:
sparse-checkout: .github/scripts/reviewers_remove.js sparse-checkout: .github/scripts/reviewers_remove.js
sparse-checkout-cone-mode: false sparse-checkout-cone-mode: false

View file

@ -15,7 +15,7 @@ jobs:
TREE_SITTER: ${{ github.workspace }}/target/release/tree-sitter TREE_SITTER: ${{ github.workspace }}/target/release/tree-sitter
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v6 uses: actions/checkout@v5
- name: Install UBSAN library - name: Install UBSAN library
run: sudo apt-get update -y && sudo apt-get install -y libubsan1 run: sudo apt-get update -y && sudo apt-get install -y libubsan1

View file

@ -16,7 +16,7 @@ jobs:
if: github.event.label.name == 'spam' if: github.event.label.name == 'spam'
steps: steps:
- name: Checkout script - name: Checkout script
uses: actions/checkout@v6 uses: actions/checkout@v5
with: with:
sparse-checkout: .github/scripts/close_spam.js sparse-checkout: .github/scripts/close_spam.js
sparse-checkout-cone-mode: false sparse-checkout-cone-mode: false

View file

@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v6 uses: actions/checkout@v5
- name: Set up stable Rust toolchain - name: Set up stable Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1 uses: actions-rust-lang/setup-rust-toolchain@v1

View file

@ -1,19 +0,0 @@
name: Check Wasm Stdlib build
on:
workflow_call:
jobs:
check:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Check directory changes
uses: actions/github-script@v8
with:
script: |
const scriptPath = `${process.env.GITHUB_WORKSPACE}/.github/scripts/wasm_stdlib.js`;
const script = require(scriptPath);
return script({ github, context, core });

View file

@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.13) cmake_minimum_required(VERSION 3.13)
project(tree-sitter project(tree-sitter
VERSION "0.27.0" VERSION "0.26.0"
DESCRIPTION "An incremental parsing system for programming tools" DESCRIPTION "An incremental parsing system for programming tools"
HOMEPAGE_URL "https://tree-sitter.github.io/tree-sitter/" HOMEPAGE_URL "https://tree-sitter.github.io/tree-sitter/"
LANGUAGES C) LANGUAGES C)
@ -81,7 +81,7 @@ set_target_properties(tree-sitter
SOVERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}" SOVERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}"
DEFINE_SYMBOL "") DEFINE_SYMBOL "")
target_compile_definitions(tree-sitter PRIVATE _POSIX_C_SOURCE=200112L _DEFAULT_SOURCE _BSD_SOURCE _DARWIN_C_SOURCE) target_compile_definitions(tree-sitter PRIVATE _POSIX_C_SOURCE=200112L _DEFAULT_SOURCE)
include(GNUInstallDirs) include(GNUInstallDirs)

929
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -20,7 +20,7 @@ authors = [
"Amaan Qureshi <amaanq12@gmail.com>", "Amaan Qureshi <amaanq12@gmail.com>",
] ]
edition = "2021" edition = "2021"
rust-version = "1.85" rust-version = "1.84"
homepage = "https://tree-sitter.github.io/tree-sitter" homepage = "https://tree-sitter.github.io/tree-sitter"
repository = "https://github.com/tree-sitter/tree-sitter" repository = "https://github.com/tree-sitter/tree-sitter"
license = "MIT" license = "MIT"
@ -103,11 +103,11 @@ codegen-units = 256
[workspace.dependencies] [workspace.dependencies]
ansi_colours = "1.2.3" ansi_colours = "1.2.3"
anstyle = "1.0.13" anstyle = "1.0.11"
anyhow = "1.0.100" anyhow = "1.0.99"
bstr = "1.12.0" bstr = "1.12.0"
cc = "1.2.53" cc = "1.2.37"
clap = { version = "4.5.54", features = [ clap = { version = "4.5.45", features = [
"cargo", "cargo",
"derive", "derive",
"env", "env",
@ -115,49 +115,46 @@ clap = { version = "4.5.54", features = [
"string", "string",
"unstable-styles", "unstable-styles",
] } ] }
clap_complete = "4.5.65" clap_complete = "4.5.57"
clap_complete_nushell = "4.5.10" clap_complete_nushell = "4.5.8"
crc32fast = "1.5.0"
ctor = "0.2.9" ctor = "0.2.9"
ctrlc = { version = "3.5.0", features = ["termination"] } ctrlc = { version = "3.5.0", features = ["termination"] }
dialoguer = { version = "0.11.0", features = ["fuzzy-select"] } dialoguer = { version = "0.11.0", features = ["fuzzy-select"] }
etcetera = "0.11.0" etcetera = "0.10.0"
fs4 = "0.12.0" fs4 = "0.12.0"
glob = "0.3.3" glob = "0.3.3"
heck = "0.5.0" heck = "0.5.0"
html-escape = "0.2.13" html-escape = "0.2.13"
indexmap = "2.12.1" indexmap = "2.11.1"
indoc = "2.0.6" indoc = "2.0.6"
libloading = "0.9.0" libloading = "0.8.8"
log = { version = "0.4.28", features = ["std"] } log = { version = "0.4.28", features = ["std"] }
memchr = "2.7.6" memchr = "2.7.5"
once_cell = "1.21.3" once_cell = "1.21.3"
pretty_assertions = "1.4.1" pretty_assertions = "1.4.1"
rand = "0.8.5" rand = "0.8.5"
regex = "1.11.3" regex = "1.11.2"
regex-syntax = "0.8.6" regex-syntax = "0.8.6"
rustc-hash = "2.1.1" rustc-hash = "2.1.1"
schemars = "1.0.5"
semver = { version = "1.0.27", features = ["serde"] } semver = { version = "1.0.27", features = ["serde"] }
serde = { version = "1.0.219", features = ["derive"] } serde = { version = "1.0.219", features = ["derive"] }
serde_json = { version = "1.0.149", features = ["preserve_order"] } serde_json = { version = "1.0.145", features = ["preserve_order"] }
similar = "2.7.0" similar = "2.7.0"
smallbitvec = "2.6.0" smallbitvec = "2.6.0"
streaming-iterator = "0.1.9" streaming-iterator = "0.1.9"
tempfile = "3.23.0" tempfile = "3.22.0"
thiserror = "2.0.17" thiserror = "2.0.16"
tiny_http = "0.12.0" tiny_http = "0.12.0"
topological-sort = "0.2.2" topological-sort = "0.2.2"
unindent = "0.2.4" unindent = "0.2.4"
walkdir = "2.5.0" walkdir = "2.5.0"
wasmparser = "0.243.0" wasmparser = "0.229.0"
webbrowser = "1.0.5" webbrowser = "1.0.5"
tree-sitter = { version = "0.27.0", path = "./lib" } tree-sitter = { version = "0.27.0", path = "./lib" }
tree-sitter-generate = { version = "0.27.0", path = "./crates/generate" } tree-sitter-generate = { version = "0.27.0", path = "./crates/generate" }
tree-sitter-language = { path = "./crates/language" }
tree-sitter-loader = { version = "0.27.0", path = "./crates/loader" } tree-sitter-loader = { version = "0.27.0", path = "./crates/loader" }
tree-sitter-config = { version = "0.27.0", path = "./crates/config" } tree-sitter-config = { version = "0.27.0", path = "./crates/config" }
tree-sitter-highlight = { version = "0.27.0", path = "./crates/highlight" } tree-sitter-highlight = { version = "0.27.0", path = "./crates/highlight" }
tree-sitter-tags = { version = "0.27.0", path = "./crates/tags" } tree-sitter-tags = { version = "0.27.0", path = "./crates/tags" }
tree-sitter-language = { version = "0.1", path = "./crates/language" }

View file

@ -1,4 +1,4 @@
VERSION := 0.27.0 VERSION := 0.26.0
DESCRIPTION := An incremental parsing system for programming tools DESCRIPTION := An incremental parsing system for programming tools
HOMEPAGE_URL := https://tree-sitter.github.io/tree-sitter/ HOMEPAGE_URL := https://tree-sitter.github.io/tree-sitter/
@ -24,7 +24,7 @@ OBJ := $(SRC:.c=.o)
ARFLAGS := rcs ARFLAGS := rcs
CFLAGS ?= -O3 -Wall -Wextra -Wshadow -Wpedantic -Werror=incompatible-pointer-types CFLAGS ?= -O3 -Wall -Wextra -Wshadow -Wpedantic -Werror=incompatible-pointer-types
override CFLAGS += -std=c11 -fPIC -fvisibility=hidden override CFLAGS += -std=c11 -fPIC -fvisibility=hidden
override CFLAGS += -D_POSIX_C_SOURCE=200112L -D_DEFAULT_SOURCE -D_BSD_SOURCE -D_DARWIN_C_SOURCE override CFLAGS += -D_POSIX_C_SOURCE=200112L -D_DEFAULT_SOURCE
override CFLAGS += -Ilib/src -Ilib/src/wasm -Ilib/include override CFLAGS += -Ilib/src -Ilib/src/wasm -Ilib/include
# ABI versioning # ABI versioning

View file

@ -27,8 +27,6 @@ let package = Package(
.headerSearchPath("src"), .headerSearchPath("src"),
.define("_POSIX_C_SOURCE", to: "200112L"), .define("_POSIX_C_SOURCE", to: "200112L"),
.define("_DEFAULT_SOURCE"), .define("_DEFAULT_SOURCE"),
.define("_BSD_SOURCE"),
.define("_DARWIN_C_SOURCE"),
]), ]),
], ],
cLanguageStandard: .c11 cLanguageStandard: .c11

View file

@ -40,8 +40,6 @@ pub fn build(b: *std.Build) !void {
lib.root_module.addCMacro("_POSIX_C_SOURCE", "200112L"); lib.root_module.addCMacro("_POSIX_C_SOURCE", "200112L");
lib.root_module.addCMacro("_DEFAULT_SOURCE", ""); lib.root_module.addCMacro("_DEFAULT_SOURCE", "");
lib.root_module.addCMacro("_BSD_SOURCE", "");
lib.root_module.addCMacro("_DARWIN_C_SOURCE", "");
if (wasm) { if (wasm) {
if (b.lazyDependency(wasmtimeDep(target.result), .{})) |wasmtime| { if (b.lazyDependency(wasmtimeDep(target.result), .{})) |wasmtime| {

View file

@ -1,7 +1,7 @@
.{ .{
.name = .tree_sitter, .name = .tree_sitter,
.fingerprint = 0x841224b447ac0d4f, .fingerprint = 0x841224b447ac0d4f,
.version = "0.27.0", .version = "0.26.0",
.minimum_zig_version = "0.14.1", .minimum_zig_version = "0.14.1",
.paths = .{ .paths = .{
"build.zig", "build.zig",

View file

@ -42,7 +42,6 @@ bstr.workspace = true
clap.workspace = true clap.workspace = true
clap_complete.workspace = true clap_complete.workspace = true
clap_complete_nushell.workspace = true clap_complete_nushell.workspace = true
crc32fast.workspace = true
ctor.workspace = true ctor.workspace = true
ctrlc.workspace = true ctrlc.workspace = true
dialoguer.workspace = true dialoguer.workspace = true
@ -54,13 +53,11 @@ log.workspace = true
memchr.workspace = true memchr.workspace = true
rand.workspace = true rand.workspace = true
regex.workspace = true regex.workspace = true
schemars.workspace = true
semver.workspace = true semver.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
similar.workspace = true similar.workspace = true
streaming-iterator.workspace = true streaming-iterator.workspace = true
thiserror.workspace = true
tiny_http.workspace = true tiny_http.workspace = true
walkdir.workspace = true walkdir.workspace = true
wasmparser.workspace = true wasmparser.workspace = true
@ -75,7 +72,7 @@ tree-sitter-tags.workspace = true
[dev-dependencies] [dev-dependencies]
encoding_rs = "0.8.35" encoding_rs = "0.8.35"
widestring = "1.2.1" widestring = "1.2.0"
tree_sitter_proc_macro = { path = "src/tests/proc_macro", package = "tree-sitter-tests-proc-macro" } tree_sitter_proc_macro = { path = "src/tests/proc_macro", package = "tree-sitter-tests-proc-macro" }
tempfile.workspace = true tempfile.workspace = true

View file

@ -7,8 +7,7 @@
[npmjs.com]: https://www.npmjs.org/package/tree-sitter-cli [npmjs.com]: https://www.npmjs.org/package/tree-sitter-cli
[npmjs.com badge]: https://img.shields.io/npm/v/tree-sitter-cli.svg?color=%23BF4A4A [npmjs.com badge]: https://img.shields.io/npm/v/tree-sitter-cli.svg?color=%23BF4A4A
The Tree-sitter CLI allows you to develop, test, and use Tree-sitter grammars from the command line. It works on `MacOS`, The Tree-sitter CLI allows you to develop, test, and use Tree-sitter grammars from the command line. It works on `MacOS`, `Linux`, and `Windows`.
`Linux`, and `Windows`.
### Installation ### Installation
@ -35,11 +34,9 @@ The `tree-sitter` binary itself has no dependencies, but specific commands have
### Commands ### Commands
* `generate` - The `tree-sitter generate` command will generate a Tree-sitter parser based on the grammar in the current * `generate` - The `tree-sitter generate` command will generate a Tree-sitter parser based on the grammar in the current working directory. See [the documentation] for more information.
working directory. See [the documentation] for more information.
* `test` - The `tree-sitter test` command will run the unit tests for the Tree-sitter parser in the current working directory. * `test` - The `tree-sitter test` command will run the unit tests for the Tree-sitter parser in the current working directory. See [the documentation] for more information.
See [the documentation] for more information.
* `parse` - The `tree-sitter parse` command will parse a file (or list of files) using Tree-sitter parsers. * `parse` - The `tree-sitter parse` command will parse a file (or list of files) using Tree-sitter parsers.

View file

@ -805,9 +805,9 @@
"peer": true "peer": true
}, },
"node_modules/js-yaml": { "node_modules/js-yaml": {
"version": "4.1.1", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"license": "MIT", "license": "MIT",
"peer": true, "peer": true,
"dependencies": { "dependencies": {

View file

@ -29,7 +29,6 @@ type Rule =
| PrecRule | PrecRule
| Repeat1Rule | Repeat1Rule
| RepeatRule | RepeatRule
| ReservedRule
| SeqRule | SeqRule
| StringRule | StringRule
| SymbolRule<string> | SymbolRule<string>

View file

@ -1,12 +1,12 @@
{ {
"name": "tree-sitter-cli", "name": "tree-sitter-cli",
"version": "0.27.0", "version": "0.26.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "tree-sitter-cli", "name": "tree-sitter-cli",
"version": "0.27.0", "version": "0.26.0",
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"bin": { "bin": {

View file

@ -1,6 +1,6 @@
{ {
"name": "tree-sitter-cli", "name": "tree-sitter-cli",
"version": "0.27.0", "version": "0.26.0",
"author": { "author": {
"name": "Max Brunsfeld", "name": "Max Brunsfeld",
"email": "maxbrunsfeld@gmail.com" "email": "maxbrunsfeld@gmail.com"

View file

@ -25,7 +25,7 @@ use crate::{
random::Rand, random::Rand,
}, },
parse::perform_edit, parse::perform_edit,
test::{parse_tests, strip_sexp_fields, DiffKey, TestDiff, TestEntry}, test::{parse_tests, print_diff, print_diff_key, strip_sexp_fields, TestEntry},
}; };
pub static LOG_ENABLED: LazyLock<bool> = LazyLock::new(|| env::var("TREE_SITTER_LOG").is_ok()); pub static LOG_ENABLED: LazyLock<bool> = LazyLock::new(|| env::var("TREE_SITTER_LOG").is_ok());
@ -183,8 +183,8 @@ pub fn fuzz_language_corpus(
if actual_output != test.output { if actual_output != test.output {
println!("Incorrect initial parse for {test_name}"); println!("Incorrect initial parse for {test_name}");
DiffKey::print(); print_diff_key();
println!("{}", TestDiff::new(&actual_output, &test.output)); print_diff(&actual_output, &test.output, true);
println!(); println!();
return false; return false;
} }
@ -276,8 +276,8 @@ pub fn fuzz_language_corpus(
if actual_output != test.output && !test.error { if actual_output != test.output && !test.error {
println!("Incorrect parse for {test_name} - seed {seed}"); println!("Incorrect parse for {test_name} - seed {seed}");
DiffKey::print(); print_diff_key();
println!("{}", TestDiff::new(&actual_output, &test.output)); print_diff(&actual_output, &test.output, true);
println!(); println!();
return false; return false;
} }

View file

@ -5,20 +5,14 @@ use std::{
}; };
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use crc32fast::hash as crc32;
use heck::{ToKebabCase, ToShoutySnakeCase, ToSnakeCase, ToUpperCamelCase}; use heck::{ToKebabCase, ToShoutySnakeCase, ToSnakeCase, ToUpperCamelCase};
use indoc::{formatdoc, indoc}; use indoc::{formatdoc, indoc};
use log::info; use log::warn;
use rand::{thread_rng, Rng};
use semver::Version; use semver::Version;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::{Map, Value}; use serde_json::{Map, Value};
use tree_sitter_generate::write_file; use tree_sitter_generate::write_file;
use tree_sitter_loader::{ use tree_sitter_loader::{Author, Bindings, Grammar, Links, Metadata, PathsJSON, TreeSitterJSON};
Author, Bindings, Grammar, Links, Metadata, PathsJSON, TreeSitterJSON,
DEFAULT_HIGHLIGHTS_QUERY_FILE_NAME, DEFAULT_INJECTIONS_QUERY_FILE_NAME,
DEFAULT_LOCALS_QUERY_FILE_NAME, DEFAULT_TAGS_QUERY_FILE_NAME,
};
const CLI_VERSION: &str = env!("CARGO_PKG_VERSION"); const CLI_VERSION: &str = env!("CARGO_PKG_VERSION");
const CLI_VERSION_PLACEHOLDER: &str = "CLI_VERSION"; const CLI_VERSION_PLACEHOLDER: &str = "CLI_VERSION";
@ -36,12 +30,9 @@ const PARSER_CLASS_NAME_PLACEHOLDER: &str = "PARSER_CLASS_NAME";
const PARSER_DESCRIPTION_PLACEHOLDER: &str = "PARSER_DESCRIPTION"; const PARSER_DESCRIPTION_PLACEHOLDER: &str = "PARSER_DESCRIPTION";
const PARSER_LICENSE_PLACEHOLDER: &str = "PARSER_LICENSE"; const PARSER_LICENSE_PLACEHOLDER: &str = "PARSER_LICENSE";
const PARSER_NS_PLACEHOLDER: &str = "PARSER_NS";
const PARSER_NS_CLEANED_PLACEHOLDER: &str = "PARSER_NS_CLEANED";
const PARSER_URL_PLACEHOLDER: &str = "PARSER_URL"; const PARSER_URL_PLACEHOLDER: &str = "PARSER_URL";
const PARSER_URL_STRIPPED_PLACEHOLDER: &str = "PARSER_URL_STRIPPED"; const PARSER_URL_STRIPPED_PLACEHOLDER: &str = "PARSER_URL_STRIPPED";
const PARSER_VERSION_PLACEHOLDER: &str = "PARSER_VERSION"; const PARSER_VERSION_PLACEHOLDER: &str = "PARSER_VERSION";
const PARSER_FINGERPRINT_PLACEHOLDER: &str = "PARSER_FINGERPRINT";
const AUTHOR_NAME_PLACEHOLDER: &str = "PARSER_AUTHOR_NAME"; const AUTHOR_NAME_PLACEHOLDER: &str = "PARSER_AUTHOR_NAME";
const AUTHOR_EMAIL_PLACEHOLDER: &str = "PARSER_AUTHOR_EMAIL"; const AUTHOR_EMAIL_PLACEHOLDER: &str = "PARSER_AUTHOR_EMAIL";
@ -60,22 +51,12 @@ const AUTHOR_BLOCK_RS: &str = "\nauthors = [";
const AUTHOR_NAME_PLACEHOLDER_RS: &str = "PARSER_AUTHOR_NAME"; const AUTHOR_NAME_PLACEHOLDER_RS: &str = "PARSER_AUTHOR_NAME";
const AUTHOR_EMAIL_PLACEHOLDER_RS: &str = " PARSER_AUTHOR_EMAIL"; const AUTHOR_EMAIL_PLACEHOLDER_RS: &str = " PARSER_AUTHOR_EMAIL";
const AUTHOR_BLOCK_JAVA: &str = "\n <developer>";
const AUTHOR_NAME_PLACEHOLDER_JAVA: &str = "\n <name>PARSER_AUTHOR_NAME</name>";
const AUTHOR_EMAIL_PLACEHOLDER_JAVA: &str = "\n <email>PARSER_AUTHOR_EMAIL</email>";
const AUTHOR_URL_PLACEHOLDER_JAVA: &str = "\n <url>PARSER_AUTHOR_URL</url>";
const AUTHOR_BLOCK_GRAMMAR: &str = "\n * @author "; const AUTHOR_BLOCK_GRAMMAR: &str = "\n * @author ";
const AUTHOR_NAME_PLACEHOLDER_GRAMMAR: &str = "PARSER_AUTHOR_NAME"; const AUTHOR_NAME_PLACEHOLDER_GRAMMAR: &str = "PARSER_AUTHOR_NAME";
const AUTHOR_EMAIL_PLACEHOLDER_GRAMMAR: &str = " PARSER_AUTHOR_EMAIL"; const AUTHOR_EMAIL_PLACEHOLDER_GRAMMAR: &str = " PARSER_AUTHOR_EMAIL";
const FUNDING_URL_PLACEHOLDER: &str = "FUNDING_URL"; const FUNDING_URL_PLACEHOLDER: &str = "FUNDING_URL";
const HIGHLIGHTS_QUERY_PATH_PLACEHOLDER: &str = "HIGHLIGHTS_QUERY_PATH";
const INJECTIONS_QUERY_PATH_PLACEHOLDER: &str = "INJECTIONS_QUERY_PATH";
const LOCALS_QUERY_PATH_PLACEHOLDER: &str = "LOCALS_QUERY_PATH";
const TAGS_QUERY_PATH_PLACEHOLDER: &str = "TAGS_QUERY_PATH";
const GRAMMAR_JS_TEMPLATE: &str = include_str!("./templates/grammar.js"); const GRAMMAR_JS_TEMPLATE: &str = include_str!("./templates/grammar.js");
const PACKAGE_JSON_TEMPLATE: &str = include_str!("./templates/package.json"); const PACKAGE_JSON_TEMPLATE: &str = include_str!("./templates/package.json");
const GITIGNORE_TEMPLATE: &str = include_str!("./templates/gitignore"); const GITIGNORE_TEMPLATE: &str = include_str!("./templates/gitignore");
@ -114,16 +95,12 @@ const TEST_BINDING_PY_TEMPLATE: &str = include_str!("./templates/test_binding.py
const PACKAGE_SWIFT_TEMPLATE: &str = include_str!("./templates/package.swift"); const PACKAGE_SWIFT_TEMPLATE: &str = include_str!("./templates/package.swift");
const TESTS_SWIFT_TEMPLATE: &str = include_str!("./templates/tests.swift"); const TESTS_SWIFT_TEMPLATE: &str = include_str!("./templates/tests.swift");
const POM_XML_TEMPLATE: &str = include_str!("./templates/pom.xml");
const BINDING_JAVA_TEMPLATE: &str = include_str!("./templates/binding.java");
const TEST_JAVA_TEMPLATE: &str = include_str!("./templates/test.java");
const BUILD_ZIG_TEMPLATE: &str = include_str!("./templates/build.zig"); const BUILD_ZIG_TEMPLATE: &str = include_str!("./templates/build.zig");
const BUILD_ZIG_ZON_TEMPLATE: &str = include_str!("./templates/build.zig.zon"); const BUILD_ZIG_ZON_TEMPLATE: &str = include_str!("./templates/build.zig.zon");
const ROOT_ZIG_TEMPLATE: &str = include_str!("./templates/root.zig"); const ROOT_ZIG_TEMPLATE: &str = include_str!("./templates/root.zig");
const TEST_ZIG_TEMPLATE: &str = include_str!("./templates/test.zig"); const TEST_ZIG_TEMPLATE: &str = include_str!("./templates/test.zig");
pub const TREE_SITTER_JSON_SCHEMA: &str = const TREE_SITTER_JSON_SCHEMA: &str =
"https://tree-sitter.github.io/tree-sitter/assets/schemas/config.schema.json"; "https://tree-sitter.github.io/tree-sitter/assets/schemas/config.schema.json";
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone)]
@ -145,7 +122,6 @@ pub struct JsonConfigOpts {
pub email: Option<String>, pub email: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>, pub url: Option<String>,
pub namespace: Option<String>,
pub bindings: Bindings, pub bindings: Bindings,
} }
@ -178,7 +154,7 @@ impl JsonConfigOpts {
authors: Some(vec![Author { authors: Some(vec![Author {
name: self.author, name: self.author,
email: self.email, email: self.email,
url: self.url, url: self.url.map(|url| url.to_string()),
}]), }]),
links: Some(Links { links: Some(Links {
repository: self.repository.unwrap_or_else(|| { repository: self.repository.unwrap_or_else(|| {
@ -186,7 +162,7 @@ impl JsonConfigOpts {
}), }),
funding: self.funding, funding: self.funding,
}), }),
namespace: self.namespace, namespace: None,
}, },
bindings: self.bindings, bindings: self.bindings,
} }
@ -209,7 +185,6 @@ impl Default for JsonConfigOpts {
author: String::new(), author: String::new(),
email: None, email: None,
url: None, url: None,
namespace: None,
bindings: Bindings::default(), bindings: Bindings::default(),
} }
} }
@ -227,11 +202,6 @@ struct GenerateOpts<'a> {
camel_parser_name: &'a str, camel_parser_name: &'a str,
title_parser_name: &'a str, title_parser_name: &'a str,
class_name: &'a str, class_name: &'a str,
highlights_query_path: &'a str,
injections_query_path: &'a str,
locals_query_path: &'a str,
tags_query_path: &'a str,
namespace: Option<&'a str>,
} }
pub fn generate_grammar_files( pub fn generate_grammar_files(
@ -282,11 +252,6 @@ pub fn generate_grammar_files(
.clone() .clone()
.unwrap_or_else(|| format!("TreeSitter{}", language_name.to_upper_camel_case())); .unwrap_or_else(|| format!("TreeSitter{}", language_name.to_upper_camel_case()));
let default_highlights_path = Path::new("queries").join(DEFAULT_HIGHLIGHTS_QUERY_FILE_NAME);
let default_injections_path = Path::new("queries").join(DEFAULT_INJECTIONS_QUERY_FILE_NAME);
let default_locals_path = Path::new("queries").join(DEFAULT_LOCALS_QUERY_FILE_NAME);
let default_tags_path = Path::new("queries").join(DEFAULT_TAGS_QUERY_FILE_NAME);
let generate_opts = GenerateOpts { let generate_opts = GenerateOpts {
author_name: authors author_name: authors
.map(|a| a.first().map(|a| a.name.as_str())) .map(|a| a.first().map(|a| a.name.as_str()))
@ -313,19 +278,6 @@ pub fn generate_grammar_files(
camel_parser_name: &camel_name, camel_parser_name: &camel_name,
title_parser_name: &title_name, title_parser_name: &title_name,
class_name: &class_name, class_name: &class_name,
highlights_query_path: tree_sitter_config.grammars[0]
.highlights
.to_variable_value(&default_highlights_path),
injections_query_path: tree_sitter_config.grammars[0]
.injections
.to_variable_value(&default_injections_path),
locals_query_path: tree_sitter_config.grammars[0]
.locals
.to_variable_value(&default_locals_path),
tags_query_path: tree_sitter_config.grammars[0]
.tags
.to_variable_value(&default_tags_path),
namespace: tree_sitter_config.metadata.namespace.as_deref(),
}; };
// Create package.json // Create package.json
@ -352,11 +304,11 @@ pub fn generate_grammar_files(
"tree-sitter-cli":"#}, "tree-sitter-cli":"#},
indoc! {r#" indoc! {r#"
"prebuildify": "^6.0.1", "prebuildify": "^6.0.1",
"tree-sitter": "^0.25.0", "tree-sitter": "^0.22.4",
"tree-sitter-cli":"#}, "tree-sitter-cli":"#},
); );
if !contents.contains("module") { if !contents.contains("module") {
info!("Migrating package.json to ESM"); warn!("Updating package.json");
contents = contents.replace( contents = contents.replace(
r#""repository":"#, r#""repository":"#,
indoc! {r#" indoc! {r#"
@ -378,7 +330,6 @@ pub fn generate_grammar_files(
|path| { |path| {
let mut contents = fs::read_to_string(path)?; let mut contents = fs::read_to_string(path)?;
if contents.contains("module.exports") { if contents.contains("module.exports") {
info!("Migrating grammars.js to ESM");
contents = contents.replace("module.exports =", "export default"); contents = contents.replace("module.exports =", "export default");
write_file(path, contents)?; write_file(path, contents)?;
} }
@ -394,16 +345,10 @@ pub fn generate_grammar_files(
allow_update, allow_update,
|path| generate_file(path, GITIGNORE_TEMPLATE, language_name, &generate_opts), |path| generate_file(path, GITIGNORE_TEMPLATE, language_name, &generate_opts),
|path| { |path| {
let mut contents = fs::read_to_string(path)?; let contents = fs::read_to_string(path)?;
if !contents.contains("Zig artifacts") { if !contents.contains("Zig artifacts") {
info!("Adding zig entries to .gitignore"); warn!("Replacing .gitignore");
contents.push('\n'); generate_file(path, GITIGNORE_TEMPLATE, language_name, &generate_opts)?;
contents.push_str(indoc! {"
# Zig artifacts
.zig-cache/
zig-cache/
zig-out/
"});
} }
Ok(()) Ok(())
}, },
@ -416,13 +361,8 @@ pub fn generate_grammar_files(
|path| generate_file(path, GITATTRIBUTES_TEMPLATE, language_name, &generate_opts), |path| generate_file(path, GITATTRIBUTES_TEMPLATE, language_name, &generate_opts),
|path| { |path| {
let mut contents = fs::read_to_string(path)?; let mut contents = fs::read_to_string(path)?;
let c_bindings_entry = "bindings/c/* "; contents = contents.replace("bindings/c/* ", "bindings/c/** ");
if contents.contains(c_bindings_entry) {
info!("Updating c bindings entry in .gitattributes");
contents = contents.replace(c_bindings_entry, "bindings/c/** ");
}
if !contents.contains("Zig bindings") { if !contents.contains("Zig bindings") {
info!("Adding zig entries to .gitattributes");
contents.push('\n'); contents.push('\n');
contents.push_str(indoc! {" contents.push_str(indoc! {"
# Zig bindings # Zig bindings
@ -445,48 +385,8 @@ pub fn generate_grammar_files(
// Generate Rust bindings // Generate Rust bindings
if tree_sitter_config.bindings.rust { if tree_sitter_config.bindings.rust {
missing_path(bindings_dir.join("rust"), create_dir)?.apply(|path| { missing_path(bindings_dir.join("rust"), create_dir)?.apply(|path| {
missing_path_else(path.join("lib.rs"), allow_update, |path| { missing_path(path.join("lib.rs"), |path| {
generate_file(path, LIB_RS_TEMPLATE, language_name, &generate_opts) generate_file(path, LIB_RS_TEMPLATE, language_name, &generate_opts)
}, |path| {
let mut contents = fs::read_to_string(path)?;
if !contents.contains("#[cfg(with_highlights_query)]") {
info!("Updating query constants in bindings/rust/lib.rs");
let replacement = indoc! {r#"
#[cfg(with_highlights_query)]
/// The syntax highlighting query for this grammar.
pub const HIGHLIGHTS_QUERY: &str = include_str!("../../HIGHLIGHTS_QUERY_PATH");
#[cfg(with_injections_query)]
/// The language injection query for this grammar.
pub const INJECTIONS_QUERY: &str = include_str!("../../INJECTIONS_QUERY_PATH");
#[cfg(with_locals_query)]
/// The local variable query for this grammar.
pub const LOCALS_QUERY: &str = include_str!("../../LOCALS_QUERY_PATH");
#[cfg(with_tags_query)]
/// The symbol tagging query for this grammar.
pub const TAGS_QUERY: &str = include_str!("../../TAGS_QUERY_PATH");
"#}
.replace("HIGHLIGHTS_QUERY_PATH", generate_opts.highlights_query_path)
.replace("INJECTIONS_QUERY_PATH", generate_opts.injections_query_path)
.replace("LOCALS_QUERY_PATH", generate_opts.locals_query_path)
.replace("TAGS_QUERY_PATH", generate_opts.tags_query_path);
contents = contents
.replace(
indoc! {r#"
// NOTE: uncomment these to include any queries that this grammar contains:
// pub const HIGHLIGHTS_QUERY: &str = include_str!("../../queries/highlights.scm");
// pub const INJECTIONS_QUERY: &str = include_str!("../../queries/injections.scm");
// pub const LOCALS_QUERY: &str = include_str!("../../queries/locals.scm");
// pub const TAGS_QUERY: &str = include_str!("../../queries/tags.scm");
"#},
&replacement,
);
}
write_file(path, contents)?;
Ok(())
})?; })?;
missing_path_else( missing_path_else(
@ -494,76 +394,37 @@ pub fn generate_grammar_files(
allow_update, allow_update,
|path| generate_file(path, BUILD_RS_TEMPLATE, language_name, &generate_opts), |path| generate_file(path, BUILD_RS_TEMPLATE, language_name, &generate_opts),
|path| { |path| {
let replacement = indoc!{r#"
c_config.flag("-utf-8");
if std::env::var("TARGET").unwrap() == "wasm32-unknown-unknown" {
let Ok(wasm_headers) = std::env::var("DEP_TREE_SITTER_LANGUAGE_WASM_HEADERS") else {
panic!("Environment variable DEP_TREE_SITTER_LANGUAGE_WASM_HEADERS must be set by the language crate");
};
let Ok(wasm_src) =
std::env::var("DEP_TREE_SITTER_LANGUAGE_WASM_SRC").map(std::path::PathBuf::from)
else {
panic!("Environment variable DEP_TREE_SITTER_LANGUAGE_WASM_SRC must be set by the language crate");
};
c_config.include(&wasm_headers);
c_config.files([
wasm_src.join("stdio.c"),
wasm_src.join("stdlib.c"),
wasm_src.join("string.c"),
]);
}
"#};
let indented_replacement = replacement
.lines()
.map(|line| if line.is_empty() { line.to_string() } else { format!(" {line}") })
.collect::<Vec<_>>()
.join("\n");
let mut contents = fs::read_to_string(path)?; let mut contents = fs::read_to_string(path)?;
if !contents.contains("wasm32-unknown-unknown") { if !contents.contains("wasm32-unknown-unknown") {
info!("Adding wasm32-unknown-unknown target to bindings/rust/build.rs"); contents = contents.replace(r#" c_config.flag("-utf-8");"#, &indented_replacement);
let replacement = indoc!{r#"
c_config.flag("-utf-8");
if std::env::var("TARGET").unwrap() == "wasm32-unknown-unknown" {
let Ok(wasm_headers) = std::env::var("DEP_TREE_SITTER_LANGUAGE_WASM_HEADERS") else {
panic!("Environment variable DEP_TREE_SITTER_LANGUAGE_WASM_HEADERS must be set by the language crate");
};
let Ok(wasm_src) =
std::env::var("DEP_TREE_SITTER_LANGUAGE_WASM_SRC").map(std::path::PathBuf::from)
else {
panic!("Environment variable DEP_TREE_SITTER_LANGUAGE_WASM_SRC must be set by the language crate");
};
c_config.include(&wasm_headers);
c_config.files([
wasm_src.join("stdio.c"),
wasm_src.join("stdlib.c"),
wasm_src.join("string.c"),
]);
}
"#}
.lines()
.map(|line| if line.is_empty() { line.to_string() } else { format!(" {line}") })
.collect::<Vec<_>>()
.join("\n");
contents = contents.replace(r#" c_config.flag("-utf-8");"#, &replacement);
}
// Introduce configuration variables for dynamic query inclusion
if !contents.contains("with_highlights_query") {
info!("Adding support for dynamic query inclusion to bindings/rust/build.rs");
let replaced = indoc! {r#"
c_config.compile("tree-sitter-KEBAB_PARSER_NAME");
}"#}
.replace("KEBAB_PARSER_NAME", &language_name.to_kebab_case());
let replacement = indoc! {r#"
c_config.compile("tree-sitter-KEBAB_PARSER_NAME");
println!("cargo:rustc-check-cfg=cfg(with_highlights_query)");
if !"HIGHLIGHTS_QUERY_PATH".is_empty() && std::path::Path::new("HIGHLIGHTS_QUERY_PATH").exists() {
println!("cargo:rustc-cfg=with_highlights_query");
}
println!("cargo:rustc-check-cfg=cfg(with_injections_query)");
if !"INJECTIONS_QUERY_PATH".is_empty() && std::path::Path::new("INJECTIONS_QUERY_PATH").exists() {
println!("cargo:rustc-cfg=with_injections_query");
}
println!("cargo:rustc-check-cfg=cfg(with_locals_query)");
if !"LOCALS_QUERY_PATH".is_empty() && std::path::Path::new("LOCALS_QUERY_PATH").exists() {
println!("cargo:rustc-cfg=with_locals_query");
}
println!("cargo:rustc-check-cfg=cfg(with_tags_query)");
if !"TAGS_QUERY_PATH".is_empty() && std::path::Path::new("TAGS_QUERY_PATH").exists() {
println!("cargo:rustc-cfg=with_tags_query");
}
}"#}
.replace("KEBAB_PARSER_NAME", &language_name.to_kebab_case())
.replace("HIGHLIGHTS_QUERY_PATH", generate_opts.highlights_query_path)
.replace("INJECTIONS_QUERY_PATH", generate_opts.injections_query_path)
.replace("LOCALS_QUERY_PATH", generate_opts.locals_query_path)
.replace("TAGS_QUERY_PATH", generate_opts.tags_query_path);
contents = contents.replace(
&replaced,
&replacement,
);
} }
write_file(path, contents)?; write_file(path, contents)?;
@ -585,7 +446,6 @@ pub fn generate_grammar_files(
|path| { |path| {
let contents = fs::read_to_string(path)?; let contents = fs::read_to_string(path)?;
if contents.contains("\"LICENSE\"") { if contents.contains("\"LICENSE\"") {
info!("Adding LICENSE entry to bindings/rust/Cargo.toml");
write_file(path, contents.replace("\"LICENSE\"", "\"/LICENSE\""))?; write_file(path, contents.replace("\"LICENSE\"", "\"/LICENSE\""))?;
} }
Ok(()) Ok(())
@ -605,27 +465,17 @@ pub fn generate_grammar_files(
|path| generate_file(path, INDEX_JS_TEMPLATE, language_name, &generate_opts), |path| generate_file(path, INDEX_JS_TEMPLATE, language_name, &generate_opts),
|path| { |path| {
let contents = fs::read_to_string(path)?; let contents = fs::read_to_string(path)?;
if !contents.contains("Object.defineProperty") { if !contents.contains("new URL") {
info!("Replacing index.js"); warn!("Replacing index.js");
generate_file(path, INDEX_JS_TEMPLATE, language_name, &generate_opts)?; generate_file(path, INDEX_JS_TEMPLATE, language_name, &generate_opts)?;
} }
Ok(()) Ok(())
}, },
)?; )?;
missing_path_else( missing_path(path.join("index.d.ts"), |path| {
path.join("index.d.ts"), generate_file(path, INDEX_D_TS_TEMPLATE, language_name, &generate_opts)
allow_update, })?;
|path| generate_file(path, INDEX_D_TS_TEMPLATE, language_name, &generate_opts),
|path| {
let contents = fs::read_to_string(path)?;
if !contents.contains("export default binding") {
info!("Replacing index.d.ts");
generate_file(path, INDEX_D_TS_TEMPLATE, language_name, &generate_opts)?;
}
Ok(())
},
)?;
missing_path_else( missing_path_else(
path.join("binding_test.js"), path.join("binding_test.js"),
@ -641,7 +491,7 @@ pub fn generate_grammar_files(
|path| { |path| {
let contents = fs::read_to_string(path)?; let contents = fs::read_to_string(path)?;
if !contents.contains("import") { if !contents.contains("import") {
info!("Replacing binding_test.js"); warn!("Replacing binding_test.js");
generate_file( generate_file(
path, path,
BINDING_TEST_JS_TEMPLATE, BINDING_TEST_JS_TEMPLATE,
@ -664,7 +514,6 @@ pub fn generate_grammar_files(
|path| { |path| {
let contents = fs::read_to_string(path)?; let contents = fs::read_to_string(path)?;
if contents.contains("fs.exists(") { if contents.contains("fs.exists(") {
info!("Replacing `fs.exists` calls in binding.gyp");
write_file(path, contents.replace("fs.exists(", "fs.existsSync("))?; write_file(path, contents.replace("fs.exists(", "fs.existsSync("))?;
} }
Ok(()) Ok(())
@ -677,17 +526,14 @@ pub fn generate_grammar_files(
// Generate C bindings // Generate C bindings
if tree_sitter_config.bindings.c { if tree_sitter_config.bindings.c {
let kebab_case_name = language_name.to_kebab_case();
missing_path(bindings_dir.join("c"), create_dir)?.apply(|path| { missing_path(bindings_dir.join("c"), create_dir)?.apply(|path| {
let header_name = format!("tree-sitter-{kebab_case_name}.h"); let old_file = &path.join(format!("tree-sitter-{}.h", language_name.to_kebab_case()));
let old_file = &path.join(&header_name);
if allow_update && fs::exists(old_file).unwrap_or(false) { if allow_update && fs::exists(old_file).unwrap_or(false) {
info!("Removing bindings/c/{header_name}");
fs::remove_file(old_file)?; fs::remove_file(old_file)?;
} }
missing_path(path.join("tree_sitter"), create_dir)?.apply(|include_path| { missing_path(path.join("tree_sitter"), create_dir)?.apply(|include_path| {
missing_path( missing_path(
include_path.join(&header_name), include_path.join(format!("tree-sitter-{}.h", language_name.to_kebab_case())),
|path| { |path| {
generate_file(path, PARSER_NAME_H_TEMPLATE, language_name, &generate_opts) generate_file(path, PARSER_NAME_H_TEMPLATE, language_name, &generate_opts)
}, },
@ -696,7 +542,7 @@ pub fn generate_grammar_files(
})?; })?;
missing_path( missing_path(
path.join(format!("tree-sitter-{kebab_case_name}.pc.in")), path.join(format!("tree-sitter-{}.pc.in", language_name.to_kebab_case())),
|path| { |path| {
generate_file( generate_file(
path, path,
@ -716,27 +562,23 @@ pub fn generate_grammar_files(
|path| { |path| {
let mut contents = fs::read_to_string(path)?; let mut contents = fs::read_to_string(path)?;
if !contents.contains("cd '$(DESTDIR)$(LIBDIR)' && ln -sf") { if !contents.contains("cd '$(DESTDIR)$(LIBDIR)' && ln -sf") {
info!("Replacing Makefile"); warn!("Replacing Makefile");
generate_file(path, MAKEFILE_TEMPLATE, language_name, &generate_opts)?; generate_file(path, MAKEFILE_TEMPLATE, language_name, &generate_opts)?;
} else { } else {
let replaced = indoc! {r" contents = contents
$(PARSER): $(SRC_DIR)/grammar.json .replace(
$(TS) generate $^ indoc! {r"
"}; $(PARSER): $(SRC_DIR)/grammar.json
if contents.contains(replaced) { $(TS) generate $^
info!("Adding --no-parser target to Makefile"); "},
contents = contents indoc! {r"
.replace( $(SRC_DIR)/grammar.json: grammar.js
replaced, $(TS) generate --emit=json $^
indoc! {r"
$(SRC_DIR)/grammar.json: grammar.js
$(TS) generate --no-parser $^
$(PARSER): $(SRC_DIR)/grammar.json $(PARSER): $(SRC_DIR)/grammar.json
$(TS) generate $^ $(TS) generate --emit=parser $^
"} "}
); );
}
write_file(path, contents)?; write_file(path, contents)?;
} }
Ok(()) Ok(())
@ -748,8 +590,8 @@ pub fn generate_grammar_files(
allow_update, allow_update,
|path| generate_file(path, CMAKELISTS_TXT_TEMPLATE, language_name, &generate_opts), |path| generate_file(path, CMAKELISTS_TXT_TEMPLATE, language_name, &generate_opts),
|path| { |path| {
let contents = fs::read_to_string(path)?; let mut contents = fs::read_to_string(path)?;
let replaced_contents = contents contents = contents
.replace("add_custom_target(test", "add_custom_target(ts-test") .replace("add_custom_target(test", "add_custom_target(ts-test")
.replace( .replace(
&formatdoc! {r#" &formatdoc! {r#"
@ -780,27 +622,21 @@ pub fn generate_grammar_files(
"#}, "#},
indoc! {r#" indoc! {r#"
add_custom_command(OUTPUT "${CMAKE_CURRENT_SOURCE_DIR}/src/grammar.json" add_custom_command(OUTPUT "${CMAKE_CURRENT_SOURCE_DIR}/src/grammar.json"
"${CMAKE_CURRENT_SOURCE_DIR}/src/node-types.json"
DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/grammar.js" DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/grammar.js"
COMMAND "${TREE_SITTER_CLI}" generate grammar.js --no-parser COMMAND "${TREE_SITTER_CLI}" generate grammar.js
--emit=json
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
COMMENT "Generating grammar.json") COMMENT "Generating grammar.json")
add_custom_command(OUTPUT "${CMAKE_CURRENT_SOURCE_DIR}/src/parser.c" add_custom_command(OUTPUT "${CMAKE_CURRENT_SOURCE_DIR}/src/parser.c"
BYPRODUCTS "${CMAKE_CURRENT_SOURCE_DIR}/src/tree_sitter/parser.h"
"${CMAKE_CURRENT_SOURCE_DIR}/src/tree_sitter/alloc.h"
"${CMAKE_CURRENT_SOURCE_DIR}/src/tree_sitter/array.h"
DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/grammar.json" DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/grammar.json"
COMMAND "${TREE_SITTER_CLI}" generate src/grammar.json COMMAND "${TREE_SITTER_CLI}" generate src/grammar.json
--abi=${TREE_SITTER_ABI_VERSION} --emit=parser --abi=${TREE_SITTER_ABI_VERSION}
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
COMMENT "Generating parser.c") COMMENT "Generating parser.c")
"#} "#}
); );
if !replaced_contents.eq(&contents) { write_file(path, contents)?;
info!("Updating CMakeLists.txt");
write_file(path, replaced_contents)?;
}
Ok(()) Ok(())
}, },
)?; )?;
@ -836,8 +672,7 @@ pub fn generate_grammar_files(
// Generate Python bindings // Generate Python bindings
if tree_sitter_config.bindings.python { if tree_sitter_config.bindings.python {
missing_path(bindings_dir.join("python"), create_dir)?.apply(|path| { missing_path(bindings_dir.join("python"), create_dir)?.apply(|path| {
let snake_case_grammar_name = format!("tree_sitter_{}", language_name.to_snake_case()); let lang_path = path.join(format!("tree_sitter_{}", language_name.to_snake_case()));
let lang_path = path.join(&snake_case_grammar_name);
missing_path(&lang_path, create_dir)?; missing_path(&lang_path, create_dir)?;
missing_path_else( missing_path_else(
@ -847,7 +682,6 @@ pub fn generate_grammar_files(
|path| { |path| {
let mut contents = fs::read_to_string(path)?; let mut contents = fs::read_to_string(path)?;
if !contents.contains("PyModuleDef_Init") { if !contents.contains("PyModuleDef_Init") {
info!("Updating bindings/python/{snake_case_grammar_name}/binding.c");
contents = contents contents = contents
.replace("PyModule_Create", "PyModuleDef_Init") .replace("PyModule_Create", "PyModuleDef_Init")
.replace( .replace(
@ -880,21 +714,9 @@ pub fn generate_grammar_files(
}, },
)?; )?;
missing_path_else( missing_path(lang_path.join("__init__.py"), |path| {
lang_path.join("__init__.py"), generate_file(path, INIT_PY_TEMPLATE, language_name, &generate_opts)
allow_update, })?;
|path| {
generate_file(path, INIT_PY_TEMPLATE, language_name, &generate_opts)
},
|path| {
let contents = fs::read_to_string(path)?;
if !contents.contains("uncomment these to include any queries") {
info!("Replacing __init__.py");
generate_file(path, INIT_PY_TEMPLATE, language_name, &generate_opts)?;
}
Ok(())
},
)?;
missing_path_else( missing_path_else(
lang_path.join("__init__.pyi"), lang_path.join("__init__.pyi"),
@ -902,11 +724,7 @@ pub fn generate_grammar_files(
|path| generate_file(path, INIT_PYI_TEMPLATE, language_name, &generate_opts), |path| generate_file(path, INIT_PYI_TEMPLATE, language_name, &generate_opts),
|path| { |path| {
let mut contents = fs::read_to_string(path)?; let mut contents = fs::read_to_string(path)?;
if contents.contains("uncomment these to include any queries") { if !contents.contains("CapsuleType") {
info!("Replacing __init__.pyi");
generate_file(path, INIT_PYI_TEMPLATE, language_name, &generate_opts)?;
} else if !contents.contains("CapsuleType") {
info!("Updating __init__.pyi");
contents = contents contents = contents
.replace( .replace(
"from typing import Final", "from typing import Final",
@ -938,7 +756,6 @@ pub fn generate_grammar_files(
|path| { |path| {
let mut contents = fs::read_to_string(path)?; let mut contents = fs::read_to_string(path)?;
if !contents.contains("Parser(Language(") { if !contents.contains("Parser(Language(") {
info!("Updating Language function in bindings/python/tests/test_binding.py");
contents = contents contents = contents
.replace("tree_sitter.Language(", "Parser(Language(") .replace("tree_sitter.Language(", "Parser(Language(")
.replace(".language())\n", ".language()))\n") .replace(".language())\n", ".language()))\n")
@ -959,19 +776,11 @@ pub fn generate_grammar_files(
allow_update, allow_update,
|path| generate_file(path, SETUP_PY_TEMPLATE, language_name, &generate_opts), |path| generate_file(path, SETUP_PY_TEMPLATE, language_name, &generate_opts),
|path| { |path| {
let mut contents = fs::read_to_string(path)?; let contents = fs::read_to_string(path)?;
if !contents.contains("build_ext") { if !contents.contains("build_ext") {
info!("Replacing setup.py"); warn!("Replacing setup.py");
generate_file(path, SETUP_PY_TEMPLATE, language_name, &generate_opts)?; generate_file(path, SETUP_PY_TEMPLATE, language_name, &generate_opts)?;
} }
if !contents.contains(" and not get_config_var") {
info!("Updating Python free-threading support in setup.py");
contents = contents.replace(
r#"startswith("cp"):"#,
r#"startswith("cp") and not get_config_var("Py_GIL_DISABLED"):"#
);
write_file(path, contents)?;
}
Ok(()) Ok(())
}, },
)?; )?;
@ -990,7 +799,6 @@ pub fn generate_grammar_files(
|path| { |path| {
let mut contents = fs::read_to_string(path)?; let mut contents = fs::read_to_string(path)?;
if !contents.contains("cp310-*") { if !contents.contains("cp310-*") {
info!("Updating dependencies in pyproject.toml");
contents = contents contents = contents
.replace(r#"build = "cp39-*""#, r#"build = "cp310-*""#) .replace(r#"build = "cp39-*""#, r#"build = "cp310-*""#)
.replace(r#"python = ">=3.9""#, r#"python = ">=3.10""#) .replace(r#"python = ">=3.9""#, r#"python = ">=3.10""#)
@ -1028,18 +836,15 @@ pub fn generate_grammar_files(
allow_update, allow_update,
|path| generate_file(path, PACKAGE_SWIFT_TEMPLATE, language_name, &generate_opts), |path| generate_file(path, PACKAGE_SWIFT_TEMPLATE, language_name, &generate_opts),
|path| { |path| {
let contents = fs::read_to_string(path)?; let mut contents = fs::read_to_string(path)?;
let replaced_contents = contents contents = contents
.replace( .replace(
"https://github.com/ChimeHQ/SwiftTreeSitter", "https://github.com/ChimeHQ/SwiftTreeSitter",
"https://github.com/tree-sitter/swift-tree-sitter", "https://github.com/tree-sitter/swift-tree-sitter",
) )
.replace("version: \"0.8.0\")", "version: \"0.9.0\")") .replace("version: \"0.8.0\")", "version: \"0.9.0\")")
.replace("(url:", "(name: \"SwiftTreeSitter\", url:"); .replace("(url:", "(name: \"SwiftTreeSitter\", url:");
if !replaced_contents.eq(&contents) { write_file(path, contents)?;
info!("Updating tree-sitter dependency in Package.swift");
write_file(path, contents)?;
}
Ok(()) Ok(())
}, },
)?; )?;
@ -1057,7 +862,7 @@ pub fn generate_grammar_files(
|path| { |path| {
let contents = fs::read_to_string(path)?; let contents = fs::read_to_string(path)?;
if !contents.contains("b.pkg_hash.len") { if !contents.contains("b.pkg_hash.len") {
info!("Replacing build.zig"); warn!("Replacing build.zig");
generate_file(path, BUILD_ZIG_TEMPLATE, language_name, &generate_opts) generate_file(path, BUILD_ZIG_TEMPLATE, language_name, &generate_opts)
} else { } else {
Ok(()) Ok(())
@ -1072,7 +877,7 @@ pub fn generate_grammar_files(
|path| { |path| {
let contents = fs::read_to_string(path)?; let contents = fs::read_to_string(path)?;
if !contents.contains(".name = .tree_sitter_") { if !contents.contains(".name = .tree_sitter_") {
info!("Replacing build.zig.zon"); warn!("Replacing build.zig.zon");
generate_file(path, BUILD_ZIG_ZON_TEMPLATE, language_name, &generate_opts) generate_file(path, BUILD_ZIG_ZON_TEMPLATE, language_name, &generate_opts)
} else { } else {
Ok(()) Ok(())
@ -1088,7 +893,7 @@ pub fn generate_grammar_files(
|path| { |path| {
let contents = fs::read_to_string(path)?; let contents = fs::read_to_string(path)?;
if contents.contains("ts.Language") { if contents.contains("ts.Language") {
info!("Replacing root.zig"); warn!("Replacing root.zig");
generate_file(path, ROOT_ZIG_TEMPLATE, language_name, &generate_opts) generate_file(path, ROOT_ZIG_TEMPLATE, language_name, &generate_opts)
} else { } else {
Ok(()) Ok(())
@ -1104,45 +909,6 @@ pub fn generate_grammar_files(
})?; })?;
} }
// Generate Java bindings
if tree_sitter_config.bindings.java {
missing_path(repo_path.join("pom.xml"), |path| {
generate_file(path, POM_XML_TEMPLATE, language_name, &generate_opts)
})?;
missing_path(bindings_dir.join("java"), create_dir)?.apply(|path| {
missing_path(path.join("main"), create_dir)?.apply(|path| {
let package_path = generate_opts
.namespace
.unwrap_or("io.github.treesitter")
.replace(['-', '_'], "")
.split('.')
.fold(path.to_path_buf(), |path, dir| path.join(dir))
.join("jtreesitter")
.join(language_name.to_lowercase().replace('_', ""));
missing_path(package_path, create_dir)?.apply(|path| {
missing_path(path.join(format!("{class_name}.java")), |path| {
generate_file(path, BINDING_JAVA_TEMPLATE, language_name, &generate_opts)
})?;
Ok(())
})?;
Ok(())
})?;
missing_path(path.join("test"), create_dir)?.apply(|path| {
missing_path(path.join(format!("{class_name}Test.java")), |path| {
generate_file(path, TEST_JAVA_TEMPLATE, language_name, &generate_opts)
})?;
Ok(())
})?;
Ok(())
})?;
}
Ok(()) Ok(())
} }
@ -1192,15 +958,6 @@ fn generate_file(
) -> Result<()> { ) -> Result<()> {
let filename = path.file_name().unwrap().to_str().unwrap(); let filename = path.file_name().unwrap().to_str().unwrap();
let lower_parser_name = if path
.extension()
.is_some_and(|e| e.eq_ignore_ascii_case("java"))
{
language_name.to_snake_case().replace('_', "")
} else {
language_name.to_snake_case()
};
let mut replacement = template let mut replacement = template
.replace( .replace(
CAMEL_PARSER_NAME_PLACEHOLDER, CAMEL_PARSER_NAME_PLACEHOLDER,
@ -1214,11 +971,14 @@ fn generate_file(
UPPER_PARSER_NAME_PLACEHOLDER, UPPER_PARSER_NAME_PLACEHOLDER,
&language_name.to_shouty_snake_case(), &language_name.to_shouty_snake_case(),
) )
.replace(
LOWER_PARSER_NAME_PLACEHOLDER,
&language_name.to_snake_case(),
)
.replace( .replace(
KEBAB_PARSER_NAME_PLACEHOLDER, KEBAB_PARSER_NAME_PLACEHOLDER,
&language_name.to_kebab_case(), &language_name.to_kebab_case(),
) )
.replace(LOWER_PARSER_NAME_PLACEHOLDER, &lower_parser_name)
.replace(PARSER_NAME_PLACEHOLDER, language_name) .replace(PARSER_NAME_PLACEHOLDER, language_name)
.replace(CLI_VERSION_PLACEHOLDER, CLI_VERSION) .replace(CLI_VERSION_PLACEHOLDER, CLI_VERSION)
.replace(RUST_BINDING_VERSION_PLACEHOLDER, RUST_BINDING_VERSION) .replace(RUST_BINDING_VERSION_PLACEHOLDER, RUST_BINDING_VERSION)
@ -1227,20 +987,7 @@ fn generate_file(
PARSER_VERSION_PLACEHOLDER, PARSER_VERSION_PLACEHOLDER,
&generate_opts.version.to_string(), &generate_opts.version.to_string(),
) )
.replace(PARSER_CLASS_NAME_PLACEHOLDER, generate_opts.class_name) .replace(PARSER_CLASS_NAME_PLACEHOLDER, generate_opts.class_name);
.replace(
HIGHLIGHTS_QUERY_PATH_PLACEHOLDER,
generate_opts.highlights_query_path,
)
.replace(
INJECTIONS_QUERY_PATH_PLACEHOLDER,
generate_opts.injections_query_path,
)
.replace(
LOCALS_QUERY_PATH_PLACEHOLDER,
generate_opts.locals_query_path,
)
.replace(TAGS_QUERY_PATH_PLACEHOLDER, generate_opts.tags_query_path);
if let Some(name) = generate_opts.author_name { if let Some(name) = generate_opts.author_name {
replacement = replacement.replace(AUTHOR_NAME_PLACEHOLDER, name); replacement = replacement.replace(AUTHOR_NAME_PLACEHOLDER, name);
@ -1258,9 +1005,6 @@ fn generate_file(
"Cargo.toml" => { "Cargo.toml" => {
replacement = replacement.replace(AUTHOR_NAME_PLACEHOLDER_RS, ""); replacement = replacement.replace(AUTHOR_NAME_PLACEHOLDER_RS, "");
} }
"pom.xml" => {
replacement = replacement.replace(AUTHOR_NAME_PLACEHOLDER_JAVA, "");
}
_ => {} _ => {}
} }
} }
@ -1286,52 +1030,30 @@ fn generate_file(
"Cargo.toml" => { "Cargo.toml" => {
replacement = replacement.replace(AUTHOR_EMAIL_PLACEHOLDER_RS, ""); replacement = replacement.replace(AUTHOR_EMAIL_PLACEHOLDER_RS, "");
} }
"pom.xml" => {
replacement = replacement.replace(AUTHOR_EMAIL_PLACEHOLDER_JAVA, "");
}
_ => {} _ => {}
} }
} }
match (generate_opts.author_url, filename) { if filename == "package.json" {
(Some(url), "package.json" | "pom.xml") => { if let Some(url) = generate_opts.author_url {
replacement = replacement.replace(AUTHOR_URL_PLACEHOLDER, url); replacement = replacement.replace(AUTHOR_URL_PLACEHOLDER, url);
} } else {
(None, "package.json") => {
replacement = replacement.replace(AUTHOR_URL_PLACEHOLDER_JS, ""); replacement = replacement.replace(AUTHOR_URL_PLACEHOLDER_JS, "");
} }
(None, "pom.xml") => {
replacement = replacement.replace(AUTHOR_URL_PLACEHOLDER_JAVA, "");
}
_ => {}
} }
if generate_opts.author_name.is_none() if generate_opts.author_name.is_none()
&& generate_opts.author_email.is_none() && generate_opts.author_email.is_none()
&& generate_opts.author_url.is_none() && generate_opts.author_url.is_none()
&& filename == "package.json"
{ {
match filename { if let Some(start_idx) = replacement.find(AUTHOR_BLOCK_JS) {
"package.json" => { if let Some(end_idx) = replacement[start_idx..]
if let Some(start_idx) = replacement.find(AUTHOR_BLOCK_JS) { .find("},")
if let Some(end_idx) = replacement[start_idx..] .map(|i| i + start_idx + 2)
.find("},") {
.map(|i| i + start_idx + 2) replacement.replace_range(start_idx..end_idx, "");
{
replacement.replace_range(start_idx..end_idx, "");
}
}
} }
"pom.xml" => {
if let Some(start_idx) = replacement.find(AUTHOR_BLOCK_JAVA) {
if let Some(end_idx) = replacement[start_idx..]
.find("</developer>")
.map(|i| i + start_idx + 12)
{
replacement.replace_range(start_idx..end_idx, "");
}
}
}
_ => {}
} }
} else if generate_opts.author_name.is_none() && generate_opts.author_email.is_none() { } else if generate_opts.author_name.is_none() && generate_opts.author_email.is_none() {
match filename { match filename {
@ -1412,19 +1134,6 @@ fn generate_file(
); );
} }
if let Some(namespace) = generate_opts.namespace {
replacement = replacement
.replace(
PARSER_NS_CLEANED_PLACEHOLDER,
&namespace.replace(['-', '_'], ""),
)
.replace(PARSER_NS_PLACEHOLDER, namespace);
} else {
replacement = replacement
.replace(PARSER_NS_CLEANED_PLACEHOLDER, "io.github.treesitter")
.replace(PARSER_NS_PLACEHOLDER, "io.github.tree-sitter");
}
if let Some(funding_url) = generate_opts.funding { if let Some(funding_url) = generate_opts.funding {
match filename { match filename {
"pyproject.toml" | "package.json" => { "pyproject.toml" | "package.json" => {
@ -1444,18 +1153,6 @@ fn generate_file(
} }
} }
if filename == "build.zig.zon" {
let id = thread_rng().gen_range(1u32..0xFFFF_FFFFu32);
let checksum = crc32(format!("tree_sitter_{language_name}").as_bytes());
replacement = replacement.replace(
PARSER_FINGERPRINT_PLACEHOLDER,
#[cfg(target_endian = "little")]
&format!("0x{checksum:x}{id:x}"),
#[cfg(target_endian = "big")]
&format!("0x{id:x}{checksum:x}"),
);
}
write_file(path, replacement)?; write_file(path, replacement)?;
Ok(()) Ok(())
} }

View file

@ -20,20 +20,18 @@ use tree_sitter_cli::{
LOG_GRAPH_ENABLED, START_SEED, LOG_GRAPH_ENABLED, START_SEED,
}, },
highlight::{self, HighlightOptions}, highlight::{self, HighlightOptions},
init::{generate_grammar_files, JsonConfigOpts, TREE_SITTER_JSON_SCHEMA}, init::{generate_grammar_files, JsonConfigOpts},
input::{get_input, get_tmp_source_file, CliInput}, input::{get_input, get_tmp_source_file, CliInput},
logger, logger,
parse::{self, ParseDebugType, ParseFileOptions, ParseOutput, ParseTheme}, parse::{self, ParseDebugType, ParseFileOptions, ParseOutput, ParseTheme},
playground, playground, query,
query::{self, QueryFileOptions},
tags::{self, TagsOptions}, tags::{self, TagsOptions},
test::{self, TestOptions, TestStats, TestSummary}, test::{self, TestOptions, TestStats},
test_highlight, test_tags, util, test_highlight, test_tags, util, version,
version::{self, BumpLevel}, version::BumpLevel,
wasm, wasm,
}; };
use tree_sitter_config::Config; use tree_sitter_config::Config;
use tree_sitter_generate::OptLevel;
use tree_sitter_highlight::Highlighter; use tree_sitter_highlight::Highlighter;
use tree_sitter_loader::{self as loader, Bindings, TreeSitterJSON}; use tree_sitter_loader::{self as loader, Bindings, TreeSitterJSON};
use tree_sitter_tags::TagsContext; use tree_sitter_tags::TagsContext;
@ -89,6 +87,17 @@ struct Init {
pub grammar_path: Option<PathBuf>, pub grammar_path: Option<PathBuf>,
} }
#[derive(Clone, Debug, Default, ValueEnum, PartialEq, Eq)]
enum GenerationEmit {
/// Generate `grammar.json` and `node-types.json`
Json,
/// Generate `parser.c` and related files
#[default]
Parser,
/// Compile to a library
Lib,
}
#[derive(Args)] #[derive(Args)]
#[command(alias = "gen", alias = "g")] #[command(alias = "gen", alias = "g")]
struct Generate { struct Generate {
@ -111,38 +120,28 @@ struct Generate {
) )
)] )]
pub abi_version: Option<String>, pub abi_version: Option<String>,
/// Only generate `grammar.json` and `node-types.json` /// What generated files to emit
#[arg(long)] #[arg(long)]
pub no_parser: bool, #[clap(value_enum, default_value_t=GenerationEmit::Parser)]
/// Deprecated: use the `build` command pub emit: GenerationEmit,
#[arg(long, short = 'b')] /// Deprecated: use --emit=lib.
#[arg(long, short = 'b', conflicts_with = "emit")]
pub build: bool, pub build: bool,
/// Deprecated: use the `build` command /// Compile a parser in debug mode
#[arg(long, short = '0')] #[arg(long, short = '0')]
pub debug_build: bool, pub debug_build: bool,
/// Deprecated: use the `build` command /// The path to the directory containing the parser library
#[arg(long, value_name = "PATH")] #[arg(long, value_name = "PATH")]
pub libdir: Option<PathBuf>, pub libdir: Option<PathBuf>,
/// The path to output the generated source files /// The path to output the generated source files
#[arg(long, short, value_name = "DIRECTORY")] #[arg(long, short, value_name = "DIRECTORY")]
pub output: Option<PathBuf>, pub output: Option<PathBuf>,
/// Produce a report of the states for the given rule, use `-` to report every rule /// Produce a report of the states for the given rule, use `-` to report every rule
#[arg(long, conflicts_with = "json", conflicts_with = "json_summary")] #[arg(long)]
pub report_states_for_rule: Option<String>, pub report_states_for_rule: Option<String>,
/// Deprecated: use --json-summary
#[arg(
long,
conflicts_with = "json_summary",
conflicts_with = "report_states_for_rule"
)]
pub json: bool,
/// Report conflicts in a JSON format /// Report conflicts in a JSON format
#[arg( #[arg(long)]
long, pub json: bool,
conflicts_with = "json",
conflicts_with = "report_states_for_rule"
)]
pub json_summary: bool,
/// The name or path of the JavaScript runtime to use for generating parsers /// The name or path of the JavaScript runtime to use for generating parsers
#[cfg(not(feature = "qjs-rt"))] #[cfg(not(feature = "qjs-rt"))]
#[arg( #[arg(
@ -163,11 +162,6 @@ struct Generate {
/// The name or path of the JavaScript runtime to use for generating parsers, specify `native` /// The name or path of the JavaScript runtime to use for generating parsers, specify `native`
/// to use the native `QuickJS` runtime /// to use the native `QuickJS` runtime
pub js_runtime: Option<String>, pub js_runtime: Option<String>,
/// Disable optimizations when generating the parser. Currently, this only affects
/// the merging of compatible parse states.
#[arg(long)]
pub disable_optimizations: bool,
} }
#[derive(Args)] #[derive(Args)]
@ -223,7 +217,7 @@ struct Parse {
#[arg(long, short = 'D')] #[arg(long, short = 'D')]
pub debug_graph: bool, pub debug_graph: bool,
/// Compile parsers to Wasm instead of native dynamic libraries /// Compile parsers to Wasm instead of native dynamic libraries
#[arg(long, hide = cfg!(not(feature = "wasm")))] #[arg(long)]
pub wasm: bool, pub wasm: bool,
/// Output the parse data with graphviz dot /// Output the parse data with graphviz dot
#[arg(long = "dot")] #[arg(long = "dot")]
@ -235,7 +229,7 @@ struct Parse {
#[arg(long = "cst", short = 'c')] #[arg(long = "cst", short = 'c')]
pub output_cst: bool, pub output_cst: bool,
/// Show parsing statistic /// Show parsing statistic
#[arg(long, short, conflicts_with = "json", conflicts_with = "json_summary")] #[arg(long, short)]
pub stat: bool, pub stat: bool,
/// Interrupt the parsing process by timeout (µs) /// Interrupt the parsing process by timeout (µs)
#[arg(long)] #[arg(long)]
@ -260,12 +254,9 @@ struct Parse {
/// Open `log.html` in the default browser, if `--debug-graph` is supplied /// Open `log.html` in the default browser, if `--debug-graph` is supplied
#[arg(long)] #[arg(long)]
pub open_log: bool, pub open_log: bool,
/// Deprecated: use --json-summary
#[arg(long, conflicts_with = "json_summary", conflicts_with = "stat")]
pub json: bool,
/// Output parsing results in a JSON format /// Output parsing results in a JSON format
#[arg(long, short = 'j', conflicts_with = "json", conflicts_with = "stat")] #[arg(long, short = 'j')]
pub json_summary: bool, pub json: bool,
/// The path to an alternative config.json file /// The path to an alternative config.json file
#[arg(long)] #[arg(long)]
pub config_path: Option<PathBuf>, pub config_path: Option<PathBuf>,
@ -323,7 +314,7 @@ struct Test {
#[arg(long, short = 'D')] #[arg(long, short = 'D')]
pub debug_graph: bool, pub debug_graph: bool,
/// Compile parsers to Wasm instead of native dynamic libraries /// Compile parsers to Wasm instead of native dynamic libraries
#[arg(long, hide = cfg!(not(feature = "wasm")))] #[arg(long)]
pub wasm: bool, pub wasm: bool,
/// Open `log.html` in the default browser, if `--debug-graph` is supplied /// Open `log.html` in the default browser, if `--debug-graph` is supplied
#[arg(long)] #[arg(long)]
@ -343,9 +334,6 @@ struct Test {
/// Show only the pass-fail overview tree /// Show only the pass-fail overview tree
#[arg(long)] #[arg(long)]
pub overview_only: bool, pub overview_only: bool,
/// Output the test summary in a JSON format
#[arg(long)]
pub json_summary: bool,
} }
#[derive(Args)] #[derive(Args)]
@ -448,14 +436,6 @@ struct Query {
/// The range of rows in which the query will be executed /// The range of rows in which the query will be executed
#[arg(long)] #[arg(long)]
pub row_range: Option<String>, pub row_range: Option<String>,
/// The range of byte offsets in which the query will be executed. Only the matches that are fully contained within the provided
/// byte range will be returned.
#[arg(long)]
pub containing_byte_range: Option<String>,
/// The range of rows in which the query will be executed. Only the matches that are fully contained within the provided row range
/// will be returned.
#[arg(long)]
pub containing_row_range: Option<String>,
/// Select a language by the scope instead of a file extension /// Select a language by the scope instead of a file extension
#[arg(long)] #[arg(long)]
pub scope: Option<String>, pub scope: Option<String>,
@ -597,20 +577,6 @@ pub enum Shell {
Nushell, Nushell,
} }
/// Complete `action` if the wasm feature is enabled, otherwise return an error
macro_rules! checked_wasm {
($action:block) => {
#[cfg(feature = "wasm")]
{
$action
}
#[cfg(not(feature = "wasm"))]
{
Err(anyhow!("--wasm flag specified, but this build of tree-sitter-cli does not include the wasm feature"))?;
}
};
}
impl InitConfig { impl InitConfig {
fn run() -> Result<()> { fn run() -> Result<()> {
if let Ok(Some(config_path)) = Config::find_config_file() { if let Ok(Some(config_path)) = Config::find_config_file() {
@ -772,14 +738,6 @@ impl Init {
.map(|e| Some(e.trim().to_string())) .map(|e| Some(e.trim().to_string()))
}; };
let namespace = || {
Input::<String>::with_theme(&ColorfulTheme::default())
.with_prompt("Package namespace")
.default("io.github.tree-sitter".to_string())
.allow_empty(true)
.interact()
};
let bindings = || { let bindings = || {
let languages = Bindings::default().languages(); let languages = Bindings::default().languages();
@ -809,7 +767,6 @@ impl Init {
"author", "author",
"email", "email",
"url", "url",
"namespace",
"bindings", "bindings",
"exit", "exit",
]; ];
@ -830,7 +787,6 @@ impl Init {
"author" => opts.author = author()?, "author" => opts.author = author()?,
"email" => opts.email = email()?, "email" => opts.email = email()?,
"url" => opts.url = url()?, "url" => opts.url = url()?,
"namespace" => opts.namespace = Some(namespace()?),
"bindings" => opts.bindings = bindings()?, "bindings" => opts.bindings = bindings()?,
"exit" => break, "exit" => break,
_ => unreachable!(), _ => unreachable!(),
@ -867,26 +823,10 @@ impl Init {
(opts.name.clone(), Some(opts)) (opts.name.clone(), Some(opts))
} else { } else {
let old_config = fs::read_to_string(current_dir.join("tree-sitter.json")) let mut json = serde_json::from_str::<TreeSitterJSON>(
.with_context(|| "Failed to read tree-sitter.json")?; &fs::read_to_string(current_dir.join("tree-sitter.json"))
.with_context(|| "Failed to read tree-sitter.json")?,
let mut json = serde_json::from_str::<TreeSitterJSON>(&old_config)?; )?;
if json.schema.is_none() {
json.schema = Some(TREE_SITTER_JSON_SCHEMA.to_string());
}
let new_config = format!("{}\n", serde_json::to_string_pretty(&json)?);
// Write the re-serialized config back, as newly added optional boolean fields
// will be included with explicit `false`s rather than implict `null`s
if self.update && !old_config.trim().eq(new_config.trim()) {
info!("Updating tree-sitter.json");
fs::write(
current_dir.join("tree-sitter.json"),
serde_json::to_string_pretty(&json)?,
)
.with_context(|| "Failed to write tree-sitter.json")?;
}
(json.grammars.swap_remove(0).name, None) (json.grammars.swap_remove(0).name, None)
}; };
@ -916,13 +856,9 @@ impl Generate {
version.parse().expect("invalid abi version flag") version.parse().expect("invalid abi version flag")
} }
}); });
if self.build {
let json_summary = if self.json { warn!("--build is deprecated, use --emit=lib instead");
warn!("--json is deprecated, use --json-summary instead"); }
true
} else {
self.json_summary
};
if let Err(err) = tree_sitter_generate::generate_parser_in_directory( if let Err(err) = tree_sitter_generate::generate_parser_in_directory(
current_dir, current_dir,
@ -931,14 +867,9 @@ impl Generate {
abi_version, abi_version,
self.report_states_for_rule.as_deref(), self.report_states_for_rule.as_deref(),
self.js_runtime.as_deref(), self.js_runtime.as_deref(),
!self.no_parser, self.emit != GenerationEmit::Json,
if self.disable_optimizations {
OptLevel::empty()
} else {
OptLevel::default()
},
) { ) {
if json_summary { if self.json {
eprintln!("{}", serde_json::to_string_pretty(&err)?); eprintln!("{}", serde_json::to_string_pretty(&err)?);
// Exit early to prevent errors from being printed a second time in the caller // Exit early to prevent errors from being printed a second time in the caller
std::process::exit(1); std::process::exit(1);
@ -947,8 +878,7 @@ impl Generate {
Err(anyhow!(err.to_string())).with_context(|| "Error when generating parser")?; Err(anyhow!(err.to_string())).with_context(|| "Error when generating parser")?;
} }
} }
if self.build { if self.emit == GenerationEmit::Lib || self.build {
warn!("--build is deprecated, use the `build` command");
if let Some(path) = self.libdir { if let Some(path) = self.libdir {
loader = loader::Loader::with_parser_lib_path(path); loader = loader::Loader::with_parser_lib_path(path);
} }
@ -971,21 +901,11 @@ impl Build {
} else { } else {
let output_path = if let Some(ref path) = self.output { let output_path = if let Some(ref path) = self.output {
let path = Path::new(path); let path = Path::new(path);
let full_path = if path.is_absolute() { if path.is_absolute() {
path.to_path_buf() path.to_path_buf()
} else { } else {
current_dir.join(path) current_dir.join(path)
}; }
let parent_path = full_path
.parent()
.context("Output path must have a parent")?;
let name = full_path
.file_name()
.context("Ouput path must have a filename")?;
fs::create_dir_all(parent_path).context("Failed to create output path")?;
let mut canon_path = parent_path.canonicalize().context("Invalid output path")?;
canon_path.push(name);
canon_path
} else { } else {
let file_name = grammar_path let file_name = grammar_path
.file_stem() .file_stem()
@ -1008,9 +928,12 @@ impl Build {
loader.force_rebuild(true); loader.force_rebuild(true);
let config = Config::load(None)?;
let loader_config = config.get()?;
loader.find_all_languages(&loader_config).unwrap();
loader loader
.compile_parser_at_path(&grammar_path, output_path, flags) .compile_parser_at_path(&grammar_path, output_path, flags)
.context("Failed to compile parser")?; .unwrap();
} }
Ok(()) Ok(())
} }
@ -1020,19 +943,13 @@ impl Parse {
fn run(self, mut loader: loader::Loader, current_dir: &Path) -> Result<()> { fn run(self, mut loader: loader::Loader, current_dir: &Path) -> Result<()> {
let config = Config::load(self.config_path)?; let config = Config::load(self.config_path)?;
let color = env::var("NO_COLOR").map_or(true, |v| v != "1"); let color = env::var("NO_COLOR").map_or(true, |v| v != "1");
let json_summary = if self.json {
warn!("--json is deprecated, use --json-summary instead");
true
} else {
self.json_summary
};
let output = if self.output_dot { let output = if self.output_dot {
ParseOutput::Dot ParseOutput::Dot
} else if self.output_xml { } else if self.output_xml {
ParseOutput::Xml ParseOutput::Xml
} else if self.output_cst { } else if self.output_cst {
ParseOutput::Cst ParseOutput::Cst
} else if self.quiet || json_summary { } else if self.quiet || self.json {
ParseOutput::Quiet ParseOutput::Quiet
} else { } else {
ParseOutput::Normal ParseOutput::Normal
@ -1063,14 +980,13 @@ impl Parse {
loader.debug_build(self.debug_build); loader.debug_build(self.debug_build);
loader.force_rebuild(self.rebuild || self.grammar_path.is_some()); loader.force_rebuild(self.rebuild || self.grammar_path.is_some());
#[cfg(feature = "wasm")]
if self.wasm { if self.wasm {
checked_wasm!({ let engine = tree_sitter::wasmtime::Engine::default();
let engine = tree_sitter::wasmtime::Engine::default(); parser
parser .set_wasm_store(tree_sitter::WasmStore::new(&engine).unwrap())
.set_wasm_store(tree_sitter::WasmStore::new(&engine).unwrap()) .unwrap();
.unwrap(); loader.use_wasm(&engine);
loader.use_wasm(&engine);
});
} }
let timeout = self.timeout.unwrap_or_default(); let timeout = self.timeout.unwrap_or_default();
@ -1107,7 +1023,7 @@ impl Parse {
let mut update_stats = |stats: &mut parse::ParseStats| { let mut update_stats = |stats: &mut parse::ParseStats| {
let parse_result = stats.parse_summaries.last().unwrap(); let parse_result = stats.parse_summaries.last().unwrap();
if should_track_stats || json_summary { if should_track_stats {
stats.cumulative_stats.total_parses += 1; stats.cumulative_stats.total_parses += 1;
if parse_result.successful { if parse_result.successful {
stats.cumulative_stats.successful_parses += 1; stats.cumulative_stats.successful_parses += 1;
@ -1139,19 +1055,18 @@ impl Parse {
.map(|p| p.to_string_lossy().chars().count()) .map(|p| p.to_string_lossy().chars().count())
.max() .max()
.unwrap_or(0); .unwrap_or(0);
options.stats.source_count = paths.len();
for path in &paths { for path in &paths {
let path = Path::new(&path); let path = Path::new(&path);
let language = loader let language = loader
.select_language( .select_language(
Some(path), path,
current_dir, current_dir,
self.scope.as_deref(), self.scope.as_deref(),
lib_info.as_ref(), lib_info.as_ref(),
) )
.with_context(|| { .with_context(|| {
anyhow!("Failed to load language for path \"{}\"", path.display()) anyhow!("Failed to load langauge for path \"{}\"", path.display())
})?; })?;
parse::parse_file_at_path( parse::parse_file_at_path(
@ -1176,12 +1091,7 @@ impl Parse {
let language = if let Some(ref lib_path) = self.lib_path { let language = if let Some(ref lib_path) = self.lib_path {
&loader &loader
.select_language( .select_language(lib_path, current_dir, None, lib_info.as_ref())
None,
current_dir,
self.scope.as_deref(),
lib_info.as_ref(),
)
.with_context(|| { .with_context(|| {
anyhow!( anyhow!(
"Failed to load language for path \"{}\"", "Failed to load language for path \"{}\"",
@ -1215,12 +1125,8 @@ impl Parse {
let path = get_tmp_source_file(&contents)?; let path = get_tmp_source_file(&contents)?;
let name = "stdin"; let name = "stdin";
let language = loader.select_language( let language =
None, loader.select_language(&path, current_dir, None, lib_info.as_ref())?;
current_dir,
self.scope.as_deref(),
lib_info.as_ref(),
)?;
parse::parse_file_at_path( parse::parse_file_at_path(
&mut parser, &mut parser,
@ -1238,7 +1144,7 @@ impl Parse {
if should_track_stats { if should_track_stats {
println!("\n{}", stats.cumulative_stats); println!("\n{}", stats.cumulative_stats);
} }
if json_summary { if self.json {
println!("{}", serde_json::to_string_pretty(&stats)?); println!("{}", serde_json::to_string_pretty(&stats)?);
} }
@ -1250,28 +1156,6 @@ impl Parse {
} }
} }
/// In case an error is encountered, prints out the contents of `test_summary` and
/// propagates the error
fn check_test(
test_result: Result<()>,
test_summary: &TestSummary,
json_summary: bool,
) -> Result<()> {
if let Err(e) = test_result {
if json_summary {
let json_summary = serde_json::to_string_pretty(test_summary)
.expect("Failed to encode summary to JSON");
println!("{json_summary}");
} else {
println!("{test_summary}");
}
Err(e)?;
}
Ok(())
}
impl Test { impl Test {
fn run(self, mut loader: loader::Loader, current_dir: &Path) -> Result<()> { fn run(self, mut loader: loader::Loader, current_dir: &Path) -> Result<()> {
let config = Config::load(self.config_path)?; let config = Config::load(self.config_path)?;
@ -1283,14 +1167,13 @@ impl Test {
let mut parser = Parser::new(); let mut parser = Parser::new();
#[cfg(feature = "wasm")]
if self.wasm { if self.wasm {
checked_wasm!({ let engine = tree_sitter::wasmtime::Engine::default();
let engine = tree_sitter::wasmtime::Engine::default(); parser
parser .set_wasm_store(tree_sitter::WasmStore::new(&engine).unwrap())
.set_wasm_store(tree_sitter::WasmStore::new(&engine).unwrap()) .unwrap();
.unwrap(); loader.use_wasm(&engine);
loader.use_wasm(&engine);
});
} }
if self.lib_path.is_none() && self.lang_name.is_some() { if self.lib_path.is_none() && self.lang_name.is_some() {
@ -1301,7 +1184,7 @@ impl Test {
let lib_info = let lib_info =
get_lib_info(self.lib_path.as_ref(), self.lang_name.as_ref(), current_dir); get_lib_info(self.lib_path.as_ref(), self.lang_name.as_ref(), current_dir);
&loader &loader
.select_language(None, current_dir, None, lib_info.as_ref()) .select_language(lib_path, current_dir, None, lib_info.as_ref())
.with_context(|| { .with_context(|| {
anyhow!( anyhow!(
"Failed to load language for path \"{}\"", "Failed to load language for path \"{}\"",
@ -1317,18 +1200,15 @@ impl Test {
parser.set_language(language)?; parser.set_language(language)?;
let test_dir = current_dir.join("test"); let test_dir = current_dir.join("test");
let mut test_summary = TestSummary::new( let mut stats = parse::Stats::default();
color,
stat,
self.update,
self.overview_only,
self.json_summary,
);
// Run the corpus tests. Look for them in `test/corpus`. // Run the corpus tests. Look for them in `test/corpus`.
let test_corpus_dir = test_dir.join("corpus"); let test_corpus_dir = test_dir.join("corpus");
if test_corpus_dir.is_dir() { if test_corpus_dir.is_dir() {
let opts = TestOptions { let mut output = String::new();
let mut rates = Vec::new();
let mut opts = TestOptions {
output: &mut output,
path: test_corpus_dir, path: test_corpus_dir,
debug: self.debug, debug: self.debug,
debug_graph: self.debug_graph, debug_graph: self.debug_graph,
@ -1339,67 +1219,51 @@ impl Test {
open_log: self.open_log, open_log: self.open_log,
languages: languages.iter().map(|(l, n)| (n.as_str(), l)).collect(), languages: languages.iter().map(|(l, n)| (n.as_str(), l)).collect(),
color, color,
test_num: 1,
parse_rates: &mut rates,
stat_display: stat,
stats: &mut stats,
show_fields: self.show_fields, show_fields: self.show_fields,
overview_only: self.overview_only, overview_only: self.overview_only,
}; };
check_test( test::run_tests_at_path(&mut parser, &mut opts)?;
test::run_tests_at_path(&mut parser, &opts, &mut test_summary), println!("\n{stats}");
&test_summary,
self.json_summary,
)?;
test_summary.test_num = 1;
} }
// Check that all of the queries are valid. // Check that all of the queries are valid.
let query_dir = current_dir.join("queries"); test::check_queries_at_path(language, &current_dir.join("queries"))?;
check_test(
test::check_queries_at_path(language, &query_dir),
&test_summary,
self.json_summary,
)?;
test_summary.test_num = 1;
// Run the syntax highlighting tests. // Run the syntax highlighting tests.
let test_highlight_dir = test_dir.join("highlight"); let test_highlight_dir = test_dir.join("highlight");
if test_highlight_dir.is_dir() { if test_highlight_dir.is_dir() {
let mut highlighter = Highlighter::new(); let mut highlighter = Highlighter::new();
highlighter.parser = parser; highlighter.parser = parser;
check_test( test_highlight::test_highlights(
test_highlight::test_highlights( &loader,
&loader, &config.get()?,
&config.get()?, &mut highlighter,
&mut highlighter, &test_highlight_dir,
&test_highlight_dir, color,
&mut test_summary,
),
&test_summary,
self.json_summary,
)?; )?;
parser = highlighter.parser; parser = highlighter.parser;
test_summary.test_num = 1;
} }
let test_tag_dir = test_dir.join("tags"); let test_tag_dir = test_dir.join("tags");
if test_tag_dir.is_dir() { if test_tag_dir.is_dir() {
let mut tags_context = TagsContext::new(); let mut tags_context = TagsContext::new();
tags_context.parser = parser; tags_context.parser = parser;
check_test( test_tags::test_tags(
test_tags::test_tags( &loader,
&loader, &config.get()?,
&config.get()?, &mut tags_context,
&mut tags_context, &test_tag_dir,
&test_tag_dir, color,
&mut test_summary,
),
&test_summary,
self.json_summary,
)?; )?;
test_summary.test_num = 1;
} }
// For the rest of the queries, find their tests and run them // For the rest of the queries, find their tests and run them
for entry in walkdir::WalkDir::new(&query_dir) for entry in walkdir::WalkDir::new(current_dir.join("queries"))
.into_iter() .into_iter()
.filter_map(|e| e.ok()) .filter_map(|e| e.ok())
.filter(|e| e.file_type().is_file()) .filter(|e| e.file_type().is_file())
@ -1422,48 +1286,34 @@ impl Test {
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
if !entries.is_empty() { if !entries.is_empty() {
test_summary.query_results.add_group(stem); println!("{stem}:");
} }
test_summary.test_num = 1; for entry in entries {
let opts = QueryFileOptions::default();
for entry in &entries {
let path = entry.path(); let path = entry.path();
check_test( query::query_file_at_path(
query::query_file_at_path( language,
language, path,
path, &path.display().to_string(),
&path.display().to_string(), path,
path, false,
&opts, None,
Some(&mut test_summary), None,
), true,
&test_summary, false,
self.json_summary, false,
false,
)?; )?;
} }
if !entries.is_empty() {
test_summary.query_results.pop_traversal();
}
} }
} }
test_summary.test_num = 1;
if self.json_summary {
let json_summary = serde_json::to_string_pretty(&test_summary)
.expect("Failed to encode test summary to JSON");
println!("{json_summary}");
} else {
println!("{test_summary}");
}
Ok(()) Ok(())
} }
} }
impl Version { impl Version {
fn run(self, current_dir: PathBuf) -> Result<()> { fn run(self, current_dir: PathBuf) -> Result<()> {
Ok(version::Version::new(self.version, current_dir, self.bump).run()?) version::Version::new(self.version, current_dir, self.bump).run()
} }
} }
@ -1482,7 +1332,7 @@ impl Fuzz {
let lang_name = lib_info.1.to_string(); let lang_name = lib_info.1.to_string();
&( &(
loader loader
.select_language(None, current_dir, None, Some(&lib_info)) .select_language(lib_path, current_dir, None, Some(&lib_info))
.with_context(|| { .with_context(|| {
anyhow!( anyhow!(
"Failed to load language for path \"{}\"", "Failed to load language for path \"{}\"",
@ -1527,11 +1377,18 @@ impl Query {
loader.find_all_languages(&loader_config)?; loader.find_all_languages(&loader_config)?;
let query_path = Path::new(&self.query_path); let query_path = Path::new(&self.query_path);
let byte_range = parse_range(&self.byte_range, |x| x)?; let byte_range = self.byte_range.as_ref().and_then(|range| {
let point_range = parse_range(&self.row_range, |row| Point::new(row, 0))?; let mut parts = range.split(':');
let containing_byte_range = parse_range(&self.containing_byte_range, |x| x)?; let start = parts.next()?.parse().ok()?;
let containing_point_range = let end = parts.next().unwrap().parse().ok()?;
parse_range(&self.containing_row_range, |row| Point::new(row, 0))?; Some(start..end)
});
let point_range = self.row_range.as_ref().and_then(|range| {
let mut parts = range.split(':');
let start = parts.next()?.parse().ok()?;
let end = parts.next().unwrap().parse().ok()?;
Some(Point::new(start, 0)..Point::new(end, 0))
});
let cancellation_flag = util::cancel_on_signal(); let cancellation_flag = util::cancel_on_signal();
@ -1550,30 +1407,25 @@ impl Query {
match input { match input {
CliInput::Paths(paths) => { CliInput::Paths(paths) => {
let language = loader.select_language( let language = loader.select_language(
Some(Path::new(&paths[0])), Path::new(&paths[0]),
current_dir, current_dir,
self.scope.as_deref(), self.scope.as_deref(),
lib_info.as_ref(), lib_info.as_ref(),
)?; )?;
let opts = QueryFileOptions {
ordered_captures: self.captures,
byte_range,
point_range,
containing_byte_range,
containing_point_range,
quiet: self.quiet,
print_time: self.time,
stdin: false,
};
for path in paths { for path in paths {
query::query_file_at_path( query::query_file_at_path(
&language, &language,
&path, &path,
&path.display().to_string(), &path.display().to_string(),
query_path, query_path,
&opts, self.captures,
None, byte_range.clone(),
point_range.clone(),
self.test,
self.quiet,
self.time,
false,
)?; )?;
} }
} }
@ -1586,7 +1438,7 @@ impl Query {
let languages = loader.languages_at_path(current_dir)?; let languages = loader.languages_at_path(current_dir)?;
let language = if let Some(ref lib_path) = self.lib_path { let language = if let Some(ref lib_path) = self.lib_path {
&loader &loader
.select_language(None, current_dir, None, lib_info.as_ref()) .select_language(lib_path, current_dir, None, lib_info.as_ref())
.with_context(|| { .with_context(|| {
anyhow!( anyhow!(
"Failed to load language for path \"{}\"", "Failed to load language for path \"{}\"",
@ -1601,17 +1453,19 @@ impl Query {
.map(|(l, _)| l.clone()) .map(|(l, _)| l.clone())
.ok_or_else(|| anyhow!("No language found"))? .ok_or_else(|| anyhow!("No language found"))?
}; };
let opts = QueryFileOptions { query::query_file_at_path(
ordered_captures: self.captures, language,
&path,
&name,
query_path,
self.captures,
byte_range, byte_range,
point_range, point_range,
containing_byte_range, self.test,
containing_point_range, self.quiet,
quiet: self.quiet, self.time,
print_time: self.time, true,
stdin: true, )?;
};
query::query_file_at_path(language, &path, &name, query_path, &opts, None)?;
fs::remove_file(path)?; fs::remove_file(path)?;
} }
CliInput::Stdin(contents) => { CliInput::Stdin(contents) => {
@ -1620,18 +1474,20 @@ impl Query {
let path = get_tmp_source_file(&contents)?; let path = get_tmp_source_file(&contents)?;
let language = let language =
loader.select_language(None, current_dir, None, lib_info.as_ref())?; loader.select_language(&path, current_dir, None, lib_info.as_ref())?;
let opts = QueryFileOptions { query::query_file_at_path(
ordered_captures: self.captures, &language,
&path,
"stdin",
query_path,
self.captures,
byte_range, byte_range,
point_range, point_range,
containing_byte_range, self.test,
containing_point_range, self.quiet,
quiet: self.quiet, self.time,
print_time: self.time, true,
stdin: true, )?;
};
query::query_file_at_path(&language, &path, "stdin", query_path, &opts, None)?;
fs::remove_file(path)?; fs::remove_file(path)?;
} }
} }
@ -1648,7 +1504,6 @@ impl Highlight {
let loader_config = config.get()?; let loader_config = config.get()?;
loader.find_all_languages(&loader_config)?; loader.find_all_languages(&loader_config)?;
loader.force_rebuild(self.rebuild || self.grammar_path.is_some()); loader.force_rebuild(self.rebuild || self.grammar_path.is_some());
let languages = loader.languages_at_path(current_dir)?;
let cancellation_flag = util::cancel_on_signal(); let cancellation_flag = util::cancel_on_signal();
@ -1729,6 +1584,7 @@ impl Highlight {
} => { } => {
let path = get_tmp_source_file(&contents)?; let path = get_tmp_source_file(&contents)?;
let languages = loader.languages_at_path(current_dir)?;
let language = languages let language = languages
.iter() .iter()
.find(|(_, n)| language_names.contains(&Box::from(n.as_str()))) .find(|(_, n)| language_names.contains(&Box::from(n.as_str())))
@ -1759,6 +1615,7 @@ impl Highlight {
if let (Some(l), Some(lc)) = (language.clone(), language_configuration) { if let (Some(l), Some(lc)) = (language.clone(), language_configuration) {
(l, lc) (l, lc)
} else { } else {
let languages = loader.languages_at_path(current_dir)?;
let language = languages let language = languages
.first() .first()
.map(|(l, _)| l.clone()) .map(|(l, _)| l.clone())
@ -2131,32 +1988,3 @@ fn get_lib_info<'a>(
None None
} }
} }
/// Parse a range string of the form "start:end" into an optional Range<T>.
fn parse_range<T>(
range_str: &Option<String>,
make: impl Fn(usize) -> T,
) -> Result<Option<std::ops::Range<T>>> {
if let Some(range) = range_str.as_ref() {
let err_msg = format!("Invalid range '{range}', expected 'start:end'");
let mut parts = range.split(':');
let Some(part) = parts.next() else {
Err(anyhow!(err_msg))?
};
let Ok(start) = part.parse::<usize>() else {
Err(anyhow!(err_msg))?
};
let Some(part) = parts.next() else {
Err(anyhow!(err_msg))?
};
let Ok(end) = part.parse::<usize>() else {
Err(anyhow!(err_msg))?
};
Ok(Some(make(start)..make(end)))
} else {
Ok(None)
}
}

View file

@ -11,7 +11,6 @@ use anstyle::{AnsiColor, Color, RgbColor};
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use clap::ValueEnum; use clap::ValueEnum;
use log::info; use log::info;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tree_sitter::{ use tree_sitter::{
ffi, InputEdit, Language, LogType, ParseOptions, ParseState, Parser, Point, Range, Tree, ffi, InputEdit, Language, LogType, ParseOptions, ParseState, Parser, Point, Range, Tree,
@ -20,7 +19,7 @@ use tree_sitter::{
use crate::{fuzz::edits::Edit, logger::paint, util}; use crate::{fuzz::edits::Edit, logger::paint, util};
#[derive(Debug, Default, Serialize, JsonSchema)] #[derive(Debug, Default, Serialize)]
pub struct Stats { pub struct Stats {
pub successful_parses: usize, pub successful_parses: usize,
pub total_parses: usize, pub total_parses: usize,
@ -231,21 +230,10 @@ impl ParseSummary {
} }
} }
#[derive(Serialize, Debug)] #[derive(Serialize, Debug, Default)]
pub struct ParseStats { pub struct ParseStats {
pub parse_summaries: Vec<ParseSummary>, pub parse_summaries: Vec<ParseSummary>,
pub cumulative_stats: Stats, pub cumulative_stats: Stats,
pub source_count: usize,
}
impl Default for ParseStats {
fn default() -> Self {
Self {
parse_summaries: Vec::new(),
cumulative_stats: Stats::default(),
source_count: 1,
}
}
} }
#[derive(Serialize, ValueEnum, Debug, Copy, Clone, Default, Eq, PartialEq)] #[derive(Serialize, ValueEnum, Debug, Copy, Clone, Default, Eq, PartialEq)]
@ -515,22 +503,16 @@ pub fn parse_file_at_path(
if opts.output == ParseOutput::Cst { if opts.output == ParseOutput::Cst {
render_cst(&source_code, &tree, &mut cursor, opts, &mut stdout)?; render_cst(&source_code, &tree, &mut cursor, opts, &mut stdout)?;
println!();
} }
if opts.output == ParseOutput::Xml { if opts.output == ParseOutput::Xml {
let mut needs_newline = false; let mut needs_newline = false;
let mut indent_level = 2; let mut indent_level = 0;
let mut did_visit_children = false; let mut did_visit_children = false;
let mut had_named_children = false; let mut had_named_children = false;
let mut tags = Vec::<&str>::new(); let mut tags = Vec::<&str>::new();
writeln!(&mut stdout, "<?xml version=\"1.0\"?>")?;
// If we're parsing the first file, write the header
if opts.stats.parse_summaries.is_empty() {
writeln!(&mut stdout, "<?xml version=\"1.0\"?>")?;
writeln!(&mut stdout, "<sources>")?;
}
writeln!(&mut stdout, " <source name=\"{}\">", path.display())?;
loop { loop {
let node = cursor.node(); let node = cursor.node();
let is_named = node.is_named(); let is_named = node.is_named();
@ -609,14 +591,8 @@ pub fn parse_file_at_path(
} }
} }
} }
writeln!(&mut stdout)?;
writeln!(&mut stdout, " </source>")?;
// If we parsed the last file, write the closing tag for the `sources` header
if opts.stats.parse_summaries.len() == opts.stats.source_count - 1 {
writeln!(&mut stdout, "</sources>")?;
}
cursor.reset(tree.root_node()); cursor.reset(tree.root_node());
println!();
} }
if opts.output == ParseOutput::Dot { if opts.output == ParseOutput::Dot {
@ -674,9 +650,10 @@ pub fn parse_file_at_path(
width = max_path_length width = max_path_length
)?; )?;
if let Some(node) = first_error { if let Some(node) = first_error {
let node_kind = node.kind(); let start = node.start_position();
let mut node_text = String::with_capacity(node_kind.len()); let end = node.end_position();
for c in node_kind.chars() { let mut node_text = String::new();
for c in node.kind().chars() {
if let Some(escaped) = escape_invisible(c) { if let Some(escaped) = escape_invisible(c) {
node_text += escaped; node_text += escaped;
} else { } else {
@ -693,9 +670,6 @@ pub fn parse_file_at_path(
} else { } else {
write!(&mut stdout, "{node_text}")?; write!(&mut stdout, "{node_text}")?;
} }
let start = node.start_position();
let end = node.end_position();
write!( write!(
&mut stdout, &mut stdout,
" [{}, {}] - [{}, {}])", " [{}, {}] - [{}, {}])",
@ -784,7 +758,7 @@ pub fn render_cst<'a, 'b: 'a>(
.map(|(row, col)| (row as f64).log10() as usize + (col.len() as f64).log10() as usize + 1) .map(|(row, col)| (row as f64).log10() as usize + (col.len() as f64).log10() as usize + 1)
.max() .max()
.unwrap_or(1); .unwrap_or(1);
let mut indent_level = usize::from(!opts.no_ranges); let mut indent_level = 1;
let mut did_visit_children = false; let mut did_visit_children = false;
let mut in_error = false; let mut in_error = false;
loop { loop {
@ -882,24 +856,35 @@ fn write_node_text(
0 0
}; };
let formatted_line = render_line_feed(line, opts); let formatted_line = render_line_feed(line, opts);
write!( if !opts.no_ranges {
out, write!(
"{}{}{}{}{}{}", out,
if multiline { "\n" } else { " " }, "{}{}{}{}{}{}",
if multiline && !opts.no_ranges { if multiline { "\n" } else { "" },
render_node_range(opts, cursor, is_named, true, total_width, node_range) if multiline {
} else { render_node_range(opts, cursor, is_named, true, total_width, node_range)
String::new() } else {
}, String::new()
if multiline { },
" ".repeat(indent_level + 1) if multiline {
} else { " ".repeat(indent_level + 1)
String::new() } else {
}, String::new()
paint(quote_color, &String::from(quote)), },
paint(color, &render_node_text(&formatted_line)), paint(quote_color, &String::from(quote)),
paint(quote_color, &String::from(quote)), &paint(color, &render_node_text(&formatted_line)),
)?; paint(quote_color, &String::from(quote)),
)?;
} else {
write!(
out,
"\n{}{}{}{}",
" ".repeat(indent_level + 1),
paint(quote_color, &String::from(quote)),
&paint(color, &render_node_text(&formatted_line)),
paint(quote_color, &String::from(quote)),
)?;
}
} }
} }
@ -953,7 +938,7 @@ fn render_node_range(
fn cst_render_node( fn cst_render_node(
opts: &ParseFileOptions, opts: &ParseFileOptions,
cursor: &TreeCursor, cursor: &mut TreeCursor,
source_code: &[u8], source_code: &[u8],
out: &mut impl Write, out: &mut impl Write,
total_width: usize, total_width: usize,
@ -999,9 +984,10 @@ fn cst_render_node(
} else { } else {
opts.parse_theme.node_kind opts.parse_theme.node_kind
}; };
write!(out, "{}", paint(kind_color, node.kind()))?; write!(out, "{}", paint(kind_color, node.kind()),)?;
if node.child_count() == 0 { if node.child_count() == 0 {
write!(out, " ")?;
// Node text from a pattern or external scanner // Node text from a pattern or external scanner
write_node_text( write_node_text(
opts, opts,

View file

@ -19,8 +19,7 @@
--light-scrollbar-track: #f1f1f1; --light-scrollbar-track: #f1f1f1;
--light-scrollbar-thumb: #c1c1c1; --light-scrollbar-thumb: #c1c1c1;
--light-scrollbar-thumb-hover: #a8a8a8; --light-scrollbar-thumb-hover: #a8a8a8;
--light-tree-row-bg: #e3f2fd;
--dark-bg: #1d1f21; --dark-bg: #1d1f21;
--dark-border: #2d2d2d; --dark-border: #2d2d2d;
--dark-text: #c5c8c6; --dark-text: #c5c8c6;
@ -29,7 +28,6 @@
--dark-scrollbar-track: #25282c; --dark-scrollbar-track: #25282c;
--dark-scrollbar-thumb: #4a4d51; --dark-scrollbar-thumb: #4a4d51;
--dark-scrollbar-thumb-hover: #5a5d61; --dark-scrollbar-thumb-hover: #5a5d61;
--dark-tree-row-bg: #373737;
--primary-color: #0550ae; --primary-color: #0550ae;
--primary-color-alpha: rgba(5, 80, 174, 0.1); --primary-color-alpha: rgba(5, 80, 174, 0.1);
@ -44,7 +42,6 @@
--text-color: var(--dark-text); --text-color: var(--dark-text);
--panel-bg: var(--dark-panel-bg); --panel-bg: var(--dark-panel-bg);
--code-bg: var(--dark-code-bg); --code-bg: var(--dark-code-bg);
--tree-row-bg: var(--dark-tree-row-bg);
} }
[data-theme="light"] { [data-theme="light"] {
@ -53,7 +50,6 @@
--text-color: var(--light-text); --text-color: var(--light-text);
--panel-bg: white; --panel-bg: white;
--code-bg: white; --code-bg: white;
--tree-row-bg: var(--light-tree-row-bg);
} }
/* Base Styles */ /* Base Styles */
@ -279,7 +275,7 @@
} }
#output-container a.highlighted { #output-container a.highlighted {
background-color: #cae2ff; background-color: #d9d9d9;
color: red; color: red;
border-radius: 3px; border-radius: 3px;
text-decoration: underline; text-decoration: underline;
@ -350,7 +346,7 @@
} }
& #output-container a.highlighted { & #output-container a.highlighted {
background-color: #656669; background-color: #373b41;
color: red; color: red;
} }
@ -377,9 +373,6 @@
color: var(--dark-text); color: var(--dark-text);
} }
} }
.tree-row:has(.highlighted) {
background-color: var(--tree-row-bg);
}
</style> </style>
</head> </head>

View file

@ -6,35 +6,30 @@ use std::{
time::Instant, time::Instant,
}; };
use anstyle::AnsiColor;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use log::warn; use log::warn;
use streaming_iterator::StreamingIterator; use streaming_iterator::StreamingIterator;
use tree_sitter::{Language, Parser, Point, Query, QueryCursor}; use tree_sitter::{Language, Parser, Point, Query, QueryCursor};
use crate::{ use crate::{
logger::paint,
query_testing::{self, to_utf8_point}, query_testing::{self, to_utf8_point},
test::{TestInfo, TestOutcome, TestResult, TestSummary},
}; };
#[derive(Default)] #[allow(clippy::too_many_arguments)]
pub struct QueryFileOptions {
pub ordered_captures: bool,
pub byte_range: Option<Range<usize>>,
pub point_range: Option<Range<Point>>,
pub containing_byte_range: Option<Range<usize>>,
pub containing_point_range: Option<Range<Point>>,
pub quiet: bool,
pub print_time: bool,
pub stdin: bool,
}
pub fn query_file_at_path( pub fn query_file_at_path(
language: &Language, language: &Language,
path: &Path, path: &Path,
name: &str, name: &str,
query_path: &Path, query_path: &Path,
opts: &QueryFileOptions, ordered_captures: bool,
test_summary: Option<&mut TestSummary>, byte_range: Option<Range<usize>>,
point_range: Option<Range<Point>>,
should_test: bool,
quiet: bool,
print_time: bool,
stdin: bool,
) -> Result<()> { ) -> Result<()> {
let stdout = io::stdout(); let stdout = io::stdout();
let mut stdout = stdout.lock(); let mut stdout = stdout.lock();
@ -44,26 +39,19 @@ pub fn query_file_at_path(
let query = Query::new(language, &query_source).with_context(|| "Query compilation failed")?; let query = Query::new(language, &query_source).with_context(|| "Query compilation failed")?;
let mut query_cursor = QueryCursor::new(); let mut query_cursor = QueryCursor::new();
if let Some(ref range) = opts.byte_range { if let Some(range) = byte_range {
query_cursor.set_byte_range(range.clone()); query_cursor.set_byte_range(range);
} }
if let Some(ref range) = opts.point_range { if let Some(range) = point_range {
query_cursor.set_point_range(range.clone()); query_cursor.set_point_range(range);
}
if let Some(ref range) = opts.containing_byte_range {
query_cursor.set_containing_byte_range(range.clone());
}
if let Some(ref range) = opts.containing_point_range {
query_cursor.set_containing_point_range(range.clone());
} }
let mut parser = Parser::new(); let mut parser = Parser::new();
parser.set_language(language)?; parser.set_language(language)?;
let mut results = Vec::new(); let mut results = Vec::new();
let should_test = test_summary.is_some();
if !should_test && !opts.stdin { if !should_test && !stdin {
writeln!(&mut stdout, "{name}")?; writeln!(&mut stdout, "{name}")?;
} }
@ -72,12 +60,12 @@ pub fn query_file_at_path(
let tree = parser.parse(&source_code, None).unwrap(); let tree = parser.parse(&source_code, None).unwrap();
let start = Instant::now(); let start = Instant::now();
if opts.ordered_captures { if ordered_captures {
let mut captures = query_cursor.captures(&query, tree.root_node(), source_code.as_slice()); let mut captures = query_cursor.captures(&query, tree.root_node(), source_code.as_slice());
while let Some((mat, capture_index)) = captures.next() { while let Some((mat, capture_index)) = captures.next() {
let capture = mat.captures[*capture_index]; let capture = mat.captures[*capture_index];
let capture_name = &query.capture_names()[capture.index as usize]; let capture_name = &query.capture_names()[capture.index as usize];
if !opts.quiet && !should_test { if !quiet && !should_test {
writeln!( writeln!(
&mut stdout, &mut stdout,
" pattern: {:>2}, capture: {} - {capture_name}, start: {}, end: {}, text: `{}`", " pattern: {:>2}, capture: {} - {capture_name}, start: {}, end: {}, text: `{}`",
@ -88,25 +76,23 @@ pub fn query_file_at_path(
capture.node.utf8_text(&source_code).unwrap_or("") capture.node.utf8_text(&source_code).unwrap_or("")
)?; )?;
} }
if should_test { results.push(query_testing::CaptureInfo {
results.push(query_testing::CaptureInfo { name: (*capture_name).to_string(),
name: (*capture_name).to_string(), start: to_utf8_point(capture.node.start_position(), source_code.as_slice()),
start: to_utf8_point(capture.node.start_position(), source_code.as_slice()), end: to_utf8_point(capture.node.end_position(), source_code.as_slice()),
end: to_utf8_point(capture.node.end_position(), source_code.as_slice()), });
});
}
} }
} else { } else {
let mut matches = query_cursor.matches(&query, tree.root_node(), source_code.as_slice()); let mut matches = query_cursor.matches(&query, tree.root_node(), source_code.as_slice());
while let Some(m) = matches.next() { while let Some(m) = matches.next() {
if !opts.quiet && !should_test { if !quiet && !should_test {
writeln!(&mut stdout, " pattern: {}", m.pattern_index)?; writeln!(&mut stdout, " pattern: {}", m.pattern_index)?;
} }
for capture in m.captures { for capture in m.captures {
let start = capture.node.start_position(); let start = capture.node.start_position();
let end = capture.node.end_position(); let end = capture.node.end_position();
let capture_name = &query.capture_names()[capture.index as usize]; let capture_name = &query.capture_names()[capture.index as usize];
if !opts.quiet && !should_test { if !quiet && !should_test {
if end.row == start.row { if end.row == start.row {
writeln!( writeln!(
&mut stdout, &mut stdout,
@ -121,52 +107,38 @@ pub fn query_file_at_path(
)?; )?;
} }
} }
if should_test { results.push(query_testing::CaptureInfo {
results.push(query_testing::CaptureInfo { name: (*capture_name).to_string(),
name: (*capture_name).to_string(), start: to_utf8_point(capture.node.start_position(), source_code.as_slice()),
start: to_utf8_point(capture.node.start_position(), source_code.as_slice()), end: to_utf8_point(capture.node.end_position(), source_code.as_slice()),
end: to_utf8_point(capture.node.end_position(), source_code.as_slice()), });
});
}
} }
} }
} }
if query_cursor.did_exceed_match_limit() { if !query_cursor.did_exceed_match_limit() {
warn!("Query exceeded maximum number of in-progress captures!"); warn!("Query exceeded maximum number of in-progress captures!");
} }
if should_test { if should_test {
let path_name = if opts.stdin { let path_name = if stdin {
"stdin" "stdin"
} else { } else {
Path::new(&path).file_name().unwrap().to_str().unwrap() Path::new(&path).file_name().unwrap().to_str().unwrap()
}; };
// Invariant: `test_summary` will always be `Some` when `should_test` is true
let test_summary = test_summary.unwrap();
match query_testing::assert_expected_captures(&results, path, &mut parser, language) { match query_testing::assert_expected_captures(&results, path, &mut parser, language) {
Ok(assertion_count) => { Ok(assertion_count) => {
test_summary.query_results.add_case(TestResult { println!(
name: path_name.to_string(), " ✓ {} ({} assertions)",
info: TestInfo::AssertionTest { paint(Some(AnsiColor::Green), path_name),
outcome: TestOutcome::AssertionPassed { assertion_count }, assertion_count
test_num: test_summary.test_num, );
},
});
} }
Err(e) => { Err(e) => {
test_summary.query_results.add_case(TestResult { println!("{}", paint(Some(AnsiColor::Red), path_name));
name: path_name.to_string(),
info: TestInfo::AssertionTest {
outcome: TestOutcome::AssertionFailed {
error: e.to_string(),
},
test_num: test_summary.test_num,
},
});
return Err(e); return Err(e);
} }
} }
} }
if opts.print_time { if print_time {
writeln!(&mut stdout, "{:?}", start.elapsed())?; writeln!(&mut stdout, "{:?}", start.elapsed())?;
} }

View file

@ -3,11 +3,11 @@ root = true
[*] [*]
charset = utf-8 charset = utf-8
[*.{json,toml,yml,gyp,xml}] [*.{json,toml,yml,gyp}]
indent_style = space indent_style = space
indent_size = 2 indent_size = 2
[*.{js,ts}] [*.js]
indent_style = space indent_style = space
indent_size = 2 indent_size = 2
@ -31,10 +31,6 @@ indent_size = 4
indent_style = space indent_style = space
indent_size = 4 indent_size = 4
[*.java]
indent_style = space
indent_size = 4
[*.go] [*.go]
indent_style = tab indent_style = tab
indent_size = 8 indent_size = 8

View file

@ -6,33 +6,32 @@ from ._binding import language
def _get_query(name, file): def _get_query(name, file):
try: query = _files(f"{__package__}.queries") / file
query = _files(f"{__package__}") / file globals()[name] = query.read_text()
globals()[name] = query.read_text()
except FileNotFoundError:
globals()[name] = None
return globals()[name] return globals()[name]
def __getattr__(name): def __getattr__(name):
if name == "HIGHLIGHTS_QUERY": # NOTE: uncomment these to include any queries that this grammar contains:
return _get_query("HIGHLIGHTS_QUERY", "HIGHLIGHTS_QUERY_PATH")
if name == "INJECTIONS_QUERY": # if name == "HIGHLIGHTS_QUERY":
return _get_query("INJECTIONS_QUERY", "INJECTIONS_QUERY_PATH") # return _get_query("HIGHLIGHTS_QUERY", "highlights.scm")
if name == "LOCALS_QUERY": # if name == "INJECTIONS_QUERY":
return _get_query("LOCALS_QUERY", "LOCALS_QUERY_PATH") # return _get_query("INJECTIONS_QUERY", "injections.scm")
if name == "TAGS_QUERY": # if name == "LOCALS_QUERY":
return _get_query("TAGS_QUERY", "TAGS_QUERY_PATH") # return _get_query("LOCALS_QUERY", "locals.scm")
# if name == "TAGS_QUERY":
# return _get_query("TAGS_QUERY", "tags.scm")
raise AttributeError(f"module {__name__!r} has no attribute {name!r}") raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
__all__ = [ __all__ = [
"language", "language",
"HIGHLIGHTS_QUERY", # "HIGHLIGHTS_QUERY",
"INJECTIONS_QUERY", # "INJECTIONS_QUERY",
"LOCALS_QUERY", # "LOCALS_QUERY",
"TAGS_QUERY", # "TAGS_QUERY",
] ]

View file

@ -1,17 +1,11 @@
from typing import Final from typing import Final
from typing_extensions import CapsuleType from typing_extensions import CapsuleType
HIGHLIGHTS_QUERY: Final[str] | None # NOTE: uncomment these to include any queries that this grammar contains:
"""The syntax highlighting query for this grammar."""
INJECTIONS_QUERY: Final[str] | None # HIGHLIGHTS_QUERY: Final[str]
"""The language injection query for this grammar.""" # INJECTIONS_QUERY: Final[str]
# LOCALS_QUERY: Final[str]
# TAGS_QUERY: Final[str]
LOCALS_QUERY: Final[str] | None def language() -> CapsuleType: ...
"""The local variable query for this grammar."""
TAGS_QUERY: Final[str] | None
"""The symbol tagging query for this grammar."""
def language() -> CapsuleType:
"""The tree-sitter language function for this grammar."""

View file

@ -1,65 +0,0 @@
package PARSER_NS_CLEANED.jtreesitter.LOWER_PARSER_NAME;
import java.lang.foreign.*;
public final class PARSER_CLASS_NAME {
private static final ValueLayout VOID_PTR =
ValueLayout.ADDRESS.withTargetLayout(MemoryLayout.sequenceLayout(Long.MAX_VALUE, ValueLayout.JAVA_BYTE));
private static final FunctionDescriptor FUNC_DESC = FunctionDescriptor.of(VOID_PTR);
private static final Linker LINKER = Linker.nativeLinker();
private static final PARSER_CLASS_NAME INSTANCE = new PARSER_CLASS_NAME();
private final Arena arena = Arena.ofAuto();
private volatile SymbolLookup lookup = null;
private PARSER_CLASS_NAME() {}
/**
* Get the tree-sitter language for this grammar.
*/
public static MemorySegment language() {
if (INSTANCE.lookup == null)
INSTANCE.lookup = INSTANCE.findLibrary();
return language(INSTANCE.lookup);
}
/**
* Get the tree-sitter language for this grammar.
*
* <strong>The {@linkplain Arena} used in the {@code lookup}
* must not be closed while the language is being used.</strong>
*/
public static MemorySegment language(SymbolLookup lookup) {
return call(lookup, "tree_sitter_PARSER_NAME");
}
private SymbolLookup findLibrary() {
try {
var library = System.mapLibraryName("tree-sitter-KEBAB_PARSER_NAME");
return SymbolLookup.libraryLookup(library, arena);
} catch (IllegalArgumentException ex1) {
try {
System.loadLibrary("tree-sitter-KEBAB_PARSER_NAME");
return SymbolLookup.loaderLookup();
} catch (UnsatisfiedLinkError ex2) {
ex1.addSuppressed(ex2);
throw ex1;
}
}
}
private static UnsatisfiedLinkError unresolved(String name) {
return new UnsatisfiedLinkError("Unresolved symbol: %s".formatted(name));
}
@SuppressWarnings("SameParameterValue")
private static MemorySegment call(SymbolLookup lookup, String name) throws UnsatisfiedLinkError {
var address = lookup.find(name).orElseThrow(() -> unresolved(name));
try {
var function = LINKER.downcallHandle(address, FUNC_DESC);
return (MemorySegment) function.invokeExact();
} catch (Throwable e) {
throw new RuntimeException("Call to %s failed".formatted(name), e);
}
}
}

View file

@ -36,21 +36,4 @@ fn main() {
} }
c_config.compile("tree-sitter-KEBAB_PARSER_NAME"); c_config.compile("tree-sitter-KEBAB_PARSER_NAME");
println!("cargo:rustc-check-cfg=cfg(with_highlights_query)");
if !"HIGHLIGHTS_QUERY_PATH".is_empty() && std::path::Path::new("HIGHLIGHTS_QUERY_PATH").exists() {
println!("cargo:rustc-cfg=with_highlights_query");
}
println!("cargo:rustc-check-cfg=cfg(with_injections_query)");
if !"INJECTIONS_QUERY_PATH".is_empty() && std::path::Path::new("INJECTIONS_QUERY_PATH").exists() {
println!("cargo:rustc-cfg=with_injections_query");
}
println!("cargo:rustc-check-cfg=cfg(with_locals_query)");
if !"LOCALS_QUERY_PATH".is_empty() && std::path::Path::new("LOCALS_QUERY_PATH").exists() {
println!("cargo:rustc-cfg=with_locals_query");
}
println!("cargo:rustc-check-cfg=cfg(with_tags_query)");
if !"TAGS_QUERY_PATH".is_empty() && std::path::Path::new("TAGS_QUERY_PATH").exists() {
println!("cargo:rustc-cfg=with_tags_query");
}
} }

View file

@ -1,6 +1,5 @@
.{ .{
.name = .tree_sitter_PARSER_NAME, .name = .tree_sitter_PARSER_NAME,
.fingerprint = PARSER_FINGERPRINT,
.version = "PARSER_VERSION", .version = "PARSER_VERSION",
.dependencies = .{ .dependencies = .{
.tree_sitter = .{ .tree_sitter = .{

View file

@ -20,19 +20,16 @@ include(GNUInstallDirs)
find_program(TREE_SITTER_CLI tree-sitter DOC "Tree-sitter CLI") find_program(TREE_SITTER_CLI tree-sitter DOC "Tree-sitter CLI")
add_custom_command(OUTPUT "${CMAKE_CURRENT_SOURCE_DIR}/src/grammar.json" add_custom_command(OUTPUT "${CMAKE_CURRENT_SOURCE_DIR}/src/grammar.json"
"${CMAKE_CURRENT_SOURCE_DIR}/src/node-types.json"
DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/grammar.js" DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/grammar.js"
COMMAND "${TREE_SITTER_CLI}" generate grammar.js --no-parser COMMAND "${TREE_SITTER_CLI}" generate grammar.js
--emit=json
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
COMMENT "Generating grammar.json") COMMENT "Generating grammar.json")
add_custom_command(OUTPUT "${CMAKE_CURRENT_SOURCE_DIR}/src/parser.c" add_custom_command(OUTPUT "${CMAKE_CURRENT_SOURCE_DIR}/src/parser.c"
BYPRODUCTS "${CMAKE_CURRENT_SOURCE_DIR}/src/tree_sitter/parser.h"
"${CMAKE_CURRENT_SOURCE_DIR}/src/tree_sitter/alloc.h"
"${CMAKE_CURRENT_SOURCE_DIR}/src/tree_sitter/array.h"
DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/grammar.json" DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/grammar.json"
COMMAND "${TREE_SITTER_CLI}" generate src/grammar.json COMMAND "${TREE_SITTER_CLI}" generate src/grammar.json
--abi=${TREE_SITTER_ABI_VERSION} --emit=parser --abi=${TREE_SITTER_ABI_VERSION}
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
COMMENT "Generating parser.c") COMMENT "Generating parser.c")

View file

@ -40,7 +40,3 @@ Package.resolved linguist-generated
bindings/zig/* linguist-generated bindings/zig/* linguist-generated
build.zig linguist-generated build.zig linguist-generated
build.zig.zon linguist-generated build.zig.zon linguist-generated
# Java bindings
pom.xml linguist-generated
bindings/java/** linguist-generated

View file

@ -45,4 +45,3 @@ zig-out/
*.tar.gz *.tar.gz
*.tgz *.tgz
*.zip *.zip
*.jar

View file

@ -18,43 +18,10 @@ type NodeInfo =
children: ChildNode[]; children: ChildNode[];
}); });
/** type Language = {
* The tree-sitter language object for this grammar.
*
* @see {@linkcode https://tree-sitter.github.io/node-tree-sitter/interfaces/Parser.Language.html Parser.Language}
*
* @example
* import Parser from "tree-sitter";
* import CAMEL_PARSER_NAME from "tree-sitter-KEBAB_PARSER_NAME";
*
* const parser = new Parser();
* parser.setLanguage(CAMEL_PARSER_NAME);
*/
declare const binding: {
/**
* The inner language object.
* @private
*/
language: unknown; language: unknown;
/**
* The content of the `node-types.json` file for this grammar.
*
* @see {@linkplain https://tree-sitter.github.io/tree-sitter/using-parsers/6-static-node-types Static Node Types}
*/
nodeTypeInfo: NodeInfo[]; nodeTypeInfo: NodeInfo[];
/** The syntax highlighting query for this grammar. */
HIGHLIGHTS_QUERY?: string;
/** The language injection query for this grammar. */
INJECTIONS_QUERY?: string;
/** The local variable query for this grammar. */
LOCALS_QUERY?: string;
/** The symbol tagging query for this grammar. */
TAGS_QUERY?: string;
}; };
export default binding; declare const language: Language;
export = language;

View file

@ -1,7 +1,4 @@
import { readFileSync } from "node:fs"; const root = new URL("../..", import.meta.url).pathname;
import { fileURLToPath } from "node:url";
const root = fileURLToPath(new URL("../..", import.meta.url));
const binding = typeof process.versions.bun === "string" const binding = typeof process.versions.bun === "string"
// Support `bun build --compile` by being statically analyzable enough to find the .node file at build-time // Support `bun build --compile` by being statically analyzable enough to find the .node file at build-time
@ -9,29 +6,8 @@ const binding = typeof process.versions.bun === "string"
: (await import("node-gyp-build")).default(root); : (await import("node-gyp-build")).default(root);
try { try {
const nodeTypes = await import(`${root}/src/node-types.json`, { with: { type: "json" } }); const nodeTypes = await import(`${root}/src/node-types.json`, {with: {type: "json"}});
binding.nodeTypeInfo = nodeTypes.default; binding.nodeTypeInfo = nodeTypes.default;
} catch { } } catch (_) {}
const queries = [
["HIGHLIGHTS_QUERY", `${root}/HIGHLIGHTS_QUERY_PATH`],
["INJECTIONS_QUERY", `${root}/INJECTIONS_QUERY_PATH`],
["LOCALS_QUERY", `${root}/LOCALS_QUERY_PATH`],
["TAGS_QUERY", `${root}/TAGS_QUERY_PATH`],
];
for (const [prop, path] of queries) {
Object.defineProperty(binding, prop, {
configurable: true,
enumerable: true,
get() {
delete binding[prop];
try {
binding[prop] = readFileSync(path, "utf8");
} catch { }
return binding[prop];
}
});
}
export default binding; export default binding;

View file

@ -32,21 +32,12 @@ pub const LANGUAGE: LanguageFn = unsafe { LanguageFn::from_raw(tree_sitter_PARSE
/// [`node-types.json`]: https://tree-sitter.github.io/tree-sitter/using-parsers/6-static-node-types /// [`node-types.json`]: https://tree-sitter.github.io/tree-sitter/using-parsers/6-static-node-types
pub const NODE_TYPES: &str = include_str!("../../src/node-types.json"); pub const NODE_TYPES: &str = include_str!("../../src/node-types.json");
#[cfg(with_highlights_query)] // NOTE: uncomment these to include any queries that this grammar contains:
/// The syntax highlighting query for this grammar.
pub const HIGHLIGHTS_QUERY: &str = include_str!("../../HIGHLIGHTS_QUERY_PATH");
#[cfg(with_injections_query)] // pub const HIGHLIGHTS_QUERY: &str = include_str!("../../queries/highlights.scm");
/// The language injection query for this grammar. // pub const INJECTIONS_QUERY: &str = include_str!("../../queries/injections.scm");
pub const INJECTIONS_QUERY: &str = include_str!("../../INJECTIONS_QUERY_PATH"); // pub const LOCALS_QUERY: &str = include_str!("../../queries/locals.scm");
// pub const TAGS_QUERY: &str = include_str!("../../queries/tags.scm");
#[cfg(with_locals_query)]
/// The local variable query for this grammar.
pub const LOCALS_QUERY: &str = include_str!("../../LOCALS_QUERY_PATH");
#[cfg(with_tags_query)]
/// The symbol tagging query for this grammar.
pub const TAGS_QUERY: &str = include_str!("../../TAGS_QUERY_PATH");
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {

View file

@ -73,10 +73,10 @@ $(LANGUAGE_NAME).pc: bindings/c/$(LANGUAGE_NAME).pc.in
-e 's|@CMAKE_INSTALL_PREFIX@|$(PREFIX)|' $< > $@ -e 's|@CMAKE_INSTALL_PREFIX@|$(PREFIX)|' $< > $@
$(SRC_DIR)/grammar.json: grammar.js $(SRC_DIR)/grammar.json: grammar.js
$(TS) generate --no-parser $^ $(TS) generate --emit=json $^
$(PARSER): $(SRC_DIR)/grammar.json $(PARSER): $(SRC_DIR)/grammar.json
$(TS) generate $^ $(TS) generate --emit=parser $^
install: all install: all
install -d '$(DESTDIR)$(DATADIR)'/tree-sitter/queries/KEBAB_PARSER_NAME '$(DESTDIR)$(INCLUDEDIR)'/tree_sitter '$(DESTDIR)$(PCLIBDIR)' '$(DESTDIR)$(LIBDIR)' install -d '$(DESTDIR)$(DATADIR)'/tree-sitter/queries/KEBAB_PARSER_NAME '$(DESTDIR)$(INCLUDEDIR)'/tree_sitter '$(DESTDIR)$(PCLIBDIR)' '$(DESTDIR)$(LIBDIR)'

View file

@ -38,11 +38,11 @@
}, },
"devDependencies": { "devDependencies": {
"prebuildify": "^6.0.1", "prebuildify": "^6.0.1",
"tree-sitter": "^0.25.0", "tree-sitter": "^0.22.4",
"tree-sitter-cli": "^CLI_VERSION" "tree-sitter-cli": "^CLI_VERSION"
}, },
"peerDependencies": { "peerDependencies": {
"tree-sitter": "^0.25.0" "tree-sitter": "^0.22.4"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"tree-sitter": { "tree-sitter": {

View file

@ -1,154 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>PARSER_NS</groupId>
<artifactId>jtreesitter-KEBAB_PARSER_NAME</artifactId>
<name>JTreeSitter CAMEL_PARSER_NAME</name>
<version>PARSER_VERSION</version>
<description>PARSER_DESCRIPTION</description>
<url>PARSER_URL</url>
<licenses>
<license>
<name>PARSER_LICENSE</name>
<url>https://spdx.org/licenses/PARSER_LICENSE.html</url>
</license>
</licenses>
<developers>
<developer>
<name>PARSER_AUTHOR_NAME</name>
<email>PARSER_AUTHOR_EMAIL</email>
<url>PARSER_AUTHOR_URL</url>
</developer>
</developers>
<scm>
<url>PARSER_URL</url>
<connection>scm:git:git://PARSER_URL_STRIPPED.git</connection>
<developerConnection>scm:git:ssh://PARSER_URL_STRIPPED.git</developerConnection>
</scm>
<properties>
<maven.compiler.release>23</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.deploy.skip>true</maven.deploy.skip>
<gpg.skip>true</gpg.skip>
<publish.auto>false</publish.auto>
<publish.skip>true</publish.skip>
</properties>
<dependencies>
<dependency>
<groupId>io.github.tree-sitter</groupId>
<artifactId>jtreesitter</artifactId>
<version>0.26.0</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>6.0.1</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<sourceDirectory>bindings/java/main</sourceDirectory>
<testSourceDirectory>bindings/java/test</testSourceDirectory>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.4</version>
<configuration>
<reportsDirectory>
${project.build.directory}/reports/surefire
</reportsDirectory>
<argLine>--enable-native-access=ALL-UNNAMED</argLine>
</configuration>
</plugin>
<plugin>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.12.0</version>
<executions>
<execution>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
<configuration>
<show>public</show>
<nohelp>true</nohelp>
<noqualifier>true</noqualifier>
<doclint>all,-missing</doclint>
</configuration>
</plugin>
<plugin>
<artifactId>maven-source-plugin</artifactId>
<version>3.3.1</version>
<executions>
<execution>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-gpg-plugin</artifactId>
<version>3.2.8</version>
<executions>
<execution>
<phase>verify</phase>
<goals>
<goal>sign</goal>
</goals>
<configuration>
<bestPractices>true</bestPractices>
<gpgArguments>
<arg>--no-tty</arg>
<arg>--pinentry-mode</arg>
<arg>loopback</arg>
</gpgArguments>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>io.github.mavenplugins</groupId>
<artifactId>central-publishing-maven-plugin</artifactId>
<version>1.1.1</version>
<executions>
<execution>
<phase>deploy</phase>
<goals>
<goal>publish</goal>
</goals>
<configuration>
<waitUntil>validated</waitUntil>
<autoPublish>${publish.auto}</autoPublish>
<skipPublishing>${publish.skip}</skipPublishing>
<outputFilename>${project.artifactId}-${project.version}.zip</outputFilename>
<deploymentName>${project.artifactId}-${project.version}.zip</deploymentName>
</configuration>
</execution>
</executions>
<extensions>true</extensions>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>ci</id>
<activation>
<property>
<name>env.CI</name>
<value>true</value>
</property>
</activation>
<properties>
<gpg.skip>false</gpg.skip>
<publish.auto>true</publish.auto>
<publish.skip>false</publish.skip>
</properties>
</profile>
</profiles>
</project>

View file

@ -32,7 +32,7 @@ class BuildExt(build_ext):
class BdistWheel(bdist_wheel): class BdistWheel(bdist_wheel):
def get_tag(self): def get_tag(self):
python, abi, platform = super().get_tag() python, abi, platform = super().get_tag()
if python.startswith("cp") and not get_config_var("Py_GIL_DISABLED"): if python.startswith("cp"):
python, abi = "cp310", "abi3" python, abi = "cp310", "abi3"
return python, abi, platform return python, abi, platform

View file

@ -1,12 +0,0 @@
import io.github.treesitter.jtreesitter.Language;
import PARSER_NS_CLEANED.jtreesitter.LOWER_PARSER_NAME.PARSER_CLASS_NAME;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
public class PARSER_CLASS_NAMETest {
@Test
public void testCanLoadLanguage() {
assertDoesNotThrow(() -> new Language(PARSER_CLASS_NAME.language()));
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,13 +1,14 @@
use std::{fs, path::Path}; use std::{fs, path::Path};
use anstyle::AnsiColor;
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use tree_sitter::Point; use tree_sitter::Point;
use tree_sitter_highlight::{Highlight, HighlightConfiguration, HighlightEvent, Highlighter}; use tree_sitter_highlight::{Highlight, HighlightConfiguration, HighlightEvent, Highlighter};
use tree_sitter_loader::{Config, Loader}; use tree_sitter_loader::{Config, Loader};
use crate::{ use crate::{
logger::paint,
query_testing::{parse_position_comments, to_utf8_point, Assertion, Utf8Point}, query_testing::{parse_position_comments, to_utf8_point, Assertion, Utf8Point},
test::{TestInfo, TestOutcome, TestResult, TestSummary},
util, util,
}; };
@ -47,7 +48,19 @@ pub fn test_highlights(
loader_config: &Config, loader_config: &Config,
highlighter: &mut Highlighter, highlighter: &mut Highlighter,
directory: &Path, directory: &Path,
test_summary: &mut TestSummary, use_color: bool,
) -> Result<()> {
println!("syntax highlighting:");
test_highlights_indented(loader, loader_config, highlighter, directory, use_color, 2)
}
fn test_highlights_indented(
loader: &Loader,
loader_config: &Config,
highlighter: &mut Highlighter,
directory: &Path,
use_color: bool,
indent_level: usize,
) -> Result<()> { ) -> Result<()> {
let mut failed = false; let mut failed = false;
@ -55,22 +68,25 @@ pub fn test_highlights(
let highlight_test_file = highlight_test_file?; let highlight_test_file = highlight_test_file?;
let test_file_path = highlight_test_file.path(); let test_file_path = highlight_test_file.path();
let test_file_name = highlight_test_file.file_name(); let test_file_name = highlight_test_file.file_name();
print!(
"{indent:indent_level$}",
indent = "",
indent_level = indent_level * 2
);
if test_file_path.is_dir() && test_file_path.read_dir()?.next().is_some() { if test_file_path.is_dir() && test_file_path.read_dir()?.next().is_some() {
test_summary println!("{}:", test_file_name.to_string_lossy());
.highlight_results if test_highlights_indented(
.add_group(test_file_name.to_string_lossy().as_ref());
if test_highlights(
loader, loader,
loader_config, loader_config,
highlighter, highlighter,
&test_file_path, &test_file_path,
test_summary, use_color,
indent_level + 1,
) )
.is_err() .is_err()
{ {
failed = true; failed = true;
} }
test_summary.highlight_results.pop_traversal();
} else { } else {
let (language, language_config) = loader let (language, language_config) = loader
.language_configuration_for_file_name(&test_file_path)? .language_configuration_for_file_name(&test_file_path)?
@ -82,12 +98,7 @@ pub fn test_highlights(
})?; })?;
let highlight_config = language_config let highlight_config = language_config
.highlight_config(language, None)? .highlight_config(language, None)?
.ok_or_else(|| { .ok_or_else(|| anyhow!("No highlighting config found for {test_file_path:?}"))?;
anyhow!(
"No highlighting config found for {}",
test_file_path.display()
)
})?;
match test_highlight( match test_highlight(
loader, loader,
highlighter, highlighter,
@ -95,28 +106,30 @@ pub fn test_highlights(
fs::read(&test_file_path)?.as_slice(), fs::read(&test_file_path)?.as_slice(),
) { ) {
Ok(assertion_count) => { Ok(assertion_count) => {
test_summary.highlight_results.add_case(TestResult { println!(
name: test_file_name.to_string_lossy().to_string(), "✓ {} ({assertion_count} assertions)",
info: TestInfo::AssertionTest { paint(
outcome: TestOutcome::AssertionPassed { assertion_count }, use_color.then_some(AnsiColor::Green),
test_num: test_summary.test_num, test_file_name.to_string_lossy().as_ref()
}, ),
}); );
} }
Err(e) => { Err(e) => {
test_summary.highlight_results.add_case(TestResult { println!(
name: test_file_name.to_string_lossy().to_string(), "✗ {}",
info: TestInfo::AssertionTest { paint(
outcome: TestOutcome::AssertionFailed { use_color.then_some(AnsiColor::Red),
error: e.to_string(), test_file_name.to_string_lossy().as_ref()
}, )
test_num: test_summary.test_num, );
}, println!(
}); "{indent:indent_level$} {e}",
indent = "",
indent_level = indent_level * 2
);
failed = true; failed = true;
} }
} }
test_summary.test_num += 1;
} }
} }

View file

@ -1,12 +1,13 @@
use std::{fs, path::Path}; use std::{fs, path::Path};
use anstyle::AnsiColor;
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use tree_sitter_loader::{Config, Loader}; use tree_sitter_loader::{Config, Loader};
use tree_sitter_tags::{TagsConfiguration, TagsContext}; use tree_sitter_tags::{TagsConfiguration, TagsContext};
use crate::{ use crate::{
logger::paint,
query_testing::{parse_position_comments, to_utf8_point, Assertion, Utf8Point}, query_testing::{parse_position_comments, to_utf8_point, Assertion, Utf8Point},
test::{TestInfo, TestOutcome, TestResult, TestSummary},
util, util,
}; };
@ -46,7 +47,19 @@ pub fn test_tags(
loader_config: &Config, loader_config: &Config,
tags_context: &mut TagsContext, tags_context: &mut TagsContext,
directory: &Path, directory: &Path,
test_summary: &mut TestSummary, use_color: bool,
) -> Result<()> {
println!("tags:");
test_tags_indented(loader, loader_config, tags_context, directory, use_color, 2)
}
pub fn test_tags_indented(
loader: &Loader,
loader_config: &Config,
tags_context: &mut TagsContext,
directory: &Path,
use_color: bool,
indent_level: usize,
) -> Result<()> { ) -> Result<()> {
let mut failed = false; let mut failed = false;
@ -54,22 +67,25 @@ pub fn test_tags(
let tag_test_file = tag_test_file?; let tag_test_file = tag_test_file?;
let test_file_path = tag_test_file.path(); let test_file_path = tag_test_file.path();
let test_file_name = tag_test_file.file_name(); let test_file_name = tag_test_file.file_name();
print!(
"{indent:indent_level$}",
indent = "",
indent_level = indent_level * 2
);
if test_file_path.is_dir() && test_file_path.read_dir()?.next().is_some() { if test_file_path.is_dir() && test_file_path.read_dir()?.next().is_some() {
test_summary println!("{}:", test_file_name.to_string_lossy());
.tag_results if test_tags_indented(
.add_group(test_file_name.to_string_lossy().as_ref());
if test_tags(
loader, loader,
loader_config, loader_config,
tags_context, tags_context,
&test_file_path, &test_file_path,
test_summary, use_color,
indent_level + 1,
) )
.is_err() .is_err()
{ {
failed = true; failed = true;
} }
test_summary.tag_results.pop_traversal();
} else { } else {
let (language, language_config) = loader let (language, language_config) = loader
.language_configuration_for_file_name(&test_file_path)? .language_configuration_for_file_name(&test_file_path)?
@ -81,35 +97,37 @@ pub fn test_tags(
})?; })?;
let tags_config = language_config let tags_config = language_config
.tags_config(language)? .tags_config(language)?
.ok_or_else(|| anyhow!("No tags config found for {}", test_file_path.display()))?; .ok_or_else(|| anyhow!("No tags config found for {test_file_path:?}"))?;
match test_tag( match test_tag(
tags_context, tags_context,
tags_config, tags_config,
fs::read(&test_file_path)?.as_slice(), fs::read(&test_file_path)?.as_slice(),
) { ) {
Ok(assertion_count) => { Ok(assertion_count) => {
test_summary.tag_results.add_case(TestResult { println!(
name: test_file_name.to_string_lossy().to_string(), "✓ {} ({assertion_count} assertions)",
info: TestInfo::AssertionTest { paint(
outcome: TestOutcome::AssertionPassed { assertion_count }, use_color.then_some(AnsiColor::Green),
test_num: test_summary.test_num, test_file_name.to_string_lossy().as_ref()
}, ),
}); );
} }
Err(e) => { Err(e) => {
test_summary.tag_results.add_case(TestResult { println!(
name: test_file_name.to_string_lossy().to_string(), "✗ {}",
info: TestInfo::AssertionTest { paint(
outcome: TestOutcome::AssertionFailed { use_color.then_some(AnsiColor::Red),
error: e.to_string(), test_file_name.to_string_lossy().as_ref()
}, )
test_num: test_summary.test_num, );
}, println!(
}); "{indent:indent_level$} {e}",
indent = "",
indent_level = indent_level * 2
);
failed = true; failed = true;
} }
} }
test_summary.test_num += 1;
} }
} }

View file

@ -1,10 +1,11 @@
mod async_boundary_test; mod async_context_test;
mod corpus_test; mod corpus_test;
mod detect_language; mod detect_language;
mod helpers; mod helpers;
mod highlight_test; mod highlight_test;
mod language_test; mod language_test;
mod node_test; mod node_test;
mod parser_hang_test;
mod parser_test; mod parser_test;
mod pathological_test; mod pathological_test;
mod query_test; mod query_test;
@ -26,8 +27,6 @@ pub use crate::fuzz::{
ITERATION_COUNT, ITERATION_COUNT,
}; };
pub use helpers::fixtures::get_language;
/// This is a simple wrapper around [`tree_sitter_generate::generate_parser_for_grammar`], because /// This is a simple wrapper around [`tree_sitter_generate::generate_parser_for_grammar`], because
/// our tests do not need to pass in a version number, only the grammar JSON. /// our tests do not need to pass in a version number, only the grammar JSON.
fn generate_parser(grammar_json: &str) -> GenerateResult<(String, String)> { fn generate_parser(grammar_json: &str) -> GenerateResult<(String, String)> {

View file

@ -1,150 +0,0 @@
use std::{
future::Future,
pin::Pin,
ptr,
task::{Context, Poll, RawWaker, RawWakerVTable, Waker},
};
use tree_sitter::Parser;
use super::helpers::fixtures::get_language;
#[test]
fn test_node_across_async_boundaries() {
let mut parser = Parser::new();
let language = get_language("bash");
parser.set_language(&language).unwrap();
let tree = parser.parse("#", None).unwrap();
let root = tree.root_node();
let (result, yields) = simple_async_executor(async {
let root_ref = &root;
// Test node captured by value
let fut_by_value = async {
yield_once().await;
root.child(0).unwrap().kind()
};
// Test node captured by reference
let fut_by_ref = async {
yield_once().await;
root_ref.child(0).unwrap().kind()
};
let result1 = fut_by_value.await;
let result2 = fut_by_ref.await;
assert_eq!(result1, result2);
result1
});
assert_eq!(result, "comment");
assert_eq!(yields, 2);
}
#[test]
fn test_cursor_across_async_boundaries() {
let mut parser = Parser::new();
let language = get_language("c");
parser.set_language(&language).unwrap();
let tree = parser.parse("#", None).unwrap();
let mut cursor = tree.walk();
let ((), yields) = simple_async_executor(async {
cursor.goto_first_child();
// Test cursor usage across yield point
yield_once().await;
cursor.goto_first_child();
// Test cursor in async block
let cursor_ref = &mut cursor;
let fut = async {
yield_once().await;
cursor_ref.goto_first_child();
};
fut.await;
});
assert_eq!(yields, 2);
}
#[test]
fn test_node_and_cursor_together() {
let mut parser = Parser::new();
let language = get_language("javascript");
parser.set_language(&language).unwrap();
let tree = parser.parse("#", None).unwrap();
let root = tree.root_node();
let mut cursor = tree.walk();
let ((), yields) = simple_async_executor(async {
cursor.goto_first_child();
let fut = async {
yield_once().await;
let _ = root.to_sexp();
cursor.goto_first_child();
};
yield_once().await;
fut.await;
});
assert_eq!(yields, 2);
}
fn simple_async_executor<F>(future: F) -> (F::Output, u32)
where
F: Future,
{
let waker = noop_waker();
let mut cx = Context::from_waker(&waker);
let mut yields = 0;
let mut future = Box::pin(future);
loop {
match future.as_mut().poll(&mut cx) {
Poll::Ready(result) => return (result, yields),
Poll::Pending => yields += 1,
}
}
}
async fn yield_once() {
struct YieldOnce {
yielded: bool,
}
impl Future for YieldOnce {
type Output = ();
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
if self.yielded {
Poll::Ready(())
} else {
self.yielded = true;
cx.waker().wake_by_ref();
Poll::Pending
}
}
}
YieldOnce { yielded: false }.await;
}
const fn noop_waker() -> Waker {
const VTABLE: RawWakerVTable = RawWakerVTable::new(
// Cloning just returns a new no-op raw waker
|_| RAW,
// `wake` does nothing
|_| {},
// `wake_by_ref` does nothing
|_| {},
// Dropping does nothing as we don't allocate anything
|_| {},
);
const RAW: RawWaker = RawWaker::new(ptr::null(), &VTABLE);
unsafe { Waker::from_raw(RAW) }
}

View file

@ -0,0 +1,278 @@
use std::{
future::Future,
pin::{pin, Pin},
ptr,
task::{self, Context, Poll, RawWaker, RawWakerVTable, Waker},
};
use tree_sitter::Parser;
use super::helpers::fixtures::get_language;
#[test]
fn test_node_in_fut() {
let (ret, pended) = tokio_like_spawn(async {
let mut parser = Parser::new();
let language = get_language("bash");
parser.set_language(&language).unwrap();
let tree = parser.parse("#", None).unwrap();
let root = tree.root_node();
let root_ref = &root;
let fut_val_fn = || async {
yield_now().await;
root.child(0).unwrap().kind()
};
yield_now().await;
let fut_ref_fn = || async {
yield_now().await;
root_ref.child(0).unwrap().kind()
};
let f1 = fut_val_fn().await;
let f2 = fut_ref_fn().await;
assert_eq!(f1, f2);
let fut_val = async {
yield_now().await;
root.child(0).unwrap().kind()
};
let fut_ref = async {
yield_now().await;
root_ref.child(0).unwrap().kind()
};
let f1 = fut_val.await;
let f2 = fut_ref.await;
assert_eq!(f1, f2);
f1
})
.join();
assert_eq!(ret, "comment");
assert_eq!(pended, 5);
}
#[test]
fn test_node_and_cursor_ref_in_fut() {
let ((), pended) = tokio_like_spawn(async {
let mut parser = Parser::new();
let language = get_language("c");
parser.set_language(&language).unwrap();
let tree = parser.parse("#", None).unwrap();
let root = tree.root_node();
let root_ref = &root;
let mut cursor = tree.walk();
let cursor_ref = &mut cursor;
cursor_ref.goto_first_child();
let fut_val = async {
yield_now().await;
let _ = root.to_sexp();
};
yield_now().await;
let fut_ref = async {
yield_now().await;
let _ = root_ref.to_sexp();
cursor_ref.goto_first_child();
};
fut_val.await;
fut_ref.await;
cursor_ref.goto_first_child();
})
.join();
assert_eq!(pended, 3);
}
#[test]
fn test_node_and_cursor_ref_in_fut_with_fut_fabrics() {
let ((), pended) = tokio_like_spawn(async {
let mut parser = Parser::new();
let language = get_language("javascript");
parser.set_language(&language).unwrap();
let tree = parser.parse("#", None).unwrap();
let root = tree.root_node();
let root_ref = &root;
let mut cursor = tree.walk();
let cursor_ref = &mut cursor;
cursor_ref.goto_first_child();
let fut_val = || async {
yield_now().await;
let _ = root.to_sexp();
};
yield_now().await;
let fut_ref = || async move {
yield_now().await;
let _ = root_ref.to_sexp();
cursor_ref.goto_first_child();
};
fut_val().await;
fut_val().await;
fut_ref().await;
})
.join();
assert_eq!(pended, 4);
}
#[test]
fn test_node_and_cursor_ref_in_fut_with_inner_spawns() {
let (ret, pended) = tokio_like_spawn(async {
let mut parser = Parser::new();
let language = get_language("rust");
parser.set_language(&language).unwrap();
let tree = parser.parse("#", None).unwrap();
let mut cursor = tree.walk();
let cursor_ref = &mut cursor;
cursor_ref.goto_first_child();
let fut_val = || {
let tree = tree.clone();
async move {
let root = tree.root_node();
let mut cursor = tree.walk();
let cursor_ref = &mut cursor;
yield_now().await;
let _ = root.to_sexp();
cursor_ref.goto_first_child();
}
};
yield_now().await;
let fut_ref = || {
let tree = tree.clone();
async move {
let root = tree.root_node();
let root_ref = &root;
let mut cursor = tree.walk();
let cursor_ref = &mut cursor;
yield_now().await;
let _ = root_ref.to_sexp();
cursor_ref.goto_first_child();
}
};
let ((), p1) = tokio_like_spawn(fut_val()).await.unwrap();
let ((), p2) = tokio_like_spawn(fut_ref()).await.unwrap();
cursor_ref.goto_first_child();
fut_val().await;
fut_val().await;
fut_ref().await;
cursor_ref.goto_first_child();
p1 + p2
})
.join();
assert_eq!(pended, 4);
assert_eq!(ret, 2);
}
fn tokio_like_spawn<T>(future: T) -> JoinHandle<(T::Output, usize)>
where
T: Future + Send + 'static,
T::Output: Send + 'static,
{
// No runtime, just noop waker
let waker = noop_waker();
let mut cx = task::Context::from_waker(&waker);
let mut pending = 0;
let mut future = pin!(future);
let ret = loop {
match future.as_mut().poll(&mut cx) {
Poll::Pending => pending += 1,
Poll::Ready(r) => {
break r;
}
}
};
JoinHandle::new((ret, pending))
}
async fn yield_now() {
struct SimpleYieldNow {
yielded: bool,
}
impl Future for SimpleYieldNow {
type Output = ();
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
cx.waker().wake_by_ref();
if self.yielded {
return Poll::Ready(());
}
self.yielded = true;
Poll::Pending
}
}
SimpleYieldNow { yielded: false }.await;
}
pub const fn noop_waker() -> Waker {
const VTABLE: RawWakerVTable = RawWakerVTable::new(
// Cloning just returns a new no-op raw waker
|_| RAW,
// `wake` does nothing
|_| {},
// `wake_by_ref` does nothing
|_| {},
// Dropping does nothing as we don't allocate anything
|_| {},
);
const RAW: RawWaker = RawWaker::new(ptr::null(), &VTABLE);
unsafe { Waker::from_raw(RAW) }
}
struct JoinHandle<T> {
data: Option<T>,
}
impl<T> JoinHandle<T> {
#[must_use]
const fn new(data: T) -> Self {
Self { data: Some(data) }
}
const fn join(&mut self) -> T {
self.data.take().unwrap()
}
}
impl<T: Unpin> Future for JoinHandle<T> {
type Output = std::result::Result<T, ()>;
fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Self::Output> {
let data = self.get_mut().data.take().unwrap();
Poll::Ready(Ok(data))
}
}

View file

@ -16,7 +16,7 @@ use crate::{
LOG_GRAPH_ENABLED, START_SEED, LOG_GRAPH_ENABLED, START_SEED,
}, },
parse::perform_edit, parse::perform_edit,
test::{parse_tests, strip_sexp_fields, DiffKey, TestDiff}, test::{parse_tests, print_diff, print_diff_key, strip_sexp_fields},
tests::{ tests::{
allocations, allocations,
helpers::fixtures::{fixtures_dir, get_language, get_test_language, SCRATCH_BASE_DIR}, helpers::fixtures::{fixtures_dir, get_language, get_test_language, SCRATCH_BASE_DIR},
@ -209,8 +209,8 @@ pub fn test_language_corpus(
if actual_output != test.output { if actual_output != test.output {
println!("Incorrect initial parse for {test_name}"); println!("Incorrect initial parse for {test_name}");
DiffKey::print(); print_diff_key();
println!("{}", TestDiff::new(&actual_output, &test.output)); print_diff(&actual_output, &test.output, true);
println!(); println!();
return false; return false;
} }
@ -297,8 +297,8 @@ pub fn test_language_corpus(
if actual_output != test.output { if actual_output != test.output {
println!("Incorrect parse for {test_name} - seed {seed}"); println!("Incorrect parse for {test_name} - seed {seed}");
DiffKey::print(); print_diff_key();
println!("{}", TestDiff::new(&actual_output, &test.output)); print_diff(&actual_output, &test.output, true);
println!(); println!();
return false; return false;
} }
@ -428,8 +428,8 @@ fn test_feature_corpus_files() {
if actual_output == test.output { if actual_output == test.output {
true true
} else { } else {
DiffKey::print(); print_diff_key();
print!("{}", TestDiff::new(&actual_output, &test.output)); print_diff(&actual_output, &test.output, true);
println!(); println!();
false false
} }

View file

@ -90,7 +90,7 @@ fn detect_language_by_first_line_regex() {
} }
#[test] #[test]
fn detect_language_by_double_barrel_file_extension() { fn detect_langauge_by_double_barrel_file_extension() {
let blade_dir = tree_sitter_dir( let blade_dir = tree_sitter_dir(
r#"{ r#"{
"grammars": [ "grammars": [

View file

@ -12,7 +12,7 @@ pub struct Pattern {
named: bool, named: bool,
field: Option<&'static str>, field: Option<&'static str>,
capture: Option<String>, capture: Option<String>,
children: Vec<Self>, children: Vec<Pattern>,
} }
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
@ -225,7 +225,7 @@ impl Pattern {
} }
// Find every matching combination of child patterns and child nodes. // Find every matching combination of child patterns and child nodes.
let mut finished_matches = Vec::<Match<'_, 'tree>>::new(); let mut finished_matches = Vec::<Match>::new();
if cursor.goto_first_child() { if cursor.goto_first_child() {
let mut match_states = vec![(0, mat)]; let mut match_states = vec![(0, mat)];
loop { loop {

View file

@ -481,7 +481,7 @@ fn test_highlighting_cancellation() {
// The initial `highlight` call, which eagerly parses the outer document, should not fail. // The initial `highlight` call, which eagerly parses the outer document, should not fail.
let mut highlighter = Highlighter::new(); let mut highlighter = Highlighter::new();
let mut events = highlighter let events = highlighter
.highlight( .highlight(
&HTML_HIGHLIGHT, &HTML_HIGHLIGHT,
source.as_bytes(), source.as_bytes(),
@ -492,18 +492,14 @@ fn test_highlighting_cancellation() {
// Iterating the scopes should not panic. It should return an error once the // Iterating the scopes should not panic. It should return an error once the
// cancellation is detected. // cancellation is detected.
let found_cancellation_error = events.any(|event| match event { for event in events {
Ok(_) => false, if let Err(e) = event {
Err(Error::Cancelled) => true, assert_eq!(e, Error::Cancelled);
Err(Error::InvalidLanguage | Error::Unknown) => { return;
unreachable!("Unexpected error type while iterating events")
} }
}); }
assert!( panic!("Expected an error while iterating highlighter");
found_cancellation_error,
"Expected a cancellation error while iterating events"
);
} }
#[test] #[test]

View file

@ -0,0 +1,104 @@
// For some reasons `Command::spawn` doesn't work in CI env for many exotic arches.
#![cfg(all(any(target_arch = "x86_64", target_arch = "x86"), not(sanitizing)))]
use std::{
env::VarError,
process::{Command, Stdio},
};
use tree_sitter::Parser;
use tree_sitter_generate::load_grammar_file;
use super::generate_parser;
use crate::tests::helpers::fixtures::{fixtures_dir, get_test_language};
// The `sanitizing` cfg is required to don't run tests under specific sunitizer
// because they don't work well with subprocesses _(it's an assumption)_.
//
// Below are two alternative examples of how to disable tests for some arches
// if a way with excluding the whole mod from compilation wouldn't work well.
//
// XXX: Also may be it makes sense to keep such tests as ignored by default
// to omit surprises and enable them on CI by passing an extra option explicitly:
//
// > cargo test -- --include-ignored
//
// #[cfg(all(any(target_arch = "x86_64", target_arch = "x86"), not(sanitizing)))]
// #[cfg_attr(not(all(any(target_arch = "x86_64", target_arch = "x86"), not(sanitizing))), ignore)]
//
#[test]
fn test_grammar_that_should_hang_and_not_segfault() {
let parent_sleep_millis = 1000;
let test_name = "test_grammar_that_should_hang_and_not_segfault";
let test_var = "CARGO_HANG_TEST";
eprintln!(" {test_name}");
let tests_exec_path = std::env::args()
.next()
.expect("Failed to get tests executable path");
match std::env::var(test_var) {
Ok(v) if v == test_name => {
eprintln!(" child process id {}", std::process::id());
hang_test();
}
Err(VarError::NotPresent) => {
eprintln!(" parent process id {}", std::process::id());
let mut command = Command::new(tests_exec_path);
command.arg(test_name).env(test_var, test_name);
if std::env::args().any(|x| x == "--nocapture") {
command.arg("--nocapture");
} else {
command.stdout(Stdio::null()).stderr(Stdio::null());
}
match command.spawn() {
Ok(mut child) => {
std::thread::sleep(std::time::Duration::from_millis(parent_sleep_millis));
match child.try_wait() {
Ok(Some(status)) if status.success() => {
panic!("Child didn't hang and exited successfully")
}
Ok(Some(status)) => panic!(
"Child didn't hang and exited with status code: {:?}",
status.code()
),
_ => (),
}
if let Err(e) = child.kill() {
eprintln!(
"Failed to kill hang test's process id: {}, error: {e}",
child.id()
);
}
}
Err(e) => panic!("{e}"),
}
}
Err(e) => panic!("Env var error: {e}"),
_ => unreachable!(),
}
}
fn hang_test() {
let test_grammar_dir = fixtures_dir()
.join("test_grammars")
.join("get_col_should_hang_not_crash");
let grammar_json = load_grammar_file(&test_grammar_dir.join("grammar.js"), None).unwrap();
let (parser_name, parser_code) = generate_parser(grammar_json.as_str()).unwrap();
let language = get_test_language(&parser_name, &parser_code, Some(test_grammar_dir.as_path()));
let mut parser = Parser::new();
parser.set_language(&language).unwrap();
let code_that_should_hang = "\nHello";
parser.parse(code_that_should_hang, None).unwrap();
}

View file

@ -1,17 +1,12 @@
use std::{ use std::{
ops::ControlFlow, ops::ControlFlow,
sync::{ sync::atomic::{AtomicUsize, Ordering},
atomic::{AtomicUsize, Ordering}, thread, time,
mpsc,
},
thread,
time::{self, Duration},
}; };
use tree_sitter::{ use tree_sitter::{
Decode, IncludedRangesError, InputEdit, LogType, ParseOptions, ParseState, Parser, Point, Range, Decode, IncludedRangesError, InputEdit, LogType, ParseOptions, ParseState, Parser, Point, Range,
}; };
use tree_sitter_generate::load_grammar_file;
use tree_sitter_proc_macro::retry; use tree_sitter_proc_macro::retry;
use super::helpers::{ use super::helpers::{
@ -22,11 +17,7 @@ use super::helpers::{
use crate::{ use crate::{
fuzz::edits::Edit, fuzz::edits::Edit,
parse::perform_edit, parse::perform_edit,
tests::{ tests::{generate_parser, helpers::fixtures::get_test_fixture_language, invert_edit},
generate_parser,
helpers::fixtures::{fixtures_dir, get_test_fixture_language},
invert_edit,
},
}; };
#[test] #[test]
@ -2134,46 +2125,3 @@ fn test_parse_options_reborrow() {
assert!(parse_count.load(Ordering::SeqCst) > 0); assert!(parse_count.load(Ordering::SeqCst) > 0);
} }
#[test]
fn test_grammar_that_should_hang_and_not_segfault() {
fn hang_test() {
let test_grammar_dir = fixtures_dir()
.join("test_grammars")
.join("get_col_should_hang_not_crash");
let grammar_json = load_grammar_file(&test_grammar_dir.join("grammar.js"), None)
.expect("Failed to load grammar file");
let (parser_name, parser_code) =
generate_parser(grammar_json.as_str()).expect("Failed to generate parser");
let language =
get_test_language(&parser_name, &parser_code, Some(test_grammar_dir.as_path()));
let mut parser = Parser::new();
parser
.set_language(&language)
.expect("Failed to set parser language");
let code_that_should_hang = "\nHello";
parser
.parse(code_that_should_hang, None)
.expect("Parse operation completed unexpectedly");
}
let timeout = Duration::from_millis(500);
let (tx, rx) = mpsc::channel();
thread::spawn(move || tx.send(std::panic::catch_unwind(hang_test)));
match rx.recv_timeout(timeout) {
Ok(Ok(())) => panic!("The test completed rather than hanging"),
Ok(Err(panic_info)) => panic!("The test panicked unexpectedly: {panic_info:?}"),
Err(mpsc::RecvTimeoutError::Timeout) => {} // Expected
Err(mpsc::RecvTimeoutError::Disconnected) => {
panic!("The test thread disconnected unexpectedly")
}
}
}

View file

@ -8,7 +8,6 @@ use tree_sitter::{
QueryCursorOptions, QueryError, QueryErrorKind, QueryPredicate, QueryPredicateArg, QueryCursorOptions, QueryError, QueryErrorKind, QueryPredicate, QueryPredicateArg,
QueryProperty, Range, QueryProperty, Range,
}; };
use tree_sitter_generate::load_grammar_file;
use unindent::Unindent; use unindent::Unindent;
use super::helpers::{ use super::helpers::{
@ -238,20 +237,6 @@ fn test_query_errors_on_invalid_syntax() {
] ]
.join("\n") .join("\n")
); );
assert_eq!(
Query::new(&language, "(statement / export_statement)").unwrap_err(),
QueryError {
row: 0,
offset: 11,
column: 11,
kind: QueryErrorKind::Syntax,
message: [
"(statement / export_statement)", //
" ^"
]
.join("\n")
}
);
}); });
} }
@ -430,11 +415,11 @@ fn test_query_errors_on_impossible_patterns() {
Err(QueryError { Err(QueryError {
kind: QueryErrorKind::Structure, kind: QueryErrorKind::Structure,
row: 0, row: 0,
offset: 37, offset: 51,
column: 37, column: 51,
message: [ message: [
"(binary_expression left: (expression (identifier)) left: (expression (identifier)))", "(binary_expression left: (expression (identifier)) left: (expression (identifier)))",
" ^", " ^",
] ]
.join("\n"), .join("\n"),
}) })
@ -2669,64 +2654,6 @@ fn test_query_matches_within_range_of_long_repetition() {
}); });
} }
#[test]
fn test_query_matches_contained_within_range() {
allocations::record(|| {
let language = get_language("json");
let query = Query::new(
&language,
r#"
("[" @l_bracket "]" @r_bracket)
("{" @l_brace "}" @r_brace)
"#,
)
.unwrap();
let source = r#"
[
{"key1": "value1"},
{"key2": "value2"},
{"key3": "value3"},
{"key4": "value4"},
{"key5": "value5"},
{"key6": "value6"},
{"key7": "value7"},
{"key8": "value8"},
{"key9": "value9"},
{"key10": "value10"},
{"key11": "value11"},
{"key12": "value12"},
]
"#
.unindent();
let mut parser = Parser::new();
parser.set_language(&language).unwrap();
let tree = parser.parse(&source, None).unwrap();
let expected_matches = [
(1, vec![("l_brace", "{"), ("r_brace", "}")]),
(1, vec![("l_brace", "{"), ("r_brace", "}")]),
];
{
let mut cursor = QueryCursor::new();
let matches = cursor
.set_containing_point_range(Point::new(5, 0)..Point::new(7, 0))
.matches(&query, tree.root_node(), source.as_bytes());
assert_eq!(collect_matches(matches, &query, &source), &expected_matches);
}
{
let mut cursor = QueryCursor::new();
let matches = cursor.set_containing_byte_range(78..120).matches(
&query,
tree.root_node(),
source.as_bytes(),
);
assert_eq!(collect_matches(matches, &query, &source), &expected_matches);
}
});
}
#[test] #[test]
fn test_query_matches_different_queries_same_cursor() { fn test_query_matches_different_queries_same_cursor() {
allocations::record(|| { allocations::record(|| {
@ -4259,9 +4186,12 @@ fn test_query_random() {
let pattern = pattern_ast.to_string(); let pattern = pattern_ast.to_string();
let expected_matches = pattern_ast.matches_in_tree(&test_tree); let expected_matches = pattern_ast.matches_in_tree(&test_tree);
let query = Query::new(&language, &pattern).unwrap_or_else(|e| { let query = match Query::new(&language, &pattern) {
panic!("failed to build query for pattern {pattern}. seed: {seed}\n{e}") Ok(query) => query,
}); Err(e) => {
panic!("failed to build query for pattern {pattern} - {e}. seed: {seed}");
}
};
let mut actual_matches = Vec::new(); let mut actual_matches = Vec::new();
let mut match_iter = cursor.matches( let mut match_iter = cursor.matches(
&query, &query,
@ -5090,26 +5020,6 @@ fn test_query_quantified_captures() {
("comment.documentation", "// quuz"), ("comment.documentation", "// quuz"),
], ],
}, },
Row {
description: "multiple quantifiers should not hang query parsing",
language: get_language("c"),
code: indoc! {"
// foo
// bar
// baz
"},
pattern: r"
((comment) ?+ @comment)
",
// This should be identical to the `*` quantifier.
captures: &[
("comment", "// foo"),
("comment", "// foo"),
("comment", "// foo"),
("comment", "// bar"),
("comment", "// baz"),
],
},
]; ];
allocations::record(|| { allocations::record(|| {
@ -5855,109 +5765,3 @@ fn test_query_allows_error_nodes_with_children() {
assert_eq!(matches, &[(0, vec![("error", ".bar")])]); assert_eq!(matches, &[(0, vec![("error", ".bar")])]);
}); });
} }
#[test]
fn test_query_assertion_on_unreachable_node_with_child() {
// The `await_binding` rule is unreachable because it has a lower precedence than
// `identifier`, so we'll always reduce to an expression of type `identifier`
// instead whenever we see the token `await` followed by an identifier.
//
// A query that tries to capture the `await` token in the `await_binding` rule
// should not cause an assertion failure during query analysis.
let grammar = r#"
export default grammar({
name: "query_assertion_crash",
rules: {
source_file: $ => repeat($.expression),
expression: $ => choice(
$.await_binding,
$.await_expr,
$.equal_expr,
prec(3, $.identifier),
),
await_binding: $ => prec(1, seq('await', $.identifier, '=', $.expression)),
await_expr: $ => prec(1, seq('await', $.expression)),
equal_expr: $ => prec.right(2, seq($.expression, '=', $.expression)),
identifier: _ => /[a-z]+/,
}
});
"#;
let file = tempfile::NamedTempFile::with_suffix(".js").unwrap();
std::fs::write(file.path(), grammar).unwrap();
let grammar_json = load_grammar_file(file.path(), None).unwrap();
let (parser_name, parser_code) = generate_parser(&grammar_json).unwrap();
let language = get_test_language(&parser_name, &parser_code, None);
let query_result = Query::new(&language, r#"(await_binding "await")"#);
assert!(query_result.is_err());
assert_eq!(
query_result.unwrap_err(),
QueryError {
kind: QueryErrorKind::Structure,
row: 0,
offset: 0,
column: 0,
message: ["(await_binding \"await\")", "^"].join("\n"),
}
);
}
#[test]
fn test_query_supertype_with_anonymous_node() {
let grammar = r#"
export default grammar({
name: "supertype_anonymous_test",
extras: $ => [/\s/, $.comment],
supertypes: $ => [$.expression],
word: $ => $.identifier,
rules: {
source_file: $ => repeat($.expression),
expression: $ => choice(
$.function_call,
'()' // an empty tuple, which should be queryable with the supertype syntax
),
function_call: $ => seq($.identifier, '()'),
identifier: _ => /[a-zA-Z_][a-zA-Z0-9_]*/,
comment: _ => token(seq('//', /.*/)),
}
});
"#;
let file = tempfile::NamedTempFile::with_suffix(".js").unwrap();
std::fs::write(file.path(), grammar).unwrap();
let grammar_json = load_grammar_file(file.path(), None).unwrap();
let (parser_name, parser_code) = generate_parser(&grammar_json).unwrap();
let language = get_test_language(&parser_name, &parser_code, None);
let query_result = Query::new(&language, r#"(expression/"()") @tuple"#);
assert!(query_result.is_ok());
let query = query_result.unwrap();
let source = "foo()\n()";
assert_query_matches(&language, &query, source, &[(0, vec![("tuple", "()")])]);
}

View file

@ -1,7 +1,6 @@
use std::{ use std::{
ffi::{CStr, CString}, ffi::{CStr, CString},
fs, ptr, slice, str, fs, ptr, slice, str,
sync::atomic::{AtomicUsize, Ordering},
}; };
use tree_sitter::Point; use tree_sitter::Point;
@ -263,34 +262,34 @@ fn test_tags_ruby() {
#[test] #[test]
fn test_tags_cancellation() { fn test_tags_cancellation() {
use std::sync::atomic::{AtomicUsize, Ordering};
allocations::record(|| { allocations::record(|| {
// Large javascript document // Large javascript document
let source = "/* hi */ class A { /* ok */ b() {} }\n".repeat(500); let source = (0..500)
.map(|_| "/* hi */ class A { /* ok */ b() {} }\n")
.collect::<String>();
let cancellation_flag = AtomicUsize::new(0); let cancellation_flag = AtomicUsize::new(0);
let language = get_language("javascript"); let language = get_language("javascript");
let tags_config = TagsConfiguration::new(language, JS_TAG_QUERY, "").unwrap(); let tags_config = TagsConfiguration::new(language, JS_TAG_QUERY, "").unwrap();
let mut tag_context = TagsContext::new(); let mut tag_context = TagsContext::new();
let tags = tag_context let tags = tag_context
.generate_tags(&tags_config, source.as_bytes(), Some(&cancellation_flag)) .generate_tags(&tags_config, source.as_bytes(), Some(&cancellation_flag))
.unwrap(); .unwrap();
let found_cancellation_error = tags.0.enumerate().any(|(i, tag)| { for (i, tag) in tags.0.enumerate() {
if i == 150 { if i == 150 {
cancellation_flag.store(1, Ordering::SeqCst); cancellation_flag.store(1, Ordering::SeqCst);
} }
match tag { if let Err(e) = tag {
Ok(_) => false, assert_eq!(e, Error::Cancelled);
Err(Error::Cancelled) => true, return;
Err(e) => {
unreachable!("Unexpected error type while iterating tags: {e}")
}
} }
}); }
assert!( panic!("Expected to halt tagging with an error");
found_cancellation_error,
"Expected to halt tagging with a cancellation error"
);
}); });
} }

View file

@ -1,5 +1,6 @@
use std::{fs, path::PathBuf, process::Command}; use std::{fs, path::PathBuf, process::Command};
use anyhow::{anyhow, Context, Result};
use clap::ValueEnum; use clap::ValueEnum;
use log::{info, warn}; use log::{info, warn};
use regex::Regex; use regex::Regex;
@ -21,36 +22,6 @@ pub struct Version {
pub bump: Option<BumpLevel>, pub bump: Option<BumpLevel>,
} }
#[derive(thiserror::Error, Debug)]
pub enum VersionError {
#[error(transparent)]
Json(#[from] serde_json::Error),
#[error(transparent)]
Io(#[from] std::io::Error),
#[error("Failed to update one or more files:\n\n{0}")]
Update(UpdateErrors),
}
#[derive(thiserror::Error, Debug)]
pub struct UpdateErrors(Vec<UpdateError>);
impl std::fmt::Display for UpdateErrors {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for error in &self.0 {
writeln!(f, "{error}\n")?;
}
Ok(())
}
}
#[derive(thiserror::Error, Debug)]
pub enum UpdateError {
#[error("Failed to update {1}:\n{0}")]
Io(std::io::Error, PathBuf),
#[error("Failed to run `{0}`:\n{1}")]
Command(&'static str, String),
}
impl Version { impl Version {
#[must_use] #[must_use]
pub const fn new( pub const fn new(
@ -65,7 +36,7 @@ impl Version {
} }
} }
pub fn run(mut self) -> Result<(), VersionError> { pub fn run(mut self) -> Result<()> {
let tree_sitter_json = self.current_dir.join("tree-sitter.json"); let tree_sitter_json = self.current_dir.join("tree-sitter.json");
let tree_sitter_json = let tree_sitter_json =
@ -113,101 +84,98 @@ impl Version {
let is_multigrammar = tree_sitter_json.grammars.len() > 1; let is_multigrammar = tree_sitter_json.grammars.len() > 1;
let mut errors = Vec::new(); self.update_treesitter_json().with_context(|| {
format!(
// Helper to push errors into the errors vector, returns true if an error was pushed "Failed to update tree-sitter.json at {}",
let mut push_err = |result: Result<(), UpdateError>| -> bool { self.current_dir.display()
if let Err(e) = result { )
errors.push(e); })?;
return true; self.update_cargo_toml().with_context(|| {
} format!(
false "Failed to update Cargo.toml at {}",
}; self.current_dir.display()
)
push_err(self.update_treesitter_json()); })?;
self.update_package_json().with_context(|| {
// Only update Cargo.lock if Cargo.toml was updated format!(
push_err(self.update_cargo_toml()).then(|| push_err(self.update_cargo_lock())); "Failed to update package.json at {}",
self.current_dir.display()
// Only update package-lock.json if package.json was updated )
push_err(self.update_package_json()).then(|| push_err(self.update_package_lock_json())); })?;
self.update_makefile(is_multigrammar).with_context(|| {
push_err(self.update_makefile(is_multigrammar)); format!(
push_err(self.update_cmakelists_txt()); "Failed to update Makefile at {}",
push_err(self.update_pyproject_toml()); self.current_dir.display()
push_err(self.update_zig_zon()); )
})?;
if errors.is_empty() { self.update_cmakelists_txt().with_context(|| {
Ok(()) format!(
} else { "Failed to update CMakeLists.txt at {}",
Err(VersionError::Update(UpdateErrors(errors))) self.current_dir.display()
} )
} })?;
self.update_pyproject_toml().with_context(|| {
fn update_file_with<F>(&self, path: &PathBuf, update_fn: F) -> Result<(), UpdateError> format!(
where "Failed to update pyproject.toml at {}",
F: Fn(&str) -> String, self.current_dir.display()
{ )
let content = fs::read_to_string(path).map_err(|e| UpdateError::Io(e, path.clone()))?;
let updated_content = update_fn(&content);
fs::write(path, updated_content).map_err(|e| UpdateError::Io(e, path.clone()))
}
fn update_treesitter_json(&self) -> Result<(), UpdateError> {
let json_path = self.current_dir.join("tree-sitter.json");
self.update_file_with(&json_path, |content| {
content
.lines()
.map(|line| {
if line.contains("\"version\":") {
let prefix_index =
line.find("\"version\":").unwrap() + "\"version\":".len();
let start_quote =
line[prefix_index..].find('"').unwrap() + prefix_index + 1;
let end_quote =
line[start_quote + 1..].find('"').unwrap() + start_quote + 1;
format!(
"{}{}{}",
&line[..start_quote],
self.version.as_ref().unwrap(),
&line[end_quote..]
)
} else {
line.to_string()
}
})
.collect::<Vec<_>>()
.join("\n")
+ "\n"
})
}
fn update_cargo_toml(&self) -> Result<(), UpdateError> {
let cargo_toml_path = self.current_dir.join("Cargo.toml");
if !cargo_toml_path.exists() {
return Ok(());
}
self.update_file_with(&cargo_toml_path, |content| {
content
.lines()
.map(|line| {
if line.starts_with("version =") {
format!("version = \"{}\"", self.version.as_ref().unwrap())
} else {
line.to_string()
}
})
.collect::<Vec<_>>()
.join("\n")
+ "\n"
})?; })?;
Ok(()) Ok(())
} }
fn update_cargo_lock(&self) -> Result<(), UpdateError> { fn update_treesitter_json(&self) -> Result<()> {
let tree_sitter_json = &fs::read_to_string(self.current_dir.join("tree-sitter.json"))?;
let tree_sitter_json = tree_sitter_json
.lines()
.map(|line| {
if line.contains("\"version\":") {
let prefix_index = line.find("\"version\":").unwrap() + "\"version\":".len();
let start_quote = line[prefix_index..].find('"').unwrap() + prefix_index + 1;
let end_quote = line[start_quote + 1..].find('"').unwrap() + start_quote + 1;
format!(
"{}{}{}",
&line[..start_quote],
self.version.as_ref().unwrap(),
&line[end_quote..]
)
} else {
line.to_string()
}
})
.collect::<Vec<_>>()
.join("\n")
+ "\n";
fs::write(self.current_dir.join("tree-sitter.json"), tree_sitter_json)?;
Ok(())
}
fn update_cargo_toml(&self) -> Result<()> {
if !self.current_dir.join("Cargo.toml").exists() {
return Ok(());
}
let cargo_toml = fs::read_to_string(self.current_dir.join("Cargo.toml"))?;
let cargo_toml = cargo_toml
.lines()
.map(|line| {
if line.starts_with("version =") {
format!("version = \"{}\"", self.version.as_ref().unwrap())
} else {
line.to_string()
}
})
.collect::<Vec<_>>()
.join("\n")
+ "\n";
fs::write(self.current_dir.join("Cargo.toml"), cargo_toml)?;
if self.current_dir.join("Cargo.lock").exists() { if self.current_dir.join("Cargo.lock").exists() {
let Ok(cmd) = Command::new("cargo") let Ok(cmd) = Command::new("cargo")
.arg("generate-lockfile") .arg("generate-lockfile")
@ -220,9 +188,8 @@ impl Version {
if !cmd.status.success() { if !cmd.status.success() {
let stderr = String::from_utf8_lossy(&cmd.stderr); let stderr = String::from_utf8_lossy(&cmd.stderr);
return Err(UpdateError::Command( return Err(anyhow!(
"cargo generate-lockfile", "Failed to run `cargo generate-lockfile`:\n{stderr}"
stderr.to_string(),
)); ));
} }
} }
@ -230,43 +197,37 @@ impl Version {
Ok(()) Ok(())
} }
fn update_package_json(&self) -> Result<(), UpdateError> { fn update_package_json(&self) -> Result<()> {
let package_json_path = self.current_dir.join("package.json"); if !self.current_dir.join("package.json").exists() {
if !package_json_path.exists() {
return Ok(()); return Ok(());
} }
self.update_file_with(&package_json_path, |content| { let package_json = &fs::read_to_string(self.current_dir.join("package.json"))?;
content
.lines()
.map(|line| {
if line.contains("\"version\":") {
let prefix_index =
line.find("\"version\":").unwrap() + "\"version\":".len();
let start_quote =
line[prefix_index..].find('"').unwrap() + prefix_index + 1;
let end_quote =
line[start_quote + 1..].find('"').unwrap() + start_quote + 1;
format!( let package_json = package_json
"{}{}{}", .lines()
&line[..start_quote], .map(|line| {
self.version.as_ref().unwrap(), if line.contains("\"version\":") {
&line[end_quote..] let prefix_index = line.find("\"version\":").unwrap() + "\"version\":".len();
) let start_quote = line[prefix_index..].find('"').unwrap() + prefix_index + 1;
} else { let end_quote = line[start_quote + 1..].find('"').unwrap() + start_quote + 1;
line.to_string()
}
})
.collect::<Vec<_>>()
.join("\n")
+ "\n"
})?;
Ok(()) format!(
} "{}{}{}",
&line[..start_quote],
self.version.as_ref().unwrap(),
&line[end_quote..]
)
} else {
line.to_string()
}
})
.collect::<Vec<_>>()
.join("\n")
+ "\n";
fs::write(self.current_dir.join("package.json"), package_json)?;
fn update_package_lock_json(&self) -> Result<(), UpdateError> {
if self.current_dir.join("package-lock.json").exists() { if self.current_dir.join("package-lock.json").exists() {
let Ok(cmd) = Command::new("npm") let Ok(cmd) = Command::new("npm")
.arg("install") .arg("install")
@ -279,117 +240,82 @@ impl Version {
if !cmd.status.success() { if !cmd.status.success() {
let stderr = String::from_utf8_lossy(&cmd.stderr); let stderr = String::from_utf8_lossy(&cmd.stderr);
return Err(UpdateError::Command("npm install", stderr.to_string())); return Err(anyhow!("Failed to run `npm install`:\n{stderr}"));
} }
} }
Ok(()) Ok(())
} }
fn update_makefile(&self, is_multigrammar: bool) -> Result<(), UpdateError> { fn update_makefile(&self, is_multigrammar: bool) -> Result<()> {
let makefile_path = if is_multigrammar { let makefile = if is_multigrammar {
self.current_dir.join("common").join("common.mak") if !self.current_dir.join("common").join("common.mak").exists() {
return Ok(());
}
fs::read_to_string(self.current_dir.join("Makefile"))?
} else { } else {
self.current_dir.join("Makefile") if !self.current_dir.join("Makefile").exists() {
return Ok(());
}
fs::read_to_string(self.current_dir.join("Makefile"))?
}; };
self.update_file_with(&makefile_path, |content| { let makefile = makefile
content .lines()
.lines() .map(|line| {
.map(|line| { if line.starts_with("VERSION") {
if line.starts_with("VERSION") { format!("VERSION := {}", self.version.as_ref().unwrap())
format!("VERSION := {}", self.version.as_ref().unwrap()) } else {
} else { line.to_string()
line.to_string() }
} })
}) .collect::<Vec<_>>()
.collect::<Vec<_>>() .join("\n")
.join("\n") + "\n";
+ "\n"
})?; fs::write(self.current_dir.join("Makefile"), makefile)?;
Ok(()) Ok(())
} }
fn update_cmakelists_txt(&self) -> Result<(), UpdateError> { fn update_cmakelists_txt(&self) -> Result<()> {
let cmake_lists_path = self.current_dir.join("CMakeLists.txt"); if !self.current_dir.join("CMakeLists.txt").exists() {
if !cmake_lists_path.exists() {
return Ok(()); return Ok(());
} }
self.update_file_with(&cmake_lists_path, |content| { let cmake = fs::read_to_string(self.current_dir.join("CMakeLists.txt"))?;
let re = Regex::new(r#"(\s*VERSION\s+)"[0-9]+\.[0-9]+\.[0-9]+""#)
.expect("Failed to compile regex"); let re = Regex::new(r#"(\s*VERSION\s+)"[0-9]+\.[0-9]+\.[0-9]+""#)?;
re.replace( let cmake = re.replace(&cmake, format!(r#"$1"{}""#, self.version.as_ref().unwrap()));
content,
format!(r#"$1"{}""#, self.version.as_ref().unwrap()), fs::write(self.current_dir.join("CMakeLists.txt"), cmake.as_bytes())?;
)
.to_string()
})?;
Ok(()) Ok(())
} }
fn update_pyproject_toml(&self) -> Result<(), UpdateError> { fn update_pyproject_toml(&self) -> Result<()> {
let pyproject_toml_path = self.current_dir.join("pyproject.toml"); if !self.current_dir.join("pyproject.toml").exists() {
if !pyproject_toml_path.exists() {
return Ok(()); return Ok(());
} }
self.update_file_with(&pyproject_toml_path, |content| { let pyproject_toml = fs::read_to_string(self.current_dir.join("pyproject.toml"))?;
content
.lines()
.map(|line| {
if line.starts_with("version =") {
format!("version = \"{}\"", self.version.as_ref().unwrap())
} else {
line.to_string()
}
})
.collect::<Vec<_>>()
.join("\n")
+ "\n"
})?;
Ok(()) let pyproject_toml = pyproject_toml
} .lines()
.map(|line| {
if line.starts_with("version =") {
format!("version = \"{}\"", self.version.as_ref().unwrap())
} else {
line.to_string()
}
})
.collect::<Vec<_>>()
.join("\n")
+ "\n";
fn update_zig_zon(&self) -> Result<(), UpdateError> { fs::write(self.current_dir.join("pyproject.toml"), pyproject_toml)?;
let zig_zon_path = self.current_dir.join("build.zig.zon");
if !zig_zon_path.exists() {
return Ok(());
}
self.update_file_with(&zig_zon_path, |content| {
let zig_version_prefix = ".version =";
content
.lines()
.map(|line| {
if line
.trim_start_matches(|c: char| c.is_ascii_whitespace())
.starts_with(zig_version_prefix)
{
let prefix_index =
line.find(zig_version_prefix).unwrap() + zig_version_prefix.len();
let start_quote =
line[prefix_index..].find('"').unwrap() + prefix_index + 1;
let end_quote =
line[start_quote + 1..].find('"').unwrap() + start_quote + 1;
format!(
"{}{}{}",
&line[..start_quote],
self.version.as_ref().unwrap(),
&line[end_quote..]
)
} else {
line.to_string()
}
})
.collect::<Vec<_>>()
.join("\n")
+ "\n"
})?;
Ok(()) Ok(())
} }

View file

@ -20,8 +20,8 @@ path = "src/tree_sitter_config.rs"
workspace = true workspace = true
[dependencies] [dependencies]
anyhow.workspace = true
etcetera.workspace = true etcetera.workspace = true
log.workspace = true log.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
thiserror.workspace = true

View file

@ -1,54 +1,12 @@
#![cfg_attr(not(any(test, doctest)), doc = include_str!("../README.md"))] #![cfg_attr(not(any(test, doctest)), doc = include_str!("../README.md"))]
use std::{ use std::{env, fs, path::PathBuf};
env, fs,
path::{Path, PathBuf},
};
use anyhow::{Context, Result};
use etcetera::BaseStrategy as _; use etcetera::BaseStrategy as _;
use log::warn; use log::warn;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value; use serde_json::Value;
use thiserror::Error;
pub type ConfigResult<T> = Result<T, ConfigError>;
#[derive(Debug, Error)]
pub enum ConfigError {
#[error("Bad JSON config {0} -- {1}")]
ConfigRead(String, serde_json::Error),
#[error(transparent)]
HomeDir(#[from] etcetera::HomeDirError),
#[error(transparent)]
IO(IoError),
#[error(transparent)]
Serialization(#[from] serde_json::Error),
}
#[derive(Debug, Error)]
pub struct IoError {
pub error: std::io::Error,
pub path: Option<String>,
}
impl IoError {
fn new(error: std::io::Error, path: Option<&Path>) -> Self {
Self {
error,
path: path.map(|p| p.to_string_lossy().to_string()),
}
}
}
impl std::fmt::Display for IoError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.error)?;
if let Some(ref path) = self.path {
write!(f, " ({path})")?;
}
Ok(())
}
}
/// Holds the contents of tree-sitter's configuration file. /// Holds the contents of tree-sitter's configuration file.
/// ///
@ -65,7 +23,7 @@ pub struct Config {
} }
impl Config { impl Config {
pub fn find_config_file() -> ConfigResult<Option<PathBuf>> { pub fn find_config_file() -> Result<Option<PathBuf>> {
if let Ok(path) = env::var("TREE_SITTER_DIR") { if let Ok(path) = env::var("TREE_SITTER_DIR") {
let mut path = PathBuf::from(path); let mut path = PathBuf::from(path);
path.push("config.json"); path.push("config.json");
@ -88,12 +46,8 @@ impl Config {
.join("tree-sitter") .join("tree-sitter")
.join("config.json"); .join("config.json");
if legacy_apple_path.is_file() { if legacy_apple_path.is_file() {
let xdg_dir = xdg_path.parent().unwrap(); fs::create_dir_all(xdg_path.parent().unwrap())?;
fs::create_dir_all(xdg_dir) fs::rename(&legacy_apple_path, &xdg_path)?;
.map_err(|e| ConfigError::IO(IoError::new(e, Some(xdg_dir))))?;
fs::rename(&legacy_apple_path, &xdg_path).map_err(|e| {
ConfigError::IO(IoError::new(e, Some(legacy_apple_path.as_path())))
})?;
warn!( warn!(
"Your config.json file has been automatically migrated from \"{}\" to \"{}\"", "Your config.json file has been automatically migrated from \"{}\" to \"{}\"",
legacy_apple_path.display(), legacy_apple_path.display(),
@ -113,7 +67,7 @@ impl Config {
Ok(None) Ok(None)
} }
fn xdg_config_file() -> ConfigResult<PathBuf> { fn xdg_config_file() -> Result<PathBuf> {
let xdg_path = etcetera::choose_base_strategy()? let xdg_path = etcetera::choose_base_strategy()?
.config_dir() .config_dir()
.join("tree-sitter") .join("tree-sitter")
@ -130,7 +84,7 @@ impl Config {
/// [`etcetera::choose_base_strategy`](https://docs.rs/etcetera/*/etcetera/#basestrategy) /// [`etcetera::choose_base_strategy`](https://docs.rs/etcetera/*/etcetera/#basestrategy)
/// - `$HOME/.tree-sitter/config.json` as a fallback from where tree-sitter _used_ to store /// - `$HOME/.tree-sitter/config.json` as a fallback from where tree-sitter _used_ to store
/// its configuration /// its configuration
pub fn load(path: Option<PathBuf>) -> ConfigResult<Self> { pub fn load(path: Option<PathBuf>) -> Result<Self> {
let location = if let Some(path) = path { let location = if let Some(path) = path {
path path
} else if let Some(path) = Self::find_config_file()? { } else if let Some(path) = Self::find_config_file()? {
@ -140,9 +94,9 @@ impl Config {
}; };
let content = fs::read_to_string(&location) let content = fs::read_to_string(&location)
.map_err(|e| ConfigError::IO(IoError::new(e, Some(location.as_path()))))?; .with_context(|| format!("Failed to read {}", location.to_string_lossy()))?;
let config = serde_json::from_str(&content) let config = serde_json::from_str(&content)
.map_err(|e| ConfigError::ConfigRead(location.to_string_lossy().to_string(), e))?; .with_context(|| format!("Bad JSON config {}", location.to_string_lossy()))?;
Ok(Self { location, config }) Ok(Self { location, config })
} }
@ -152,7 +106,7 @@ impl Config {
/// disk. /// disk.
/// ///
/// (Note that this is typically only done by the `tree-sitter init-config` command.) /// (Note that this is typically only done by the `tree-sitter init-config` command.)
pub fn initial() -> ConfigResult<Self> { pub fn initial() -> Result<Self> {
let location = if let Ok(path) = env::var("TREE_SITTER_DIR") { let location = if let Ok(path) = env::var("TREE_SITTER_DIR") {
let mut path = PathBuf::from(path); let mut path = PathBuf::from(path);
path.push("config.json"); path.push("config.json");
@ -165,20 +119,17 @@ impl Config {
} }
/// Saves this configuration to the file that it was originally loaded from. /// Saves this configuration to the file that it was originally loaded from.
pub fn save(&self) -> ConfigResult<()> { pub fn save(&self) -> Result<()> {
let json = serde_json::to_string_pretty(&self.config)?; let json = serde_json::to_string_pretty(&self.config)?;
let config_dir = self.location.parent().unwrap(); fs::create_dir_all(self.location.parent().unwrap())?;
fs::create_dir_all(config_dir) fs::write(&self.location, json)?;
.map_err(|e| ConfigError::IO(IoError::new(e, Some(config_dir))))?;
fs::write(&self.location, json)
.map_err(|e| ConfigError::IO(IoError::new(e, Some(self.location.as_path()))))?;
Ok(()) Ok(())
} }
/// Parses a component-specific configuration from the configuration file. The type `C` must /// Parses a component-specific configuration from the configuration file. The type `C` must
/// be [deserializable](https://docs.rs/serde/*/serde/trait.Deserialize.html) from a JSON /// be [deserializable](https://docs.rs/serde/*/serde/trait.Deserialize.html) from a JSON
/// object, and must only include the fields relevant to that component. /// object, and must only include the fields relevant to that component.
pub fn get<C>(&self) -> ConfigResult<C> pub fn get<C>(&self) -> Result<C>
where where
C: for<'de> Deserialize<'de>, C: for<'de> Deserialize<'de>,
{ {
@ -189,7 +140,7 @@ impl Config {
/// Adds a component-specific configuration to the configuration file. The type `C` must be /// Adds a component-specific configuration to the configuration file. The type `C` must be
/// [serializable](https://docs.rs/serde/*/serde/trait.Serialize.html) into a JSON object, and /// [serializable](https://docs.rs/serde/*/serde/trait.Serialize.html) into a JSON object, and
/// must only include the fields relevant to that component. /// must only include the fields relevant to that component.
pub fn add<C>(&mut self, config: C) -> ConfigResult<()> pub fn add<C>(&mut self, config: C) -> Result<()>
where where
C: Serialize, C: Serialize,
{ {

View file

@ -25,7 +25,7 @@ load = ["dep:semver"]
qjs-rt = ["load", "rquickjs", "pathdiff"] qjs-rt = ["load", "rquickjs", "pathdiff"]
[dependencies] [dependencies]
bitflags = "2.9.4" anyhow.workspace = true
dunce = "1.0.5" dunce = "1.0.5"
indexmap.workspace = true indexmap.workspace = true
indoc.workspace = true indoc.workspace = true
@ -33,7 +33,7 @@ log.workspace = true
pathdiff = { version = "0.2.3", optional = true } pathdiff = { version = "0.2.3", optional = true }
regex.workspace = true regex.workspace = true
regex-syntax.workspace = true regex-syntax.workspace = true
rquickjs = { version = "0.11.0", optional = true, features = [ rquickjs = { version = "0.9.0", optional = true, features = [
"bindgen", "bindgen",
"loader", "loader",
"macro", "macro",

View file

@ -27,7 +27,6 @@ use crate::{
node_types::VariableInfo, node_types::VariableInfo,
rules::{AliasMap, Symbol, SymbolType, TokenSet}, rules::{AliasMap, Symbol, SymbolType, TokenSet},
tables::{LexTable, ParseAction, ParseTable, ParseTableEntry}, tables::{LexTable, ParseAction, ParseTable, ParseTableEntry},
OptLevel,
}; };
pub struct Tables { pub struct Tables {
@ -44,7 +43,6 @@ pub fn build_tables(
variable_info: &[VariableInfo], variable_info: &[VariableInfo],
inlines: &InlinedProductionMap, inlines: &InlinedProductionMap,
report_symbol_name: Option<&str>, report_symbol_name: Option<&str>,
optimizations: OptLevel,
) -> BuildTableResult<Tables> { ) -> BuildTableResult<Tables> {
let item_set_builder = ParseItemSetBuilder::new(syntax_grammar, lexical_grammar, inlines); let item_set_builder = ParseItemSetBuilder::new(syntax_grammar, lexical_grammar, inlines);
let following_tokens = let following_tokens =
@ -80,7 +78,6 @@ pub fn build_tables(
simple_aliases, simple_aliases,
&token_conflict_map, &token_conflict_map,
&keywords, &keywords,
optimizations,
); );
let lex_tables = build_lex_table( let lex_tables = build_lex_table(
&mut parse_table, &mut parse_table,
@ -103,10 +100,6 @@ pub fn build_tables(
); );
} }
if parse_table.states.len() > u16::MAX as usize {
Err(ParseTableBuilderError::StateCount(parse_table.states.len()))?;
}
Ok(Tables { Ok(Tables {
parse_table, parse_table,
main_lex_table: lex_tables.main_lex_table, main_lex_table: lex_tables.main_lex_table,

View file

@ -77,11 +77,9 @@ pub enum ParseTableBuilderError {
"The non-terminal rule `{0}` is used in a non-terminal `extra` rule, which is not allowed." "The non-terminal rule `{0}` is used in a non-terminal `extra` rule, which is not allowed."
)] )]
ImproperNonTerminalExtra(String), ImproperNonTerminalExtra(String),
#[error("State count `{0}` exceeds the max value {max}.", max=u16::MAX)]
StateCount(usize),
} }
#[derive(Default, Debug, Serialize, Error)] #[derive(Default, Debug, Serialize)]
pub struct ConflictError { pub struct ConflictError {
pub symbol_sequence: Vec<String>, pub symbol_sequence: Vec<String>,
pub conflicting_lookahead: String, pub conflicting_lookahead: String,
@ -89,7 +87,7 @@ pub struct ConflictError {
pub possible_resolutions: Vec<Resolution>, pub possible_resolutions: Vec<Resolution>,
} }
#[derive(Default, Debug, Serialize, Error)] #[derive(Default, Debug, Serialize)]
pub struct Interpretation { pub struct Interpretation {
pub preceding_symbols: Vec<String>, pub preceding_symbols: Vec<String>,
pub variable_name: String, pub variable_name: String,
@ -108,7 +106,7 @@ pub enum Resolution {
AddConflict { symbols: Vec<String> }, AddConflict { symbols: Vec<String> },
} }
#[derive(Debug, Serialize, Error)] #[derive(Debug, Serialize)]
pub struct AmbiguousExtraError { pub struct AmbiguousExtraError {
pub parent_symbols: Vec<String>, pub parent_symbols: Vec<String>,
} }
@ -238,6 +236,9 @@ impl std::fmt::Display for AmbiguousExtraError {
} }
} }
impl std::error::Error for ConflictError {}
impl std::error::Error for AmbiguousExtraError {}
impl<'a> ParseTableBuilder<'a> { impl<'a> ParseTableBuilder<'a> {
fn new( fn new(
syntax_grammar: &'a SyntaxGrammar, syntax_grammar: &'a SyntaxGrammar,

View file

@ -204,7 +204,7 @@ impl fmt::Display for ParseItemDisplay<'_> {
|| step.reserved_word_set_id != ReservedWordSetId::default() || step.reserved_word_set_id != ReservedWordSetId::default()
{ {
write!(f, " (")?; write!(f, " (")?;
if !step.precedence.is_none() { if step.precedence.is_none() {
write!(f, " {}", step.precedence)?; write!(f, " {}", step.precedence)?;
} }
if let Some(associativity) = step.associativity { if let Some(associativity) = step.associativity {

View file

@ -11,7 +11,6 @@ use crate::{
grammars::{LexicalGrammar, SyntaxGrammar, VariableType}, grammars::{LexicalGrammar, SyntaxGrammar, VariableType},
rules::{AliasMap, Symbol, TokenSet}, rules::{AliasMap, Symbol, TokenSet},
tables::{GotoAction, ParseAction, ParseState, ParseStateId, ParseTable, ParseTableEntry}, tables::{GotoAction, ParseAction, ParseState, ParseStateId, ParseTable, ParseTableEntry},
OptLevel,
}; };
pub fn minimize_parse_table( pub fn minimize_parse_table(
@ -21,7 +20,6 @@ pub fn minimize_parse_table(
simple_aliases: &AliasMap, simple_aliases: &AliasMap,
token_conflict_map: &TokenConflictMap, token_conflict_map: &TokenConflictMap,
keywords: &TokenSet, keywords: &TokenSet,
optimizations: OptLevel,
) { ) {
let mut minimizer = Minimizer { let mut minimizer = Minimizer {
parse_table, parse_table,
@ -31,9 +29,7 @@ pub fn minimize_parse_table(
keywords, keywords,
simple_aliases, simple_aliases,
}; };
if optimizations.contains(OptLevel::MergeStates) { minimizer.merge_compatible_states();
minimizer.merge_compatible_states();
}
minimizer.remove_unit_reductions(); minimizer.remove_unit_reductions();
minimizer.remove_unused_states(); minimizer.remove_unused_states();
minimizer.reorder_states_by_descending_size(); minimizer.reorder_states_by_descending_size();
@ -306,7 +302,9 @@ impl Minimizer<'_> {
return true; return true;
} }
for (action1, action2) in actions1.iter().zip(actions2.iter()) { for (i, action1) in actions1.iter().enumerate() {
let action2 = &actions2[i];
// Two shift actions are equivalent if their destinations are in the same group. // Two shift actions are equivalent if their destinations are in the same group.
if let ( if let (
ParseAction::Shift { ParseAction::Shift {

View file

@ -28,7 +28,7 @@ pub struct TokenConflictMap<'a> {
impl<'a> TokenConflictMap<'a> { impl<'a> TokenConflictMap<'a> {
/// Create a token conflict map based on a lexical grammar, which describes the structure /// Create a token conflict map based on a lexical grammar, which describes the structure
/// of each token, and a `following_token` map, which indicates which tokens may be appear /// each token, and a `following_token` map, which indicates which tokens may be appear
/// immediately after each other token. /// immediately after each other token.
/// ///
/// This analyzes the possible kinds of overlap between each pair of tokens and stores /// This analyzes the possible kinds of overlap between each pair of tokens and stores

View file

@ -1,4 +1,4 @@
use std::{collections::BTreeMap, sync::LazyLock}; use std::{collections::HashMap, sync::LazyLock};
#[cfg(feature = "load")] #[cfg(feature = "load")]
use std::{ use std::{
env, fs, env, fs,
@ -7,7 +7,7 @@ use std::{
process::{Command, Stdio}, process::{Command, Stdio},
}; };
use bitflags::bitflags; use anyhow::Result;
use log::warn; use log::warn;
use node_types::VariableInfo; use node_types::VariableInfo;
use regex::{Regex, RegexBuilder}; use regex::{Regex, RegexBuilder};
@ -56,7 +56,7 @@ struct JSONOutput {
syntax_grammar: SyntaxGrammar, syntax_grammar: SyntaxGrammar,
lexical_grammar: LexicalGrammar, lexical_grammar: LexicalGrammar,
inlines: InlinedProductionMap, inlines: InlinedProductionMap,
simple_aliases: BTreeMap<Symbol, Alias>, simple_aliases: HashMap<Symbol, Alias>,
variable_info: Vec<VariableInfo>, variable_info: Vec<VariableInfo>,
} }
@ -80,8 +80,8 @@ pub type GenerateResult<T> = Result<T, GenerateError>;
pub enum GenerateError { pub enum GenerateError {
#[error("Error with specified path -- {0}")] #[error("Error with specified path -- {0}")]
GrammarPath(String), GrammarPath(String),
#[error(transparent)] #[error("{0}")]
IO(IoError), IO(String),
#[cfg(feature = "load")] #[cfg(feature = "load")]
#[error(transparent)] #[error(transparent)]
LoadGrammarFile(#[from] LoadGrammarError), LoadGrammarFile(#[from] LoadGrammarError),
@ -100,28 +100,9 @@ pub enum GenerateError {
SuperTypeCycle(#[from] SuperTypeCycleError), SuperTypeCycle(#[from] SuperTypeCycleError),
} }
#[derive(Debug, Error, Serialize)] impl From<std::io::Error> for GenerateError {
pub struct IoError { fn from(value: std::io::Error) -> Self {
pub error: String, Self::IO(value.to_string())
pub path: Option<String>,
}
impl IoError {
fn new(error: &std::io::Error, path: Option<&Path>) -> Self {
Self {
error: error.to_string(),
path: path.map(|p| p.to_string_lossy().to_string()),
}
}
}
impl std::fmt::Display for IoError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.error)?;
if let Some(ref path) = self.path {
write!(f, " ({path})")?;
}
Ok(())
} }
} }
@ -136,11 +117,18 @@ pub enum LoadGrammarError {
#[error("Failed to load grammar.js -- {0}")] #[error("Failed to load grammar.js -- {0}")]
LoadJSGrammarFile(#[from] JSError), LoadJSGrammarFile(#[from] JSError),
#[error("Failed to load grammar.json -- {0}")] #[error("Failed to load grammar.json -- {0}")]
IO(IoError), IO(String),
#[error("Unknown grammar file extension: {0:?}")] #[error("Unknown grammar file extension: {0:?}")]
FileExtension(PathBuf), FileExtension(PathBuf),
} }
#[cfg(feature = "load")]
impl From<std::io::Error> for LoadGrammarError {
fn from(value: std::io::Error) -> Self {
Self::IO(value.to_string())
}
}
#[cfg(feature = "load")] #[cfg(feature = "load")]
#[derive(Debug, Error, Serialize)] #[derive(Debug, Error, Serialize)]
pub enum ParseVersionError { pub enum ParseVersionError {
@ -148,8 +136,8 @@ pub enum ParseVersionError {
Version(String), Version(String),
#[error("{0}")] #[error("{0}")]
JSON(String), JSON(String),
#[error(transparent)] #[error("{0}")]
IO(IoError), IO(String),
} }
#[cfg(feature = "load")] #[cfg(feature = "load")]
@ -164,21 +152,8 @@ pub enum JSError {
JSRuntimeUtf8 { runtime: String, error: String }, JSRuntimeUtf8 { runtime: String, error: String },
#[error("`{runtime}` process exited with status {code}")] #[error("`{runtime}` process exited with status {code}")]
JSRuntimeExit { runtime: String, code: i32 }, JSRuntimeExit { runtime: String, code: i32 },
#[error("Failed to open stdin for `{runtime}`")] #[error("{0}")]
JSRuntimeStdin { runtime: String }, IO(String),
#[error("Failed to write {item} to `{runtime}`'s stdin -- {error}")]
JSRuntimeWrite {
runtime: String,
item: String,
error: String,
},
#[error("Failed to read output from `{runtime}` -- {error}")]
JSRuntimeRead { runtime: String, error: String },
#[error(transparent)]
IO(IoError),
#[cfg(feature = "qjs-rt")]
#[error("Failed to get relative path")]
RelativePath,
#[error("Could not parse this package's version as semver -- {0}")] #[error("Could not parse this package's version as semver -- {0}")]
Semver(String), Semver(String),
#[error("Failed to serialze grammar JSON -- {0}")] #[error("Failed to serialze grammar JSON -- {0}")]
@ -188,6 +163,13 @@ pub enum JSError {
QuickJS(String), QuickJS(String),
} }
#[cfg(feature = "load")]
impl From<std::io::Error> for JSError {
fn from(value: std::io::Error) -> Self {
Self::IO(value.to_string())
}
}
#[cfg(feature = "load")] #[cfg(feature = "load")]
impl From<serde_json::Error> for JSError { impl From<serde_json::Error> for JSError {
fn from(value: serde_json::Error) -> Self { fn from(value: serde_json::Error) -> Self {
@ -209,19 +191,6 @@ impl From<rquickjs::Error> for JSError {
} }
} }
bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct OptLevel: u32 {
const MergeStates = 1 << 0;
}
}
impl Default for OptLevel {
fn default() -> Self {
Self::MergeStates
}
}
#[cfg(feature = "load")] #[cfg(feature = "load")]
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub fn generate_parser_in_directory<T, U, V>( pub fn generate_parser_in_directory<T, U, V>(
@ -232,7 +201,6 @@ pub fn generate_parser_in_directory<T, U, V>(
report_symbol_name: Option<&str>, report_symbol_name: Option<&str>,
js_runtime: Option<&str>, js_runtime: Option<&str>,
generate_parser: bool, generate_parser: bool,
optimizations: OptLevel,
) -> GenerateResult<()> ) -> GenerateResult<()>
where where
T: Into<PathBuf>, T: Into<PathBuf>,
@ -248,8 +216,7 @@ where
.try_exists() .try_exists()
.map_err(|e| GenerateError::GrammarPath(e.to_string()))? .map_err(|e| GenerateError::GrammarPath(e.to_string()))?
{ {
fs::create_dir_all(&path_buf) fs::create_dir_all(&path_buf)?;
.map_err(|e| GenerateError::IO(IoError::new(&e, Some(path_buf.as_path()))))?;
repo_path = path_buf; repo_path = path_buf;
repo_path.join("grammar.js") repo_path.join("grammar.js")
} else { } else {
@ -266,12 +233,15 @@ where
let header_path = src_path.join("tree_sitter"); let header_path = src_path.join("tree_sitter");
// Ensure that the output directory exists // Ensure that the output directory exists
fs::create_dir_all(&src_path) fs::create_dir_all(&src_path)?;
.map_err(|e| GenerateError::IO(IoError::new(&e, Some(src_path.as_path()))))?;
if grammar_path.file_name().unwrap() != "grammar.json" { if grammar_path.file_name().unwrap() != "grammar.json" {
fs::write(src_path.join("grammar.json"), &grammar_json) fs::write(src_path.join("grammar.json"), &grammar_json).map_err(|e| {
.map_err(|e| GenerateError::IO(IoError::new(&e, Some(src_path.as_path()))))?; GenerateError::IO(format!(
"Failed to write grammar.json to {} -- {e}",
src_path.display()
))
})?;
} }
// If our job is only to generate `grammar.json` and not `parser.c`, stop here. // If our job is only to generate `grammar.json` and not `parser.c`, stop here.
@ -308,13 +278,11 @@ where
abi_version, abi_version,
semantic_version.map(|v| (v.major as u8, v.minor as u8, v.patch as u8)), semantic_version.map(|v| (v.major as u8, v.minor as u8, v.patch as u8)),
report_symbol_name, report_symbol_name,
optimizations,
)?; )?;
write_file(&src_path.join("parser.c"), c_code)?; write_file(&src_path.join("parser.c"), c_code)?;
write_file(&src_path.join("node-types.json"), node_types_json)?; write_file(&src_path.join("node-types.json"), node_types_json)?;
fs::create_dir_all(&header_path) fs::create_dir_all(&header_path)?;
.map_err(|e| GenerateError::IO(IoError::new(&e, Some(header_path.as_path()))))?;
write_file(&header_path.join("alloc.h"), ALLOC_HEADER)?; write_file(&header_path.join("alloc.h"), ALLOC_HEADER)?;
write_file(&header_path.join("array.h"), ARRAY_HEADER)?; write_file(&header_path.join("array.h"), ARRAY_HEADER)?;
write_file(&header_path.join("parser.h"), PARSER_HEADER)?; write_file(&header_path.join("parser.h"), PARSER_HEADER)?;
@ -333,7 +301,6 @@ pub fn generate_parser_for_grammar(
LANGUAGE_VERSION, LANGUAGE_VERSION,
semantic_version, semantic_version,
None, None,
OptLevel::empty(),
)?; )?;
Ok((input_grammar.name, parser.c_code)) Ok((input_grammar.name, parser.c_code))
} }
@ -367,7 +334,6 @@ fn generate_parser_for_grammar_with_opts(
abi_version: usize, abi_version: usize,
semantic_version: Option<(u8, u8, u8)>, semantic_version: Option<(u8, u8, u8)>,
report_symbol_name: Option<&str>, report_symbol_name: Option<&str>,
optimizations: OptLevel,
) -> GenerateResult<GeneratedParser> { ) -> GenerateResult<GeneratedParser> {
let JSONOutput { let JSONOutput {
syntax_grammar, syntax_grammar,
@ -387,7 +353,6 @@ fn generate_parser_for_grammar_with_opts(
&variable_info, &variable_info,
&inlines, &inlines,
report_symbol_name, report_symbol_name,
optimizations,
)?; )?;
let c_code = render_c_code( let c_code = render_c_code(
&input_grammar.name, &input_grammar.name,
@ -430,8 +395,9 @@ fn read_grammar_version(repo_path: &Path) -> Result<Option<Version>, ParseVersio
let json = path let json = path
.exists() .exists()
.then(|| { .then(|| {
let contents = fs::read_to_string(path.as_path()) let contents = fs::read_to_string(path.as_path()).map_err(|e| {
.map_err(|e| ParseVersionError::IO(IoError::new(&e, Some(path.as_path()))))?; ParseVersionError::IO(format!("Failed to read `{}` -- {e}", path.display()))
})?;
serde_json::from_str::<TreeSitterJson>(&contents).map_err(|e| { serde_json::from_str::<TreeSitterJson>(&contents).map_err(|e| {
ParseVersionError::JSON(format!("Failed to parse `{}` -- {e}", path.display())) ParseVersionError::JSON(format!("Failed to parse `{}` -- {e}", path.display()))
}) })
@ -465,16 +431,14 @@ pub fn load_grammar_file(
} }
match grammar_path.extension().and_then(|e| e.to_str()) { match grammar_path.extension().and_then(|e| e.to_str()) {
Some("js") => Ok(load_js_grammar_file(grammar_path, js_runtime)?), Some("js") => Ok(load_js_grammar_file(grammar_path, js_runtime)?),
Some("json") => Ok(fs::read_to_string(grammar_path) Some("json") => Ok(fs::read_to_string(grammar_path)?),
.map_err(|e| LoadGrammarError::IO(IoError::new(&e, Some(grammar_path))))?),
_ => Err(LoadGrammarError::FileExtension(grammar_path.to_owned()))?, _ => Err(LoadGrammarError::FileExtension(grammar_path.to_owned()))?,
} }
} }
#[cfg(feature = "load")] #[cfg(feature = "load")]
fn load_js_grammar_file(grammar_path: &Path, js_runtime: Option<&str>) -> JSResult<String> { fn load_js_grammar_file(grammar_path: &Path, js_runtime: Option<&str>) -> JSResult<String> {
let grammar_path = dunce::canonicalize(grammar_path) let grammar_path = dunce::canonicalize(grammar_path)?;
.map_err(|e| JSError::IO(IoError::new(&e, Some(grammar_path))))?;
#[cfg(feature = "qjs-rt")] #[cfg(feature = "qjs-rt")]
if js_runtime == Some("native") { if js_runtime == Some("native") {
@ -515,9 +479,7 @@ fn load_js_grammar_file(grammar_path: &Path, js_runtime: Option<&str>) -> JSResu
let mut js_stdin = js_process let mut js_stdin = js_process
.stdin .stdin
.take() .take()
.ok_or_else(|| JSError::JSRuntimeStdin { .ok_or_else(|| JSError::IO(format!("Failed to open stdin for `{js_runtime}`")))?;
runtime: js_runtime.to_string(),
})?;
let cli_version = Version::parse(env!("CARGO_PKG_VERSION"))?; let cli_version = Version::parse(env!("CARGO_PKG_VERSION"))?;
write!( write!(
@ -527,27 +489,23 @@ fn load_js_grammar_file(grammar_path: &Path, js_runtime: Option<&str>) -> JSResu
globalThis.TREE_SITTER_CLI_VERSION_PATCH = {};", globalThis.TREE_SITTER_CLI_VERSION_PATCH = {};",
cli_version.major, cli_version.minor, cli_version.patch, cli_version.major, cli_version.minor, cli_version.patch,
) )
.map_err(|e| JSError::JSRuntimeWrite { .map_err(|e| {
runtime: js_runtime.to_string(), JSError::IO(format!(
item: "tree-sitter version".to_string(), "Failed to write tree-sitter version to `{js_runtime}`'s stdin -- {e}"
error: e.to_string(), ))
})?;
js_stdin.write(include_bytes!("./dsl.js")).map_err(|e| {
JSError::IO(format!(
"Failed to write grammar dsl to `{js_runtime}`'s stdin -- {e}"
))
})?; })?;
js_stdin
.write(include_bytes!("./dsl.js"))
.map_err(|e| JSError::JSRuntimeWrite {
runtime: js_runtime.to_string(),
item: "grammar dsl".to_string(),
error: e.to_string(),
})?;
drop(js_stdin); drop(js_stdin);
let output = js_process let output = js_process
.wait_with_output() .wait_with_output()
.map_err(|e| JSError::JSRuntimeRead { .map_err(|e| JSError::IO(format!("Failed to read output from `{js_runtime}` -- {e}")))?;
runtime: js_runtime.to_string(),
error: e.to_string(),
})?;
match output.status.code() { match output.status.code() {
None => panic!("`{js_runtime}` process was killed"),
Some(0) => { Some(0) => {
let stdout = String::from_utf8(output.stdout).map_err(|e| JSError::JSRuntimeUtf8 { let stdout = String::from_utf8(output.stdout).map_err(|e| JSError::JSRuntimeUtf8 {
runtime: js_runtime.to_string(), runtime: js_runtime.to_string(),
@ -562,15 +520,9 @@ fn load_js_grammar_file(grammar_path: &Path, js_runtime: Option<&str>) -> JSResu
grammar_json = &stdout[pos + 1..]; grammar_json = &stdout[pos + 1..];
let mut stdout = std::io::stdout().lock(); let mut stdout = std::io::stdout().lock();
stdout stdout.write_all(node_output.as_bytes())?;
.write_all(node_output.as_bytes()) stdout.write_all(b"\n")?;
.map_err(|e| JSError::IO(IoError::new(&e, None)))?; stdout.flush()?;
stdout
.write_all(b"\n")
.map_err(|e| JSError::IO(IoError::new(&e, None)))?;
stdout
.flush()
.map_err(|e| JSError::IO(IoError::new(&e, None)))?;
} }
Ok(serde_json::to_string_pretty(&serde_json::from_str::< Ok(serde_json::to_string_pretty(&serde_json::from_str::<
@ -581,16 +533,13 @@ fn load_js_grammar_file(grammar_path: &Path, js_runtime: Option<&str>) -> JSResu
runtime: js_runtime.to_string(), runtime: js_runtime.to_string(),
code, code,
}), }),
None => Err(JSError::JSRuntimeExit {
runtime: js_runtime.to_string(),
code: -1,
}),
} }
} }
#[cfg(feature = "load")] #[cfg(feature = "load")]
pub fn write_file(path: &Path, body: impl AsRef<[u8]>) -> GenerateResult<()> { pub fn write_file(path: &Path, body: impl AsRef<[u8]>) -> GenerateResult<()> {
fs::write(path, body).map_err(|e| GenerateError::IO(IoError::new(&e, Some(path)))) fs::write(path, body)
.map_err(|e| GenerateError::IO(format!("Failed to write {:?} -- {e}", path.file_name())))
} }
#[cfg(test)] #[cfg(test)]

View file

@ -1,5 +1,6 @@
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; use std::collections::{BTreeMap, HashMap, HashSet};
use anyhow::Result;
use serde::Serialize; use serde::Serialize;
use thiserror::Error; use thiserror::Error;
@ -377,11 +378,11 @@ pub fn get_variable_info(
fn get_aliases_by_symbol( fn get_aliases_by_symbol(
syntax_grammar: &SyntaxGrammar, syntax_grammar: &SyntaxGrammar,
default_aliases: &AliasMap, default_aliases: &AliasMap,
) -> HashMap<Symbol, BTreeSet<Option<Alias>>> { ) -> HashMap<Symbol, HashSet<Option<Alias>>> {
let mut aliases_by_symbol = HashMap::new(); let mut aliases_by_symbol = HashMap::new();
for (symbol, alias) in default_aliases { for (symbol, alias) in default_aliases {
aliases_by_symbol.insert(*symbol, { aliases_by_symbol.insert(*symbol, {
let mut aliases = BTreeSet::new(); let mut aliases = HashSet::new();
aliases.insert(Some(alias.clone())); aliases.insert(Some(alias.clone()));
aliases aliases
}); });
@ -390,7 +391,7 @@ fn get_aliases_by_symbol(
if !default_aliases.contains_key(extra_symbol) { if !default_aliases.contains_key(extra_symbol) {
aliases_by_symbol aliases_by_symbol
.entry(*extra_symbol) .entry(*extra_symbol)
.or_insert_with(BTreeSet::new) .or_insert_with(HashSet::new)
.insert(None); .insert(None);
} }
} }
@ -399,7 +400,7 @@ fn get_aliases_by_symbol(
for step in &production.steps { for step in &production.steps {
aliases_by_symbol aliases_by_symbol
.entry(step.symbol) .entry(step.symbol)
.or_insert_with(BTreeSet::new) .or_insert_with(HashSet::new)
.insert( .insert(
step.alias step.alias
.as_ref() .as_ref()
@ -530,7 +531,7 @@ pub fn generate_node_types_json(
let aliases_by_symbol = get_aliases_by_symbol(syntax_grammar, default_aliases); let aliases_by_symbol = get_aliases_by_symbol(syntax_grammar, default_aliases);
let empty = BTreeSet::new(); let empty = HashSet::new();
let extra_names = syntax_grammar let extra_names = syntax_grammar
.extra_symbols .extra_symbols
.iter() .iter()
@ -589,7 +590,7 @@ pub fn generate_node_types_json(
} else if !syntax_grammar.variables_to_inline.contains(&symbol) { } else if !syntax_grammar.variables_to_inline.contains(&symbol) {
// If a rule is aliased under multiple names, then its information // If a rule is aliased under multiple names, then its information
// contributes to multiple entries in the final JSON. // contributes to multiple entries in the final JSON.
for alias in aliases_by_symbol.get(&symbol).unwrap_or(&BTreeSet::new()) { for alias in aliases_by_symbol.get(&symbol).unwrap_or(&HashSet::new()) {
let kind; let kind;
let is_named; let is_named;
if let Some(alias) = alias { if let Some(alias) = alias {
@ -783,9 +784,6 @@ pub fn generate_node_types_json(
a_is_leaf.cmp(&b_is_leaf) a_is_leaf.cmp(&b_is_leaf)
}) })
.then_with(|| a.kind.cmp(&b.kind)) .then_with(|| a.kind.cmp(&b.kind))
.then_with(|| a.named.cmp(&b.named))
.then_with(|| a.root.cmp(&b.root))
.then_with(|| a.extra.cmp(&b.extra))
}); });
result.dedup(); result.dedup();
Ok(result) Ok(result)
@ -828,12 +826,12 @@ fn extend_sorted<'a, T>(vec: &mut Vec<T>, values: impl IntoIterator<Item = &'a T
where where
T: 'a + Clone + Eq + Ord, T: 'a + Clone + Eq + Ord,
{ {
values.into_iter().fold(false, |acc, value| { values.into_iter().any(|value| {
if let Err(i) = vec.binary_search(value) { if let Err(i) = vec.binary_search(value) {
vec.insert(i, value.clone()); vec.insert(i, value.clone());
true true
} else { } else {
acc false
} }
}) })
} }

View file

@ -1,5 +1,6 @@
use std::collections::HashSet; use std::collections::HashSet;
use anyhow::Result;
use log::warn; use log::warn;
use regex::Regex; use regex::Regex;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -17,7 +18,7 @@ use crate::{
#[allow(clippy::upper_case_acronyms)] #[allow(clippy::upper_case_acronyms)]
enum RuleJSON { enum RuleJSON {
ALIAS { ALIAS {
content: Box<Self>, content: Box<RuleJSON>,
named: bool, named: bool,
value: String, value: String,
}, },
@ -33,46 +34,46 @@ enum RuleJSON {
name: String, name: String,
}, },
CHOICE { CHOICE {
members: Vec<Self>, members: Vec<RuleJSON>,
}, },
FIELD { FIELD {
name: String, name: String,
content: Box<Self>, content: Box<RuleJSON>,
}, },
SEQ { SEQ {
members: Vec<Self>, members: Vec<RuleJSON>,
}, },
REPEAT { REPEAT {
content: Box<Self>, content: Box<RuleJSON>,
}, },
REPEAT1 { REPEAT1 {
content: Box<Self>, content: Box<RuleJSON>,
}, },
PREC_DYNAMIC { PREC_DYNAMIC {
value: i32, value: i32,
content: Box<Self>, content: Box<RuleJSON>,
}, },
PREC_LEFT { PREC_LEFT {
value: PrecedenceValueJSON, value: PrecedenceValueJSON,
content: Box<Self>, content: Box<RuleJSON>,
}, },
PREC_RIGHT { PREC_RIGHT {
value: PrecedenceValueJSON, value: PrecedenceValueJSON,
content: Box<Self>, content: Box<RuleJSON>,
}, },
PREC { PREC {
value: PrecedenceValueJSON, value: PrecedenceValueJSON,
content: Box<Self>, content: Box<RuleJSON>,
}, },
TOKEN { TOKEN {
content: Box<Self>, content: Box<RuleJSON>,
}, },
IMMEDIATE_TOKEN { IMMEDIATE_TOKEN {
content: Box<Self>, content: Box<RuleJSON>,
}, },
RESERVED { RESERVED {
context_name: String, context_name: String,
content: Box<Self>, content: Box<RuleJSON>,
}, },
} }

View file

@ -12,6 +12,7 @@ use std::{
mem, mem,
}; };
use anyhow::Result;
pub use expand_tokens::ExpandTokensError; pub use expand_tokens::ExpandTokensError;
pub use extract_tokens::ExtractTokensError; pub use extract_tokens::ExtractTokensError;
pub use flatten_grammar::FlattenGrammarError; pub use flatten_grammar::FlattenGrammarError;

View file

@ -1,3 +1,4 @@
use anyhow::Result;
use regex_syntax::{ use regex_syntax::{
hir::{Class, Hir, HirKind}, hir::{Class, Hir, HirKind},
ParserBuilder, ParserBuilder,
@ -26,7 +27,7 @@ pub enum ExpandTokensError {
"The rule `{0}` matches the empty string. "The rule `{0}` matches the empty string.
Tree-sitter does not support syntactic rules that match the empty string Tree-sitter does not support syntactic rules that match the empty string
unless they are used only as the grammar's start rule. unless they are used only as the grammar's start rule.
" "
)] )]
EmptyString(String), EmptyString(String),
#[error(transparent)] #[error(transparent)]
@ -188,7 +189,7 @@ impl NfaBuilder {
} }
Rule::String(s) => { Rule::String(s) => {
for c in s.chars().rev() { for c in s.chars().rev() {
self.push_advance(CharacterSet::from_char(c), next_state_id); self.push_advance(CharacterSet::empty().add_char(c), next_state_id);
next_state_id = self.nfa.last_state_id(); next_state_id = self.nfa.last_state_id();
} }
Ok(!s.is_empty()) Ok(!s.is_empty())

View file

@ -69,7 +69,9 @@ pub(super) fn extract_default_aliases(
SymbolType::External => &mut external_status_list[symbol.index], SymbolType::External => &mut external_status_list[symbol.index],
SymbolType::NonTerminal => &mut non_terminal_status_list[symbol.index], SymbolType::NonTerminal => &mut non_terminal_status_list[symbol.index],
SymbolType::Terminal => &mut terminal_status_list[symbol.index], SymbolType::Terminal => &mut terminal_status_list[symbol.index],
SymbolType::End | SymbolType::EndOfNonTerminalExtra => panic!("Unexpected end token"), SymbolType::End | SymbolType::EndOfNonTerminalExtra => {
panic!("Unexpected end token")
}
}; };
status.appears_unaliased = true; status.appears_unaliased = true;
} }

View file

@ -1,5 +1,6 @@
use std::collections::HashMap; use std::collections::HashMap;
use anyhow::Result;
use serde::Serialize; use serde::Serialize;
use thiserror::Error; use thiserror::Error;
@ -152,7 +153,7 @@ pub(super) fn extract_tokens(
} }
} }
let mut external_tokens = Vec::with_capacity(grammar.external_tokens.len()); let mut external_tokens = Vec::new();
for external_token in grammar.external_tokens { for external_token in grammar.external_tokens {
let rule = symbol_replacer.replace_symbols_in_rule(&external_token.rule); let rule = symbol_replacer.replace_symbols_in_rule(&external_token.rule);
if let Rule::Symbol(symbol) = rule { if let Rule::Symbol(symbol) = rule {
@ -590,13 +591,14 @@ mod test {
]); ]);
grammar.external_tokens = vec![Variable::named("rule_1", Rule::non_terminal(1))]; grammar.external_tokens = vec![Variable::named("rule_1", Rule::non_terminal(1))];
let result = extract_tokens(grammar); match extract_tokens(grammar) {
assert!(result.is_err(), "Expected an error but got no error"); Err(e) => {
let err = result.err().unwrap(); assert_eq!(e.to_string(), "Rule 'rule_1' cannot be used as both an external token and a non-terminal rule");
assert_eq!( }
err.to_string(), _ => {
"Rule 'rule_1' cannot be used as both an external token and a non-terminal rule" panic!("Expected an error but got no error");
); }
}
} }
#[test] #[test]

View file

@ -1,5 +1,6 @@
use std::collections::HashMap; use std::collections::HashMap;
use anyhow::Result;
use serde::Serialize; use serde::Serialize;
use thiserror::Error; use thiserror::Error;

View file

@ -1,3 +1,4 @@
use anyhow::Result;
use log::warn; use log::warn;
use serde::Serialize; use serde::Serialize;
use thiserror::Error; use thiserror::Error;
@ -278,9 +279,10 @@ mod tests {
fn test_grammar_with_undefined_symbols() { fn test_grammar_with_undefined_symbols() {
let result = intern_symbols(&build_grammar(vec![Variable::named("x", Rule::named("y"))])); let result = intern_symbols(&build_grammar(vec![Variable::named("x", Rule::named("y"))]));
assert!(result.is_err(), "Expected an error but got none"); match result {
let e = result.err().unwrap(); Err(e) => assert_eq!(e.to_string(), "Undefined symbol `y`"),
assert_eq!(e.to_string(), "Undefined symbol `y`"); _ => panic!("Expected an error but got none"),
}
} }
fn build_grammar(variables: Vec<Variable>) -> InputGrammar { fn build_grammar(variables: Vec<Variable>) -> InputGrammar {

View file

@ -1,5 +1,6 @@
use std::collections::HashMap; use std::collections::HashMap;
use anyhow::Result;
use serde::Serialize; use serde::Serialize;
use thiserror::Error; use thiserror::Error;
@ -70,13 +71,12 @@ impl InlinedProductionMapBuilder {
let production_map = production_indices_by_step_id let production_map = production_indices_by_step_id
.into_iter() .into_iter()
.map(|(step_id, production_indices)| { .map(|(step_id, production_indices)| {
let production = let production = step_id.variable_index.map_or_else(
core::ptr::from_ref::<Production>(step_id.variable_index.map_or_else( || &productions[step_id.production_index],
|| &productions[step_id.production_index], |variable_index| {
|variable_index| { &grammar.variables[variable_index].productions[step_id.production_index]
&grammar.variables[variable_index].productions[step_id.production_index] },
}, ) as *const Production;
));
((production, step_id.step_index as u32), production_indices) ((production, step_id.step_index as u32), production_indices)
}) })
.collect(); .collect();
@ -549,9 +549,10 @@ mod tests {
..Default::default() ..Default::default()
}; };
let result = process_inlines(&grammar, &lexical_grammar); if let Err(error) = process_inlines(&grammar, &lexical_grammar) {
assert!(result.is_err(), "expected an error, but got none"); assert_eq!(error.to_string(), "Token `something` cannot be inlined");
let err = result.err().unwrap(); } else {
assert_eq!(err.to_string(), "Token `something` cannot be inlined",); panic!("expected an error, but got none");
}
} }
} }

View file

@ -10,7 +10,7 @@ use rquickjs::{
Context, Ctx, Function, Module, Object, Runtime, Type, Value, Context, Ctx, Function, Module, Object, Runtime, Type, Value,
}; };
use super::{IoError, JSError, JSResult}; use super::{JSError, JSResult};
const DSL: &[u8] = include_bytes!("dsl.js"); const DSL: &[u8] = include_bytes!("dsl.js");
@ -95,27 +95,9 @@ impl Console {
Type::Module => "module".to_string(), Type::Module => "module".to_string(),
Type::BigInt => v.get::<String>().unwrap_or_else(|_| "BigInt".to_string()), Type::BigInt => v.get::<String>().unwrap_or_else(|_| "BigInt".to_string()),
Type::Unknown => "unknown".to_string(), Type::Unknown => "unknown".to_string(),
Type::Array => {
let js_vals = v
.as_array()
.unwrap()
.iter::<Value<'_>>()
.filter_map(|x| x.ok())
.map(|x| {
if x.is_string() {
format!("'{}'", Self::format_args(&[x]))
} else {
Self::format_args(&[x])
}
})
.collect::<Vec<_>>()
.join(", ");
format!("[ {js_vals} ]")
}
Type::Symbol Type::Symbol
| Type::Object | Type::Object
| Type::Proxy | Type::Array
| Type::Function | Type::Function
| Type::Constructor | Type::Constructor
| Type::Promise | Type::Promise
@ -215,11 +197,11 @@ fn try_resolve_path(path: &Path) -> rquickjs::Result<PathBuf> {
} }
#[allow(clippy::needless_pass_by_value)] #[allow(clippy::needless_pass_by_value)]
fn require_from_module<'js>( fn require_from_module<'a>(
ctx: Ctx<'js>, ctx: Ctx<'a>,
module_path: String, module_path: String,
from_module: &str, from_module: &str,
) -> rquickjs::Result<Value<'js>> { ) -> rquickjs::Result<Value<'a>> {
let current_module = PathBuf::from(from_module); let current_module = PathBuf::from(from_module);
let current_dir = if current_module.is_file() { let current_dir = if current_module.is_file() {
current_module.parent().unwrap_or(Path::new(".")) current_module.parent().unwrap_or(Path::new("."))
@ -234,13 +216,13 @@ fn require_from_module<'js>(
load_module_from_content(&ctx, &resolved_path, &contents) load_module_from_content(&ctx, &resolved_path, &contents)
} }
fn load_module_from_content<'js>( fn load_module_from_content<'a>(
ctx: &Ctx<'js>, ctx: &Ctx<'a>,
path: &Path, path: &Path,
contents: &str, contents: &str,
) -> rquickjs::Result<Value<'js>> { ) -> rquickjs::Result<Value<'a>> {
if path.extension().is_some_and(|ext| ext == "json") { if path.extension().is_some_and(|ext| ext == "json") {
return ctx.eval::<Value<'js>, _>(format!("JSON.parse({contents:?})")); return ctx.eval::<Value, _>(format!("JSON.parse({contents:?})"));
} }
let exports = Object::new(ctx.clone())?; let exports = Object::new(ctx.clone())?;
@ -256,7 +238,7 @@ fn load_module_from_content<'js>(
let module_path = filename.clone(); let module_path = filename.clone();
let require = Function::new( let require = Function::new(
ctx.clone(), ctx.clone(),
move |ctx_inner: Ctx<'js>, target_path: String| -> rquickjs::Result<Value<'js>> { move |ctx_inner: Ctx<'a>, target_path: String| -> rquickjs::Result<Value<'a>> {
require_from_module(ctx_inner, target_path, &module_path) require_from_module(ctx_inner, target_path, &module_path)
}, },
)?; )?;
@ -264,8 +246,8 @@ fn load_module_from_content<'js>(
let wrapper = let wrapper =
format!("(function(exports, require, module, __filename, __dirname) {{ {contents} }})"); format!("(function(exports, require, module, __filename, __dirname) {{ {contents} }})");
let module_func = ctx.eval::<Function<'js>, _>(wrapper)?; let module_func = ctx.eval::<Function, _>(wrapper)?;
module_func.call::<_, Value<'js>>((exports, require, module_obj.clone(), filename, dirname))?; module_func.call::<_, Value>((exports, require, module_obj.clone(), filename, dirname))?;
module_obj.get("exports") module_obj.get("exports")
} }
@ -279,16 +261,15 @@ pub fn execute_native_runtime(grammar_path: &Path) -> JSResult<String> {
let context = Context::full(&runtime)?; let context = Context::full(&runtime)?;
let resolver = FileResolver::default() let resolver = FileResolver::default()
.with_path("./node_modules")
.with_path("./") .with_path("./")
.with_pattern("{}.mjs"); .with_pattern("{}.mjs");
let loader = ScriptLoader::default().with_extension("mjs"); let loader = ScriptLoader::default().with_extension("mjs");
runtime.set_loader(resolver, loader); runtime.set_loader(resolver, loader);
let cwd = std::env::current_dir().map_err(|e| JSError::IO(IoError::new(&e, None)))?; let cwd = std::env::current_dir()?;
let relative_path = pathdiff::diff_paths(grammar_path, &cwd) let relative_path = pathdiff::diff_paths(grammar_path, &cwd)
.map(|p| p.to_string_lossy().to_string()) .map(|p| p.to_string_lossy().to_string())
.ok_or(JSError::RelativePath)?; .ok_or_else(|| JSError::IO("Failed to get relative path".to_string()))?;
context.with(|ctx| -> JSResult<String> { context.with(|ctx| -> JSResult<String> {
let globals = ctx.globals(); let globals = ctx.globals();

View file

@ -34,8 +34,6 @@ macro_rules! add {
macro_rules! add_whitespace { macro_rules! add_whitespace {
($this:tt) => {{ ($this:tt) => {{
// 4 bytes per char, 2 spaces per indent level
$this.buffer.reserve(4 * 2 * $this.indent_level);
for _ in 0..$this.indent_level { for _ in 0..$this.indent_level {
write!(&mut $this.buffer, " ").unwrap(); write!(&mut $this.buffer, " ").unwrap();
} }
@ -690,14 +688,13 @@ impl Generator {
flat_field_map.push((field_name.clone(), *location)); flat_field_map.push((field_name.clone(), *location));
} }
} }
let field_map_len = flat_field_map.len();
field_map_ids.push(( field_map_ids.push((
self.get_field_map_id( self.get_field_map_id(
flat_field_map, flat_field_map.clone(),
&mut flat_field_maps, &mut flat_field_maps,
&mut next_flat_field_map_index, &mut next_flat_field_map_index,
), ),
field_map_len, flat_field_map.len(),
)); ));
} }
} }
@ -965,7 +962,10 @@ impl Generator {
large_char_set_ix = Some(char_set_ix); large_char_set_ix = Some(char_set_ix);
} }
let line_break = format!("\n{}", " ".repeat(self.indent_level + 2)); let mut line_break = "\n".to_string();
for _ in 0..self.indent_level + 2 {
line_break.push_str(" ");
}
let has_positive_condition = large_char_set_ix.is_some() || !asserted_chars.is_empty(); let has_positive_condition = large_char_set_ix.is_some() || !asserted_chars.is_empty();
let has_negative_condition = !negated_chars.is_empty(); let has_negative_condition = !negated_chars.is_empty();

View file

@ -1,4 +1,4 @@
use std::{collections::BTreeMap, fmt}; use std::{collections::HashMap, fmt};
use serde::Serialize; use serde::Serialize;
use smallbitvec::SmallBitVec; use smallbitvec::SmallBitVec;
@ -34,7 +34,7 @@ pub enum Precedence {
Name(String), Name(String),
} }
pub type AliasMap = BTreeMap<Symbol, Alias>; pub type AliasMap = HashMap<Symbol, Alias>;
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize)] #[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize)]
pub struct MetadataParams { pub struct MetadataParams {
@ -60,15 +60,15 @@ pub enum Rule {
Pattern(String, String), Pattern(String, String),
NamedSymbol(String), NamedSymbol(String),
Symbol(Symbol), Symbol(Symbol),
Choice(Vec<Self>), Choice(Vec<Rule>),
Metadata { Metadata {
params: MetadataParams, params: MetadataParams,
rule: Box<Self>, rule: Box<Rule>,
}, },
Repeat(Box<Self>), Repeat(Box<Rule>),
Seq(Vec<Self>), Seq(Vec<Rule>),
Reserved { Reserved {
rule: Box<Self>, rule: Box<Rule>,
context_name: String, context_name: String,
}, },
} }

View file

@ -189,7 +189,7 @@ struct HighlightIterLayer<'a> {
depth: usize, depth: usize,
} }
pub struct _QueryCaptures<'query, 'tree, T: TextProvider<I>, I: AsRef<[u8]>> { pub struct _QueryCaptures<'query, 'tree: 'query, T: TextProvider<I>, I: AsRef<[u8]>> {
ptr: *mut ffi::TSQueryCursor, ptr: *mut ffi::TSQueryCursor,
query: &'query Query, query: &'query Query,
text_provider: T, text_provider: T,
@ -225,7 +225,7 @@ impl<'tree> _QueryMatch<'_, 'tree> {
} }
} }
impl<'query, 'tree, T: TextProvider<I>, I: AsRef<[u8]>> Iterator impl<'query, 'tree: 'query, T: TextProvider<I>, I: AsRef<[u8]>> Iterator
for _QueryCaptures<'query, 'tree, T, I> for _QueryCaptures<'query, 'tree, T, I>
{ {
type Item = (QueryMatch<'query, 'tree>, usize); type Item = (QueryMatch<'query, 'tree>, usize);
@ -344,13 +344,11 @@ impl HighlightConfiguration {
locals_query: &str, locals_query: &str,
) -> Result<Self, QueryError> { ) -> Result<Self, QueryError> {
// Concatenate the query strings, keeping track of the start offset of each section. // Concatenate the query strings, keeping track of the start offset of each section.
let mut query_source = String::with_capacity( let mut query_source = String::new();
injection_query.len() + locals_query.len() + highlights_query.len(),
);
query_source.push_str(injection_query); query_source.push_str(injection_query);
let locals_query_offset = injection_query.len(); let locals_query_offset = query_source.len();
query_source.push_str(locals_query); query_source.push_str(locals_query);
let highlights_query_offset = injection_query.len() + locals_query.len(); let highlights_query_offset = query_source.len();
query_source.push_str(highlights_query); query_source.push_str(highlights_query);
// Construct a single query by concatenating the three query strings, but record the // Construct a single query by concatenating the three query strings, but record the
@ -594,7 +592,6 @@ impl<'a> HighlightIterLayer<'a> {
} }
} }
// SAFETY:
// The `captures` iterator borrows the `Tree` and the `QueryCursor`, which // The `captures` iterator borrows the `Tree` and the `QueryCursor`, which
// prevents them from being moved. But both of these values are really just // prevents them from being moved. But both of these values are really just
// pointers, so it's actually ok to move them. // pointers, so it's actually ok to move them.

View file

@ -1,7 +1,7 @@
[package] [package]
name = "tree-sitter-language" name = "tree-sitter-language"
description = "The tree-sitter Language type, used by the library and by language implementations" description = "The tree-sitter Language type, used by the library and by language implementations"
version = "0.1.7" version = "0.1.4"
authors.workspace = true authors.workspace = true
edition.workspace = true edition.workspace = true
rust-version = "1.77" rust-version = "1.77"

View file

@ -23,15 +23,9 @@ typedef long unsigned int size_t;
typedef long unsigned int uintptr_t; typedef long unsigned int uintptr_t;
#define INT8_MAX 127
#define INT16_MAX 32767
#define INT32_MAX 2147483647L
#define INT64_MAX 9223372036854775807LL
#define UINT8_MAX 255
#define UINT16_MAX 65535 #define UINT16_MAX 65535
#define UINT32_MAX 4294967295U #define UINT32_MAX 4294967295U
#define UINT64_MAX 18446744073709551615ULL
#if defined(__wasm32__) #if defined(__wasm32__)

View file

@ -13,6 +13,4 @@ void *memset(void *dst, int value, size_t count);
int strncmp(const char *left, const char *right, size_t n); int strncmp(const char *left, const char *right, size_t n);
size_t strlen(const char *str);
#endif // TREE_SITTER_WASM_STRING_H_ #endif // TREE_SITTER_WASM_STRING_H_

View file

@ -1,5 +1,4 @@
#include <stdio.h> #include <stdio.h>
#include <string.h>
typedef struct { typedef struct {
bool left_justify; // - bool left_justify; // -
@ -106,6 +105,12 @@ static int ptr_to_str(void *ptr, char *buffer) {
return 2 + len; return 2 + len;
} }
size_t strlen(const char *str) {
const char *s = str;
while (*s) s++;
return s - str;
}
char *strncpy(char *dest, const char *src, size_t n) { char *strncpy(char *dest, const char *src, size_t n) {
char *d = dest; char *d = dest;
const char *s = src; const char *s = src;

View file

@ -58,9 +58,3 @@ int strncmp(const char *left, const char *right, size_t n) {
} }
return 0; return 0;
} }
size_t strlen(const char *str) {
const char *s = str;
while (*s) s++;
return s - str;
}

View file

@ -28,6 +28,7 @@ wasm = ["tree-sitter/wasm"]
default = ["tree-sitter-highlight", "tree-sitter-tags"] default = ["tree-sitter-highlight", "tree-sitter-tags"]
[dependencies] [dependencies]
anyhow.workspace = true
cc.workspace = true cc.workspace = true
etcetera.workspace = true etcetera.workspace = true
fs4.workspace = true fs4.workspace = true
@ -40,7 +41,6 @@ semver.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
tempfile.workspace = true tempfile.workspace = true
thiserror.workspace = true
tree-sitter = { workspace = true } tree-sitter = { workspace = true }
tree-sitter-highlight = { workspace = true, optional = true } tree-sitter-highlight = { workspace = true, optional = true }

View file

@ -1 +1 @@
4.0.15 4.0.12

File diff suppressed because it is too large Load diff

View file

@ -1 +0,0 @@
29.0

View file

@ -313,7 +313,6 @@ impl TagsContext {
) )
.ok_or(Error::Cancelled)?; .ok_or(Error::Cancelled)?;
// SAFETY:
// The `matches` iterator borrows the `Tree`, which prevents it from being // The `matches` iterator borrows the `Tree`, which prevents it from being
// moved. But the tree is really just a pointer, so it's actually ok to // moved. But the tree is really just a pointer, so it's actually ok to
// move it. // move it.

View file

@ -19,13 +19,9 @@ anstyle.workspace = true
anyhow.workspace = true anyhow.workspace = true
bindgen = { version = "0.72.0" } bindgen = { version = "0.72.0" }
clap.workspace = true clap.workspace = true
etcetera.workspace = true
indoc.workspace = true indoc.workspace = true
regex.workspace = true regex.workspace = true
schemars.workspace = true
semver.workspace = true semver.workspace = true
serde_json.workspace = true serde_json.workspace = true
tree-sitter-cli = { path = "../cli/" }
tree-sitter-loader = { path = "../loader/" }
notify = "8.2.0" notify = "8.2.0"
notify-debouncer-full = "0.6.0" notify-debouncer-full = "0.6.0"

Some files were not shown because too many files have changed in this diff Show more