Compare commits

..

No commits in common. "master" and "v0.26.3" have entirely different histories.

344 changed files with 14636 additions and 28727 deletions

2
.dockerignore Normal file
View file

@ -0,0 +1,2 @@
target
.git

5
.envrc
View file

@ -1,4 +1 @@
source_up_if_exists
if [ -z "${IN_NIX_SHELL:-}" ]; then
use flake .
fi
use flake

View file

@ -2,14 +2,6 @@ name: Bug Report
description: Report a problem
type: Bug
body:
- type: checkboxes
attributes:
label: AI Policy
description: Review our [AI Policy](https://tree-sitter.github.io/tree-sitter/6-contributing.html#ai-policy).
options:
- label: I have read the AI Policy and this issue complies with it.
required: true
- type: textarea
attributes:
label: "Problem"

View file

@ -2,14 +2,6 @@ name: Feature request
description: Request an enhancement
type: Feature
body:
- type: checkboxes
attributes:
label: AI Policy
description: Review our [AI Policy](https://tree-sitter.github.io/tree-sitter/6-contributing.html#ai-policy).
options:
- label: I have read the AI Policy and this issue complies with it.
required: true
- type: markdown
attributes:
value: |

View file

@ -10,7 +10,7 @@ outputs:
runs:
using: composite
steps:
- uses: actions/cache@v5
- uses: actions/cache@v4
id: cache
with:
path: |
@ -21,9 +21,5 @@ runs:
'lib/src/parser.h',
'lib/src/array.h',
'lib/src/alloc.h',
'lib/src/wasm-stdlib/external_scanner_stdlib.h',
'crates/loader/wasi-sdk-version',
'crates/loader/binaryen-version',
'test/fixtures/grammars/*/**/src/*.c',
'test/fixtures/fixtures.json',
'.github/actions/cache/action.yml') }}

20
.github/cliff.toml vendored
View file

@ -43,16 +43,16 @@ commit_preprocessors = [
]
# regex for parsing and grouping commits
commit_parsers = [
{ group = "<!-- 0 -->Breaking", message = "!:" },
{ group = "<!-- 1 -->Features", message = "^feat" },
{ group = "<!-- 2 -->Bug Fixes", message = "^fix" },
{ group = "<!-- 3 -->Performance", message = "^perf" },
{ group = "<!-- 4 -->Documentation", message = "^doc" },
{ group = "<!-- 5 -->Refactor", message = "^refactor" },
{ group = "<!-- 6 -->Testing", message = "^test" },
{ group = "<!-- 7 -->Build System and CI", message = "^build" },
{ group = "<!-- 7 -->Build System and CI", message = "^ci" },
{ group = "<!-- 8 -->Other", message = ".*" },
{ message = "!:", group = "<!-- 0 -->Breaking" },
{ message = "^feat", group = "<!-- 1 -->Features" },
{ message = "^fix", group = "<!-- 2 -->Bug Fixes" },
{ message = "^perf", group = "<!-- 3 -->Performance" },
{ message = "^doc", group = "<!-- 4 -->Documentation" },
{ message = "^refactor", group = "<!-- 5 -->Refactor" },
{ message = "^test", group = "<!-- 6 -->Testing" },
{ message = "^build", group = "<!-- 7 -->Build System and CI" },
{ message = "^ci", group = "<!-- 7 -->Build System and CI" },
{ message = ".*", group = "<!-- 8 -->Other" },
]
# filter out the commits that are not matched by commit parsers
filter_commits = false

View file

@ -1,6 +0,0 @@
### AI Policy
- [ ] I have read the [AI Policy](https://tree-sitter.github.io/tree-sitter/6-contributing.html#ai-policy) and this PR complies with it.
- [ ] If AI tools were used: I have disclosed the tool and extent of usage below.
<!-- If you used AI tools, state which tool and how it was used. Delete this section if not applicable. -->

16
.github/scripts/reviewers_remove.js vendored Normal file
View file

@ -0,0 +1,16 @@
module.exports = async ({ github, context }) => {
const requestedReviewers = await github.rest.pulls.listRequestedReviewers({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
});
const reviewers = requestedReviewers.data.users.map((e) => e.login);
github.rest.pulls.removeRequestedReviewers({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
reviewers: reviewers,
});
};

View file

@ -1,35 +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 files = await github.paginate(github.rest.pulls.listFiles, {
owner,
repo,
pull_number: prNumber,
per_page: 100
});
const changedFiles = files.map(file => file.filename);
const wasmStdLibSources = [
'lib/src/wasm-stdlib/external_scanner_allocator.c',
'lib/src/wasm-stdlib/imports.txt',
'lib/src/wasm-stdlib/libc.c',
'lib/src/wasm-stdlib/stdio.c'
];
const dirChanged = changedFiles.some(file =>
wasmStdLibSources.includes(file) ||
file.startsWith('lib/src/wasm-stdlib/libc/ctype/') ||
file.startsWith('lib/src/wasm-stdlib/libc/string/')
);
if (!dirChanged) return;
const wasmStdLibHeader = 'lib/src/wasm-stdlib/external_scanner_stdlib.h';
const requiredChanged = changedFiles.includes(wasmStdLibHeader);
if (!requiredChanged) core.setFailed(`Changes detected in the Wasm stdlib sources but ${wasmStdLibHeader} was not modified.`);
};

View file

@ -14,20 +14,17 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v7.0.1
with:
persist-credentials: true
ref: ${{ github.event.pull_request.base.ref }}
uses: actions/checkout@v6
- name: Create app token
uses: actions/create-github-app-token@v3
uses: actions/create-github-app-token@v2
id: app-token
with:
app-id: ${{ vars.BACKPORT_APP }}
private-key: ${{ secrets.BACKPORT_KEY }}
- name: Create backport PR
uses: korthout/backport-action@v4.6.0
uses: korthout/backport-action@v3
with:
pull_title: "${pull_title}"
label_pattern: "^ci:backport ([^ ]+)$"

View file

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

View file

@ -26,14 +26,12 @@ jobs:
- windows-x86
- macos-arm64
- macos-x64
- illumos-x64
- wasm32
include:
# When adding a new `target`:
# 1. Define a new platform alias above
# 2. Add a new record to the matrix map in `crates/cli/npm/install.js`
# 3. Consider adding the mapping at the end of 'crates/cli/Cargo.toml' for cargo-binstall support
- { platform: linux-arm64 , target: aarch64-unknown-linux-gnu , os: ubuntu-24.04-arm }
- { platform: linux-arm , target: armv7-unknown-linux-gnueabihf , os: ubuntu-24.04-arm }
- { platform: linux-x64 , target: x86_64-unknown-linux-gnu , os: ubuntu-24.04 }
@ -46,13 +44,10 @@ jobs:
- { platform: macos-x64 , target: x86_64-apple-darwin , os: macos-15-intel }
- { platform: wasm32 , target: wasm32-unknown-unknown , os: ubuntu-24.04 }
# illumos is not supported OOTB, it runs in a vm
- { platform: illumos-x64 , target: x86_64-unknown-illumos , os: ubuntu-24.04 , vm: true , no-run: true }
# Extra features
- { platform: linux-arm64 , features: wasm }
- { platform: linux-x64 , features: wasm , run-wasm-test: true }
- { platform: macos-arm64 , features: wasm , run-wasm-test: true }
- { platform: linux-x64 , features: wasm }
- { platform: macos-arm64 , features: wasm }
- { platform: macos-x64 , features: wasm }
# Cross-compilation
@ -73,7 +68,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v7.0.1
uses: actions/checkout@v6
- name: Set up cross-compilation
if: matrix.cross
@ -90,48 +85,20 @@ jobs:
} >> $GITHUB_ENV
- name: Get emscripten version
if: contains(matrix.features, 'wasm') && (matrix.run-wasm-test || !inputs.run-test)
if: contains(matrix.features, 'wasm')
run: printf 'EMSCRIPTEN_VERSION=%s\n' "$(<crates/loader/emscripten-version)" >> $GITHUB_ENV
- name: Cache Emscripten SDK
if: contains(matrix.features, 'wasm') && (matrix.run-wasm-test || !inputs.run-test)
uses: actions/cache@v6
with:
path: emsdk
key: emsdk-${{ env.EMSCRIPTEN_VERSION }}-${{ runner.os }}-${{ runner.arch }}
- name: Install Emscripten
if: contains(matrix.features, 'wasm') && (matrix.run-wasm-test || !inputs.run-test)
run: |
if [[ ! -d emsdk ]]; then
git clone --depth 1 https://github.com/emscripten-core/emsdk.git
fi
cd emsdk
./emsdk install ${{ env.EMSCRIPTEN_VERSION }}
./emsdk activate ${{ env.EMSCRIPTEN_VERSION }}
echo "$PWD" >> "$GITHUB_PATH"
echo "$PWD/upstream/emscripten" >> "$GITHUB_PATH"
echo "EMSDK=$PWD" >> "$GITHUB_ENV"
echo "EMSDK_NODE=$PWD/node/$(ls node)/bin/node" >> "$GITHUB_ENV"
- name: Set up Node.js
if: contains(matrix.features, 'wasm') && (matrix.run-wasm-test || !inputs.run-test)
uses: actions/setup-node@v7.0.0
if: contains(matrix.features, 'wasm')
uses: mymindstorm/setup-emsdk@v14
with:
node-version: 24
cache: npm
cache-dependency-path: lib/binding_web/package-lock.json
version: ${{ env.EMSCRIPTEN_VERSION }}
- name: Set up Rust
if: ${{ !matrix.vm }}
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
target: ${{ matrix.target }}
- name: Install Rust Wasm test target
if: matrix.run-wasm-test
run: rustup toolchain install nightly --profile minimal --component rust-src
- name: Install cross-compilation toolchain
if: matrix.cross
run: |
@ -150,12 +117,34 @@ jobs:
if: matrix.platform == 'windows-x64'
uses: msys2/setup-msys2@v2
with:
update: true
install: |
mingw-w64-x86_64-toolchain
mingw-w64-x86_64-clang
mingw-w64-x86_64-make
mingw-w64-x86_64-cmake
# TODO: Remove RUSTFLAGS="--cap-lints allow" once we use a wasmtime release that addresses
# the `mismatched-lifetime-syntaxes` lint
- name: Build wasmtime library (Windows x64 MSYS2)
if: contains(matrix.features, 'wasm') && matrix.platform == 'windows-x64'
run: |
mkdir -p target
WASMTIME_VERSION=$(cargo metadata --format-version=1 --locked --features wasm | \
jq -r '.packages[] | select(.name == "wasmtime-c-api-impl") | .version')
curl -LSs "$WASMTIME_REPO/archive/refs/tags/v${WASMTIME_VERSION}.tar.gz" | tar xzf - -C target
cd target/wasmtime-${WASMTIME_VERSION}
cmake -S crates/c-api -B target/c-api \
-DCMAKE_INSTALL_PREFIX="$PWD/artifacts" \
-DWASMTIME_DISABLE_ALL_FEATURES=ON \
-DWASMTIME_FEATURE_CRANELIFT=ON \
-DWASMTIME_TARGET='x86_64-pc-windows-gnu'
cmake --build target/c-api && cmake --install target/c-api
printf 'CMAKE_PREFIX_PATH=%s\n' "$PWD/artifacts" >> $GITHUB_ENV
env:
WASMTIME_REPO: https://github.com/bytecodealliance/wasmtime
RUSTFLAGS: ${{ env.RUSTFLAGS }} --cap-lints allow
- name: Build C library (Windows x64 MSYS2 CMake)
if: matrix.platform == 'windows-x64'
shell: msys2 {0}
@ -182,26 +171,29 @@ jobs:
env:
WASM: ${{ contains(matrix.features, 'wasm') && 'ON' || 'OFF' }}
- name: Download wasmtime C API
if: contains(matrix.features, 'wasm') && (matrix.run-wasm-test || !inputs.run-test)
# TODO: Remove RUSTFLAGS="--cap-lints allow" once we use a wasmtime release that addresses
# the `mismatched-lifetime-syntaxes` lint
- name: Build wasmtime library
if: contains(matrix.features, 'wasm')
run: |
mkdir -p target
WASMTIME_VERSION=$(cargo metadata --format-version=1 --locked --features wasm | \
jq -r '.packages[] | select(.name == "wasmtime-c-api-impl") | .version')
case '${{ matrix.target }}' in
x86_64-unknown-linux-gnu) WT_TARGET=x86_64-linux ;;
aarch64-unknown-linux-gnu) WT_TARGET=aarch64-linux ;;
x86_64-apple-darwin) WT_TARGET=x86_64-macos ;;
aarch64-apple-darwin) WT_TARGET=aarch64-macos ;;
esac
curl -LSs "$WASMTIME_REPO/releases/download/v${WASMTIME_VERSION}/wasmtime-v${WASMTIME_VERSION}-${WT_TARGET}-c-api.tar.xz" \
| tar xJf - -C target
printf 'CMAKE_PREFIX_PATH=%s\n' "$PWD/target/wasmtime-v${WASMTIME_VERSION}-${WT_TARGET}-c-api" >> $GITHUB_ENV
curl -LSs "$WASMTIME_REPO/archive/refs/tags/v${WASMTIME_VERSION}.tar.gz" | tar xzf - -C target
cd target/wasmtime-${WASMTIME_VERSION}
cmake -S crates/c-api -B target/c-api \
-DCMAKE_INSTALL_PREFIX="$PWD/artifacts" \
-DWASMTIME_DISABLE_ALL_FEATURES=ON \
-DWASMTIME_FEATURE_CRANELIFT=ON \
-DWASMTIME_TARGET='${{ matrix.target }}'
cmake --build target/c-api && cmake --install target/c-api
printf 'CMAKE_PREFIX_PATH=%s\n' "$PWD/artifacts" >> $GITHUB_ENV
env:
WASMTIME_REPO: https://github.com/bytecodealliance/wasmtime
RUSTFLAGS: ${{ env.RUSTFLAGS }} --cap-lints allow
- name: Build C library (make)
if: runner.os != 'Windows' && !matrix.vm
if: runner.os != 'Windows'
run: |
if [[ $PLATFORM == linux-arm ]]; then
CC=arm-linux-gnueabihf-gcc; AR=arm-linux-gnueabihf-ar
@ -215,10 +207,10 @@ jobs:
make -j CFLAGS="$CFLAGS" CC=$CC AR=$AR
env:
PLATFORM: ${{ matrix.platform }}
CFLAGS: -g -Werror -Wall -Wextra -Wshadow -Wpedantic -Werror=incompatible-pointer-types -Werror=strict-aliasing -Wstrict-aliasing=2
CFLAGS: -g -Werror -Wall -Wextra -Wshadow -Wpedantic -Werror=incompatible-pointer-types
- name: Build C library (CMake)
if: "!matrix.cross && !matrix.vm"
if: "!matrix.cross"
run: |
cmake -S . -B build/static \
-DBUILD_SHARED_LIBS=OFF \
@ -235,24 +227,10 @@ jobs:
cmake --build build/shared --verbose
env:
CC: ${{ contains(matrix.platform, 'linux') && 'clang' || '' }}
WASM: ${{ contains(matrix.features, 'wasm') && (matrix.run-wasm-test || !inputs.run-test) && 'ON' || 'OFF' }}
- name: Build C library and Rust crate (illumos gmake)
if: matrix.platform == 'illumos-x64'
uses: vmactions/omnios-vm@v1.3.6
with:
release: r151056-build
copyback: false
prepare: |
pkg install -q build-essential || [ $? -eq 4 ]
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal
run: |
. "$HOME/.cargo/env"
gmake -j
cargo build -p tree-sitter
WASM: ${{ contains(matrix.features, 'wasm') && 'ON' || 'OFF' }}
- name: Build Wasm library
if: contains(matrix.features, 'wasm') && (matrix.run-wasm-test || !inputs.run-test)
if: contains(matrix.features, 'wasm')
shell: bash
run: |
cd lib/binding_web
@ -269,8 +247,7 @@ jobs:
run: cargo check --no-default-features --target='${{ matrix.target }}'
- name: Build target
if: "!inputs.run-test && !matrix.vm"
run: cargo build --release --target='${{ matrix.target }}' --features='${{ (matrix.run-wasm-test || !inputs.run-test) && matrix.features || '' }}' $PACKAGE
run: cargo build --release --target='${{ matrix.target }}' --features='${{ matrix.features }}' $PACKAGE
env:
PACKAGE: ${{ matrix.platform == 'wasm32' && '-p tree-sitter' || '' }}
@ -288,24 +265,20 @@ jobs:
run: cargo run -p xtask --target='${{ matrix.target }}' -- generate-fixtures
- name: Generate Wasm fixtures
if: inputs.run-test && !matrix.no-run && contains(matrix.features, 'wasm') && matrix.run-wasm-test && steps.cache.outputs.cache-hit != 'true'
if: inputs.run-test && !matrix.no-run && contains(matrix.features, 'wasm') && steps.cache.outputs.cache-hit != 'true'
run: cargo run -p xtask --target='${{ matrix.target }}' -- generate-fixtures --wasm
- name: Run main tests
if: inputs.run-test && !matrix.no-run
run: cargo test --workspace --target='${{ matrix.target }}' --features='${{ (matrix.run-wasm-test || !inputs.run-test) && matrix.features || '' }}'
run: cargo test --target='${{ matrix.target }}' --features='${{ matrix.features }}'
- name: Run Wasm tests
if: inputs.run-test && !matrix.no-run && contains(matrix.features, 'wasm') && matrix.run-wasm-test
if: inputs.run-test && !matrix.no-run && contains(matrix.features, 'wasm')
run: cargo run -p xtask --target='${{ matrix.target }}' -- test-wasm
- name: Run Rust Wasm web test
if: inputs.run-test && !matrix.no-run && contains(matrix.features, 'wasm') && matrix.run-wasm-test
run: cargo run -p xtask --target='${{ matrix.target }}' -- test-rust-wasm-web
- name: Upload CLI artifact
if: "!inputs.run-test && !matrix.no-run"
uses: actions/upload-artifact@v7
if: "!matrix.no-run"
uses: actions/upload-artifact@v5
with:
name: tree-sitter.${{ matrix.platform }}
path: target/${{ matrix.target }}/release/tree-sitter${{ contains(matrix.target, 'windows') && '.exe' || '' }}
@ -313,8 +286,8 @@ jobs:
retention-days: 7
- name: Upload Wasm artifacts
if: "!inputs.run-test && matrix.platform == 'linux-x64'"
uses: actions/upload-artifact@v7
if: matrix.platform == 'linux-x64'
uses: actions/upload-artifact@v5
with:
name: tree-sitter.wasm
path: |

View file

@ -1,14 +1,21 @@
name: CI
on:
push:
branches:
- 'master'
- 'release-[0-9]+.[0-9]+'
pull_request:
branches:
- 'master'
- 'release-[0-9]+.[0-9]+'
paths-ignore:
- docs/**
- "**/README.md"
- CONTRIBUTING.md
- LICENSE
- cli/src/templates
push:
branches: [master]
paths-ignore:
- docs/**
- "**/README.md"
- CONTRIBUTING.md
- LICENSE
- cli/src/templates
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@ -19,7 +26,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v7.0.1
uses: actions/checkout@v6
- name: Set up stable Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
@ -27,25 +34,13 @@ jobs:
toolchain: stable
components: clippy, rustfmt
- name: Lint Rust files
run: make lint
- name: Install Taplo
uses: taiki-e/install-action@v2
with:
tool: taplo@0.10.0
- name: Lint TOML files
run: make lint-toml
- name: Lint web files
run: make lint-web
- name: Lint files
run: |
make lint
make lint-web
sanitize:
uses: ./.github/workflows/sanitize.yml
build:
uses: ./.github/workflows/build.yml
check-wasm-stdlib:
uses: ./.github/workflows/wasm_stdlib.yml

View file

@ -1,22 +0,0 @@
name: Crate Versions Check
on:
pull_request:
types: [labeled, opened, synchronize, reopened]
workflow_dispatch:
jobs:
check-crates:
runs-on: ubuntu-latest
if: contains(github.event.pull_request.labels.*.name, 'ci:check release') || github.event_name == 'workflow_dispatch'
steps:
- name: Checkout repository
uses: actions/checkout@v7.0.1
- name: Set up Rust
uses: actions-rust-lang/setup-rust-toolchain@v1
- name: Check crates against crates.io
uses: katyo/publish-crates@v2
with:
dry-run: true

View file

@ -16,29 +16,35 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v7.0.1
uses: actions/checkout@v6
- name: Set up Rust
uses: actions-rust-lang/setup-rust-toolchain@v1
- name: Install mdbook
env:
GH_TOKEN: ${{ github.token }}
run: |
jq_expr='.assets[] | select(.name | contains("x86_64-unknown-linux-gnu")) | .browser_download_url'
url=$(gh api repos/rust-lang/mdbook/releases/tags/v0.5.4 --jq "$jq_expr")
url=$(gh api repos/rust-lang/mdbook/releases/tags/v0.4.52 --jq "$jq_expr")
mkdir mdbook
curl -sSL "$url" | tar -xz -C mdbook
printf '%s/mdbook\n' "$PWD" >> "$GITHUB_PATH"
- name: Install mdbook-admonish
run: cargo install mdbook-admonish
- name: Build Book
run: mdbook build docs
- name: Setup Pages
uses: actions/configure-pages@v6
uses: actions/configure-pages@v5
- name: Upload artifact
uses: actions/upload-pages-artifact@v5
uses: actions/upload-pages-artifact@v4
with:
path: docs/book
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v5
uses: actions/deploy-pages@v4

View file

@ -3,7 +3,6 @@ name: nvim-treesitter parser tests
on:
pull_request:
paths:
- 'lib/**'
- 'crates/cli/**'
- 'crates/config/**'
- 'crates/generate/**'
@ -26,54 +25,34 @@ jobs:
name: ${{ matrix.os }} - ${{ matrix.type }}
runs-on: ${{ matrix.os }}
env:
NVIM: ${{ matrix.os == 'windows-latest' && 'nvim.exe' || 'nvim' }}
NVIM_TAG: stable
NVIM_DIR: neovim
NVIM: ${{ matrix.os == 'windows-latest' && 'nvim-win64\\bin\\nvim.exe' || 'nvim' }}
NVIM_TS_DIR: nvim-treesitter
steps:
- uses: actions/checkout@v7.0.1
- uses: actions-rust-lang/setup-rust-toolchain@v1
- run: cargo build --profile optimize
- uses: actions/checkout@v6
- name: Clone Neovim
uses: actions/checkout@v7.0.1
with:
repository: neovim/neovim
ref: ${{ env.NVIM_TAG }}
path: ${{ env.NVIM_DIR }}
- if: runner.os != 'Windows'
name: Setup environment (Posix)
run: |
echo ${{ github.workspace }}/target/optimize >> "$GITHUB_PATH"
echo ${{ github.workspace }}/neovim/build/bin >> "$GITHUB_PATH"
echo "VIMRUNTIME=${{ github.workspace }}/neovim/runtime" >> "$GITHUB_ENV"
- if: runner.os == 'Windows'
name: Setup environment (why can't you just be normal?!)
run: |
${{ env.NVIM_DIR }}/.github/scripts/env.ps1
echo ${{ github.workspace }}/target/optimize >> "$env:GITHUB_PATH"
echo ${{ github.workspace }}/neovim/build/bin >> "$env:GITHUB_PATH"
echo "VIMRUNTIME=${{ github.workspace }}/neovim/runtime" >> "$env:GITHUB_ENV"
- name: Build Neovim
working-directory: ${{ env.NVIM_DIR }}
run: |
cmake -S cmake.deps -B .deps -G Ninja -D CMAKE_BUILD_TYPE=Release -D TREESITTER_URL=https://github.com/tree-sitter/tree-sitter/archive/${{ github.event.pull_request.head.sha }}.tar.gz -D DEPS_IGNORE_SHA=TRUE
cmake --build .deps --config Release
cmake -B build -G Ninja -D CMAKE_BUILD_TYPE=Release
cmake --build build --config Release
- name: Clone nvim-treesitter
uses: actions/checkout@v7.0.1
- uses: actions/checkout@v6
with:
repository: nvim-treesitter/nvim-treesitter
path: ${{ env.NVIM_TS_DIR }}
ref: main
- if: runner.os != 'Windows'
run: echo ${{ github.workspace }}/target/release >> $GITHUB_PATH
- if: runner.os == 'Windows'
run: echo ${{ github.workspace }}/target/release >> $env:GITHUB_PATH
- uses: actions-rust-lang/setup-rust-toolchain@v1
- run: cargo build --release
- uses: ilammy/msvc-dev-cmd@v1
- name: Install and prepare Neovim
run: bash ./scripts/ci-install.sh
working-directory: ${{ env.NVIM_TS_DIR }}
- if: matrix.type == 'generate'
name: Generate and compile parsers
run: $NVIM -l ./scripts/install-parsers.lua --generate --max-jobs=10
run: $NVIM -l ./scripts/install-parsers.lua --generate --max-jobs=2
working-directory: ${{ env.NVIM_TS_DIR }}
shell: bash
@ -84,13 +63,7 @@ jobs:
shell: bash
- if: "!cancelled()"
name: Test parsers
run: $NVIM -l ./scripts/check-parsers.lua
working-directory: ${{ env.NVIM_TS_DIR }}
shell: bash
- if: "!cancelled()"
name: Test queries
name: Check query files
run: $NVIM -l ./scripts/check-queries.lua
working-directory: ${{ env.NVIM_TS_DIR }}
shell: bash

View file

@ -1,14 +1,7 @@
name: Release
on:
schedule:
- cron: '5 5 * * *'
workflow_dispatch:
inputs:
tag_name:
description: 'Tag name for release'
required: false
default: nightly
push:
tags:
- v[0-9]+.[0-9]+.[0-9]+
@ -28,22 +21,11 @@ jobs:
attestations: write
contents: write
steps:
- if: github.event_name == 'workflow_dispatch'
env:
TAG_NAME: ${{ github.event.inputs.tag_name }}
run: echo "TAG_NAME=${TAG_NAME}" >> $GITHUB_ENV
- if: github.event_name == 'schedule'
run: echo 'TAG_NAME=nightly' >> $GITHUB_ENV
- if: github.event_name == 'push'
run: echo "TAG_NAME=${GITHUB_REF_NAME}" >> $GITHUB_ENV
- name: Checkout repository
uses: actions/checkout@v7.0.1
uses: actions/checkout@v6
- name: Download build artifacts
uses: actions/download-artifact@v8
uses: actions/download-artifact@v6
with:
path: artifacts
@ -62,41 +44,28 @@ jobs:
for platform in $(cd artifacts; ls | sed 's/^tree-sitter\.//'); do
exe=$(ls artifacts/tree-sitter.$platform/tree-sitter*)
chmod +x $exe
gzip --stdout --name $exe > target/tree-sitter-$platform.gz
zip -j9 target/tree-sitter-cli-$platform.zip $exe
done
rm -rf artifacts
ls -l target/
- name: Generate attestations
uses: actions/attest-build-provenance@v4
uses: actions/attest-build-provenance@v3
with:
subject-path: |
target/tree-sitter-*.gz
target/tree-sitter-cli-*.zip
target/web-tree-sitter.tar.gz
- if: env.TAG_NAME == 'nightly'
run: |
echo 'PRERELEASE=--prerelease' >> $GITHUB_ENV
gh release delete nightly --yes || true
git push https://${GITHUB_ACTOR}:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY} :nightly || true
env:
GH_TOKEN: ${{ github.token }}
- name: Create release
run: |-
gh release create ${{ env.TAG_NAME }} $PRERELEASE \
gh release create $GITHUB_REF_NAME \
target/tree-sitter-*.gz \
target/tree-sitter-cli-*.zip \
target/web-tree-sitter.tar.gz
env:
GH_TOKEN: ${{ github.token }}
crates_io:
name: Publish packages to Crates.io
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.tag_name != 'nightly')
runs-on: ubuntu-latest
environment: crates
permissions:
@ -105,7 +74,7 @@ jobs:
needs: release
steps:
- name: Checkout repository
uses: actions/checkout@v7.0.1
uses: actions/checkout@v6
- name: Set up Rust
uses: actions-rust-lang/setup-rust-toolchain@v1
@ -121,7 +90,6 @@ jobs:
npm:
name: Publish packages to npmjs.com
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.tag_name != 'nightly')
runs-on: ubuntu-latest
environment: npm
permissions:
@ -134,10 +102,10 @@ jobs:
directory: [crates/cli/npm, lib/binding_web]
steps:
- name: Checkout repository
uses: actions/checkout@v7.0.1
uses: actions/checkout@v6
- name: Set up Node
uses: actions/setup-node@v7.0.0
uses: actions/setup-node@v6
with:
node-version: 24
registry-url: https://registry.npmjs.org

View file

