Compare commits

..

38 commits

Author SHA1 Message Date
Will Lillis 470813116b 0.26.5 2026-02-01 20:27:27 +01:00
Will Lillis 7ec1794d6b 0.26.4 2026-02-01 19:59:04 +01:00
Will Lillis fa8811f7f7 docs: include info on environment variables for fuzz command
(cherry picked from commit 7100767f64)
2026-02-01 19:07:59 +01:00
Will Lillis ef4999bf61 fix(cli): include default values for --edits and --iterations in
`fuzz` command help

(cherry picked from commit 0b8f124453)
2026-02-01 19:07:59 +01:00
Will Lillis 77e43dd116 fix(cli): use --edits value for fuzz tests
(cherry picked from commit c9d9ce6cbb)
2026-02-01 19:07:59 +01:00
Will Lillis 666144d3ed fix(wasm): when reallocating the last allocated region, properly grow
the region in place.

Previous changes to `malloc` caused `realloc` to sometimes pull regions
off of the free list during this optimization. Because no `memcpy` is
performed, this resulted in corrupted data returning to the caller.

Co-authored-by: trim21 <i@trim21.me>
2026-01-31 20:06:32 -05:00
Will Lillis ce2cb41e1f test: rename wasm corpus test "wasm_realloc"->"wasm_realloc_overflow_heap" 2026-01-31 20:06:32 -05:00
Will Lillis a423343bd3 fix(loader): don't rely on nm to verify scanner symbols
Relying on a user's system's installation of `nm` has proven to be bug
prone and flaky. Instead, we can enforce that these symbols are defined
by requiring the linker to resolve them. This is already the default on
macos, but linux has looser requirements which defers the error to when
the library is opened. This check was not run on Windows (because
there's no `nm`), but the msvc linker is similiarly strict to macos's
w.r.t. resolving symbols at build time rather than runtime, so there's
no issue here.
2026-01-31 19:18:46 -05:00
Will Lillis c8aedb8cfa fix(loader): account for nm/ld fix on newer powerpc linux toolchains
Previously a bug in linux powerpc linkers/nm caused function symbols to
be incorrectly reported in the data "D" section. Newer toolchains now
correctly report these symbols' sections as "T". Account for both to
maintain compatibility with older toolchains
2026-01-31 19:18:46 -05:00
Will Lillis 308b96d927 generate: return error rather than assert when ABI incompatibility is
detected

(cherry picked from commit 94f4143ad6)
2026-01-31 00:24:51 -05:00
Will Lillis e98a09b6cc fix(generate): ensure parse table action id's fit within uint16_t
Without this check, action ids > 65,535 can be assigned to `uint16_t`s
in parser.c. This causes parsing to either crash or silently misbehave.

(cherry picked from commit e65dc4c3b5)
2026-01-31 00:24:51 -05:00
Will Lillis d3a20faff9 fix(generate): error if supertype is defined as a terminal rule
(cherry picked from commit 4a2b8ed299)
2026-01-30 23:39:14 -05:00
Will Lillis 88a5475496 fix(wasm): return early from calloc if malloc fails
Co-authored-by: trim21 <i@trim21.me>
2026-01-26 23:36:50 -05:00
Will Lillis 6a8a5e33d9 fix(wasm): correct several bugs in realloc
- free memory if 0 size is passed in
- Don't `memcpy` contents if new pointer is `NULL`
- Copy old region's contents only up to size of new region
- free old region

Co-authored-by: trim21 <i@trim21.me>
2026-01-26 23:36:50 -05:00
Will Lillis bc2e4e2386 fix(rust): address new nightly lint
(cherry picked from commit c0137208ec)
2026-01-25 18:18:58 -05:00
Will Lillis f44e86628a fix(init): correct paths in rust bindings on Windows
(cherry picked from commit 8f4df58983)
2026-01-25 18:18:58 -05:00
Will Lillis ed6e42cbf0 fix(lib): address strict aliasing violations with Array type
Altering the `Array` type itself isn't feasible, as this causes
unacceptable breakage with existing parsers that depend on it. Instead,
pass in individual `Array` fields for to various `_array__*` functions.

Any time the `contents` of an array may be modified (`free`d, `realloc`d,
etc), return the potentially new address out by value. This prevents any
strict aliasing violations as we're no longer writing to a type-casted
pointer.

Co-authored-by: Nathaniel Wesley Filardo <nwfilardo@gmail.com>
(cherry picked from commit 5177b3dc26)
2026-01-24 22:19:28 +01:00
Will Lillis 4809aaaf04 feat(xtask): allow alternate branch for fixture grammars
Set tree-sitter-php fixture to `upstream_test_fixture` branch.
Also remove the `--update` flag, as it does nothing.

(cherry picked from commit 55fdc50e81)
2026-01-24 22:19:28 +01:00
Will Lillis 152d2756fc fix(cli): warn user when nm can't be run to verify the symbols inside
the parser being built

(cherry picked from commit 0cdb6bef7b)
2026-01-18 23:26:47 -05:00
Christian Clason f05efbb352 fix(wasm): regenerate stdlib with wasm-opt
Problem: Output of `cargo xtask build-wasm-stdlib` depends on whether
`wasm-opt` is installed (since `clang` will use it by default if it
finds it).

Solution: Install it and rerun the xtask.
(cherry picked from commit 5d290a2a75)
2026-01-15 16:52:47 +01:00
Will Lillis 1f221c8500 fix(build): define _BSD_SOURCE
System endian conversion macros are gated behind this feature flag for
older versions of GLIBC. `_BSD_SOURCE` and `_SVID_SOURCE` were
deprecated and replaced with `_DEFAULT_SOURCE` starting with GLIBC 2.19.

(cherry picked from commit aefae11c0d)
2026-01-12 23:43:46 -05:00
Kevin Wang fdca0718bc fix(templates): fix python free-threading compatibility
(cherry picked from commit 630fa52717)
2026-01-10 04:01:08 -06:00
Christian Clason fa7b1b2a66 fix(wasm): update wasm-stdlib.h
(cherry picked from commit cd6672701b)
2026-01-06 19:27:35 +01:00
tree-sitter-ci-bot[bot] adcc4d1f7b
fix(wasm): add common definitions to stdlib (#5199) (#5208)
Also expose `strlen` through `string.h` instead of `stdio.h`.

(cherry picked from commit f4ca3d95ca)

Co-authored-by: Trim21 <trim21.me@gmail.com>
2026-01-06 12:27:26 +01:00
skewb1k 7d9c544c96 fix(cli): restore test summary output for tree-sitter test
Problem:
After commit f02d7e7e33
the `tree-sitter test` command no longer printed the final test summary,
leaving empty line. The `Stats` struct was embedded into `TestSummary`,
and the explicit call to print it was removed.

Solution:
Print `parse_stats` from `TestSummary.fmt()` implementation.

(cherry picked from commit 17e3c7a5c5)
2026-01-04 22:45:41 -08:00
WillLillis c1e49d1571 feat(cli): fill in missing fields to tree-sitter.json when running
`tree-sitter init -u`

(cherry picked from commit dd60d5cff0)
2025-12-31 20:37:15 +01:00
WillLillis eae6554735 fix(cli): increase verbosity of tree-sitter init -u updates
Also, use `info` logs rather than `warn`

(cherry picked from commit f1288ea5c9)
2025-12-31 20:37:15 +01:00
WillLillis 48ee942c4f fix(cli): canonicalize build --output path
This fixes a potential issue with the new lock file hashing mechanism,
in which two different path literals pointing to the same location would
hash to separate lock files, allowing a race condition.

(cherry picked from commit 93d793d249)
2025-12-30 17:49:45 +01:00
Firas al-Khalil 9ee2b87dd6 feat(cli): concurrent build of same grammar on different paths
(cherry picked from commit 5d9605a91e)
2025-12-29 12:37:04 +01:00
Firas al-Khalil fb91deb8d9 fix(cli): report library load failure
Instead of panicking somehere else.

This happens on concurrent builds of the the same grammar.

(cherry picked from commit 5293dd683e)
2025-12-29 12:37:04 +01:00
Firas al-Khalil 789a966f96 fix(cli): report context on compile fail
(cherry picked from commit 62effdf128)
2025-12-29 12:37:04 +01:00
WillLillis 3c49fef0e3 fix(rust): address nightly clippy lint
(cherry picked from commit 8e4f21aba0)
2025-12-27 19:39:28 -05:00
WillLillis 8a297b86bc fix(cli): set language in cwd for all usages of highlight command
(cherry picked from commit 5208299bbb)
2025-12-27 19:39:28 -05:00
skewb1k ac6644016c fix(cli): remove extra newline with --cst
Makes CST output consistent with other formats.

(cherry picked from commit f05e57e2fc)
2025-12-24 15:37:30 +01:00
skewb1k a80765614b fix(cli): remove extra indentation with --cst --no-ranges
(cherry picked from commit 2f33a37dff)
2025-12-24 15:37:30 +01:00
kevin-hua-kraken 34602af22c fix(playground): update query API
(cherry picked from commit a7d8c0cbb2)
2025-12-23 14:18:14 +01:00
Will Lillis c4f81931e6 fix(cli): correct discrepancy with cst for --no-ranges
(cherry picked from commit eacb95c85d)
2025-12-16 23:24:07 -05:00
skewb1k 25777e5a64 fix(cli): trailing whitespace after multiline text nodes in CST
Problem:
The CST printer emits trailing whitespace after multiline text nodes.
With 1704c604bf and `:cst` corpus tests
this causes trailing spaces to appear on `test --update`.
These spaces cannot be removed afterward, as the test runner
expects an exact character-for-character match for CST tests.

Solution:
Print whitespace only if node is not multiline.

(cherry picked from commit 4ac2d5d276)
2025-12-14 22:41:02 -05:00
334 changed files with 14517 additions and 28064 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.5"
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)

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.5"
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.5", path = "./lib" }
tree-sitter-generate = { version = "0.26.5", path = "./crates/generate" }
tree-sitter-loader = { version = "0.26.5", path = "./crates/loader" }
tree-sitter-config = { version = "0.26.5", path = "./crates/config" }
tree-sitter-highlight = { version = "0.26.5", path = "./crates/highlight" }
tree-sitter-tags = { version = "0.26.5", 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.5
DESCRIPTION := An incremental parsing system for programming tools
HOMEPAGE_URL := https://tree-sitter.github.io/tree-sitter/
@ -22,7 +22,7 @@ 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 += -Ilib/src -Ilib/src/wasm -Ilib/include
@ -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)

35
Package.swift Normal file
View file

@ -0,0 +1,35 @@
// 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("_BSD_SOURCE"),
.define("_DARWIN_C_SOURCE"),
]),
],
cLanguageStandard: .c11
)

