Compare commits

..

454 commits

Author SHA1 Message Date
Christian Clason de98c6c970 build(deps): bump rquickjs to v0.13.0
Some checks failed
CI / sanitize (push) Failing after 15s
CI / build (push) Failing after 17s
CI / check-wasm-stdlib (push) Failing after 15s
CI / checks (push) Has been cancelled
2026-09-09 01:25:17 +02:00
Christian Clason 8351896bea build(deps): cargo update
Some checks failed
CI / checks (push) Waiting to run
CI / sanitize (push) Failing after 15s
CI / build (push) Failing after 17s
CI / check-wasm-stdlib (push) Failing after 15s
2026-09-08 09:46:26 +02:00
Will Lillis 072f68c829 feat(generate)!: derive PartialEq + Eq on GenerateError
Some checks failed
CI / checks (push) Waiting to run
CI / sanitize (push) Failing after 15s
CI / build (push) Failing after 16s
CI / check-wasm-stdlib (push) Failing after 14s
Also use `IoError` more consistently throughout the rest of the project
where its straightforward to do so.

BREAKING CHANGE: Changes public error types for config, generate, and
loader crates.
2026-09-07 17:53:09 -04:00
dependabot[bot] badf2882c6 build(deps): bump the npm group across 1 directory with 5 updates
Bumps the npm group with 5 updates in the /lib/binding_web directory:

| Package | From | To |
| --- | --- | --- |
| [@types/emscripten](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/emscripten) | `1.41.5` | `1.41.6` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `26.4.0` | `26.4.1` |
| [eslint](https://github.com/eslint/eslint) | `10.9.1` | `10.10.0` |
| [tsx](https://github.com/privatenumber/tsx) | `4.23.12` | `4.23.13` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.68.0` | `8.69.0` |



Updates `@types/emscripten` from 1.41.5 to 1.41.6
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/emscripten)

Updates `@types/node` from 26.4.0 to 26.4.1
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `eslint` from 10.9.1 to 10.10.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.9.1...v10.10.0)