@ -17,13 +17,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout script
uses: actions/checkout@v7.0.1
uses: actions/checkout@v6
with:
sparse-checkout: .github/scripts/close_unresponsive.js
sparse-checkout-cone-mode: false
- name: Run script
uses: actions/github-script@v9
uses: actions/github-script@v8
with:
script: |
const script = require('./.github/scripts/close_unresponsive.js')
@ -35,13 +35,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout script
uses: actions/checkout@v7.0.1
uses: actions/checkout@v6
with:
sparse-checkout: .github/scripts/remove_response_label.js
sparse-checkout-cone-mode: false
- name: Run script
uses: actions/github-script@v9
uses: actions/github-script@v8
with:
script: |
const script = require('./.github/scripts/remove_response_label.js')

View file

@ -11,21 +11,15 @@ jobs:
remove-reviewers:
runs-on: ubuntu-latest
steps:
- name: Remove reviewers
uses: actions/github-script@v9
- name: Checkout script
uses: actions/checkout@v6
with:
sparse-checkout: .github/scripts/reviewers_remove.js
sparse-checkout-cone-mode: false
- name: Run script
uses: actions/github-script@v8
with:
script: |
const requestedReviewers = await github.rest.pulls.listRequestedReviewers({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
});
const reviewers = requestedReviewers.data.users.map((e) => e.login);
github.rest.pulls.removeRequestedReviewers({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
reviewers: reviewers,
});
const script = require('./.github/scripts/reviewers_remove.js')
await script({github, context})

View file

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

View file

@ -16,13 +16,13 @@ jobs:
if: github.event.label.name == 'spam'
steps:
- name: Checkout script
uses: actions/checkout@v7.0.1
uses: actions/checkout@v6
with:
sparse-checkout: .github/scripts/close_spam.js
sparse-checkout-cone-mode: false
- name: Run script
uses: actions/github-script@v9
uses: actions/github-script@v8
with:
script: |
const script = require('./.github/scripts/close_spam.js')

View file

@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v7.0.1
uses: actions/checkout@v6
- name: Set up stable Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
@ -31,7 +31,7 @@ jobs:
- name: Build C library (make)
run: make -j CFLAGS="$CFLAGS"
env:
CFLAGS: -g -Werror -Wall -Wextra -Wshadow -Wpedantic -Werror=incompatible-pointer-types -Werror=strict-aliasing -Wstrict-aliasing=2
CFLAGS: -g -Werror -Wall -Wextra -Wshadow -Wpedantic -Werror=incompatible-pointer-types
- name: Build Wasm Library
working-directory: lib/binding_web

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@v7.0.1
- name: Check directory changes
uses: actions/github-script@v9
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,19 +0,0 @@
[formatting]
column_width = 100
compact_arrays = false
reorder_inline_tables = true
reorder_keys = true
[[rule]]
include = [ "**/Cargo.toml" ]
keys = [ "package" ]
[rule.formatting]
reorder_keys = false
[[rule]]
include = [ "**/Cargo.toml" ]
keys = [ "profile" ]
[rule.formatting]
reorder_keys = false

11
.zed/settings.json Normal file
View file

@ -0,0 +1,11 @@
{
"lsp": {
"rust-analyzer": {
"initialization_options": {
"cargo": {
"features": "all"
}
}
}
}
}

View file

@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.13)
project(tree-sitter
VERSION "0.28.0"
VERSION "0.26.3"
DESCRIPTION "An incremental parsing system for programming tools"
HOMEPAGE_URL "https://tree-sitter.github.io/tree-sitter/"
LANGUAGES C)
@ -33,8 +33,7 @@ if(MSVC)
else()
target_compile_options(tree-sitter PRIVATE
-Wall -Wextra -Wshadow -Wpedantic
-Werror=incompatible-pointer-types
-Werror=strict-aliasing -Wstrict-aliasing=2)
-Werror=incompatible-pointer-types)
endif()
if(TREE_SITTER_FEATURE_WASM)
@ -82,7 +81,7 @@ set_target_properties(tree-sitter
SOVERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}"
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 _DARWIN_C_SOURCE)
include(GNUInstallDirs)

BIN
Cargo.lock generated

Binary file not shown.

View file

@ -1,5 +1,5 @@
[workspace]
default-members = [ "crates/cli" ]
default-members = ["crates/cli"]
members = [
"crates/cli",
"crates/config",
@ -14,22 +14,25 @@ members = [
resolver = "2"
[workspace.package]
authors = [ "Max Brunsfeld <maxbrunsfeld@gmail.com>", "Amaan Qureshi <amaanq12@gmail.com>" ]
categories = [ "command-line-utilities", "parsing" ]
edition = "2024"
version = "0.26.3"
authors = [
"Max Brunsfeld <maxbrunsfeld@gmail.com>",
"Amaan Qureshi <amaanq12@gmail.com>",
]
edition = "2021"
rust-version = "1.84"
homepage = "https://tree-sitter.github.io/tree-sitter"
keywords = [ "incremental", "parsing" ]
license = "MIT"
repository = "https://github.com/tree-sitter/tree-sitter"
rust-version = "1.90"
version = "0.28.0"
license = "MIT"
keywords = ["incremental", "parsing"]
categories = ["command-line-utilities", "parsing"]
[workspace.lints.clippy]
cargo = { level = "warn", priority = -1 }
dbg_macro = "deny"
nursery = { level = "warn", priority = -1 }
pedantic = { level = "warn", priority = -1 }
todo = "deny"
pedantic = { level = "warn", priority = -1 }
nursery = { level = "warn", priority = -1 }
cargo = { level = "warn", priority = -1 }
# The lints below are a specific subset of the pedantic+nursery lints
# that we explicitly allow in the tree-sitter codebase because they either:
@ -38,31 +41,52 @@ todo = "deny"
# 2. Are unnecessary, or
# 3. Worsen the code
branches_sharing_code = "allow"
cast_lossless = "allow"
cast_possible_truncation = "allow"
cast_possible_wrap = "allow"
cast_precision_loss = "allow"
cast_sign_loss = "allow"
checked_conversions = "allow"
cognitive_complexity = "allow"
collection_is_never_read = "allow"
fallible_impl_from = "allow"
fn_params_excessive_bools = "allow"
inline_always = "allow"
if_not_else = "allow"
items_after_statements = "allow"
match_wildcard_for_single_variants = "allow"
missing_errors_doc = "allow"
missing_panics_doc = "allow"
module_name_repetitions = "allow"
multiple_crate_versions = "allow"
needless_for_each = "allow"
obfuscated_if_else = "allow"
option_if_let_else = "allow"
or_fun_call = "allow"
range_plus_one = "allow"
redundant_clone = "allow"
redundant_closure_for_method_calls = "allow"
ref_option = "allow"
similar_names = "allow"
string_lit_as_bytes = "allow"
struct_excessive_bools = "allow"
struct_field_names = "allow"
transmute_undefined_repr = "allow"
too_many_lines = "allow"
tuple_array_conversions = "allow"
unnecessary_wraps = "allow"
unused_self = "allow"
used_underscore_items = "allow"
[workspace.lints.rust]
mismatched_lifetime_syntaxes = "allow"
[profile.optimize]
inherits = "release"
codegen-units = 1 # Maximum size reduction optimizations.
strip = true # Automatically strip symbols from the binary.
lto = true # Link-time optimization.
opt-level = 3 # Optimization level 3.
strip = true # Automatically strip symbols from the binary.
codegen-units = 1 # Maximum size reduction optimizations.
[profile.size]
inherits = "optimize"
@ -70,69 +94,70 @@ opt-level = "s" # Optimize for size.
[profile.release-dev]
inherits = "release"
codegen-units = 256
lto = false
debug = true
debug-assertions = true
incremental = true
lto = false
overflow-checks = true
incremental = true
codegen-units = 256
[workspace.dependencies]
ansi_colours = "1.2.3"
anstyle = "1.0.14"
anyhow = "1.0.102"
bstr = "1.12.1"
cc = "1.2.63"
clap = { features = [
anstyle = "1.0.13"
anyhow = "1.0.100"
bstr = "1.12.0"
cc = "1.2.48"
clap = { version = "4.5.53", features = [
"cargo",
"derive",
"env",
"help",
"string",
"unstable-styles",
], version = "4.5.58" }
clap_complete = "4.6.3"
] }
clap_complete = "4.5.61"
clap_complete_nushell = "4.5.10"
crc32fast = "1.5.0"
ctor = "0.6.3"
ctrlc = { features = [ "termination" ], version = "3.5.2" }
dialoguer = { features = [ "fuzzy-select" ], version = "0.12.0" }
ctor = "0.2.9"
ctrlc = { version = "3.5.0", features = ["termination"] }
dialoguer = { version = "0.11.0", features = ["fuzzy-select"] }
etcetera = "0.11.0"
fs4 = "0.12.0"
glob = "0.3.3"
hashbrown = { default-features = false, version = "0.17.1" }
heck = "0.5.0"
html-escape = "0.2.13"
indexmap = "2.13.0"
indoc = "2.0.7"
indexmap = "2.12.1"
indoc = "2.0.6"
libloading = "0.9.0"
log = { features = [ "std" ], version = "0.4.30" }
memchr = "2.8.1"
log = { version = "0.4.28", features = ["std"] }
memchr = "2.7.6"
once_cell = "1.21.3"
pretty_assertions = "1.4.1"
rand = "0.10.1"
regex = "1.12.3"
regex-syntax = "0.8.9"
rand = "0.8.5"
regex = "1.11.3"
regex-syntax = "0.8.6"
rustc-hash = "2.1.1"
schemars = "1.2.1"
semver = { features = [ "serde" ], version = "1.0.27" }
serde = { features = [ "derive" ], version = "1.0.228" }
serde_json = { features = [ "preserve_order" ], version = "1.0.150" }
schemars = "1.0.5"
semver = { version = "1.0.27", features = ["serde"] }
serde = { version = "1.0.219", features = ["derive"] }
serde_json = { version = "1.0.145", features = ["preserve_order"] }
similar = "2.7.0"
smallbitvec = "2.6.0"
streaming-iterator = "0.1.9"
tempfile = "3.25.0"
thiserror = "2.0.18"
tempfile = "3.23.0"
thiserror = "2.0.17"
tiny_http = "0.12.0"
topological-sort = "0.2.2"
unindent = "0.2.4"
walkdir = "2.5.0"
wasmparser = "0.244.0"
webbrowser = "1.2.1"
wasmparser = "0.243.0"
webbrowser = "1.0.5"
tree-sitter = { path = "./lib", version = "0.28.0" }
tree-sitter-config = { path = "./crates/config", version = "0.28.0" }
tree-sitter-generate = { default-features = false, path = "./crates/generate", version = "0.28.0" }
tree-sitter-highlight = { path = "./crates/highlight", version = "0.28.0" }
tree-sitter-loader = { path = "./crates/loader", version = "0.28.0" }
tree-sitter-tags = { path = "./crates/tags", version = "0.28.0" }
tree-sitter = { version = "0.26.3", path = "./lib" }
tree-sitter-generate = { version = "0.26.3", path = "./crates/generate" }
tree-sitter-loader = { version = "0.26.3", path = "./crates/loader" }
tree-sitter-config = { version = "0.26.3", path = "./crates/config" }
tree-sitter-highlight = { version = "0.26.3", path = "./crates/highlight" }
tree-sitter-tags = { version = "0.26.3", path = "./crates/tags" }
tree-sitter-language = { path = "./crates/language", version = "0.1.8" }
tree-sitter-language = { version = "0.1", path = "./crates/language" }

10
Dockerfile Normal file
View file

@ -0,0 +1,10 @@
FROM rust:1.76-buster
WORKDIR /app
RUN apt-get update
RUN apt-get install -y nodejs
COPY . .
CMD cargo test --all-features

View file

@ -1,4 +1,4 @@
VERSION := 0.28.0
VERSION := 0.26.3
DESCRIPTION := An incremental parsing system for programming tools
HOMEPAGE_URL := https://tree-sitter.github.io/tree-sitter/
@ -22,9 +22,9 @@ OBJ := $(SRC:.c=.o)
# define default flags, and override to append mandatory flags
ARFLAGS := rcs
CFLAGS ?= -O3 -Wall -Wextra -Wshadow -Wpedantic -Werror=incompatible-pointer-types -Werror=strict-aliasing -Wstrict-aliasing=2
CFLAGS ?= -O3 -Wall -Wextra -Wshadow -Wpedantic -Werror=incompatible-pointer-types
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 -D_DARWIN_C_SOURCE
override CFLAGS += -Ilib/src -Ilib/src/wasm -Ilib/include
# ABI versioning
@ -122,6 +122,7 @@ test-wasm:
lint:
cargo update --workspace --locked --quiet
cargo check --workspace --all-targets
cargo fmt --all --check
cargo clippy --workspace --all-targets -- -D warnings
@ -129,13 +130,8 @@ lint-web:
npm --prefix lib/binding_web ci
npm --prefix lib/binding_web run lint
lint-toml:
taplo check
taplo format --check --diff
format:
cargo fmt --all
taplo format
changelog:
@git-cliff --config .github/cliff.toml --prepend CHANGELOG.md --latest --github-token $(shell gh auth token)

34
Package.swift Normal file
View file

@ -0,0 +1,34 @@
// swift-tools-version: 5.8
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "TreeSitter",
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "TreeSitter",
targets: ["TreeSitter"]),
],
targets: [
.target(name: "TreeSitter",
path: "lib",
exclude: [
"src/unicode/ICU_SHA",
"src/unicode/README.md",
"src/unicode/LICENSE",
"src/wasm/stdlib-symbols.txt",
"src/lib.c",
],
sources: ["src"],
publicHeadersPath: "include",
cSettings: [
.headerSearchPath("src"),
.define("_POSIX_C_SOURCE", to: "200112L"),
.define("_DEFAULT_SOURCE"),
.define("_DARWIN_C_SOURCE"),
]),
],
cLanguageStandard: .c11
)

View file