View file

@ -4,56 +4,51 @@ 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("_BSD_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 +122,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.5",
.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");
@ -22,16 +21,10 @@ static EXAMPLE_FILTER: LazyLock<Option<String>> =
static REPETITION_COUNT: LazyLock<usize> = LazyLock::new(|| {
env::var("TREE_SITTER_BENCHMARK_REPETITION_COUNT").map_or(5, |s| s.parse::<usize>().unwrap())
});
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 +36,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 +93,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 +121,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 +139,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 +220,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.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "tree-sitter-cli",
"version": "0.28.0",
"version": "0.26.5",
"hasInstallScript": true,
"license": "MIT",
"bin": {

View file

@ -1,6 +1,6 @@
{
"name": "tree-sitter-cli",
"version": "0.28.0",
"version": "0.26.5",
"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());
@ -63,9 +63,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 +97,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 +111,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 +142,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 +167,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}");
@ -234,7 +255,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 +271,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 +324,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 +362,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, DEFAULT_EDIT_COUNT, DEFAULT_ITERATION_COUNT, 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, TREE_SITTER_JSON_SCHEMA},
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,7 +389,7 @@ 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,
@ -437,10 +420,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 +432,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 +487,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 +527,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 +789,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 +863,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]);
@ -911,7 +881,7 @@ impl Init {
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
// will be included with explicit `false`s rather than implict `null`s
if self.update && !old_config.trim().eq(new_config.trim()) {
info!("Updating tree-sitter.json");
fs::write(
@ -958,8 +928,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 +941,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 +968,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));
@ -1034,7 +985,7 @@ impl Build {
.context("Output path must have a parent")?;
let name = full_path
.file_name()
.context("Output path must have a filename")?;
.context("Ouput path must have a filename")?;
fs::create_dir_all(parent_path).context("Failed to create output path")?;
let mut canon_path = parent_path.canonicalize().context("Invalid output path")?;
canon_path.push(name);
@ -1072,6 +1023,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 +1042,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 +1080,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 +1125,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 +1279,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 +1297,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 +1321,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 +1342,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 +1353,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 +1405,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 +1476,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 +1526,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,
@ -1704,29 +1667,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(
@ -1811,7 +1760,7 @@ 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 language = languages
@ -1953,7 +1902,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 +1956,7 @@ impl DumpLanguages {
concat!(
"name: {}\n",
"scope: {}\n",
"parser: {}\n",
"parser: {:?}\n",
"highlights: {:?}\n",
"file_types: {:?}\n",
"content_regex: {:?}\n",
@ -2015,7 +1964,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 +1997,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 +2053,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 +2125,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 +2138,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,7 +510,7 @@ pub fn parse_file_at_path(
}
}
cursor.reset(tree.root_node());
writeln!(&mut stdout)?;
println!();
}
if opts.output == ParseOutput::Cst {
@ -542,10 +544,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 +581,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,16 +776,12 @@ 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);
@ -827,19 +825,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 +848,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,34 +881,24 @@ 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, " ")?;
}
} else {
write!(out, " ")?;
}
let formatted_line = render_line_feed(line, opts);
write!(
out,
"{}{}{}",
paint(quote_color, quote),
paint(color, CstLineFeed { source: line, opts }),
paint(quote_color, quote),
"{}{}{}{}{}{}",
if multiline { "\n" } else { " " },
if multiline && !opts.no_ranges {
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)),
)?;
}
}
@ -918,93 +906,67 @@ fn write_node_text(
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 +984,7 @@ fn cst_render_node(
write!(
out,
"{}",
paint(opts.parse_theme.field, format_args!("{field_name}: "))
paint(opts.parse_theme.field, &format!("{field_name}: "))
)?;
}
@ -1093,13 +1055,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 +1096,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 +1139,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

@ -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))))");
});
}
@ -267,18 +258,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 +299,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 +323,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