Updates `tsx` from 4.23.12 to 4.23.13
- [Release notes](https://github.com/privatenumber/tsx/releases)
- [Changelog](https://github.com/privatenumber/tsx/blob/master/release.config.cjs)
- [Commits](https://github.com/privatenumber/tsx/compare/v4.23.12...v4.23.13)

Updates `typescript-eslint` from 8.68.0 to 8.69.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.69.0/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: "@types/emscripten"
  dependency-version: 1.41.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm
- dependency-name: "@types/node"
  dependency-version: 26.4.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm
- dependency-name: eslint
  dependency-version: 10.10.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm
- dependency-name: tsx
  dependency-version: 4.23.13
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm
- dependency-name: typescript-eslint
  dependency-version: 8.69.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-07 23:46:17 +02:00
dependabot[bot] 9c2c5714cd build(deps): bump cc from 1.4.4 to 1.4.5 in the cargo group
Bumps the cargo group with 1 update: [cc](https://github.com/rust-lang/cc-rs).


Updates `cc` from 1.4.4 to 1.4.5
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/cc-rs/compare/cc-v1.4.4...cc-v1.4.5)

---
updated-dependencies:
- dependency-name: cc
  dependency-version: 1.4.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-07 23:45:04 +02:00
Will Lillis f430449ab9 fix(rust): address manual-readme lint
Some checks failed
CI / sanitize (push) Failing after 15s
CI / build (push) Failing after 16s
CI / check-wasm-stdlib (push) Failing after 15s
CI / checks (push) Has been cancelled
Cargo automatically resolves this, so the `Cargo.toml` entries are
redundant.
2026-09-04 14:48:46 -04:00
Will Lillis 068db86ca9 fix(rust): address new assert-is-empty nightly lint
The resulting code is arguably uglier, but there is a strong argument
for the improved message when the assert trips.
2026-09-04 14:48:46 -04:00
cui fliter ee0c20b1f5
fix(cli)!: fix query test capture range matching across lines (#5917)
Some checks failed
CI / checks (push) Waiting to run
CI / sanitize (push) Failing after 14s
CI / build (push) Failing after 15s
CI / check-wasm-stdlib (push) Failing after 15s
Signed-off-by: cuishuang <imcusg@gmail.com>
BREAKING CHANGE: changes the signature of `assert_expected_captures`, which is exposed publicly when tree-sitter-cli is consumed as a library.
2026-09-03 23:25:21 -04:00
dependabot[bot] 702ba49a6a build(deps): bump @humanfs/node in /crates/cli/eslint
Some checks failed
CI / checks (push) Waiting to run
CI / sanitize (push) Failing after 15s
CI / build (push) Failing after 15s
CI / check-wasm-stdlib (push) Failing after 15s
Bumps [@humanfs/node](https://github.com/humanwhocodes/humanfs/tree/HEAD/packages/node) from 0.16.7 to 0.16.8.
- [Release notes](https://github.com/humanwhocodes/humanfs/releases)
- [Changelog](https://github.com/humanwhocodes/humanfs/blob/main/packages/node/CHANGELOG.md)
- [Commits](https://github.com/humanwhocodes/humanfs/commits/node-v0.16.8/packages/node)

---
updated-dependencies:
- dependency-name: "@humanfs/node"
  dependency-version: 0.16.8
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-03 16:35:52 +02:00
dependabot[bot] b9c5eb2b73 build(deps): bump @humanfs/node in /lib/binding_web
Bumps [@humanfs/node](https://github.com/humanwhocodes/humanfs/tree/HEAD/packages/node) from 0.16.7 to 0.16.8.
- [Release notes](https://github.com/humanwhocodes/humanfs/releases)
- [Changelog](https://github.com/humanwhocodes/humanfs/blob/main/packages/node/CHANGELOG.md)
- [Commits](https://github.com/humanwhocodes/humanfs/commits/node-v0.16.8/packages/node)

---
updated-dependencies:
- dependency-name: "@humanfs/node"
  dependency-version: 0.16.8
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-03 16:35:42 +02:00
Yudai Takada 351bd71e52
fix(lib): decode UTF-16 surrogate pairs with input endianness (#5912)
Some checks failed
CI / checks (push) Waiting to run
CI / sanitize (push) Failing after 15s
CI / build (push) Failing after 15s
CI / check-wasm-stdlib (push) Failing after 15s
Problem: Parsing UTF-16BE source containing supplementary-plane characters, such as `let emoji = "😀"`, decoded the emoji as two isolated surrogates on little-endian hosts, causing incorrect lexer lookahead and potentially shifted token boundaries.

Soluton: Fix the trailing surrogate byte-order conversion in both UTF-16LE and UTF-16BE decoders and adds a regression test for U+1F600.
2026-09-03 08:21:08 +02:00
dependabot[bot] c206ad1e6a build(deps): bump the npm group across 1 directory with 3 updates
Some checks failed
CI / sanitize (push) Failing after 15s
CI / build (push) Failing after 16s
CI / check-wasm-stdlib (push) Failing after 15s
CI / checks (push) Has been cancelled
Bumps the npm group with 3 updates in the /lib/binding_web directory: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node), [eslint](https://github.com/eslint/eslint) and [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint).


Updates `@types/node` from 26.2.0 to 26.4.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `eslint` from 10.9.0 to 10.9.1
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.9.0...v10.9.1)

Updates `typescript-eslint` from 8.67.0 to 8.68.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.68.0/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 26.4.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm
- dependency-name: eslint
  dependency-version: 10.9.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm
- dependency-name: typescript-eslint
  dependency-version: 8.68.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-31 23:52:08 +02:00
dependabot[bot] 51da0190f5 ci: bump vmactions/omnios-vm from 1.3.4 to 1.3.6 in the actions group
Bumps the actions group with 1 update: [vmactions/omnios-vm](https://github.com/vmactions/omnios-vm).


Updates `vmactions/omnios-vm` from 1.3.4 to 1.3.6
- [Release notes](https://github.com/vmactions/omnios-vm/releases)
- [Commits](https://github.com/vmactions/omnios-vm/compare/v1.3.4...v1.3.6)

---
updated-dependencies:
- dependency-name: vmactions/omnios-vm
  dependency-version: 1.3.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-31 23:51:47 +02:00
Amaan Qureshi e5cbeb74eb nix: fix the flake
Some checks failed
CI / checks (push) Waiting to run
CI / build (push) Failing after 16s
CI / sanitize (push) Failing after 15s
CI / check-wasm-stdlib (push) Failing after 14s
Deploy Docs / deploy-docs (push) Has been cancelled
Check Wasm Exports / check-wasm-exports (push) Has been cancelled
2026-08-30 17:33:12 -04:00
Amaan Qureshi ea9ddbe190 docs: replace mdbook-admonish with mdBook admonitions 2026-08-30 17:33:12 -04:00
Will Lillis aa1b924997 fix(generate): defer token rewrites until lexical expansion
In a future feature, `RulePool` nodes can be shared between lexical and
syntax roots. No grammars generated by grammar.js/grammra.json are affected
by this issue, as they lead to a tree pool rather than a DAG.

With a DAG `RulePool` representation, however, rewriting tokens during
extraction can destroy a token body before lexical expansion consumes
it and can renumber shared syntax nodes more than once.

Record terminal rewrites while inspecting the original pool, expand tokens
and separators first, then commit the rewrites and renumber each reachable
syntax node once. Resolve grammar metadata against the pending rewrites
before mutating the pool.
2026-08-30 14:23:29 -04:00
Will Lillis 1b64a459ff perf(generate): flatten Choices directly into the children tail 2026-08-30 14:23:29 -04:00
Will Lillis 1e070eb28e perf(generate): build Seq and Repeat child ranges without temporaries
`try_seq` reserves a `Seq`'s child range up front and fills slots as
recursion produces each member. The `Repeat` arm builds
`Choice(repeat(x), blank)` directly, skipping the `choice` helper.

Slightly faster and less memory used.
2026-08-30 14:23:29 -04:00
Christian Clason 91e875ad7b chore: start working on v0.28 2026-08-30 19:50:28 +02:00
Arne Bertrand 6070dbfefd
fix(nix): remove Dockerfile reference from flake (#5892)
Fixup for https://github.com/tree-sitter/tree-sitter/pull/5872.
2026-08-30 16:49:12 +00:00
Christian Clason f52e78cb9f build(deps): cargo update
Some checks failed
CI / checks (push) Waiting to run
CI / sanitize (push) Failing after 15s
CI / build (push) Failing after 16s
CI / check-wasm-stdlib (push) Failing after 15s
2026-08-30 09:29:09 +02:00
Will Lillis 49dbf70ebd fix(rust): ensure C returned pointers are non-null in
`Node::grammar_name` and `Node::kind`
2026-08-30 02:58:20 -04:00
Will Lillis 9540ea3b10 fix(lib): reject an old_tree from a different language 2026-08-30 02:58:20 -04:00
Will Lillis 3e7be91c46 fix(rust): reject out-of-range node kind ids
`node_kind_is_named`, `node_kind_is_visible` and `node_kind_is_supertype`
forwarded their `u16` straight to `ts_language_symbol_type`, which indexes
`symbol_metadata` unchecked. Bound the id by `node_kind_count()`.
2026-08-30 02:58:20 -04:00
Will Lillis f47170927d fix(lib): bounds-check symbol and state ids in language lookups
`ts_language_next_state` indexed the parse table with the caller's `state`
and `symbol` without validating either, and `ts_language_subtypes` read
`symbol_metadata` before checking `supertype` was in range.
2026-08-30 02:58:20 -04:00
Will Lillis 5ddf71b9f4 fix(rust)!: expose QueryMatch::captures as a method
The field handed out a slice whose lifetime outlived the streaming-iterator
loan it came from. An accessor reborrowing through `&self` bounds it correctly.

BREAKING CHANGE: In the rust bindings, replace `m.captures` with `m.captures()`.
2026-08-30 02:58:20 -04:00
cuishuang efa0dd9a36 fix(cli): correct zero-based positions for --edits
Some checks failed
CI / checks (push) Waiting to run
CI / sanitize (push) Failing after 15s
CI / build (push) Failing after 16s
CI / check-wasm-stdlib (push) Failing after 15s
Signed-off-by: cuishuang <imcusg@gmail.com>
2026-08-29 21:06:30 -04:00
Christian Clason 16d5e9d227 ci(checks): add lint-toml step using taplo
Some checks failed
CI / checks (push) Waiting to run
CI / sanitize (push) Failing after 14s
CI / build (push) Failing after 15s
CI / check-wasm-stdlib (push) Failing after 14s
Deploy Docs / deploy-docs (push) Has been cancelled
2026-08-29 11:10:37 +02:00
Will Lillis 3ac04a64f7 docs: add taplo instructions 2026-08-29 11:10:37 +02:00
Will Lillis d9cb739430 fix(toml): drop align_entries = true from .taplo.toml
This key was being applied inconsistently depending on nesting level.
2026-08-29 11:10:37 +02:00
Will Lillis 7a2dd61a7c fix(xtask): preserve format padding when updating Cargo.toml files 2026-08-29 11:10:37 +02:00
Christian Clason a520ad0fc3 chore: remove .dockerignore
Fixup for #5872, which missed the hidden file.
2026-08-29 10:30:35 +02:00
Christian Clason da219f40f8 chore: remove Zed editor config
Problem: Editor-specific settings should not be part of the global
repository. (Only config files that control standard tooling in order to
enforce, e.g., formatting policies are appropriate to commit -- and only
if covered by CI.)

Solution: Drop the Zed config files from the repo.
2026-08-29 10:30:35 +02:00
Christian Clason 664e9a6786 build(deps)!: bump wasi-sdk to v34
Some checks failed
CI / checks (push) Waiting to run
Deploy Docs / deploy-docs (push) Waiting to run
CI / sanitize (push) Failing after 15s
CI / build (push) Failing after 15s
CI / check-wasm-stdlib (push) Failing after 14s
Check Bindgen Output / check-bindgen (push) Has been cancelled
Check Wasm Exports / check-wasm-exports (push) Has been cancelled
Breaking change: LLVM 23 replaces `wasm32-wasi` target with
`wasm32-wasip1`, requiring updating the flags used by
`tree-sitter build --wasm`.
2026-08-27 10:35:19 +02:00
Christian Clason fcef970e81 build(deps): bump wasmtime-c-api to v48.0.1 2026-08-27 09:37:57 +02:00
Kyuhwan Lee 8933cb7b40 docs(web): add WASM version compatibility section
Add documentation about version compatibility requirements between
web-tree-sitter parser ABIs for WASM file generation.

This helps users understand why Language.load() might fail when using
pre-built WASM files from third-party packages that were built with
incompatible tree-sitter-cli versions.

Co-authored-by: Will Lillis <will.lillis24@gmail.com>
2026-08-25 17:34:56 -04:00
Will Lillis 82639170a7 docs: specify Emscripten version constraints for web binding builds 2026-08-25 17:34:37 -04:00
dependabot[bot] 129d8cd141 build(deps): bump eslint
Bumps the npm group with 1 update in the /lib/binding_web directory: [eslint](https://github.com/eslint/eslint).


Updates `eslint` from 10.8.1 to 10.9.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.8.1...v10.9.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 10.9.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-25 09:59:01 +02:00
dependabot[bot] f3a9521894 build(deps): bump cc from 1.4.3 to 1.4.4 in the cargo group
Bumps the cargo group with 1 update: [cc](https://github.com/rust-lang/cc-rs).


Updates `cc` from 1.4.3 to 1.4.4
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/cc-rs/compare/cc-v1.4.3...cc-v1.4.4)

---
updated-dependencies:
- dependency-name: cc
  dependency-version: 1.4.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-25 09:58:43 +02:00
Peter Stenger 0be5f898ad fix(web): map Node-only specifiers to false via browser field
Browser bundlers (esbuild, webpack, vite, rollup) statically discover the
`await import("fs/promises")` and `await import("module")` calls in the
shipped ESM bundle and try to resolve them, even though both sit in
Node-only branches that never execute in a browser.

Adding a top-level `browser` field tells bundlers to substitute empty
stubs for these specifiers when targeting the browser. The runtime gates
(`globalThis.process?.versions.node` and `ENVIRONMENT_IS_NODE`) already
prevent the stubs from ever being invoked.

Fixes #5545.
2026-08-25 00:27:50 -04:00
katrinafyi ac17c8d003 playground: set default LANGUAGE_BASE_URL to "." to fix subfolder hosting
Previously with the empty string base URL, the playground would not work when hosted at a subfolder of the domain. This is because playground.js has this code:

      const url = `${LANGUAGE_BASE_URL}/tree-sitter-${newLanguageName}.wasm`;

With an empty string, it would look for wasm at the root of the website and 404.

Github code search shows that this is an issue which lots of people have run into, and everyone has to work around it: https://github.com/search?q=%2FLANGUAGE_BASE_URL+%3D+%22%22%2F+-language%3AHTML&type=code

Related to https://www.github.com/tree-sitter/tree-sitter/issues/5230
2026-08-24 19:41:26 -04:00
Will Lillis c6c137a329 fix(generate): avoid mutating inherited reserved sets
Copy the base grammar's reserved set map before adding or replacing sets
in a derived grammar.
2026-08-23 17:08:31 -04:00
Will Lillis 1cb91662a5 fix(generate): validate symbol-only grammar fields
Require `word`, `conflicts`, `inline`, and `supertypes` callbacks
to return named grammar symbols instead of silently accepting values
that produce undefined names.

Restrict `precedences` lists to named rules and precedence names, and
preserve the existing handling of undefined and duplicate `inline`
rules.
2026-08-23 17:08:31 -04:00
Mike Arndt 7ca101cb7e fix(cli): align grammar DSL declarations with runtime
Model grammar() as returning the evaluated grammar beneath a `grammar`
property, with callbacks replaced by their normalized values and
runtime-initialized fields represented as required properties.

Correct the evaluated rule types for named precedences and regular
expression flags. Narrow symbol-only callbacks such as `conflicts`,
`inline`, `supertypes`, and `word`, and expose the inherited values
passed to `extras` and `reserved` callbacks.

Co-authored-by: Will Lillis <will.lillis24@gmail.com>
2026-08-23 17:08:31 -04:00
Will Lillis dad7d0bd88 generate: use a single buffer for the string pool's backing store
This representation slightly lowers walltime and allocations, but also
makes `StrPool` `Send`.
2026-08-23 14:15:16 -04:00
Will Lillis f9dcb005cd fix(generate)!: use node identities in node-types bookkeeping
Some node-types bookkeeping still used the type name without its named
status. This caused anonymous nodes to inherit extra metadata from named
nodes with the same text, and could create false supertype cycles when a
named supertype and anonymous alias shared a name.

Key extra metadata and supertype dependencies by the complete
(type, named) identity.

Reject named aliases that collide with a canonical supertype. Such an
identity cannot be represented as both a concrete node and an abstract
supertype in node-types.json, and previously caused generation to panic.

All output entries now come from the map keyed by (type, named), so no
two entries can reach the final sort with the same identity. Remove the
unreachable root and extra tie-breakers and the now-redundant deduplication.

BREAKING CHANGE: Alters a public error enum for the generate crate.
2026-08-23 13:40:32 -04:00
Will Lillis 78267a0a96 fix(generate): merge anonymous token aliases in node-types.json
When an anonymous token alias and an anonymous syntax-node alias have
the same name, node-types.json emits two entries with the same type and
named status. For example,

```
alias($._node, "same")
```
and

```
alias("!", "same")
```

produce separate anonymous "same" entries. Add anonymous tokens to
the node-type map instead. When a token shares an identity with a
structured node, retain its possible children and fields but mark
them as optional because the token appearance is a leaf.
2026-08-23 13:40:32 -04:00
Will Lillis 00beb27f83 fix(generate): distinguish named and anonymous node types
A node-types entry is identified by both its type name and its `named`
value. The generator only keyed accumulated entries by name, so aliases
like

    alias($._node, $.same)
    alias($._node, "same")

were incorrectly merged into one entry. Whichever alias was processed
first determined the entry's `named` value, and the structures of two
distinct node types could be combined.

Key accumulated entries by `NodeTypeRef` so named and anonymous nodes
with the same type name remain separate.
2026-08-23 13:40:32 -04:00
Will Lillis 7bd12334c4 fix(generate): include aliased supertypes in node-types.json
Aliasing a supertype makes the aliased occurrence appear as a regular
node in the syntax tree. For example,

```
alias($.expression, $.expression_target)
```

can produce:

```
(expression_target (identifier))
```

Previously, node-types generation skipped the source supertype entirely.
It could reference `expression_target` as a field or child type without
emitting a corresponding top-level entry for it.

Continue emitting the canonical `expression` entry with its `subtypes`,
but pass aliased appearances through regular node generation so their
children and fields are recorded and merged normally.
2026-08-23 13:40:32 -04:00
Will Lillis 54a46eb49b fix(rust): remove must_use attribute from Tree::changed_ranges
`ExactSizeIterator` implements `std::iter::Iterator`, which is already
`must_use`.
2026-08-23 09:45:42 +02:00
Christian Clason 2452dc7a71 chore: remove Dockerfile
Problem: The committed Dockerfile is not used by the standard tooling in
this repo and therefore bitrots freely, which increases maintenance
burden on the (non-Docker-using) maintainers.

Solution: Drop the Dockerfile from the repo.
2026-08-23 00:35:12 -04:00
Amaan Qureshi 74b7d0c951 feat!: add an eof function
This commit adds an `eof()` function for grammars, which is easier to
use than the NUL byte directly. It compiles down to a constraint that
the enclosing production can only reduce at end of input, not to a
shiftable token.

BREAKING CHANGE: Public error types for `tree-sitter-generate` were modified.

Co-authored-by: Will Lillis <will.lillis24@gmail.com>
2026-08-21 20:02:58 -04:00
Will Lillis baad4174e5 fix(generate)!: hold IoError inside GenerateError::GrammarPath 2026-08-20 22:11:42 -04:00
Will Lillis cde84508ac fix(generate)!: error when nonexistent _file_ path is passed to
`generate_parser_in_directory`.
2026-08-20 22:11:42 -04:00
Max Brunsfeld 43623ec9bf
Upgrade to latest wasmtime, improve robustness and perf of wasm-based parsing (#5847)
* build(deps): upgrade wasmtime C API to 48.0.0

Upgrade the Rust and Zig Wasmtime dependencies and enable reference values with the null GC collector.

Wasmtime 48 requires a newer Rust toolchain, whose Clippy version identifies three item helpers that can be const. Mark them const so the workspace continues to pass Clippy with warnings denied.

* feat(benchmark): support Wasm grammars

* perf(wasm): cache language function handles

* fix(wasm): improve validation and failure cleanup

Bounds-check dylink metadata parsing, require exact import and export names, and restore memory and function-table allocation offsets when language loading fails.

* fix(wasm): copy the complete supertype map
2026-08-20 21:02:11 +00:00
Will Lillis fe3fe327e2 fix(generate): correct test assertion for inlined supertype warning 2026-08-20 02:47:10 -04:00
Will Lillis 03ae6710cd fix(ci): run all workspace tests
Without `--workspace`, all generate tests were silently skipped.
2026-08-20 02:47:10 -04:00
Amaan Qureshi 067f85e5a2 generate: hold StrIds in node-types JSON output
The JSON output types now hold ids from the string pool instead of
cloning into owned `String`s, which makes `NodeTypeJSON` `Copy` and
drops most allocations when building the output.

Co-authored-by: Will Lillis <will.lillis24@gmail.com>
2026-08-19 20:30:17 -04:00
Amaan Qureshi 5800563fe7 generate: refactor get_variable_info and generate_node_types_json
This splits both functions into small, focused helpers with no
behavioral change.
2026-08-19 20:30:17 -04:00
Amaan Qureshi 14c158bbba generate: warn when a symbol is both a supertype and inlined
Currently, a symbol listed in both `supertypes` and `inline` produces
a phantom supertype entry in `node-types.json` for a rule that can
never appear in a tree. `intern_symbols` now drops the supertype with
a warning. A hard error would break 29 published grammars, including
python, go, ruby, rust, and scala, so that that decision is deferred
to the next breaking release. Fixes #5218
2026-08-19 20:30:17 -04:00
dependabot[bot] f235c2f1c3 build(deps): bump the cargo group with 4 updates
Bumps the cargo group with 4 updates: [bstr](https://github.com/BurntSushi/bstr), [cc](https://github.com/rust-lang/cc-rs), [clap_complete_nushell](https://github.com/clap-rs/clap) and [thiserror](https://github.com/dtolnay/thiserror).


Updates `bstr` from 1.13.0 to 1.13.1
- [Commits](https://github.com/BurntSushi/bstr/compare/1.13.0...1.13.1)

Updates `cc` from 1.4.2 to 1.4.3
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/cc-rs/compare/cc-v1.4.2...cc-v1.4.3)

Updates `clap_complete_nushell` from 4.6.1 to 4.6.2
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete_nushell-v4.6.1...clap_complete_nushell-v4.6.2)

Updates `thiserror` from 2.0.19 to 2.0.20
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/2.0.19...2.0.20)

---
updated-dependencies:
- dependency-name: bstr
  dependency-version: 1.13.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: cc
  dependency-version: 1.4.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: clap_complete_nushell
  dependency-version: 4.6.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: thiserror
  dependency-version: 2.0.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-18 10:39:56 +02:00
dependabot[bot] 95eaaa2130 build(deps): bump the npm group across 1 directory with 3 updates
Bumps the npm group with 3 updates in the /lib/binding_web directory: [esbuild](https://github.com/evanw/esbuild), [tsx](https://github.com/privatenumber/tsx) and [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint).


Updates `esbuild` from 0.28.1 to 0.28.2
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.28.1...v0.28.2)

Updates `tsx` from 4.23.11 to 4.23.12
- [Release notes](https://github.com/privatenumber/tsx/releases)
- [Changelog](https://github.com/privatenumber/tsx/blob/master/release.config.cjs)
- [Commits](https://github.com/privatenumber/tsx/compare/v4.23.11...v4.23.12)

Updates `typescript-eslint` from 8.66.0 to 8.67.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.67.0/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: esbuild
  dependency-version: 0.28.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm
- dependency-name: tsx
  dependency-version: 4.23.12
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm
- dependency-name: typescript-eslint
  dependency-version: 8.67.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-18 10:37:31 +02:00
dependabot[bot] dff1fd868c build(deps): bump esbuild and vite in /lib/binding_web
Bumps [esbuild](https://github.com/evanw/esbuild) and [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite). These dependencies needed to be updated together.

Updates `esbuild` from 0.27.7 to 0.28.1
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.27.7...v0.28.1)

Updates `vite` from 7.3.2 to 7.3.6
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v7.3.6/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.3.6/packages/vite)

---
updated-dependencies:
- dependency-name: esbuild
  dependency-version: 0.28.1
  dependency-type: indirect
- dependency-name: vite
  dependency-version: 7.3.6
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-15 10:13:38 +02:00
Max Brunsfeld 0e2af0d8d1
Allow using Tree-sitter rust lib in multi-threaded web apps compiled to wasm32-unknown-unknown (#5851)
* feat(wasm): make syntax trees sendable

* test(wasm): transfer trees across workers

* test(wasm): use JSON grammar for tree transfer

* test(wasm): edit trees across workers

* test(wasm): share dlmalloc with tree-sitter

* test(wasm): simplify worker tree exchange

* test(wasm): drive tree exchange from Rust

* test(wasm): split sendable-tree xtask

* test(wasm): generalize Rust web fixture

* test(wasm): exercise parallel Rust tree access

* fix(wasm): use Rust global allocator for C core

* test(wasm): use default Rust allocator

* feat(wasm): support external scanners in Rust web apps

* Simplify example further, add a readme

* Regenerate wasm-stdlib

* Fix wasm_stdlib check script

* Vendor the Wasm standard library subset

* Test Unicode Ruby scanner behavior in Wasm

* Make Wasm tree languages instance-aware

* Test multi-threaded use of queries in wasm32-unknown

* Refactor reference-counted language storage

* Check ABI version compat before loading rest of language

* 🎨 Remove redundant #ifdef block

* Reject unsupported Rust Wasm builds on 0.26
2026-08-14 17:05:42 -07:00
Will Lillis 21e3614b8d perf(lib): fast path for ASCII characters in ts_lexer__advance
Yields a 0-5% improvement on parse speed (very grammar dependent).
2026-08-14 18:11:27 +02:00
Will Lillis 730946cfd5 perf(lib): fast path for ASCII characters in ts_lexer__get_lookahead
Yields a ~2% improvement on parse speed.
2026-08-14 18:11:27 +02:00
Christian Clason 277f53f886 build(swift)!: drop Package.swift
Problem: `Package.swift` build script is unmaintained and leads to build
errors.

Solution: Remove `Package.swift`; downstream tools should rely on the
swift-tree-sitter bindings (or, if they want a custom bare-metal build,
handle this in their own build scripts).
2026-08-14 12:51:38 +02:00
Christian Clason f8e735212b build(deps): bump binaryen to v132 2026-08-14 09:33:39 +02:00
Tim Vermeulen f30ee2b300 fix(parser): nest error children during recovery 2026-08-14 00:51:11 -04:00
Tim Vermeulen 869638f6cf fix(lib): accumulate error costs through hidden error nodes 2026-08-14 00:51:11 -04:00
Will Lillis c279989118 fix(xtask): check for vitest rather than chai and mocha
`test::run_wasm` gated dependency installation on `node_modules/chai` and
`node_modules/mocha`. Neither is a dependency (vitest is used instead),
so the check never passed and every `cargo xtask test-wasm`
reran `npm install`.
2026-08-14 00:13:54 -04:00
Will Lillis c68605f18d fix(rust)!: tie language string lifetimes to the proper containers
`Node::kind`, `Node::grammar_name`, the field name accessors,
`TreeCursor::field_name`, and the `Language` name lookups returned `&'static
str` while pointing into storage owned by the `TSLanguage`. Releasing the last
handle to a Wasm language frees that storage.

`Node` and `TreeCursor` now return `&'tree str`, which is ok because
`ts_tree_new` takes a reference to the language via `ts_language_copy` and
holds it until `ts_tree_delete`. The `Language` lookups return strings borrowed
from `&self`.
2026-08-14 00:13:54 -04:00
Will Lillis 7be497a14e fix(web)!: report null when lookahead iterator is not positioned
`currentType` and `currentTypeId` return `null` before the first iteration
step, after exhaustion, and after a reset.

`currentType` also falls back to the language's own name table instead of the
literal 'ERROR' when `Language.types` has no entry (auxiliary symbols). Other
bindings report `end` where this one reported 'ERROR'.
2026-08-14 00:13:54 -04:00
Will Lillis 081929092f fix(rust)!: make lookahead accessors fallible and iteration fused
`current_symbol` and `current_symbol_name` return an `Option` (`None` when the
iterator is not positioned on a symbol). Change `iter_names`'s item from
`&'static str` to `&str`.
2026-08-14 00:13:54 -04:00
Will Lillis 7bf4a10995 fix(lib)!: make lookahead iterator exhaustion sticky and retain its language
Track the iterator's phase so exhaustion is sticky, and gate the symbol name on
it, so `NULL` means "not positioned on a symbol".

`ts_lookahead_iterator_new` was also the only language consuming constructor
that did not `ts_language_copy`, so an iterator outliving a wasm language read
freed memory. Retain in `_new` and `_reset`, release in `_delete`.

Previously:
- `ts_lookahead_iterator__next` left a small parse state's cursor one past its
group end, so re-advancing an exhausted iterator resumed returning `true` and
walked through the rest of `ts_small_parse_table` and off the end of it.

- `ts_lookahead_iterator_current_symbol_name` returned `NULL` only when the
exhausted symbol index happened to fall outside the names table, so a grammar
with aliases returned a real but wrong name instead.
2026-08-14 00:13:54 -04:00
Will Lillis 721b211168 fix(generate)!: reject non-ASCII byte classes in (?-u:...) patterns
`(?-u:...)` switches `regex_syntax` to matching raw bytes, but the lexer
dispatches on decoded characters, so `expand_regex` converted the byte
class with a u8 cast. This is exact for ASCII bytes, and silently
misleading for anything aboove 0x80.

This is not reachable from grammar.js (node and QuickJS both reject
`(?-u:...)`). This change is to guard against future JS runtime changes,
as well as alternative frontends to the generate crate.
2026-08-14 00:13:39 -04:00
Will Lillis 2b15649f93 fix(generate): fold case-insensitive patterns at the AST leaves
A previous fix moved case folding for `/i` patterns out of `regex_syntax` and
into `expand_regex`, so folding could drop the two non-ASCII code points
Unicode simple folding maps onto ASCII letters: the long s `ſ` (U+017F)
onto `s`, and the Kelvin sign `K` (U+212A) onto `k`. Left in, they leak
into otherwise-ASCII tokens and stop those tokens from being extracted as
keywords.

By that point, though, the HIR has already turned a negated class into a
complement, so folding it applies the fold on the wrong side of the
negation. `(?i)[^a-z]` folds a set that contains `A-Z`, which re-admits
`a-z` and leaves a class matching very nearly everything.

`regex_syntax` folds each leaf of a class expression before applying that
leaf's negation and the set algebra above it. Keep that order and change
only the fold: walk the AST, replace each leaf with its fold, and translate
with `case_insensitive(false)`.
2026-08-12 23:40:32 -04:00
Jason Boatman 18cc71a6d9 feat(lib): add state to missing nodes.
This allows them to return possible missing values from a `LookaheadIterator`.
2026-08-12 23:39:44 -04:00
Will Lillis 816a2a0c15
refactor(rust): trim unused imports (#5845)
* Importing `char` brings deprecated methods into scope in some cases.
* Importing `str` isn't necessary in many other cases.
2026-08-12 14:27:52 +02:00
Will Lillis 42f33fe2f8 fix(query): correctly set a state's skipped_quantifier flag when a
zero quantifier skip is performed.

The zero-skip branch currently sets skipped_quantifier unconditionally.
That flag is correct only when the quantified step and its skip target
are _siblings_ at the same query depth. Otherwise, setting it allows for
a "leak" and disables unrelated, "outer" anchors.
2026-08-10 23:59:11 -05:00
dependabot[bot] 5c2533bb7b build(deps): bump the cargo group with 3 updates
Bumps the cargo group with 3 updates: [cc](https://github.com/rust-lang/cc-rs), [clap](https://github.com/clap-rs/clap) and [clap_complete](https://github.com/clap-rs/clap).


Updates `cc` from 1.4.0 to 1.4.1
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/cc-rs/compare/cc-v1.4.0...cc-v1.4.1)

Updates `clap` from 4.6.5 to 4.6.6
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.6.5...clap_complete-v4.6.6)

Updates `clap_complete` from 4.6.8 to 4.6.9
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.6.8...clap_complete-v4.6.9)

---
updated-dependencies:
- dependency-name: cc
  dependency-version: 1.4.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: clap
  dependency-version: 4.6.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: clap_complete
  dependency-version: 4.6.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-10 23:42:21 +02:00
dependabot[bot] 6ea9a7c031 build(deps): bump the npm group across 1 directory with 4 updates
Bumps the npm group with 4 updates in the /lib/binding_web directory: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node), [eslint](https://github.com/eslint/eslint), [tsx](https://github.com/privatenumber/tsx) and [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint).


Updates `@types/node` from 26.1.2 to 26.2.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `eslint` from 10.8.0 to 10.8.1
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.8.0...v10.8.1)

Updates `tsx` from 4.23.1 to 4.23.11
- [Release notes](https://github.com/privatenumber/tsx/releases)
- [Changelog](https://github.com/privatenumber/tsx/blob/master/release.config.cjs)
- [Commits](https://github.com/privatenumber/tsx/compare/v4.23.1...v4.23.11)

Updates `typescript-eslint` from 8.65.0 to 8.66.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.66.0/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 26.2.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm
- dependency-name: eslint
  dependency-version: 10.8.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm
- dependency-name: tsx
  dependency-version: 4.23.11
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm
- dependency-name: typescript-eslint
  dependency-version: 8.66.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-10 23:40:39 +02:00
Will Lillis 543734d286 feat(test): allow corpus tests to use cst outputs
This is already exposed for consumers via the CLI, and is a natural way
to express some test expectations over the sexp form.

Also clean up some repeated logic in the internal test code, and narrow
the cst rendering return type to `std::io::Result` rather than
`anyhow::Result`.
2026-08-09 15:48:37 -05:00
Will Lillis 5fae914f8f fix(query): correctly identify MISSING nodes in queries 2026-08-09 15:48:07 -05:00
Will Lillis 1b916ab754 ci: increase nvim-ts generate max jobs to 10 2026-08-09 12:28:50 -05:00
Will Lillis fad0a62a35 perf(generate): pool parse-table action lists
Every (state, terminal) parse-table entry stored its action list inline as a
32-byte `ParseTableEntry`, but across a grammar those lists are ~98-99% _duplicates_.
The number of distinct lists is a few thousand regardless of grammar size, while
total entries scale into the hundreds of thousands.

Store each unique action list once in a shared `ActionListPool` (a flat arena of
actions plus `(offset, len)` ranges) and replace the inline entry with a 4-byte
`ActionListId` (a pool index with the `reusable` flag packed into the high bit).

- intern_table converts the freshly built `ParseTable<ParseTableEntry>` into
  `ParseTable<ActionListId>`.
- minimize carries and operates on the 4-byte ids. The three global state
  renumberings rewrite Shift targets once at the pool level
  (`remap_terminal_references`), while the per-state unit-reduction redirects
  copy the changed list into a new slot (COW). `mark_fragile_tokens` becomes a
  free bit flip on the id.
- `canonicalize` dedups and compacts the pool once before `render`, dropping the
  dead and duplicate slots the remaps and COW leave behind. `render` assigns the
  output action-list offsets directly.

Yields ~7% wall time reduction, ~12% peak rss reduction.
2026-08-09 12:28:50 -05:00
Will Lillis dbcdd0416b perf(generate): shrink parse/lex/production ids from usize to u32
`ParseStateId`, `LexStateId`, `ProductionInfoId`, and `ReservedWordSetId`
each held a `usize`, while the runtime stores state/field ids as `u16` (so
`u32` has plenty of headroom). Shrinking them saves space for every parse-table
entry, and a number of other data structures.

Reduces wall time by 2-5%, peak rss by 9-12%.
2026-08-09 12:28:50 -05:00
Will Lillis 87d14ddfc6 generate: use the LexStateId typedef for lex state ids
Lex state ids were passed as bare usize in several spots. Route them
through the existing `LexStateId` typedef.
2026-08-09 12:28:50 -05:00
Will Lillis 142c60dca1 perf(generate): memoize auxiliary node info within each state
`get_auxiliary_node_info` scans every entry in the item set to collect the
non-auxiliary parents of a given auxiliary symbol. It was called once per
entry whose next symbol is auxiliary, and the same auxiliary symbol recurs
across many entries in a single state (a state's GOTO on a repeat symbol is
shared by every item advancing over it), so the same full scan was repeated
many times per state. Memoize the result per symbol within a single
`add_actions` call.

Reduces wall time 1-4%, rss flat.
2026-08-09 12:28:50 -05:00
Will Lillis b14413e4f4 perf(generate): inline single-action parse table entries
`ParseTableEntry` held a `Vec<ParseAction>` per entry. `ActionList` keeps up
to one action inline and spills to a Vec only for conflict entries.

Reduces walltime by >20%, peak rss by ~30%.
2026-08-09 12:28:50 -05:00
Will Lillis 2a81995058 perf(generate): memoize get_production_id
The production info depends only on the production, but was recomputed and
deduplicated via a linear deep-equality scan over production_infos for every
reduce item in every state. Cache `prod_id` -> `ProductionInfoId`.

Wall time reduction 0-3%, peak rss neutral.
2026-08-09 12:28:50 -05:00
Will Lillis d6ae19cc6d perf(generate): intern lookahead TokenSets
`ParseItemSetEntry` stores a `LookaheadSetId` into a `LookaheadSetPool` instead
of an owned `TokenSet`. Ids are canonical, so entry hash/eq/clone are integer
ops and state dedup stops walking set words. Unions and single-token inserts
are memoized by id, so the transitive closure and successor-kernel construction
stop re-materializing the same unions per state. Closure additions carry interned
ids with word-token membership precomputed.

For cpp, 3566 distinct lookahead sets back all 49,992 states. For ruby 2000 sets
for 91,991 states, and rust 646 for 16,796.

Reduces wall time by 10-15%, peak rss by 3-5%.
2026-08-09 12:28:50 -05:00
Will Lillis 6659ff57eb perf(generate): vectorize minimize's token_conflicts with precomputed bitsets
The state-merge pass's `token_conflicts` scanned every terminal entry of the
other state per non-shared token. Precompute flat, word-aligned bitsets,
`ConflictBits`, so each call is a handful of word ANDs.

Yields a 5-6% wall time reduction on large grammars, and flat to ~1%
loss on small grammers. Peak RSS unchanged.
2026-08-09 12:28:50 -05:00
Will Lillis b304ac9e56 perf(generate): stop duplicating kernel item sets in parse_state_info
Every state's kernel set was cloned into `parse_state_info` even though the
identical set already lives as the state-dedup map key at the same index
(state ids are the map's insertion order). `ParseStateInfo` now holds the
preceding-symbol sequences plus the dedup map moved out of the builder,
allowing the report path to read kernels via `get_index`. Drops the
duplicate kernel storage, allowing for a small reduction in wall time
and peak rss (1-3%).
2026-08-09 12:28:50 -05:00
Will Lillis 2ce7b409a8 perf(generate): replace coincident state lists with a without-word bitset
`CoincidentTokenIndex` kept a `Vec<ParseStateId>` per token pair whose
only user was keyword identification asking one question: "do all states
where a given pair coincides also allow the word token?" Track that
directly as a second bitset built in the same pass.

Reduces walltime by ~7% for small grammars (i.e. Go), and 20-25% for
larger grammars (i.e. Rust, cpp). Reduces peak rss by ~20% for smaller
grammars and ~40-50% for larger grammars.
2026-08-09 12:28:50 -05:00
Will Lillis 110a8becb8 perf(generate): shrink Symbol::index from usize to u32
This cuts `Symbol`'s size from 16 to 8 bytes. Results in a ~2-7% wall time
reduction, and ~7-10% peak RSS reduction.
2026-08-09 12:28:50 -05:00
Will Lillis fb2052f02d perf(generate): represent the grammar IR as a flat, pooled arena
Replace the owned `Rule`-tree grammar with a flat, pooled representation. `Rule`
nodes live in a `RulePool` arena and reference their children and params by
index (`RuleId`). Strings are interned in a `StrPool` and referred to by `StrId`.
Productions are stored as flat `ProductionStep`/`Production` slices indexed per
variable, rather than as nested vectors hanging off each `SyntaxVariable`. This
drops the per-rule heap allocation of the owned-tree form and is the groundwork
for the later table-building optimizations.

`parse_grammar` builds the pool, the prepare passes rewrite nodes in place, and
`prepare_grammar` returns a `PreparedGrammar` bundle that owns the run's `StrPool`
alongside the grammars. `build_tables`, `node_types`, and `render` borrow that
pool and resolve `StrId`s only at the output boundary.

Shows an average ~10% walltime reduction when combined with the previous
commit, with nearly all of the savings in `build_tables`. The `prepare`
stage is also ~70% faster and much more stable, but this contributes
much less to overall generate time. Improving early stages such as
`prepare` will be important for future interactive uses.
2026-08-09 12:28:50 -05:00
Will Lillis e72c8594fb perf(generate): precompute order-preserving item identity keys
Parse-item identity dominates parse-table construction. `ParseItem`'s
`Hash`, `Eq`, and `Ord` walk the entire production on every call,
comparing per-step precedence, aliases, and field names. These are
stored as strings for named precedences and for every alias and field.

Precompute, once per table build, two dense `u32` ids for every
`(production, dot)` slot by ranking the whole slot universe with the
item-content comparator, now extracted as `ItemContent`. Equal-content
slots are adjacent after sorting and the id increments only at content
boundaries, so comparing the `cmp` id reproduces the structural order
exactly (both an ordering and an equality). A second id, `eq_with_syms`,
refines `cmp` by the preceding symbols, which enter equality only for
items with `has_preceding_inherited_fields`.

`ParseItem` now carries its production's key slice, and `Hash`/`Eq`/`Ord`
become integer ops.

On the current string-based production storage this is performance-neutral
in isolation (the up-front ranking pays back the per-comparison savings).
It is the foundation for the following change to a pooled, integer-keyed
production representation, which makes both the ranking and the item
comparisons cheaper integer work.
2026-08-09 12:28:50 -05:00
Christian Clason 4f7ab22584 ci(release): add nightly builds 2026-08-08 19:09:56 +02:00
Newosko 308aee0c90 fix(lib): continue search for later named siblings in
`ts_tree_cursor_current_status`

By terminating early on `has_later_siblings`, `has_later_named_siblings`
was incorrectly reported as `false` in some cases. This led to the
execution of some queries to terminate early.

Also remove some dead branches inside `ts_tree_cursor_current_status`.

Co-authored-by: Will Lillis <will.lillis24@gmail.com>
2026-08-07 17:54:36 -04:00
Jason Boatman 003b10c280 feat(generate)!: expose OptLevel on generate_parser_for_grammar 2026-08-07 09:47:11 +02:00
Will Lillis 7511e3adaa fix(rust)!: tie query iterators to options lifetime
`QueryMatches` and `QueryCaptures` retain query cursor options while they are
being iterated. However, their types did not preserve the lifetime of the
options' progress callback, allowing the callback to be dropped while it
was still referenced by the cursor.

Add an explicit options lifetime to both iterator types and propagate it
through `matches_with_options` and `captures_with_options`. Use a static
options lifetime for iterators created without options.
2026-08-07 01:15:38 -04:00
Christian Clason 8966c69fcd build(deps): bump wasmtime-c-api to v36.0.13 2026-08-06 01:06:13 -04:00
dependabot[bot] 97e2eeb83c build(deps): bump brace-expansion in /crates/cli/eslint
Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion) from 5.0.8 to 5.0.9.
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/v5.0.8...v5.0.9)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 5.0.9
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-04 09:59:36 +02:00
dependabot[bot] fecce454cb build(deps): bump @types/node
Bumps the npm group with 1 update in the /lib/binding_web directory: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node).


Updates `@types/node` from 26.1.1 to 26.1.2
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 26.1.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-04 09:38:07 +02:00
dependabot[bot] 36865d63bb build(deps): bump the cargo group with 2 updates
Bumps the cargo group with 2 updates: [clap](https://github.com/clap-rs/clap) and [webbrowser](https://github.com/amodm/webbrowser-rs).


Updates `clap` from 4.6.4 to 4.6.5
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.6.4...clap_complete-v4.6.5)

Updates `webbrowser` from 1.2.1 to 1.2.2
- [Release notes](https://github.com/amodm/webbrowser-rs/releases)
- [Changelog](https://github.com/amodm/webbrowser-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/amodm/webbrowser-rs/compare/v1.2.1...v1.2.2)

---
updated-dependencies:
- dependency-name: clap
  dependency-version: 4.6.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: webbrowser
  dependency-version: 1.2.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-04 09:37:48 +02:00
Christian Clason 963b5a5a97 build(deps): bump rquickjs to v0.12.2 2026-07-29 02:21:32 +02:00
dependabot[bot] 4deef2d5ef build(deps): bump eslint
Bumps the npm group with 1 update in the /lib/binding_web directory: [eslint](https://github.com/eslint/eslint).


Updates `eslint` from 10.7.0 to 10.8.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.7.0...v10.8.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 10.8.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-28 00:29:36 +02:00
dependabot[bot] 049dee2634 build(deps): bump the cargo group with 6 updates
Bumps the cargo group with 6 updates:

| Package | From | To |
| --- | --- | --- |
| [anyhow](https://github.com/dtolnay/anyhow) | `1.0.103` | `1.0.104` |
| [clap](https://github.com/clap-rs/clap) | `4.6.3` | `4.6.4` |
| [glob](https://github.com/rust-lang/glob) | `0.3.3` | `0.3.4` |
| [serde](https://github.com/serde-rs/serde) | `1.0.228` | `1.0.229` |
| [serde_json](https://github.com/serde-rs/json) | `1.0.150` | `1.0.151` |
| [thiserror](https://github.com/dtolnay/thiserror) | `2.0.18` | `2.0.19` |


Updates `anyhow` from 1.0.103 to 1.0.104
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.103...1.0.104)

Updates `clap` from 4.6.3 to 4.6.4
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.6.3...clap_complete-v4.6.4)

Updates `glob` from 0.3.3 to 0.3.4
- [Release notes](https://github.com/rust-lang/glob/releases)
- [Changelog](https://github.com/rust-lang/glob/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/glob/compare/v0.3.3...v0.3.4)

Updates `serde` from 1.0.228 to 1.0.229
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.228...v1.0.229)

Updates `serde_json` from 1.0.150 to 1.0.151
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.150...v1.0.151)

Updates `thiserror` from 2.0.18 to 2.0.19
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/2.0.18...2.0.19)

---
updated-dependencies:
- dependency-name: anyhow
  dependency-version: 1.0.104
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: clap
  dependency-version: 4.6.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: glob
  dependency-version: 0.3.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: serde
  dependency-version: 1.0.229
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: serde_json
  dependency-version: 1.0.151
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: thiserror
  dependency-version: 2.0.19
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-27 23:49:19 +02:00
Sjoerd Langkemper 0900f84eab fix(generate): honor right associativity despite a lower-precedence shift
A SHIFT/REDUCE conflict can bundle several shift interpretations with
different precedences against a single reduce. `handle_conflict` weighed them
with only `shift_is_less` and `shift_is_more`, so a lone lower-precedence
shift set `shift_is_less` and the REDUCE won outright, even when another
interpretation tied the REDUCE in precedence and the REDUCE was declared
right-associative. That tie's associativity should have won by shifting, so a
right-associative rule silently became left-associative as soon as an
unrelated lower-precedence rule was added to the grammar.

Track the equal-precedence case explicitly, and in the reduce-wins branch
shift instead when a tying interpretation exists and the reduce actions are
purely right-associative.

Co-authored-by: Will Lillis <will.lillis24@gmail.com>
2026-07-27 02:06:35 -04:00
Christian Clason 4d0f126996 test(web): fix new lint failure 2026-07-25 18:02:19 +02:00
dependabot[bot] ee3fa37bc3 build(deps): bump the npm group across 1 directory with 7 updates
Bumps the npm group with 7 updates in the /lib/binding_web directory:

| Package | From | To |
| --- | --- | --- |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.5.2` | `26.1.1` |
| [dts-buddy](https://github.com/sveltejs/dts-buddy) | `0.7.0` | `0.8.3` |
| [esbuild](https://github.com/evanw/esbuild) | `0.27.7` | `0.28.1` |
| [eslint](https://github.com/eslint/eslint) | `9.39.4` | `10.7.0` |
| [source-map](https://github.com/mozilla/source-map) | `0.7.6` | `0.8.0` |
| [tsx](https://github.com/privatenumber/tsx) | `4.21.0` | `4.23.1` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.58.0` | `8.65.0` |



Updates `@types/node` from 25.5.2 to 26.1.1
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `dts-buddy` from 0.7.0 to 0.8.3
- [Release notes](https://github.com/sveltejs/dts-buddy/releases)
- [Changelog](https://github.com/sveltejs/dts-buddy/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/dts-buddy/compare/v0.7.0...v0.8.3)

Updates `esbuild` from 0.27.7 to 0.28.1
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.27.7...v0.28.1)

Updates `eslint` from 9.39.4 to 10.7.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v9.39.4...v10.7.0)

Updates `source-map` from 0.7.6 to 0.8.0
- [Release notes](https://github.com/mozilla/source-map/releases)
- [Changelog](https://github.com/mozilla/source-map/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mozilla/source-map/compare/0.7.6...v0.8.0)

Updates `tsx` from 4.21.0 to 4.23.1
- [Release notes](https://github.com/privatenumber/tsx/releases)
- [Changelog](https://github.com/privatenumber/tsx/blob/master/release.config.cjs)
- [Commits](https://github.com/privatenumber/tsx/compare/v4.21.0...v4.23.1)

Updates `typescript-eslint` from 8.58.0 to 8.65.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.65.0/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 26.1.1
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: npm
- dependency-name: dts-buddy
  dependency-version: 0.8.3
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm
- dependency-name: esbuild
  dependency-version: 0.28.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm
- dependency-name: eslint
  dependency-version: 10.7.0
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: npm
- dependency-name: source-map
  dependency-version: 0.8.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm
- dependency-name: tsx
  dependency-version: 4.23.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm
- dependency-name: typescript-eslint
  dependency-version: 8.65.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-25 18:02:19 +02:00
Will Lillis 15ea3328e1 fix(parser): restart recovery for invalid tokens in the error state
The early `ts_parser__recover` dispatch for `ERROR_STATE` in
`ts_parser__advance` was dropped in `201b41cf1`. Without it, tokens
with no valid action re-enter `ts_parser__handle_error` per token
and fragment the tree instead of extending one ERROR node. Restore
it to recover the old (pre-0.25) error recovery behavior.
2026-07-25 18:01:29 +02:00
dependabot[bot] 86e1fedfaf build(deps): bump postcss from 8.5.12 to 8.5.23 in /lib/binding_web
Bumps [postcss](https://github.com/postcss/postcss) from 8.5.12 to 8.5.23.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.5.12...8.5.23)

---
updated-dependencies:
- dependency-name: postcss
  dependency-version: 8.5.23
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-25 17:56:46 +02:00
dependabot[bot] 4777d410dd build(deps): bump brace-expansion in /crates/cli/eslint
Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion) from 5.0.7 to 5.0.8.
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/v5.0.7...v5.0.8)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 5.0.8
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-25 17:56:30 +02:00
dependabot[bot] 5d232ff77c build(deps): bump js-yaml from 4.2.0 to 4.3.0 in /lib/binding_web
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.2.0 to 4.3.0.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.2.0...4.3.0)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 4.3.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-25 17:42:33 +02:00
dependabot[bot] 9beff8a998 build(deps): bump brace-expansion in /crates/cli/eslint
Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion) from 5.0.6 to 5.0.7.
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/v5.0.6...v5.0.7)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 5.0.7
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-25 17:42:03 +02:00
Will Lillis 3a76a8c8ca feat(cli)!: rework HTML output modes in highlight
- `--layout <document|line-numbers|fragment>` (default `document`): the
  document structure. `document` is a self-contained page wrapping a plain
  `<div class="highlight"><pre><code>` block, `line-numbers` adds a line-number
  `<table>`, and `fragment` emits only the code markup with no surrounding
  `<head>`/`<style>`/`<body>` so it can be embedded in an existing page.

- `--style <classes|inline|minimal>` (default `classes`): how token colors
  are applied. `classes` uses `class="..."` spans plus a generated `<style>`,
  `inline` bakes colors onto each span so the markup is self-contained, and
  `minimal` emits bare classes with no colors (bring your own stylesheet).

Deprecates the `--css-classes` flag.
2026-07-24 20:56:56 -04:00
Will Lillis d205dc92de fix(rust)!: require parser loggers to be Send + 'static
`Parser::set_logger` boxes the logger callback into a type-erased C
pointer, but `Parser` carries no lifetime parameter. This allows for
two kinds of UB:

- Use-after-free: a logger could capture a non-`'static` borrow, let the
  borrowed value drop, and then dangle the next time the parser logged.
- Data race: a logger could capture a `!Send` value such as an `Rc`. The
  parser could then be moved to another thread and logged from there
  while the original thread still held a clone, racing on the reference
  count.

As a fix, we just require that logger is `Send + 'static`. The
alternative here is to attach a lifetime parameter to `Parser`, but this
is highly breaking for what is mostly a debugging utility. As an escape
hatch `set_logger_unchecked` is added to the public API as an `unsafe fn`
to correctly communicate the risks and invariants that must be held.
2026-07-24 20:56:16 -04:00
Alex Le Blanc 65b5b03c94
fix(highlight): use std::sync::OnceCell for various loader fields
`std::unsync::OnceCell` allows for data races and requires an (incorrect) manual impl of the `Sync` trait.
2026-07-23 18:30:15 -04:00
dependabot[bot] 0a0e77c375 ci: bump the actions group with 3 updates
Bumps the actions group with 3 updates: [actions/checkout](https://github.com/actions/checkout), [actions/setup-node](https://github.com/actions/setup-node) and [vmactions/omnios-vm](https://github.com/vmactions/omnios-vm).


Updates `actions/checkout` from 7.0.0 to 7.0.1
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v7.0.0...v7.0.1)

Updates `actions/setup-node` from 6.4.0 to 7.0.0
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v6.4.0...v7.0.0)

Updates `vmactions/omnios-vm` from 1.3.3 to 1.3.4
- [Release notes](https://github.com/vmactions/omnios-vm/releases)
- [Commits](https://github.com/vmactions/omnios-vm/compare/v1.3.3...v1.3.4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 7.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: actions/setup-node
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: vmactions/omnios-vm
  dependency-version: 1.3.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-20 23:46:28 +02:00
dependabot[bot] 5e37a97684 build(deps): bump the cargo group with 4 updates
Bumps the cargo group with 4 updates: [cc](https://github.com/rust-lang/cc-rs), [clap](https://github.com/clap-rs/clap), [clap_complete_nushell](https://github.com/clap-rs/clap) and [bitflags](https://github.com/bitflags/bitflags).


Updates `cc` from 1.2.66 to 1.2.67
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/cc-rs/compare/cc-v1.2.66...cc-v1.2.67)

Updates `clap` from 4.6.1 to 4.6.2
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.6.1...clap_complete-v4.6.2)

Updates `clap_complete_nushell` from 4.6.0 to 4.6.1
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete_nushell-v4.6.0...clap_complete_nushell-v4.6.1)

Updates `bitflags` from 2.13.0 to 2.13.1
- [Release notes](https://github.com/bitflags/bitflags/releases)
- [Changelog](https://github.com/bitflags/bitflags/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bitflags/bitflags/compare/2.13.0...2.13.1)

---
updated-dependencies:
- dependency-name: cc
  dependency-version: 1.2.67
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: clap
  dependency-version: 4.6.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: clap_complete_nushell
  dependency-version: 4.6.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: bitflags
  dependency-version: 2.13.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-20 23:39:49 +02:00
Will Lillis f45a488dea feat(cli)!: make parent flags a hard requirements via clap
Also correct a parameter name in `print_tree_graph`
2026-07-18 17:00:53 -04:00
Will Lillis 78481a8dfc fix(init): don't lowercase repository url 2026-07-18 15:35:49 -04:00
Will Lillis 1ffd612be5 fix(query): transfer a leading boundary anchor across a zero-matched quantifier
`(P . Q* Y)` with zero `Q` dropped the leading `.`, so `Y` matched at any
position instead of being pinned to the parent's first named child. On a `?`/`*`
zero-skip, when the skipped step is the parent's first child and carries a
leading anchor, transfer that first-child requirement to the skip target.
2026-07-17 17:53:45 -04:00
Will Lillis dfcf73921c fix(query): keep the trailing anchor when a zero-matched quantifier is anchored
on both sides

In `A . Q* . B`, a zero-matched `Q` made both anchors vacuous, so `A` and `B`
were no longer required to be immediate siblings. Only relax the following
anchor on a `?`/`*` zero-skip when the skipped step has no leading anchor of
its own; otherwise the adjacency transfers through the empty run.
2026-07-17 17:53:45 -04:00
Philipp 8fe4f218fb
fix(cli): init --update should not update setup.py after replacing it (#5760) 2026-07-17 17:53:14 -04:00
Christian Clason 8b7c213443 build(deps): bump binaryen to v131 2026-07-17 02:16:46 -04:00
Will Lillis 0e76a0bd53 fix(generate): fold case-insensitive patterns ourselves
Unicode simple case folding maps two non-ASCII code points onto ASCII
letters: the long s `ſ` (U+017F) onto `s`, and the Kelvin sign `K` (U+212A)
onto `k`. So `regex_syntax` pulls them into any case-insensitive pattern,
which is virtually never intended and has two bad effects:

  * such tokens can no longer be extracted as keywords, because they are not
    a subset of an ASCII `word` token (#5607)
  * a broad class like `[^"]` or `\p{L}` carrying `/i` loses `ſ`/`K`, even
    though it legitimately contains them (#5755)

Rather than let `regex_syntax` fold, parse patterns unfolded and fold them
ourselves in `case_fold_ascii_safe`: fold via `regex_syntax`, then drop
`ſ`/`K` only when folding introduced them (they were not already in the
base set). A class that already contains them keeps them.
2026-07-16 21:32:24 -04:00
Kamal Chandra Upreti 49dd7b4079
docs(init): fix broken link 2026-07-15 12:13:07 +00:00
Will Lillis b23c9fceb7 feat(tags)!: include underlying LanguageError in tag
crate's error enum
2026-07-14 19:19:49 -04:00
Will Lillis db078ba439 feat(highlight)!: include underlying LanguageError in highlight
crate's error enum
2026-07-14 19:19:49 -04:00
dependabot[bot] da2838c39c build(deps): bump memchr from 2.8.2 to 2.8.3 in the cargo group
Bumps the cargo group with 1 update: [memchr](https://github.com/BurntSushi/memchr).


Updates `memchr` from 2.8.2 to 2.8.3
- [Commits](https://github.com/BurntSushi/memchr/compare/2.8.2...2.8.3)

---
updated-dependencies:
- dependency-name: memchr
  dependency-version: 2.8.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-13 23:47:28 +02:00
Will Lillis d11d18f746 feat(web): expand Language::load to accept URL filepath
Some checks failed
CI / check-wasm-stdlib (push) Has been cancelled
CI / sanitize (push) Has been cancelled
CI / build (push) Has been cancelled
CI / checks (push) Has been cancelled
2026-07-11 12:56:21 -04:00
Will Lillis ee0847d605 fix(ci): re-set executable permission on release artifacts before zipping
Some checks are pending
CI / checks (push) Waiting to run
CI / sanitize (push) Waiting to run
CI / build (push) Waiting to run
CI / check-wasm-stdlib (push) Waiting to run
2026-07-11 10:25:33 +02:00
ObserverOfTime 08fcb7f2ef fix(templates): replace deprecated method in Package.swift
Some checks are pending
CI / checks (push) Waiting to run
CI / sanitize (push) Waiting to run
CI / build (push) Waiting to run
CI / check-wasm-stdlib (push) Waiting to run
2026-07-11 00:50:56 -04:00
ObserverOfTime 805a7cd0ae fix(templates): add C source files to Python sdist 2026-07-11 00:50:56 -04:00
Andrew Mayo d2193f4dba docs(comment): misleading/wrong comment stating that initial state TSStateId is 0 rather than 1 2026-07-11 00:49:54 -04:00
Will Lillis 07c4ed220e fix(generate): strip non-ASCII case folds
Regexes with the case-insensitive 'i' flag caused unicode simple case folding,
which maps two non-ASCII code points onto ASCII letters:

- `ſ` (U+017F) onto `s`
- the Kelvin sign `K` (U+212A) onto `k`

This pushed such tokens outside an ASCII `word` token, so they failed to
extract as keywords.
2026-07-11 00:46:53 -04:00
Will Lillis 7027979c2b feat(cli)!: don't load all languages via config when lib path is passed
Also, make `--lang-name` without `--lib-path` a hard error
2026-07-11 00:27:13 -04:00
Will Lillis 17a02fef5e fix(rust): address new clippy lints 2026-07-11 00:03:44 -04:00
Julia Hansbrough cc7be1fd47
fix(templates): generated array macros do not compile with C++
Some checks failed
CI / checks (push) Has been cancelled
CI / sanitize (push) Has been cancelled
CI / build (push) Has been cancelled
CI / check-wasm-stdlib (push) Has been cancelled
Deploy Docs / deploy-docs (push) Has been cancelled
Problem: A set of `array_*` macros (`array_push`, `array_extend`, etc) implicitly convert a `void*` into a different pointer type.  In environments that compile these headers as C++, this implicit conversion is an error.

Solution: This commit adds an `_array_cast` macro that uses `decltype` to cast the `void*` to the proper type when compiling as C++.
2026-07-08 08:38:37 +00:00
Will Lillis cce7768b27 docs: specify valid children of supertype nodes 2026-07-08 10:36:50 +02:00
Will Lillis 15da6ddf12 feat(generate): improve error message for supertypes made invalid by
hidden children
2026-07-08 10:36:50 +02:00
Christian Clason e470c32c24 build(deps): bump rquickjs to v0.12.1 2026-07-08 10:35:41 +02:00
dependabot[bot] a1f55783e6 ci: bump korthout/backport-action in the actions group
Some checks failed
CI / checks (push) Has been cancelled
CI / sanitize (push) Has been cancelled
CI / build (push) Has been cancelled
CI / check-wasm-stdlib (push) Has been cancelled
Bumps the actions group with 1 update: [korthout/backport-action](https://github.com/korthout/backport-action).


Updates `korthout/backport-action` from 4.5.2 to 4.6.0
- [Release notes](https://github.com/korthout/backport-action/releases)
- [Commits](https://github.com/korthout/backport-action/compare/v4.5.2...v4.6.0)

---
updated-dependencies:
- dependency-name: korthout/backport-action
  dependency-version: 4.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-07 00:33:16 +02:00
dependabot[bot] bf6f829c83 build(deps): bump the cargo group with 3 updates
Bumps the cargo group with 3 updates: [clap_complete](https://github.com/clap-rs/clap), [rand](https://github.com/rust-random/rand) and [rustc-hash](https://github.com/rust-lang/rustc-hash).


Updates `clap_complete` from 4.6.5 to 4.6.7
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.6.5...clap_complete-v4.6.7)

Updates `rand` from 0.10.1 to 0.10.2
- [Release notes](https://github.com/rust-random/rand/releases)
- [Changelog](https://github.com/rust-random/rand/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-random/rand/compare/0.10.1...0.10.2)

Updates `rustc-hash` from 2.1.2 to 2.1.3
- [Changelog](https://github.com/rust-lang/rustc-hash/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/rustc-hash/compare/v2.1.2...v2.1.3)

---
updated-dependencies:
- dependency-name: clap_complete
  dependency-version: 4.6.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: rand
  dependency-version: 0.10.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: rustc-hash
  dependency-version: 2.1.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-07 00:06:55 +02:00
Will Lillis 9fc2f486a8 docs(queries): clarify behavior of first child anchor example
Some checks failed
Check Bindgen Output / check-bindgen (push) Has been cancelled
CI / checks (push) Has been cancelled
CI / sanitize (push) Has been cancelled
CI / build (push) Has been cancelled
CI / check-wasm-stdlib (push) Has been cancelled
Deploy Docs / deploy-docs (push) Has been cancelled
Check Wasm Exports / check-wasm-exports (push) Has been cancelled
2026-07-01 19:31:05 -04:00
Will Lillis ec872bca82 docs(queries): specify anchor behavior with quantifiers and groups
- An anchor between two patterns is vacuous when an adjacent quantifier
matches zero

- A leading or trailing anchor on a node applies to the nearest matched node
when an adjacent optional is skipped

- An anchor at the edge of a group or alternation is not allowed.
2026-07-01 19:31:05 -04:00
Will Lillis 139b801cce fix(query): apply a trailing anchor when its optional node is skipped
A trailing `.` after an optional node, e.g. `(p (a)+ @a . (b)? @b .)`, sets
`is_last_child` on the `(b)?` step. When `(b)?` matched zero, the zero-skip
jumped past that step to completion, so the last-child requirement was never
enforced.

When a zero-skip would bypass a step carrying `is_last_child`, require that
the last matched node really is the last named child. Gated on the
`alternative_is_skip` edge marker.
2026-07-01 19:31:05 -04:00
Will Lillis a6bc724748 fix(query): make an anchor after a zero-matched quantifier vacuous
A `.` anchor between a quantified pattern and a following node, e.g.
`(parent (comment)* @c . (decl))`, was treated as a leading anchor when the
quantifier matched zero. The zero-skip jumps a state directly onto the anchored
step, where `is_immediate` was enforced even though no sibling had matched before
it. Name that skip edge (`alternative_is_skip`) and, when a state follows it,
record that it skipped the quantifier (`skipped_quantifier`). The
immediate anchor is then supressoed for that state's next match.
2026-07-01 19:31:05 -04:00
Will Lillis b69d00b0fe fix(query): forbid an anchor at the end of a group
A `.` at the end of a group, such as `((comment)+ @c .)`, was silently dropped
during compilation. Reject rather than dropping.
2026-07-01 19:31:05 -04:00
Will Lillis 4915d1bd09 feat(query): enhanced step dumper
Extract the step dump into `ts_query__dump_steps` and enrich it with the control-flow
fields and analysis annotations.
2026-07-01 19:31:05 -04:00
Will Lillis 91fd04f96a fix(query): short-circuit longest-match dedup for disjoint states
An unanchored quantified sibling like `(program (comment)+ @doc (class))`
keeps one match state per matching sibling. A recent fix stopped over-pruning
them, but the per-node longest-match dedup is pairwise, so with n live states
matching became O(n^3).

Two states can only be capture subsets of one another if their captured
byte ranges overlap, so keep each group ordered by first-capture position
and stop the pairwise scan once the rest of the group is disjoint.

Refs neovim/neovim#40517
2026-07-01 18:07:14 -04:00
Will Lillis 57ea5340c6 fix(xtask): eliminate silent errors in zig fetch 2026-06-30 19:37:55 -04:00
dependabot[bot] 5b591babd1 ci: bump actions/cache from 5 to 6 in the actions group
Bumps the actions group with 1 update: [actions/cache](https://github.com/actions/cache).


Updates `actions/cache` from 5 to 6
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-29 23:39:20 +02:00
Christian Clason ae2081989e fix(deps): zig manifest for wasmtime is missing windows hashes
Problem: `zig fetch` on 0.16 can't download zip archives, which wasmtime
uses for Windows. This makes `cargo xtask upgrade-wastime` fail silently
with empty hashes for these platforms.

Solution: Re-run xtask with Zig 0.17 nightly.
2026-06-29 23:18:32 +02:00
Will Lillis 41d3e16eac test(query): regression test for anchored siblings inside a parent 2026-06-28 13:43:53 +02:00
Will Lillis 98c25b9aa0 feat(rust)!: change ts_set_allocator to accept one optional Allocator
object rather than 4 separate optional function pointers.

Helps prevent misuse via mixing different allocators. Also update the
doc comment with relevant safety information.
2026-06-27 23:22:11 -04:00
Will Lillis aec288f83c feat(generate)!: collect warnings as structured data
Allows for use by library consumers, CLI users when the `--json` flag is
passed, and cleans up core logic.
2026-06-27 22:39:24 -04:00
Michael Davis 9991c0f46f Add ts_query_copy for cloning queries
This allows duplicating a query so that it can be modified by using
disable_capture or disable_pattern, without re-parsing the query. The
new `ts_query_copy` creates a deep copy of the entire query object.

A motivation for this is tags.scm code navigation queries. You might
want to have one version of the compiled query capture only definitions
and the other only capture references, since references are much much
more common than definitions in a typical codebase. Instead of
re-parsing and analyzing the query and then calling
`ts_query_disable_capture`, we can clone the built query all-at-once and
then disable captures / patterns.
2026-06-27 17:53:13 -04:00
Lucas M. de Jong Larrarte 99bceec689 fix(query): skip passthrough steps when checking for fallible step splitting
Problem: In queries matching a parent node with anchored children
(siblings) where the first sibling has a quantifier and is not
captured, the check for fallible steps skips splitting the state because
the next node is a passthrough node (and not an is_immediate one). This
prevents the query from matching beyond the first occurrence.

Solution: In the check for fallible steps, skip the next steps if they
are passthrough steps.
2026-06-27 23:44:02 +02:00
Lucas M. de Jong Larrarte 8b43f42dca fix(query): avoid overriding states not seeking immediate match with states seeking immediate match
Problem: In queries matching a parent node with anchored children
(siblings) where the first anchored sibling has a quantifier, the
deduplication logic for states is overly aggressive. The logic causes
the state split on the first capture (with the parent) to be dropped in
favor of the loopback state. This results in the query not being able to
match beyond the first occurrence.

Solution: Prevent the deduplication logic from dropping a state that is
not seeking an immediate match for one that is seeking an immediate
match, since the one not seeking for an immediate match could still
match later nodes.
2026-06-27 23:44:02 +02:00
Lucas M. de Jong Larrarte 1b110f62dd fix(query): take anchors between siblings into account for fallible step splitting
Problem: In queries matching a parent node with anchored children
(siblings), the check for fallible steps only considers nodes with
children, which results in no state split being made for anchored
siblings (node1) . (node2). This prevents the query from matching beyond
the first occurrence.

Solution: Make the check for fallible steps consider also the case where
the next step is at the same depth and must be matched immediately
after.
2026-06-27 23:44:02 +02:00
Christian Clason cdae49be40 ci: update workflows for breaking actions/checkout change
Problem: actions/checkout@v7 refuses to checkout fork pull request code
on `pull_request_target` trigger, breaking backport and reviewers-remove
workflows on external PRs.

Solution:
* backport: Checkout the merged-into base branch by name.
* reviewers-remove: inline script so no checkout needed.
2026-06-27 23:25:18 +02:00
Andrew Gibb cbee467266
docs(readme): add link for cargo-binstall
* Update cli README.md

Added a link to CLI readme to make it clear to an outsider that
cargo-binstall is a different tool than cargo.

* Apply suggestion from @WillLillis

Co-authored-by: Will Lillis <will.lillis24@gmail.com>

---------

Co-authored-by: Christian Clason <ch.clason+github@icloud.com>
Co-authored-by: Will Lillis <will.lillis24@gmail.com>
2026-06-26 08:10:43 -04:00
Christian Clason 8668f2c74f build(deps): bump wasmtime-c-api to v36.0.12 2026-06-26 14:07:35 +02:00
Christian Clason cc85df9440 build(deps): bump wasmtime-c-api to v36.0.11 2026-06-25 22:54:45 -04:00
ALancki1 b360d655cc Clarify behavior of advance(..., skip) in external scanner docs
Clarifies the behavior of `TSLexer::advance` when `skip=true`.

The previous documentation didn't make it clear that `skip=true` should only be used before a token starts. Using it after `mark_end` can affect the token’s starting position and lead to incorrect or zero-length ranges.

Fixes #2315
2026-06-25 22:54:18 -04:00
Will Lillis a8338eccef fix(dsl): forbid invalid field names 2026-06-24 17:38:21 -04:00
dependabot[bot] 68316916e0 ci: bump the actions group with 2 updates
Bumps the actions group with 2 updates: [actions/checkout](https://github.com/actions/checkout) and [vmactions/omnios-vm](https://github.com/vmactions/omnios-vm).


Updates `actions/checkout` from 6.0.3 to 7.0.0
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6.0.3...v7.0.0)

Updates `vmactions/omnios-vm` from 1.3.2 to 1.3.3
- [Release notes](https://github.com/vmactions/omnios-vm/releases)
- [Commits](https://github.com/vmactions/omnios-vm/compare/v1.3.2...v1.3.3)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: vmactions/omnios-vm
  dependency-version: 1.3.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-23 00:04:47 +02:00
dependabot[bot] 29e38c284e build(deps): bump cc from 1.2.64 to 1.2.65 in the cargo group
Bumps the cargo group with 1 update: [cc](https://github.com/rust-lang/cc-rs).


Updates `cc` from 1.2.64 to 1.2.65
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/cc-rs/compare/cc-v1.2.64...cc-v1.2.65)

---
updated-dependencies:
- dependency-name: cc
  dependency-version: 1.2.65
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-22 23:46:36 +02:00
Will Lillis d861eab22b fix(build): use std helper rather than passing -std=c11 flag
MSVC expects `/std:c11`
2026-06-16 23:55:43 -04:00
Will Lillis 6b7e298549 fix(lib): address strict aliasing violations in TreeCursor type 2026-06-16 23:55:43 -04:00
dependabot[bot] 04bac19343 ci: bump vmactions/omnios-vm from 1 to 1.3.2 in the actions group
Bumps the actions group with 1 update: [vmactions/omnios-vm](https://github.com/vmactions/omnios-vm).


Updates `vmactions/omnios-vm` from 1 to 1.3.2
- [Release notes](https://github.com/vmactions/omnios-vm/releases)
- [Commits](https://github.com/vmactions/omnios-vm/compare/v1...v1.3.2)

---
updated-dependencies:
- dependency-name: vmactions/omnios-vm
  dependency-version: 1.3.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-16 10:40:26 +02:00
dependabot[bot] 6ad626c686 build(deps): bump js-yaml from 4.1.1 to 4.2.0 in /lib/binding_web
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.1 to 4.2.0.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/commits)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 4.2.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-16 10:39:58 +02:00
Christian Clason d3314cb62e build(deps): bump rquickjs to v0.12.0 2026-06-15 18:49:48 -04:00
dependabot[bot] d77f764dcd build(deps): bump the cargo group with 2 updates
Bumps the cargo group with 2 updates: [cc](https://github.com/rust-lang/cc-rs) and [memchr](https://github.com/BurntSushi/memchr).


Updates `cc` from 1.2.63 to 1.2.64
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/cc-rs/compare/cc-v1.2.63...cc-v1.2.64)

Updates `memchr` from 2.8.1 to 2.8.2
- [Commits](https://github.com/BurntSushi/memchr/compare/2.8.1...2.8.2)

---
updated-dependencies:
- dependency-name: cc
  dependency-version: 1.2.64
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: memchr
  dependency-version: 2.8.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-15 23:44:16 +02:00
buckynbrocko 49f5027158 Added tree-sitter test option --show-diff-markers to force use of + & - while still coloring output
Also cleanup the trailing newline special casing in the `TestDiff`
`Display` impl.

Co-authored-by: Will Lillis <will.lillis24@gmail.com>
2026-06-13 10:29:37 -04:00
Will Lillis 3ed12db8f0 fix(rust): use more accurate types for various IoError structs 2026-06-13 09:35:12 -04:00
Christian Clason de4b622223 build(deps): bump wasmtime-c-api to v36.0.10 2026-06-10 17:36:02 -04:00
Christian Clason 2fb79a4325 build(deps): bump binaryen to v130 2026-06-10 17:25:15 -04:00
jannschu 519d511488 fix(cli): highlight test did not properly check for spans on later rows
The early exit condition to skip remaining highlights was incorrectly
checking the lexicographical order, which could lead to false
passes if a highlight was on a later row with smaller column number.
2026-06-08 18:09:03 -04:00
dependabot[bot] 811b5308f8 ci: bump actions/checkout from 6.0.2 to 6.0.3 in the actions group
Bumps the actions group with 1 update: [actions/checkout](https://github.com/actions/checkout).


Updates `actions/checkout` from 6.0.2 to 6.0.3
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6.0.2...v6.0.3)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 6.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-08 23:50:59 +02:00
Will Lillis 4455da7102 fix(rust): more clippy fixes in generate
Additional clippy lints fire when certain modules are temporarily made
public, i.e. for testing purposes. Fixing these reduces clutter and
helps with internal development.
2026-06-05 17:31:40 -04:00
Will Lillis 4154ff23c0 fix(docs): explicitly warn against aliasing supertypes 2026-06-05 17:31:26 -04:00
Will Lillis d10b7272d2 fix(cli): warn user for various malformed tests
- attribute on same line of name
- typo'd attribute
- no closing `=====` line after the test body
2026-06-05 17:31:09 -04:00
Will Lillis d9acc99734 fix(cli): display warning to user if parser test corpus dir isn't found 2026-06-05 17:30:52 -04:00
Will Lillis 0617a59ed6 fix: add DESCRIPTION to grammar Makefile template 2026-06-05 09:43:49 +02:00
dependabot[bot] 5742052982 build(deps): bump the cargo group with 3 updates
Bumps the cargo group with 3 updates: [cc](https://github.com/rust-lang/cc-rs), [log](https://github.com/rust-lang/log) and [memchr](https://github.com/BurntSushi/memchr).


Updates `cc` from 1.2.62 to 1.2.63
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/cc-rs/compare/cc-v1.2.62...cc-v1.2.63)

Updates `log` from 0.4.29 to 0.4.30
- [Release notes](https://github.com/rust-lang/log/releases)
- [Changelog](https://github.com/rust-lang/log/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/log/compare/0.4.29...0.4.30)

Updates `memchr` from 2.8.0 to 2.8.1
- [Commits](https://github.com/BurntSushi/memchr/compare/2.8.0...2.8.1)

---
updated-dependencies:
- dependency-name: cc
  dependency-version: 1.2.63
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: log
  dependency-version: 0.4.30
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: memchr
  dependency-version: 2.8.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-03 12:06:15 +02:00
dependabot[bot] 94d419832e ci: bump actions/checkout from 6 to 6.0.2 in the actions group
Bumps the actions group with 1 update: [actions/checkout](https://github.com/actions/checkout).


Updates `actions/checkout` from 6 to 6.0.2
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v6.0.2)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 6.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-03 12:05:42 +02:00
Trevor Burnham ff4dbc0589
perf(bindings): defer importlib.resources import in python template (#5589)
Co-authored-by: ObserverOfTime <chronobserver@disroot.org>
2026-06-01 23:08:00 -04:00
Will Lillis 15ddfb21ed fix(lib): Consider subtree lookahead bytes when determining whether the
parser can reuse a node.

Lookahead bytes can be used to decide what a node is parsed as, so it's
resaonable to consider this as part of a node's "range" when deciding
which edits affect it.
2026-06-01 18:12:56 -04:00
jannschu f541fec5ad fix(cli): infinite loop in highlight test 2026-05-31 12:37:38 -04:00
Will Lillis cd338a7a43 fix(loader): skip --no-undefined for sanitizer grammar builds
Compiling a grammar with `-fsanitize=address` (or any other sanitizer)
emits a module constructor in the parser object file that references
runtime symbols like `__asan_init`. These runtime-resolved symbols are
incompatible with `-Wl,--no-undefined`, which breaking sanitized test
runs on clang. (gcc happens to paper over this by auto-linking libasan
into the .so)

To fix, detect `-fsanitize=` in the compile flags and omit `--no-undefined`
in that case.
2026-05-31 10:56:40 -04:00
Will Lillis ceef7d179f fix(lib): rewrite ts_subtree__write_to_string to be iterative 2026-05-31 10:56:12 -04:00
Will Lillis 797d2bddc4 fix(cli): make check_consistent_sizes iterative
The recursive helper could overflow with deeply nested trees.
2026-05-31 10:56:12 -04:00
Will Lillis e00bf7bc84 fix(cli): print fuzzing seed unconditionally 2026-05-31 10:56:12 -04:00
Will Lillis f1db12c31c fix(cli): improve soundness of cst range calculation 2026-05-31 10:55:47 -04:00
Will Lillis 4d362b4b98 feat(generate): derive Deserialize on GenerateError and all subtypes
These types all derive `Serialize` already.
2026-05-31 08:45:11 +02:00
Georges Savoundararadj 1da46327b3 fix(lib): use correct TREE_SITTER_HIDE_SYMBOLS macro name in alloc.h
The alloc.h header checked for TREE_SITTER_HIDDEN_SYMBOLS, but the
canonical macro name used everywhere else (api.h, render.rs, setup.py)
is TREE_SITTER_HIDE_SYMBOLS. This mismatch meant that defining
TREE_SITTER_HIDE_SYMBOLS (as the Python binding build does) would not
actually hide the allocator symbols in alloc.h.

Fixes tree-sitter/tree-sitter#5625

Signed-off-by: Georges Savoundararadj <savoundg@amazon.com>
2026-05-30 21:42:31 -04:00
Will Lillis 17125cefa6 fix(lib): update doc comment for ts_parser_parse 2026-05-30 04:11:53 -04:00
dependabot[bot] e39ef3e700 build(deps): bump serde_json from 1.0.149 to 1.0.150 in the cargo group
Bumps the cargo group with 1 update: [serde_json](https://github.com/serde-rs/json).


Updates `serde_json` from 1.0.149 to 1.0.150
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.149...v1.0.150)

---
updated-dependencies:
- dependency-name: serde_json
  dependency-version: 1.0.150
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-26 09:59:47 +02:00
Will Lillis 30c00bc4fe fix(cli): place generate artifacts next to grammar file rather than cwd
- If grammar.js is passed, place it in the parent directory
- If grammar.json  is passed, place it in the grandparent directory
2026-05-24 22:12:56 -04:00
Will Lillis a5c0d24749 fix(test): restore corpus test header blank line and divider tie behavior 2026-05-23 12:32:46 -04:00
dependabot[bot] b1fa9725cf build(deps): bump brace-expansion in /crates/cli/eslint
Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion) from 5.0.5 to 5.0.6.
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/v5.0.5...v5.0.6)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 5.0.6
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-19 10:42:31 +02:00
dependabot[bot] edae7391ed ci: bump korthout/backport-action in the actions group
Bumps the actions group with 1 update: [korthout/backport-action](https://github.com/korthout/backport-action).


Updates `korthout/backport-action` from 4.5.1 to 4.5.2
- [Release notes](https://github.com/korthout/backport-action/releases)
- [Commits](https://github.com/korthout/backport-action/compare/v4.5.1...v4.5.2)

---
updated-dependencies:
- dependency-name: korthout/backport-action
  dependency-version: 4.5.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-19 09:51:24 +02:00
Max Brunsfeld a53c3b03a0
fix(wasm): load supertype tables for ABI 15 grammars (#5605)
The wasm store gated supertype_symbols / supertype_map_slices /
supertype_map_entries copies on abi_version > LANGUAGE_VERSION_WITH_RESERVED_WORDS,
but every other consumer (language.c, query.c) treats those tables as
present when abi_version >= LANGUAGE_VERSION_WITH_RESERVED_WORDS.

A Wasm grammar built at ABI exactly 15 with supertype_count > 0 ends up
with supertype_count copied into the native TSLanguage but supertype_map_slices
left NULL. ts_query__analyze_patterns then calls ts_language_subtypes,
which dereferences self->supertype_map_slices[supertype] and crashes.
2026-05-18 16:33:55 -04:00
Will Lillis f535c3bb97 fix(test): write fixture headers once
Several test functions compile the same grammar fixture.
Tests sharing a grammar name also share a src_dir. Each test also
unconditionally writes the three tree-sitter headers into
src_dir/tree_sitter/, leading to a race.

Write the three headers exactly once per src_dir, controlled via a
global `HashSet`.
2026-05-17 19:55:37 -04:00
Will Lillis 71040925fe perf(cli): stream CST rendering via Display wrappers
Co-authored-by: Amaan Qureshi <git@amaanq.com>
2026-05-17 15:02:08 -04:00
Will Lillis ffea4e8e58 fix(cli): rework coloring abstraction and fix NO_COLOR handling
Co-authored-by: Amaan Qureshi <git@amaanq.com>
2026-05-17 15:02:08 -04:00
Christian Clason d4e7f9793c build(deps): bump wasi-sdk to v33 2026-05-16 11:45:48 +02:00
Christian Clason 3a260021dd build(deps): bump wasmtime-c-api to v36.0.9 2026-05-16 11:45:39 +02:00
dependabot[bot] a858378ce7 build(deps): bump cc from 1.2.61 to 1.2.62 in the cargo group
Bumps the cargo group with 1 update: [cc](https://github.com/rust-lang/cc-rs).


Updates `cc` from 1.2.61 to 1.2.62
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/cc-rs/compare/cc-v1.2.61...cc-v1.2.62)

---
updated-dependencies:
- dependency-name: cc
  dependency-version: 1.2.62
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-12 09:13:21 +02:00
dependabot[bot] aa83bf0402 ci: bump korthout/backport-action in the actions group
Bumps the actions group with 1 update: [korthout/backport-action](https://github.com/korthout/backport-action).


Updates `korthout/backport-action` from 4.5.0 to 4.5.1
- [Release notes](https://github.com/korthout/backport-action/releases)
- [Commits](https://github.com/korthout/backport-action/compare/v4.5.0...v4.5.1)

---
updated-dependencies:
- dependency-name: korthout/backport-action
  dependency-version: 4.5.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-12 09:13:01 +02:00
Will Lillis 50bb81484d fix(generate): consider reserved words when removing unused rules 2026-05-10 23:53:48 -04:00
Will Lillis 3b328005c9 fix(generate): rewrite parse_grammar with forward DFS
The previous implementation of `InputGrammar::normalize` (inlined  in
`parse_grammar` iterated over every variable, checking whether  it was
reachable _backwards_ from teh root by recursing over rules that
referenced it.

This means that each and every top level call re-traversed the entire
graph. The runtime performance of this backwards walk was dependent on
the _order_ of rules as declared in `grammar.js`. All existing grammars
have an ordering that's reasonably friendly to this iteration pattern
(BFS-ish order, top down from the start rule), but this leaves us open
to a catastrophic performance cliff.

Instead, seed a `used` set with the start rule, the word token, and an
names referenced from `extras`/`externals`. Then propagate via direct rule
references. This yields anywhere from a 2-~2200x speedup for
`parse_grammar`. This greatly speeds up `--no-parser` runs, but is
relatively unimportant for `parser.c` generation for _existing_
grammars. The important piece is eliminating the potential cliff.
2026-05-10 23:53:48 -04:00
Will Lillis a376ad491f fix(generate): improve error message for nonterminals used in immediate token rule 2026-05-10 10:39:48 -04:00
𝙽!𝙻 21cfae7b56
Validate Wasm language memory reads (#5569) 2026-05-06 11:54:31 -07:00
Will Lillis 5cac4316db fix(cli): account for process versions > 5 in the parse command's pretty
debug output.

The initial implementation of `--debug pretty` assumed process version
was bounded by `MAX_VERSION_COUNT`. However, we also have to account for
`MAX_VERSION_COUNT_OVERFLOW` as well as `halted_version_count`.
2026-05-05 18:17:07 -04:00
dependabot[bot] bc2802abed build(deps): bump clap_complete from 4.6.2 to 4.6.3 in the cargo group
Bumps the cargo group with 1 update: [clap_complete](https://github.com/clap-rs/clap).


Updates `clap_complete` from 4.6.2 to 4.6.3
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.6.2...clap_complete-v4.6.3)

---
updated-dependencies:
- dependency-name: clap_complete
  dependency-version: 4.6.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-05 08:19:48 +02:00
dependabot[bot] 16e48018b3 ci: bump korthout/backport-action in the actions group
Bumps the actions group with 1 update: [korthout/backport-action](https://github.com/korthout/backport-action).


Updates `korthout/backport-action` from 4.4.0 to 4.5.0
- [Release notes](https://github.com/korthout/backport-action/releases)
- [Commits](https://github.com/korthout/backport-action/compare/v4.4.0...v4.5.0)

---
updated-dependencies:
- dependency-name: korthout/backport-action
  dependency-version: 4.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-05 08:19:21 +02:00
Christian Clason 85f985a778 build(deps): bump wasmtime-c-api to v36.0.8 2026-05-05 08:19:02 +02:00
Will Lillis 8d737aa238 docs: note zero point unbounded behavior in query functions 2026-04-30 04:28:59 -04:00
dependabot[bot] aedab6e0f2 build(deps): bump postcss from 8.5.8 to 8.5.12 in /lib/binding_web
Bumps [postcss](https://github.com/postcss/postcss) from 8.5.8 to 8.5.12.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.5.8...8.5.12)

---
updated-dependencies:
- dependency-name: postcss
  dependency-version: 8.5.12
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-29 11:12:37 +02:00
dependabot[bot] fb3e029911 ci: bump the actions group with 2 updates
Bumps the actions group with 2 updates: [korthout/backport-action](https://github.com/korthout/backport-action) and [actions/setup-node](https://github.com/actions/setup-node).


Updates `korthout/backport-action` from 4 to 4.4.0
- [Release notes](https://github.com/korthout/backport-action/releases)
- [Commits](https://github.com/korthout/backport-action/compare/v4...v4.4.0)

Updates `actions/setup-node` from 6.3.0 to 6.4.0
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v6.3.0...v6.4.0)

---
updated-dependencies:
- dependency-name: korthout/backport-action
  dependency-version: 4.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: actions/setup-node
  dependency-version: 6.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-28 09:30:25 +02:00
dependabot[bot] 6c674cb5bd build(deps): bump cc from 1.2.60 to 1.2.61 in the cargo group
Bumps the cargo group with 1 update: [cc](https://github.com/rust-lang/cc-rs).


Updates `cc` from 1.2.60 to 1.2.61
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/cc-rs/compare/cc-v1.2.60...cc-v1.2.61)

---
updated-dependencies:
- dependency-name: cc
  dependency-version: 1.2.61
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-28 09:29:50 +02:00
Wilco Kruijer fb348c3493 wasm: add Language.loadSync for synchronous loading
Co-authored-by: Amaan Qureshi <git@amaanq.com>
2026-04-26 18:58:50 -04:00
Amaan Qureshi 43dc8eadbe query: fix finished state heap invariants 2026-04-26 18:03:55 -04:00
Will Lillis 123fb1c13c perf(query): use min-heap for finished_states in next_capture
`ts_query_cursor_next_capture` linearly scanned all finished states to
find the one with the earliest next capture byte offset. With deeply
nested code, this O(n) scan per capture caused the highlight crate to
hang for minutes on large files.

Replace the linear scan with a min-heap over the finished_states array,
keyed by (next_capture_byte_offset, pattern_index, id). The heap is
maintained lazily: ts_query_cursor__advance uses plain array_push
(preserving FIFO insertion order), and next_capture sifts new elements
into the heap on entry via a tracked heap_size boundary. This preserves
the documented "order found" guarantee for next_match while giving
next_capture O(log n) per call.
2026-04-26 18:03:55 -04:00
Will Lillis 361f293a73 fix(query): widen capture list pool from uint16_t to uint32_t
Commit 1f6eac55 ("query: Use uint32_t for capture list IDs") widened
QueryState.capture_list_id to uint32_t and removed the 65536 pool cap,
but left the pool function signatures as uint16_t. This caused silent
truncation when the pool exceeded 65535 entries, leading to a segfault.
2026-04-26 18:03:55 -04:00
Amaan Qureshi e24c109c82 ci: run on illumos 2026-04-26 13:23:32 -04:00
Petr Sumbera 77cb23fccc lib: add Solaris support to the portable endian header
Solaris does not provide <endian.h> or <sys/endian.h>, but it does expose
byte-order definitions and conversion helpers via <sys/isa_defs.h> and
<sys/byteorder.h>.

Add a __sun branch so the portable header defines __BYTE_ORDER and the
htobe*/le*toh conversions on Solaris.

Co-authored-by: Amaan Qureshi <git@amaanq.com>
2026-04-26 13:23:32 -04:00
Daniel Jalkut 0f6780b9a3 lexer: pass code-unit length to U16_NEXT in UTF-16 decoders
`ts_decode_utf16_le` and `ts_decode_utf16_be` passed a byte length to
`U16_NEXT_LE` and `U16_NEXT_BE`, but those macros count `uint16_t` code
units. If a lead surrogate was the last code unit in a chunk, the decoder
could peek past the chunk and combine it with adjacent memory.

This commit passes the code-unit length to the UTF-16 macros, and fails
with `TS_DECODE_ERROR` when a chunk is too short to contain even one full
code unit. This lets the lexer retry with a fresh chunk or advance through
the invalid byte as it already does.

Co-authored-by: Will Lillis <will.lillis24@gmail.com>
Co-authored-by: Amaan Qureshi <git@amaanq.com>
2026-04-26 03:38:47 -04:00
Amaan Qureshi 475c48d1e3 docs: add AI policy
Co-authored-by: Christian Clason <c.clason@uni-graz.at>
2026-04-26 02:58:56 -04:00
Will Lillis 457eb295b7 fix(generate): pass default optimization level in
`generate_parser_for_grammar`

Passing `empty` here causes some grammars (i.e. tree-sitter-cpp) to fail
to generate.
2026-04-24 08:52:18 +02:00
Antonin Delpeuch eccef997df
feat(dist): enable install via cargo binstall (#5533) 2026-04-24 08:52:01 +02:00
Will Lillis 15154504de fix(ci): include fixture lockfile in cache hash 2026-04-23 03:31:00 -04:00
Will Lillis d6e33d0722 test: bump c test fixture to v0.24.2 2026-04-23 03:31:00 -04:00
Volker Mische 4cb11acd46
fix(loader): allow filenames with dots (#5529) 2026-04-22 03:41:04 -04:00
dependabot[bot] 4701df97c0 build(deps): bump the cargo group with 3 updates
Bumps the cargo group with 3 updates: [clap_complete](https://github.com/clap-rs/clap), [webbrowser](https://github.com/amodm/webbrowser-rs) and [bitflags](https://github.com/bitflags/bitflags).


Updates `clap_complete` from 4.6.1 to 4.6.2
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.6.1...clap_complete-v4.6.2)

Updates `webbrowser` from 1.2.0 to 1.2.1
- [Release notes](https://github.com/amodm/webbrowser-rs/releases)
- [Changelog](https://github.com/amodm/webbrowser-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/amodm/webbrowser-rs/compare/v1.2.0...v1.2.1)

Updates `bitflags` from 2.11.0 to 2.11.1
- [Release notes](https://github.com/bitflags/bitflags/releases)
- [Changelog](https://github.com/bitflags/bitflags/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bitflags/bitflags/compare/2.11.0...2.11.1)

---
updated-dependencies:
- dependency-name: clap_complete
  dependency-version: 4.6.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: webbrowser
  dependency-version: 1.2.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: bitflags
  dependency-version: 2.11.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-21 09:38:08 +02:00
dependabot[bot] 5c1dd38c45 ci: bump actions/setup-node from 6 to 6.3.0 in the actions group
Bumps the actions group with 1 update: [actions/setup-node](https://github.com/actions/setup-node).


Updates `actions/setup-node` from 6 to 6.3.0
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v6...v6.3.0)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: 6.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-21 09:37:49 +02:00
Chinmay Dalal 20bdf3a8a0
feat(bindings): update zig template to 0.16 2026-04-20 09:31:18 +02:00
changw98ic aff9b9d92e fix: update broken link to Parser.Language in template
The link `https://tree-sitter.github.io/node-tree-sitter/interfaces/Parser.Language.html` returns a 404 error.

In the current typedoc output, the Language interface is at `/interfaces/Language.html` (without the `Parser.` prefix), as the documentation flattens namespace members.

Fixes #5525
2026-04-17 08:20:05 +02:00
Chinmay Dalal 24a64db7cc build(zig): update to zig 0.16 2026-04-15 14:54:53 +02:00
dependabot[bot] b3a752aa5b ci: bump the actions group with 2 updates
Bumps the actions group with 2 updates: [actions/upload-pages-artifact](https://github.com/actions/upload-pages-artifact) and [actions/github-script](https://github.com/actions/github-script).


Updates `actions/upload-pages-artifact` from 4 to 5
- [Release notes](https://github.com/actions/upload-pages-artifact/releases)
- [Commits](https://github.com/actions/upload-pages-artifact/compare/v4...v5)

Updates `actions/github-script` from 8 to 9
- [Release notes](https://github.com/actions/github-script/releases)
- [Commits](https://github.com/actions/github-script/compare/v8...v9)

---
updated-dependencies:
- dependency-name: actions/upload-pages-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: actions/github-script
  dependency-version: '9'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-14 08:19:47 +02:00
dependabot[bot] 63b7d3b811 build(deps): bump rand from 0.10.0 to 0.10.1
Bumps [rand](https://github.com/rust-random/rand) from 0.10.0 to 0.10.1.
- [Release notes](https://github.com/rust-random/rand/releases)
- [Changelog](https://github.com/rust-random/rand/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-random/rand/compare/0.10.0...0.10.1)

---
updated-dependencies:
- dependency-name: rand
  dependency-version: 0.10.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-14 08:18:47 +02:00
Christian Clason 75fe13e7aa build(deps): cargo update 2026-04-11 18:29:00 -04:00
Christian Clason 8178cfddd8 build(deps): bump wasmtime-c-api to v36.0.7 2026-04-10 00:41:06 +02:00
Christian Clason 78714eb1b8 ci(nvim-ts): further improvements to Neovim build
* Build Neovim on Windows as well
* Use latest stable release instead of `HEAD` as base
* Run parser check (prints ABI)
2026-04-07 22:55:22 +02:00
dependabot[bot] b018c20018 build(deps): bump vite from 7.3.1 to 7.3.2 in /lib/binding_web
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.3.1 to 7.3.2.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v7.3.2/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.3.2/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 7.3.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-07 10:10:18 +02:00
Christian Clason aa2c18895b ci(nvim-ts): build nvim against PR lib
Problem: The nvim-treesitter tests use a prebuilt Neovim, which means
changes to the library are not tested and parser generation is locked at
the ABI Neovim supports.

Solution: Build Neovim against the PR version of the library on Ubuntu
and macOS. On Windows, we still download binaries (because one does not
simply build Neovim on Windows...) so we still can check backward
compatibility.
2026-04-05 13:27:17 +02:00
Will Lillis ec0f2b59ee perf(generate): use bitsets for coincident tokens and TokenConflictMap
- replace `Vec<Vec<ParseStateId>>` with a flat bitset in
  `CoincidentTokenIndex` for membership queries

The previous representation stored full lists of parse state IDs for
each token pair. Since only membership (yes/no) is needed for most
queries, a flat bitset (`Vec<u64>` indexed as `a * n + b`) is
better. Both `(a,b)` and `(b,a)` bits are set during construction
so `contains()` needs no min/max normalization. The original
`entries: Vec<Vec<ParseStateId>>` and `states_with()` method are
retained alongside the bitset, because `identify_keywords` needs
to iterate over the specific parse states where two tokens co-occur
rather than scanning all states. ~8% walltime reduction for
tree-sitter-bash.

- add word-aligned per-row bitsets to `CoincidentTokenIndex` for
  vectorized intersection checks

Row `a` spans `[a * row_words .. (a+1) * row_words]` where
`row_words = n.div_ceil(64)`. This enables callers in
`build_lex_table` to perform word-level AND operations against
`TokenSet::terminal_bits_words()` instead of iterating individual
token indices. ~6% walltime reduction on tree-sitter-bash.

- add `TokenSet::terminal_bits_words()` accessor and `#[inline]` on
  `BitVec::insert_all`

Expose the raw `&[u64]` backing the terminal bitset so callers can
perform word-level bitwise intersection. Mark `insert_all` as
`#[inline]` to allow the compiler to optimize the hot OR loop.
2026-04-04 17:58:29 -04:00
Will Lillis 13a7a04e79 perf(generate): optimize NFA operations and token conflict precomputation
- precompute a flat NFA-state-to-variable-index lookup table

Build a `Vec<usize>` mapping each NFA state ID to its owning
variable index up front in `TokenConflictMap::new`, replacing
repeated binary searches in `compute_conflict_status` with
array lookups. Also simplifies `mark_fragile_tokens` to
collect terminal indices into a `Vec` rather than maintaining
a boolean mask.

- replace `FxHashSet<Vec<u32>>` visited set with `FxHashSet<u64>`
  using a hash-of-states key

In `compute_conflict_status`, the visited-state-set previously
stored cloned `Vec<u32>` NFA state sets. Replace this with an
`FxHashSet<u64>` keyed by hashing the sorted state slice via
`FxHasher`. This eliminates the `Vec` clone on every BFS step.

- reduce allocations in `NfaCursor::group_transitions`

Reuse a single `CharacterSet` buffer across iterations via `assign`
and `mem::take` instead of cloning the input on every raw
transition. Also, replace `Vec::insert` with `Vec::push` when
splitting intersection transitions. The final sort makes
mid-loop ordering irrelevant, and the disjointness of the split
sets guarantees correctness.

- pre-allocate `NfaTransition` result vectors with a capacity of 8
  and use `swap_remove` instead of `remove` when merging duplicate
  transition entries

- lazily compute `within_separator` in `compute_conflict_status`

Most BFS states have no completions, so the separator check
(which iterates all cursor transitions) is never needed in those
iterations. Wrap it in `Option::get_or_insert_with` to defer
computation until first use.

- add `NfaCursor::transitions_and_any_sep` to fuse the transitions
  and separator-check passes

Callers like `build_lex_table::add_state` previously called
`transitions()` and then `transition_chars().any(|sep|)` separately,
iterating the raw NFA transitions twice. The fused method computes
both in a single pass.
2026-04-04 17:58:29 -04:00
Will Lillis f1142b6e42 perf(generate): use FxHasher over default hash implementation
Replace `std::collections::{HashMap, HashSet}` with
`rustc_hash::{FxHashMap, FxHashSet}` throughout `generate`. `FxHash`
is _much_ faster than the default SipHash for small integer keys,
which are used extensively through `generate`. The one "risk" here is
that `FxHash` is more vulnerable to DDOS attacks, but that isn't as much
of a concern for us here.
2026-04-04 17:58:29 -04:00
Will Lillis 6c8ac80be9 perf(generate): optimize token conflict analysis and symbol handling
- use bitflags for `TokenConflictStatus`

Replace the individual boolean fields on the conflict status type
with a `bitflags!` representation. This takes up significantly less
space and allows for faster combination/testing in `token_conflicts`.

- hoist loop bounds in the merge-join loops of `states_conflict`

Extract `Vec::len()` outside of `while` loops to avoid recomputing
bounds on every iteration. Remove the now-unused `row_offset` /
`does_conflict_at` indirection on `TokenConflictMap`, inlining the
conflict check directly.

- optimize `variable_index_for_nfa_state` in `grammars.rs`

Small optimization to the NFA state-to-variable index mapping.

- refactor symbol key into a proper newtype

Extract the raw `u64` symbol key bit manipulation into a `key_index`
helper function for some extra type safety.
2026-04-04 17:58:29 -04:00
Will Lillis d4db0a32f0 perf(generate): optimize minimize_parse_table with precomputation and compact types
- use a boolean `Vec` instead of `Vec::contains` in `dedup`

Replace linear containment checks (`split_state_ids.contains()`)
with a constant lookup indexed by state ID.

- narrow parse state IDs from `usize` to `u16`

Reduces memory footprint and improves packing in `ParseTable` and
`build_parse_table`. This is more cache-friendly and reduces peak rss by
~1-2%.

- precompute lookup tables for parse table minimization

Build several maps up front (symbol-to-action, nonterminal indices,
symbol keys) rather than recomputing them in the hot minimization
loop. The `SymbolKey` optimization is the largest single win: it
packs each `Symbol`'s type tag and index into a single u64, enabling
single-instruction comparisons in the merge-join that dominates
`states_conflict`. This gives a significant speedup for larger
grammars.

- store ParseTableEntry` refs in the precomputed entry maps to
  avoid `IndexMap::get_index` lookups in the inner loop of
  `states_conflict`

This eliminates an indirect lookup per comparison in the merge-join.

- use unchecked array accesses in hot loops

Where indices are known valid from construction (e.g. the merge-join
in `states_conflict` and dedup), skip bounds checks in the innermost
loops. The compiler can _usually_ prove this and remove the checks
itself, but not always.
2026-04-04 17:58:29 -04:00
Will Lillis 86d99e4f8e fix(ci): include wasi-sdk and binaryen version files in fixture cache
hash
2026-04-04 17:03:58 -04:00
Christian Clason 83d61ea20e build(deps): bump binaryen to v129 2026-04-04 21:32:49 +02:00
Christian Clason 35d4954030 feat(generate): don't require tree-sitter.json for ABI 15
Problem: Many parser projects (about 100 out of ~330 tracked by
nvim-treesitter) still do not contain a `tree-sitter.json` config file,
so `tree-sitter generate` refuses to generate ABI 15 parsers due to the
lack of version information. This prevents these parsers from profiting
from other improvements in ABI 15+ and will eventually make them
unusable when ABI 14 support is dropped.

Solution: Simply use a default version `0.0.0` if no `tree-sitter.json`
is found.
2026-04-04 11:14:43 +02:00
Christian Clason bf37664420 ci(nvim-ts): minor improvements
* remove obsolete msvc-dev-cmd action (runners come with build tools)
* build tree-sitter CLI with --profile optimize (longer build time due
  to LTO, but generate workflow should profit)
* increase generate parallelism to 3
2026-04-04 04:38:21 -04:00
dependabot[bot] 422d7cc503 build(deps): bump the npm group across 1 directory with 4 updates
Bumps the npm group with 4 updates in the /lib/binding_web directory: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node), [dts-buddy](https://github.com/sveltejs/dts-buddy), [esbuild](https://github.com/evanw/esbuild) and [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint).

Updates `@types/node` from 24.10.1 to 25.5.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `dts-buddy` from 0.6.2 to 0.7.0
- [Release notes](https://github.com/sveltejs/dts-buddy/releases)
- [Changelog](https://github.com/sveltejs/dts-buddy/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/dts-buddy/compare/v0.6.2...v0.7.0)

Updates `esbuild` from 0.27.1 to 0.27.4
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/v0.27.4/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.27.1...v0.27.4)

Updates `typescript-eslint` from 8.48.1 to 8.58.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.58.0/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 25.5.0
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: npm
- dependency-name: dts-buddy
  dependency-version: 0.7.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm
- dependency-name: esbuild
  dependency-version: 0.27.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm
- dependency-name: typescript-eslint
  dependency-version: 8.58.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-04 01:21:32 -04:00
Christian Clason 3a4245ba23 build(deps): cargo update 2026-04-04 01:20:58 -04:00
dependabot[bot] 7c1e6861fb build(deps): bump the npm group across 1 directory with 4 updates
Bumps the npm group with 4 updates in the /lib/binding_web directory: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node), [dts-buddy](https://github.com/sveltejs/dts-buddy), [esbuild](https://github.com/evanw/esbuild) and [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint).


Updates `@types/node` from 24.10.1 to 25.5.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `dts-buddy` from 0.6.2 to 0.7.0
- [Release notes](https://github.com/sveltejs/dts-buddy/releases)
- [Changelog](https://github.com/sveltejs/dts-buddy/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/dts-buddy/compare/v0.6.2...v0.7.0)

Updates `esbuild` from 0.27.1 to 0.27.4
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/v0.27.4/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.27.1...v0.27.4)

Updates `typescript-eslint` from 8.48.1 to 8.58.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.58.0/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 25.5.0
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: npm
- dependency-name: dts-buddy
  dependency-version: 0.7.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm
- dependency-name: esbuild
  dependency-version: 0.27.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm
- dependency-name: typescript-eslint
  dependency-version: 8.58.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-03 17:46:45 +02:00
Christian Clason 64698af1ac ci(actions): bump actions/cache to v5
Apparently dependabot doesn't cover the actions/cache/action.yml.
2026-04-01 01:35:49 -04:00
Will Lillis ba17ca14a0 fix(loader): remove CROSS_RUNNER temorary directory path
This isn't used in CI anymore.
2026-03-31 04:00:43 -04:00
Will Lillis 8eafafaa16 fix(test): split up cli testing test
Now that the loader race condition is resolved, these tests no longer
need to be grouped together.
2026-03-31 04:00:43 -04:00
Will Lillis f303a957a3 fix(loader): replace flock-based compilation lock with atomic lock file
The previous two-phase locking scheme (probe existing lock file, then
create and compile) had TOCTOU races between phases that caused
spurious failures in CI when tests compiled grammars concurrently.

Replace with a single-phase approach using `create_new` as the sole
synchronization primitive. An RAII `LockFile` guard ensures cleanup
on drop (including panics). Only "builders" attempt to acquire the
lock. Loaders simply load the file. Builders compile a temporary path
and then rename, so loaders are guaranteed a valid shared library.
The winning builder compiles and then drops the lock. Losers poll for
lock file removal, then load. Stale locks from killed processes are
detected via a timeout and an appropriate error message is displayed
to the user.
2026-03-31 04:00:43 -04:00
Will Lillis 3294a64027 perf(cli): replace regex with manual parsing in test file parser
This started as a simple one to one rewrite, just removing the regexes,
and quickly devolved into a rewrite of the test parsing logic. In
addition to the memory enhancements, the general flow should be much
clearer now. A few data points:

- JS:   walltime -1.3%, peak rss -10.2%
- C:    walltime -5.7%, peak rss -4.5%
- Rust: walltime -3.8%, peak rss -2.2%
2026-03-31 04:00:43 -04:00
Franklin Chen 7c2e757c03 generate: avoid panicking when a supertype only has hidden external token children
This commit skips adding entries to the subtype map when the subtypes
list is empty to avoid a lookup failure in the topological sort during
node type generation.

Co-authored-by: Amaan Qureshi <git@amaanq.com>
2026-03-31 02:12:56 -04:00
futsuuu 0d4f5cb13f fix(loader): don't discard CC wrapper command specified in CC variable 2026-03-31 01:43:43 -04:00
Max Brunsfeld d3ff0ce81d
Fix wasm loading of languages w/ multiple reserved word sets (#5475) 2026-03-30 22:27:57 -07:00
Will Lillis 791d7cead4 perf(cli): minor allocation and write call reductions
- Cache tree byte range length to avoid redundant FFI calls per test
- Combine 5 separate XML attribute write! calls into one
- Pre-allocate format_sexp output buffer to avoid repeated growth
2026-03-31 01:27:05 -04:00
Will Lillis 827bcdabd9 perf(cli): buffer stdout in parse and query output
Wrap stdout in a 64KB BufWriter when writing `parse` output. The tree
walking loop makes many small write calls (parentheses, indentation,
node kinds, ranges, etc.) which are expensive without buffering.

When parsing the jquery.js corpus file, cuts the total time roughly in
half. These savings only show when piping the result to a file,
otherwise terminal rendering time usually dominates, hiding all gains.

Also do the same for the `query` command's output.
2026-03-31 01:27:05 -04:00
Will Lillis 1ec3b3aabe generate: remove comment cleaning for grammar.json
A quick search through github shows that nobody utilizes this, so it's
effectively dead code.
2026-03-31 01:23:15 -04:00
Will Lillis b53ddd3d54 perf(loader): extract name from grammar.json without regex
We have to pay the cost of compiling the regex at runtime, and the
`LazyLock` overhead for each access. The pattern is simple enough that
we can manually parse and extract.

`grammar_json_name` is now ~116x faster on cold start, ~5.6x faster warm
(after the regex has been compiled). Both are fast enough to not matter
much in practice, but some perf gains and eliminating global state is a
win.
2026-03-31 01:23:15 -04:00
dependabot[bot] 1fcbacfff9 ci: bump the actions group with 2 updates
Bumps the actions group with 2 updates: [actions/configure-pages](https://github.com/actions/configure-pages) and [actions/deploy-pages](https://github.com/actions/deploy-pages).


Updates `actions/configure-pages` from 5 to 6
- [Release notes](https://github.com/actions/configure-pages/releases)
- [Commits](https://github.com/actions/configure-pages/compare/v5...v6)

Updates `actions/deploy-pages` from 4 to 5
- [Release notes](https://github.com/actions/deploy-pages/releases)
- [Commits](https://github.com/actions/deploy-pages/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/configure-pages
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: actions/deploy-pages
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-31 00:34:05 +02:00
dependabot[bot] e65b7f2875 build(deps): bump cc from 1.2.57 to 1.2.58 in the cargo group
Bumps the cargo group with 1 update: [cc](https://github.com/rust-lang/cc-rs).


Updates `cc` from 1.2.57 to 1.2.58
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/cc-rs/compare/cc-v1.2.57...cc-v1.2.58)

---
updated-dependencies:
- dependency-name: cc
  dependency-version: 1.2.58
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-31 00:24:03 +02:00
Will Lillis 01dc3dba18 fix(parser): sort error recovery reductions for deterministic ordering
The `LookaheadIterator` visits symbols in group order for small parse
states, which can produce `reduce_actions` in a different order than the
original linear symbol scan (pre c1379718). Since reductions are applied
sequentially and the last reduction version survives, different orderings
lead to different error recovery outcomes (e.g. losing nodes from ERROR
trees).

Sort the reduce_actions array by symbol (descending) after collection.
The array is typically 1-5 entries, so the insertion sort cost is
negligible and the full optimization speedup is preserved.
2026-03-29 21:36:29 -04:00
Will Lillis 0535b0ca37 fix(lib): correct various typos 2026-03-28 05:23:10 -04:00
Will Lillis 9a48405a87 fix(docs): correct various typos 2026-03-28 05:23:10 -04:00
Will Lillis cf8719f132 fix(rust): correct various typos 2026-03-28 05:23:10 -04:00
Will Lillis 1a82df3cac fix(rust): address new nightly lints 2026-03-28 05:23:10 -04:00
Will Lillis c1379718ab perf(parser): use LookaheadIterator in error recovery's all-symbol scan
In `ts_parser__do_all_potential_reductions`, when `lookahead_symbol` is
0 (error recovery), the code scanned every symbol from 1 to
`token_count` calling ts_language_table_entry for each with most returning
empty. Replace with `LookaheadIterator` which efficiently visits only symbols
with valid actions, yielding 7-20x fewer lookups for typical grammars.

Error recovery throughput improves ~29% (JS) / ~39% (C). Valid-code
parsing is unaffected.
2026-03-28 05:21:50 -04:00
Aaron Paterson cb01ffcc3f docs: update WASM build requirements for wasi-sdk
Since v0.26.1, `tree-sitter build --wasm` uses wasi-sdk instead of
Emscripten. The CLI automatically downloads wasi-sdk on first use,
so Emscripten, Docker, and Podman are no longer required.

Update the web binding README to reflect this change.

See #4393 for the original switch to wasi-sdk.
2026-03-27 15:07:54 +01:00
dependabot[bot] cc2ed5e85f build(deps): bump brace-expansion in /crates/cli/eslint
Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion) from 5.0.2 to 5.0.5.
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/v5.0.2...v5.0.5)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 5.0.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-27 14:57:11 +01:00
dependabot[bot] e957e8ce0d build(deps): bump flatted from 3.4.1 to 3.4.2 in /crates/cli/eslint
Bumps [flatted](https://github.com/WebReflection/flatted) from 3.4.1 to 3.4.2.
- [Commits](https://github.com/WebReflection/flatted/compare/v3.4.1...v3.4.2)

---
updated-dependencies:
- dependency-name: flatted
  dependency-version: 3.4.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-27 14:55:25 +01:00
dependabot[bot] 6821fa7611 build(deps): bump flatted from 3.3.2 to 3.4.2 in /lib/binding_web
Bumps [flatted](https://github.com/WebReflection/flatted) from 3.3.2 to 3.4.2.
- [Commits](https://github.com/WebReflection/flatted/compare/v3.3.2...v3.4.2)

---
updated-dependencies:
- dependency-name: flatted
  dependency-version: 3.4.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-27 14:55:06 +01:00
dependabot[bot] fd77bda97a build(deps): bump cc from 1.2.56 to 1.2.57 in the cargo group
Bumps the cargo group with 1 update: [cc](https://github.com/rust-lang/cc-rs).


Updates `cc` from 1.2.56 to 1.2.57
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/cc-rs/compare/cc-v1.2.56...cc-v1.2.57)

---
updated-dependencies:
- dependency-name: cc
  dependency-version: 1.2.57
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-23 23:02:18 +01:00
dependabot[bot] 8769254bda ci: bump actions/create-github-app-token in the actions group
Bumps the actions group with 1 update: [actions/create-github-app-token](https://github.com/actions/create-github-app-token).


Updates `actions/create-github-app-token` from 2 to 3
- [Release notes](https://github.com/actions/create-github-app-token/releases)
- [Commits](https://github.com/actions/create-github-app-token/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/create-github-app-token
  dependency-version: '3'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-23 23:01:49 +01:00
Will Lillis 08d676b417 fix(lib): document invariants that must be upheld for TSInputEdit 2026-03-22 23:17:01 -04:00
Will Lillis df3fa372a6 fix(generate): allow disabling qjs-rt feature from CLI
The workspace dependency for `tree-sitter-generate` did not set
`default-features = false`, so Cargo always enabled its default
features (including `qjs-rt` and thus `rquickjs`) regardless of
the CLI's `--no-default-features` flag.

Additionally, `tree-sitter-generate` failed to compile without the
`load` feature due to unconditional references to `cfg`-gated items.

- Set `default-features = false` on the workspace `tree-sitter-generate`
  dependency so the CLI's feature forwarding actually takes effect.
- Explicitly enable the `load` feature in the CLI's dependency on
  `tree-sitter-generate`, since the CLI needs `load`-gated functions
  unconditionally.
- Gate necessary imports behind `#[cfg(feature = "load")]` to fix
  `tree-sitter-generate`'s build without the `load` feature.
2026-03-21 05:51:10 -04:00
Will Lillis 6011985a6c fix(loader): unconditionally import Mutex
Also, remove  the `LazyLock` for `WASM_TOOL_LOCK`. This isn't necessary
as `Mutex::new` is valid in const contexts
2026-03-17 02:27:50 -04:00
dependabot[bot] af67543764 build(deps): bump anstyle from 1.0.13 to 1.0.14 in the cargo group
Bumps the cargo group with 1 update: [anstyle](https://github.com/rust-cli/anstyle).


Updates `anstyle` from 1.0.13 to 1.0.14
- [Commits](https://github.com/rust-cli/anstyle/compare/v1.0.13...v1.0.14)

---
updated-dependencies:
- dependency-name: anstyle
  dependency-version: 1.0.14
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-16 22:56:08 +01:00
dependabot[bot] 94f48e3ebf build(deps): bump flatted from 3.3.1 to 3.4.1 in /crates/cli/eslint
Bumps [flatted](https://github.com/WebReflection/flatted) from 3.3.1 to 3.4.1.
- [Commits](https://github.com/WebReflection/flatted/compare/v3.3.1...v3.4.1)

---
updated-dependencies:
- dependency-name: flatted
  dependency-version: 3.4.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-14 19:19:06 +01:00
Christian Clason 0dd08ce747 ci(release): publish zip archives for tree-sitter-cli
Problem: Publishing the built tree-sitter-cli artifacts as gzipped
binaries prevents installing them with `cargo binstall` or packaging
them for Windows Package Manager (`winget`).

Solution: Also create a zip archive for each binary, named
`tree-sitter-cli` to match now-common downstream usage. (Most package
managers have separate `tree-sitter-cli` and `tree-sitter` (library)
packages.)
2026-03-13 15:52:21 -04:00
Will Lillis 30d9c675a4 docs: indicate that dashes are not permitted in parser names 2026-03-13 15:35:14 -04:00
Will Lillis 30ed8da439 Revert "feat: allow - in grammar names"
This reverts commit 7d3c321253.
2026-03-13 15:35:14 -04:00
Christian Clason 5f7326afd5 build(deps): bump binaryen to v127 2026-03-13 15:31:37 -04:00
Christian Clason 2c7375bdb2 build(deps): bump wasi-sdk to v32 2026-03-13 15:31:37 -04:00
Christian Clason a8845c066c build(deps): cargo update 2026-03-13 15:28:46 -04:00
Christian Clason 8d656ed837 ci(tests): run required tests on all PRs
Problem: `build` tests are marked as "required" but skipped when only
touching documentation, leading to such PRs not being mergable.

Solution: Run tests on all PRs and all commits to the `master` or
`release-0.x` branches.
2026-03-13 15:21:47 -04:00
Riley Bruins cf302b07d1 fix(query): don't add copies for quantifier steps outside alternations 2026-03-04 10:32:50 +01:00
Riley Bruins a95fff5477 refactor(query): remove alternative_is_immediate
This is implied by `is_pass_through`.
2026-03-04 02:44:53 -05:00
dependabot[bot] 495b069667 ci: bump the actions group with 3 updates
Bumps the actions group with 3 updates: [actions/upload-artifact](https://github.com/actions/upload-artifact), [actions/download-artifact](https://github.com/actions/download-artifact) and [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance).


Updates `actions/upload-artifact` from 6 to 7
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v6...v7)

Updates `actions/download-artifact` from 7 to 8
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v7...v8)

Updates `actions/attest-build-provenance` from 3 to 4
- [Release notes](https://github.com/actions/attest-build-provenance/releases)
- [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md)
- [Commits](https://github.com/actions/attest-build-provenance/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: actions/download-artifact
  dependency-version: '8'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: actions/attest-build-provenance
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-03 00:17:34 +01:00
Will Lillis b27de1038f fix(rust): remove unused dependencies from loader and lib 2026-03-02 09:14:28 +01:00
Laurent Cheylus 2f747dc9b1 fix(loader): link with libc on OpenBSD to compile parser
Fix tree-sitter/tree-sitter#5333

Signed-off-by: Laurent Cheylus <foxy@free.fr>
2026-03-01 17:07:10 -05:00
John Eismeier 1f9b274dfb
chore: fix typos in messages and documentation
Signed-off-by: John E <jeis4wpi@outlook.com>
2026-03-01 11:40:15 +01:00
Riley Bruins 4ae90615d1 chore(parser): return NULL, not false, for incomplete parse
Small nit; `NULL` is returned everywhere else in this function for an
incomplete parse.
2026-03-01 03:41:27 -05:00
Will Lillis 5fa7514fee perf(generate): several small optimizations
* reduce inner loop range in `CoincidentTokenIndex::new`

The indices computed from `a, b` and `b, a` are identical, so there's
no reason to iterate over the entire set of terminal indices in the
inner loop.

* remove redundant check for `does_match_same_string` in
  `token_conflicts`

`does_match_same_string` is already covered within `does_conflict`, so
OR-ing the result of the two together is wasteful.

* mark several functions available for `inline`

Reduces walltime by 0-3% depending on the grammar.

* don't continually reserve space in render buffer

Reserving space up front for containers is great. Doing this in a loop
can lead to _more_ allocations, hurting performance.

* pre-collect terminal indices in `CoincidentTokenIndex`

In `CoincidentTokenIndex::new`, the inner loop re-iterates the
`IndexMap` keys and re-checks is_terminal() for every outer iteration.
This information can be collected once per state, avoiding redundant
calculations.
2026-02-28 18:41:29 +01:00
Will Lillis 83fafba7f0 perf(generate): add basic free list to BitVec's arena allocator
In the naive implementation, the backing arena for `BitVec` frees no
memory, resulting in an increase of ~8-15% in peak rss. To mitigate
against this, the arena manages a simple free list, keyed by block
sizes. This keeps the reduction in wall time within 3% in the worst case
(on par for most grammars) with the naive implementation, and caps the
increase in peak rss to <1% over the current master branch.
2026-02-28 18:41:29 +01:00
Will Lillis 4d787db0b1 perf(generate): iterate set bits only in merge_token_set 2026-02-28 18:41:29 +01:00
Will Lillis 34706d3ef1 perf(generate): back BitVec with a custom arena allocator. 2026-02-28 18:41:29 +01:00
Will Lillis 8e6dea673b perf(generate): pre-alloc some BitVec sizes
We don't pre-allocate _all_ `BitVec`s, as this causes a ~20% increase in
peak rss with little to no performance gain. Certain `TokenSet`s are
guaranteed to be filled out to a fixed size, and so reserving the
space (and thus avoiding the arena waste) is safe.
2026-02-28 18:41:29 +01:00
Will Lillis 603c61fcb9 perf(generate): replace SmallBitVec with word-level BitVec for TokenSet
`TokenSet` operations (insert_all_terminals, insert_all, etc.) are called
_many_ times during parser generation. `SmallBitVec` performs these
bit-by-bit, which is incredibly slow compared to word-level operations.

Replace `SmallBitVec` with a custom `BitVec` backed by Vec<u64> that
operates at the word level.
2026-02-28 18:41:29 +01:00
dependabot[bot] f9bcce2e01 build(deps): bump rollup from 4.49.0 to 4.59.0 in /lib/binding_web
Bumps [rollup](https://github.com/rollup/rollup) from 4.49.0 to 4.59.0.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.49.0...v4.59.0)

---
updated-dependencies:
- dependency-name: rollup
  dependency-version: 4.59.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-28 15:11:12 +01:00
dependabot[bot] 3a295c7493 build(deps): bump minimatch in /lib/binding_web
Bumps  and [minimatch](https://github.com/isaacs/minimatch). These dependencies needed to be updated together.

Updates `minimatch` from 9.0.5 to 9.0.9
- [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/minimatch/compare/v9.0.5...v9.0.9)

Updates `minimatch` from 3.1.2 to 3.1.5
- [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/minimatch/compare/v9.0.5...v9.0.9)

---
updated-dependencies:
- dependency-name: minimatch
  dependency-version: 9.0.9
  dependency-type: indirect
- dependency-name: minimatch
  dependency-version: 3.1.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-28 15:08:24 +01:00
dependabot[bot] 6cefea704e build(deps): bump minimatch from 10.2.2 to 10.2.4 in /crates/cli/eslint
Bumps [minimatch](https://github.com/isaacs/minimatch) from 10.2.2 to 10.2.4.
- [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/minimatch/compare/v10.2.2...v10.2.4)

---
updated-dependencies:
- dependency-name: minimatch
  dependency-version: 10.2.4
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-28 15:05:37 +01:00
Marian Buschsieweke a3bca3bc45
fix(wasm): pass target triple to clang (#5385)
Problem: The `clang` binary contained in the WASI-SDK releases downloaded from Github does not work on all platforms (e.g., Alpine/MUSL), but a custom (LLVM) `clang` built for the platform will default to that target, making it impossible to build wasm parsers.

Solution: Always pass `wasm32` target triple when calling clang to compile to wasm.

Notes:
* This assumes the custom `clang` is (installed or linked) to `$TREE_SITTER_WASI_SDK_PATH/bin`.
* This requires a full LLVM clang; Apple clang does not support `wasm` targets. 
* Tree-sitter expects a specified version of WASI-SDK, including a specific `clang` version. Other versions may but are not guaranteed to work.
2026-02-28 10:59:56 +01:00
MFS-code 146ea6e12b fix: skip missing Makefile in version command 2026-02-27 10:01:59 +01:00
Amaan Qureshi 0561079b6e cli(init): extract per-binding generators and named update functions
This commit just refactors the init command's update code, as it's quite messy. The main changes are that most bindings now go through their own generate_{lang} function.
2026-02-27 02:13:03 -05:00
Christian Clason 4d7d35818b build(deps): bump wasmtime to v36.0.6 2026-02-24 14:29:07 -05:00
dependabot[bot] b75abfff09 build(deps): bump anyhow from 1.0.101 to 1.0.102 in the cargo group
Bumps the cargo group with 1 update: [anyhow](https://github.com/dtolnay/anyhow).


Updates `anyhow` from 1.0.101 to 1.0.102
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.101...1.0.102)

---
updated-dependencies:
- dependency-name: anyhow
  dependency-version: 1.0.102
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-24 09:03:07 +01:00
Will Lillis 6ae37120b3 fix(cli): correct condition to perform __init__.py replacement for
`tree-sitter init -u` command
2026-02-23 22:53:06 -05:00
Will Lillis 5210ca0b51 fix(cli): update tree-sitter init -u for recent Package.swift
changes
2026-02-23 22:53:06 -05:00
Will Lillis f1f493e59e fix(cli): actually write updated Package.swift file 2026-02-23 22:53:06 -05:00
Amaan Qureshi 67f848cac9 loader: invalidate cached wasm toolchains on version mismatch
Previously, `get_existing_tool` checked only for the existence of
binaries in the cache directory without verifying their version, meaning bumping the version files had no effect until users manually deleted the cache directories. This commit writes a `.version` marker file after downloading and checks it on subsequent runs, removing stale caches automatically when the expected version changes.
2026-02-23 22:50:20 -05:00
Christian Clason 9b4577c6f2 feat(wasm): bump binaryen to v126 2026-02-22 17:21:20 +01:00
Christian Clason eeebf706cb feat(wasm): bump wasi-sdk to v30 2026-02-22 17:21:20 +01:00
Amaan Qureshi 787800dda6 cli(init): fix Package.swift scanner detection when used as a dependency
The template used `FileManager.default.fileExists(atPath: "src/scanner.c")` with a relative path, which resolves against the process cwd. When SwiftPM evaluates a dependency's manifest, the cwd is the consumer's directory, so the check returns `false` even when `scanner.c` exists, causing undefined symbol linker errors.

This commit uses `Context.packageDirectory`, which is available since `swift-tools-version:5.6`, to resolve the path against the package root. This does bump the minimum tools version from 5.3 to 5.6.
2026-02-22 10:55:16 -05:00
Christian Clason e62bf4ee5f ci: retrigger crates check on PR updates 2026-02-22 15:29:39 +01:00
dependabot[bot] 224598586c build(deps): bump minimatch, eslint and eslint-plugin-jsdoc
Bumps [minimatch](https://github.com/isaacs/minimatch) to 10.2.2 and updates ancestor dependencies [minimatch](https://github.com/isaacs/minimatch), [eslint](https://github.com/eslint/eslint) and [eslint-plugin-jsdoc](https://github.com/gajus/eslint-plugin-jsdoc). These dependencies need to be updated together.


Updates `minimatch` from 3.1.2 to 10.2.2
- [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/minimatch/compare/v3.1.2...v10.2.2)

Updates `eslint` from 9.39.2 to 10.0.1
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v9.39.2...v10.0.1)

Updates `eslint-plugin-jsdoc` from 50.2.4 to 62.7.0
- [Release notes](https://github.com/gajus/eslint-plugin-jsdoc/releases)
- [Commits](https://github.com/gajus/eslint-plugin-jsdoc/compare/v50.2.4...v62.7.0)

---
updated-dependencies:
- dependency-name: minimatch
  dependency-version: 10.2.2
  dependency-type: indirect
- dependency-name: eslint
  dependency-version: 10.0.1
  dependency-type: indirect
- dependency-name: eslint-plugin-jsdoc
  dependency-version: 62.7.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-21 16:48:12 +01:00
dependabot[bot] 8e330c7f24 build(deps): bump ajv from 6.12.6 to 6.14.0 in /crates/cli/eslint
Bumps [ajv](https://github.com/ajv-validator/ajv) from 6.12.6 to 6.14.0.
- [Release notes](https://github.com/ajv-validator/ajv/releases)
- [Commits](https://github.com/ajv-validator/ajv/compare/v6.12.6...v6.14.0)

---
updated-dependencies:
- dependency-name: ajv
  dependency-version: 6.14.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-21 16:43:21 +01:00
Will Lillis d0714e0828 docs: add basic information about creating releases 2026-02-21 15:59:32 +01:00
Will Lillis cf98b9b61e ci: add a dry-run workflow to test rust releases 2026-02-21 15:59:32 +01:00
lucasew 4e9fededff fix(lib): cast NULL in ts_subtree_children macro
Explicitly casting NULL to (Subtree *) in the ternary expression ensures
type consistency. This resolves ambiguous type inference issues encountered
by strict static analysis tools and C-to-Go transpilers (like ccgo).

Signed-off-by: lucasew <lucas59356@gmail.com>
2026-02-21 01:37:03 -05:00
Will Lillis 034780a0c9 fix(generate): unsafe extern -> extern in rendered C parser 2026-02-20 21:40:23 -05:00
Christian Clason a21ee02710 build(deps): bump wasmtime to v36.0.5 LTS
Wasmtime v34 introduced breaking ABI changes to `wasmtime_func_t` and
`wasmtime_table_t` structures. The `__private` field changed from
`size_t` to `void*`, requiring code updates to store complete
`wasmtime_func_t` structures instead of raw indices.

Changes:
- `BuiltinFunctionIndices`: `uint32_t` -> `wasmtime_func_t`
- `stdlib_fn_indices`: `uint32_t*` -> `wasmtime_func_t*`
- `FunctionDefinition.storage_location`: `uint32_t*` -> `void*`
- `get_builtin_extern()`: simplified to return stored func directly
- Lexer functions: use temp array for func storage, store table index
- Zero-initialize `builtin_fn_indices` so `store_id == 0` sentinel
  reliably detects missing stdlib exports
- Free `lexer_funcs` on error paths in `ts_wasm_store_new`
- Remove stale `(uint32_t *)` casts from `lexer_definitions`

Co-Authored-By: Amaan Qureshi <git@amaanq.com>
Co-Authored-By: nzinfo <li.monan@gmail.com>
2026-02-19 23:40:40 -05:00
Sergey Reshetnikov 5e23ccaac2 fix(lib): add RedoxOS support to portable/endian.h 2026-02-18 16:36:15 +01:00
Sergey Reshetnikov 5784924c59 use provided by RedoxOS byte orders implementations 2026-02-18 16:36:15 +01:00
Will Lillis 256df9304f rust: explicitly align CLI's encoding enum with constants from
tree_sitter::ffi
2026-02-18 01:54:20 -05:00
Will Lillis 27e1199387 feat(highlight)!: utf16 support 2026-02-18 01:54:20 -05:00
Amaan Qureshi e4aacc9a91 loader: replace fs4 with std file locking 2026-02-18 00:59:34 -05:00
Amaan Qureshi b8fcf27661 build(rust): update deps 2026-02-18 00:59:34 -05:00
Amaan Qureshi ebd2a56b5d treewide: add taplo config and reformat toml files 2026-02-18 00:59:34 -05:00
Amaan Qureshi 921af361cf xtask: fix clippy warnings from edition bump 2026-02-18 00:59:34 -05:00
Amaan Qureshi 7deb5092c6 cli: fix clippy warnings from edition bump 2026-02-18 00:59:34 -05:00
Amaan Qureshi 1c66a7f871 loader: fix clippy warnings from edition bump 2026-02-18 00:59:34 -05:00
Amaan Qureshi 1aa2fec173 tags: fix clippy warnings from edition bump 2026-02-18 00:59:34 -05:00
Amaan Qureshi 922bae62af highlight: fix clippy warnings from edition bump 2026-02-18 00:59:34 -05:00
Amaan Qureshi 26c52db75b generate: fix clippy warnings from edition bump 2026-02-18 00:59:34 -05:00
Amaan Qureshi 77c8f075f3 lib: fix clippy warnings from edition bump 2026-02-18 00:59:34 -05:00
Amaan Qureshi 0188262b4a rust: bump edition to 2024, MSRV to 1.90, and remove unnecessary allows 2026-02-18 00:59:34 -05:00
Amaan Qureshi 792bba8dab
nix: support single-checkout and multi-workspace direnv setups
The current `.envrc` unconditionally runs `use flake`, which breaks multi-workspace
jj setups where workspace subdirectories each have their own `.envrc`. Entering a
workspace unloads the root's nix env even when the workspace `.envrc` is blocked,
because direnv always unloads the parent env on any `.envrc` transition.

`source_up_if_exists` re-runs the parent `.envrc` and inherits its nix env in a
multi-workspace setup (fast due to caching). The `IN_NIX_SHELL` guard then skips
`use flake path:.`, which only runs in a plain single checkout where no parent
`.envrc` exists and `IN_NIX_SHELL` is unset.
2026-02-18 00:50:06 -05:00
dependabot[bot] 957ee4e634 ci: bump the actions group with 2 updates
Bumps the actions group with 2 updates: [actions/cache](https://github.com/actions/cache) and [actions/setup-node](https://github.com/actions/setup-node).


Updates `actions/cache` from 4 to 5
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v4...v5)

Updates `actions/setup-node` from 4 to 6
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: actions/setup-node
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-16 22:42:58 +01:00
dependabot[bot] 6952936eac build(deps): bump the cargo group with 4 updates
Bumps the cargo group with 4 updates: [cc](https://github.com/rust-lang/cc-rs), [clap](https://github.com/clap-rs/clap), [clap_complete](https://github.com/clap-rs/clap) and [ctrlc](https://github.com/Detegr/rust-ctrlc).


Updates `cc` from 1.2.55 to 1.2.56
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/cc-rs/compare/cc-v1.2.55...cc-v1.2.56)

Updates `clap` from 4.5.57 to 4.5.58
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.57...clap_complete-v4.5.58)

Updates `clap_complete` from 4.5.65 to 4.5.66
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.65...clap_complete-v4.5.66)

Updates `ctrlc` from 3.5.1 to 3.5.2
- [Release notes](https://github.com/Detegr/rust-ctrlc/releases)
- [Commits](https://github.com/Detegr/rust-ctrlc/compare/3.5.1...3.5.2)

---
updated-dependencies:
- dependency-name: cc
  dependency-version: 1.2.56
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: clap
  dependency-version: 4.5.58
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: clap_complete
  dependency-version: 4.5.66
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: ctrlc
  dependency-version: 3.5.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-16 22:42:31 +01:00
dependabot[bot] f3a9b493c0 build(deps): bump the cargo group with 5 updates
Bumps the cargo group with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [anyhow](https://github.com/dtolnay/anyhow) | `1.0.100` | `1.0.101` |
| [clap](https://github.com/clap-rs/clap) | `4.5.56` | `4.5.57` |
| [regex](https://github.com/rust-lang/regex) | `1.12.2` | `1.12.3` |
| [regex-syntax](https://github.com/rust-lang/regex) | `0.8.8` | `0.8.9` |
| [schemars](https://github.com/GREsau/schemars) | `1.2.0` | `1.2.1` |


Updates `anyhow` from 1.0.100 to 1.0.101
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.100...1.0.101)

Updates `clap` from 4.5.56 to 4.5.57
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.56...clap_complete-v4.5.57)

Updates `regex` from 1.12.2 to 1.12.3
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/compare/1.12.2...1.12.3)

Updates `regex-syntax` from 0.8.8 to 0.8.9
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/compare/regex-syntax-0.8.8...regex-syntax-0.8.9)

Updates `schemars` from 1.2.0 to 1.2.1
- [Release notes](https://github.com/GREsau/schemars/releases)
- [Changelog](https://github.com/GREsau/schemars/blob/master/CHANGELOG.md)
- [Commits](https://github.com/GREsau/schemars/compare/v1.2.0...v1.2.1)

---
updated-dependencies:
- dependency-name: anyhow
  dependency-version: 1.0.101
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: clap
  dependency-version: 4.5.57
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: regex
  dependency-version: 1.12.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: regex-syntax
  dependency-version: 0.8.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: schemars
  dependency-version: 1.2.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-10 10:50:33 +01:00
dependabot[bot] 377c3d1c75 build(deps): bump eslint from 9.11.0 to 9.39.2 in /crates/cli/eslint
Bumps [eslint](https://github.com/eslint/eslint) from 9.11.0 to 9.39.2.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v9.11.0...v9.39.2)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 9.39.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-09 22:55:27 -05:00
Amaan Qureshi 9a78267a7b flake: update lockfile 2026-02-09 22:52:13 -05:00
Amaan Qureshi a91efd56f8 ci: cache npm packages for the web bindings 2026-02-09 22:52:13 -05:00
Amaan Qureshi e7389ccc2a make: drop redundant cargo check 2026-02-09 22:52:13 -05:00
Amaan Qureshi b1b0ef9238 ci: skip release build on PRs 2026-02-09 22:52:13 -05:00
Amaan Qureshi ad2f2ae76d ci: don't update msys2 packages 2026-02-09 22:52:13 -05:00
Amaan Qureshi 8b72e64c3c ci: replace setup-emsdk action with manual install + actions/cache 2026-02-09 22:52:13 -05:00
Amaan Qureshi 68b6020e8e ci: download pre-built wasmtime C API instead of compiling from source
ci: cache npm
2026-02-09 22:52:13 -05:00
Amaan Qureshi 4eefcef0fc ci: only run wasm tests on linux x64 and mac arm64 2026-02-09 22:52:13 -05:00
Chad McElligott 081c90b769
web: add default export to CJS bundle 2026-02-09 22:23:12 -05:00
Will Lillis 596a4d69bb fix(query): prevent cross-branch capture contamination in alternations with quantifiers
When a branch inside an alternation has a + or * quantifier, the
quantifier's pass_through step loops back to the branch's first step.
The alternation linking also sets that step's alternative_index to
point to the next branch. This causes the quantifier loop-back to
incorrectly explore other branches in the case of a failed match that
follows a successful match, contaminating captures.

This is corrected by redirecting the quantifier loop-back to a
"clean" copy of the target step without the alternative index pointing
to the next alternation branch.

Co-authored-by: Riley Bruins <ribru17@hotmail.com>
2026-02-09 21:57:29 -05:00
Jeremy Fleischman 7d3c321253 feat: allow - in grammar names
A number of grammars in the wild have this character in their name:

- https://github.com/sogaiu/tree-sitter-janet-simple/pull/7
- https://github.com/tree-sitter/tree-sitter-c-sharp/pull/408
- https://github.com/tree-sitter/tree-sitter-embedded-template/pull/45
- https://github.com/tree-sitter/tree-sitter-ql-dbscheme/pull/7

I read through https://github.com/tree-sitter/tree-sitter/pull/3700 and
https://github.com/tree-sitter/tree-sitter/issues/3637, and it doesn't
look like there was much discussion about this name regex, so hopefully
this is an acceptable change?
2026-02-09 21:28:04 -05:00
Will Lillis 48e921883a fix(rust)!: return u32 from Node::child_count
This matches the type returned from the underlying C library, and also
composes more cleanly with other functions like `Node::child`
2026-02-09 21:05:48 -05:00
Will Lillis 9dd09fec1e fix(cli): require tree-sitter CLI in CMake template 2026-02-08 18:40:57 -05:00
Amaan Qureshi 22cda59a19 lib: clean up strict aliasing fixes in array.h 2026-02-08 11:09:31 +01:00
Amaan Qureshi a234575b04 nix: fix clangd not working 2026-02-08 11:09:31 +01:00
Will Lillis 73b2f981d5 fix(cli): allow for both debug logs and graphs 2026-02-07 17:03:56 -05:00
Will Lillis c4acd92930 fix(loader): download wasm tools into a tempdir
This ensures the directories are cleaned up even in the case of an
error during or after download.
2026-02-04 22:23:11 -05:00
Will Lillis 953e3f2630 fix(test): acquire lock before downloading wasm tools
Because Rust tests run in parallel, two tests compiling a parser to wasm
can both try to download wasi-sdk and wasm-opt to a common location,
causing corrupted files and/or other failures. To prevent this, we can
guard access to these tools. `cfg(test)` isn't passed across crate
boundaries, so this lock must be present for all build configurations.
2026-02-04 22:23:11 -05:00
Bertie Wheen 39f407e3ff
fix(lib): correct ts_parser_parse docstring (#5295) 2026-02-04 00:49:54 -05:00
dependabot[bot] 73bf3febf7 build(deps): bump bytes from 1.11.0 to 1.11.1
Bumps [bytes](https://github.com/tokio-rs/bytes) from 1.11.0 to 1.11.1.
- [Release notes](https://github.com/tokio-rs/bytes/releases)
- [Changelog](https://github.com/tokio-rs/bytes/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/bytes/compare/v1.11.0...v1.11.1)

---
updated-dependencies:
- dependency-name: bytes
  dependency-version: 1.11.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-03 22:46:35 +01:00
Will Lillis eab567a02d fix(rust): place std::env::set_var inside unsafe blocks
This is optional for the 2021 but required for 2024. The function is
unsafe either way and should be marked as such.
2026-02-02 04:29:13 -05:00
Will Lillis 2338c3d649 rust: bump language crate version
The language crate's version had to be bumped to 0.1.7 on the 0.26
release branch, so bump to 0.1.8 on master
2026-02-01 15:00:22 -05:00
Will Lillis 7e1d8add13 feat(cli): add --verbose flag to build command
This displays the working directory of the command (if present),
compiler used, all of its arguments, any environment variables set,
and anything written to stdout/stderr by the compilation tool.
2026-02-01 13:00:02 -05:00
Will Lillis 7100767f64 docs: include info on environment variables for fuzz command 2026-02-01 12:31:13 -05:00
Will Lillis 0b8f124453 fix(cli): include default values for --edits and --iterations in
`fuzz` command help
2026-02-01 12:31:13 -05:00
Will Lillis c9d9ce6cbb fix(cli): use --edits value for fuzz tests 2026-02-01 12:31:13 -05:00
Will Lillis 337e20e0f2 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 18:24:31 -05:00
Will Lillis 8fe076695f test: rename wasm corpus test "wasm_realloc"->"wasm_realloc_overflow_heap" 2026-01-31 18:24:31 -05:00
Will Lillis 439030503c 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 17:59:50 -05:00
Will Lillis 18e2fb1e78 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 17:59:50 -05:00
Christian Clason 69c6ce8c17 build(deps): cargo update 2026-01-31 17:51:43 -05:00
Christian Clason 1850706af0 fix(wasm): don't wasm-opt twice
Now that we run `wasm-opt` explicitly after compilation, skip it during
the `clang` compile and link phase.
2026-01-31 10:43:19 +01:00
Will Lillis 94f4143ad6 generate: return error rather than assert when ABI incompatibility is
detected
2026-01-30 22:45:07 -05:00
Will Lillis e65dc4c3b5 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.
2026-01-30 22:45:07 -05:00
Will Lillis 4a2b8ed299 fix(generate): error if supertype is defined as a terminal rule 2026-01-30 22:35:41 -05:00
Will Lillis 4f8983b89f fix(ci): add wasm-stdlib.h to cache key 2026-01-27 10:25:43 +01:00
Will Lillis 7ad63a865e fix(wasm): return early from calloc if malloc fails
Co-authored-by: trim21 <i@trim21.me>
2026-01-26 21:14:14 -05:00
Will Lillis 634ba3a1f7 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 21:14:14 -05:00
dependabot[bot] 2961ef5135 build(deps): bump the cargo group with 2 updates
Bumps the cargo group with 2 updates: [cc](https://github.com/rust-lang/cc-rs) and [thiserror](https://github.com/dtolnay/thiserror).


Updates `cc` from 1.2.53 to 1.2.54
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/cc-rs/compare/cc-v1.2.53...cc-v1.2.54)

Updates `thiserror` from 2.0.17 to 2.0.18
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/2.0.17...2.0.18)

---
updated-dependencies:
- dependency-name: cc
  dependency-version: 1.2.54
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: thiserror
  dependency-version: 2.0.18
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-26 23:23:15 +00:00
Will Lillis 8816d4494f docs: document parser library symbol checks for build command 2026-01-25 18:19:32 -05:00
Will Lillis c0137208ec fix(rust): address new nightly lint 2026-01-25 17:18:43 -05:00
Will Lillis 8f4df58983 fix(init): correct paths in rust bindings on Windows 2026-01-25 17:18:43 -05:00
Will Lillis 5177b3dc26 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>
2026-01-24 14:18:40 -05:00
Will Lillis 55fdc50e81 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.
2026-01-24 14:18:40 -05:00
Will Lillis d736b259de wasm: build wctype.c as part of stdlib 2026-01-24 14:19:00 +01:00
Christian Clason 397452ead5 fix(wasm): add iswpunct to stdlib 2026-01-24 14:19:00 +01:00
Will Lillis 0ab73028d6 wasm: regenerate stdlib 2026-01-24 03:04:44 -05:00
Sasha Koss ecb854da3b Add missing Wasm shims required by tree-sitter-ruby 2026-01-24 03:04:44 -05:00
Sasha Koss 400c2b5a3b Make __assert_fail static inline to avoid duplicate symbol rust-lld error 2026-01-24 03:04:44 -05:00
Will Lillis 148b4ca1b2 fix(loader): search correct cache directory for wasi-sdk clang 2026-01-23 09:10:30 +01:00
Will Lillis aea4ef2338 feat(xtask): give wasm-opt the wasi-sdk treatment 2026-01-21 21:30:13 -05:00
Will Lillis b1ec6a2586 feat(loader): use wasm-opt to optimize wasm files after compilation
Similar to wasi-sdk's `clang`, if `wasm-opt` cannot be found, binaryen
is downloaded, unzipped, and cached for future use.
2026-01-21 21:30:13 -05:00
dependabot[bot] 6739742fb6 build(deps): bump cc from 1.2.52 to 1.2.53 in the cargo group
Bumps the cargo group with 1 update: [cc](https://github.com/rust-lang/cc-rs).


Updates `cc` from 1.2.52 to 1.2.53
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/cc-rs/compare/cc-v1.2.52...cc-v1.2.53)

---
updated-dependencies:
- dependency-name: cc
  dependency-version: 1.2.53
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-20 10:34:48 +01:00
dependabot[bot] d251226a3c ci: bump actions/github-script from 7 to 8 in the actions group
Bumps the actions group with 1 update: [actions/github-script](https://github.com/actions/github-script).


Updates `actions/github-script` from 7 to 8
- [Release notes](https://github.com/actions/github-script/releases)
- [Commits](https://github.com/actions/github-script/compare/v7...v8)

---
updated-dependencies:
- dependency-name: actions/github-script
  dependency-version: '8'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-20 10:34:40 +01:00
Tam1SH ae8184b8b9 docs(playground): highlight full row for highlighted nodes 2026-01-18 23:48:58 -05:00
Will Lillis 470ecf8996 feat(ci): ensure wasm-stdlib.h is regenerated when wasm stdlib source
files are modified.
2026-01-18 22:19:52 -05:00
Will Lillis 0cdb6bef7b fix(cli): warn user when nm can't be run to verify the symbols inside
the parser being built
2026-01-18 22:19:19 -05:00
theanarkh cd603fa981
feat: free memory automatically (#5225) 2026-01-18 14:39:52 -08:00
DanikVitek b12009a746 fix: Clarify/fix lifetimes
- One has to think about lifetimes if a type has one:
  - `<&'a Node<'tree>>::language` now returns `LanguageRef<'tree>` instead of
    `LanguageRef<'a>`, as it should;
- Remove explicit "outlives" requirements from `QueryMatches`, `QueryCaptures`,
  and their impl blocks, because they're inferred
- Removed unnecessary `&mut` from `cst_render_node`'s `cursor` parameter
2026-01-17 00:14:21 -05:00
DanikVitek 9f9a0bc410 fix: Renamed TreeCursor<'cursor> into TreeCursor<'tree>,
to be consistant with the usages and reduse confusion
2026-01-17 00:14:21 -05:00
Christian Clason 5d290a2a75 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.
2026-01-15 15:33:37 +01:00
Will Lillis 5808350bfe fix(docs): appease clippy regarding spacing in README 2026-01-15 10:38:57 +01:00
Will Lillis e64e74d5ed docs: adhere to 120 new word column limit for docs 2026-01-14 18:11:42 -05:00
Will Lillis 1a88b26a10 docs: note requirement to rebuild wasm stdlib 2026-01-14 18:11:42 -05:00
dependabot[bot] 6c05cdfb0c build(deps): bump the cargo group with 3 updates
Bumps the cargo group with 3 updates: [cc](https://github.com/rust-lang/cc-rs), [clap_complete](https://github.com/clap-rs/clap) and [serde_json](https://github.com/serde-rs/json).


Updates `cc` from 1.2.51 to 1.2.52
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/cc-rs/compare/cc-v1.2.51...cc-v1.2.52)

Updates `clap_complete` from 4.5.64 to 4.5.65
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.64...clap_complete-v4.5.65)

Updates `serde_json` from 1.0.148 to 1.0.149
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.148...v1.0.149)

---
updated-dependencies:
- dependency-name: cc
  dependency-version: 1.2.52
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: clap_complete
  dependency-version: 4.5.65
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: serde_json
  dependency-version: 1.0.149
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-13 10:13:03 +01:00
Will Lillis aefae11c0d 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.
2026-01-12 19:41:58 -05:00
Kevin Wang 630fa52717 fix(templates): fix python free-threading compatibility 2026-01-09 11:44:41 +02:00
dependabot[bot] eea85f4eff build(deps): bump clap from 4.5.53 to 4.5.54 in the cargo group
Bumps the cargo group with 1 update: [clap](https://github.com/clap-rs/clap).


Updates `clap` from 4.5.53 to 4.5.54
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.53...clap_complete-v4.5.54)

---
updated-dependencies:
- dependency-name: clap
  dependency-version: 4.5.54
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-07 16:42:00 +00:00
Christian Clason cd6672701b fix(wasm): update wasm-stdlib.h 2026-01-06 17:28:39 +01:00
Trim21 f4ca3d95ca
fix(wasm) add common definitions to stdlib (#5199)
Also expose `strlen` through `string.h` instead of `stdio.h`.
2026-01-06 12:01:37 +01:00
skewb1k 17e3c7a5c5 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.
2026-01-04 21:31:11 -08:00
WillLillis dd60d5cff0 feat(cli): fill in missing fields to tree-sitter.json when running
`tree-sitter init -u`
2025-12-31 14:08:09 -05:00
WillLillis f1288ea5c9 fix(cli): increase verbosity of tree-sitter init -u updates
Also, use `info` logs rather than `warn`
2025-12-31 14:08:09 -05:00
Christian Clason 47ae060966 feat(quickjs): add console support for Array 2025-12-31 13:32:09 +01:00
Christian Clason a1893b4420 build(deps): update rquickjs to 0.11.0 2025-12-31 13:32:09 +01:00
skewb1k 999e041d49 docs: add tip about using test --update flag 2025-12-31 01:43:48 -05:00
skewb1k 0d4d854809 feat(cli): make test --update rewrite all corpus files 2025-12-31 01:43:48 -05:00
WillLillis 93d793d249 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.
2025-12-30 17:07:04 +01:00
dependabot[bot] 82486d4b0a build(deps): bump the cargo group with 2 updates
Bumps the cargo group with 2 updates: [cc](https://github.com/rust-lang/cc-rs) and [serde_json](https://github.com/serde-rs/json).


Updates `cc` from 1.2.50 to 1.2.51
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/cc-rs/compare/cc-v1.2.50...cc-v1.2.51)

Updates `serde_json` from 1.0.145 to 1.0.147
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.145...v1.0.147)

---
updated-dependencies:
- dependency-name: cc
  dependency-version: 1.2.51
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: serde_json
  dependency-version: 1.0.147
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-30 17:06:34 +01:00
Firas al-Khalil 5d9605a91e feat(cli): concurrent build of same grammar on different paths 2025-12-29 00:20:05 -05:00
Firas al-Khalil 5293dd683e fix(cli): report library load failure
Instead of panicking somehere else.

This happens on concurrent builds of the the same grammar.
2025-12-29 00:20:05 -05:00
Firas al-Khalil 62effdf128 fix(cli): report context on compile fail 2025-12-29 00:20:05 -05:00
WillLillis 8e4f21aba0 fix(rust): address nightly clippy lint 2025-12-27 17:05:53 -05:00
WillLillis 5208299bbb fix(cli): set language in cwd for all usages of highlight command 2025-12-27 17:05:53 -05:00
Christian Clason ba7350c7ee docs(cli): better description of files generated by init 2025-12-25 13:16:57 +01:00
skewb1k f96d518ebf fix(cli): remove extra newline with --cst
Makes CST output consistent with other formats.
2025-12-24 15:06:48 +01:00
skewb1k d5b82fbbab fix(cli): remove extra indentation with --cst --no-ranges 2025-12-24 15:06:48 +01:00
kevin-hua-kraken a7d8c0cbb2
fix(playground): update query API 2025-12-23 10:36:40 +02:00
dependabot[bot] 24007727d4 build(deps): bump the cargo group with 2 updates
Bumps the cargo group with 2 updates: [cc](https://github.com/rust-lang/cc-rs) and [clap_complete](https://github.com/clap-rs/clap).


Updates `cc` from 1.2.49 to 1.2.50
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/cc-rs/compare/cc-v1.2.49...cc-v1.2.50)

Updates `clap_complete` from 4.5.61 to 4.5.62
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.61...clap_complete-v4.5.62)

---
updated-dependencies:
- dependency-name: cc
  dependency-version: 1.2.50
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: clap_complete
  dependency-version: 4.5.62
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-22 22:18:42 +01:00
dependabot[bot] 6aa63a7213 ci: bump korthout/backport-action from 3 to 4 in the actions group
Bumps the actions group with 1 update: [korthout/backport-action](https://github.com/korthout/backport-action).


Updates `korthout/backport-action` from 3 to 4
- [Release notes](https://github.com/korthout/backport-action/releases)
- [Commits](https://github.com/korthout/backport-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: korthout/backport-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-22 22:18:09 +01:00
Will Lillis eacb95c85d fix(cli): correct discrepancy with cst for --no-ranges 2025-12-16 21:34:03 -05:00
dependabot[bot] 6967640571 ci: bump the actions group with 2 updates
Bumps the actions group with 2 updates: [actions/upload-artifact](https://github.com/actions/upload-artifact) and [actions/download-artifact](https://github.com/actions/download-artifact).


Updates `actions/upload-artifact` from 5 to 6
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v5...v6)

Updates `actions/download-artifact` from 6 to 7
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: actions/download-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-15 22:26:50 +01:00
skewb1k 4ac2d5d276 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.
2025-12-14 21:34:50 -05:00
skewb1k 642b56d9af fix(docs): remove conflicting --release cargo flag in contributing.md
The argument '--release' cannot be used with '--profile <PROFILE-NAME>'
2025-12-14 20:35:13 +01:00
Christian Clason 0574fcf256 docs(cli): include information on generated files 2025-12-14 14:26:49 +01:00
Christian Clason 98de2bc1a8 feat: start working on v0.27
* bump tree-sitter crates to 0.27.0
* bump tree-sitter-language to 0.1.7
2025-12-13 14:14:33 +01:00
334 changed files with 27824 additions and 14343 deletions

View file

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

5
.envrc
View file

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

View file

@ -2,6 +2,14 @@ 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,6 +2,14 @@ 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@v4
- uses: actions/cache@v5
id: cache
with:
path: |
@ -21,5 +21,9 @@ 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 = [
{ 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" },
{ 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 = ".*" },
]
# filter out the commits that are not matched by commit parsers
filter_commits = false

6
.github/pull_request_template.md vendored Normal file
View file

@ -0,0 +1,6 @@
### 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. -->

View file

@ -1,16 +0,0 @@
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,
});
};

35
.github/scripts/wasm_stdlib.js vendored Normal file
View file

@ -0,0 +1,35 @@
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,17 +14,20 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7.0.1
with:
persist-credentials: true
ref: ${{ github.event.pull_request.base.ref }}
- name: Create app token
uses: actions/create-github-app-token@v2
uses: actions/create-github-app-token@v3
id: app-token
with:
app-id: ${{ vars.BACKPORT_APP }}
private-key: ${{ secrets.BACKPORT_KEY }}
- name: Create backport PR
uses: korthout/backport-action@v3
uses: korthout/backport-action@v4.6.0
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@v6
uses: actions/checkout@v7.0.1
- name: Set up stable Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1

View file

@ -26,12 +26,14 @@ 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 }
@ -44,6 +46,9 @@ 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 }
@ -68,7 +73,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7.0.1
- name: Set up cross-compilation
if: matrix.cross
@ -90,7 +95,7 @@ jobs:
- name: Cache Emscripten SDK
if: contains(matrix.features, 'wasm') && (matrix.run-wasm-test || !inputs.run-test)
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: emsdk
key: emsdk-${{ env.EMSCRIPTEN_VERSION }}-${{ runner.os }}-${{ runner.arch }}
@ -111,17 +116,22 @@ jobs:
- name: Set up Node.js
if: contains(matrix.features, 'wasm') && (matrix.run-wasm-test || !inputs.run-test)
uses: actions/setup-node@v4
uses: actions/setup-node@v7.0.0
with:
node-version: 24
cache: npm
cache-dependency-path: lib/binding_web/package-lock.json
- 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: |
@ -191,7 +201,7 @@ jobs:
WASMTIME_REPO: https://github.com/bytecodealliance/wasmtime
- name: Build C library (make)
if: runner.os != 'Windows'
if: runner.os != 'Windows' && !matrix.vm
run: |
if [[ $PLATFORM == linux-arm ]]; then
CC=arm-linux-gnueabihf-gcc; AR=arm-linux-gnueabihf-ar
@ -205,10 +215,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
CFLAGS: -g -Werror -Wall -Wextra -Wshadow -Wpedantic -Werror=incompatible-pointer-types -Werror=strict-aliasing -Wstrict-aliasing=2
- name: Build C library (CMake)
if: "!matrix.cross"
if: "!matrix.cross && !matrix.vm"
run: |
cmake -S . -B build/static \
-DBUILD_SHARED_LIBS=OFF \
@ -227,6 +237,20 @@ jobs:
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
- name: Build Wasm library
if: contains(matrix.features, 'wasm') && (matrix.run-wasm-test || !inputs.run-test)
shell: bash
@ -245,7 +269,7 @@ jobs:
run: cargo check --no-default-features --target='${{ matrix.target }}'
- name: Build target
if: "!inputs.run-test"
if: "!inputs.run-test && !matrix.vm"
run: cargo build --release --target='${{ matrix.target }}' --features='${{ (matrix.run-wasm-test || !inputs.run-test) && matrix.features || '' }}' $PACKAGE
env:
PACKAGE: ${{ matrix.platform == 'wasm32' && '-p tree-sitter' || '' }}
@ -269,15 +293,19 @@ jobs:
- name: Run main tests
if: inputs.run-test && !matrix.no-run
run: cargo test --target='${{ matrix.target }}' --features='${{ (matrix.run-wasm-test || !inputs.run-test) && matrix.features || '' }}'
run: cargo test --workspace --target='${{ matrix.target }}' --features='${{ (matrix.run-wasm-test || !inputs.run-test) && matrix.features || '' }}'
- name: Run Wasm tests
if: inputs.run-test && !matrix.no-run && contains(matrix.features, 'wasm') && matrix.run-wasm-test
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@v6
uses: actions/upload-artifact@v7
with:
name: tree-sitter.${{ matrix.platform }}
path: target/${{ matrix.target }}/release/tree-sitter${{ contains(matrix.target, 'windows') && '.exe' || '' }}
@ -286,7 +314,7 @@ jobs:
- name: Upload Wasm artifacts
if: "!inputs.run-test && matrix.platform == 'linux-x64'"
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: tree-sitter.wasm
path: |

View file

@ -1,21 +1,14 @@
name: CI
on:
pull_request:
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
branches:
- 'master'
- 'release-[0-9]+.[0-9]+'
pull_request:
branches:
- 'master'
- 'release-[0-9]+.[0-9]+'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@ -26,7 +19,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7.0.1
- name: Set up stable Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
@ -34,13 +27,25 @@ jobs:
toolchain: stable
components: clippy, rustfmt
- name: Lint files
run: |
make lint
make lint-web
- 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
sanitize:
uses: ./.github/workflows/sanitize.yml
build:
uses: ./.github/workflows/build.yml
check-wasm-stdlib:
uses: ./.github/workflows/wasm_stdlib.yml

View file

@ -11,7 +11,7 @@ jobs:
if: contains(github.event.pull_request.labels.*.name, 'ci:check release') || github.event_name == 'workflow_dispatch'
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7.0.1
- name: Set up Rust
uses: actions-rust-lang/setup-rust-toolchain@v1

View file

@ -16,35 +16,29 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Set up Rust
uses: actions-rust-lang/setup-rust-toolchain@v1
uses: actions/checkout@v7.0.1
- 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.4.52 --jq "$jq_expr")
url=$(gh api repos/rust-lang/mdbook/releases/tags/v0.5.4 --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@v5
uses: actions/configure-pages@v6
- name: Upload artifact
uses: actions/upload-pages-artifact@v4
uses: actions/upload-pages-artifact@v5
with:
path: docs/book
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
uses: actions/deploy-pages@v5

View file

@ -3,6 +3,7 @@ name: nvim-treesitter parser tests
on:
pull_request:
paths:
- 'lib/**'
- 'crates/cli/**'
- 'crates/config/**'
- 'crates/generate/**'
@ -25,34 +26,54 @@ jobs:
name: ${{ matrix.os }} - ${{ matrix.type }}
runs-on: ${{ matrix.os }}
env:
NVIM: ${{ matrix.os == 'windows-latest' && 'nvim-win64\\bin\\nvim.exe' || 'nvim' }}
NVIM: ${{ matrix.os == 'windows-latest' && 'nvim.exe' || 'nvim' }}
NVIM_TAG: stable
NVIM_DIR: neovim
NVIM_TS_DIR: nvim-treesitter
steps:
- uses: actions/checkout@v6
- 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
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=2
run: $NVIM -l ./scripts/install-parsers.lua --generate --max-jobs=10
working-directory: ${{ env.NVIM_TS_DIR }}
shell: bash
@ -63,7 +84,13 @@ jobs:
shell: bash
- if: "!cancelled()"
name: Check query files
name: Test parsers
run: $NVIM -l ./scripts/check-parsers.lua
working-directory: ${{ env.NVIM_TS_DIR }}
shell: bash
- if: "!cancelled()"
name: Test queries
run: $NVIM -l ./scripts/check-queries.lua
working-directory: ${{ env.NVIM_TS_DIR }}
shell: bash

View file

@ -1,7 +1,14 @@
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]+
@ -21,11 +28,22 @@ 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@v6
uses: actions/checkout@v7.0.1
- name: Download build artifacts
uses: actions/download-artifact@v6
uses: actions/download-artifact@v8
with:
path: artifacts
@ -44,28 +62,41 @@ 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@v3
uses: actions/attest-build-provenance@v4
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 $GITHUB_REF_NAME \
gh release create ${{ env.TAG_NAME }} $PRERELEASE \
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:
@ -74,7 +105,7 @@ jobs:
needs: release
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7.0.1
- name: Set up Rust
uses: actions-rust-lang/setup-rust-toolchain@v1
@ -90,6 +121,7 @@ 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:
@ -102,10 +134,10 @@ jobs:
directory: [crates/cli/npm, lib/binding_web]
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7.0.1
- name: Set up Node
uses: actions/setup-node@v6
uses: actions/setup-node@v7.0.0
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@v6
uses: actions/checkout@v7.0.1
with:
sparse-checkout: .github/scripts/close_unresponsive.js
sparse-checkout-cone-mode: false
- name: Run script
uses: actions/github-script@v8
uses: actions/github-script@v9
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@v6
uses: actions/checkout@v7.0.1
with:
sparse-checkout: .github/scripts/remove_response_label.js
sparse-checkout-cone-mode: false
- name: Run script
uses: actions/github-script@v8
uses: actions/github-script@v9
with:
script: |
const script = require('./.github/scripts/remove_response_label.js')

View file

@ -11,15 +11,21 @@ jobs:
remove-reviewers:
runs-on: ubuntu-latest
steps:
- 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
- name: Remove reviewers
uses: actions/github-script@v9
with:
script: |
const script = require('./.github/scripts/reviewers_remove.js')
await script({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

@ -15,7 +15,7 @@ jobs:
TREE_SITTER: ${{ github.workspace }}/target/release/tree-sitter
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7.0.1
- 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@v6
uses: actions/checkout@v7.0.1
with:
sparse-checkout: .github/scripts/close_spam.js
sparse-checkout-cone-mode: false
- name: Run script
uses: actions/github-script@v8
uses: actions/github-script@v9
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@v6
uses: actions/checkout@v7.0.1
- 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
CFLAGS: -g -Werror -Wall -Wextra -Wshadow -Wpedantic -Werror=incompatible-pointer-types -Werror=strict-aliasing -Wstrict-aliasing=2
- name: Build Wasm Library
working-directory: lib/binding_web

19
.github/workflows/wasm_stdlib.yml vendored Normal file
View file

@ -0,0 +1,19 @@
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 });

19
.taplo.toml Normal file
View file

@ -0,0 +1,19 @@
[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

View file

@ -1,11 +0,0 @@
{
"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.26.6"
VERSION "0.28.0"
DESCRIPTION "An incremental parsing system for programming tools"
HOMEPAGE_URL "https://tree-sitter.github.io/tree-sitter/"
LANGUAGES C)
@ -33,7 +33,8 @@ if(MSVC)
else()
target_compile_options(tree-sitter PRIVATE
-Wall -Wextra -Wshadow -Wpedantic
-Werror=incompatible-pointer-types)
-Werror=incompatible-pointer-types
-Werror=strict-aliasing -Wstrict-aliasing=2)
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,25 +14,22 @@ members = [
resolver = "2"
[workspace.package]
version = "0.26.6"
authors = [
"Max Brunsfeld <maxbrunsfeld@gmail.com>",
"Amaan Qureshi <amaanq12@gmail.com>",
]
edition = "2021"
rust-version = "1.84"
authors = [ "Max Brunsfeld <maxbrunsfeld@gmail.com>", "Amaan Qureshi <amaanq12@gmail.com>" ]
categories = [ "command-line-utilities", "parsing" ]
edition = "2024"
homepage = "https://tree-sitter.github.io/tree-sitter"
repository = "https://github.com/tree-sitter/tree-sitter"
keywords = [ "incremental", "parsing" ]
license = "MIT"
keywords = ["incremental", "parsing"]
categories = ["command-line-utilities", "parsing"]
repository = "https://github.com/tree-sitter/tree-sitter"
rust-version = "1.90"
version = "0.28.0"
[workspace.lints.clippy]
dbg_macro = "deny"
todo = "deny"
pedantic = { level = "warn", priority = -1 }
nursery = { level = "warn", priority = -1 }
cargo = { level = "warn", priority = -1 }
dbg_macro = "deny"
nursery = { level = "warn", priority = -1 }
pedantic = { level = "warn", priority = -1 }
todo = "deny"
# The lints below are a specific subset of the pedantic+nursery lints
# that we explicitly allow in the tree-sitter codebase because they either:
@ -41,52 +38,31 @@ cargo = { level = "warn", priority = -1 }
# 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"
unnecessary_wraps = "allow"
unused_self = "allow"
used_underscore_items = "allow"
tuple_array_conversions = "allow"
[workspace.lints.rust]
mismatched_lifetime_syntaxes = "allow"
[profile.optimize]
inherits = "release"
strip = true # Automatically strip symbols from the binary.
codegen-units = 1 # Maximum size reduction optimizations.
lto = true # Link-time optimization.
opt-level = 3 # Optimization level 3.
codegen-units = 1 # Maximum size reduction optimizations.
strip = true # Automatically strip symbols from the binary.
[profile.size]
inherits = "optimize"
@ -94,70 +70,69 @@ opt-level = "s" # Optimize for size.
[profile.release-dev]
inherits = "release"
lto = false
codegen-units = 256
debug = true
debug-assertions = true
overflow-checks = true
incremental = true
codegen-units = 256
lto = false
overflow-checks = true
[workspace.dependencies]
ansi_colours = "1.2.3"
anstyle = "1.0.13"
anyhow = "1.0.100"
bstr = "1.12.0"
cc = "1.2.48"
clap = { version = "4.5.53", features = [
anstyle = "1.0.14"
anyhow = "1.0.102"
bstr = "1.12.1"
cc = "1.2.63"
clap = { features = [
"cargo",
"derive",
"env",
"help",
"string",
"unstable-styles",
] }
clap_complete = "4.5.61"
], version = "4.5.58" }
clap_complete = "4.6.3"
clap_complete_nushell = "4.5.10"
crc32fast = "1.5.0"
ctor = "0.2.9"
ctrlc = { version = "3.5.0", features = ["termination"] }
dialoguer = { version = "0.11.0", features = ["fuzzy-select"] }
ctor = "0.6.3"
ctrlc = { features = [ "termination" ], version = "3.5.2" }
dialoguer = { features = [ "fuzzy-select" ], version = "0.12.0" }
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.12.1"
indoc = "2.0.6"
indexmap = "2.13.0"
indoc = "2.0.7"
libloading = "0.9.0"
log = { version = "0.4.28", features = ["std"] }
memchr = "2.7.6"
log = { features = [ "std" ], version = "0.4.30" }
memchr = "2.8.1"
once_cell = "1.21.3"
pretty_assertions = "1.4.1"
rand = "0.8.5"
regex = "1.11.3"
regex-syntax = "0.8.6"
rand = "0.10.1"
regex = "1.12.3"
regex-syntax = "0.8.9"
rustc-hash = "2.1.1"
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"] }
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" }
similar = "2.7.0"
smallbitvec = "2.6.0"
streaming-iterator = "0.1.9"
tempfile = "3.23.0"
thiserror = "2.0.17"
tempfile = "3.25.0"
thiserror = "2.0.18"
tiny_http = "0.12.0"
topological-sort = "0.2.2"
unindent = "0.2.4"
walkdir = "2.5.0"
wasmparser = "0.243.0"
webbrowser = "1.0.5"
wasmparser = "0.244.0"
webbrowser = "1.2.1"
tree-sitter = { version = "0.26.6", path = "./lib" }
tree-sitter-generate = { version = "0.26.6", path = "./crates/generate" }
tree-sitter-loader = { version = "0.26.6", path = "./crates/loader" }
tree-sitter-config = { version = "0.26.6", path = "./crates/config" }
tree-sitter-highlight = { version = "0.26.6", path = "./crates/highlight" }
tree-sitter-tags = { version = "0.26.6", path = "./crates/tags" }
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-language = { version = "0.1", path = "./crates/language" }
tree-sitter-language = { path = "./crates/language", version = "0.1.8" }

View file

@ -1,10 +0,0 @@
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.26.6
VERSION := 0.28.0
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
CFLAGS ?= -O3 -Wall -Wextra -Wshadow -Wpedantic -Werror=incompatible-pointer-types -Werror=strict-aliasing -Wstrict-aliasing=2
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
@ -129,8 +129,13 @@ 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)

View file

@ -1,35 +0,0 @@
// 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,51 +4,56 @@ 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 = b.createModule(.{
.target = target,
.optimize = optimize,
.link_libc = true,
.pic = if (shared) true else null,
}),
.root_module = tree_sitter,
});
if (amalgamated) {
lib.addCSourceFile(.{
tree_sitter.addCSourceFile(.{
.file = b.path("lib/src/lib.c"),
.flags = &.{"-std=c11"},
});
} else {
const files = try findSourceFiles(b);
const files = try findSourceFiles(b, io);
defer b.allocator.free(files);
lib.addCSourceFiles(.{
tree_sitter.addCSourceFiles(.{
.root = b.path("lib/src"),
.files = files,
.flags = &.{"-std=c11"},
});
}
lib.addIncludePath(b.path("lib/include"));
lib.addIncludePath(b.path("lib/src"));
lib.addIncludePath(b.path("lib/src/wasm"));
tree_sitter.addIncludePath(b.path("lib/include"));
tree_sitter.addIncludePath(b.path("lib/src"));
tree_sitter.addIncludePath(b.path("lib/src/wasm"));
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", "");
tree_sitter.addCMacro("_POSIX_C_SOURCE", "200112L");
tree_sitter.addCMacro("_DEFAULT_SOURCE", "");
tree_sitter.addCMacro("_BSD_SOURCE", "");
tree_sitter.addCMacro("_DARWIN_C_SOURCE", "");
if (wasm) {
if (b.lazyDependency(wasmtimeDep(target.result), .{})) |wasmtime| {
lib.root_module.addCMacro("TREE_SITTER_FEATURE_WASM", "");
lib.addSystemIncludePath(wasmtime.path("include"));
lib.addLibraryPath(wasmtime.path("lib"));
if (shared) lib.linkSystemLibrary("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", .{});
}
}
@ -122,14 +127,14 @@ pub fn wasmtimeDep(target: std.Target) []const u8 {
);
}
fn findSourceFiles(b: *std.Build) ![]const []const u8 {
fn findSourceFiles(b: *std.Build, io: std.Io) ![]const []const u8 {
var sources: std.ArrayListUnmanaged([]const u8) = .empty;
var dir = try b.build_root.handle.openDir("lib/src", .{ .iterate = true });
var dir = try b.build_root.handle.openDir(io, "lib/src", .{ .iterate = true });
var iter = dir.iterate();
defer dir.close();
defer dir.close(io);
while (try iter.next()) |entry| {
while (try iter.next(io)) |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.26.6",
.minimum_zig_version = "0.14.1",
.version = "0.28.0",
.minimum_zig_version = "0.16.0",
.paths = .{
"build.zig",
"build.zig.zon",
@ -13,83 +13,83 @@
},
.dependencies = .{
.wasmtime_c_api_aarch64_android = .{
.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",
.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",
.lazy = true,
},
.wasmtime_c_api_aarch64_linux = .{
.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",
.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",
.lazy = true,
},
.wasmtime_c_api_aarch64_macos = .{
.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",
.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",
.lazy = true,
},
.wasmtime_c_api_aarch64_musl = .{
.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",
.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",
.lazy = true,
},
.wasmtime_c_api_aarch64_windows = .{
.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",
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-aarch64-windows-c-api.zip",
.hash = "N-V-__8AAHRCtQU93hJcRFOgVcof3IQpRV9stT2Pp54wpJc2",
.lazy = true,
},
.wasmtime_c_api_armv7_linux = .{
.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",
.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",
.lazy = true,
},
.wasmtime_c_api_i686_linux = .{
.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",
.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",
.lazy = true,
},
.wasmtime_c_api_i686_windows = .{
.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",
.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",
.lazy = true,
},
.wasmtime_c_api_riscv64gc_linux = .{
.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",
.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",
.lazy = true,
},
.wasmtime_c_api_s390x_linux = .{
.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_",
.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",
.lazy = true,
},
.wasmtime_c_api_x86_64_android = .{
.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",
.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",
.lazy = true,
},
.wasmtime_c_api_x86_64_linux = .{
.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",
.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",
.lazy = true,
},
.wasmtime_c_api_x86_64_macos = .{
.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",
.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",
.lazy = true,
},
.wasmtime_c_api_x86_64_mingw = .{
.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",
.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",
.lazy = true,
},
.wasmtime_c_api_x86_64_musl = .{
.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",
.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",
.lazy = true,
},
.wasmtime_c_api_x86_64_windows = .{
.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",
.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",
.lazy = true,
},
},

View file

@ -5,14 +5,13 @@ 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
@ -21,18 +20,18 @@ workspace = true
path = "src/tree_sitter_cli.rs"
[[bin]]
doc = false
name = "tree-sitter"
path = "src/main.rs"
doc = false
[[bench]]
name = "benchmark"
harness = false
name = "benchmark"
[features]
default = ["qjs-rt"]
wasm = ["tree-sitter/wasm", "tree-sitter-loader/wasm"]
qjs-rt = ["tree-sitter-generate/qjs-rt"]
default = [ "qjs-rt" ]
qjs-rt = [ "tree-sitter-generate/qjs-rt" ]
wasm = [ "tree-sitter/wasm", "tree-sitter-loader/wasm" ]
[dependencies]
ansi_colours.workspace = true
@ -67,17 +66,50 @@ 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" }
tempfile.workspace = true
pretty_assertions.workspace = true
tempfile.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,20 +7,20 @@
[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`:
You can install the `tree-sitter-cli` with [`cargo-binstall`](https://github.com/cargo-bins/cargo-binstall):
```sh
cargo install --locked tree-sitter-cli
cargo binstall tree-sitter-cli
```
or with `npm`:
or you can build it from source:
```sh
npm install tree-sitter-cli
cargo install --locked tree-sitter-cli
```
You can also download a pre-built binary for your platform from [the releases page].
@ -34,9 +34,11 @@ 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,7 +2,6 @@ use std::{
collections::BTreeMap,
env, fs,
path::{Path, PathBuf},
str,
sync::LazyLock,
time::Instant,
};
@ -10,6 +9,8 @@ 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");
@ -21,10 +22,16 @@ 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);
#[allow(clippy::type_complexity)]
#[expect(
clippy::type_complexity,
reason = "complex map type reflects benchmark data structure"
)]
static EXAMPLE_AND_QUERY_PATHS_BY_LANGUAGE_DIR: LazyLock<
BTreeMap<PathBuf, (Vec<PathBuf>, Vec<PathBuf>)>,
> = LazyLock::new(|| {
@ -36,22 +43,14 @@ 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 {
@ -93,22 +92,26 @@ fn main() {
{
let language_name = language_path.file_name().unwrap().to_str().unwrap();
if let Some(filter) = LANGUAGE_FILTER.as_ref() {
if language_name != filter.as_str() {
continue;
}
if let Some(filter) = LANGUAGE_FILTER.as_ref()
&& language_name != filter.as_str()
{
continue;
}
info!("\nLanguage: {language_name}");
let language = get_language(language_path);
let language = if *WASM {
get_wasm_language(language_name, &mut parser)
} else {
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() {
if !path.to_str().unwrap().contains(filter.as_str()) {
continue;
}
if let Some(filter) = EXAMPLE_FILTER.as_ref()
&& !path.to_str().unwrap().contains(filter.as_str())
{
continue;
}
parse(path, max_path_length, |source| {
@ -121,10 +124,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() {
if !example_path.to_str().unwrap().contains(filter.as_str()) {
continue;
}
if let Some(filter) = EXAMPLE_FILTER.as_ref()
&& !example_path.to_str().unwrap().contains(filter.as_str())
{
continue;
}
normal_speeds.push(parse(example_path, max_path_length, |code| {
@ -139,10 +142,10 @@ fn main() {
{
if other_language_path != language_path {
for example_path in example_paths {
if let Some(filter) = EXAMPLE_FILTER.as_ref() {
if !example_path.to_str().unwrap().contains(filter.as_str()) {
continue;
}
if let Some(filter) = EXAMPLE_FILTER.as_ref()
&& !example_path.to_str().unwrap().contains(filter.as_str())
{
continue;
}
error_speeds.push(parse(example_path, max_path_length, |code| {
@ -220,3 +223,32 @@ 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": "^50.2.4"
"eslint-plugin-jsdoc": "^62.7.0"
},
"peerDependencies": {
"eslint": ">= 9"

View file

@ -3,18 +3,22 @@ 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 };
type PatternRule = { type: 'PATTERN'; value: string; flags?: string };
type PrecedenceValue = string | number;
type PrecDynamicRule = { type: 'PREC_DYNAMIC'; content: Rule; value: number };
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 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 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
@ -33,7 +37,8 @@ type Rule =
| SeqRule
| StringRule
| SymbolRule<string>
| TokenRule;
| TokenRule
| EOFRule;
declare class RustRegex {
value: string;
@ -86,8 +91,8 @@ interface Grammar<
*/
precedences?: (
$: GrammarSymbols<RuleName | BaseGrammarRuleName>,
previous: Rule[][],
) => RuleOrLiteral[][],
previous: PrecedenceEntry[][],
) => (string | PrecedenceEntry)[][],
/**
* An array of arrays of rule names. Each inner array represents a set of
@ -101,8 +106,8 @@ interface Grammar<
*/
conflicts?: (
$: GrammarSymbols<RuleName | BaseGrammarRuleName>,
previous: Rule[][],
) => RuleOrLiteral[][];
previous: SymbolRule<string>[][],
) => SymbolRule<string>[][];
/**
* An array of token names which can be returned by an _external scanner_.
@ -127,9 +132,11 @@ 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[];
/**
@ -142,8 +149,8 @@ interface Grammar<
*/
inline?: (
$: GrammarSymbols<RuleName | BaseGrammarRuleName>,
previous: Rule[],
) => RuleOrLiteral[];
previous: SymbolRule<string>[],
) => SymbolRule<string>[];
/**
* A list of hidden rule names that should be considered supertypes in the
@ -155,8 +162,8 @@ interface Grammar<
*/
supertypes?: (
$: GrammarSymbols<RuleName | BaseGrammarRuleName>,
previous: Rule[],
) => RuleOrLiteral[];
previous: SymbolRule<string>[],
) => SymbolRule<string>[];
/**
* The name of a token that will match keywords for the purpose of the
@ -166,24 +173,47 @@ interface Grammar<
*
* @see https://tree-sitter.github.io/tree-sitter/creating-parsers/3-writing-the-grammar#keyword-extraction
*/
word?: ($: GrammarSymbols<RuleName | BaseGrammarRuleName>) => RuleOrLiteral;
word?: (
$: GrammarSymbols<RuleName | BaseGrammarRuleName>,
) => SymbolRule<string>;
/**
* 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.
* 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.
*/
reserved?: Record<
string,
($: GrammarSymbols<RuleName | BaseGrammarRuleName>) => RuleOrLiteral[]
(
$: GrammarSymbols<RuleName | BaseGrammarRuleName>,
previous: Rule[] | undefined,
) => 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> = {
[K in keyof Grammar<RuleName>]: K extends 'rules'
? Record<RuleName, Rule>
: Grammar<RuleName>[K];
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[]>;
};
};
/**
@ -311,7 +341,7 @@ declare const prec: {
*
* @see https://www.gnu.org/software/bison/manual/html_node/Generalized-LR-Parsing.html
*/
dynamic(value: string | number, rule: RuleOrLiteral): PrecDynamicRule;
dynamic(value: number, rule: RuleOrLiteral): PrecDynamicRule;
};
/**
@ -382,6 +412,19 @@ 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.26.6",
"version": "0.28.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "tree-sitter-cli",
"version": "0.26.6",
"version": "0.28.0",
"hasInstallScript": true,
"license": "MIT",
"bin": {

View file

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

View file

@ -13,7 +13,7 @@
installShellFiles,
}:
let
isCross = stdenv.targetPlatform == stdenv.buildPlatform;
canRunHost = stdenv.buildPlatform.canExecute stdenv.hostPlatform;
in
rustPlatform.buildRustPackage {
pname = "tree-sitter-cli";
@ -28,7 +28,7 @@ rustPlatform.buildRustPackage {
pkg-config
nodejs_22
]
++ lib.optionals (!isCross) [ installShellFiles ];
++ lib.optionals canRunHost [ installShellFiles ];
cargoLock.lockFile = ../../Cargo.lock;
@ -42,9 +42,9 @@ rustPlatform.buildRustPackage {
'';
preCheck = "export HOME=$TMPDIR";
doCheck = !isCross;
doCheck = canRunHost;
postInstall = lib.optionalString (!isCross) ''
postInstall = lib.optionalString canRunHost ''
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::Rng;
use rand::RngExt;
use regex::Regex;
use tree_sitter::{Language, Parser};
@ -25,7 +25,7 @@ use crate::{
random::Rand,
},
parse::perform_edit,
test::{parse_tests, strip_sexp_fields, DiffKey, TestDiff, TestEntry},
test::{DiffKey, TestDiff, TestEntry, TestExpectation, parse_tests, render_test_output},
};
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::thread_rng();
let seed = rng.gen::<usize>();
info!("Seed: {seed}");
let mut rng = rand::rng();
let seed = rng.random_range(0..=usize::MAX);
eprintln!("fuzz seed: {seed}");
seed
})
}
@ -97,9 +97,7 @@ pub fn fuzz_language_corpus(
.iter()
.any(|lang| lang.as_ref() == language_name)
}
TestEntry::Group {
ref mut children, ..
} => {
TestEntry::Group { children, .. } => {
children.retain_mut(|child| retain(child, language_name));
!children.is_empty()
}
@ -111,12 +109,16 @@ 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;
}
@ -142,7 +144,7 @@ pub fn fuzz_language_corpus(
.take()
.unwrap_or_default()
.into_iter()
.chain(tests.iter().filter(|x| x.skip).map(get_test_name))
.chain(tests.iter().filter(|t| t.skip()).map(get_test_name))
.map(|x| (x, 0))
.collect::<HashMap<String, usize>>();
@ -167,31 +169,8 @@ pub fn fuzz_language_corpus(
println!(" {test_index}. {test_name}");
let passed = allocations::record_checked(|| {
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
let check_output = !test.error();
test.check_initial_parse(language, &test_name, check_output)
})
.unwrap_or_else(|e| {
error!("{e}");
@ -255,7 +234,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;
}
@ -271,12 +250,9 @@ 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 mut actual_output = tree3.root_node().to_sexp();
if !test.has_fields {
actual_output = strip_sexp_fields(&actual_output);
}
let actual_output = render_test_output(&input, &tree3, test.cst, test.has_fields).unwrap();
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));
@ -324,12 +300,56 @@ pub struct FlattenedTest {
pub input: Vec<u8>,
pub output: String,
pub languages: Vec<Box<str>>,
pub error: bool,
pub skip: bool,
pub expectation: TestExpectation,
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,
@ -362,20 +382,20 @@ pub fn flatten_tests(
if !include.is_match(&name) {
return;
}
} else if let Some(exclude) = exclude {
if exclude.is_match(&name) {
return;
}
} else if let Some(exclude) = exclude
&& exclude.is_match(&name)
{
return;
}
result.push(FlattenedTest {
name,
input,
output,
has_fields,
languages: attributes.languages,
error: attributes.error,
skip: attributes.skip,
expectation: attributes.expectation,
has_fields,
cst: attributes.cst,
template_delimiters: None,
});
}

View file

@ -2,19 +2,21 @@ use std::{
collections::HashMap,
os::raw::c_void,
sync::{
atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst},
Mutex,
atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst},
},
};
#[ctor::ctor]
unsafe fn initialize_allocation_recording() {
tree_sitter::set_allocator(
Some(ts_record_malloc),
Some(ts_record_calloc),
Some(ts_record_realloc),
Some(ts_record_free),
);
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,
}));
}
}
#[derive(Debug, PartialEq, Eq, Hash)]
@ -33,7 +35,7 @@ thread_local! {
static RECORDER: AllocationRecorder = AllocationRecorder::default();
}
extern "C" {
unsafe 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;
@ -103,9 +105,11 @@ 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 {
let result = malloc(size);
record_alloc(result);
result
unsafe {
let result = malloc(size);
record_alloc(result);
result
}
}
/// # Safety
@ -114,9 +118,11 @@ 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 {
let result = calloc(count, size);
record_alloc(result);
result
unsafe {
let result = calloc(count, size);
record_alloc(result);
result
}
}
/// # Safety
@ -125,14 +131,16 @@ 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 {
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);
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
}
result
}
/// # Safety
@ -140,6 +148,8 @@ 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) {
record_dealloc(ptr);
free(ptr);
unsafe {
record_dealloc(ptr);
free(ptr);
}
}

View file

@ -1,10 +1,22 @@
use tree_sitter::{LogType, Node, Parser, Point, Range, Tree};
use super::{scope_sequence::ScopeSequence, LOG_ENABLED, LOG_GRAPH_ENABLED};
use super::{LOG_ENABLED, LOG_GRAPH_ENABLED, scope_sequence::ScopeSequence};
use crate::util;
pub fn check_consistent_sizes(tree: &Tree, input: &[u8]) {
fn check(node: Node, line_offsets: &[usize]) {
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> {
let start_byte = node.start_byte();
let end_byte = node.end_byte();
let start_point = node.start_position();
@ -18,37 +30,21 @@ pub fn check_consistent_sizes(tree: &Tree, input: &[u8]) {
);
assert_eq!(end_byte, line_offsets[end_point.row] + end_point.column);
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());
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,
}
}
}
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' {
@ -56,7 +52,41 @@ pub fn check_consistent_sizes(tree: &Tree, input: &[u8]) {
}
}
check(tree.root_node(), &line_offsets);
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());
}
}
}
pub fn check_changed_ranges(old_tree: &Tree, new_tree: &Tree, input: &[u8]) -> Result<(), String> {

View file

@ -1,7 +1,4 @@
use rand::{
distributions::Alphanumeric,
prelude::{Rng, SeedableRng, StdRng},
};
use rand::{RngExt, SeedableRng, distr::Alphanumeric, rngs::StdRng};
const OPERATORS: &[char] = &[
'+', '-', '<', '>', '(', ')', '*', '/', '&', '|', '!', ',', '.', '%',
@ -16,7 +13,7 @@ impl Rand {
}
pub fn unsigned(&mut self, max: usize) -> usize {
self.0.gen_range(0..=max)
self.0.random_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(Vec<ScopeStack>);
pub struct ScopeSequence<'a>(Vec<ScopeStack<'a>>);
type ScopeStack = Vec<&'static str>;
type ScopeStack<'a> = Vec<&'a str>;
impl ScopeSequence {
impl<'a> ScopeSequence<'a> {
#[must_use]
pub fn new(tree: &Tree) -> Self {
pub fn new(tree: &'a Tree) -> Self {
let mut result = Self(Vec::new());
let mut scope_stack = Vec::new();
@ -49,7 +49,7 @@ impl ScopeSequence {
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', b'\n'].contains(&text[i]) {
if *stack != *other_stack && !b"\r\n".contains(&text[i]) {
let containing_range = known_changed_ranges
.iter()
.find(|range| range.start_point <= position && position < range.end_point);

View file

@ -4,17 +4,18 @@ use std::{
fs,
io::{self, Write as _},
path::{self, Path, PathBuf},
str,
sync::{atomic::AtomicUsize, Arc},
sync::{Arc, atomic::AtomicUsize},
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::{ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer};
use serde_json::{json, Value};
use serde::{Deserialize, Deserializer, Serialize, Serializer, ser::SerializeMap};
use serde_json::{Value, json};
use tree_sitter::ffi::{self, TSInputEncoding};
use tree_sitter_highlight::{HighlightConfiguration, HighlightEvent, Highlighter, HtmlRenderer};
use tree_sitter_loader::Loader;
@ -25,8 +26,9 @@ pub const HTML_HEAD_HEADER: &str = "
<style>
body {
font-family: monospace
}
.line-number {
}";
pub const HTML_LINE_NUMBER_STYLE: &str = " .line-number {
user-select: none;
text-align: right;
color: rgba(27,31,35,.3);
@ -34,8 +36,7 @@ pub const HTML_HEAD_HEADER: &str = "
}
.line {
white-space: pre;
}
</style>";
}";
pub const HTML_BODY_HEADER: &str = "
</head>
@ -190,20 +191,14 @@ 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) {
@ -221,11 +216,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() {
if !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()
&& !terminal_supports_truecolor()
{
let ansi256 = Color::Ansi256(Ansi256Color(ansi256_from_rgb((red, green, blue))));
style.ansi = style.ansi.fg_color(Some(ansi256));
}
}
@ -313,15 +308,40 @@ 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>,
pub inline_styles: bool,
pub html: bool,
/// `None` for regular output, `Some((layout, style))` when emitting HTML.
pub html: Option<(HtmlOutput, HtmlStyling)>,
pub quiet: bool,
pub print_time: bool,
pub cancellation_flag: Arc<AtomicUsize>,
pub encoding: Option<TSInputEncoding>,
}
pub fn highlight(
@ -364,29 +384,60 @@ 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, Some(&opts.cancellation_flag), |string| {
loader.highlight_config_for_injection_string(string)
})?;
let events = highlighter.highlight(
config,
&source,
encoding,
Some(&opts.cancellation_flag),
|string| loader.highlight_config_for_injection_string(string),
)?;
let theme = &opts.theme;
if !opts.quiet && print_name {
// 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 {
writeln!(&mut stdout, "{name}")?;
}
if opts.html {
if !opts.quiet {
if let Some((layout, style)) = opts.html {
if !opts.quiet && layout != HtmlOutput::Fragment {
writeln!(&mut stdout, "{HTML_HEAD_HEADER}")?;
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}; }}")?;
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>")?;
@ -395,7 +446,7 @@ pub fn highlight(
let mut renderer = HtmlRenderer::new();
renderer.render(events, &source, &move |highlight, output| {
if opts.inline_styles {
if style == HtmlStyling::Inline {
output.extend(b"style='");
output.extend(
theme.styles[highlight.0]
@ -403,7 +454,6 @@ 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();
@ -413,21 +463,34 @@ pub fn highlight(
output.extend(b" ");
}
}
output.extend(b"'");
}
output.extend(b"'");
})?;
if !opts.quiet {
writeln!(&mut stdout, "<table>")?;
for (i, line) in renderer.lines().enumerate() {
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,
"<tr><td class=line-number>{}</td><td class=line>{line}</td></tr>",
i + 1,
"<div class=\"highlight\">\n<pre><code>{body}</code></pre>\n</div>",
)?;
}
writeln!(&mut stdout, "</table>")?;
writeln!(&mut stdout, "{HTML_FOOTER}")?;
if layout != HtmlOutput::Fragment {
writeln!(&mut stdout, "{HTML_FOOTER}")?;
}
}
} else {
let mut style_stack = vec![theme.default_style().ansi];
@ -474,7 +537,7 @@ mod tests {
assert_eq!(style.css, None);
// darkcyan is an ANSI color and is preserved
env::set_var("COLORTERM", "");
unsafe { env::set_var("COLORTERM", "") };
parse_style(&mut style, Value::String(DARK_CYAN.to_string()));
assert_eq!(
style.ansi.get_fg_color(),
@ -483,7 +546,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
env::set_var("COLORTERM", "truecolor");
unsafe { env::set_var("COLORTERM", "truecolor") };
parse_style(&mut style, Value::String(JUNGLE_GREEN.to_string()));
assert_eq!(
style.ansi.get_fg_color(),
@ -492,7 +555,7 @@ mod tests {
assert_eq!(style.css, Some("color: #26a69a".to_string()));
// junglegreen gets approximated as cadetblue when the terminal does not support it
env::set_var("COLORTERM", "");
unsafe { env::set_var("COLORTERM", "") };
parse_style(&mut style, Value::String(JUNGLE_GREEN.to_string()));
assert_eq!(
style.ansi.get_fg_color(),
@ -501,9 +564,9 @@ mod tests {
assert_eq!(style.css, Some("color: #26a69a".to_string()));
if let Ok(environment_variable) = original_environment_variable {
env::set_var("COLORTERM", environment_variable);
unsafe { env::set_var("COLORTERM", environment_variable) };
} else {
env::remove_var("COLORTERM");
unsafe { env::remove_var("COLORTERM") };
}
}
}

File diff suppressed because it is too large Load diff

View file

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

View file

@ -1,12 +1,8 @@
use std::io::Write;
use anstyle::{AnsiColor, Color, Style};
use log::{Level, LevelFilter, Log, Metadata, Record};
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:#}")
}
use crate::paint::{Paint, RED, YELLOW};
struct Logger;
@ -17,16 +13,8 @@ impl Log for Logger {
fn log(&self, record: &Record) {
match record.level() {
Level::Error => eprintln!(
"{} {}",
paint(Some(AnsiColor::Red), "Error:"),
record.args()
),
Level::Warn => eprintln!(
"{} {}",
paint(Some(AnsiColor::Yellow), "Warning:"),
record.args()
),
Level::Error => eprintln!("{} {}", Paint(RED, "Error:"), record.args()),
Level::Warn => eprintln!("{} {}", Paint(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::{anyhow, Context, Result};
use clap::{crate_authors, Args, Command, FromArgMatches as _, Subcommand, ValueEnum};
use anyhow::{Context, Result, anyhow};
use clap::{ArgGroup, Args, Command, FromArgMatches as _, Subcommand, ValueEnum, crate_authors};
use clap_complete::generate;
use dialoguer::{theme::ColorfulTheme, Confirm, FuzzySelect, Input, MultiSelect};
use dialoguer::{Confirm, FuzzySelect, Input, MultiSelect, theme::ColorfulTheme};
use heck::ToUpperCamelCase;
use log::{error, info, warn};
use regex::Regex;
use semver::Version as SemverVersion;
use tree_sitter::{ffi, Parser, Point};
use tree_sitter::{Parser, Point, ffi};
use tree_sitter_cli::{
fuzz::{
fuzz_language_corpus, FuzzOptions, DEFAULT_EDIT_COUNT, DEFAULT_ITERATION_COUNT, EDIT_COUNT,
ITERATION_COUNT, LOG_ENABLED, LOG_GRAPH_ENABLED, START_SEED,
DEFAULT_EDIT_COUNT, DEFAULT_ITERATION_COUNT, EDIT_COUNT, FuzzOptions, ITERATION_COUNT,
LOG_ENABLED, LOG_GRAPH_ENABLED, START_SEED, fuzz_language_corpus,
},
highlight::{self, HighlightOptions},
init::{generate_grammar_files, JsonConfigOpts, TREE_SITTER_JSON_SCHEMA},
input::{get_input, get_tmp_source_file, CliInput},
logger,
highlight::{self, HighlightOptions, HtmlOutput, HtmlStyling},
init::{JsonConfigOpts, TREE_SITTER_JSON_SCHEMA, generate_grammar_files},
input::{CliInput, get_input, get_tmp_source_file},
logger, paint,
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::OptLevel;
use tree_sitter_generate::{Diagnostic, GenerateError, OptLevel};
use tree_sitter_highlight::Highlighter;
use tree_sitter_loader::{self as loader, Bindings, TreeSitterJSON};
use tree_sitter_tags::TagsContext;
@ -188,10 +188,14 @@ 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")]
@ -207,26 +211,29 @@ 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)]
#[arg(long, requires = "lib_path")]
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`
#[allow(clippy::option_option)]
#[expect(
clippy::option_option,
reason = "required by clap for optional flag with optional value"
)]
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')]
#[arg(long, short = 'D', group = "graph_output")]
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")]
#[arg(long = "dot", group = "graph_output")]
pub output_dot: bool,
/// Output the parse data in XML format
#[arg(long = "xml", short = 'x')]
@ -234,7 +241,7 @@ struct Parse {
/// Output the parse data in a pretty-printed CST format
#[arg(long = "cst", short = 'c')]
pub output_cst: bool,
/// Show parsing statistic
/// Show parsing statistics
#[arg(long, short, conflicts_with = "json", conflicts_with = "json_summary")]
pub stat: bool,
/// Interrupt the parsing process by timeout (µs)
@ -246,7 +253,10 @@ struct Parse {
/// Suppress main output
#[arg(long, short)]
pub quiet: bool,
#[allow(clippy::doc_markdown)]
#[expect(
clippy::doc_markdown,
reason = "doc string contains format syntax, not code identifiers"
)]
/// Apply edits in the format: \"row,col|position delcount insert_text\", can be supplied
/// multiple times
#[arg(
@ -257,8 +267,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` is supplied
#[arg(long)]
/// Open `log.html` in the default browser, if `--debug-graph` or `--dot` is supplied
#[arg(long, requires = "graph_output")]
pub open_log: bool,
/// Deprecated: use --json-summary
#[arg(long, conflicts_with = "json_summary", conflicts_with = "stat")]
@ -283,9 +293,9 @@ struct Parse {
#[derive(ValueEnum, Clone)]
pub enum Encoding {
Utf8,
Utf16LE,
Utf16BE,
Utf8 = 0,
Utf16LE = 1,
Utf16BE = 2,
}
#[derive(Args)]
@ -308,7 +318,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)]
#[arg(long, requires = "lib_path")]
pub lang_name: Option<String>,
/// Update all syntax trees in corpus files with current parser output
#[arg(long, short)]
@ -326,7 +336,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)]
#[arg(long, requires = "debug_graph")]
pub open_log: bool,
/// The path to an alternative config.json file
#[arg(long)]
@ -334,6 +344,9 @@ 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>,
@ -350,6 +363,10 @@ 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
@ -389,7 +406,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)]
#[arg(long, requires = "lib_path")]
pub lang_name: Option<String>,
#[arg(
long,
@ -420,6 +437,10 @@ 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)]
@ -432,7 +453,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)]
#[arg(long, requires = "lib_path")]
pub lang_name: Option<String>,
/// Measure execution time
#[arg(long, short)]
@ -487,14 +508,20 @@ struct Highlight {
/// Generate highlighting as an HTML document
#[arg(long, short = 'H')]
pub html: bool,
/// When generating HTML, use css classes rather than inline styles
#[arg(long)]
/// Deprecated: use `--style classes`
#[arg(long, requires = "html", conflicts_with = "style")]
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)]
#[arg(long, requires = "check")]
pub captures_path: Option<PathBuf>,
/// The paths to files with queries
#[arg(long, num_args = 1..)]
@ -527,6 +554,9 @@ 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)]
@ -789,7 +819,7 @@ impl Init {
let enabled = MultiSelect::new()
.with_prompt("Bindings")
.items_checked(&languages)
.items_checked(languages.iter().copied())
.interact()?
.into_iter()
.map(|i| languages[i].0);
@ -863,7 +893,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]);
@ -881,7 +911,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 implict `null`s
// will be included with explicit `false`s rather than implicit `null`s
if self.update && !old_config.trim().eq(new_config.trim()) {
info!("Updating tree-sitter.json");
fs::write(
@ -928,7 +958,8 @@ impl Generate {
self.json_summary
};
if let Err(err) = tree_sitter_generate::generate_parser_in_directory(
let mut diagnostics = Vec::new();
let result = tree_sitter_generate::generate_parser_in_directory(
current_dir,
self.output.as_deref(),
self.grammar_path.as_deref(),
@ -941,16 +972,33 @@ impl Generate {
} else {
OptLevel::default()
},
) {
if json_summary {
eprintln!("{}", serde_json::to_string_pretty(&err)?);
&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() {
// Exit early to prevent errors from being printed a second time in the caller
std::process::exit(1);
} else {
}
} else {
for d in &diagnostics {
warn!("{d}");
}
if let Err(err) = result {
// 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 {
@ -968,6 +1016,7 @@ 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));
@ -985,7 +1034,7 @@ impl Build {
.context("Output path must have a parent")?;
let name = full_path
.file_name()
.context("Ouput path must have a filename")?;
.context("Output path must have a filename")?;
fs::create_dir_all(parent_path).context("Failed to create output path")?;
let mut canon_path = parent_path.canonicalize().context("Invalid output path")?;
canon_path.push(name);
@ -1023,7 +1072,6 @@ 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
@ -1042,7 +1090,7 @@ impl Parse {
ParseOutput::Normal
};
let parse_theme = if color {
let parse_theme = if paint::color_enabled() {
config
.get::<parse::Config>()
.with_context(|| "Failed to parse CST theme")?
@ -1080,9 +1128,6 @@ 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 {
@ -1125,10 +1170,11 @@ impl Parse {
has_error |= !parse_result.successful;
};
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);
if lib_info.is_none() {
let loader_config = config.get()?;
loader.find_all_languages(&loader_config)?;
}
let input = get_input(
self.paths_file.as_deref(),
@ -1279,7 +1325,6 @@ 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);
@ -1297,9 +1342,6 @@ 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 =
@ -1321,13 +1363,9 @@ impl Test {
parser.set_language(language)?;
let test_dir = current_dir.join("test");
let mut test_summary = TestSummary::new(
color,
stat,
self.update,
self.overview_only,
self.json_summary,
);
let mut test_summary =
TestSummary::new(stat, self.update, self.overview_only, self.json_summary);
test_summary.use_markers = self.show_diff_markers;
// Run the corpus tests. Look for them in `test/corpus`.
let test_corpus_dir = test_dir.join("corpus");
@ -1342,7 +1380,6 @@ 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,
};
@ -1353,16 +1390,20 @@ 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");
check_test(
test::check_queries_at_path(language, &query_dir),
&test_summary,
self.json_summary,
)?;
test_summary.test_num = 1;
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;
}
// Run the syntax highlighting tests.
let test_highlight_dir = test_dir.join("highlight");
@ -1405,7 +1446,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(|e| e.ok())
.filter_map(std::result::Result::ok)
.filter(|e| e.file_type().is_file())
{
let stem = entry
@ -1476,9 +1517,6 @@ 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)
@ -1526,24 +1564,23 @@ impl Fuzz {
impl Query {
fn run(self, mut loader: loader::Loader, current_dir: &Path) -> Result<()> {
let config = Config::load(self.config_path)?;
let loader_config = config.get()?;
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)?;
}
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, |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 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 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,
@ -1667,15 +1704,29 @@ 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,
inline_styles: !self.css_classes,
html: self.html,
html: self.html.then_some((self.layout, style)),
quiet: self.quiet,
print_time: self.time,
cancellation_flag: cancellation_flag.clone(),
encoding,
};
let input = get_input(
@ -1760,7 +1811,7 @@ impl Highlight {
let path = get_tmp_source_file(&contents)?;
let (language, language_config) =
if let (Some(l), Some(lc)) = (language.clone(), language_configuration) {
if let (Some(l), Some(lc)) = (language, language_configuration) {
(l, lc)
} else {
let language = languages
@ -1902,7 +1953,7 @@ impl Tags {
let path = get_tmp_source_file(&contents)?;
let (language, language_config) =
if let (Some(l), Some(lc)) = (language.clone(), language_configuration) {
if let (Some(l), Some(lc)) = (language, language_configuration) {
(l, lc)
} else {
let languages = loader.languages_at_path(current_dir)?;
@ -1956,7 +2007,7 @@ impl DumpLanguages {
concat!(
"name: {}\n",
"scope: {}\n",
"parser: {:?}\n",
"parser: {}\n",
"highlights: {:?}\n",
"file_types: {:?}\n",
"content_regex: {:?}\n",
@ -1964,7 +2015,7 @@ impl DumpLanguages {
),
configuration.language_name,
configuration.scope.as_ref().unwrap_or(&String::new()),
language_path,
language_path.display(),
configuration.highlights_filenames,
configuration.file_types,
configuration.content_regex,
@ -1997,10 +2048,10 @@ fn main() {
let result = run();
if let Err(err) = &result {
// Ignore BrokenPipe errors
if let Some(error) = err.downcast_ref::<std::io::Error>() {
if error.kind() == std::io::ErrorKind::BrokenPipe {
return;
}
if let Some(error) = err.downcast_ref::<std::io::Error>()
&& error.kind() == std::io::ErrorKind::BrokenPipe
{
return;
}
if !err.to_string().is_empty() {
error!("{err:?}");
@ -2053,7 +2104,7 @@ fn run() -> Result<()> {
| Commands::Complete(_) => &None,
}
.as_ref()
.map_or_else(|| env::current_dir().unwrap(), |p| p.clone());
.map_or_else(|| env::current_dir().unwrap(), std::clone::Clone::clone);
let loader = loader::Loader::new()?;
@ -2125,7 +2176,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(|s| s.as_str()),
language_name.map(std::string::String::as_str),
lib_path.file_stem().and_then(|s| s.to_str()),
) {
(Some(name), _) | (None, Some(name)) => Some((absolute_lib_path, name)),
@ -2138,10 +2189,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<String>,
range_str: Option<&str>,
make: impl Fn(usize) -> T,
) -> Result<Option<std::ops::Range<T>>> {
if let Some(range) = range_str.as_ref() {
if let Some(range) = range_str {
let err_msg = format!("Invalid range '{range}', expected 'start:end'");
let mut parts = range.split(':');

27
crates/cli/src/paint.rs Normal file
View file

@ -0,0 +1,27 @@
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::{anyhow, Context, Result};
use anyhow::{Context, Result, anyhow};
use clap::ValueEnum;
use log::info;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use tree_sitter::{
ffi, InputEdit, Language, LogType, ParseOptions, ParseState, Parser, Point, Range, Tree,
TreeCursor,
InputEdit, Language, LogType, ParseOptions, ParseState, Parser, Point, Range, Tree, TreeCursor,
ffi,
};
use crate::{fuzz::edits::Edit, logger::paint, util};
use crate::{fuzz::edits::Edit, paint::paint, util};
#[derive(Debug, Default, Serialize, JsonSchema)]
pub struct Stats {
@ -286,6 +286,10 @@ 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:?}"))?;
@ -297,7 +301,6 @@ 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 {
@ -306,13 +309,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::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,
];
if message.starts_with("process version:") {
let comma_idx = message.find(',').unwrap();
@ -320,30 +323,21 @@ pub fn parse_file_at_path(
.parse()
.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 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 parse_time = Instant::now();
#[inline(always)]
#[inline]
fn is_utf16_le_bom(bom_bytes: &[u8]) -> bool {
bom_bytes == [0xFF, 0xFE]
}
#[inline(always)]
#[inline]
fn is_utf16_be_bom(bom_bytes: &[u8]) -> bool {
bom_bytes == [0xFE, 0xFF]
}
@ -368,13 +362,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 {
if cancellation_flag.load(Ordering::SeqCst) != 0 {
return ControlFlow::Break(());
}
if let Some(cancellation_flag) = opts.cancellation_flag
&& cancellation_flag.load(Ordering::SeqCst) != 0
{
return ControlFlow::Break(());
}
if opts.timeout > 0 && start_time.elapsed().as_micros() > opts.timeout as u128 {
if opts.timeout > 0 && start_time.elapsed().as_micros() > u128::from(opts.timeout) {
return ControlFlow::Break(());
}
@ -386,8 +380,10 @@ pub fn parse_file_at_path(
let tree = match encoding {
Some(encoding) if encoding == ffi::TSInputEncodingUTF16LE => {
let source_code_utf16 = source_code
.chunks_exact(2)
.map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]]))
.as_chunks::<2>()
.0
.iter()
.map(|&chunk| u16::from_le_bytes(chunk))
.collect::<Vec<_>>();
parser.parse_utf16_le_with_options(
&mut |i, _| {
@ -403,8 +399,10 @@ pub fn parse_file_at_path(
}
Some(encoding) if encoding == ffi::TSInputEncodingUTF16BE => {
let source_code_utf16 = source_code
.chunks_exact(2)
.map(|chunk| u16::from_be_bytes([chunk[0], chunk[1]]))
.as_chunks::<2>()
.0
.iter()
.map(|&chunk| u16::from_be_bytes(chunk))
.collect::<Vec<_>>();
parser.parse_utf16_be_with_options(
&mut |i, _| {
@ -433,7 +431,7 @@ pub fn parse_file_at_path(
let parse_duration = parse_time.elapsed();
let stdout = io::stdout();
let mut stdout = stdout.lock();
let mut stdout = io::BufWriter::with_capacity(64 * 1024, stdout.lock());
if let Some(mut tree) = tree {
if opts.debug_graph && !opts.edits.is_empty() {
@ -510,7 +508,7 @@ pub fn parse_file_at_path(
}
}
cursor.reset(tree.root_node());
println!();
writeln!(&mut stdout)?;
}
if opts.output == ParseOutput::Cst {
@ -544,10 +542,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() {
if parent.child(parent.child_count() as u32 - 1).unwrap() == node {
stdout.write_all(b"\n")?;
}
if let Some(parent) = node.parent()
&& parent.child(parent.child_count() - 1).unwrap() == node
{
stdout.write_all(b"\n")?;
}
needs_newline = true;
}
@ -581,11 +579,11 @@ pub fn parse_file_at_path(
}
let start = node.start_position();
let end = node.end_position();
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, ">")?;
write!(
&mut stdout,
" srow=\"{}\" scol=\"{}\" erow=\"{}\" ecol=\"{}\">",
start.row, start.column, end.row, end.column
)?;
tags.push(node.kind());
needs_newline = true;
}
@ -776,12 +774,16 @@ pub fn render_cst<'a, 'b: 'a>(
cursor: &mut TreeCursor<'a>,
opts: &ParseFileOptions,
out: &mut impl Write,
) -> Result<()> {
) -> io::Result<()> {
let lossy_source_code = String::from_utf8_lossy(source_code);
let total_width = lossy_source_code
.lines()
.enumerate()
.map(|(row, col)| (row as f64).log10() as usize + (col.len() as f64).log10() as usize + 1)
.map(|(row, col)| {
row.checked_ilog10().unwrap_or(0) as usize
+ col.len().checked_ilog10().unwrap_or(0) as usize
+ 1
})
.max()
.unwrap_or(1);
let mut indent_level = usize::from(!opts.no_ranges);
@ -825,19 +827,19 @@ pub fn render_cst<'a, 'b: 'a>(
Ok(())
}
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);
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)?,
}
acc
})
}
Ok(())
}
}
fn write_node_text(
@ -848,21 +850,21 @@ fn write_node_text(
source: &str,
color: Option<impl Into<Color> + Copy>,
text_info: (usize, usize),
) -> Result<()> {
) -> io::Result<()> {
let (total_width, indent_level) = text_info;
let (quote, quote_color) = if is_named {
('`', opts.parse_theme.backtick)
} else {
('\"', color.map(|c| c.into()))
('\"', color.map(std::convert::Into::into))
};
if !is_named {
write!(
out,
"{}{}{}",
paint(quote_color, &String::from(quote)),
paint(color, &render_node_text(source)),
paint(quote_color, &String::from(quote)),
paint(quote_color, quote),
paint(color, CstNodeText(source)),
paint(quote_color, quote),
)?;
} else {
let multiline = source.contains('\n');
@ -881,24 +883,34 @@ fn write_node_text(
} else {
0
};
let formatted_line = render_line_feed(line, opts);
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, " ")?;
}
write!(
out,
"{}{}{}{}{}{}",
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)),
"{}{}{}",
paint(quote_color, quote),
paint(color, CstLineFeed { source: line, opts }),
paint(quote_color, quote),
)?;
}
}
@ -906,67 +918,93 @@ fn write_node_text(
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 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_node_range(
opts: &ParseFileOptions,
cursor: &TreeCursor,
struct CstNodeRange<'src, 'opt> {
opts: &'src ParseFileOptions<'opt>,
has_field_name: bool,
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
};
}
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,
' ',
),
)
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, ' ',
),
)
)
}
}
fn cst_render_node(
opts: &ParseFileOptions,
cursor: &mut TreeCursor,
cursor: &TreeCursor,
source_code: &[u8],
out: &mut impl Write,
total_width: usize,
indent_level: usize,
in_error: bool,
) -> Result<()> {
) -> io::Result<()> {
let node = cursor.node();
let is_named = node.is_named();
if !opts.no_ranges {
write!(
out,
"{}",
render_node_range(opts, cursor, is_named, false, total_width, node.range())
CstNodeRange {
opts,
has_field_name: cursor.field_name().is_some(),
is_named,
is_multiline: false,
total_width,
range: node.range(),
}
)?;
}
write!(
@ -984,7 +1022,7 @@ fn cst_render_node(
write!(
out,
"{}",
paint(opts.parse_theme.field, &format!("{field_name}: "))
paint(opts.parse_theme.field, format_args!("{field_name}: "))
)?;
}
@ -1055,10 +1093,13 @@ 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:
@ -1096,30 +1137,25 @@ 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 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;
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));
}
return Ok(line_start + position.column);
}
offset += 1;
break;
row += 1;
line_start = line_end + 1;
}
if position.row - row > 0 {
if row != position.row {
return Err(anyhow!("Failed to address a row: {}", position.row));
}
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 {
if position.column > input.len() - line_start {
return Err(anyhow!("Failed to address a column over the end"));
}
Ok(offset + position.column)
Ok(line_start + position.column)
}
pub fn position_for_offset(input: &[u8], offset: usize) -> Result<Point> {
@ -1139,3 +1175,45 @@ 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,7 +19,8 @@
--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;
@ -28,6 +29,7 @@
--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);
@ -42,6 +44,7 @@
--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"] {
@ -50,6 +53,7 @@
--text-color: var(--light-text);
--panel-bg: white;
--code-bg: white;
--tree-row-bg: var(--light-tree-row-bg);
}
/* Base Styles */
@ -275,7 +279,7 @@
}
#output-container a.highlighted {
background-color: #d9d9d9;
background-color: #cae2ff;
color: red;
border-radius: 3px;
text-decoration: underline;
@ -346,7 +350,7 @@
}
& #output-container a.highlighted {
background-color: #373b41;
background-color: #656669;
color: red;
}
@ -373,6 +377,9 @@
color: var(--dark-text);
}
}
.tree-row:has(.highlighted) {
background-color: var(--tree-row-bg);
}
</style>
</head>
@ -463,7 +470,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::{self, FromStr as _},
str::FromStr as _,
};
use anyhow::{anyhow, Context, Result};
use anyhow::{Context, Result, anyhow};
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 = stdout.lock();
let mut stdout = io::BufWriter::with_capacity(64 * 1024, 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,7 +142,9 @@ pub fn query_file_at_path(
};
// Invariant: `test_summary` will always be `Some` when `should_test` is true
let test_summary = test_summary.unwrap();
match query_testing::assert_expected_captures(&results, path, &mut parser, language) {
let assertions =
query_testing::parse_position_comments(&mut parser, language, source_code.as_slice())?;
match query_testing::assert_expected_captures(&results, &assertions) {
Ok(assertion_count) => {
test_summary.query_results.add_case(TestResult {
name: path_name.to_string(),

View file

@ -1,6 +1,6 @@
use std::{fs, path::Path, sync::LazyLock};
use std::sync::LazyLock;
use anyhow::{anyhow, Result};
use anyhow::{Result, anyhow};
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") {
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 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;
}
// 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() {
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 == '<';
}
// 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,19 +219,14 @@ pub fn parse_position_comments(
Ok(result)
}
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 {
pub fn assert_expected_captures(infos: &[CaptureInfo], assertions: &[Assertion]) -> Result<usize> {
for assertion in assertions {
if let Some(found) = &infos.iter().find(|p| {
assertion.position >= p.start
&& (assertion.position.row < p.end.row
|| assertion.position.column + assertion.length - 1 < p.end.column)
let assertion_end = Utf8Point::new(
assertion.position.row,
assertion.position.column + assertion.length - 1,
);
assertion.position >= p.start && assertion_end < p.end
}) {
if assertion.expected_capture_name != found.name && found.name != "name" {
return Err(anyhow!(
@ -250,5 +245,24 @@ pub fn assert_expected_captures(
));
}
}
Ok(pairs.len())
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());
}
}

View file

@ -2,8 +2,7 @@ use std::{
fs,
io::{self, Write},
path::Path,
str,
sync::{atomic::AtomicUsize, Arc},
sync::{Arc, atomic::AtomicUsize},
time::Instant,
};
@ -49,7 +48,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,
@ -59,7 +58,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,13 +1,15 @@
"""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,25 +1,18 @@
[package]
name = "tree-sitter-PARSER_NAME"
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"
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"
version = "PARSER_VERSION"
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,18 +11,8 @@ 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,46 +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 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 = b.createModule(.{
.target = target,
.optimize = optimize,
.link_libc = true,
.pic = if (shared) true else null,
}),
.root_module = grammar,
});
lib.addCSourceFile(.{
grammar.addCSourceFile(.{
.file = b.path("src/parser.c"),
.flags = &.{"-std=c11"},
});
if (fileExists(b, "src/scanner.c")) {
lib.addCSourceFile(.{
if (fileExists(b, io, "src/scanner.c")) {
grammar.addCSourceFile(.{
.file = b.path("src/scanner.c"),
.flags = &.{"-std=c11"},
});
}
if (reuse_alloc) {
lib.root_module.addCMacro("TREE_SITTER_REUSE_ALLOCATOR", "");
grammar.addCMacro("TREE_SITTER_REUSE_ALLOCATOR", "");
}
if (optimize == .Debug) {
lib.root_module.addCMacro("TREE_SITTER_DEBUG", "");
grammar.addCMacro("TREE_SITTER_DEBUG", "");
}
lib.addIncludePath(b.path("src"));
grammar.addIncludePath(b.path("src"));
b.installArtifact(lib);
b.installFile("src/node-types.json", "node-types.json");
if (fileExists(b, "queries")) {
if (fileExists(b, io, "queries")) {
b.installDirectory(.{
.source_dir = b.path("queries"),
.install_dir = .prefix,
@ -69,16 +74,10 @@ 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.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;
}
}
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"));
}
const run_tests = b.addRunArtifact(tests);
@ -86,8 +85,8 @@ pub fn build(b: *std.Build) !void {
test_step.dependOn(&run_tests.step);
}
inline fn fileExists(b: *std.Build, filename: []const u8) bool {
inline fn fileExists(b: *std.Build, io: std.Io, filename: []const u8) bool {
const dir = b.build_root.handle;
dir.access(filename, .{}) catch return false;
dir.access(io, filename, .{}) catch return false;
return true;
}

View file

@ -1,11 +1,12 @@
.{
.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#b4b72c903e69998fc88e27e154a5e3cc9166551b",
.hash = "tree_sitter-0.25.0-8heIf51vAQConvVIgvm-9mVIbqh7yabZYqPXfOpS3YoG",
.url = "git+https://github.com/tree-sitter/zig-tree-sitter#0cf58172e61f6fdd16f681cde42b4acb531a23db",
.hash = "tree_sitter-0.26.0-8heIf3CaAQDeVTQc0DMSBhbQAEx5aF-dTen4_LPxMgrv",
.lazy = true,
},
},

View file

@ -17,7 +17,7 @@ endif()
include(GNUInstallDirs)
find_program(TREE_SITTER_CLI tree-sitter DOC "Tree-sitter CLI")
find_program(TREE_SITTER_CLI tree-sitter DOC "Tree-sitter CLI" REQUIRED)
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/Parser.Language.html Parser.Language}
* @see {@linkcode https://tree-sitter.github.io/node-tree-sitter/interfaces/Language.html Parser.Language}
*
* @example
* import Parser from "tree-sitter";

View file

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

View file

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

View file

@ -1,10 +1,11 @@
// swift-tools-version:5.3
// swift-tools-version:5.6
import Foundation
import PackageDescription
let dir = Context.packageDirectory
var sources = ["src/parser.c"]
if FileManager.default.fileExists(atPath: "src/scanner.c") {
if FileManager.default.fileExists(atPath: "\(dir)/src/scanner.c") {
sources.append("src/scanner.c")
}
@ -14,7 +15,7 @@ let package = Package(
.library(name: "PARSER_CLASS_NAME", targets: ["PARSER_CLASS_NAME"]),
],
dependencies: [
.package(name: "SwiftTreeSitter", url: "https://github.com/tree-sitter/swift-tree-sitter", from: "0.9.0"),
.package(url: "https://github.com/tree-sitter/swift-tree-sitter", from: "0.10.0"),
],
targets: [
.target(
@ -31,7 +32,7 @@ let package = Package(
.testTarget(
name: "PARSER_CLASS_NAMETests",
dependencies: [
"SwiftTreeSitter",
.product(name: "SwiftTreeSitter", package: "swift-tree-sitter"),
"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]
name = "tree-sitter-PARSER_NAME"
description = "PARSER_DESCRIPTION"
version = "PARSER_VERSION"
keywords = ["incremental", "parsing", "tree-sitter", "PARSER_NAME"]
authors = [ { email = "PARSER_AUTHOR_EMAIL", name = "PARSER_AUTHOR_NAME" } ]
classifiers = [
"Intended Audience :: Developers",
"Topic :: Software Development :: Compilers",
"Topic :: Text Processing :: Linguistic",
"Typing :: Typed",
]
authors = [{ name = "PARSER_AUTHOR_NAME", email = "PARSER_AUTHOR_EMAIL" }]
requires-python = ">=3.10"
description = "PARSER_DESCRIPTION"
keywords = [ "incremental", "parsing", "tree-sitter", "PARSER_NAME" ]
license.text = "PARSER_LICENSE"
name = "tree-sitter-PARSER_NAME"
readme = "README.md"
requires-python = ">=3.10"
version = "PARSER_VERSION"
[project.urls]
Homepage = "PARSER_URL"
Funding = "FUNDING_URL"
Homepage = "PARSER_URL"
[project.optional-dependencies]
core = ["tree-sitter~=0.24"]
core = [ "tree-sitter~=0.24" ]
[tool.cibuildwheel]
build = "cp310-*"

View file

@ -42,6 +42,7 @@ 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::{anyhow, Result};
use anyhow::{Result, anyhow};
use tree_sitter::Point;
use tree_sitter_highlight::{Highlight, HighlightConfiguration, HighlightEvent, Highlighter};
use tree_sitter_loader::{Config, Loader};
use crate::{
query_testing::{parse_position_comments, to_utf8_point, Assertion, Utf8Point},
query_testing::{Assertion, Utf8Point, parse_position_comments, to_utf8_point},
test::{TestInfo, TestOutcome, TestResult, TestSummary},
util,
};
#[derive(Debug)]
pub struct Failure {
row: usize,
column: usize,
expected_highlight: String,
actual_highlights: Vec<String>,
pub(crate) row: usize,
pub(crate) column: usize,
pub(crate) expected_highlight: String,
pub(crate) actual_highlights: Vec<String>,
}
impl std::error::Error for Failure {}
@ -120,12 +120,9 @@ 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)],
@ -142,49 +139,48 @@ pub fn iterate_assertions(
expected_capture_name: expected_highlight,
} in assertions
{
let mut passed = false;
let mut end_column = position.column + length - 1;
// 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();
// 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) {
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.
if highlight.1 <= *position {
i += 1;
continue;
}
if (highlight.0.row > position.row)
|| (highlight.0.row == position.row && highlight.0.column > end_column)
{
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 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;
}
}
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_highlight.clone(),
expected_highlight: expected,
actual_highlights: actual_highlights.into_iter().cloned().collect(),
}
.into());
@ -223,9 +219,11 @@ 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, |string| {
loader.highlight_config_for_injection_string(string)
})? {
for event in
highlighter.highlight(highlight_config, source.as_bytes(), None, 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::{anyhow, Result};
use anyhow::{Result, anyhow};
use tree_sitter_loader::{Config, Loader};
use tree_sitter_tags::{TagsConfiguration, TagsContext};
use crate::{
query_testing::{parse_position_comments, to_utf8_point, Assertion, Utf8Point},
query_testing::{Assertion, Utf8Point, parse_position_comments, to_utf8_point},
test::{TestInfo, TestOutcome, TestResult, TestSummary},
util,
};
@ -113,11 +113,7 @@ pub fn test_tags(
}
}
if failed {
Err(anyhow!(""))
} else {
Ok(())
}
if failed { Err(anyhow!("")) } else { Ok(()) }
}
pub fn test_tag(

View file

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

View file

@ -2,24 +2,25 @@ 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::{parse_tests, strip_sexp_fields, DiffKey, TestDiff},
test::{DiffKey, TestDiff, parse_tests, render_test_output},
tests::{
allocations,
helpers::fixtures::{fixtures_dir, get_language, get_test_language, SCRATCH_BASE_DIR},
helpers::fixtures::{SCRATCH_BASE_DIR, fixtures_dir, get_language, get_test_language},
},
};
@ -120,10 +121,10 @@ pub fn test_language_corpus(
skipped: Option<&[&str]>,
language_dir: Option<&str>,
) {
if let Some(filter) = LANGUAGE_FILTER.as_ref() {
if language_name != filter {
return;
}
if let Some(filter) = LANGUAGE_FILTER.as_ref()
&& language_name != filter
{
return;
}
let language_dir = language_dir.unwrap_or_default();
@ -185,38 +186,17 @@ 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() {
if 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()
&& 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(|| {
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
});
let passed = allocations::record(|| test.check_initial_parse(&language, &test_name, true));
if !passed {
failure_count += 1;
@ -274,7 +254,9 @@ 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;
}
@ -290,10 +272,8 @@ 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 mut actual_output = tree3.root_node().to_sexp();
if !test.has_fields {
actual_output = strip_sexp_fields(&actual_output);
}
let actual_output =
render_test_output(&input, &tree3, test.cst, test.has_fields).unwrap();
if actual_output != test.output {
println!("Incorrect parse for {test_name} - seed {seed}");
@ -306,7 +286,9 @@ 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;
}
@ -351,10 +333,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() {
if language_name != filter {
continue;
}
if let Some(filter) = LANGUAGE_FILTER.as_ref()
&& language_name != filter
{
continue;
}
let test_path = entry.path();
@ -371,8 +353,12 @@ fn test_feature_corpus_files() {
)
})
.unwrap();
let generate_result =
tree_sitter_generate::generate_parser_for_grammar(&grammar_json, Some((0, 0, 0)));
let generate_result = tree_sitter_generate::generate_parser_for_grammar(
&grammar_json,
Some((0, 0, 0)),
OptLevel::default(),
&mut Vec::new(),
);
if error_message_path.exists() {
if EXAMPLE_INCLUDE.is_some() || EXAMPLE_EXCLUDE.is_some() {
@ -393,12 +379,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;
}
@ -416,24 +402,8 @@ fn test_feature_corpus_files() {
for test in tests {
eprintln!(" example: {:?}", test.name);
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
}
});
let passed =
allocations::record(|| test.check_initial_parse(&language, &test.name, true));
if !passed {
failure_count += 1;

View file

@ -127,6 +127,52 @@ 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, str};
use std::ops::Range;
#[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 + 1)]
&self.content[offset..=offset]
} 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(ref mut range) = &mut last_range {
if let Some(range) = &mut last_range {
if range.end == *index {
range.end += 1;
} else {

View file

@ -1,12 +1,13 @@
use std::{
collections::HashSet,
env, fs,
path::{Path, PathBuf},
sync::LazyLock,
sync::{LazyLock, Mutex},
};
use anyhow::Context;
use tree_sitter::Language;
use tree_sitter_generate::{load_grammar_file, ALLOC_HEADER, ARRAY_HEADER};
use tree_sitter_generate::{ALLOC_HEADER, ARRAY_HEADER, load_grammar_file};
use tree_sitter_highlight::HighlightConfiguration;
use tree_sitter_loader::{CompileConfig, Loader};
use tree_sitter_tags::TagsConfiguration;
@ -23,6 +24,10 @@ 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);
@ -134,17 +139,22 @@ fn get_test_language_internal(
};
let header_path = src_dir.join("tree_sitter");
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();
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();
}
}
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::prelude::Rng;
use rand::{Rng, RngExt};
use streaming_iterator::{IntoStreamingIterator, StreamingIterator};
use tree_sitter::{
Language, Node, Parser, Point, Query, QueryCapture, QueryCursor, QueryMatch, Tree, TreeCursor,
};
#[derive(Debug)]
pub struct Pattern {
kind: Option<&'static str>,
pub struct Pattern<'a> {
kind: Option<&'a str>,
named: bool,
field: Option<&'static str>,
field: Option<&'a str>,
capture: Option<String>,
children: Vec<Self>,
}
@ -25,17 +25,17 @@ const CAPTURE_NAMES: &[&str] = &[
"one", "two", "three", "four", "five", "six", "seven", "eight",
];
impl Pattern {
pub fn random_pattern_in_tree(tree: &Tree, rng: &mut impl Rng) -> (Self, Range<Point>) {
impl<'a> Pattern<'a> {
pub fn random_pattern_in_tree(tree: &'a 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.gen_range(0..cursor.node().end_byte());
let byte_offset = rng.random_range(0..cursor.node().end_byte());
while cursor.goto_first_child_for_byte(byte_offset).is_some() {
max_depth += 1;
}
let depth = rng.gen_range(0..=max_depth);
let depth = rng.random_range(0..=max_depth);
for _ in 0..depth {
cursor.goto_parent();
}
@ -45,7 +45,7 @@ impl Pattern {
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.gen_bool(0.2) {
if rng.random_bool(0.2) {
roots.push(Self::random_pattern_for_node(&mut cursor, rng));
}
}
@ -75,26 +75,26 @@ impl Pattern {
(pattern, pattern_start..pattern_end)
}
fn random_pattern_for_node(cursor: &mut TreeCursor, rng: &mut impl Rng) -> Self {
fn random_pattern_for_node(cursor: &mut TreeCursor<'a>, rng: &mut impl Rng) -> Self {
let node = cursor.node();
// Sometimes specify the node's type, sometimes use a wildcard.
let (kind, named) = if rng.gen_bool(0.9) {
let (kind, named) = if rng.random_bool(0.9) {
(Some(node.kind()), node.is_named())
} else {
(Some("_"), node.is_named() && rng.gen_bool(0.8))
(Some("_"), node.is_named() && rng.random_bool(0.8))
};
// Sometimes specify the node's field.
let field = if rng.gen_bool(0.75) {
let field = if rng.random_bool(0.75) {
cursor.field_name()
} else {
None
};
// Sometimes capture the node.
let capture = if rng.gen_bool(0.7) {
Some(CAPTURE_NAMES[rng.gen_range(0..CAPTURE_NAMES.len())].to_string())
let capture = if rng.random_bool(0.7) {
Some(CAPTURE_NAMES[rng.random_range(0..CAPTURE_NAMES.len())].to_string())
} else {
None
};
@ -102,9 +102,9 @@ impl Pattern {
// 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.gen_range(0..4);
let max_children = rng.random_range(0..4);
while cursor.goto_next_sibling() {
if rng.gen_bool(0.6) {
if rng.random_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 Pattern {
}
// If a field is specified, check that it matches the node.
if let Some(field) = self.field {
if cursor.field_name() != Some(field) {
return Vec::new();
}
if let Some(field) = self.field
&& cursor.field_name() != Some(field)
{
return Vec::new();
}
// Create a match for the current node.
@ -225,7 +225,7 @@ impl Pattern {
}
// Find every matching combination of child patterns and child nodes.
let mut finished_matches = Vec::<Match>::new();
let mut finished_matches = Vec::<Match<'_, 'tree>>::new();
if cursor.goto_first_child() {
let mut match_states = vec![(0, mat)];
loop {
@ -268,7 +268,7 @@ impl Pattern {
}
}
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, str,
ptr, slice,
sync::{
atomic::{AtomicUsize, Ordering},
LazyLock,
atomic::{AtomicUsize, Ordering},
},
};
use tree_sitter_highlight::{
c, Error, Highlight, HighlightConfiguration, HighlightEvent, Highlighter, HtmlRenderer,
Error, Highlight, HighlightConfiguration, HighlightEvent, Highlighter, HtmlRenderer, c,
};
use super::helpers::fixtures::{get_highlight_config, get_language, get_language_queries_path};
@ -485,6 +485,7 @@ fn test_highlighting_cancellation() {
.highlight(
&HTML_HIGHLIGHT,
source.as_bytes(),
None,
Some(&cancellation_flag),
injection_callback,
)
@ -495,7 +496,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")
}
});
@ -727,6 +728,7 @@ fn to_html<'a>(
language_config,
src,
None,
None,
&test_language_for_injection_string,
)?;
@ -747,7 +749,10 @@ fn to_html<'a>(
.collect())
}
#[allow(clippy::type_complexity)]
#[expect(
clippy::type_complexity,
reason = "return type represents structured highlight tokens"
)]
fn to_token_vector<'a>(
src: &'a str,
language_config: &'a HighlightConfiguration,
@ -761,6 +766,7 @@ fn to_token_vector<'a>(
language_config,
src,
None,
None,
&test_language_for_injection_string,
)?;
for event in events {

View file

@ -31,14 +31,17 @@ 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);
lookahead.reset_state(next_state);
assert!(lookahead.reset_state(next_state));
assert!(lookahead.iter_names().eq(expected_symbols));
lookahead.reset(&language, next_state);
assert!(lookahead
.map(|s| language.node_kind_for_id(s).unwrap())
.eq(expected_symbols));
assert!(lookahead.reset(&language, next_state));
assert!(
lookahead
.map(|s| language.node_kind_for_id(s).unwrap())
.eq(expected_symbols)
);
}
#[test]
@ -64,6 +67,33 @@ 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");
@ -110,7 +140,7 @@ fn test_supertypes() {
supertypes
.iter()
.filter_map(|&s| language.node_kind_for_id(s))
.map(|s| s.to_string())
.map(std::string::ToString::to_string)
.collect::<Vec<String>>(),
vec![
"_expression",

View file

@ -2,9 +2,8 @@ use tree_sitter::{InputEdit, Node, Parser, Point, Tree};
use tree_sitter_generate::load_grammar_file;
use super::{
get_random_edit,
Rand, get_random_edit,
helpers::fixtures::{fixtures_dir, get_language, get_test_language},
Rand,
};
use crate::{
parse::perform_edit,
@ -286,7 +285,10 @@ 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(),
@ -452,7 +454,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");
@ -948,6 +950,13 @@ 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#"
{
@ -970,8 +979,6 @@ 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",
@ -992,7 +999,6 @@ fn test_node_field_names() {
]
},
// Fields pointing to hidden nodes with a single child resolve to the child.
"_hidden_rule1": {
"type": "CHOICE",
"members": [
@ -1001,7 +1007,6 @@ fn test_node_field_names() {
]
},
// Fields within hidden nodes can be referenced through the parent node.
"_hidden_rule2": {
"type": "SEQ",
"members": [

View file

@ -66,9 +66,13 @@ fn test_parsing_with_logging() {
parser.set_language(&get_language("rust")).unwrap();
let mut messages = Vec::new();
parser.set_logger(Some(Box::new(|log_type, message| {
messages.push((log_type, message.to_string()));
})));
// 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
.parse(
@ -249,6 +253,79 @@ 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();
@ -745,11 +822,7 @@ 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)),
@ -772,11 +845,7 @@ 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 |_| {
@ -794,11 +863,7 @@ 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 |_| {
@ -995,9 +1060,9 @@ fn test_parsing_with_timeout_during_balancing() {
let mut parser = Parser::new();
parser.set_language(&get_language("javascript")).unwrap();
let function_count = 100;
let function_count: u32 = 100;
let code = "function() {}\n".repeat(function_count);
let code = "function() {}\n".repeat(function_count as usize);
let mut current_byte_offset = 0;
let mut in_balancing = false;
let tree = parser.parse_with_options(
@ -1071,7 +1136,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!(state.current_byte_offset() == current_byte_offset);
assert_eq!(state.current_byte_offset(), current_byte_offset);
ControlFlow::Continue(())
})),
)
@ -1739,11 +1804,15 @@ fn test_parsing_with_scanner_logging() {
.unwrap();
let mut found = false;
parser.set_logger(Some(Box::new(|log_type, message| {
if log_type == LogType::Lex && message == "Found a percent string" {
found = true;
}
})));
// 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;
}
})));
}
let source_code = "x + %(sup (external) scanner?)";
@ -1863,7 +1932,7 @@ fn test_decode_cp1252() {
fn decode(bytes: &[u8]) -> (i32, u32) {
if !bytes.is_empty() {
let byte = bytes[0];
(byte as i32, 1)
(i32::from(byte), 1)
} else {
(0, 0)
}
@ -1899,7 +1968,7 @@ fn test_decode_macintosh() {
fn decode(bytes: &[u8]) -> (i32, u32) {
if !bytes.is_empty() {
let byte = bytes[0];
(byte as i32, 1)
(i32::from(byte), 1)
} else {
(0, 0)
}
@ -1961,8 +2030,9 @@ fn test_decode_utf24le() {
#[test]
fn test_grammars_that_should_not_compile() {
assert!(generate_parser(
r#"
assert!(
generate_parser(
r#"
{
"name": "issue_1111",
"rules": {
@ -1970,11 +2040,13 @@ fn test_grammars_that_should_not_compile() {
},
}
"#
)
.is_err());
)
.is_err()
);
assert!(generate_parser(
r#"
assert!(
generate_parser(
r#"
{
"name": "issue_1271",
"rules": {
@ -1989,11 +2061,13 @@ 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": {
@ -2007,11 +2081,13 @@ 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": {
@ -2028,11 +2104,13 @@ 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": {
@ -2046,11 +2124,13 @@ 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": {
@ -2067,8 +2147,9 @@ 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 = { version = "2.0.96", features = ["full"] }
syn = { features = [ "full" ], version = "2.0.96" }

View file

@ -2,8 +2,9 @@ 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, Error, Expr, Ident, ItemFn, LitInt, Token,
parse_macro_input,
};
#[proc_macro_attribute]
@ -68,7 +69,7 @@ pub fn test_with_seed(args: TokenStream, input: TokenStream) -> TokenStream {
return Err(Error::new(
name.span(),
format!("Unsupported parameter `{x}`"),
))
));
}
}

View file

@ -1,12 +1,12 @@
use std::{env, fmt::Write, ops::ControlFlow, sync::LazyLock};
use indoc::indoc;
use rand::{prelude::StdRng, SeedableRng};
use rand::{SeedableRng, prelude::StdRng};
use streaming_iterator::StreamingIterator;
use tree_sitter::{
CaptureQuantifier, InputEdit, Language, Node, Parser, Point, Query, QueryCursor,
QueryCursorOptions, QueryError, QueryErrorKind, QueryPredicate, QueryPredicateArg,
QueryProperty, Range,
QueryCursorOptions, QueryCursorState, QueryError, QueryErrorKind, QueryPredicate,
QueryPredicateArg, QueryProperty, Range,
};
use tree_sitter_generate::load_grammar_file;
use unindent::Unindent;
@ -14,15 +14,14 @@ use unindent::Unindent;
use super::helpers::{
allocations,
fixtures::{get_language, get_test_language},
query_helpers::{assert_query_matches, Match, Pattern},
query_helpers::{Match, Pattern, assert_query_matches},
};
use crate::tests::{
generate_parser,
ITERATION_COUNT, generate_parser,
helpers::{
fixtures::get_test_fixture_language,
query_helpers::{collect_captures, collect_matches},
},
ITERATION_COUNT,
};
static EXAMPLE_FILTER: LazyLock<Option<String>> =
@ -34,11 +33,13 @@ fn test_query_errors_on_invalid_syntax() {
let language = get_language("javascript");
assert!(Query::new(&language, "(if_statement)").is_ok());
assert!(Query::new(
&language,
"(if_statement condition:(parenthesized_expression (identifier)))"
)
.is_ok());
assert!(
Query::new(
&language,
"(if_statement condition:(parenthesized_expression (identifier)))"
)
.is_ok()
);
// Mismatched parens
assert_eq!(
@ -255,6 +256,51 @@ fn test_query_errors_on_invalid_syntax() {
});
}
#[test]
fn test_query_errors_on_anchor_at_group_edge() {
allocations::record(|| {
let language = get_language("javascript");
// Anchors between siblings, or at the first/last position of a *node*
// pattern, are valid.
assert!(Query::new(&language, "((_) . (_))").is_ok());
assert!(Query::new(&language, "(program (_) (_) .)").is_ok());
assert!(Query::new(&language, "(program (_)* @x . (_))").is_ok());
// A `.` at the edge of a *group* is rejected. A group is not a node, so it
// has no last child to anchor, and there is no sibling within the group to
// anchor to.
assert_eq!(
Query::new(&language, "((_) .)").unwrap_err(),
QueryError {
row: 0,
offset: 5,
column: 5,
kind: QueryErrorKind::Syntax,
message: [
"((_) .)", //
" ^"
]
.join("\n")
}
);
assert_eq!(
Query::new(&language, "(program ((_)+ .)? (_))").unwrap_err(),
QueryError {
row: 0,
offset: 15,
column: 15,
kind: QueryErrorKind::Syntax,
message: [
"(program ((_)+ .)? (_))", //
" ^"
]
.join("\n")
}
);
});
}
#[test]
fn test_query_errors_on_invalid_symbols() {
allocations::record(|| {
@ -370,6 +416,16 @@ fn test_query_errors_on_invalid_symbols() {
message: "\"fakefield\"".to_string()
}
);
assert_eq!(
Query::new(&language, "(MISS)").unwrap_err(),
QueryError {
row: 0,
offset: 1,
column: 1,
kind: QueryErrorKind::NodeType,
message: "\"MISS\"".to_string(),
}
);
});
}
@ -1107,8 +1163,8 @@ fn test_query_matches_with_immediate_siblings() {
// siblings before that child node.
// 2. After the last child node in a pattern, it means that there cannot be any named
// sibling after that child node.
// 2. Between two child nodes in a pattern, it specifies that there cannot be any named
// siblings between those two child snodes.
// 3. Between two child nodes in a pattern, it specifies that there cannot be any named
// siblings between those two child nodes.
let query = Query::new(
&language,
"
@ -1176,6 +1232,209 @@ fn test_query_matches_with_immediate_siblings() {
});
}
#[test]
fn test_query_matches_with_anchor_after_zero_quantifier() {
allocations::record(|| {
let language = get_language("javascript");
let query = Query::new(
&language,
"(program (comment)* @doc . (function_declaration name: (identifier) @name))",
)
.unwrap();
// No comments and the function is not the first child. An anchor after a
// zero-matched quantifier is vacuous, so the function still matches.
assert_query_matches(
&language,
&query,
"
class X {}
function foo() {}
",
&[(0, vec![("name", "foo")])],
);
// With at least one comment the anchor applies, so the comments must
// immediately precede the function.
assert_query_matches(
&language,
&query,
"
// c
function foo() {}
",
&[(0, vec![("doc", "// c"), ("name", "foo")])],
);
});
}
#[test]
fn test_query_matches_with_anchor_after_nested_zero_quantifier() {
allocations::record(|| {
let language = get_language("javascript");
let query = Query::new(
&language,
r#"
(_
(field_definition
property: (_) @name
value: (_)? @value
) @field
.
";" @semicolon
)
"#,
)
.unwrap();
assert_query_matches(
&language,
&query,
"class Foo { bar; baz = 0; }",
&[
(
0,
vec![("field", "bar"), ("name", "bar"), ("semicolon", ";")],
),
(
0,
vec![
("field", "baz = 0"),
("name", "baz"),
("value", "0"),
("semicolon", ";"),
],
),
],
);
});
}
#[test]
fn test_query_matches_with_last_child_anchor_after_optional() {
allocations::record(|| {
let language = get_language("c");
let query = Query::new(
&language,
"(preproc_if (preproc_def)+ @def . (preproc_else)? @else .)",
)
.unwrap();
// The optional `(preproc_else)?` is absent, so the trailing anchor's
// last-child requirement transfers to the last `preproc_def`. A trailing
// comment means the def is not the last child, so nothing matches.
assert_query_matches(
&language,
&query,
"
#if X
#define A
// c
#endif
",
&[],
);
// With the def as the last child, the (else-less) match is allowed.
assert_query_matches(
&language,
&query,
"
#if X
#define A
#endif
",
&[(0, vec![("def", "#define A\n")])],
);
});
}
#[test]
fn test_query_matches_with_anchors_on_both_sides_of_zero_quantifier() {
allocations::record(|| {
let language = get_language("javascript");
let query = Query::new(
&language,
"(program (lexical_declaration) @a . (comment)* . (function_declaration) @b)",
)
.unwrap();
// Anchors on both sides of a zero-matched quantifier collapse into a single
// adjacency constraint: with no comments, the declaration must be immediately
// followed by the function.
assert_query_matches(
&language,
&query,
"
const a = 1;
const b = 2;
function foo() {}
",
&[(0, vec![("a", "const b = 2;"), ("b", "function foo() {}")])],
);
// With a comment present the quantifier is non-zero, so the anchors apply
// normally: the comment must sit immediately between the declaration and the
// function.
assert_query_matches(
&language,
&query,
"
const b = 2;
// c
function foo() {}
",
&[(0, vec![("a", "const b = 2;"), ("b", "function foo() {}")])],
);
});
}
#[test]
fn test_query_matches_with_leading_anchor_before_zero_quantifier() {
allocations::record(|| {
let language = get_language("c");
let query = Query::new(
&language,
"(translation_unit . (comment)* (function_definition) @f)",
)
.unwrap();
// The leading `.` anchors the comment run to the parent's first child. When the
// run matches zero comments, that first-child requirement transfers to the
// function, so it matches only when it is itself the first child.
assert_query_matches(
&language,
&query,
"
int main() {}
",
&[(0, vec![("f", "int main() {}")])],
);
// The function is the second child, so with no leading comments it must not match.
assert_query_matches(
&language,
&query,
"
int a;
int main() {}
",
&[],
);
// With a leading comment the run starts at the first child and the function follows.
assert_query_matches(
&language,
&query,
"
// c
int main() {}
",
&[(0, vec![("f", "int main() {}")])],
);
});
}
#[test]
fn test_query_matches_with_last_named_child() {
allocations::record(|| {
@ -1348,6 +1607,35 @@ fn test_query_matches_with_repeated_leaf_nodes() {
});
}
#[test]
fn test_query_matches_optional_capture_before_uncaptured_required_sibling() {
allocations::record(|| {
let language = get_language("rust");
let query = Query::new(&language, "(block (line_comment)? @doc (line_comment))").unwrap();
// The optional `(line_comment)? @doc` before an *uncaptured* required
// `(line_comment)` yields two candidate completions at the block:
// - one where the optional captured `// a` (the required node is then `// b`),
// - one where the optional matched zero (the required node absorbs `// a`,
// leaving `@doc` unbound).
// The zero-match completion's captures are a strict subset of the other's, so the
// longest-match rule must drop it (there is exactly one match). This regressed when
// the dedup pass gained an early-break. Without the accompanying capture-position
// sort, the break skips the subset "loser" and it leaks as an extra empty match.
assert_query_matches(
&language,
&query,
"
fn f() {
// a
// b
}
",
&[(0, vec![("doc", "// a")])],
);
});
}
#[test]
fn test_query_matches_with_optional_nodes_inside_of_repetitions() {
allocations::record(|| {
@ -1592,6 +1880,148 @@ fn test_query_matches_with_leading_zero_or_more_repeated_leaf_nodes() {
});
}
#[test]
fn test_matches_with_anchor_sibling_inside_parent() {
allocations::record(|| {
let language = get_language("rust");
let query = Query::new(
&language,
"
(source_file
(line_comment)
.
(function_item
name: (identifier) @name)
)",
)
.unwrap();
assert_query_matches(
&language,
&query,
"
// A
fn a() {}
// B
fn b() {}
",
&[(0, vec![("name", "a")]), (0, vec![("name", "b")])],
);
});
}
#[test]
fn test_matches_with_anchor_sibling_with_quantifier_inside_parent() {
allocations::record(|| {
let language = get_language("rust");
let query = Query::new(
&language,
"
(source_file
(line_comment)+
.
(function_item
name: (identifier) @name)
)",
)
.unwrap();
assert_query_matches(
&language,
&query,
"
// A
fn a() {}
// B
fn b() {}
",
&[(0, vec![("name", "a")]), (0, vec![("name", "b")])],
);
});
}
#[test]
fn test_matches_with_anchor_sibling_with_quantifier_captured_inside_parent() {
allocations::record(|| {
let language = get_language("rust");
let query = Query::new(
&language,
"
(source_file
(line_comment)+ @doc
.
(function_item
name: (identifier) @name)
)",
)
.unwrap();
assert_query_matches(
&language,
&query,
"
// A
fn a() {}
// B
fn b() {}
",
&[
(0, vec![("doc", "// A"), ("name", "a")]),
(0, vec![("doc", "// B"), ("name", "b")]),
],
);
});
}
#[test]
fn test_matches_anchored_quantified_sibling_inside_parent() {
allocations::record(|| {
let language = get_language("c");
let query = Query::new(
&language,
"(translation_unit (comment)* @comment . (declaration) @decl)",
)
.unwrap();
assert_query_matches(
&language,
&query,
"
void foo() {}
// this one has
// two comments
extern int baz;
// this one has a comment
extern int bar;
",
&[
(
0,
vec![
("comment", "// this one has"),
("comment", "// two comments"),
("decl", "extern int baz;"),
],
),
(
0,
vec![
("comment", "// this one has a comment"),
("decl", "extern int bar;"),
],
),
],
);
});
}
#[test]
fn test_query_matches_with_trailing_optional_nodes() {
allocations::record(|| {
@ -1940,7 +2370,7 @@ fn test_query_matches_with_too_many_permutations_to_track() {
let matches = cursor.matches(&query, tree.root_node(), source.as_bytes());
// For this pathological query, some match permutations will be dropped.
// Just check that a subset of the results are returned, and crash or
// Just check that a subset of the results are returned, and no crash or
// leak occurs.
assert_eq!(
collect_matches(matches, &query, source.as_str())[0],
@ -2167,7 +2597,7 @@ fn test_query_matches_with_supertypes() {
}
#[test]
#[allow(clippy::reversed_empty_ranges)]
#[expect(clippy::reversed_empty_ranges, reason = "testing empty range behavior")]
fn test_query_matches_within_byte_range() {
allocations::record(|| {
let language = get_language("javascript");
@ -2488,7 +2918,7 @@ fn test_query_matches_with_wildcard_at_root_intersecting_byte_range() {
);
while let Some(mat) = match_iter.next() {
if let Some(capture) = mat.captures.first() {
if let Some(capture) = mat.captures().first() {
matches.push(capture.node.kind());
}
}
@ -2504,7 +2934,7 @@ fn test_query_matches_with_wildcard_at_root_intersecting_byte_range() {
);
while let Some(mat) = match_iter.next() {
if let Some(capture) = mat.captures.first() {
if let Some(capture) = mat.captures().first() {
matches.push(capture.node.kind());
}
}
@ -2520,7 +2950,7 @@ fn test_query_matches_with_wildcard_at_root_intersecting_byte_range() {
);
while let Some(mat) = match_iter.next() {
if let Some(capture) = mat.captures.first() {
if let Some(capture) = mat.captures().first() {
matches.push(capture.node.kind());
}
}
@ -2578,7 +3008,7 @@ fn test_query_captures_within_byte_range_assigned_after_iterating() {
let mut results = Vec::new();
let mut first_five = captures.by_ref().take(5);
while let Some((mat, capture_ix)) = first_five.next() {
let capture = mat.captures[*capture_ix];
let capture = mat.captures()[*capture_ix];
results.push((
query.capture_names()[capture.index as usize],
&source[capture.node.byte_range()],
@ -2601,7 +3031,7 @@ fn test_query_captures_within_byte_range_assigned_after_iterating() {
results.clear();
captures.set_byte_range(source.find("Ok").unwrap()..source.len());
while let Some((mat, capture_ix)) = captures.next() {
let capture = mat.captures[*capture_ix];
let capture = mat.captures()[*capture_ix];
results.push((
query.capture_names()[capture.index as usize],
&source[capture.node.byte_range()],
@ -2900,7 +3330,7 @@ fn test_query_matches_with_captured_wildcard_at_root() {
while let Some(m) = match_iter.next() {
let captures = m
.captures
.captures()
.iter()
.map(|c| {
(
@ -3110,6 +3540,39 @@ fn test_query_alternation_with_inner_quantifier() {
assert_query_matches(&language, &query, source_code, matches);
}
#[test]
fn test_query_alternation_with_outer_quantifier() {
let language = get_language("c");
let source_code = "#include <foo>
#include <bar>
#include <baz>
// comment";
let matches = &[(
0,
vec![
("capture", "#include <foo>\n"),
("capture", "#include <bar>\n"),
("capture", "#include <baz>\n"),
("capture", "// comment"),
],
)];
let query = "[
(preproc_include)
(comment)
]+ @capture";
let query = Query::new(&language, query).unwrap();
assert_query_matches(&language, &query, source_code, matches);
let query = "([
(preproc_include)
(comment)
] (_)?)+ @capture";
let query = Query::new(&language, query).unwrap();
assert_query_matches(&language, &query, source_code, matches);
}
#[test]
fn test_query_matches_with_alternations_and_predicates() {
allocations::record(|| {
@ -3848,7 +4311,7 @@ fn test_query_captures_with_matches_removed() {
let mut captures = cursor.captures(&query, tree.root_node(), source.as_bytes());
while let Some((m, i)) = captures.next() {
let capture = m.captures[*i];
let capture = m.captures()[*i];
let text = capture.node.utf8_text(source.as_bytes()).unwrap();
if text == "a" {
m.remove();
@ -3893,7 +4356,7 @@ fn test_query_captures_with_matches_removed_before_they_finish() {
let mut captured_strings = Vec::new();
let mut captures = cursor.captures(&query, tree.root_node(), source.as_bytes());
while let Some((m, i)) = captures.next() {
let capture = m.captures[*i];
let capture = m.captures()[*i];
let text = capture.node.utf8_text(source.as_bytes()).unwrap();
if text == "as" {
m.remove();
@ -3934,18 +4397,18 @@ fn test_query_captures_and_matches_iterators_are_fused() {
let mut cursor = QueryCursor::new();
let mut captures = cursor.captures(&query, tree.root_node(), source.as_bytes());
assert_eq!(captures.next().unwrap().0.captures[0].index, 0);
assert_eq!(captures.next().unwrap().0.captures[0].index, 0);
assert_eq!(captures.next().unwrap().0.captures[0].index, 0);
assert_eq!(captures.next().unwrap().0.captures()[0].index, 0);
assert_eq!(captures.next().unwrap().0.captures()[0].index, 0);
assert_eq!(captures.next().unwrap().0.captures()[0].index, 0);
assert!(captures.next().is_none());
assert!(captures.next().is_none());
assert!(captures.next().is_none());
drop(captures);
let mut matches = cursor.matches(&query, tree.root_node(), source.as_bytes());
assert_eq!(matches.next().unwrap().captures[0].index, 0);
assert_eq!(matches.next().unwrap().captures[0].index, 0);
assert_eq!(matches.next().unwrap().captures[0].index, 0);
assert_eq!(matches.next().unwrap().captures()[0].index, 0);
assert_eq!(matches.next().unwrap().captures()[0].index, 0);
assert_eq!(matches.next().unwrap().captures()[0].index, 0);
assert!(matches.next().is_none());
assert!(matches.next().is_none());
assert!(matches.next().is_none());
@ -4120,13 +4583,13 @@ fn test_query_lifetime_is_separate_from_nodes_lifetime() {
let language = get_language("javascript");
let query = Query::new(&language, query).unwrap();
let mut cursor = QueryCursor::new();
let node = cursor
cursor
.matches(&query, node, source.as_bytes())
.next()
.unwrap()
.captures[0]
.node;
node
.captures()[0]
.node
}
let node = take_first_node_from_captures(source, query, tree.root_node());
@ -4140,14 +4603,14 @@ fn test_query_lifetime_is_separate_from_nodes_lifetime() {
let language = get_language("javascript");
let query = Query::new(&language, query).unwrap();
let mut cursor = QueryCursor::new();
let node = cursor
cursor
.captures(&query, node, source.as_bytes())
.next()
.unwrap()
.0
.captures[0]
.node;
node
.captures()[0]
.node
}
let node = take_first_node_from_matches(source, query, tree.root_node());
@ -4160,7 +4623,7 @@ fn test_query_with_no_patterns() {
allocations::record(|| {
let language = get_language("javascript");
let query = Query::new(&language, "").unwrap();
assert!(query.capture_names().is_empty());
assert_eq!(query.capture_names(), [] as [&str; 0]);
assert_eq!(query.pattern_count(), 0);
});
}
@ -4233,6 +4696,54 @@ fn test_query_disable_pattern() {
});
}
#[test]
fn test_query_deep_clone() {
allocations::record(|| {
let language = get_language("javascript");
let query = Query::new(
&language,
"
(function_declaration
name: (identifier) @name)
(function_declaration
body: (statement_block) @body)
",
)
.unwrap();
let mut clone = query.deep_clone();
clone.disable_pattern(1);
let source = "function foo() { return 1; }";
let mut parser = Parser::new();
parser.set_language(&language).unwrap();
let tree = parser.parse(source, None).unwrap();
let mut cursor = QueryCursor::new();
// The clone with pattern 1 disabled only produces the @name match.
let clone_matches = collect_matches(
cursor.matches(&clone, tree.root_node(), source.as_bytes()),
&clone,
source,
);
assert_eq!(clone_matches, &[(0, vec![("name", "foo")])]);
// The original is unaffected and still produces both @name and @body.
let original_matches = collect_matches(
cursor.matches(&query, tree.root_node(), source.as_bytes()),
&query,
source,
);
assert_eq!(
original_matches,
&[
(0, vec![("name", "foo")]),
(1, vec![("body", "{ return 1; }")]),
]
);
});
}
#[test]
fn test_query_alternative_predicate_prefix() {
allocations::record(|| {
@ -4308,7 +4819,7 @@ fn test_query_random() {
let transformed_match = Match {
last_node: None,
captures: mat
.captures
.captures()
.iter()
.map(|c| (query.capture_names()[c.index as usize], c.node))
.collect::<Vec<_>>(),
@ -4611,10 +5122,10 @@ fn test_query_is_pattern_guaranteed_at_step() {
eprintln!();
for row in rows {
if let Some(filter) = EXAMPLE_FILTER.as_ref() {
if !row.description.contains(filter.as_str()) {
continue;
}
if let Some(filter) = EXAMPLE_FILTER.as_ref()
&& !row.description.contains(filter.as_str())
{
continue;
}
eprintln!(" query example: {:?}", row.description);
let query = Query::new(&row.language, row.pattern).unwrap();
@ -4707,10 +5218,10 @@ fn test_query_is_pattern_rooted() {
let language = get_language("python");
for row in &rows {
if let Some(filter) = EXAMPLE_FILTER.as_ref() {
if !row.description.contains(filter.as_str()) {
continue;
}
if let Some(filter) = EXAMPLE_FILTER.as_ref()
&& !row.description.contains(filter.as_str())
{
continue;
}
eprintln!(" query example: {:?}", row.description);
let query = Query::new(&language, row.pattern).unwrap();
@ -4804,10 +5315,10 @@ fn test_query_is_pattern_non_local() {
eprintln!();
for row in &rows {
if let Some(filter) = EXAMPLE_FILTER.as_ref() {
if !row.description.contains(filter.as_str()) {
continue;
}
if let Some(filter) = EXAMPLE_FILTER.as_ref()
&& !row.description.contains(filter.as_str())
{
continue;
}
eprintln!(" query example: {:?}", row.description);
let query = Query::new(&row.language, row.pattern).unwrap();
@ -5034,10 +5545,10 @@ fn test_capture_quantifiers() {
eprintln!();
for row in rows {
if let Some(filter) = EXAMPLE_FILTER.as_ref() {
if !row.description.contains(filter.as_str()) {
continue;
}
if let Some(filter) = EXAMPLE_FILTER.as_ref()
&& !row.description.contains(filter.as_str())
{
continue;
}
eprintln!(" query example: {:?}", row.description);
let query = Query::new(&row.language, row.pattern).unwrap();
@ -5322,10 +5833,10 @@ fn test_consecutive_zero_or_modifiers() {
let mut len_1 = false;
while let Some(m) = matches.next() {
if m.captures.len() == 3 {
if m.captures().len() == 3 {
len_3 = true;
}
if m.captures.len() == 1 {
if m.captures().len() == 1 {
len_1 = true;
}
}
@ -5387,7 +5898,7 @@ fn test_query_max_start_depth_more() {
let query = Query::new(&language, "(compound_statement) @capture").unwrap();
let mut matches = cursor.matches(&query, tree.root_node(), source.as_bytes());
let node = matches.next().unwrap().captures[0].node;
let node = matches.next().unwrap().captures()[0].node;
assert_eq!(node.kind(), "compound_statement");
for row in rows {
@ -5563,21 +6074,21 @@ fn test_query_execution_with_timeout() {
let tree = parser.parse(&source_code, None).unwrap();
let query = Query::new(&language, "(function_declaration) @function").unwrap();
let mut cursor = QueryCursor::new();
let start_time = std::time::Instant::now();
let mut progress_callback = |_: &QueryCursorState| {
if start_time.elapsed().as_micros() > 1000 {
ControlFlow::Break(())
} else {
ControlFlow::Continue(())
}
};
let mut cursor = QueryCursor::new();
let matches = cursor
.matches_with_options(
&query,
tree.root_node(),
source_code.as_bytes(),
QueryCursorOptions::new().progress_callback(&mut |_| {
if start_time.elapsed().as_micros() > 1000 {
ControlFlow::Break(())
} else {
ControlFlow::Continue(())
}
}),
QueryCursorOptions::new().progress_callback(&mut progress_callback),
)
.count();
assert!(matches < 1000);
@ -5588,13 +6099,43 @@ fn test_query_execution_with_timeout() {
assert_eq!(matches, 1000);
}
#[test]
fn test_query_progress_callback_lives_as_long_as_matches() {
let language = get_language("javascript");
let mut parser = Parser::new();
parser.set_language(&language).unwrap();
let source_code = "function foo() {}\n".repeat(1000);
let tree = parser.parse(&source_code, None).unwrap();
let query = Query::new(&language, "(function_declaration) @function").unwrap();
let mut cursor = QueryCursor::new();
let mut callback_was_called = false;
let mut progress_callback = |_: &QueryCursorState| {
callback_was_called = true;
ControlFlow::Continue(())
};
let matches = cursor.matches_with_options(
&query,
tree.root_node(),
source_code.as_bytes(),
QueryCursorOptions::new().progress_callback(&mut progress_callback),
);
assert_eq!(matches.count(), 1000);
assert!(callback_was_called);
}
#[test]
fn test_query_execution_with_points_causing_underflow() {
let language = get_language("rust");
let mut parser = Parser::new();
parser.set_language(&language).unwrap();
#[allow(clippy::literal_string_with_formatting_args)]
#[expect(
clippy::literal_string_with_formatting_args,
reason = "raw string contains format syntax as literal code"
)]
let code = r#"fn main() {
println!("{:?}", foo());
}"#;
@ -5996,3 +6537,64 @@ export default grammar({
assert_query_matches(&language, &query, source, &[(0, vec![("tuple", "()")])]);
}
#[test]
fn test_last_child_anchor_looks_past_hidden_repeat() {
let language = get_test_fixture_language("last_child_anchor_past_hidden_repeat");
let source = "T a.b.c\nL a.b.c\nN a!.b!.c\n";
let query = Query::new(
&language,
"
(trailing_sep (name) @last .)
(leading_sep (name) @last .)
(trailing_named (name) @last .)
",
)
.unwrap();
assert_query_matches(
&language,
&query,
source,
&[
(0, vec![("last", "c")]),
(1, vec![("last", "c")]),
(2, vec![("last", "c")]),
],
);
let query = Query::new(
&language,
"
(trailing_sep . (name) @first)
(trailing_sep (name) @a . (name) @b)
",
)
.unwrap();
assert_query_matches(
&language,
&query,
source,
&[
(0, vec![("first", "a")]),
(1, vec![("a", "a"), ("b", "b")]),
(1, vec![("a", "b"), ("b", "c")]),
],
);
}
#[test]
fn test_last_child_anchor_looks_past_hidden_node() {
allocations::record(|| {
let language = get_language("c");
let query = Query::new(&language, "(translation_unit (_) @last .)").unwrap();
let source = "enum E { A };\nint x;\nint y;\n";
assert_query_matches(&language, &query, source, &[(0, vec![("last", "int y;")])]);
});
}

View file

@ -1,11 +1,11 @@
use std::{
ffi::{CStr, CString},
fs, ptr, slice, str,
fs, ptr, slice,
sync::atomic::{AtomicUsize, Ordering},
};
use tree_sitter::Point;
use tree_sitter_tags::{c_lib as c, Error, TagsConfiguration, TagsContext};
use tree_sitter_tags::{Error, TagsConfiguration, TagsContext, c_lib as c};
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::{parse_position_comments, Assertion, Utf8Point},
test_highlight::get_highlight_positions,
query_testing::{Assertion, Utf8Point, parse_position_comments},
test_highlight::{Failure, get_highlight_positions, iterate_assertions},
};
#[test]
@ -68,3 +68,195 @@ 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::{parse_position_comments, Assertion, Utf8Point},
query_testing::{Assertion, Utf8Point, parse_position_comments},
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,9 +126,11 @@ fn test_text_provider_callback_with_str_slice() {
check_parsing(text, |_node: Node<'_>| iter::once(text));
check_parsing_callback(
&mut |offset, _point| {
(offset < text.len())
.then_some(text.as_bytes())
.unwrap_or_default()
if offset < text.len() {
text.as_bytes()
} else {
Default::default()
}
},
|_node: Node<'_>| iter::once(text),
);
@ -140,9 +142,11 @@ fn test_text_provider_callback_with_owned_string_slice() {
check_parsing_callback(
&mut |offset, _point| {
(offset < text.len())
.then_some(text.as_bytes())
.unwrap_or_default()
if offset < text.len() {
text.as_bytes()
} else {
Default::default()
}
},
|_node: Node<'_>| {
let slice: String = text.to_owned();
@ -157,9 +161,11 @@ fn test_text_provider_callback_with_owned_bytes_vec_slice() {
check_parsing_callback(
&mut |offset, _point| {
(offset < text.len())
.then_some(text.as_bytes())
.unwrap_or_default()
if offset < text.len() {
text.as_bytes()
} else {
Default::default()
}
},
|_node: Node<'_>| {
let slice = text.to_owned().into_bytes();
@ -174,9 +180,11 @@ fn test_text_provider_callback_with_owned_arc_of_bytes_slice() {
check_parsing_callback(
&mut |offset, _point| {
(offset < text.len())
.then_some(text.as_bytes())
.unwrap_or_default()
if offset < text.len() {
text.as_bytes()
} else {
Default::default()
}
},
|_node: Node<'_>| {
let slice: Arc<[u8]> = text.to_owned().into_bytes().into();

View file

@ -1,5 +1,3 @@
use std::str;
use tree_sitter::{InputEdit, Parser, Point, Range, Tree};
use super::helpers::fixtures::get_language;
@ -795,3 +793,44 @@ 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::{get_test_fixture_language_wasm, ENGINE, WASM_DIR},
fixtures::{ENGINE, WASM_DIR, get_test_fixture_language_wasm},
};
#[test]
@ -73,7 +73,10 @@ 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)))"
);
});
}
@ -87,7 +90,10 @@ 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))))"
);
});
}
@ -101,7 +107,10 @@ 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))))"
);
});
}
@ -258,12 +267,18 @@ 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)))"
);
});
}
@ -299,6 +314,55 @@ 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(|| {
@ -323,3 +387,18 @@ 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,6 +5,7 @@ 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::{
atomic::{AtomicUsize, Ordering},
Arc,
atomic::{AtomicUsize, Ordering},
},
};
use anyhow::{anyhow, Context, Result};
use anyhow::{Context, Result, anyhow};
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, quiet: bool) -> Result<()> {
let session = LogSession::new(path, quiet)?;
pub fn print_tree_graph(tree: &Tree, path: &str, open_log: bool) -> Result<()> {
let session = LogSession::new(path, open_log)?;
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>(&self, path: &PathBuf, update_fn: F) -> Result<(), UpdateError>
fn update_file_with<F>(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,8 +292,11 @@ 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| {
@ -317,7 +320,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(
@ -336,7 +339,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| {
@ -360,7 +363,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::{anyhow, Context, Result};
use anyhow::{Context, Result, anyhow};
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,14 +83,17 @@ 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(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);
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);
}
}
}
}

View file

@ -5,7 +5,6 @@ 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,14 +28,24 @@ pub enum ConfigError {
#[derive(Debug, Error)]
pub struct IoError {
pub error: std::io::Error,
pub path: Option<String>,
pub path: Option<PathBuf>,
}
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(|p| p.to_string_lossy().to_string()),
path: path.map(Path::to_path_buf),
}
}
}
@ -44,7 +54,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})")?;
write!(f, " ({})", path.display())?;
}
Ok(())
}

View file

@ -5,7 +5,6 @@ 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"
@ -20,30 +19,24 @@ 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.9.4"
bitflags = "2.11.1"
dunce = "1.0.5"
hashbrown.workspace = true
indexmap.workspace = true
indoc.workspace = true
log.workspace = true
pathdiff = { version = "0.2.3", optional = true }
pathdiff = { optional = true, version = "0.2.3" }
regex.workspace = true
regex-syntax.workspace = true
rquickjs = { version = "0.10.0", optional = true, features = [
"bindgen",
"loader",
"macro",
"phf",
] }
rquickjs = { features = [ "bindgen", "loader", "macro", "phf" ], optional = true, version = "0.13" }
rustc-hash.workspace = true
semver = { workspace = true, optional = true }
semver = { optional = true, workspace = 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