@ -4,56 +4,50 @@ pub fn build(b: *std.Build) !void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
var threaded: std.Io.Threaded = .init(b.allocator, .{});
defer threaded.deinit();
const io = threaded.io();
const wasm = b.option(bool, "enable-wasm", "Enable Wasm support") orelse false;
const shared = b.option(bool, "build-shared", "Build a shared library") orelse false;
const amalgamated = b.option(bool, "amalgamated", "Build using an amalgamated source") orelse false;
var tree_sitter = b.createModule(.{
.target = target,
.optimize = optimize,
.link_libc = true,
.pic = if (shared) true else null,
});
const lib: *std.Build.Step.Compile = b.addLibrary(.{
.name = "tree-sitter",
.linkage = if (shared) .dynamic else .static,
.root_module = tree_sitter,
.root_module = b.createModule(.{
.target = target,
.optimize = optimize,
.link_libc = true,
.pic = if (shared) true else null,
}),
});
if (amalgamated) {
tree_sitter.addCSourceFile(.{
lib.addCSourceFile(.{
.file = b.path("lib/src/lib.c"),
.flags = &.{"-std=c11"},
});
} else {
const files = try findSourceFiles(b, io);
const files = try findSourceFiles(b);
defer b.allocator.free(files);
tree_sitter.addCSourceFiles(.{
lib.addCSourceFiles(.{
.root = b.path("lib/src"),
.files = files,
.flags = &.{"-std=c11"},
});
}
tree_sitter.addIncludePath(b.path("lib/include"));
tree_sitter.addIncludePath(b.path("lib/src"));
tree_sitter.addIncludePath(b.path("lib/src/wasm"));
lib.addIncludePath(b.path("lib/include"));
lib.addIncludePath(b.path("lib/src"));
lib.addIncludePath(b.path("lib/src/wasm"));
tree_sitter.addCMacro("_POSIX_C_SOURCE", "200112L");
tree_sitter.addCMacro("_DEFAULT_SOURCE", "");
tree_sitter.addCMacro("_BSD_SOURCE", "");
tree_sitter.addCMacro("_DARWIN_C_SOURCE", "");
lib.root_module.addCMacro("_POSIX_C_SOURCE", "200112L");
lib.root_module.addCMacro("_DEFAULT_SOURCE", "");
lib.root_module.addCMacro("_DARWIN_C_SOURCE", "");
if (wasm) {
if (b.lazyDependency(wasmtimeDep(target.result), .{})) |wasmtime| {
tree_sitter.addCMacro("TREE_SITTER_FEATURE_WASM", "");
tree_sitter.addSystemIncludePath(wasmtime.path("include"));
tree_sitter.addLibraryPath(wasmtime.path("lib"));
if (shared) tree_sitter.linkSystemLibrary("wasmtime", .{});
lib.root_module.addCMacro("TREE_SITTER_FEATURE_WASM", "");
lib.addSystemIncludePath(wasmtime.path("include"));
lib.addLibraryPath(wasmtime.path("lib"));
if (shared) lib.linkSystemLibrary("wasmtime");
}
}
@ -127,14 +121,14 @@ pub fn wasmtimeDep(target: std.Target) []const u8 {
);
}
fn findSourceFiles(b: *std.Build, io: std.Io) ![]const []const u8 {
fn findSourceFiles(b: *std.Build) ![]const []const u8 {
var sources: std.ArrayListUnmanaged([]const u8) = .empty;
var dir = try b.build_root.handle.openDir(io, "lib/src", .{ .iterate = true });
var dir = try b.build_root.handle.openDir("lib/src", .{ .iterate = true });
var iter = dir.iterate();
defer dir.close(io);
defer dir.close();
while (try iter.next(io)) |entry| {
while (try iter.next()) |entry| {
if (entry.kind != .file) continue;
const file = entry.name;
const ext = std.fs.path.extension(file);

View file

@ -1,8 +1,8 @@
.{
.name = .tree_sitter,
.fingerprint = 0x841224b447ac0d4f,
.version = "0.28.0",
.minimum_zig_version = "0.16.0",
.version = "0.26.3",
.minimum_zig_version = "0.14.1",
.paths = .{
"build.zig",
"build.zig.zon",
@ -13,83 +13,83 @@
},
.dependencies = .{
.wasmtime_c_api_aarch64_android = .{
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-aarch64-android-c-api.tar.xz",
.hash = "N-V-__8AAIp_mQVzQOITXcYcWxYLJkvB1W1SvLlrdiU2G7fj",
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v33.0.2/wasmtime-v33.0.2-aarch64-android-c-api.tar.xz",
.hash = "N-V-__8AAIfPIgdw2YnV3QyiFQ2NHdrxrXzzCdjYJyxJDOta",
.lazy = true,
},
.wasmtime_c_api_aarch64_linux = .{
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-aarch64-linux-c-api.tar.xz",
.hash = "N-V-__8AAMztsgU5Aj4oI3MRHXJVe5rW72op-kT_78I3kZVM",
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v33.0.2/wasmtime-v33.0.2-aarch64-linux-c-api.tar.xz",
.hash = "N-V-__8AAIt97QZi7Pf7nNJ2mVY6uxA80Klyuvvtop3pLMRK",
.lazy = true,
},
.wasmtime_c_api_aarch64_macos = .{
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-aarch64-macos-c-api.tar.xz",
.hash = "N-V-__8AANZxOwT27sdrKxDDGGKsiwtcZlHy204xAWNIgDBH",
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v33.0.2/wasmtime-v33.0.2-aarch64-macos-c-api.tar.xz",
.hash = "N-V-__8AAAO48QQf91w9RmmUDHTja8DrXZA1n6Bmc8waW3qe",
.lazy = true,
},
.wasmtime_c_api_aarch64_musl = .{
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-aarch64-musl-c-api.tar.xz",
.hash = "N-V-__8AAJL1zQW9yxC98uc60lSuVUHhH77QHTto0zwIQnBj",
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v33.0.2/wasmtime-v33.0.2-aarch64-musl-c-api.tar.xz",
.hash = "N-V-__8AAI196wa9pwADoA2RbCDp5F7bKQg1iOPq6gIh8-FH",
.lazy = true,
},
.wasmtime_c_api_aarch64_windows = .{
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-aarch64-windows-c-api.zip",
.hash = "N-V-__8AAHRCtQU93hJcRFOgVcof3IQpRV9stT2Pp54wpJc2",
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v33.0.2/wasmtime-v33.0.2-aarch64-windows-c-api.zip",
.hash = "N-V-__8AAC9u4wXfqd1Q6XyQaC8_DbQZClXux60Vu5743N05",
.lazy = true,
},
.wasmtime_c_api_armv7_linux = .{
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-armv7-linux-c-api.tar.xz",
.hash = "N-V-__8AAJaW6gT8QdULOU0jxX4a_DOCA5YD6cxWBC8IqhQF",
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v33.0.2/wasmtime-v33.0.2-armv7-linux-c-api.tar.xz",
.hash = "N-V-__8AAHXe8gWs3s83Cc5G6SIq0_jWxj8fGTT5xG4vb6-x",
.lazy = true,
},
.wasmtime_c_api_i686_linux = .{
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-i686-linux-c-api.tar.xz",
.hash = "N-V-__8AANguMgVX4XMhdOVkdj4yfFKXrG8RTgZDs3nQB8J8",
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v33.0.2/wasmtime-v33.0.2-i686-linux-c-api.tar.xz",
.hash = "N-V-__8AAN2pzgUUfulRCYnipSfis9IIYHoTHVlieLRmKuct",
.lazy = true,
},
.wasmtime_c_api_i686_windows = .{
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-i686-windows-c-api.zip",
.hash = "N-V-__8AANY9ggXg4rK2_1o3EIlrCq124l5RfykPv-DPforq",
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v33.0.2/wasmtime-v33.0.2-i686-windows-c-api.zip",
.hash = "N-V-__8AAJu0YAUUTFBLxFIOi-MSQVezA6MMkpoFtuaf2Quf",
.lazy = true,
},
.wasmtime_c_api_riscv64gc_linux = .{
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-riscv64gc-linux-c-api.tar.xz",
.hash = "N-V-__8AAPDtCAdQ0dD9Rs-qWl-kPr2c7L3PVsUVjwy12Iz1",
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v33.0.2/wasmtime-v33.0.2-riscv64gc-linux-c-api.tar.xz",
.hash = "N-V-__8AAG8m-gc3E3AIImtTZ3l1c7HC6HUWazQ9OH5KACX4",
.lazy = true,
},
.wasmtime_c_api_s390x_linux = .{
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-s390x-linux-c-api.tar.xz",
.hash = "N-V-__8AANA3BwY1ZOoGCWCR_tTY9G1vfIX128RGVxufN3ov",
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v33.0.2/wasmtime-v33.0.2-s390x-linux-c-api.tar.xz",
.hash = "N-V-__8AAH314gd-gE4IBp2uvAL3gHeuW1uUZjMiLLeUdXL_",
.lazy = true,
},
.wasmtime_c_api_x86_64_android = .{
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-x86_64-android-c-api.tar.xz",
.hash = "N-V-__8AAF4AIgY0ltjevj1ybGfvMU1ErPRnNve5X1TmvCru",
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v33.0.2/wasmtime-v33.0.2-x86_64-android-c-api.tar.xz",
.hash = "N-V-__8AAIPNRwfNkznebrcGb0IKUe7f35bkuZEYOjcx6q3f",
.lazy = true,
},
.wasmtime_c_api_x86_64_linux = .{
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-x86_64-linux-c-api.tar.xz",
.hash = "N-V-__8AAIR0cAbjf3DkrTbu81Oq_zociz-0lCpb5DR0lIC9",
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v33.0.2/wasmtime-v33.0.2-x86_64-linux-c-api.tar.xz",
.hash = "N-V-__8AAI8EDwcyTtk_Afhk47SEaqfpoRqGkJeZpGs69ChF",
.lazy = true,
},
.wasmtime_c_api_x86_64_macos = .{
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-x86_64-macos-c-api.tar.xz",
.hash = "N-V-__8AAFJ4lgRgCBnYdz8-Yfc4hLve45Hv-0RICAEuCp4s",
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v33.0.2/wasmtime-v33.0.2-x86_64-macos-c-api.tar.xz",
.hash = "N-V-__8AAGtGNgVaOpHSxC22IjrampbRIy6lLwscdcAE8nG1",
.lazy = true,
},
.wasmtime_c_api_x86_64_mingw = .{
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-x86_64-mingw-c-api.zip",
.hash = "N-V-__8AAMxZxQZUpp1cU8J5zgLiMNq4e4dy0hcchlQFy03J",
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v33.0.2/wasmtime-v33.0.2-x86_64-mingw-c-api.zip",
.hash = "N-V-__8AAPS2PAbVix50L6lnddlgazCPTz3whLUFk1qnRtnZ",
.lazy = true,
},
.wasmtime_c_api_x86_64_musl = .{
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-x86_64-musl-c-api.tar.xz",
.hash = "N-V-__8AAN5pWgZrZBt8VYWkN82WjyFe3DkGcE7uLn3jpt38",
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v33.0.2/wasmtime-v33.0.2-x86_64-musl-c-api.tar.xz",
.hash = "N-V-__8AAF-WEQe0nzvi09PgusM5i46FIuCKJmIDWUleWgQ3",
.lazy = true,
},
.wasmtime_c_api_x86_64_windows = .{
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-x86_64-windows-c-api.zip",
.hash = "N-V-__8AAEIJkgaVHFETgakbognNUpFELuV17vpjw6NjsWhQ",
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v33.0.2/wasmtime-v33.0.2-x86_64-windows-c-api.zip",
.hash = "N-V-__8AAKGNXwbpJQsn0_6kwSIVDDWifSg8cBzf7T2RzsC9",
.lazy = true,
},
},

View file

@ -5,13 +5,14 @@ description = "CLI tool for developing, testing, and using Tree-sitter parsers"
authors.workspace = true
edition.workspace = true
rust-version.workspace = true
readme = "README.md"
homepage.workspace = true
repository.workspace = true
documentation = "https://docs.rs/tree-sitter-cli"
license.workspace = true
keywords.workspace = true
categories.workspace = true
include = [ "build.rs", "README.md", "LICENSE", "benches/*", "src/**" ]
include = ["build.rs", "README.md", "LICENSE", "benches/*", "src/**"]
[lints]
workspace = true
@ -20,18 +21,18 @@ workspace = true
path = "src/tree_sitter_cli.rs"
[[bin]]
doc = false
name = "tree-sitter"
path = "src/main.rs"
doc = false
[[bench]]
harness = false
name = "benchmark"
harness = false
[features]
default = [ "qjs-rt" ]
qjs-rt = [ "tree-sitter-generate/qjs-rt" ]
wasm = [ "tree-sitter/wasm", "tree-sitter-loader/wasm" ]
default = ["qjs-rt"]
wasm = ["tree-sitter/wasm", "tree-sitter-loader/wasm"]
qjs-rt = ["tree-sitter-generate/qjs-rt"]
[dependencies]
ansi_colours.workspace = true
@ -66,50 +67,17 @@ wasmparser.workspace = true
webbrowser.workspace = true
tree-sitter.workspace = true
tree-sitter-generate.workspace = true
tree-sitter-config.workspace = true
tree-sitter-generate = { features = [ "load" ], workspace = true }
tree-sitter-highlight.workspace = true
tree-sitter-loader.workspace = true
tree-sitter-tags.workspace = true
[dev-dependencies]
encoding_rs = "0.8.35"
tree_sitter_proc_macro = { package = "tree-sitter-tests-proc-macro", path = "src/tests/proc_macro" }
widestring = "1.2.1"
tree_sitter_proc_macro = { path = "src/tests/proc_macro", package = "tree-sitter-tests-proc-macro" }
pretty_assertions.workspace = true
tempfile.workspace = true
pretty_assertions.workspace = true
unindent.workspace = true
[package.metadata.binstall]
pkg-fmt = "zip"
[package.metadata.binstall.overrides.aarch64-unknown-linux-gnu]
pkg-url = "{ repo }/releases/download/v{ version }/{ name }-linux-arm64{ archive-suffix }"
[package.metadata.binstall.overrides.armv7-unknown-linux-gnueabihf]
pkg-url = "{ repo }/releases/download/v{ version }/{ name }-linux-arm{ archive-suffix }"
[package.metadata.binstall.overrides.x86_64-unknown-linux-gnu]
pkg-url = "{ repo }/releases/download/v{ version }/{ name }-linux-x64{ archive-suffix }"
[package.metadata.binstall.overrides.i686-unknown-linux-gnu]
pkg-url = "{ repo }/releases/download/v{ version }/{ name }-linux-x86{ archive-suffix }"
[package.metadata.binstall.overrides.powerpc64-unknown-linux-gnu]
pkg-url = "{ repo }/releases/download/v{ version }/{ name }-linux-powerpc64{ archive-suffix }"
[package.metadata.binstall.overrides.aarch64-pc-windows-msvc]
pkg-url = "{ repo }/releases/download/v{ version }/{ name }-windows-arm64{ archive-suffix }"
[package.metadata.binstall.overrides.x86_64-pc-windows-msvc]
pkg-url = "{ repo }/releases/download/v{ version }/{ name }-windows-x64{ archive-suffix }"
[package.metadata.binstall.overrides.i686-pc-windows-msvc]
pkg-url = "{ repo }/releases/download/v{ version }/{ name }-windows-x86{ archive-suffix }"
[package.metadata.binstall.overrides.aarch64-apple-darwin]
pkg-url = "{ repo }/releases/download/v{ version }/{ name }-macos-arm64{ archive-suffix }"
[package.metadata.binstall.overrides.x86_64-apple-darwin]
pkg-url = "{ repo }/releases/download/v{ version }/{ name }-macos-x64{ archive-suffix }"

View file

@ -7,22 +7,22 @@
[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
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`.
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`.
### Installation
You can install the `tree-sitter-cli` with [`cargo-binstall`](https://github.com/cargo-bins/cargo-binstall):
You can install the `tree-sitter-cli` with `cargo`:
```sh
cargo binstall tree-sitter-cli
```
or you can build it from source:
```sh
cargo install --locked tree-sitter-cli
```
or with `npm`:
```sh
npm install tree-sitter-cli
```
You can also download a pre-built binary for your platform from [the releases page].
### Dependencies
@ -34,11 +34,9 @@ The `tree-sitter` binary itself has no dependencies, but specific commands have
### Commands
* `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.
* `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.
* `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.
* `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.
* `parse` - The `tree-sitter parse` command will parse a file (or list of files) using Tree-sitter parsers.

View file

@ -2,6 +2,7 @@ use std::{
collections::BTreeMap,
env, fs,
path::{Path, PathBuf},
str,
sync::LazyLock,
time::Instant,
};
@ -9,8 +10,6 @@ use std::{
use anyhow::Context;
use log::info;
use tree_sitter::{Language, Parser, Query};
#[cfg(feature = "wasm")]
use tree_sitter::{WasmStore, wasmtime};
use tree_sitter_loader::{CompileConfig, Loader};
include!("../src/tests/helpers/dirs.rs");
@ -20,18 +19,14 @@ static LANGUAGE_FILTER: LazyLock<Option<String>> =
static EXAMPLE_FILTER: LazyLock<Option<String>> =
LazyLock::new(|| env::var("TREE_SITTER_BENCHMARK_EXAMPLE_FILTER").ok());
static REPETITION_COUNT: LazyLock<usize> = LazyLock::new(|| {
env::var("TREE_SITTER_BENCHMARK_REPETITION_COUNT").map_or(5, |s| s.parse::<usize>().unwrap())
env::var("TREE_SITTER_BENCHMARK_REPETITION_COUNT")
.map(|s| s.parse::<usize>().unwrap())
.unwrap_or(5)
});
static WASM: LazyLock<bool> = LazyLock::new(|| env::var_os("TREE_SITTER_BENCHMARK_WASM").is_some());
static TEST_LOADER: LazyLock<Loader> =
LazyLock::new(|| Loader::with_parser_lib_path(SCRATCH_DIR.clone()));
#[cfg(feature = "wasm")]
static WASM_ENGINE: LazyLock<wasmtime::Engine> = LazyLock::new(Default::default);
#[expect(
clippy::type_complexity,
reason = "complex map type reflects benchmark data structure"
)]
#[allow(clippy::type_complexity)]
static EXAMPLE_AND_QUERY_PATHS_BY_LANGUAGE_DIR: LazyLock<
BTreeMap<PathBuf, (Vec<PathBuf>, Vec<PathBuf>)>,
> = LazyLock::new(|| {
@ -43,14 +38,22 @@ static EXAMPLE_AND_QUERY_PATHS_BY_LANGUAGE_DIR: LazyLock<
if let Ok(example_files) = fs::read_dir(dir.join("examples")) {
example_paths.extend(example_files.filter_map(|p| {
let p = p.unwrap().path();
if p.is_file() { Some(p) } else { None }
if p.is_file() {
Some(p)
} else {
None
}
}));
}
if let Ok(query_files) = fs::read_dir(dir.join("queries")) {
query_paths.extend(query_files.filter_map(|p| {
let p = p.unwrap().path();
if p.is_file() { Some(p) } else { None }
if p.is_file() {
Some(p)
} else {
None
}
}));
}
} else {
@ -92,26 +95,22 @@ fn main() {
{
let language_name = language_path.file_name().unwrap().to_str().unwrap();
if let Some(filter) = LANGUAGE_FILTER.as_ref()
&& language_name != filter.as_str()
{
continue;
if let Some(filter) = LANGUAGE_FILTER.as_ref() {
if language_name != filter.as_str() {
continue;
}
}
info!("\nLanguage: {language_name}");
let language = if *WASM {
get_wasm_language(language_name, &mut parser)
} else {
get_language(language_path)
};
let language = get_language(language_path);
parser.set_language(&language).unwrap();
info!(" Constructing Queries");
for path in query_paths {
if let Some(filter) = EXAMPLE_FILTER.as_ref()
&& !path.to_str().unwrap().contains(filter.as_str())
{
continue;
if let Some(filter) = EXAMPLE_FILTER.as_ref() {
if !path.to_str().unwrap().contains(filter.as_str()) {
continue;
}
}
parse(path, max_path_length, |source| {
@ -124,10 +123,10 @@ fn main() {
info!(" Parsing Valid Code:");
let mut normal_speeds = Vec::new();
for example_path in example_paths {
if let Some(filter) = EXAMPLE_FILTER.as_ref()
&& !example_path.to_str().unwrap().contains(filter.as_str())
{
continue;
if let Some(filter) = EXAMPLE_FILTER.as_ref() {
if !example_path.to_str().unwrap().contains(filter.as_str()) {
continue;
}
}
normal_speeds.push(parse(example_path, max_path_length, |code| {
@ -142,10 +141,10 @@ fn main() {
{
if other_language_path != language_path {
for example_path in example_paths {
if let Some(filter) = EXAMPLE_FILTER.as_ref()
&& !example_path.to_str().unwrap().contains(filter.as_str())
{
continue;
if let Some(filter) = EXAMPLE_FILTER.as_ref() {
if !example_path.to_str().unwrap().contains(filter.as_str()) {
continue;
}
}
error_speeds.push(parse(example_path, max_path_length, |code| {
@ -223,32 +222,3 @@ fn get_language(path: &Path) -> Language {
.with_context(|| format!("Failed to load language at path {}", src_path.display()))
.unwrap()
}
#[cfg(feature = "wasm")]
fn get_wasm_language(language_name: &str, parser: &mut Parser) -> Language {
let wasm_language_name = language_name.replace('-', "_");
let wasm_path = ROOT_DIR
.join("target")
.join("release")
.join(format!("tree-sitter-{language_name}.wasm"));
let wasm = fs::read(&wasm_path)
.with_context(|| {
format!(
"Failed to read {}. Generate Wasm fixtures with `cargo xtask generate-fixtures --wasm`",
wasm_path.display()
)
})
.unwrap();
let mut store = WasmStore::new(&WASM_ENGINE).expect("Failed to create Wasm store");
let language = store
.load_language(&wasm_language_name, &wasm)
.with_context(|| format!("Failed to load Wasm language at {}", wasm_path.display()))
.unwrap();
parser.set_wasm_store(store).unwrap();
language
}
#[cfg(not(feature = "wasm"))]
fn get_wasm_language(_language_name: &str, _parser: &mut Parser) -> Language {
panic!("Wasm benchmarking requires the `wasm` feature");
}

File diff suppressed because it is too large Load diff

View file

@ -17,7 +17,7 @@
"tree-sitter"
],
"dependencies": {
"eslint-plugin-jsdoc": "^62.7.0"
"eslint-plugin-jsdoc": "^50.2.4"
},
"peerDependencies": {
"eslint": ">= 9"

View file

@ -3,22 +3,18 @@ type BlankRule = { type: 'BLANK' };
type ChoiceRule = { type: 'CHOICE'; members: Rule[] };
type FieldRule = { type: 'FIELD'; name: string; content: Rule };
type ImmediateTokenRule = { type: 'IMMEDIATE_TOKEN'; content: Rule };
type PatternRule = { type: 'PATTERN'; value: string; flags?: string };
type PrecedenceValue = string | number;
type PatternRule = { type: 'PATTERN'; value: string };
type PrecDynamicRule = { type: 'PREC_DYNAMIC'; content: Rule; value: number };
type PrecLeftRule = { type: 'PREC_LEFT'; content: Rule; value: PrecedenceValue };
type PrecRightRule = { type: 'PREC_RIGHT'; content: Rule; value: PrecedenceValue };
type PrecRule = { type: 'PREC'; content: Rule; value: PrecedenceValue };
type PrecLeftRule = { type: 'PREC_LEFT'; content: Rule; value: number };
type PrecRightRule = { type: 'PREC_RIGHT'; content: Rule; value: number };
type PrecRule = { type: 'PREC'; content: Rule; value: number };
type Repeat1Rule = { type: 'REPEAT1'; content: Rule };
type RepeatRule = { type: 'REPEAT'; content: Rule };
type ReservedRule = { type: 'RESERVED'; content: Rule; context_name: string };
type SeqRule = { type: 'SEQ'; members: Rule[] };
type StringRule = { type: 'STRING'; value: string };
type SymbolRule<Name extends string> = { type: 'SYMBOL'; name: Name };
type PrecedenceEntry = StringRule | SymbolRule<string>;
type TokenRule = { type: 'TOKEN'; content: Rule };
type EOFRule = { type: 'EOF' };
type Rule =
| AliasRule
@ -37,8 +33,7 @@ type Rule =
| SeqRule
| StringRule
| SymbolRule<string>
| TokenRule
| EOFRule;
| TokenRule;
declare class RustRegex {
value: string;
@ -91,8 +86,8 @@ interface Grammar<
*/
precedences?: (
$: GrammarSymbols<RuleName | BaseGrammarRuleName>,
previous: PrecedenceEntry[][],
) => (string | PrecedenceEntry)[][],
previous: Rule[][],
) => RuleOrLiteral[][],
/**
* An array of arrays of rule names. Each inner array represents a set of
@ -106,8 +101,8 @@ interface Grammar<
*/
conflicts?: (
$: GrammarSymbols<RuleName | BaseGrammarRuleName>,
previous: SymbolRule<string>[][],
) => SymbolRule<string>[][];
previous: Rule[][],
) => RuleOrLiteral[][];
/**
* An array of token names which can be returned by an _external scanner_.
@ -132,11 +127,9 @@ interface Grammar<
* specify extras: `$ => []` in your grammar.
*
* @param $ grammar rules
* @param previous array of extras from the base grammar
*/
extras?: (
$: GrammarSymbols<RuleName | BaseGrammarRuleName>,
previous: Rule[],
) => RuleOrLiteral[];
/**
@ -149,8 +142,8 @@ interface Grammar<
*/
inline?: (
$: GrammarSymbols<RuleName | BaseGrammarRuleName>,
previous: SymbolRule<string>[],
) => SymbolRule<string>[];
previous: Rule[],
) => RuleOrLiteral[];
/**
* A list of hidden rule names that should be considered supertypes in the
@ -162,8 +155,8 @@ interface Grammar<
*/
supertypes?: (
$: GrammarSymbols<RuleName | BaseGrammarRuleName>,
previous: SymbolRule<string>[],
) => SymbolRule<string>[];
previous: Rule[],
) => RuleOrLiteral[];
/**
* The name of a token that will match keywords for the purpose of the
@ -173,47 +166,24 @@ interface Grammar<
*
* @see https://tree-sitter.github.io/tree-sitter/creating-parsers/3-writing-the-grammar#keyword-extraction
*/
word?: (
$: GrammarSymbols<RuleName | BaseGrammarRuleName>,
) => SymbolRule<string>;
word?: ($: GrammarSymbols<RuleName | BaseGrammarRuleName>) => RuleOrLiteral;
/**
* Mapping of names to reserved word sets. The first reserved word set is the
* global word set, meaning it applies to every rule in every parse state.
* The other word sets can be used with the `reserved` function. Each callback
* receives the base grammar's reserved word set of the same name as its second
* argument, or `undefined` if no matching set exists.
* The other word sets can be used with the `reserved` function.
*/
reserved?: Record<
string,
(
$: GrammarSymbols<RuleName | BaseGrammarRuleName>,
previous: Rule[] | undefined,
) => RuleOrLiteral[]
($: GrammarSymbols<RuleName | BaseGrammarRuleName>) => RuleOrLiteral[]
>;
}
/**
* Return type of grammar(). The runtime evaluates and normalizes the grammar
* beneath a "grammar" key. Optional input fields become required output fields
* with default values when not provided.
*/
type GrammarSchema<RuleName extends string> = {
grammar: {
name: string;
/** Base grammar name when extending; undefined for root grammars. */
inherits: string | undefined;
rules: Record<RuleName, Rule>;
precedences: PrecedenceEntry[][];
conflicts: string[][];
externals: Rule[];
extras: Rule[];
inline: string[];
supertypes: string[];
word: string | undefined;
reserved: Record<string, Rule[]>;
};
[K in keyof Grammar<RuleName>]: K extends 'rules'
? Record<RuleName, Rule>
: Grammar<RuleName>[K];
};
/**
@ -341,7 +311,7 @@ declare const prec: {
*
* @see https://www.gnu.org/software/bison/manual/html_node/Generalized-LR-Parsing.html
*/
dynamic(value: number, rule: RuleOrLiteral): PrecDynamicRule;
dynamic(value: string | number, rule: RuleOrLiteral): PrecDynamicRule;
};
/**
@ -412,19 +382,6 @@ declare const token: {
immediate(rule: RuleOrLiteral): ImmediateTokenRule;
};
/**
* Matches the end of input. May only appear as the final symbol of a
* (possibly nested) sequence; a production ending in `eof()` reduces only
* when the lookahead is end-of-input, rather than shifting a token.
*
* Choice branches that continue past `eof()` are dropped as unreachable,
* and `eof()` is not allowed inside `token()`.
*
* Useful when a rule should match either an explicit terminator (e.g. a
* newline) or the end of the file.
*/
declare function eof(): EOFRule;
/**
* Creates a new language grammar with the provided schema.
*

View file

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

View file

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

View file

@ -13,7 +13,7 @@
installShellFiles,
}:
let
canRunHost = stdenv.buildPlatform.canExecute stdenv.hostPlatform;
isCross = stdenv.targetPlatform == stdenv.buildPlatform;
in
rustPlatform.buildRustPackage {
pname = "tree-sitter-cli";
@ -28,7 +28,7 @@ rustPlatform.buildRustPackage {
pkg-config
nodejs_22
]
++ lib.optionals canRunHost [ installShellFiles ];
++ lib.optionals (!isCross) [ installShellFiles ];
cargoLock.lockFile = ../../Cargo.lock;
@ -42,9 +42,9 @@ rustPlatform.buildRustPackage {
'';
preCheck = "export HOME=$TMPDIR";
doCheck = canRunHost;
doCheck = !isCross;
postInstall = lib.optionalString canRunHost ''
postInstall = lib.optionalString (!isCross) ''
installShellCompletion --cmd tree-sitter \
--bash <($out/bin/tree-sitter complete --shell bash) \
--zsh <($out/bin/tree-sitter complete --shell zsh) \

View file

@ -6,7 +6,7 @@ use std::{
};
use log::{error, info};
use rand::RngExt;
use rand::Rng;
use regex::Regex;
use tree_sitter::{Language, Parser};
@ -25,7 +25,7 @@ use crate::{
random::Rand,
},
parse::perform_edit,
test::{DiffKey, TestDiff, TestEntry, TestExpectation, parse_tests, render_test_output},
test::{parse_tests, strip_sexp_fields, DiffKey, TestDiff, TestEntry},
};
pub static LOG_ENABLED: LazyLock<bool> = LazyLock::new(|| env::var("TREE_SITTER_LOG").is_ok());
@ -44,13 +44,11 @@ pub static EXAMPLE_EXCLUDE: LazyLock<Option<Regex>> =
pub static START_SEED: LazyLock<usize> = LazyLock::new(new_seed);
pub const DEFAULT_EDIT_COUNT: usize = 3;
pub static EDIT_COUNT: LazyLock<usize> =
LazyLock::new(|| int_env_var("TREE_SITTER_EDITS").unwrap_or(DEFAULT_EDIT_COUNT));
LazyLock::new(|| int_env_var("TREE_SITTER_EDITS").unwrap_or(3));
pub const DEFAULT_ITERATION_COUNT: usize = 10;
pub static ITERATION_COUNT: LazyLock<usize> =
LazyLock::new(|| int_env_var("TREE_SITTER_ITERATIONS").unwrap_or(DEFAULT_ITERATION_COUNT));
LazyLock::new(|| int_env_var("TREE_SITTER_ITERATIONS").unwrap_or(10));
fn int_env_var(name: &'static str) -> Option<usize> {
env::var(name).ok().and_then(|e| e.parse().ok())
@ -63,9 +61,9 @@ fn regex_env_var(name: &'static str) -> Option<Regex> {
#[must_use]
pub fn new_seed() -> usize {
int_env_var("TREE_SITTER_SEED").unwrap_or_else(|| {
let mut rng = rand::rng();
let seed = rng.random_range(0..=usize::MAX);
eprintln!("fuzz seed: {seed}");
let mut rng = rand::thread_rng();
let seed = rng.gen::<usize>();
info!("Seed: {seed}");
seed
})
}
@ -97,7 +95,9 @@ pub fn fuzz_language_corpus(
.iter()
.any(|lang| lang.as_ref() == language_name)
}
TestEntry::Group { children, .. } => {
TestEntry::Group {
ref mut children, ..
} => {
children.retain_mut(|child| retain(child, language_name));
!children.is_empty()
}
@ -109,16 +109,12 @@ pub fn fuzz_language_corpus(
let corpus_dir = grammar_dir.join(subdir).join("test").join("corpus");
if !corpus_dir.exists() || !corpus_dir.is_dir() {
error!(
"No corpus directory found, ensure that you have a `test/corpus` directory in your grammar directory with at least one test file."
);
error!("No corpus directory found, ensure that you have a `test/corpus` directory in your grammar directory with at least one test file.");
return;
}
if std::fs::read_dir(&corpus_dir).unwrap().count() == 0 {
error!(
"No corpus files found in `test/corpus`, ensure that you have at least one test file in your corpus directory."
);
error!("No corpus files found in `test/corpus`, ensure that you have at least one test file in your corpus directory.");
return;
}
@ -144,7 +140,7 @@ pub fn fuzz_language_corpus(
.take()
.unwrap_or_default()
.into_iter()
.chain(tests.iter().filter(|t| t.skip()).map(get_test_name))
.chain(tests.iter().filter(|x| x.skip).map(get_test_name))
.map(|x| (x, 0))
.collect::<HashMap<String, usize>>();
@ -169,8 +165,31 @@ pub fn fuzz_language_corpus(
println!(" {test_index}. {test_name}");
let passed = allocations::record_checked(|| {
let check_output = !test.error();
test.check_initial_parse(language, &test_name, check_output)
let mut log_session = None;
let mut parser = get_parser(&mut log_session, "log.html");
parser.set_language(language).unwrap();
set_included_ranges(&mut parser, &test.input, test.template_delimiters);
let tree = parser.parse(&test.input, None).unwrap();
if test.error {
return true;
}
let mut actual_output = tree.root_node().to_sexp();
if !test.has_fields {
actual_output = strip_sexp_fields(&actual_output);
}
if actual_output != test.output {
println!("Incorrect initial parse for {test_name}");
DiffKey::print();
println!("{}", TestDiff::new(&actual_output, &test.output));
println!();
return false;
}
true
})
.unwrap_or_else(|e| {
error!("{e}");
@ -202,7 +221,7 @@ pub fn fuzz_language_corpus(
}
// Perform a random series of edits and reparse.
let edit_count = rand.unsigned(options.edits);
let edit_count = rand.unsigned(*EDIT_COUNT);
let mut undo_stack = Vec::with_capacity(edit_count);
for _ in 0..=edit_count {
let edit = get_random_edit(&mut rand, &input);
@ -234,7 +253,7 @@ pub fn fuzz_language_corpus(
// Check that the new tree is consistent.
check_consistent_sizes(&tree2, &input);
if let Err(message) = check_changed_ranges(&tree, &tree2, &input) {
error!("\nUnexpected scope change in seed {seed} with start seed {start_seed}\n{message}\n\n");
error!("\nUnexpected scope change in seed {seed} with start seed {start_seed}\n{message}\n\n",);
return false;
}
@ -250,9 +269,12 @@ pub fn fuzz_language_corpus(
let tree3 = parser.parse(&input, Some(&tree2)).unwrap();
// Verify that the final tree matches the expectation from the corpus.
let actual_output = render_test_output(&input, &tree3, test.cst, test.has_fields).unwrap();
let mut actual_output = tree3.root_node().to_sexp();
if !test.has_fields {
actual_output = strip_sexp_fields(&actual_output);
}
if actual_output != test.output && !test.error() {
if actual_output != test.output && !test.error {
println!("Incorrect parse for {test_name} - seed {seed}");
DiffKey::print();
println!("{}", TestDiff::new(&actual_output, &test.output));
@ -300,56 +322,12 @@ pub struct FlattenedTest {
pub input: Vec<u8>,
pub output: String,
pub languages: Vec<Box<str>>,
pub expectation: TestExpectation,
pub error: bool,
pub skip: bool,
pub has_fields: bool,
pub cst: bool,
pub template_delimiters: Option<(&'static str, &'static str)>,
}
impl FlattenedTest {
#[must_use]
fn skip(&self) -> bool {
self.expectation == TestExpectation::Skip
}
#[must_use]
fn error(&self) -> bool {
self.expectation == TestExpectation::Error
}
#[must_use]
pub(crate) fn check_initial_parse(
&self,
language: &Language,
display_name: &str,
check_output: bool,
) -> bool {
let mut log_session = None;
let mut parser = get_parser(&mut log_session, "log.html");
parser.set_language(language).unwrap();
set_included_ranges(&mut parser, &self.input, self.template_delimiters);
let tree = parser.parse(&self.input, None).unwrap();
if !check_output {
return true;
}
let actual_output =
render_test_output(&self.input, &tree, self.cst, self.has_fields).unwrap();
if actual_output == self.output {
true
} else {
println!("Incorrect initial parse for {display_name}");
DiffKey::print();
println!("{}", TestDiff::new(&actual_output, &self.output));
println!();
false
}
}
}
#[must_use]
pub fn flatten_tests(
test: TestEntry,
@ -382,20 +360,20 @@ pub fn flatten_tests(
if !include.is_match(&name) {
return;
}
} else if let Some(exclude) = exclude
&& exclude.is_match(&name)
{
return;
} else if let Some(exclude) = exclude {
if exclude.is_match(&name) {
return;
}
}
result.push(FlattenedTest {
name,
input,
output,
languages: attributes.languages,
expectation: attributes.expectation,
has_fields,
cst: attributes.cst,
languages: attributes.languages,
error: attributes.error,
skip: attributes.skip,
template_delimiters: None,
});
}

View file

@ -2,21 +2,19 @@ use std::{
collections::HashMap,
os::raw::c_void,
sync::{
Mutex,
atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst},
Mutex,
},
};
#[ctor::ctor]
unsafe fn initialize_allocation_recording() {
unsafe {
tree_sitter::set_allocator(Some(tree_sitter::Allocator {
malloc: ts_record_malloc,
calloc: ts_record_calloc,
realloc: ts_record_realloc,
free: ts_record_free,
}));
}
tree_sitter::set_allocator(
Some(ts_record_malloc),
Some(ts_record_calloc),
Some(ts_record_realloc),
Some(ts_record_free),
);
}
#[derive(Debug, PartialEq, Eq, Hash)]
@ -35,7 +33,7 @@ thread_local! {
static RECORDER: AllocationRecorder = AllocationRecorder::default();
}
unsafe extern "C" {
extern "C" {
fn malloc(size: usize) -> *mut c_void;
fn calloc(count: usize, size: usize) -> *mut c_void;
fn realloc(ptr: *mut c_void, size: usize) -> *mut c_void;
@ -105,11 +103,9 @@ fn record_dealloc(ptr: *mut c_void) {
/// freed by calling `ts_record_free`.
#[must_use]
pub unsafe extern "C" fn ts_record_malloc(size: usize) -> *mut c_void {
unsafe {
let result = malloc(size);
record_alloc(result);
result
}
let result = malloc(size);
record_alloc(result);
result
}
/// # Safety
@ -118,11 +114,9 @@ pub unsafe extern "C" fn ts_record_malloc(size: usize) -> *mut c_void {
/// freed by calling `ts_record_free`.
#[must_use]
pub unsafe extern "C" fn ts_record_calloc(count: usize, size: usize) -> *mut c_void {
unsafe {
let result = calloc(count, size);
record_alloc(result);
result
}
let result = calloc(count, size);
record_alloc(result);
result
}
/// # Safety
@ -131,16 +125,14 @@ pub unsafe extern "C" fn ts_record_calloc(count: usize, size: usize) -> *mut c_v
/// freed by calling `ts_record_free`.
#[must_use]
pub unsafe extern "C" fn ts_record_realloc(ptr: *mut c_void, size: usize) -> *mut c_void {
unsafe {
let result = realloc(ptr, size);
if ptr.is_null() {
record_alloc(result);
} else if !core::ptr::eq(ptr, result) {
record_dealloc(ptr);
record_alloc(result);
}
result
let result = realloc(ptr, size);
if ptr.is_null() {
record_alloc(result);
} else if !core::ptr::eq(ptr, result) {
record_dealloc(ptr);
record_alloc(result);
}
result
}
/// # Safety
@ -148,8 +140,6 @@ pub unsafe extern "C" fn ts_record_realloc(ptr: *mut c_void, size: usize) -> *mu
/// The caller must ensure that `ptr` was allocated by a previous call
/// to `ts_record_malloc`, `ts_record_calloc`, or `ts_record_realloc`.
pub unsafe extern "C" fn ts_record_free(ptr: *mut c_void) {
unsafe {
record_dealloc(ptr);
free(ptr);
}
record_dealloc(ptr);
free(ptr);
}

View file

@ -1,22 +1,10 @@
use tree_sitter::{LogType, Node, Parser, Point, Range, Tree};
use super::{LOG_ENABLED, LOG_GRAPH_ENABLED, scope_sequence::ScopeSequence};
use super::{scope_sequence::ScopeSequence, LOG_ENABLED, LOG_GRAPH_ENABLED};
use crate::util;
struct SizeCheckFrame<'a> {
node: Node<'a>,
end_byte: usize,
end_point: Point,
child_count: u32,
child_index: u32,
last_child_end_byte: usize,
last_child_end_point: Point,
some_child_has_changes: bool,
actual_named_child_count: usize,
}
impl SizeCheckFrame<'_> {
fn new<'a>(node: Node<'a>, line_offsets: &[usize]) -> SizeCheckFrame<'a> {
pub fn check_consistent_sizes(tree: &Tree, input: &[u8]) {
fn check(node: Node, line_offsets: &[usize]) {
let start_byte = node.start_byte();
let end_byte = node.end_byte();
let start_point = node.start_position();
@ -30,21 +18,37 @@ impl SizeCheckFrame<'_> {
);
assert_eq!(end_byte, line_offsets[end_point.row] + end_point.column);
SizeCheckFrame {
node,
end_byte,
end_point,
child_count: node.child_count(),
child_index: 0,
last_child_end_byte: start_byte,
last_child_end_point: start_point,
some_child_has_changes: false,
actual_named_child_count: 0,
let mut last_child_end_byte = start_byte;
let mut last_child_end_point = start_point;
let mut some_child_has_changes = false;
let mut actual_named_child_count = 0;
for i in 0..node.child_count() {
let child = node.child(i as u32).unwrap();
assert!(child.start_byte() >= last_child_end_byte);
assert!(child.start_position() >= last_child_end_point);
check(child, line_offsets);
if child.has_changes() {
some_child_has_changes = true;
}
if child.is_named() {
actual_named_child_count += 1;
}
last_child_end_byte = child.end_byte();
last_child_end_point = child.end_position();
}
assert_eq!(actual_named_child_count, node.named_child_count());
if node.child_count() > 0 {
assert!(end_byte >= last_child_end_byte);
assert!(end_point >= last_child_end_point);
}
if some_child_has_changes {
assert!(node.has_changes());
}
}
}
pub fn check_consistent_sizes(tree: &Tree, input: &[u8]) {
let mut line_offsets = vec![0];
for (i, c) in input.iter().enumerate() {
if *c == b'\n' {
@ -52,41 +56,7 @@ pub fn check_consistent_sizes(tree: &Tree, input: &[u8]) {
}
}
let mut stack: Vec<SizeCheckFrame> = vec![SizeCheckFrame::new(tree.root_node(), &line_offsets)];
while let Some(top) = stack.last_mut() {
if top.child_index < top.child_count {
let i = top.child_index;
let child = top.node.child(i).unwrap();
assert!(child.start_byte() >= top.last_child_end_byte);
assert!(child.start_position() >= top.last_child_end_point);
if child.has_changes() {
top.some_child_has_changes = true;
}
if child.is_named() {
top.actual_named_child_count += 1;
}
top.last_child_end_byte = child.end_byte();
top.last_child_end_point = child.end_position();
top.child_index += 1;
stack.push(SizeCheckFrame::new(child, &line_offsets));
continue;
}
let frame = stack.pop().unwrap();
assert_eq!(
frame.actual_named_child_count,
frame.node.named_child_count()
);
if frame.child_count > 0 {
assert!(frame.end_byte >= frame.last_child_end_byte);
assert!(frame.end_point >= frame.last_child_end_point);
}
if frame.some_child_has_changes {
assert!(frame.node.has_changes());
}
}
check(tree.root_node(), &line_offsets);
}
pub fn check_changed_ranges(old_tree: &Tree, new_tree: &Tree, input: &[u8]) -> Result<(), String> {

View file

@ -1,4 +1,7 @@
use rand::{RngExt, SeedableRng, distr::Alphanumeric, rngs::StdRng};
use rand::{
distributions::Alphanumeric,
prelude::{Rng, SeedableRng, StdRng},
};
const OPERATORS: &[char] = &[
'+', '-', '<', '>', '(', ')', '*', '/', '&', '|', '!', ',', '.', '%',
@ -13,7 +16,7 @@ impl Rand {
}
pub fn unsigned(&mut self, max: usize) -> usize {
self.0.random_range(0..=max)
self.0.gen_range(0..=max)
}
pub fn words(&mut self, max_count: usize) -> Vec<u8> {

View file

@ -1,13 +1,13 @@
use tree_sitter::{Point, Range, Tree};
#[derive(Debug)]
pub struct ScopeSequence<'a>(Vec<ScopeStack<'a>>);
pub struct ScopeSequence(Vec<ScopeStack>);
type ScopeStack<'a> = Vec<&'a str>;
type ScopeStack = Vec<&'static str>;
impl<'a> ScopeSequence<'a> {
impl ScopeSequence {
#[must_use]
pub fn new(tree: &'a Tree) -> Self {
pub fn new(tree: &Tree) -> Self {
let mut result = Self(Vec::new());
let mut scope_stack = Vec::new();
@ -49,7 +49,7 @@ impl<'a> ScopeSequence<'a> {
for i in 0..(self.0.len().max(other.0.len())) {
let stack = &self.0.get(i);
let other_stack = &other.0.get(i);
if *stack != *other_stack && !b"\r\n".contains(&text[i]) {
if *stack != *other_stack && ![b'\r', b'\n'].contains(&text[i]) {
let containing_range = known_changed_ranges
.iter()
.find(|range| range.start_point <= position && position < range.end_point);

View file

@ -4,18 +4,17 @@ use std::{
fs,
io::{self, Write as _},
path::{self, Path, PathBuf},
sync::{Arc, atomic::AtomicUsize},
str,
sync::{atomic::AtomicUsize, Arc},
time::Instant,
};
use ansi_colours::{ansi256_from_rgb, rgb_from_ansi256};
use anstyle::{Ansi256Color, AnsiColor, Color, Effects, RgbColor};
use anyhow::Result;
use clap::ValueEnum;
use log::{info, warn};
use serde::{Deserialize, Deserializer, Serialize, Serializer, ser::SerializeMap};
use serde_json::{Value, json};
use tree_sitter::ffi::{self, TSInputEncoding};
use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer};
use serde_json::{json, Value};
use tree_sitter_highlight::{HighlightConfiguration, HighlightEvent, Highlighter, HtmlRenderer};
use tree_sitter_loader::Loader;
@ -26,9 +25,8 @@ pub const HTML_HEAD_HEADER: &str = "
<style>
body {
font-family: monospace
}";
pub const HTML_LINE_NUMBER_STYLE: &str = " .line-number {
}
.line-number {
user-select: none;
text-align: right;
color: rgba(27,31,35,.3);
@ -36,7 +34,8 @@ pub const HTML_LINE_NUMBER_STYLE: &str = " .line-number {
}
.line {
white-space: pre;
}";
}
</style>";
pub const HTML_BODY_HEADER: &str = "
</head>
@ -191,14 +190,20 @@ fn parse_style(style: &mut Style, json: Value) {
if let Value::Object(entries) = json {
for (property_name, value) in entries {
match property_name.as_str() {
"bold" if value == Value::Bool(true) => {
style.ansi = style.ansi.bold();
"bold" => {
if value == Value::Bool(true) {
style.ansi = style.ansi.bold();
}
}
"italic" if value == Value::Bool(true) => {
style.ansi = style.ansi.italic();
"italic" => {
if value == Value::Bool(true) {
style.ansi = style.ansi.italic();
}
}
"underline" if value == Value::Bool(true) => {
style.ansi = style.ansi.underline();
"underline" => {
if value == Value::Bool(true) {
style.ansi = style.ansi.underline();
}
}
"color" => {
if let Some(color) = parse_color(value) {
@ -216,11 +221,11 @@ fn parse_style(style: &mut Style, json: Value) {
style.css = None;
}
if let Some(Color::Rgb(RgbColor(red, green, blue))) = style.ansi.get_fg_color()
&& !terminal_supports_truecolor()
{
let ansi256 = Color::Ansi256(Ansi256Color(ansi256_from_rgb((red, green, blue))));
style.ansi = style.ansi.fg_color(Some(ansi256));
if let Some(Color::Rgb(RgbColor(red, green, blue))) = style.ansi.get_fg_color() {
if !terminal_supports_truecolor() {
let ansi256 = Color::Ansi256(Ansi256Color(ansi256_from_rgb((red, green, blue))));
style.ansi = style.ansi.fg_color(Some(ansi256));
}
}
}
@ -308,40 +313,15 @@ fn terminal_supports_truecolor() -> bool {
.is_ok_and(|truecolor| truecolor == "truecolor" || truecolor == "24bit")
}
/// The kind of HTML emitted when highlighting to HTML.
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum HtmlOutput {
/// A complete, self-contained document wrapping a plain
/// `<div class="highlight"><pre><code>` block.
Document,
/// A complete document with a line-number column (a `<table>` layout).
#[value(name = "line-numbers")]
NumberedDocument,
/// Only the code markup, without the surrounding document.
Fragment,
}
/// How token colors are applied in HTML output.
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum HtmlStyling {
/// `class="..."` spans plus a generated `<style>` carrying the theme's colors.
Classes,
/// `style="..."` spans with the colors inlined.
Inline,
/// `class="..."` spans with no colors emitted (supply your own stylesheet).
Minimal,
}
pub struct HighlightOptions {
pub theme: Theme,
pub check: bool,
pub captures_path: Option<PathBuf>,
/// `None` for regular output, `Some((layout, style))` when emitting HTML.
pub html: Option<(HtmlOutput, HtmlStyling)>,
pub inline_styles: bool,
pub html: bool,
pub quiet: bool,
pub print_time: bool,
pub cancellation_flag: Arc<AtomicUsize>,
pub encoding: Option<TSInputEncoding>,
}
pub fn highlight(
@ -384,60 +364,29 @@ pub fn highlight(
}
let source = fs::read(path)?;
fn is_utf16_le_bom(bom_bytes: &[u8]) -> bool {
bom_bytes == [0xFF, 0xFE]
}
fn is_utf16_be_bom(bom_bytes: &[u8]) -> bool {
bom_bytes == [0xFE, 0xFF]
}
let encoding = match opts.encoding {
None if source.len() >= 2 => {
if is_utf16_le_bom(&source[0..2]) {
Some(ffi::TSInputEncodingUTF16LE)
} else if is_utf16_be_bom(&source[0..2]) {
Some(ffi::TSInputEncodingUTF16BE)
} else {
None
}
}
_ => opts.encoding,
};
let stdout = io::stdout();
let mut stdout = stdout.lock();
let time = Instant::now();
let mut highlighter = Highlighter::new();
let events = highlighter.highlight(
config,
&source,
encoding,
Some(&opts.cancellation_flag),
|string| loader.highlight_config_for_injection_string(string),
)?;
let events =
highlighter.highlight(config, &source, Some(&opts.cancellation_flag), |string| {
loader.highlight_config_for_injection_string(string)
})?;
let theme = &opts.theme;
// A fragment is pure code markup, so it must not be prefixed with the filename.
let html_fragment = opts
.html
.is_some_and(|(layout, _)| layout == HtmlOutput::Fragment);
if !opts.quiet && print_name && !html_fragment {
if !opts.quiet && print_name {
writeln!(&mut stdout, "{name}")?;
}
if let Some((layout, style)) = opts.html {
if !opts.quiet && layout != HtmlOutput::Fragment {
if opts.html {
if !opts.quiet {
writeln!(&mut stdout, "{HTML_HEAD_HEADER}")?;
if layout == HtmlOutput::NumberedDocument {
writeln!(&mut stdout, "{HTML_LINE_NUMBER_STYLE}")?;
}
if style == HtmlStyling::Classes {
for (name, style) in theme.highlight_names.iter().zip(&theme.styles) {
if let Some(css) = &style.css {
writeln!(&mut stdout, " .{name} {{ {css}; }}")?;
}
writeln!(&mut stdout, " <style>")?;
let names = theme.highlight_names.iter();
let styles = theme.styles.iter();
for (name, style) in names.zip(styles) {
if let Some(css) = &style.css {
writeln!(&mut stdout, " .{name} {{ {css}; }}")?;
}
}
writeln!(&mut stdout, " </style>")?;
@ -446,7 +395,7 @@ pub fn highlight(
let mut renderer = HtmlRenderer::new();
renderer.render(events, &source, &move |highlight, output| {
if style == HtmlStyling::Inline {
if opts.inline_styles {
output.extend(b"style='");
output.extend(
theme.styles[highlight.0]
@ -454,6 +403,7 @@ pub fn highlight(
.as_ref()
.map_or_else(|| "".as_bytes(), |css_style| css_style.as_bytes()),
);
output.extend(b"'");
} else {
output.extend(b"class='");
let mut parts = theme.highlight_names[highlight.0].split('.').peekable();
@ -463,34 +413,21 @@ pub fn highlight(
output.extend(b" ");
}
}
output.extend(b"'");
}
output.extend(b"'");
})?;
if !opts.quiet {
if layout == HtmlOutput::NumberedDocument {
writeln!(&mut stdout, "<table>")?;
for (i, line) in renderer.lines().enumerate() {
writeln!(
&mut stdout,
"<tr><td class=line-number>{}</td><td class=line>{line}</td></tr>",
i + 1,
)?;
}
writeln!(&mut stdout, "</table>")?;
} else {
let mut body = renderer.lines().collect::<String>();
if body.ends_with('\n') {
body.pop();
}
writeln!(&mut stdout, "<table>")?;
for (i, line) in renderer.lines().enumerate() {
writeln!(
&mut stdout,
"<div class=\"highlight\">\n<pre><code>{body}</code></pre>\n</div>",
"<tr><td class=line-number>{}</td><td class=line>{line}</td></tr>",
i + 1,
)?;
}
if layout != HtmlOutput::Fragment {
writeln!(&mut stdout, "{HTML_FOOTER}")?;
}
writeln!(&mut stdout, "</table>")?;
writeln!(&mut stdout, "{HTML_FOOTER}")?;
}
} else {
let mut style_stack = vec![theme.default_style().ansi];
@ -537,7 +474,7 @@ mod tests {
assert_eq!(style.css, None);
// darkcyan is an ANSI color and is preserved
unsafe { env::set_var("COLORTERM", "") };
env::set_var("COLORTERM", "");
parse_style(&mut style, Value::String(DARK_CYAN.to_string()));
assert_eq!(
style.ansi.get_fg_color(),
@ -546,7 +483,7 @@ mod tests {
assert_eq!(style.css, Some("color: #00af87".to_string()));
// junglegreen is not an ANSI color and is preserved when the terminal supports it
unsafe { env::set_var("COLORTERM", "truecolor") };
env::set_var("COLORTERM", "truecolor");
parse_style(&mut style, Value::String(JUNGLE_GREEN.to_string()));
assert_eq!(
style.ansi.get_fg_color(),
@ -555,7 +492,7 @@ mod tests {
assert_eq!(style.css, Some("color: #26a69a".to_string()));
// junglegreen gets approximated as cadetblue when the terminal does not support it
unsafe { env::set_var("COLORTERM", "") };
env::set_var("COLORTERM", "");
parse_style(&mut style, Value::String(JUNGLE_GREEN.to_string()));
assert_eq!(
style.ansi.get_fg_color(),
@ -564,9 +501,9 @@ mod tests {
assert_eq!(style.css, Some("color: #26a69a".to_string()));
if let Ok(environment_variable) = original_environment_variable {
unsafe { env::set_var("COLORTERM", environment_variable) };
env::set_var("COLORTERM", environment_variable);
} else {
unsafe { env::remove_var("COLORTERM") };
env::remove_var("COLORTERM");
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -3,16 +3,15 @@ use std::{
io::{Read, Write},
path::{Path, PathBuf},
sync::{
Arc,
atomic::{AtomicUsize, Ordering},
mpsc,
mpsc, Arc,
},
};
use anyhow::{Context, Result, anyhow, bail};
use anyhow::{anyhow, bail, Context, Result};
use glob::glob;
use crate::test::{TestEntry, parse_tests};
use crate::test::{parse_tests, TestEntry};
pub enum CliInput {
Paths(Vec<PathBuf>),
@ -147,6 +146,7 @@ pub fn get_input(
}
}
#[allow(clippy::type_complexity)]
pub fn get_test_info(
test_entry: &TestEntry,
target_test: u32,

View file

@ -1,8 +1,12 @@
use std::io::Write;
use anstyle::{AnsiColor, Color, Style};
use log::{Level, LevelFilter, Log, Metadata, Record};
use crate::paint::{Paint, RED, YELLOW};
pub fn paint(color: Option<impl Into<Color>>, text: &str) -> String {
let style = Style::new().fg_color(color.map(Into::into));
format!("{style}{text}{style:#}")
}
struct Logger;
@ -13,8 +17,16 @@ impl Log for Logger {
fn log(&self, record: &Record) {
match record.level() {
Level::Error => eprintln!("{} {}", Paint(RED, "Error:"), record.args()),
Level::Warn => eprintln!("{} {}", Paint(YELLOW, "Warning:"), record.args()),
Level::Error => eprintln!(
"{} {}",
paint(Some(AnsiColor::Red), "Error:"),
record.args()
),
Level::Warn => eprintln!(
"{} {}",
paint(Some(AnsiColor::Yellow), "Warning:"),
record.args()
),
Level::Info | Level::Debug => eprintln!("{}", record.args()),
Level::Trace => eprintln!(
"[{}] {}",

View file

@ -5,24 +5,24 @@ use std::{
};
use anstyle::{AnsiColor, Color, Style};
use anyhow::{Context, Result, anyhow};
use clap::{ArgGroup, Args, Command, FromArgMatches as _, Subcommand, ValueEnum, crate_authors};
use anyhow::{anyhow, Context, Result};
use clap::{crate_authors, Args, Command, FromArgMatches as _, Subcommand, ValueEnum};
use clap_complete::generate;
use dialoguer::{Confirm, FuzzySelect, Input, MultiSelect, theme::ColorfulTheme};
use dialoguer::{theme::ColorfulTheme, Confirm, FuzzySelect, Input, MultiSelect};
use heck::ToUpperCamelCase;
use log::{error, info, warn};
use regex::Regex;
use semver::Version as SemverVersion;
use tree_sitter::{Parser, Point, ffi};
use tree_sitter::{ffi, Parser, Point};
use tree_sitter_cli::{
fuzz::{
DEFAULT_EDIT_COUNT, DEFAULT_ITERATION_COUNT, EDIT_COUNT, FuzzOptions, ITERATION_COUNT,
LOG_ENABLED, LOG_GRAPH_ENABLED, START_SEED, fuzz_language_corpus,
fuzz_language_corpus, FuzzOptions, EDIT_COUNT, ITERATION_COUNT, LOG_ENABLED,
LOG_GRAPH_ENABLED, START_SEED,
},
highlight::{self, HighlightOptions, HtmlOutput, HtmlStyling},
init::{JsonConfigOpts, TREE_SITTER_JSON_SCHEMA, generate_grammar_files},
input::{CliInput, get_input, get_tmp_source_file},
logger, paint,
highlight::{self, HighlightOptions},
init::{generate_grammar_files, JsonConfigOpts},
input::{get_input, get_tmp_source_file, CliInput},
logger,
parse::{self, ParseDebugType, ParseFileOptions, ParseOutput, ParseTheme},
playground,
query::{self, QueryFileOptions},
@ -33,7 +33,7 @@ use tree_sitter_cli::{
wasm,
};
use tree_sitter_config::Config;
use tree_sitter_generate::{Diagnostic, GenerateError, OptLevel};
use tree_sitter_generate::OptLevel;
use tree_sitter_highlight::Highlighter;
use tree_sitter_loader::{self as loader, Bindings, TreeSitterJSON};
use tree_sitter_tags::TagsContext;
@ -188,14 +188,10 @@ struct Build {
/// Compile a parser in debug mode
#[arg(long, short = '0')]
pub debug: bool,
/// Display verbose build information
#[arg(short, long)]
pub verbose: bool,
}
#[derive(Args)]
#[command(alias = "p")]
#[command(group(ArgGroup::new("graph_output").multiple(true)))]
struct Parse {
/// The path to a file with paths to source file(s)
#[arg(long = "paths")]
@ -211,29 +207,26 @@ struct Parse {
pub lib_path: Option<PathBuf>,
/// If `--lib-path` is used, the name of the language used to extract the
/// library's language function
#[arg(long, requires = "lib_path")]
#[arg(long)]
pub lang_name: Option<String>,
/// Select a language by the scope instead of a file extension
#[arg(long)]
pub scope: Option<String>,
/// Show parsing debug log
#[arg(long, short = 'd')] // TODO: Rework once clap adds `default_missing_value_t`
#[expect(
clippy::option_option,
reason = "required by clap for optional flag with optional value"
)]
#[allow(clippy::option_option)]
pub debug: Option<Option<ParseDebugType>>,
/// Compile a parser in debug mode
#[arg(long, short = '0')]
pub debug_build: bool,
/// Produce the log.html file with debug graphs
#[arg(long, short = 'D', group = "graph_output")]
#[arg(long, short = 'D')]
pub debug_graph: bool,
/// Compile parsers to Wasm instead of native dynamic libraries
#[arg(long, hide = cfg!(not(feature = "wasm")))]
pub wasm: bool,
/// Output the parse data with graphviz dot
#[arg(long = "dot", group = "graph_output")]
#[arg(long = "dot")]
pub output_dot: bool,
/// Output the parse data in XML format
#[arg(long = "xml", short = 'x')]
@ -241,7 +234,7 @@ struct Parse {
/// Output the parse data in a pretty-printed CST format
#[arg(long = "cst", short = 'c')]
pub output_cst: bool,
/// Show parsing statistics
/// Show parsing statistic
#[arg(long, short, conflicts_with = "json", conflicts_with = "json_summary")]
pub stat: bool,
/// Interrupt the parsing process by timeout (µs)
@ -253,10 +246,7 @@ struct Parse {
/// Suppress main output
#[arg(long, short)]
pub quiet: bool,
#[expect(
clippy::doc_markdown,
reason = "doc string contains format syntax, not code identifiers"
)]
#[allow(clippy::doc_markdown)]
/// Apply edits in the format: \"row,col|position delcount insert_text\", can be supplied
/// multiple times
#[arg(
@ -267,8 +257,8 @@ struct Parse {
/// The encoding of the input files
#[arg(long)]
pub encoding: Option<Encoding>,
/// Open `log.html` in the default browser, if `--debug-graph` or `--dot` is supplied
#[arg(long, requires = "graph_output")]
/// Open `log.html` in the default browser, if `--debug-graph` is supplied
#[arg(long)]
pub open_log: bool,
/// Deprecated: use --json-summary
#[arg(long, conflicts_with = "json_summary", conflicts_with = "stat")]
@ -293,9 +283,9 @@ struct Parse {
#[derive(ValueEnum, Clone)]
pub enum Encoding {
Utf8 = 0,
Utf16LE = 1,
Utf16BE = 2,
Utf8,
Utf16LE,
Utf16BE,
}
#[derive(Args)]
@ -318,7 +308,7 @@ struct Test {
pub lib_path: Option<PathBuf>,
/// If `--lib-path` is used, the name of the language used to extract the
/// library's language function
#[arg(long, requires = "lib_path")]
#[arg(long)]
pub lang_name: Option<String>,
/// Update all syntax trees in corpus files with current parser output
#[arg(long, short)]
@ -336,7 +326,7 @@ struct Test {
#[arg(long, hide = cfg!(not(feature = "wasm")))]
pub wasm: bool,
/// Open `log.html` in the default browser, if `--debug-graph` is supplied
#[arg(long, requires = "debug_graph")]
#[arg(long)]
pub open_log: bool,
/// The path to an alternative config.json file
#[arg(long)]
@ -344,9 +334,6 @@ struct Test {
/// Force showing fields in test diffs
#[arg(long)]
pub show_fields: bool,
/// Force showing '+' and '-' in test diffs
#[arg(long)]
pub show_diff_markers: bool,
/// Show parsing statistics
#[arg(long)]
pub stat: Option<TestStats>,
@ -363,10 +350,6 @@ struct Test {
#[derive(Args)]
#[command(alias = "publish")]
#[expect(
clippy::struct_field_names,
reason = "field names map to CLI arguments"
)]
/// Display or increment the version of a grammar
struct Version {
/// The version to bump to
@ -406,17 +389,13 @@ struct Fuzz {
pub lib_path: Option<PathBuf>,
/// If `--lib-path` is used, the name of the language used to extract the
/// library's language function
#[arg(long, requires = "lib_path")]
#[arg(long)]
pub lang_name: Option<String>,
#[arg(
long,
help=format!("Maximum number of edits to perform per fuzz test (Default: {DEFAULT_EDIT_COUNT})")
)]
/// Maximum number of edits to perform per fuzz test
#[arg(long)]
pub edits: Option<usize>,
#[arg(
long,
help=format!("Number of fuzzing iterations to run per test (Default: {DEFAULT_ITERATION_COUNT})")
)]
/// Number of fuzzing iterations to run per test
#[arg(long)]
pub iterations: Option<usize>,
/// Only fuzz corpus test cases whose name matches the given regex
#[arg(long, short)]
@ -437,10 +416,6 @@ struct Fuzz {
#[derive(Args)]
#[command(alias = "q")]
#[expect(
clippy::struct_field_names,
reason = "field names map to CLI arguments"
)]
struct Query {
/// Path to a file with queries
#[arg(index = 1, required = true)]
@ -453,7 +428,7 @@ struct Query {
pub lib_path: Option<PathBuf>,
/// If `--lib-path` is used, the name of the language used to extract the
/// library's language function
#[arg(long, requires = "lib_path")]
#[arg(long)]
pub lang_name: Option<String>,
/// Measure execution time
#[arg(long, short)]
@ -508,20 +483,14 @@ struct Highlight {
/// Generate highlighting as an HTML document
#[arg(long, short = 'H')]
pub html: bool,
/// Deprecated: use `--style classes`
#[arg(long, requires = "html", conflicts_with = "style")]
/// When generating HTML, use css classes rather than inline styles
#[arg(long)]
pub css_classes: bool,
/// When generating HTML, the document structure to emit
#[arg(long, requires = "html", value_enum, default_value = "document")]
pub layout: HtmlOutput,
/// When generating HTML, how token colors are applied
#[arg(long, requires = "html", value_enum, default_value = "classes")]
pub style: HtmlStyling,
/// Check that highlighting captures conform strictly to standards
#[arg(long)]
pub check: bool,
/// The path to a file with captures
#[arg(long, requires = "check")]
#[arg(long)]
pub captures_path: Option<PathBuf>,
/// The paths to files with queries
#[arg(long, num_args = 1..)]
@ -554,9 +523,6 @@ struct Highlight {
/// Force rebuild the parser
#[arg(short, long)]
pub rebuild: bool,
/// The encoding of the input files
#[arg(long)]
pub encoding: Option<Encoding>,
}
#[derive(Args)]
@ -819,7 +785,7 @@ impl Init {
let enabled = MultiSelect::new()
.with_prompt("Bindings")
.items_checked(languages.iter().copied())
.items_checked(&languages)
.interact()?
.into_iter()
.map(|i| languages[i].0);
@ -893,7 +859,7 @@ impl Init {
let idx = FuzzySelect::with_theme(&ColorfulTheme::default())
.with_prompt("Which field would you like to change?")
.items(choices)
.items(&choices)
.interact()?;
set_choice!(choices[idx]);
@ -901,26 +867,10 @@ impl Init {
(opts.name.clone(), Some(opts))
} else {
let old_config = 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 implicit `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")?;
}
let mut json = serde_json::from_str::<TreeSitterJSON>(
&fs::read_to_string(current_dir.join("tree-sitter.json"))
.with_context(|| "Failed to read tree-sitter.json")?,
)?;
(json.grammars.swap_remove(0).name, None)
};
@ -958,8 +908,7 @@ impl Generate {
self.json_summary
};
let mut diagnostics = Vec::new();
let result = tree_sitter_generate::generate_parser_in_directory(
if let Err(err) = tree_sitter_generate::generate_parser_in_directory(
current_dir,
self.output.as_deref(),
self.grammar_path.as_deref(),
@ -972,33 +921,16 @@ impl Generate {
} else {
OptLevel::default()
},
&mut diagnostics,
);
if json_summary {
#[derive(serde::Serialize)]
struct Envelope<'a> {
diagnostics: &'a [Diagnostic],
error: Option<&'a GenerateError>,
}
let envelope = Envelope {
diagnostics: &diagnostics,
error: result.as_ref().err(),
};
eprintln!("{}", serde_json::to_string_pretty(&envelope)?);
if result.is_err() {
) {
if json_summary {
eprintln!("{}", serde_json::to_string_pretty(&err)?);
// Exit early to prevent errors from being printed a second time in the caller
std::process::exit(1);
}
} else {
for d in &diagnostics {
warn!("{d}");
}
if let Err(err) = result {
} else {
// Removes extra context associated with the error
Err(anyhow!(err.to_string())).with_context(|| "Error when generating parser")?;
}
}
if self.build {
warn!("--build is deprecated, use the `build` command");
if let Some(path) = self.libdir {
@ -1016,7 +948,6 @@ impl Build {
let grammar_path = current_dir.join(self.path.unwrap_or_default());
loader.debug_build(self.debug);
loader.verbose_build(self.verbose);
if self.wasm {
let output_path = self.output.map(|path| current_dir.join(path));
@ -1024,21 +955,11 @@ impl Build {
} else {
let output_path = if let Some(ref path) = self.output {
let path = Path::new(path);
let full_path = if path.is_absolute() {
if path.is_absolute() {
path.to_path_buf()
} else {
current_dir.join(path)
};
let parent_path = full_path
.parent()
.context("Output path must have a parent")?;
let name = full_path
.file_name()
.context("Output 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 {
let file_name = grammar_path
.file_stem()
@ -1063,7 +984,7 @@ impl Build {
loader
.compile_parser_at_path(&grammar_path, output_path, flags)
.context("Failed to compile parser")?;
.unwrap();
}
Ok(())
}
@ -1072,6 +993,7 @@ impl Build {
impl Parse {
fn run(self, mut loader: loader::Loader, current_dir: &Path) -> Result<()> {
let config = Config::load(self.config_path)?;
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
@ -1090,7 +1012,7 @@ impl Parse {
ParseOutput::Normal
};
let parse_theme = if paint::color_enabled() {
let parse_theme = if color {
config
.get::<parse::Config>()
.with_context(|| "Failed to parse CST theme")?
@ -1128,6 +1050,9 @@ impl Parse {
let timeout = self.timeout.unwrap_or_default();
let mut has_error = false;
let loader_config = config.get()?;
loader.find_all_languages(&loader_config)?;
let should_track_stats = self.stat;
let mut stats = parse::ParseStats::default();
let debug: ParseDebugType = match self.debug {
@ -1170,11 +1095,10 @@ impl Parse {
has_error |= !parse_result.successful;
};
let lib_info = get_lib_info(self.lib_path.as_ref(), self.lang_name.as_ref(), current_dir);
if lib_info.is_none() {
let loader_config = config.get()?;
loader.find_all_languages(&loader_config)?;
if self.lib_path.is_none() && self.lang_name.is_some() {
warn!("--lang-name` specified without --lib-path. This argument will be ignored.");
}
let lib_info = get_lib_info(self.lib_path.as_ref(), self.lang_name.as_ref(), current_dir);
let input = get_input(
self.paths_file.as_deref(),
@ -1325,6 +1249,7 @@ fn check_test(
impl Test {
fn run(self, mut loader: loader::Loader, current_dir: &Path) -> Result<()> {
let config = Config::load(self.config_path)?;
let color = env::var("NO_COLOR").map_or(true, |v| v != "1");
let stat = self.stat.unwrap_or_default();
loader.debug_build(self.debug_build);
@ -1342,6 +1267,9 @@ impl Test {
});
}
if self.lib_path.is_none() && self.lang_name.is_some() {
warn!("--lang-name` specified without --lib-path. This argument will be ignored.");
}
let languages = loader.languages_at_path(current_dir)?;
let language = if let Some(ref lib_path) = self.lib_path {
let lib_info =
@ -1363,9 +1291,13 @@ impl Test {
parser.set_language(language)?;
let test_dir = current_dir.join("test");
let mut test_summary =
TestSummary::new(stat, self.update, self.overview_only, self.json_summary);
test_summary.use_markers = self.show_diff_markers;
let mut test_summary = TestSummary::new(
color,
stat,
self.update,
self.overview_only,
self.json_summary,
);
// Run the corpus tests. Look for them in `test/corpus`.
let test_corpus_dir = test_dir.join("corpus");
@ -1380,6 +1312,7 @@ impl Test {
update: self.update,
open_log: self.open_log,
languages: languages.iter().map(|(l, n)| (n.as_str(), l)).collect(),
color,
show_fields: self.show_fields,
overview_only: self.overview_only,
};
@ -1390,20 +1323,16 @@ impl Test {
self.json_summary,
)?;
test_summary.test_num = 1;
} else {
warn!("Test corpus not found at {}", test_corpus_dir.display());
}
// Check that all of the queries are valid.
let query_dir = current_dir.join("queries");
if query_dir.is_dir() {
check_test(
test::check_queries_at_path(language, &query_dir),
&test_summary,
self.json_summary,
)?;
test_summary.test_num = 1;
}
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.
let test_highlight_dir = test_dir.join("highlight");
@ -1446,7 +1375,7 @@ impl Test {
// For the rest of the queries, find their tests and run them
for entry in walkdir::WalkDir::new(&query_dir)
.into_iter()
.filter_map(std::result::Result::ok)
.filter_map(|e| e.ok())
.filter(|e| e.file_type().is_file())
{
let stem = entry
@ -1517,6 +1446,9 @@ impl Fuzz {
loader.sanitize_build(true);
loader.force_rebuild(self.rebuild || self.grammar_path.is_some());
if self.lib_path.is_none() && self.lang_name.is_some() {
warn!("--lang-name` specified without --lib-path. This argument will be ignored.");
}
let languages = loader.languages_at_path(current_dir)?;
let (language, language_name) = if let Some(ref lib_path) = self.lib_path {
let lib_info = get_lib_info(Some(lib_path), self.lang_name.as_ref(), current_dir)
@ -1564,23 +1496,24 @@ impl Fuzz {
impl Query {
fn run(self, mut loader: loader::Loader, current_dir: &Path) -> Result<()> {
let config = Config::load(self.config_path)?;
let lib_info = get_lib_info(self.lib_path.as_ref(), self.lang_name.as_ref(), current_dir);
if lib_info.is_none() {
let loader_config = config.get()?;
loader.find_all_languages(&loader_config)?;
}
let loader_config = config.get()?;
loader.force_rebuild(self.rebuild || self.grammar_path.is_some());
loader.find_all_languages(&loader_config)?;
let query_path = Path::new(&self.query_path);
let byte_range = parse_range(self.byte_range.as_deref(), |x| x)?;
let point_range = parse_range(self.row_range.as_deref(), |row| Point::new(row, 0))?;
let containing_byte_range = parse_range(self.containing_byte_range.as_deref(), |x| x)?;
let containing_point_range = parse_range(self.containing_row_range.as_deref(), |row| {
Point::new(row, 0)
})?;
let byte_range = parse_range(&self.byte_range, |x| x)?;
let point_range = parse_range(&self.row_range, |row| Point::new(row, 0))?;
let containing_byte_range = parse_range(&self.containing_byte_range, |x| x)?;
let containing_point_range =
parse_range(&self.containing_row_range, |row| Point::new(row, 0))?;
let cancellation_flag = util::cancel_on_signal();
if self.lib_path.is_none() && self.lang_name.is_some() {
warn!("--lang-name specified without --lib-path. This argument will be ignored.");
}
let lib_info = get_lib_info(self.lib_path.as_ref(), self.lang_name.as_ref(), current_dir);
let input = get_input(
self.paths_file.as_deref(),
self.paths,
@ -1689,7 +1622,6 @@ impl Highlight {
let loader_config = config.get()?;
loader.find_all_languages(&loader_config)?;
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();
@ -1704,29 +1636,15 @@ impl Highlight {
}
}
let encoding = self.encoding.map(|e| match e {
Encoding::Utf8 => ffi::TSInputEncodingUTF8,
Encoding::Utf16LE => ffi::TSInputEncodingUTF16LE,
Encoding::Utf16BE => ffi::TSInputEncodingUTF16BE,
});
let style = if self.css_classes {
// TODO: Remove during the 0.28 release cycle
warn!("--css-classes is deprecated, use --style classes instead");
HtmlStyling::Classes
} else {
self.style
};
let options = HighlightOptions {
theme: theme_config.theme,
check: self.check,
captures_path: self.captures_path,
html: self.html.then_some((self.layout, style)),
inline_styles: !self.css_classes,
html: self.html,
quiet: self.quiet,
print_time: self.time,
cancellation_flag: cancellation_flag.clone(),
encoding,
};
let input = get_input(
@ -1784,6 +1702,7 @@ impl Highlight {
} => {
let path = get_tmp_source_file(&contents)?;
let languages = loader.languages_at_path(current_dir)?;
let language = languages
.iter()
.find(|(_, n)| language_names.contains(&Box::from(n.as_str())))
@ -1811,9 +1730,10 @@ impl Highlight {
let path = get_tmp_source_file(&contents)?;
let (language, language_config) =
if let (Some(l), Some(lc)) = (language, language_configuration) {
if let (Some(l), Some(lc)) = (language.clone(), language_configuration) {
(l, lc)
} else {
let languages = loader.languages_at_path(current_dir)?;
let language = languages
.first()
.map(|(l, _)| l.clone())
@ -1953,7 +1873,7 @@ impl Tags {
let path = get_tmp_source_file(&contents)?;
let (language, language_config) =
if let (Some(l), Some(lc)) = (language, language_configuration) {
if let (Some(l), Some(lc)) = (language.clone(), language_configuration) {
(l, lc)
} else {
let languages = loader.languages_at_path(current_dir)?;
@ -2007,7 +1927,7 @@ impl DumpLanguages {
concat!(
"name: {}\n",
"scope: {}\n",
"parser: {}\n",
"parser: {:?}\n",
"highlights: {:?}\n",
"file_types: {:?}\n",
"content_regex: {:?}\n",
@ -2015,7 +1935,7 @@ impl DumpLanguages {
),
configuration.language_name,
configuration.scope.as_ref().unwrap_or(&String::new()),
language_path.display(),
language_path,
configuration.highlights_filenames,
configuration.file_types,
configuration.content_regex,
@ -2048,10 +1968,10 @@ fn main() {
let result = run();
if let Err(err) = &result {
// Ignore BrokenPipe errors
if let Some(error) = err.downcast_ref::<std::io::Error>()
&& error.kind() == std::io::ErrorKind::BrokenPipe
{
return;
if let Some(error) = err.downcast_ref::<std::io::Error>() {
if error.kind() == std::io::ErrorKind::BrokenPipe {
return;
}
}
if !err.to_string().is_empty() {
error!("{err:?}");
@ -2104,7 +2024,7 @@ fn run() -> Result<()> {
| Commands::Complete(_) => &None,
}
.as_ref()
.map_or_else(|| env::current_dir().unwrap(), std::clone::Clone::clone);
.map_or_else(|| env::current_dir().unwrap(), |p| p.clone());
let loader = loader::Loader::new()?;
@ -2176,7 +2096,7 @@ fn get_lib_info<'a>(
// Use the user-specified name if present, otherwise try to derive it from
// the lib path
match (
language_name.map(std::string::String::as_str),
language_name.map(|s| s.as_str()),
lib_path.file_stem().and_then(|s| s.to_str()),
) {
(Some(name), _) | (None, Some(name)) => Some((absolute_lib_path, name)),
@ -2189,10 +2109,10 @@ fn get_lib_info<'a>(
/// Parse a range string of the form "start:end" into an optional Range<T>.
fn parse_range<T>(
range_str: Option<&str>,
range_str: &Option<String>,
make: impl Fn(usize) -> T,
) -> Result<Option<std::ops::Range<T>>> {
if let Some(range) = range_str {
if let Some(range) = range_str.as_ref() {
let err_msg = format!("Invalid range '{range}', expected 'start:end'");
let mut parts = range.split(':');

View file

@ -1,27 +0,0 @@
use anstyle::{AnsiColor, Color, Style};
pub const RED: Style = Style::new().fg_color(Some(Color::Ansi(AnsiColor::Red)));
pub const YELLOW: Style = Style::new().fg_color(Some(Color::Ansi(AnsiColor::Yellow)));
/// Wraps a `Display` value with a style; emits ANSI codes only when
/// [`color_enabled`] is true.
pub struct Paint<T>(pub Style, pub T);
pub fn color_enabled() -> bool {
static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*ENABLED.get_or_init(|| std::env::var_os("NO_COLOR").is_none_or(|v| v.is_empty()))
}
pub fn paint<T>(color: Option<impl Into<Color>>, text: T) -> Paint<T> {
Paint(Style::new().fg_color(color.map(Into::into)), text)
}
impl<T: std::fmt::Display> std::fmt::Display for Paint<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if color_enabled() {
write!(f, "{}{}{:#}", self.0, self.1, self.0)
} else {
self.1.fmt(f)
}
}
}

View file

@ -8,17 +8,17 @@ use std::{
};
use anstyle::{AnsiColor, Color, RgbColor};
use anyhow::{Context, Result, anyhow};
use anyhow::{anyhow, Context, Result};
use clap::ValueEnum;
use log::info;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use tree_sitter::{
InputEdit, Language, LogType, ParseOptions, ParseState, Parser, Point, Range, Tree, TreeCursor,
ffi,
ffi, InputEdit, Language, LogType, ParseOptions, ParseState, Parser, Point, Range, Tree,
TreeCursor,
};
use crate::{fuzz::edits::Edit, paint::paint, util};
use crate::{fuzz::edits::Edit, logger::paint, util};
#[derive(Debug, Default, Serialize, JsonSchema)]
pub struct Stats {
@ -286,10 +286,6 @@ pub fn parse_file_at_path(
max_path_length: usize,
opts: &mut ParseFileOptions,
) -> Result<()> {
#[expect(
clippy::collection_is_never_read,
reason = "value is held for its Drop side effect"
)]
let mut _log_session = None;
parser.set_language(language)?;
let mut source_code = fs::read(path).with_context(|| format!("Error reading {name:?}"))?;
@ -301,6 +297,7 @@ pub fn parse_file_at_path(
// Log to stderr if `--debug` was passed
else if opts.debug != ParseDebugType::Quiet {
let mut curr_version: usize = 0;
let use_color = std::env::var("NO_COLOR").map_or(true, |v| v != "1");
let debug = opts.debug;
parser.set_logger(Some(Box::new(move |log_type, message| {
if debug == ParseDebugType::Normal {
@ -309,13 +306,13 @@ pub fn parse_file_at_path(
}
writeln!(&mut io::stderr(), "{message}").unwrap();
} else {
#[rustfmt::skip]
let colors = &[
AnsiColor::White, AnsiColor::Red, AnsiColor::Blue, AnsiColor::Green,
AnsiColor::Cyan, AnsiColor::Yellow, AnsiColor::Magenta,
AnsiColor::BrightWhite, AnsiColor::BrightRed, AnsiColor::BrightBlue,
AnsiColor::BrightGreen, AnsiColor::BrightCyan, AnsiColor::BrightYellow,
AnsiColor::BrightMagenta,
AnsiColor::White,
AnsiColor::Red,
AnsiColor::Blue,
AnsiColor::Green,
AnsiColor::Cyan,
AnsiColor::Yellow,
];
if message.starts_with("process version:") {
let comma_idx = message.find(',').unwrap();
@ -323,21 +320,30 @@ pub fn parse_file_at_path(
.parse()
.unwrap();
}
let color = Some(colors[curr_version % colors.len()]);
let prefix = if log_type == LogType::Lex { " " } else { "" };
writeln!(&mut io::stderr(), "{prefix}{}", paint(color, message)).unwrap();
let color = if use_color {
Some(colors[curr_version])
} else {
None
};
let mut out = if log_type == LogType::Lex {
" ".to_string()
} else {
String::new()
};
out += &paint(color, message);
writeln!(&mut io::stderr(), "{out}").unwrap();
}
})));
}
let parse_time = Instant::now();
#[inline]
#[inline(always)]
fn is_utf16_le_bom(bom_bytes: &[u8]) -> bool {
bom_bytes == [0xFF, 0xFE]
}
#[inline]
#[inline(always)]
fn is_utf16_be_bom(bom_bytes: &[u8]) -> bool {
bom_bytes == [0xFE, 0xFF]
}
@ -362,13 +368,13 @@ pub fn parse_file_at_path(
// after the specified number of microseconds.
let start_time = Instant::now();
let progress_callback = &mut |_: &ParseState| {
if let Some(cancellation_flag) = opts.cancellation_flag
&& cancellation_flag.load(Ordering::SeqCst) != 0
{
return ControlFlow::Break(());
if let Some(cancellation_flag) = opts.cancellation_flag {
if cancellation_flag.load(Ordering::SeqCst) != 0 {
return ControlFlow::Break(());
}
}
if opts.timeout > 0 && start_time.elapsed().as_micros() > u128::from(opts.timeout) {
if opts.timeout > 0 && start_time.elapsed().as_micros() > opts.timeout as u128 {
return ControlFlow::Break(());
}
@ -380,10 +386,8 @@ pub fn parse_file_at_path(
let tree = match encoding {
Some(encoding) if encoding == ffi::TSInputEncodingUTF16LE => {
let source_code_utf16 = source_code
.as_chunks::<2>()
.0
.iter()
.map(|&chunk| u16::from_le_bytes(chunk))
.chunks_exact(2)
.map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]]))
.collect::<Vec<_>>();
parser.parse_utf16_le_with_options(
&mut |i, _| {
@ -399,10 +403,8 @@ pub fn parse_file_at_path(
}
Some(encoding) if encoding == ffi::TSInputEncodingUTF16BE => {
let source_code_utf16 = source_code
.as_chunks::<2>()
.0
.iter()
.map(|&chunk| u16::from_be_bytes(chunk))
.chunks_exact(2)
.map(|chunk| u16::from_be_bytes([chunk[0], chunk[1]]))
.collect::<Vec<_>>();
parser.parse_utf16_be_with_options(
&mut |i, _| {
@ -431,7 +433,7 @@ pub fn parse_file_at_path(
let parse_duration = parse_time.elapsed();
let stdout = io::stdout();
let mut stdout = io::BufWriter::with_capacity(64 * 1024, stdout.lock());
let mut stdout = stdout.lock();
if let Some(mut tree) = tree {
if opts.debug_graph && !opts.edits.is_empty() {
@ -508,11 +510,12 @@ pub fn parse_file_at_path(
}
}
cursor.reset(tree.root_node());
writeln!(&mut stdout)?;
println!();
}
if opts.output == ParseOutput::Cst {
render_cst(&source_code, &tree, &mut cursor, opts, &mut stdout)?;
println!();
}
if opts.output == ParseOutput::Xml {
@ -542,10 +545,10 @@ pub fn parse_file_at_path(
}
write!(&mut stdout, "</{}>", tag.expect("there is a tag"))?;
// we only write a line in the case where it's the last sibling
if let Some(parent) = node.parent()
&& parent.child(parent.child_count() - 1).unwrap() == node
{
stdout.write_all(b"\n")?;
if let Some(parent) = node.parent() {
if parent.child(parent.child_count() as u32 - 1).unwrap() == node {
stdout.write_all(b"\n")?;
}
}
needs_newline = true;
}
@ -579,11 +582,11 @@ pub fn parse_file_at_path(
}
let start = node.start_position();
let end = node.end_position();
write!(
&mut stdout,
" srow=\"{}\" scol=\"{}\" erow=\"{}\" ecol=\"{}\">",
start.row, start.column, end.row, end.column
)?;
write!(&mut stdout, " srow=\"{}\"", start.row)?;
write!(&mut stdout, " scol=\"{}\"", start.column)?;
write!(&mut stdout, " erow=\"{}\"", end.row)?;
write!(&mut stdout, " ecol=\"{}\"", end.column)?;
write!(&mut stdout, ">")?;
tags.push(node.kind());
needs_newline = true;
}
@ -774,19 +777,15 @@ pub fn render_cst<'a, 'b: 'a>(
cursor: &mut TreeCursor<'a>,
opts: &ParseFileOptions,
out: &mut impl Write,
) -> io::Result<()> {
) -> Result<()> {
let lossy_source_code = String::from_utf8_lossy(source_code);
let total_width = lossy_source_code
.lines()
.enumerate()
.map(|(row, col)| {
row.checked_ilog10().unwrap_or(0) as usize
+ col.len().checked_ilog10().unwrap_or(0) as usize
+ 1
})
.map(|(row, col)| (row as f64).log10() as usize + (col.len() as f64).log10() as usize + 1)
.max()
.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 in_error = false;
loop {
@ -827,19 +826,19 @@ pub fn render_cst<'a, 'b: 'a>(
Ok(())
}
struct CstNodeText<'a>(&'a str);
impl std::fmt::Display for CstNodeText<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use std::fmt::Write as _;
for c in self.0.chars() {
match escape_invisible(c).or_else(|| escape_delimiter(c)) {
Some(esc) => f.write_str(esc)?,
None => f.write_char(c)?,
fn render_node_text(source: &str) -> String {
source
.chars()
.fold(String::with_capacity(source.len()), |mut acc, c| {
if let Some(esc) = escape_invisible(c) {
acc.push_str(esc);
} else if let Some(esc) = escape_delimiter(c) {
acc.push_str(esc);
} else {
acc.push(c);
}
}
Ok(())
}
acc
})
}
fn write_node_text(
@ -850,21 +849,21 @@ fn write_node_text(
source: &str,
color: Option<impl Into<Color> + Copy>,
text_info: (usize, usize),
) -> io::Result<()> {
) -> Result<()> {
let (total_width, indent_level) = text_info;
let (quote, quote_color) = if is_named {
('`', opts.parse_theme.backtick)
} else {
('\"', color.map(std::convert::Into::into))
('\"', color.map(|c| c.into()))
};
if !is_named {
write!(
out,
"{}{}{}",
paint(quote_color, quote),
paint(color, CstNodeText(source)),
paint(quote_color, quote),
paint(quote_color, &String::from(quote)),
paint(color, &render_node_text(source)),
paint(quote_color, &String::from(quote)),
)?;
} else {
let multiline = source.contains('\n');
@ -883,128 +882,103 @@ fn write_node_text(
} else {
0
};
if multiline {
writeln!(out)?;
if !opts.no_ranges {
write!(
out,
"{}",
CstNodeRange {
opts,
has_field_name: cursor.field_name().is_some(),
is_named,
is_multiline: true,
total_width,
range: node_range,
}
)?;
}
for _ in 0..=indent_level {
write!(out, " ")?;
}
let formatted_line = render_line_feed(line, opts);
if !opts.no_ranges {
write!(
out,
"{}{}{}{}{}{}",
if multiline { "\n" } else { "" },
if multiline {
render_node_range(opts, cursor, is_named, true, total_width, node_range)
} else {
String::new()
},
if multiline {
" ".repeat(indent_level + 1)
} else {
String::new()
},
paint(quote_color, &String::from(quote)),
&paint(color, &render_node_text(&formatted_line)),
paint(quote_color, &String::from(quote)),
)?;
} else {
write!(out, " ")?;
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)),
)?;
}
write!(
out,
"{}{}{}",
paint(quote_color, quote),
paint(color, CstLineFeed { source: line, opts }),
paint(quote_color, quote),
)?;
}
}
Ok(())
}
struct CstLineFeed<'src, 'opt> {
source: &'src str,
opts: &'src ParseFileOptions<'opt>,
}
impl std::fmt::Display for CstLineFeed<'_, '_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
#[cfg(windows)]
let lf = "\r\n";
#[cfg(not(windows))]
let lf = "\n";
let painted = paint(self.opts.parse_theme.line_feed, CstNodeText(lf));
let mut parts = self.source.split(lf);
if let Some(first) = parts.next() {
write!(f, "{}", CstNodeText(first))?;
}
for part in parts {
write!(f, "{painted}{}", CstNodeText(part))?;
}
Ok(())
fn render_line_feed(source: &str, opts: &ParseFileOptions) -> String {
if cfg!(windows) {
source.replace("\r\n", &paint(opts.parse_theme.line_feed, "\r\n"))
} else {
source.replace('\n', &paint(opts.parse_theme.line_feed, "\n"))
}
}
struct CstNodeRange<'src, 'opt> {
opts: &'src ParseFileOptions<'opt>,
has_field_name: bool,
fn render_node_range(
opts: &ParseFileOptions,
cursor: &TreeCursor,
is_named: bool,
is_multiline: bool,
total_width: usize,
range: Range,
}
) -> String {
let has_field_name = cursor.field_name().is_some();
let range_color = if is_named && !is_multiline && !has_field_name {
opts.parse_theme.row_color_named
} else {
opts.parse_theme.row_color
};
impl std::fmt::Display for CstNodeRange<'_, '_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let start = self.range.start_point;
let end = self.range.end_point;
let range_color = if self.is_named && !self.is_multiline && !self.has_field_name {
self.opts.parse_theme.row_color_named
} else {
self.opts.parse_theme.row_color
};
let remaining_width = |row: usize, col: usize| {
(self
.total_width
.saturating_sub(row.checked_ilog10().unwrap_or(0) as usize)
.saturating_sub(col.checked_ilog10().unwrap_or(0) as usize))
.max(1)
};
let remaining_width_start = remaining_width(start.row, start.column);
let remaining_width_end = remaining_width(end.row, end.column);
write!(
f,
"{}",
paint(
range_color,
format_args!(
"{}:{}{:remaining_width_start$}- {}:{}{:remaining_width_end$}",
start.row, start.column, ' ', end.row, end.column, ' ',
),
)
)
}
let remaining_width_start = (total_width
- (range.start_point.row as f64).log10() as usize
- (range.start_point.column as f64).log10() as usize)
.max(1);
let remaining_width_end = (total_width
- (range.end_point.row as f64).log10() as usize
- (range.end_point.column as f64).log10() as usize)
.max(1);
paint(
range_color,
&format!(
"{}:{}{:remaining_width_start$}- {}:{}{:remaining_width_end$}",
range.start_point.row,
range.start_point.column,
' ',
range.end_point.row,
range.end_point.column,
' ',
),
)
}
fn cst_render_node(
opts: &ParseFileOptions,
cursor: &TreeCursor,
cursor: &mut TreeCursor,
source_code: &[u8],
out: &mut impl Write,
total_width: usize,
indent_level: usize,
in_error: bool,
) -> io::Result<()> {
) -> Result<()> {
let node = cursor.node();
let is_named = node.is_named();
if !opts.no_ranges {
write!(
out,
"{}",
CstNodeRange {
opts,
has_field_name: cursor.field_name().is_some(),
is_named,
is_multiline: false,
total_width,
range: node.range(),
}
render_node_range(opts, cursor, is_named, false, total_width, node.range())
)?;
}
write!(
@ -1022,7 +996,7 @@ fn cst_render_node(
write!(
out,
"{}",
paint(opts.parse_theme.field, format_args!("{field_name}: "))
paint(opts.parse_theme.field, &format!("{field_name}: "))
)?;
}
@ -1037,9 +1011,10 @@ fn cst_render_node(
} else {
opts.parse_theme.node_kind
};
write!(out, "{}", paint(kind_color, node.kind()))?;
write!(out, "{}", paint(kind_color, node.kind()),)?;
if node.child_count() == 0 {
write!(out, " ")?;
// Node text from a pattern or external scanner
write_node_text(
opts,
@ -1093,13 +1068,10 @@ pub fn perform_edit(tree: &mut Tree, input: &mut Vec<u8>, edit: &Edit) -> Result
fn parse_edit_flag(source_code: &[u8], flag: &str) -> Result<Edit> {
let error = || {
anyhow!(
concat!(
"Invalid edit string '{}'. ",
"Edit strings must match the pattern '<START_BYTE_OR_POSITION> <REMOVED_LENGTH> <NEW_TEXT>'"
),
flag
)
anyhow!(concat!(
"Invalid edit string '{}'. ",
"Edit strings must match the pattern '<START_BYTE_OR_POSITION> <REMOVED_LENGTH> <NEW_TEXT>'"
), flag)
};
// Three whitespace-separated parts:
@ -1137,25 +1109,30 @@ fn parse_edit_flag(source_code: &[u8], flag: &str) -> Result<Edit> {
pub fn offset_for_position(input: &[u8], position: Point) -> Result<usize> {
let mut row = 0;
let mut line_start = 0;
for line_end in memchr::memchr_iter(b'\n', input) {
if row == position.row {
if position.column > line_end - line_start {
return Err(anyhow!("Failed to address a column: {}", position.column));
let mut offset = 0;
let mut iter = memchr::memchr_iter(b'\n', input);
loop {
if let Some(pos) = iter.next() {
if row < position.row {
row += 1;
offset = pos;
continue;
}
return Ok(line_start + position.column);
}
row += 1;
line_start = line_end + 1;
offset += 1;
break;
}
if row != position.row {
if position.row - row > 0 {
return Err(anyhow!("Failed to address a row: {}", position.row));
}
if position.column > input.len() - line_start {
if let Some(pos) = iter.next() {
if (pos - offset < position.column) || (input[offset] == b'\n' && position.column > 0) {
return Err(anyhow!("Failed to address a column: {}", position.column));
}
} else if input.len() - offset < position.column {
return Err(anyhow!("Failed to address a column over the end"));
}
Ok(line_start + position.column)
Ok(offset + position.column)
}
pub fn position_for_offset(input: &[u8], offset: usize) -> Result<Point> {
@ -1175,45 +1152,3 @@ pub fn position_for_offset(input: &[u8], offset: usize) -> Result<Point> {
};
Ok(result)
}
#[cfg(test)]
mod tests {
use super::{offset_for_position, parse_edit_flag};
use tree_sitter::Point;
#[test]
fn offset_for_position_uses_zero_based_line_and_column_coordinates() {
let input = b"abc\n";
assert_eq!(
offset_for_position(input, Point { row: 0, column: 0 }).unwrap(),
0
);
assert_eq!(
offset_for_position(input, Point { row: 0, column: 1 }).unwrap(),
1
);
assert_eq!(
offset_for_position(input, Point { row: 0, column: 3 }).unwrap(),
3
);
assert_eq!(
offset_for_position(input, Point { row: 1, column: 0 }).unwrap(),
4
);
}
#[test]
fn offset_for_position_rejects_out_of_bounds_coordinates() {
let input = b"abc\ndef";
assert!(offset_for_position(input, Point { row: 0, column: 4 }).is_err());
assert!(offset_for_position(input, Point { row: 2, column: 0 }).is_err());
}
#[test]
fn parse_edit_flag_resolves_first_line_positions() {
let edit = parse_edit_flag(b"abc\n", "0,0 0 X").unwrap();
assert_eq!(edit.position, 0);
assert_eq!(edit.deleted_length, 0);
assert_eq!(edit.inserted_text, b"X");
}
}

View file

@ -19,8 +19,7 @@
--light-scrollbar-track: #f1f1f1;
--light-scrollbar-thumb: #c1c1c1;
--light-scrollbar-thumb-hover: #a8a8a8;
--light-tree-row-bg: #e3f2fd;
--dark-bg: #1d1f21;
--dark-border: #2d2d2d;
--dark-text: #c5c8c6;
@ -29,7 +28,6 @@
--dark-scrollbar-track: #25282c;
--dark-scrollbar-thumb: #4a4d51;
--dark-scrollbar-thumb-hover: #5a5d61;
--dark-tree-row-bg: #373737;
--primary-color: #0550ae;
--primary-color-alpha: rgba(5, 80, 174, 0.1);
@ -44,7 +42,6 @@
--text-color: var(--dark-text);
--panel-bg: var(--dark-panel-bg);
--code-bg: var(--dark-code-bg);
--tree-row-bg: var(--dark-tree-row-bg);
}
[data-theme="light"] {
@ -53,7 +50,6 @@
--text-color: var(--light-text);
--panel-bg: white;
--code-bg: white;
--tree-row-bg: var(--light-tree-row-bg);
}
/* Base Styles */
@ -279,7 +275,7 @@
}
#output-container a.highlighted {
background-color: #cae2ff;
background-color: #d9d9d9;
color: red;
border-radius: 3px;
text-decoration: underline;
@ -350,7 +346,7 @@
}
& #output-container a.highlighted {
background-color: #656669;
background-color: #373b41;
color: red;
}
@ -377,9 +373,6 @@
color: var(--dark-text);
}
}
.tree-row:has(.highlighted) {
background-color: var(--tree-row-bg);
}
</style>
</head>
@ -470,7 +463,7 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/codemirror.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/clusterize.js/0.19.0/clusterize.min.js"></script>
<script>LANGUAGE_BASE_URL = ".";</script>
<script>LANGUAGE_BASE_URL = "";</script>
<script type="module" src="playground.js"></script>
<script type="module">
import * as TreeSitter from './web-tree-sitter.js';

View file

@ -3,10 +3,10 @@ use std::{
env, fs,
net::TcpListener,
path::{Path, PathBuf},
str::FromStr as _,
str::{self, FromStr as _},
};
use anyhow::{Context, Result, anyhow};
use anyhow::{anyhow, Context, Result};
use log::{error, info};
use tiny_http::{Header, Response, Server};

View file

@ -37,7 +37,7 @@ pub fn query_file_at_path(
test_summary: Option<&mut TestSummary>,
) -> Result<()> {
let stdout = io::stdout();
let mut stdout = io::BufWriter::with_capacity(64 * 1024, stdout.lock());
let mut stdout = stdout.lock();
let query_source = fs::read_to_string(query_path)
.with_context(|| format!("Error reading query file {}", query_path.display()))?;
@ -75,18 +75,18 @@ pub fn query_file_at_path(
if opts.ordered_captures {
let mut captures = query_cursor.captures(&query, tree.root_node(), source_code.as_slice());
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];
if !opts.quiet && !should_test {
writeln!(
&mut stdout,
" pattern: {:>2}, capture: {} - {capture_name}, start: {}, end: {}, text: `{}`",
mat.pattern_index,
capture.index,
capture.node.start_position(),
capture.node.end_position(),
capture.node.utf8_text(&source_code).unwrap_or("")
)?;
&mut stdout,
" pattern: {:>2}, capture: {} - {capture_name}, start: {}, end: {}, text: `{}`",
mat.pattern_index,
capture.index,
capture.node.start_position(),
capture.node.end_position(),
capture.node.utf8_text(&source_code).unwrap_or("")
)?;
}
if should_test {
results.push(query_testing::CaptureInfo {
@ -102,18 +102,18 @@ pub fn query_file_at_path(
if !opts.quiet && !should_test {
writeln!(&mut stdout, " pattern: {}", m.pattern_index)?;
}
for capture in m.captures() {
for capture in m.captures {
let start = capture.node.start_position();
let end = capture.node.end_position();
let capture_name = &query.capture_names()[capture.index as usize];
if !opts.quiet && !should_test {
if end.row == start.row {
writeln!(
&mut stdout,
" capture: {} - {capture_name}, start: {start}, end: {end}, text: `{}`",
capture.index,
capture.node.utf8_text(&source_code).unwrap_or("")
)?;
&mut stdout,
" capture: {} - {capture_name}, start: {start}, end: {end}, text: `{}`",
capture.index,
capture.node.utf8_text(&source_code).unwrap_or("")
)?;
} else {
writeln!(
&mut stdout,
@ -142,9 +142,7 @@ pub fn query_file_at_path(
};
// Invariant: `test_summary` will always be `Some` when `should_test` is true
let test_summary = test_summary.unwrap();
let assertions =
query_testing::parse_position_comments(&mut parser, language, source_code.as_slice())?;
match query_testing::assert_expected_captures(&results, &assertions) {
match query_testing::assert_expected_captures(&results, path, &mut parser, language) {
Ok(assertion_count) => {
test_summary.query_results.add_case(TestResult {
name: path_name.to_string(),

View file

@ -1,6 +1,6 @@
use std::sync::LazyLock;
use std::{fs, path::Path, sync::LazyLock};
use anyhow::{Result, anyhow};
use anyhow::{anyhow, Result};
use bstr::{BStr, ByteSlice};
use regex::Regex;
use tree_sitter::{Language, Parser, Point};
@ -106,66 +106,66 @@ pub fn parse_position_comments(
let node = cursor.node();
// Find every comment node.
if node.kind().to_lowercase().contains("comment")
&& let Ok(text) = node.utf8_text(source)
{
let mut position = node.start_position();
if position.row > 0 {
// Find the arrow character ("^" or "<-") in the comment. A left arrow
// refers to the column where the comment node starts. An up arrow refers
// to its own column.
let mut has_left_caret = false;
let mut has_arrow = false;
let mut negative = false;
let mut arrow_end = 0;
let mut arrow_count = 1;
for (i, c) in text.char_indices() {
arrow_end = i + 1;
if c == '-' && has_left_caret {
has_arrow = true;
break;
if node.kind().to_lowercase().contains("comment") {
if let Ok(text) = node.utf8_text(source) {
let mut position = node.start_position();
if position.row > 0 {
// Find the arrow character ("^" or "<-") in the comment. A left arrow
// refers to the column where the comment node starts. An up arrow refers
// to its own column.
let mut has_left_caret = false;
let mut has_arrow = false;
let mut negative = false;
let mut arrow_end = 0;
let mut arrow_count = 1;
for (i, c) in text.char_indices() {
arrow_end = i + 1;
if c == '-' && has_left_caret {
has_arrow = true;
break;
}
if c == '^' {
has_arrow = true;
position.column += i;
// Continue counting remaining arrows and update their end column
for (_, c) in text[arrow_end..].char_indices() {
if c != '^' {
arrow_end += arrow_count - 1;
break;
}
arrow_count += 1;
}
break;
}
has_left_caret = c == '<';
}
if c == '^' {
has_arrow = true;
position.column += i;
// Continue counting remaining arrows and update their end column
for (_, c) in text[arrow_end..].char_indices() {
if c != '^' {
arrow_end += arrow_count - 1;
// find any ! after arrows but before capture name
if has_arrow {
for (i, c) in text[arrow_end..].char_indices() {
if c == '!' {
negative = true;
arrow_end += i + 1;
break;
} else if !c.is_whitespace() {
break;
}
arrow_count += 1;
}
break;
}
has_left_caret = c == '<';
}
// find any ! after arrows but before capture name
if has_arrow {
for (i, c) in text[arrow_end..].char_indices() {
if c == '!' {
negative = true;
arrow_end += i + 1;
break;
} else if !c.is_whitespace() {
break;
}
}
}
// If the comment node contains an arrow and a highlight name, record the
// highlight name and the position.
if let (true, Some(mat)) =
(has_arrow, CAPTURE_NAME_REGEX.find(&text[arrow_end..]))
{
assertion_ranges.push((node.start_position(), node.end_position()));
result.push(Assertion {
position: to_utf8_point(position, source),
length: arrow_count,
negative,
expected_capture_name: mat.as_str().to_string(),
});
// If the comment node contains an arrow and a highlight name, record the
// highlight name and the position.
if let (true, Some(mat)) =
(has_arrow, CAPTURE_NAME_REGEX.find(&text[arrow_end..]))
{
assertion_ranges.push((node.start_position(), node.end_position()));
result.push(Assertion {
position: to_utf8_point(position, source),
length: arrow_count,
negative,
expected_capture_name: mat.as_str().to_string(),
});
}
}
}
}
@ -219,14 +219,19 @@ pub fn parse_position_comments(
Ok(result)
}
pub fn assert_expected_captures(infos: &[CaptureInfo], assertions: &[Assertion]) -> Result<usize> {
for assertion in assertions {
pub fn assert_expected_captures(
infos: &[CaptureInfo],
path: &Path,
parser: &mut Parser,
language: &Language,
) -> Result<usize> {
let contents = fs::read_to_string(path)?;
let pairs = parse_position_comments(parser, language, contents.as_bytes())?;
for assertion in &pairs {
if let Some(found) = &infos.iter().find(|p| {
let assertion_end = Utf8Point::new(
assertion.position.row,
assertion.position.column + assertion.length - 1,
);
assertion.position >= p.start && assertion_end < p.end
assertion.position >= p.start
&& (assertion.position.row < p.end.row
|| assertion.position.column + assertion.length - 1 < p.end.column)
}) {
if assertion.expected_capture_name != found.name && found.name != "name" {
return Err(anyhow!(
@ -245,24 +250,5 @@ pub fn assert_expected_captures(infos: &[CaptureInfo], assertions: &[Assertion])
));
}
}
Ok(assertions.len())
}
#[cfg(test)]
mod tests {
use super::{Assertion, CaptureInfo, Utf8Point, assert_expected_captures};
#[test]
fn test_assertion_after_multiline_capture_does_not_match() {
let captures = [CaptureInfo {
name: "foo".to_string(),
start: Utf8Point::new(0, 0),
end: Utf8Point::new(1, 1),
}];
let assertions = [Assertion::new(2, 0, 1, false, "foo".to_string())];
let result = assert_expected_captures(&captures, &assertions);
assert!(result.is_err());
}
Ok(pairs.len())
}

View file

@ -2,7 +2,8 @@ use std::{
fs,
io::{self, Write},
path::Path,
sync::{Arc, atomic::AtomicUsize},
str,
sync::{atomic::AtomicUsize, Arc},
time::Instant,
};
@ -48,7 +49,7 @@ pub fn generate_tags(
&mut stdout,
"{indent_str}{:<10}\t | {:<8}\t{} {} - {} `{}`",
str::from_utf8(&source[tag.name_range]).unwrap_or(""),
config.syntax_type_name(tag.syntax_type_id),
&config.syntax_type_name(tag.syntax_type_id),
if tag.is_definition { "def" } else { "ref" },
tag.span.start,
tag.span.end,
@ -58,7 +59,7 @@ pub fn generate_tags(
if docs.len() > 120 {
write!(&mut stdout, "\t{:?}...", docs.get(0..120).unwrap_or(""))?;
} else {
write!(&mut stdout, "\t{docs:?}")?;
write!(&mut stdout, "\t{:?}", &docs)?;
}
}
writeln!(&mut stdout)?;

View file

@ -1,15 +1,13 @@
"""PARSER_DESCRIPTION"""
from importlib.resources import files as _files
from ._binding import language
def _get_query(name, file):
files = globals().get("_files")
if files is None:
from importlib.resources import files
globals()["_files"] = files
try:
query = files(f"{__package__}") / file
query = _files(f"{__package__}") / file
globals()[name] = query.read_text()
except FileNotFoundError:
globals()[name] = None

View file

@ -1,18 +1,25 @@
[package]
authors = [ "PARSER_AUTHOR_NAME PARSER_AUTHOR_EMAIL" ]
autoexamples = false
categories = [ "parser-implementations", "parsing", "text-editors" ]
description = "PARSER_DESCRIPTION"
edition = "2024"
keywords = [ "incremental", "parsing", "tree-sitter", "PARSER_NAME" ]
license = "PARSER_LICENSE"
name = "tree-sitter-PARSER_NAME"
readme = "README.md"
repository = "PARSER_URL"
description = "PARSER_DESCRIPTION"
version = "PARSER_VERSION"
authors = ["PARSER_AUTHOR_NAME PARSER_AUTHOR_EMAIL"]
license = "PARSER_LICENSE"
readme = "README.md"
keywords = ["incremental", "parsing", "tree-sitter", "PARSER_NAME"]
categories = ["parser-implementations", "parsing", "text-editors"]
repository = "PARSER_URL"
edition = "2021"
autoexamples = false
build = "bindings/rust/build.rs"
include = [ "bindings/rust/*", "grammar.js", "queries/*", "src/*", "tree-sitter.json", "/LICENSE" ]
include = [
"bindings/rust/*",
"grammar.js",
"queries/*",
"src/*",
"tree-sitter.json",
"/LICENSE",
]
[lib]
path = "bindings/rust/lib.rs"

View file

@ -11,8 +11,18 @@ fn main() {
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 parser_path = src_dir.join("parser.c");

View file

@ -4,51 +4,46 @@ pub fn build(b: *std.Build) !void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
var threaded: std.Io.Threaded = .init(b.allocator, .{});
defer threaded.deinit();
const io = threaded.io();
const shared = b.option(bool, "build-shared", "Build a shared library") orelse true;
const reuse_alloc = b.option(bool, "reuse-allocator", "Reuse the library allocator") orelse false;
const library_name = "tree-sitter-PARSER_NAME";
var grammar = b.createModule(.{
.target = target,
.optimize = optimize,
.link_libc = true,
.pic = if (shared) true else null,
});
const lib: *std.Build.Step.Compile = b.addLibrary(.{
.name = library_name,
.linkage = if (shared) .dynamic else .static,
.root_module = grammar,
.root_module = b.createModule(.{
.target = target,
.optimize = optimize,
.link_libc = true,
.pic = if (shared) true else null,
}),
});
grammar.addCSourceFile(.{
lib.addCSourceFile(.{
.file = b.path("src/parser.c"),
.flags = &.{"-std=c11"},
});
if (fileExists(b, io, "src/scanner.c")) {
grammar.addCSourceFile(.{
if (fileExists(b, "src/scanner.c")) {
lib.addCSourceFile(.{
.file = b.path("src/scanner.c"),
.flags = &.{"-std=c11"},
});
}
if (reuse_alloc) {
grammar.addCMacro("TREE_SITTER_REUSE_ALLOCATOR", "");
lib.root_module.addCMacro("TREE_SITTER_REUSE_ALLOCATOR", "");
}
if (optimize == .Debug) {
grammar.addCMacro("TREE_SITTER_DEBUG", "");
lib.root_module.addCMacro("TREE_SITTER_DEBUG", "");
}
grammar.addIncludePath(b.path("src"));
lib.addIncludePath(b.path("src"));
b.installArtifact(lib);
b.installFile("src/node-types.json", "node-types.json");
if (fileExists(b, io, "queries")) {
if (fileExists(b, "queries")) {
b.installDirectory(.{
.source_dir = b.path("queries"),
.install_dir = .prefix,
@ -74,10 +69,16 @@ pub fn build(b: *std.Build) !void {
tests.root_module.addImport(library_name, module);
// HACK: fetch tree-sitter dependency only when testing this module
if (b.option(bool, "test", "Fetch test dependencies") orelse false) {
const ts_dep = b.lazyDependency("tree_sitter", .{});
if (ts_dep) |dep|
tests.root_module.addImport("tree-sitter", dep.module("tree_sitter"));
if (b.pkg_hash.len == 0) {
var args = try std.process.argsWithAllocator(b.allocator);
defer args.deinit();
while (args.next()) |a| {
if (std.mem.eql(u8, a, "test")) {
const ts_dep = b.lazyDependency("tree_sitter", .{}) orelse continue;
tests.root_module.addImport("tree-sitter", ts_dep.module("tree-sitter"));
break;
}
}
}
const run_tests = b.addRunArtifact(tests);
@ -85,8 +86,8 @@ pub fn build(b: *std.Build) !void {
test_step.dependOn(&run_tests.step);
}
inline fn fileExists(b: *std.Build, io: std.Io, filename: []const u8) bool {
inline fn fileExists(b: *std.Build, filename: []const u8) bool {
const dir = b.build_root.handle;
dir.access(io, filename, .{}) catch return false;
dir.access(filename, .{}) catch return false;
return true;
}

View file

@ -1,12 +1,11 @@
.{
.name = .tree_sitter_PARSER_NAME,
.fingerprint = PARSER_FINGERPRINT,
.minimum_zig_version = "0.16.0",
.version = "PARSER_VERSION",
.dependencies = .{
.tree_sitter = .{
.url = "git+https://github.com/tree-sitter/zig-tree-sitter#0cf58172e61f6fdd16f681cde42b4acb531a23db",
.hash = "tree_sitter-0.26.0-8heIf3CaAQDeVTQc0DMSBhbQAEx5aF-dTen4_LPxMgrv",
.url = "git+https://github.com/tree-sitter/zig-tree-sitter#b4b72c903e69998fc88e27e154a5e3cc9166551b",
.hash = "tree_sitter-0.25.0-8heIf51vAQConvVIgvm-9mVIbqh7yabZYqPXfOpS3YoG",
.lazy = true,
},
},

View file

@ -17,7 +17,7 @@ endif()
include(GNUInstallDirs)
find_program(TREE_SITTER_CLI tree-sitter DOC "Tree-sitter CLI" REQUIRED)
find_program(TREE_SITTER_CLI tree-sitter DOC "Tree-sitter CLI")
add_custom_command(OUTPUT "${CMAKE_CURRENT_SOURCE_DIR}/src/grammar.json"
"${CMAKE_CURRENT_SOURCE_DIR}/src/node-types.json"

View file

@ -21,7 +21,7 @@ type NodeInfo =
/**
* The tree-sitter language object for this grammar.
*
* @see {@linkcode https://tree-sitter.github.io/node-tree-sitter/interfaces/Language.html Parser.Language}
* @see {@linkcode https://tree-sitter.github.io/node-tree-sitter/interfaces/Parser.Language.html Parser.Language}
*
* @example
* import Parser from "tree-sitter";

View file

@ -20,7 +20,7 @@
use tree_sitter_language::LanguageFn;
unsafe extern "C" {
extern "C" {
fn tree_sitter_PARSER_NAME() -> *const ();
}

View file

@ -1,7 +1,6 @@
LANGUAGE_NAME := tree-sitter-KEBAB_PARSER_NAME
HOMEPAGE_URL := PARSER_URL
VERSION := PARSER_VERSION
DESCRIPTION := PARSER_DESCRIPTION
# repository
SRC_DIR := src

View file

@ -1,11 +1,10 @@
// swift-tools-version:5.6
// swift-tools-version:5.3
import Foundation
import PackageDescription
let dir = Context.packageDirectory
var sources = ["src/parser.c"]
if FileManager.default.fileExists(atPath: "\(dir)/src/scanner.c") {
if FileManager.default.fileExists(atPath: "src/scanner.c") {
sources.append("src/scanner.c")
}
@ -15,7 +14,7 @@ let package = Package(
.library(name: "PARSER_CLASS_NAME", targets: ["PARSER_CLASS_NAME"]),
],
dependencies: [
.package(url: "https://github.com/tree-sitter/swift-tree-sitter", from: "0.10.0"),
.package(name: "SwiftTreeSitter", url: "https://github.com/tree-sitter/swift-tree-sitter", from: "0.9.0"),
],
targets: [
.target(
@ -32,7 +31,7 @@ let package = Package(
.testTarget(
name: "PARSER_CLASS_NAMETests",
dependencies: [
.product(name: "SwiftTreeSitter", package: "swift-tree-sitter"),
"SwiftTreeSitter",
"PARSER_CLASS_NAME",
],
path: "bindings/swift/PARSER_CLASS_NAMETests"

View file

@ -1,29 +1,29 @@
[build-system]
requires = ["setuptools>=62.4.0", "wheel"]
build-backend = "setuptools.build_meta"
requires = [ "setuptools>=62.4.0", "wheel" ]
[project]
authors = [ { email = "PARSER_AUTHOR_EMAIL", name = "PARSER_AUTHOR_NAME" } ]
name = "tree-sitter-PARSER_NAME"
description = "PARSER_DESCRIPTION"
version = "PARSER_VERSION"
keywords = ["incremental", "parsing", "tree-sitter", "PARSER_NAME"]
classifiers = [
"Intended Audience :: Developers",
"Topic :: Software Development :: Compilers",
"Topic :: Text Processing :: Linguistic",
"Typing :: Typed",
]
description = "PARSER_DESCRIPTION"
keywords = [ "incremental", "parsing", "tree-sitter", "PARSER_NAME" ]
license.text = "PARSER_LICENSE"
name = "tree-sitter-PARSER_NAME"
readme = "README.md"
authors = [{ name = "PARSER_AUTHOR_NAME", email = "PARSER_AUTHOR_EMAIL" }]
requires-python = ">=3.10"
version = "PARSER_VERSION"
license.text = "PARSER_LICENSE"
readme = "README.md"
[project.urls]
Funding = "FUNDING_URL"
Homepage = "PARSER_URL"
Funding = "FUNDING_URL"
[project.optional-dependencies]
core = [ "tree-sitter~=0.24" ]
core = ["tree-sitter~=0.24"]
[tool.cibuildwheel]
build = "cp310-*"

View file

@ -32,7 +32,7 @@ class BuildExt(build_ext):
class BdistWheel(bdist_wheel):
def get_tag(self):
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"
return python, abi, platform
@ -42,7 +42,6 @@ class EggInfo(egg_info):
super().find_sources()
self.filelist.recursive_include("queries", "*.scm")
self.filelist.include("src/tree_sitter/*.h")
self.filelist.include("src/*.c")
setup(

File diff suppressed because it is too large Load diff

View file

@ -1,22 +1,22 @@
use std::{fs, path::Path};
use anyhow::{Result, anyhow};
use anyhow::{anyhow, Result};
use tree_sitter::Point;
use tree_sitter_highlight::{Highlight, HighlightConfiguration, HighlightEvent, Highlighter};
use tree_sitter_loader::{Config, Loader};
use crate::{
query_testing::{Assertion, Utf8Point, parse_position_comments, to_utf8_point},
query_testing::{parse_position_comments, to_utf8_point, Assertion, Utf8Point},
test::{TestInfo, TestOutcome, TestResult, TestSummary},
util,
};
#[derive(Debug)]
pub struct Failure {
pub(crate) row: usize,
pub(crate) column: usize,
pub(crate) expected_highlight: String,
pub(crate) actual_highlights: Vec<String>,
row: usize,
column: usize,
expected_highlight: String,
actual_highlights: Vec<String>,
}
impl std::error::Error for Failure {}
@ -120,9 +120,12 @@ pub fn test_highlights(
}
}
if failed { Err(anyhow!("")) } else { Ok(()) }
if failed {
Err(anyhow!(""))
} else {
Ok(())
}
}
pub fn iterate_assertions(
assertions: &[Assertion],
highlights: &[(Utf8Point, Utf8Point, Highlight)],
@ -139,48 +142,49 @@ pub fn iterate_assertions(
expected_capture_name: expected_highlight,
} in assertions
{
// Iterate through all of the highlights that start at or before this assertion's
// position, looking for one that matches the assertion.
actual_highlights.clear();
let mut passed = false;
let end_column = position.column + length - 1;
for highlight in &highlights[i..] {
// The assertions are ordered by position, so skip past all of the highlights that
// end at or before this assertion's position.
let mut end_column = position.column + length - 1;
actual_highlights.clear();
// The assertions are ordered by position, so skip past all of the highlights that
// end at or before this assertion's position.
'highlight_loop: while let Some(highlight) = highlights.get(i) {
if highlight.1 <= *position {
i += 1;
continue;
}
if (highlight.0.row > position.row)
|| (highlight.0.row == position.row && highlight.0.column > end_column)
{
break;
}
// If the highlight matches the assertion, or if the highlight doesn't
// match the assertion but it's negative, this test passes. Otherwise,
// add this highlight to the list of actual highlights that span the
// assertion's position, in order to generate an error message in the event
// of a failure.
let highlight_name = &highlight_names[(highlight.2).0];
if (*highlight_name == *expected_highlight) == *negative {
actual_highlights.push(highlight_name);
} else {
passed = true;
break;
// Iterate through all of the highlights that start at or before this assertion's
// position, looking for one that matches the assertion.
let mut j = i;
while let (false, Some(highlight)) = (passed, highlights.get(j)) {
end_column = position.column + length - 1;
if highlight.0.row >= position.row && highlight.0.column > end_column {
break 'highlight_loop;
}
// If the highlight matches the assertion, or if the highlight doesn't
// match the assertion but it's negative, this test passes. Otherwise,
// add this highlight to the list of actual highlights that span the
// assertion's position, in order to generate an error message in the event
// of a failure.
let highlight_name = &highlight_names[(highlight.2).0];
if (*highlight_name == *expected_highlight) == *negative {
actual_highlights.push(highlight_name);
} else {
passed = true;
break 'highlight_loop;
}
j += 1;
}
}
if !passed {
let mut expected = String::with_capacity(expected_highlight.len() + 1);
if *negative {
expected.push('!');
}
expected.push_str(expected_highlight);
return Err(Failure {
row: position.row,
column: end_column,
expected_highlight: expected,
expected_highlight: expected_highlight.clone(),
actual_highlights: actual_highlights.into_iter().cloned().collect(),
}
.into());
@ -219,11 +223,9 @@ pub fn get_highlight_positions(
let mut highlight_stack = Vec::new();
let source = String::from_utf8_lossy(source);
let mut char_indices = source.char_indices();
for event in
highlighter.highlight(highlight_config, source.as_bytes(), None, None, |string| {
loader.highlight_config_for_injection_string(string)
})?
{
for event in highlighter.highlight(highlight_config, source.as_bytes(), None, |string| {
loader.highlight_config_for_injection_string(string)
})? {
match event? {
HighlightEvent::HighlightStart(h) => highlight_stack.push(h),
HighlightEvent::HighlightEnd => {

View file

@ -1,11 +1,11 @@
use std::{fs, path::Path};
use anyhow::{Result, anyhow};
use anyhow::{anyhow, Result};
use tree_sitter_loader::{Config, Loader};
use tree_sitter_tags::{TagsConfiguration, TagsContext};
use crate::{
query_testing::{Assertion, Utf8Point, parse_position_comments, to_utf8_point},
query_testing::{parse_position_comments, to_utf8_point, Assertion, Utf8Point},
test::{TestInfo, TestOutcome, TestResult, TestSummary},
util,
};
@ -113,7 +113,11 @@ pub fn test_tags(
}
}
if failed { Err(anyhow!("")) } else { Ok(()) }
if failed {
Err(anyhow!(""))
} else {
Ok(())
}
}
pub fn test_tag(

View file

@ -17,12 +17,13 @@ mod tree_test;
#[cfg(feature = "wasm")]
mod wasm_language_test;
use tree_sitter_generate::{GenerateResult, OptLevel};
use tree_sitter_generate::GenerateResult;
pub use crate::fuzz::{
ITERATION_COUNT, allocations,
allocations,
edits::{get_random_edit, invert_edit},
random::Rand,
ITERATION_COUNT,
};
pub use helpers::fixtures::get_language;
@ -30,10 +31,5 @@ pub use helpers::fixtures::get_language;
/// 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.
fn generate_parser(grammar_json: &str) -> GenerateResult<(String, String)> {
tree_sitter_generate::generate_parser_for_grammar(
grammar_json,
Some((0, 0, 0)),
OptLevel::default(),
&mut Vec::new(),
)
tree_sitter_generate::generate_parser_for_grammar(grammar_json, Some((0, 0, 0)))
}

View file

@ -2,25 +2,24 @@ use std::{collections::HashMap, env, fs};
use anyhow::Context;
use tree_sitter::Parser;
use tree_sitter_generate::OptLevel;
use tree_sitter_proc_macro::test_with_seed;
use crate::{
fuzz::{
EDIT_COUNT, EXAMPLE_EXCLUDE, EXAMPLE_INCLUDE, ITERATION_COUNT, LANGUAGE_FILTER,
LOG_GRAPH_ENABLED, START_SEED,
corpus_test::{
check_changed_ranges, check_consistent_sizes, get_parser, set_included_ranges,
},
edits::{get_random_edit, invert_edit},
flatten_tests, new_seed,
random::Rand,
EDIT_COUNT, EXAMPLE_EXCLUDE, EXAMPLE_INCLUDE, ITERATION_COUNT, LANGUAGE_FILTER,
LOG_GRAPH_ENABLED, START_SEED,
},
parse::perform_edit,
test::{DiffKey, TestDiff, parse_tests, render_test_output},
test::{parse_tests, strip_sexp_fields, DiffKey, TestDiff},
tests::{
allocations,
helpers::fixtures::{SCRATCH_BASE_DIR, fixtures_dir, get_language, get_test_language},
helpers::fixtures::{fixtures_dir, get_language, get_test_language, SCRATCH_BASE_DIR},
},
};
@ -121,10 +120,10 @@ pub fn test_language_corpus(
skipped: Option<&[&str]>,
language_dir: Option<&str>,
) {
if let Some(filter) = LANGUAGE_FILTER.as_ref()
&& language_name != filter
{
return;
if let Some(filter) = LANGUAGE_FILTER.as_ref() {
if language_name != filter {
return;
}
}
let language_dir = language_dir.unwrap_or_default();
@ -186,17 +185,38 @@ pub fn test_language_corpus(
println!();
for (test_index, test) in tests.iter().enumerate() {
let test_name = format!("{language_name} - {}", test.name);
if let Some(skipped) = skipped.as_mut()
&& let Some(counter) = skipped.get_mut(test_name.as_str())
{
println!(" {test_index}. {test_name} - SKIPPED");
*counter += 1;
continue;
if let Some(skipped) = skipped.as_mut() {
if let Some(counter) = skipped.get_mut(test_name.as_str()) {
println!(" {test_index}. {test_name} - SKIPPED");
*counter += 1;
continue;
}
}
println!(" {test_index}. {test_name}");
let passed = allocations::record(|| test.check_initial_parse(&language, &test_name, true));
let passed = allocations::record(|| {
let mut log_session = None;
let mut parser = get_parser(&mut log_session, "log.html");
parser.set_language(&language).unwrap();
set_included_ranges(&mut parser, &test.input, test.template_delimiters);
let tree = parser.parse(&test.input, None).unwrap();
let mut actual_output = tree.root_node().to_sexp();
if !test.has_fields {
actual_output = strip_sexp_fields(&actual_output);
}
if actual_output != test.output {
println!("Incorrect initial parse for {test_name}");
DiffKey::print();
println!("{}", TestDiff::new(&actual_output, &test.output));
println!();
return false;
}
true
});
if !passed {
failure_count += 1;
@ -254,9 +274,7 @@ pub fn test_language_corpus(
// Check that the new tree is consistent.
check_consistent_sizes(&tree2, &input);
if let Err(message) = check_changed_ranges(&tree, &tree2, &input) {
println!(
"\nUnexpected scope change in seed {seed} with start seed {start_seed}\n{message}\n\n",
);
println!("\nUnexpected scope change in seed {seed} with start seed {start_seed}\n{message}\n\n",);
return false;
}
@ -272,8 +290,10 @@ pub fn test_language_corpus(
let tree3 = parser.parse(&input, Some(&tree2)).unwrap();
// Verify that the final tree matches the expectation from the corpus.
let actual_output =
render_test_output(&input, &tree3, test.cst, test.has_fields).unwrap();
let mut actual_output = tree3.root_node().to_sexp();
if !test.has_fields {
actual_output = strip_sexp_fields(&actual_output);
}
if actual_output != test.output {
println!("Incorrect parse for {test_name} - seed {seed}");
@ -286,9 +306,7 @@ pub fn test_language_corpus(
// Check that the edited tree is consistent.
check_consistent_sizes(&tree3, &input);
if let Err(message) = check_changed_ranges(&tree2, &tree3, &input) {
println!(
"Unexpected scope change in seed {seed} with start seed {start_seed}\n{message}\n\n"
);
println!("Unexpected scope change in seed {seed} with start seed {start_seed}\n{message}\n\n");
return false;
}
@ -333,10 +351,10 @@ fn test_feature_corpus_files() {
let language_name = entry.file_name();
let language_name = language_name.to_str().unwrap();
if let Some(filter) = LANGUAGE_FILTER.as_ref()
&& language_name != filter
{
continue;
if let Some(filter) = LANGUAGE_FILTER.as_ref() {
if language_name != filter {
continue;
}
}
let test_path = entry.path();
@ -353,12 +371,8 @@ fn test_feature_corpus_files() {
)
})
.unwrap();
let generate_result = tree_sitter_generate::generate_parser_for_grammar(
&grammar_json,
Some((0, 0, 0)),
OptLevel::default(),
&mut Vec::new(),
);
let generate_result =
tree_sitter_generate::generate_parser_for_grammar(&grammar_json, Some((0, 0, 0)));
if error_message_path.exists() {
if EXAMPLE_INCLUDE.is_some() || EXAMPLE_EXCLUDE.is_some() {
@ -379,12 +393,12 @@ fn test_feature_corpus_files() {
failure_count += 1;
}
} else {
eprintln!("Expected error message but got none for test grammar '{language_name}'");
eprintln!("Expected error message but got none for test grammar '{language_name}'",);
failure_count += 1;
}
} else {
if let Err(e) = &generate_result {
eprintln!("Unexpected error for test grammar '{language_name}':\n{e}");
eprintln!("Unexpected error for test grammar '{language_name}':\n{e}",);
failure_count += 1;
continue;
}
@ -402,8 +416,24 @@ fn test_feature_corpus_files() {
for test in tests {
eprintln!(" example: {:?}", test.name);
let passed =
allocations::record(|| test.check_initial_parse(&language, &test.name, true));
let passed = allocations::record(|| {
let mut log_session = None;
let mut parser = get_parser(&mut log_session, "log.html");
parser.set_language(&language).unwrap();
let tree = parser.parse(&test.input, None).unwrap();
let mut actual_output = tree.root_node().to_sexp();
if !test.has_fields {
actual_output = strip_sexp_fields(&actual_output);
}
if actual_output == test.output {
true
} else {
DiffKey::print();
print!("{}", TestDiff::new(&actual_output, &test.output));
println!();
false
}
});
if !passed {
failure_count += 1;

View file

@ -127,52 +127,6 @@ fn detect_language_by_double_barrel_file_extension() {
);
}
#[test]
fn detect_language_with_dots_in_filename() {
let blade_dir = tree_sitter_dir(
r#"{
"grammars": [
{
"name": "blade_dots",
"path": ".",
"scope": "source.blade",
"file-types": [
"blade.php"
]
},
{
"name": "php_dots",
"path": ".",
"scope": "source.php",
"file-types": [
"php"
]
}
],
"metadata": {
"version": "0.0.1"
}
}
"#,
"blade_dots",
);
let mut loader = Loader::with_parser_lib_path(scratch_dir().to_path_buf());
let config = loader
.find_language_configurations_at_path(blade_dir.path(), false)
.unwrap();
// this is just to validate that we can read the tree-sitter.json correctly
assert_eq!(config[0].scope.as_ref().unwrap(), "source.blade");
let file_name = blade_dir.path().join("foo.bar.baz.blade.php");
fs::write(&file_name, "").unwrap();
assert_eq!(
get_lang_scope(&loader, &file_name),
Some("source.blade".into())
);
}
#[test]
fn detect_language_without_filename() {
let gitignore_dir = tree_sitter_dir(

View file

@ -1,4 +1,4 @@
use std::ops::Range;
use std::{ops::Range, str};
#[derive(Debug)]
pub struct ReadRecorder<'a> {
@ -20,7 +20,7 @@ impl<'a> ReadRecorder<'a> {
if let Err(i) = self.indices_read.binary_search(&offset) {
self.indices_read.insert(i, offset);
}
&self.content[offset..=offset]
&self.content[offset..(offset + 1)]
} else {
&[]
}
@ -30,7 +30,7 @@ impl<'a> ReadRecorder<'a> {
let mut result = Vec::new();
let mut last_range = Option::<Range<usize>>::None;
for index in &self.indices_read {
if let Some(range) = &mut last_range {
if let Some(ref mut range) = &mut last_range {
if range.end == *index {
range.end += 1;
} else {

View file

@ -1,13 +1,12 @@
use std::{
collections::HashSet,
env, fs,
path::{Path, PathBuf},
sync::{LazyLock, Mutex},
sync::LazyLock,
};
use anyhow::Context;
use tree_sitter::Language;
use tree_sitter_generate::{ALLOC_HEADER, ARRAY_HEADER, load_grammar_file};
use tree_sitter_generate::{load_grammar_file, ALLOC_HEADER, ARRAY_HEADER};
use tree_sitter_highlight::HighlightConfiguration;
use tree_sitter_loader::{CompileConfig, Loader};
use tree_sitter_tags::TagsConfiguration;
@ -24,10 +23,6 @@ static TEST_LOADER: LazyLock<Loader> = LazyLock::new(|| {
loader
});
// Prevents parallel tests from racing on the same per-grammar
// `src_dir/tree_sitter/` and observing a half-rewritten header.
static WRITTEN_HEADER_DIRS: LazyLock<Mutex<HashSet<PathBuf>>> = LazyLock::new(Default::default);
#[cfg(feature = "wasm")]
pub static ENGINE: LazyLock<tree_sitter::wasmtime::Engine> = LazyLock::new(Default::default);
@ -139,22 +134,17 @@ fn get_test_language_internal(
};
let header_path = src_dir.join("tree_sitter");
if WRITTEN_HEADER_DIRS
.lock()
.unwrap()
.insert(header_path.clone())
{
fs::create_dir_all(&header_path).unwrap();
for (file, content) in [
("alloc.h", ALLOC_HEADER),
("array.h", ARRAY_HEADER),
("parser.h", tree_sitter::PARSER_HEADER),
] {
let path = header_path.join(file);
fs::write(&path, content)
.with_context(|| format!("Failed to write {}", path.display()))
.unwrap();
}
fs::create_dir_all(&header_path).unwrap();
for (file, content) in [
("alloc.h", ALLOC_HEADER),
("array.h", ARRAY_HEADER),
("parser.h", tree_sitter::PARSER_HEADER),
] {
let file = header_path.join(file);
fs::write(&file, content)
.with_context(|| format!("Failed to write {:?}", file.file_name().unwrap()))
.unwrap();
}
let paths_to_check = if let Some(scanner_path) = &scanner_path {

View file

@ -1,16 +1,16 @@
use std::{cmp::Ordering, fmt::Write, ops::Range};
use rand::{Rng, RngExt};
use rand::prelude::Rng;
use streaming_iterator::{IntoStreamingIterator, StreamingIterator};
use tree_sitter::{
Language, Node, Parser, Point, Query, QueryCapture, QueryCursor, QueryMatch, Tree, TreeCursor,
};
#[derive(Debug)]
pub struct Pattern<'a> {
kind: Option<&'a str>,
pub struct Pattern {
kind: Option<&'static str>,
named: bool,
field: Option<&'a str>,
field: Option<&'static str>,
capture: Option<String>,
children: Vec<Self>,
}
@ -25,17 +25,17 @@ const CAPTURE_NAMES: &[&str] = &[
"one", "two", "three", "four", "five", "six", "seven", "eight",
];
impl<'a> Pattern<'a> {
pub fn random_pattern_in_tree(tree: &'a Tree, rng: &mut impl Rng) -> (Self, Range<Point>) {
impl Pattern {
pub fn random_pattern_in_tree(tree: &Tree, rng: &mut impl Rng) -> (Self, Range<Point>) {
let mut cursor = tree.walk();
// Descend to the node at a random byte offset and depth.
let mut max_depth = 0;
let byte_offset = rng.random_range(0..cursor.node().end_byte());
let byte_offset = rng.gen_range(0..cursor.node().end_byte());
while cursor.goto_first_child_for_byte(byte_offset).is_some() {
max_depth += 1;
}
let depth = rng.random_range(0..=max_depth);
let depth = rng.gen_range(0..=max_depth);
for _ in 0..depth {
cursor.goto_parent();
}
@ -45,7 +45,7 @@ impl<'a> Pattern<'a> {
let pattern_start = cursor.node().start_position();
let mut roots = vec![Self::random_pattern_for_node(&mut cursor, rng)];
while roots.len() < 5 && cursor.goto_next_sibling() {
if rng.random_bool(0.2) {
if rng.gen_bool(0.2) {
roots.push(Self::random_pattern_for_node(&mut cursor, rng));
}
}
@ -75,26 +75,26 @@ impl<'a> Pattern<'a> {
(pattern, pattern_start..pattern_end)
}
fn random_pattern_for_node(cursor: &mut TreeCursor<'a>, rng: &mut impl Rng) -> Self {
fn random_pattern_for_node(cursor: &mut TreeCursor, rng: &mut impl Rng) -> Self {
let node = cursor.node();
// Sometimes specify the node's type, sometimes use a wildcard.
let (kind, named) = if rng.random_bool(0.9) {
let (kind, named) = if rng.gen_bool(0.9) {
(Some(node.kind()), node.is_named())
} else {
(Some("_"), node.is_named() && rng.random_bool(0.8))
(Some("_"), node.is_named() && rng.gen_bool(0.8))
};
// Sometimes specify the node's field.
let field = if rng.random_bool(0.75) {
let field = if rng.gen_bool(0.75) {
cursor.field_name()
} else {
None
};
// Sometimes capture the node.
let capture = if rng.random_bool(0.7) {
Some(CAPTURE_NAMES[rng.random_range(0..CAPTURE_NAMES.len())].to_string())
let capture = if rng.gen_bool(0.7) {
Some(CAPTURE_NAMES[rng.gen_range(0..CAPTURE_NAMES.len())].to_string())
} else {
None
};
@ -102,9 +102,9 @@ impl<'a> Pattern<'a> {
// Walk the children and include child patterns for some of them.
let mut children = Vec::new();
if named && cursor.goto_first_child() {
let max_children = rng.random_range(0..4);
let max_children = rng.gen_range(0..4);
while cursor.goto_next_sibling() {
if rng.random_bool(0.6) {
if rng.gen_bool(0.6) {
let child_ast = Self::random_pattern_for_node(cursor, rng);
children.push(child_ast);
if children.len() >= max_children {
@ -204,10 +204,10 @@ impl<'a> Pattern<'a> {
}
// If a field is specified, check that it matches the node.
if let Some(field) = self.field
&& cursor.field_name() != Some(field)
{
return Vec::new();
if let Some(field) = self.field {
if cursor.field_name() != Some(field) {
return Vec::new();
}
}
// Create a match for the current node.
@ -225,7 +225,7 @@ impl<'a> Pattern<'a> {
}
// 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() {
let mut match_states = vec![(0, mat)];
loop {
@ -268,7 +268,7 @@ impl<'a> Pattern<'a> {
}
}
impl std::fmt::Display for Pattern<'_> {
impl std::fmt::Display for Pattern {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut result = String::new();
self.write_to_string(&mut result, 0);
@ -336,7 +336,7 @@ pub fn collect_matches<'a>(
while let Some(m) = matches.next() {
result.push((
m.pattern_index,
format_captures(m.captures().iter().into_streaming_iter_ref(), query, source),
format_captures(m.captures.iter().into_streaming_iter_ref(), query, source),
));
}
result
@ -347,7 +347,7 @@ pub fn collect_captures<'a>(
query: &'a Query,
source: &'a str,
) -> Vec<(&'a str, &'a str)> {
format_captures(captures.map(|(m, i)| m.captures()[*i]), query, source)
format_captures(captures.map(|(m, i)| m.captures[*i]), query, source)
}
fn format_captures<'a>(

View file

@ -2,15 +2,15 @@ use std::{
ffi::CString,
fs,
os::raw::c_char,
ptr, slice,
ptr, slice, str,
sync::{
LazyLock,
atomic::{AtomicUsize, Ordering},
LazyLock,
},
};
use tree_sitter_highlight::{
Error, Highlight, HighlightConfiguration, HighlightEvent, Highlighter, HtmlRenderer, c,
c, Error, Highlight, HighlightConfiguration, HighlightEvent, Highlighter, HtmlRenderer,
};
use super::helpers::fixtures::{get_highlight_config, get_language, get_language_queries_path};
@ -485,7 +485,6 @@ fn test_highlighting_cancellation() {
.highlight(
&HTML_HIGHLIGHT,
source.as_bytes(),
None,
Some(&cancellation_flag),
injection_callback,
)
@ -496,7 +495,7 @@ fn test_highlighting_cancellation() {
let found_cancellation_error = events.any(|event| match event {
Ok(_) => false,
Err(Error::Cancelled) => true,
Err(Error::InvalidLanguage(_) | Error::Unknown) => {
Err(Error::InvalidLanguage | Error::Unknown) => {
unreachable!("Unexpected error type while iterating events")
}
});
@ -728,7 +727,6 @@ fn to_html<'a>(
language_config,
src,
None,
None,
&test_language_for_injection_string,
)?;
@ -749,10 +747,7 @@ fn to_html<'a>(
.collect())
}
#[expect(
clippy::type_complexity,
reason = "return type represents structured highlight tokens"
)]
#[allow(clippy::type_complexity)]
fn to_token_vector<'a>(
src: &'a str,
language_config: &'a HighlightConfiguration,
@ -766,7 +761,6 @@ fn to_token_vector<'a>(
language_config,
src,
None,
None,
&test_language_for_injection_string,
)?;
for event in events {

View file

@ -31,17 +31,14 @@ fn test_lookahead_iterator() {
let mut lookahead = language.lookahead_iterator(next_state).unwrap();
assert_eq!(*lookahead.language(), language);
assert!(lookahead.iter_names().eq(expected_symbols));
assert_eq!(lookahead.iter_names().count(), 0);
assert!(lookahead.reset_state(next_state));
lookahead.reset_state(next_state);
assert!(lookahead.iter_names().eq(expected_symbols));
assert!(lookahead.reset(&language, next_state));
assert!(
lookahead
.map(|s| language.node_kind_for_id(s).unwrap())
.eq(expected_symbols)
);
lookahead.reset(&language, next_state);
assert!(lookahead
.map(|s| language.node_kind_for_id(s).unwrap())
.eq(expected_symbols));
}
#[test]
@ -67,33 +64,6 @@ fn test_lookahead_iterator_modifiable_only_by_mut() {
let _ = names.next();
}
#[test]
fn test_lookahead_iterator_exhaustion() {
let language = get_language("json");
for state in 0..language.parse_state_count() {
let state = u16::try_from(state).unwrap();
let mut lookahead = language.lookahead_iterator(state).unwrap();
// A fresh iterator is not positioned on a symbol.
assert_eq!(lookahead.current_symbol(), None);
assert_eq!(lookahead.current_symbol_name(), None);
let count = lookahead.by_ref().count();
// An exhausted iterator is not positioned on a symbol, and stays exhausted.
assert_eq!(lookahead.current_symbol(), None);
assert_eq!(lookahead.current_symbol_name(), None);
assert_eq!(lookahead.by_ref().count(), 0);
assert_eq!(lookahead.iter_names().count(), 0);
// Resetting restores it exactly.
assert!(lookahead.reset_state(state));
assert_eq!(lookahead.current_symbol(), None);
assert_eq!(lookahead.by_ref().count(), count);
}
}
#[test]
fn test_symbol_metadata_checks() {
let language = get_language("rust");
@ -140,7 +110,7 @@ fn test_supertypes() {
supertypes
.iter()
.filter_map(|&s| language.node_kind_for_id(s))
.map(std::string::ToString::to_string)
.map(|s| s.to_string())
.collect::<Vec<String>>(),
vec![
"_expression",

View file

@ -2,8 +2,9 @@ use tree_sitter::{InputEdit, Node, Parser, Point, Tree};
use tree_sitter_generate::load_grammar_file;
use super::{
Rand, get_random_edit,
get_random_edit,
helpers::fixtures::{fixtures_dir, get_language, get_test_language},
Rand,
};
use crate::{
parse::perform_edit,
@ -285,10 +286,7 @@ fn test_parent_of_zero_width_node() {
assert_eq!(block.to_string(), "(block)");
assert_eq!(block_parent.kind(), "function_definition");
assert_eq!(
block_parent.to_string(),
"(function_definition name: (identifier) parameters: (parameters (identifier)) body: (block))"
);
assert_eq!(block_parent.to_string(), "(function_definition name: (identifier) parameters: (parameters (identifier)) body: (block))");
assert_eq!(
root.child_with_descendant(block).unwrap(),
@ -454,7 +452,7 @@ fn test_node_child_by_field_name_with_extra_hidden_children() {
// In the Python grammar, some fields are applied to `suite` nodes,
// which consist of an invisible `indent` token followed by a block.
// Check that when searching for a child with a field name, we don't
// return a hidden child node.
//
let tree = parser.parse("while a:\n pass", None).unwrap();
let while_node = tree.root_node().child(0).unwrap();
assert_eq!(while_node.kind(), "while_statement");
@ -950,13 +948,6 @@ fn test_node_sexp() {
#[test]
fn test_node_field_names() {
// - "x":
// This isn't used in the test, but prevents `_hidden_rule1` from being eliminated as a
// unit reduction.
// - "_hidden_rule1":
// Fields pointing to hidden nodes with a single child resolve to the child.
// - "_hidden_rule2":
// Fields within hidden nodes can be referenced through the parent node.
let (parser_name, parser_code) = generate_parser(
r#"
{
@ -979,6 +970,8 @@ fn test_node_field_names() {
{"type": "STRING", "value": "child-1"},
{"type": "BLANK"},
// This isn't used in the test, but prevents `_hidden_rule1`
// from being eliminated as a unit reduction.
{
"type": "ALIAS",
"value": "x",
@ -999,6 +992,7 @@ fn test_node_field_names() {
]
},
// Fields pointing to hidden nodes with a single child resolve to the child.
"_hidden_rule1": {
"type": "CHOICE",
"members": [
@ -1007,6 +1001,7 @@ fn test_node_field_names() {
]
},
// Fields within hidden nodes can be referenced through the parent node.
"_hidden_rule2": {
"type": "SEQ",
"members": [

View file

@ -66,13 +66,9 @@ fn test_parsing_with_logging() {
parser.set_language(&get_language("rust")).unwrap();
let mut messages = Vec::new();
// SAFETY: the logger borrows `messages` and is only invoked during the
// `parse` call below while `messages` is in scope.
unsafe {
parser.set_logger_unchecked(Some(Box::new(|log_type, message| {
messages.push((log_type, message.to_string()));
})));
}
parser.set_logger(Some(Box::new(|log_type, message| {
messages.push((log_type, message.to_string()));
})));
parser
.parse(
@ -253,79 +249,6 @@ fn test_parsing_with_custom_utf16_be_input() {
assert_eq!(root.child(0).unwrap().kind(), "function_item");
}
#[test]
fn test_utf16_decodes_surrogate_pairs() {
let mut parser = Parser::new();
let language = get_test_fixture_language("utf16_surrogate_oob");
parser.set_language(&language).unwrap();
let le = [0xD83D_u16.to_le(), 0xDE00_u16.to_le()];
let tree = parser.parse_utf16_le(le, None).unwrap();
assert_eq!(tree.root_node().to_sexp(), "(program (supplementary))");
let be = [0xD83D_u16.to_be(), 0xDE00_u16.to_be()];
let tree = parser.parse_utf16_be(be, None).unwrap();
assert_eq!(tree.root_node().to_sexp(), "(program (supplementary))");
}
#[test]
fn test_utf16_decode_does_not_read_oob() {
// Test for a buffer over-read in ts_decode_utf16_le/be when a lead surrogate
// is the last code unit in a chunk. The test grammar's external scanner
// distinguishes surrogate code points from supplementary-plane characters,
// making the over-read directly observable in the parse tree.
//
// Buffer layout:
// buf[0] = 0xD83E (lead surrogate)
// buf[1] = 0xDD8B (POISON: fake trail surrogate, adjacent in memory)
//
// The callback returns only buf[0..1] (one code unit = 2 bytes).
//
// When functioning correctly, this test passes a length of 2 bytes, which is
// interpreted as 2/2 = 1 code unit, and thus doesn't over-read into the "poison"
// fake trail surrogate. If an over-read does occur, the scanner sees a
// supplementary token.
let mut parser = Parser::new();
let language = get_test_fixture_language("utf16_surrogate_oob");
parser.set_language(&language).unwrap();
let buf = vec![
0xD83E, // lead surrogate (the only "visible" code unit)
0xDD8B, // POISON: adjacent in Vec memory, past the chunk
];
assert_eq!("🦋", String::from_utf16(&buf).unwrap());
let mut callback = |offset: usize, _position: Point| -> &[u16] {
// only expose buf[0], never buf[1]
if offset >= 1 {
return [].as_slice();
}
&buf[0..1]
};
// Use the parse function matching the host endianness, since the
// buffer contains native u16 values.
#[cfg(target_endian = "little")]
let tree = parser
.parse_utf16_le_with_options(&mut callback, None, None)
.unwrap();
#[cfg(target_endian = "big")]
let tree = parser
.parse_utf16_be_with_options(&mut callback, None, None)
.unwrap();
let root = tree.root_node();
// Correct: scanner sees raw surrogate (0xD83E) -> `surrogate` node
// Incorrect: scanner sees supplementary (U+1F98B, aka 🦋) -> `supplementary` node
assert_eq!(
root.to_sexp(),
"(program (surrogate))",
"buffer over-read: decoder read past chunk boundary and formed a \
supplementary character from OOB adjacent memory"
);
}
#[test]
fn test_parsing_with_callback_returning_owned_strings() {
let mut parser = Parser::new();
@ -822,7 +745,11 @@ fn test_parsing_cancelled_by_another_thread() {
&mut |offset, _| {
thread::yield_now();
thread::sleep(time::Duration::from_millis(10));
if offset == 0 { b" [" } else { b"0," }
if offset == 0 {
b" ["
} else {
b"0,"
}
},
None,
Some(ParseOptions::new().progress_callback(callback)),
@ -845,7 +772,11 @@ fn test_parsing_with_a_timeout() {
let start_time = time::Instant::now();
let tree = parser.parse_with_options(
&mut |offset, _| {
if offset == 0 { b" [" } else { b",0" }
if offset == 0 {
b" ["
} else {
b",0"
}
},
None,
Some(ParseOptions::new().progress_callback(&mut |_| {
@ -863,7 +794,11 @@ fn test_parsing_with_a_timeout() {
let start_time = time::Instant::now();
let tree = parser.parse_with_options(
&mut |offset, _| {
if offset == 0 { b" [" } else { b",0" }
if offset == 0 {
b" ["
} else {
b",0"
}
},
None,
Some(ParseOptions::new().progress_callback(&mut |_| {
@ -1060,9 +995,9 @@ fn test_parsing_with_timeout_during_balancing() {
let mut parser = Parser::new();
parser.set_language(&get_language("javascript")).unwrap();
let function_count: u32 = 100;
let function_count = 100;
let code = "function() {}\n".repeat(function_count as usize);
let code = "function() {}\n".repeat(function_count);
let mut current_byte_offset = 0;
let mut in_balancing = false;
let tree = parser.parse_with_options(
@ -1136,7 +1071,7 @@ fn test_parsing_with_timeout_during_balancing() {
Some(ParseOptions::new().progress_callback(&mut |state| {
// Because we've already finished parsing, we should only be resuming the
// balancing phase.
assert_eq!(state.current_byte_offset(), current_byte_offset);
assert!(state.current_byte_offset() == current_byte_offset);
ControlFlow::Continue(())
})),
)
@ -1804,15 +1739,11 @@ fn test_parsing_with_scanner_logging() {
.unwrap();
let mut found = false;
// SAFETY: the logger borrows `found` and is only invoked during the `parse`
// call below, while `found` is in scope.
unsafe {
parser.set_logger_unchecked(Some(Box::new(|log_type, message| {
if log_type == LogType::Lex && message == "Found a percent string" {
found = true;
}
})));
}
parser.set_logger(Some(Box::new(|log_type, message| {
if log_type == LogType::Lex && message == "Found a percent string" {
found = true;
}
})));
let source_code = "x + %(sup (external) scanner?)";
@ -1932,7 +1863,7 @@ fn test_decode_cp1252() {
fn decode(bytes: &[u8]) -> (i32, u32) {
if !bytes.is_empty() {
let byte = bytes[0];
(i32::from(byte), 1)
(byte as i32, 1)
} else {
(0, 0)
}
@ -1968,7 +1899,7 @@ fn test_decode_macintosh() {
fn decode(bytes: &[u8]) -> (i32, u32) {
if !bytes.is_empty() {
let byte = bytes[0];
(i32::from(byte), 1)
(byte as i32, 1)
} else {
(0, 0)
}
@ -2030,9 +1961,8 @@ fn test_decode_utf24le() {
#[test]
fn test_grammars_that_should_not_compile() {
assert!(
generate_parser(
r#"
assert!(generate_parser(
r#"
{
"name": "issue_1111",
"rules": {
@ -2040,13 +1970,11 @@ fn test_grammars_that_should_not_compile() {
},
}
"#
)
.is_err()
);
)
.is_err());
assert!(
generate_parser(
r#"
assert!(generate_parser(
r#"
{
"name": "issue_1271",
"rules": {
@ -2061,13 +1989,11 @@ fn test_grammars_that_should_not_compile() {
},
}
"#
)
.is_err()
);
)
.is_err());
assert!(
generate_parser(
r#"
assert!(generate_parser(
r#"
{
"name": "issue_1156_expl_1",
"rules": {
@ -2081,13 +2007,11 @@ fn test_grammars_that_should_not_compile() {
},
}
"#
)
.is_err()
);
)
.is_err());
assert!(
generate_parser(
r#"
assert!(generate_parser(
r#"
{
"name": "issue_1156_expl_2",
"rules": {
@ -2104,13 +2028,11 @@ fn test_grammars_that_should_not_compile() {
},
}
"#
)
.is_err()
);
)
.is_err());
assert!(
generate_parser(
r#"
assert!(generate_parser(
r#"
{
"name": "issue_1156_expl_3",
"rules": {
@ -2124,13 +2046,11 @@ fn test_grammars_that_should_not_compile() {
},
}
"#
)
.is_err()
);
)
.is_err());
assert!(
generate_parser(
r#"
assert!(generate_parser(
r#"
{
"name": "issue_1156_expl_4",
"rules": {
@ -2147,9 +2067,8 @@ fn test_grammars_that_should_not_compile() {
},
}
"#
)
.is_err()
);
)
.is_err());
}
const fn simple_range(start: usize, end: usize) -> Range {

View file

@ -14,4 +14,4 @@ proc-macro = true
[dependencies]
proc-macro2 = "1.0.93"
quote = "1.0.38"
syn = { features = [ "full" ], version = "2.0.96" }
syn = { version = "2.0.96", features = ["full"] }

View file

@ -2,9 +2,8 @@ use proc_macro::TokenStream;
use proc_macro2::Span;
use quote::quote;
use syn::{
Error, Expr, Ident, ItemFn, LitInt, Token,
parse::{Parse, ParseStream},
parse_macro_input,
parse_macro_input, Error, Expr, Ident, ItemFn, LitInt, Token,
};
#[proc_macro_attribute]
@ -69,7 +68,7 @@ pub fn test_with_seed(args: TokenStream, input: TokenStream) -> TokenStream {
return Err(Error::new(
name.span(),
format!("Unsupported parameter `{x}`"),
));
))
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,11 +1,11 @@
use std::{
ffi::{CStr, CString},
fs, ptr, slice,
fs, ptr, slice, str,
sync::atomic::{AtomicUsize, Ordering},
};
use tree_sitter::Point;
use tree_sitter_tags::{Error, TagsConfiguration, TagsContext, c_lib as c};
use tree_sitter_tags::{c_lib as c, Error, TagsConfiguration, TagsContext};
use super::helpers::{
allocations,

View file

@ -3,8 +3,8 @@ use tree_sitter_highlight::{Highlight, Highlighter};
use super::helpers::fixtures::{get_highlight_config, get_language, test_loader};
use crate::{
query_testing::{Assertion, Utf8Point, parse_position_comments},
test_highlight::{Failure, get_highlight_positions, iterate_assertions},
query_testing::{parse_position_comments, Assertion, Utf8Point},
test_highlight::get_highlight_positions,
};
#[test]
@ -68,195 +68,3 @@ fn test_highlight_test_with_basic_test() {
]
);
}
#[test]
fn test_assertion_with_non_matching_highlight_at_same_position() {
// Test that an assertion fails when the highlight at the position does not match
let highlight_names = vec!["keyword".to_string(), "variable".to_string()];
let assertions = vec![Assertion::new(1, 0, 1, false, String::from("keyword"))];
let highlights = vec![
(Utf8Point::new(1, 0), Utf8Point::new(1, 5), Highlight(1)), // "variable" highlight
];
let result = iterate_assertions(&assertions, &highlights, &highlight_names);
assert!(result.is_err());
let err = result.unwrap_err().downcast::<Failure>().unwrap();
assert_eq!(err.row, 1);
assert_eq!(err.column, 0);
assert_eq!(err.expected_highlight, "keyword");
assert_eq!(err.actual_highlights, vec!["variable".to_string()]);
}
#[test]
fn test_assertion_with_exact_matching_highlight() {
// Test exact match: assertion and highlight have same start and end
let highlight_names = vec!["keyword".to_string()];
let assertions = vec![Assertion::new(0, 5, 3, false, String::from("keyword"))];
let highlights = vec![(Utf8Point::new(0, 5), Utf8Point::new(0, 8), Highlight(0))];
let result = iterate_assertions(&assertions, &highlights, &highlight_names);
assert!(result.is_ok());
assert_eq!(result.unwrap(), 1);
}
#[test]
fn test_assertion_contained_within_highlight() {
// Test where assertion is fully contained within a larger highlight
let highlight_names = vec!["keyword".to_string()];
let assertions = vec![Assertion::new(0, 3, 2, false, String::from("keyword"))];
let highlights = vec![(Utf8Point::new(0, 0), Utf8Point::new(0, 10), Highlight(0))];
let result = iterate_assertions(&assertions, &highlights, &highlight_names);
assert!(result.is_ok());
assert_eq!(result.unwrap(), 1);
}
#[test]
fn test_assertion_overlapping_highlight_start() {
// Test where assertion starts before highlight but overlaps with it
let highlight_names = vec!["keyword".to_string()];
let assertions = vec![Assertion::new(0, 3, 4, false, String::from("keyword"))];
let highlights = vec![(Utf8Point::new(0, 5), Utf8Point::new(0, 10), Highlight(0))];
let result = iterate_assertions(&assertions, &highlights, &highlight_names);
assert!(result.is_ok());
assert_eq!(result.unwrap(), 1);
}
#[test]
fn test_assertion_with_no_highlights() {
// Test that an assertion fails when there are no highlights at all
let highlight_names = vec!["keyword".to_string()];
let assertions = vec![Assertion::new(0, 0, 1, false, String::from("keyword"))];
let highlights = vec![];
let result = iterate_assertions(&assertions, &highlights, &highlight_names);
assert!(result.is_err());
let err = result.unwrap_err().downcast::<Failure>().unwrap();
assert_eq!(err.row, 0);
assert_eq!(err.column, 0);
assert_eq!(err.expected_highlight, "keyword");
assert_eq!(err.actual_highlights, Vec::<String>::new());
}
#[test]
fn test_assertion_with_highlight_ending_before() {
// Test where highlight ends before the assertion starts
let highlight_names = vec!["keyword".to_string()];
let assertions = vec![Assertion::new(0, 10, 1, false, String::from("keyword"))];
let highlights = vec![(Utf8Point::new(0, 0), Utf8Point::new(0, 5), Highlight(0))];
let result = iterate_assertions(&assertions, &highlights, &highlight_names);
assert!(result.is_err());
let err = result.unwrap_err().downcast::<Failure>().unwrap();
assert_eq!(err.row, 0);
assert_eq!(err.column, 10);
assert_eq!(err.expected_highlight, "keyword");
assert_eq!(err.actual_highlights, Vec::<String>::new());
}
#[test]
fn test_negative_assertion_with_non_matching_highlight() {
// Test that a negative assertion passes when the specified highlight is NOT present
let highlight_names = vec!["keyword".to_string(), "variable".to_string()];
let assertions = vec![Assertion::new(0, 0, 1, true, String::from("keyword"))];
let highlights = vec![
(Utf8Point::new(0, 0), Utf8Point::new(0, 5), Highlight(1)), // "variable" highlight
];
let result = iterate_assertions(&assertions, &highlights, &highlight_names);
assert!(result.is_ok());
assert_eq!(result.unwrap(), 1);
}
#[test]
fn test_negative_assertion_with_matching_highlight() {
// Test that a negative assertion fails when the specified highlight IS present
let highlight_names = vec!["keyword".to_string()];
let assertions = vec![Assertion::new(0, 0, 1, true, String::from("keyword"))];
let highlights = vec![
(Utf8Point::new(0, 0), Utf8Point::new(0, 5), Highlight(0)), // "keyword" highlight
];
let result = iterate_assertions(&assertions, &highlights, &highlight_names);
assert!(result.is_err());
let err = result.unwrap_err().downcast::<Failure>().unwrap();
assert_eq!(err.row, 0);
assert_eq!(err.column, 0);
assert_eq!(err.expected_highlight, "!keyword");
assert_eq!(err.actual_highlights, vec!["keyword".to_string()]);
}
#[test]
fn test_multiple_assertions_sequential() {
// Test multiple assertions in sequence with non-overlapping highlights
let highlight_names = vec!["keyword".to_string(), "variable".to_string()];
let assertions = vec![
Assertion::new(0, 0, 3, false, String::from("keyword")),
Assertion::new(0, 10, 1, false, String::from("variable")),
];
let highlights = vec![
(Utf8Point::new(0, 0), Utf8Point::new(0, 3), Highlight(0)), // "keyword"
(Utf8Point::new(0, 10), Utf8Point::new(0, 11), Highlight(1)), // "variable"
];
let result = iterate_assertions(&assertions, &highlights, &highlight_names);
assert!(result.is_ok());
assert_eq!(result.unwrap(), 2);
}
#[test]
fn test_multiple_highlights_at_same_position() {
// Test where multiple highlights overlap at the assertion position
let highlight_names = vec![
"keyword".to_string(),
"variable".to_string(),
"function".to_string(),
];
let assertions = vec![Assertion::new(0, 5, 1, false, String::from("variable"))];
let highlights = vec![
(Utf8Point::new(0, 0), Utf8Point::new(0, 10), Highlight(0)), // "keyword" spans entire range
(Utf8Point::new(0, 5), Utf8Point::new(0, 8), Highlight(1)), // "variable" at assertion position
(Utf8Point::new(0, 7), Utf8Point::new(0, 12), Highlight(2)), // "function" overlaps
];
let result = iterate_assertions(&assertions, &highlights, &highlight_names);
assert!(result.is_ok());
assert_eq!(result.unwrap(), 1);
}
#[test]
fn test_assertions_across_multiple_rows() {
// Test assertions on different rows
let highlight_names = vec!["keyword".to_string(), "variable".to_string()];
let assertions = vec![
Assertion::new(0, 5, 3, false, String::from("keyword")),
Assertion::new(2, 10, 1, false, String::from("variable")),
];
let highlights = vec![
(Utf8Point::new(0, 5), Utf8Point::new(0, 8), Highlight(0)), // "keyword" on row 0
(Utf8Point::new(2, 10), Utf8Point::new(2, 11), Highlight(1)), // "variable" on row 2
];
let result = iterate_assertions(&assertions, &highlights, &highlight_names);
assert!(result.is_ok());
assert_eq!(result.unwrap(), 2);
}
#[test]
fn test_assertion_should_not_match_highlight_on_later_row() {
// Test logic for early exit when highlight is on a later row than the assertion
let highlight_names = vec!["keyword".to_string()];
let assertions = vec![Assertion::new(0, 5, 3, false, String::from("keyword"))];
let highlights = vec![
(Utf8Point::new(1, 0), Utf8Point::new(1, 5), Highlight(0)), // wrong row
];
let result = iterate_assertions(&assertions, &highlights, &highlight_names);
assert!(result.is_err());
let err = result.unwrap_err().downcast::<Failure>().unwrap();
assert_eq!(err.row, 0);
assert_eq!(err.column, 7); // end_column
assert_eq!(err.expected_highlight, "keyword");
assert_eq!(err.actual_highlights, Vec::<String>::new());
}

View file

@ -3,7 +3,7 @@ use tree_sitter_tags::TagsContext;
use super::helpers::fixtures::{get_language, get_tags_config};
use crate::{
query_testing::{Assertion, Utf8Point, parse_position_comments},
query_testing::{parse_position_comments, Assertion, Utf8Point},
test_tags::get_tag_positions,
};

View file

@ -30,7 +30,7 @@ fn tree_query<I: AsRef<[u8]>>(tree: &Tree, text: impl TextProvider<I>, language:
let mut cursor = QueryCursor::new();
let mut captures = cursor.captures(&query, tree.root_node(), text);
let (match_, idx) = captures.next().unwrap();
let capture = match_.captures()[*idx];
let capture = match_.captures[*idx];
assert_eq!(capture.index as usize, *idx);
assert_eq!("comment", capture.node.kind());
}
@ -126,11 +126,9 @@ fn test_text_provider_callback_with_str_slice() {
check_parsing(text, |_node: Node<'_>| iter::once(text));
check_parsing_callback(
&mut |offset, _point| {
if offset < text.len() {
text.as_bytes()
} else {
Default::default()
}
(offset < text.len())
.then_some(text.as_bytes())
.unwrap_or_default()
},
|_node: Node<'_>| iter::once(text),
);
@ -142,11 +140,9 @@ fn test_text_provider_callback_with_owned_string_slice() {
check_parsing_callback(
&mut |offset, _point| {
if offset < text.len() {
text.as_bytes()
} else {
Default::default()
}
(offset < text.len())
.then_some(text.as_bytes())
.unwrap_or_default()
},
|_node: Node<'_>| {
let slice: String = text.to_owned();
@ -161,11 +157,9 @@ fn test_text_provider_callback_with_owned_bytes_vec_slice() {
check_parsing_callback(
&mut |offset, _point| {
if offset < text.len() {
text.as_bytes()
} else {
Default::default()
}
(offset < text.len())
.then_some(text.as_bytes())
.unwrap_or_default()
},
|_node: Node<'_>| {
let slice = text.to_owned().into_bytes();
@ -180,11 +174,9 @@ fn test_text_provider_callback_with_owned_arc_of_bytes_slice() {
check_parsing_callback(
&mut |offset, _point| {
if offset < text.len() {
text.as_bytes()
} else {
Default::default()
}
(offset < text.len())
.then_some(text.as_bytes())
.unwrap_or_default()
},
|_node: Node<'_>| {
let slice: Arc<[u8]> = text.to_owned().into_bytes().into();

View file

@ -1,3 +1,5 @@
use std::str;
use tree_sitter::{InputEdit, Parser, Point, Range, Tree};
use super::helpers::fixtures::get_language;
@ -793,44 +795,3 @@ fn get_changed_ranges(
*tree = new_tree;
result
}
// Regression test for an incremental reparse bug where an external
// scanner's choice depends on lexer->eof() (and thus on the parser's
// current included ranges). The cached token at byte 0 was emitted when
// the included range stopped just after the opener. Widening the range
// to include a later matching delimiter must invalidate that cached
// token so the scanner re-runs and emits the open form instead of the
// unclosed form.
#[test]
fn test_reuse_invalidates_scanner_token_when_included_range_expands() {
let language = get_test_fixture_language("external_lookahead_eof_boundary");
let mut parser = Parser::new();
parser.set_language(&language).unwrap();
let source = "``";
parser
.set_included_ranges(&[Range {
start_byte: 0,
end_byte: 1,
start_point: Point::new(0, 0),
end_point: Point::new(0, 1),
}])
.unwrap();
let tree1 = parser.parse(source, None).unwrap();
assert_eq!(tree1.root_node().to_sexp(), "(document (unclosed_delim))");
parser
.set_included_ranges(&[Range {
start_byte: 0,
end_byte: 2,
start_point: Point::new(0, 0),
end_point: Point::new(0, 2),
}])
.unwrap();
let tree2 = parser.parse(source, Some(&tree1)).unwrap();
assert_eq!(
tree2.root_node().to_sexp(),
"(document (span (open_delim) (close_delim)))"
);
}

View file

@ -5,7 +5,7 @@ use tree_sitter::{Parser, Query, QueryCursor, WasmError, WasmErrorKind, WasmStor
use crate::tests::helpers::{
allocations,
fixtures::{ENGINE, WASM_DIR, get_test_fixture_language_wasm},
fixtures::{get_test_fixture_language_wasm, ENGINE, WASM_DIR},
};
#[test]
@ -73,10 +73,7 @@ fn test_load_wasm_rust_language() {
parser.set_wasm_store(store).unwrap();
parser.set_language(&language).unwrap();
let tree = parser.parse("fn main() {}", None).unwrap();
assert_eq!(
tree.root_node().to_sexp(),
"(source_file (function_item name: (identifier) parameters: (parameters) body: (block)))"
);
assert_eq!(tree.root_node().to_sexp(), "(source_file (function_item name: (identifier) parameters: (parameters) body: (block)))");
});
}
@ -90,10 +87,7 @@ fn test_load_wasm_javascript_language() {
parser.set_wasm_store(store).unwrap();
parser.set_language(&language).unwrap();
let tree = parser.parse("const a = b\nconst c = d", None).unwrap();
assert_eq!(
tree.root_node().to_sexp(),
"(program (lexical_declaration (variable_declarator name: (identifier) value: (identifier))) (lexical_declaration (variable_declarator name: (identifier) value: (identifier))))"
);
assert_eq!(tree.root_node().to_sexp(), "(program (lexical_declaration (variable_declarator name: (identifier) value: (identifier))) (lexical_declaration (variable_declarator name: (identifier) value: (identifier))))");
});
}
@ -107,10 +101,7 @@ fn test_load_wasm_python_language() {
parser.set_wasm_store(store).unwrap();
parser.set_language(&language).unwrap();
let tree = parser.parse("a = b\nc = d", None).unwrap();
assert_eq!(
tree.root_node().to_sexp(),
"(module (expression_statement (assignment left: (identifier) right: (identifier))) (expression_statement (assignment left: (identifier) right: (identifier))))"
);
assert_eq!(tree.root_node().to_sexp(), "(module (expression_statement (assignment left: (identifier) right: (identifier))) (expression_statement (assignment left: (identifier) right: (identifier))))");
});
}
@ -127,32 +118,6 @@ fn test_load_fixture_language_wasm() {
});
}
#[test]
fn test_wasm_realloc_smaller_size() {
allocations::record(|| {
let store = WasmStore::new(&ENGINE).unwrap();
let mut parser = Parser::new();
let language = get_test_fixture_language_wasm("wasm_realloc_overflow_heap");
parser.set_wasm_store(store).unwrap();
parser.set_language(&language).unwrap();
let tree = parser.parse("hello", None).unwrap();
assert_eq!(tree.root_node().to_sexp(), "(document (zero_width))");
});
}
#[test]
fn test_wasm_realloc_clobber_region() {
allocations::record(|| {
let store = WasmStore::new(&ENGINE).unwrap();
let mut parser = Parser::new();
let language = get_test_fixture_language_wasm("wasm_realloc_clobber_region");
parser.set_wasm_store(store).unwrap();
parser.set_language(&language).unwrap();
let tree = parser.parse("hello", None).unwrap();
assert_eq!(tree.root_node().to_sexp(), "(document (zero_width))");
});
}
#[test]
fn test_load_multiple_wasm_languages() {
allocations::record(|| {
@ -267,18 +232,12 @@ fn test_reset_wasm_store() {
parser.set_wasm_store(parser_store).unwrap();
parser.set_language(&language).unwrap();
let tree = parser.parse("fn main() {}", None).unwrap();
assert_eq!(
tree.root_node().to_sexp(),
"(source_file (function_item name: (identifier) parameters: (parameters) body: (block)))"
);
assert_eq!(tree.root_node().to_sexp(), "(source_file (function_item name: (identifier) parameters: (parameters) body: (block)))");
let parser_store = WasmStore::new(&ENGINE).unwrap();
parser.set_wasm_store(parser_store).unwrap();
let tree = parser.parse("fn main() {}", None).unwrap();
assert_eq!(
tree.root_node().to_sexp(),
"(source_file (function_item name: (identifier) parameters: (parameters) body: (block)))"
);
assert_eq!(tree.root_node().to_sexp(), "(source_file (function_item name: (identifier) parameters: (parameters) body: (block)))");
});
}
@ -314,55 +273,6 @@ fn test_load_wasm_errors() {
});
}
#[test]
fn test_load_wasm_language_with_reserved_words() {
// This test exercises a grammar with multiple reserved word sets loaded via WASM.
allocations::record(|| {
let store = WasmStore::new(&ENGINE).unwrap();
let language = get_test_fixture_language_wasm("reserved_words");
let mut parser = Parser::new();
parser.set_wasm_store(store).unwrap();
parser.set_language(&language).unwrap();
// "if" and "while" are globally reserved, so using them as identifiers
// should produce an error recovery.
let tree = parser
.parse("var a =\n\nif (something) {\n c();\n}", None)
.unwrap();
assert_eq!(
tree.root_node().to_sexp(),
concat!(
"(program ",
"(ERROR (identifier)) ",
"(if_statement (parenthesized_expression (identifier)) ",
"(block (expression_statement (call_expression (identifier))))))",
)
);
// "if" and "while" are NOT reserved in the 'property' context, so they
// can appear as object keys without error.
let tree = parser
.parse("var x = {\n if: a,\n while: b,\n};", None)
.unwrap();
assert_eq!(
tree.root_node().to_sexp(),
concat!(
"(program (var_declaration (identifier) (object ",
"(pair (identifier) (identifier)) (pair (identifier) (identifier)))))"
)
);
// "var" IS reserved in the 'property' context, so using it as a property
// key triggers error recovery.
let tree = parser.parse("var x = {\nvar y = z;", None).unwrap();
assert_eq!(
tree.root_node().to_sexp(),
"(program (ERROR (identifier)) (var_declaration (identifier) (identifier)))"
);
});
}
#[test]
fn test_wasm_oom() {
allocations::record(|| {
@ -387,18 +297,3 @@ fn test_wasm_oom() {
);
});
}
#[test]
fn test_lookahead_iterator_outlives_wasm_language() {
allocations::record(|| {
let mut store = WasmStore::new(&ENGINE).unwrap();
let wasm = fs::read(WASM_DIR.join("tree-sitter-ruby.wasm")).unwrap();
let language = store.load_language("ruby", &wasm).unwrap();
let mut lookahead = language.lookahead_iterator(0).unwrap();
drop(language);
// The iterator retains the language, so the names are still live.
assert!(lookahead.iter_names().count() > 0);
});
}

View file

@ -5,7 +5,6 @@ pub mod highlight;
pub mod init;
pub mod input;
pub mod logger;
pub mod paint;
pub mod parse;
pub mod playground;
pub mod query;

View file

@ -2,12 +2,12 @@ use std::{
path::{Path, PathBuf},
process::{Child, ChildStdin, Command, Stdio},
sync::{
Arc,
atomic::{AtomicUsize, Ordering},
Arc,
},
};
use anyhow::{Context, Result, anyhow};
use anyhow::{anyhow, Context, Result};
use indoc::indoc;
use log::error;
use tree_sitter::{Parser, Tree};
@ -71,8 +71,8 @@ pub struct LogSession {
open_log: bool,
}
pub fn print_tree_graph(tree: &Tree, path: &str, open_log: bool) -> Result<()> {
let session = LogSession::new(path, open_log)?;
pub fn print_tree_graph(tree: &Tree, path: &str, quiet: bool) -> Result<()> {
let session = LogSession::new(path, quiet)?;
tree.print_dot_graph(session.dot_process_stdin.as_ref().unwrap());
Ok(())
}
@ -94,9 +94,9 @@ impl LogSession {
.stdin(Stdio::piped())
.stdout(dot_file)
.spawn()
.with_context(
|| "Failed to run the `dot` command. Check that graphviz is installed.",
)?;
.with_context(|| {
"Failed to run the `dot` command. Check that graphviz is installed."
})?;
let dot_stdin = dot_process
.stdin
.take()

View file

@ -144,7 +144,7 @@ impl Version {
}
}
fn update_file_with<F>(path: &PathBuf, update_fn: F) -> Result<(), UpdateError>
fn update_file_with<F>(&self, path: &PathBuf, update_fn: F) -> Result<(), UpdateError>
where
F: Fn(&str) -> String,
{
@ -155,7 +155,7 @@ impl Version {
fn update_treesitter_json(&self) -> Result<(), UpdateError> {
let json_path = self.current_dir.join("tree-sitter.json");
Self::update_file_with(&json_path, |content| {
self.update_file_with(&json_path, |content| {
content
.lines()
.map(|line| {
@ -189,7 +189,7 @@ impl Version {
return Ok(());
}
Self::update_file_with(&cargo_toml_path, |content| {
self.update_file_with(&cargo_toml_path, |content| {
content
.lines()
.map(|line| {
@ -236,7 +236,7 @@ impl Version {
return Ok(());
}
Self::update_file_with(&package_json_path, |content| {
self.update_file_with(&package_json_path, |content| {
content
.lines()
.map(|line| {
@ -292,11 +292,8 @@ impl Version {
} else {
self.current_dir.join("Makefile")
};
if !makefile_path.exists() {
return Ok(());
}
Self::update_file_with(&makefile_path, |content| {
self.update_file_with(&makefile_path, |content| {
content
.lines()
.map(|line| {
@ -320,7 +317,7 @@ impl Version {
return Ok(());
}
Self::update_file_with(&cmake_lists_path, |content| {
self.update_file_with(&cmake_lists_path, |content| {
let re = Regex::new(r#"(\s*VERSION\s+)"[0-9]+\.[0-9]+\.[0-9]+""#)
.expect("Failed to compile regex");
re.replace(
@ -339,7 +336,7 @@ impl Version {
return Ok(());
}
Self::update_file_with(&pyproject_toml_path, |content| {
self.update_file_with(&pyproject_toml_path, |content| {
content
.lines()
.map(|line| {
@ -363,7 +360,7 @@ impl Version {
return Ok(());
}
Self::update_file_with(&zig_zon_path, |content| {
self.update_file_with(&zig_zon_path, |content| {
let zig_version_prefix = ".version =";
content
.lines()

View file

@ -3,7 +3,7 @@ use std::{
path::{Path, PathBuf},
};
use anyhow::{Context, Result, anyhow};
use anyhow::{anyhow, Context, Result};
use tree_sitter::wasm_stdlib_symbols;
use tree_sitter_generate::{load_grammar_file, parse_grammar::GrammarJSON};
use tree_sitter_loader::Loader;
@ -15,7 +15,7 @@ pub fn load_language_wasm_file(language_dir: &Path) -> Result<(String, Vec<u8>)>
.unwrap();
let wasm_filename = format!("tree-sitter-{grammar_name}.wasm");
let contents = fs::read(language_dir.join(&wasm_filename)).with_context(|| {
format!("Failed to read {wasm_filename}. Run `tree-sitter build --wasm` first.")
format!("Failed to read {wasm_filename}. Run `tree-sitter build --wasm` first.",)
})?;
Ok((grammar_name, contents))
}
@ -83,17 +83,14 @@ pub fn compile_language_to_wasm(
let wasm_bytes = fs::read(&output_filename)?;
let parser = Parser::new(0);
for payload in parser.parse_all(&wasm_bytes) {
if let wasmparser::Payload::ImportSection(reader) = payload? {
for imports in reader {
for import in imports? {
let (_, import) = import?;
let name = import.name;
if !builtin_symbols.contains(&name)
&& !stdlib_symbols.contains(&name)
&& !dylink_symbols.contains(&name)
{
missing_symbols.push(name);
}
if let wasmparser::Payload::ImportSection(imports) = payload? {
for import in imports {
let import = import?.name;
if !builtin_symbols.contains(&import)
&& !stdlib_symbols.contains(&import)
&& !dylink_symbols.contains(&import)
{
missing_symbols.push(import);
}
}
}

View file

@ -5,6 +5,7 @@ description = "User configuration of tree-sitter's command line programs"
authors.workspace = true
edition.workspace = true
rust-version.workspace = true
readme = "README.md"
homepage.workspace = true
repository.workspace = true
documentation = "https://docs.rs/tree-sitter-config"

View file

@ -28,24 +28,14 @@ pub enum ConfigError {
#[derive(Debug, Error)]
pub struct IoError {
pub error: std::io::Error,
pub path: Option<PathBuf>,
pub path: Option<String>,
}
impl PartialEq for IoError {
fn eq(&self, other: &Self) -> bool {
self.path == other.path
&& self.error.kind() == other.error.kind()
&& self.error.raw_os_error() == other.error.raw_os_error()
}
}
impl Eq for IoError {}
impl IoError {
fn new(error: std::io::Error, path: Option<&Path>) -> Self {
Self {
error,
path: path.map(Path::to_path_buf),
path: path.map(|p| p.to_string_lossy().to_string()),
}
}
}
@ -54,7 +44,7 @@ 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.display())?;
write!(f, " ({path})")?;
}
Ok(())
}

View file

@ -5,6 +5,7 @@ description = "Library for generating C source code from a tree-sitter grammar"
authors.workspace = true
edition.workspace = true
rust-version.workspace = true
readme = "README.md"
homepage.workspace = true
repository.workspace = true
documentation = "https://docs.rs/tree-sitter-generate"
@ -19,24 +20,30 @@ path = "src/generate.rs"
workspace = true
[features]
default = [ "qjs-rt" ]
load = [ "dep:semver" ]
qjs-rt = [ "load", "rquickjs", "pathdiff" ]
default = ["qjs-rt"]
load = ["dep:semver"]
qjs-rt = ["load", "rquickjs", "pathdiff"]
[dependencies]
bitflags = "2.11.1"
bitflags = "2.9.4"
dunce = "1.0.5"
hashbrown.workspace = true
indexmap.workspace = true
indoc.workspace = true
log.workspace = true
pathdiff = { optional = true, version = "0.2.3" }
pathdiff = { version = "0.2.3", optional = true }
regex.workspace = true
regex-syntax.workspace = true
rquickjs = { features = [ "bindgen", "loader", "macro", "phf" ], optional = true, version = "0.13" }
rquickjs = { version = "0.10.0", optional = true, features = [
"bindgen",
"loader",
"macro",
"phf",
] }
rustc-hash.workspace = true
semver = { optional = true, workspace = true }
semver = { workspace = true, optional = true }
serde.workspace = true
serde_json.workspace = true
smallbitvec.workspace = true
thiserror.workspace = true
topological-sort.workspace = true

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