Compare commits

...
Sign in to create a new pull request.

172 commits

Author SHA1 Message Date
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
Christian Clason
cd4b6e2ef9 0.26.3 2025-12-13 13:41:03 +01:00
Christian Clason
8caecbc13f build(deps): cargo update 2025-12-13 12:58:59 +01:00
ObserverOfTime
1b654ae35d ci(release): use node 24 2025-12-13 06:28:18 -05:00
Marcono1234
3bd44afcaa docs(cli): fix wrong file path for Java bindings test
The test is currently generated in the default (= unnamed) package.
2025-12-10 20:54:07 +02:00
Will Lillis
8b8199775f 0.26.x
Also bump the tree-sitter-language crate to 0.1.6
2025-12-09 17:19:25 -05:00
dependabot[bot]
744e556f7e build(deps): bump esbuild
Bumps the npm group with 1 update in the /lib/binding_web directory: [esbuild](https://github.com/evanw/esbuild).


Updates `esbuild` from 0.27.0 to 0.27.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.0...v0.27.1)

---
updated-dependencies:
- dependency-name: esbuild
  dependency-version: 0.27.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-08 23:43:47 +01:00
Will Lillis
8a3dcc6155 release 0.26.1 2025-12-08 17:05:03 -05:00
dependabot[bot]
b0afbf3762 build(deps): bump wasmparser from 0.242.0 to 0.243.0 in the cargo group
Bumps the cargo group with 1 update: [wasmparser](https://github.com/bytecodealliance/wasm-tools).


Updates `wasmparser` from 0.242.0 to 0.243.0
- [Release notes](https://github.com/bytecodealliance/wasm-tools/releases)
- [Commits](https://github.com/bytecodealliance/wasm-tools/commits)

---
updated-dependencies:
- dependency-name: wasmparser
  dependency-version: 0.243.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-08 22:38:09 +01:00
Will Lillis
974be3bb30 fix(rust): specify workspace dependency of tree-sitter-language crate
as "0.1"

If a rust project depends on both the tree-sitter lib bindings and the
language crate, cargo needs to be able to resolve a common version of
the tree-sitter-language crate. Specifying exactly "0.1.5" for the lib
bindings is overly restrictive, and could lead to future headaches. By
specifying "0.1", any "0.1.x" version should be available to resolve to.
2025-12-08 16:00:57 -05:00
ObserverOfTime
d861e2bcd9 docs(cli): list Java & Zig binding files 2025-12-08 15:47:15 -05:00
ObserverOfTime
b9c2d1dc89 feat(bindings): add Java bindings 2025-12-08 15:47:15 -05:00
ObserverOfTime
8ca17d1bb1 ci(release): enable trusted publishing & attestations 2025-12-08 15:38:21 -05:00
ObserverOfTime
3182efeccc feat(bindings): add byproducts to cmake 2025-12-08 04:35:09 -05:00
Will Lillis
bec7c3272b fix(loader)!: correct arguments passed to select_language 2025-12-07 17:11:28 -05:00
dependabot[bot]
e6bfed33ee 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 |
| --- | --- | --- |
| [@eslint/js](https://github.com/eslint/eslint/tree/HEAD/packages/js) | `9.36.0` | `9.39.1` |
| [@types/emscripten](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/emscripten) | `1.41.2` | `1.41.5` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `24.5.2` | `24.10.1` |
| [esbuild](https://github.com/evanw/esbuild) | `0.25.10` | `0.27.0` |
| [eslint](https://github.com/eslint/eslint) | `9.36.0` | `9.39.1` |
| [tsx](https://github.com/privatenumber/tsx) | `4.20.5` | `4.21.0` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.44.1` | `8.48.1` |



Updates `@eslint/js` from 9.36.0 to 9.39.1
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/commits/v9.39.1/packages/js)

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

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

Updates `esbuild` from 0.25.10 to 0.27.0
- [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.25.10...v0.27.0)

Updates `eslint` from 9.36.0 to 9.39.1
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v9.36.0...v9.39.1)

Updates `tsx` from 4.20.5 to 4.21.0
- [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.20.5...v4.21.0)

Updates `typescript-eslint` from 8.44.1 to 8.48.1
- [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.48.1/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: "@eslint/js"
  dependency-version: 9.39.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm
- dependency-name: "@types/emscripten"
  dependency-version: 1.41.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm
- dependency-name: "@types/node"
  dependency-version: 24.10.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm
- dependency-name: esbuild
  dependency-version: 0.27.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm
- dependency-name: eslint
  dependency-version: 9.39.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm
- dependency-name: tsx
  dependency-version: 4.21.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm
- dependency-name: typescript-eslint
  dependency-version: 8.48.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-06 13:05:02 +01:00
Christian Clason
053b264502 build(deps): cargo update 2025-12-06 12:53:10 +01:00
dependabot[bot]
a8f25fa441 build(deps): bump glob from 10.4.5 to 10.5.0 in /lib/binding_web
Bumps [glob](https://github.com/isaacs/node-glob) from 10.4.5 to 10.5.0.
- [Changelog](https://github.com/isaacs/node-glob/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/node-glob/compare/v10.4.5...v10.5.0)

---
updated-dependencies:
- dependency-name: glob
  dependency-version: 10.5.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-06 12:21:22 +01:00
dependabot[bot]
f450ce4f6e build(deps): bump vite from 7.1.5 to 7.1.11 in /lib/binding_web
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.1.5 to 7.1.11.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.1.11/packages/vite)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-05 23:59:37 -05:00
dependabot[bot]
3ff8edf9e8 build(deps): bump js-yaml from 4.1.0 to 4.1.1 in /crates/cli/eslint
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.0 to 4.1.1.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.1.0...4.1.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-05 22:25:23 -05:00
dependabot[bot]
6b6040961c build(deps): bump js-yaml from 4.1.0 to 4.1.1 in /lib/binding_web
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.0 to 4.1.1.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.1.0...4.1.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-05 22:24:47 -05:00
Will Lillis
888f57657d fix(cli): improve error reporting for invalid range arguments to query
command
2025-12-03 18:06:16 -05:00
Max Brunsfeld
be8fe690d8 Clean up node range tracking in query_cursor__advance 2025-12-03 18:06:16 -05:00
Piotr Osiewicz
c0b1710f8a Add containing range APIs to query cursor
Co-authored-by: Kirill Bulatov <mail4score@gmail.com>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: dino <dinojoaocosta@gmail.com>
Co-authored-by: John Tur <john-tur@outlook.com>
Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
Co-authored-by: dino <dinojoaocosta@gmail.com>
Co-authored-by: Will Lillis <will.lillis24@gmail.com>
2025-12-03 18:06:16 -05:00
Piotr Osiewicz
7d3feeae9a cli: Do not validate UTF-8 boundaries when query results are not being
tested

Co-authored-by: Kirill Bulatov <mail4score@gmail.com>
Co-authored-by: dino <dinojoaocosta@gmail.com>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: John Tur <john-tur@outlook.com>
2025-12-03 18:06:16 -05:00
dependabot[bot]
3f85f65e3f 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 [wasmparser](https://github.com/bytecodealliance/wasm-tools).


Updates `cc` from 1.2.47 to 1.2.48
- [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.47...cc-v1.2.48)

Updates `wasmparser` from 0.241.2 to 0.242.0
- [Release notes](https://github.com/bytecodealliance/wasm-tools/releases)
- [Commits](https://github.com/bytecodealliance/wasm-tools/commits)

---
updated-dependencies:
- dependency-name: cc
  dependency-version: 1.2.48
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: wasmparser
  dependency-version: 0.242.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-02 08:10:34 +01:00
WillLillis
df8b62fc50 feat(xtask): bring wasi-sdk treatment up to par with the loader
The loader package's `ensure_wasi_sdk_exists` private method checks for
the wasi-sdk, fetching it if it can't be found. This logic was
re-implemented in xtask for `build-wasm-stdlib`, but without the
fetching functionality. We can have nice things in xtask too! Rather
than make this function a public member of `tree-sitter-loader`, we
just re-implement and leave a nice comment asking people to keep the
two in sync.
2025-11-27 16:06:25 -05:00
ObserverOfTime
14b4708018 fix(loader): write Wasm lib directly to lib dir
Problem: `fs::rename` fails if the parser directory and the Tree-sitter
library directory are on different file systems.

Solution: Write the library file directly to the final directory.
2025-11-25 17:23:44 +01:00
WillLillis
dcef0cc0ee fix(cli): correct query match limit warning condition 2025-11-25 17:23:19 +01:00
WillLillis
c1a0f48781 fix(cli): return error if --wasm flag is passed when the wasm feature
is disabled

This applies to the `parse` and `test` commands, but not `build` as it
doesn't require the wasm feature. Also, hide the `--wasm` options if
from the `--help` output if the feature is disabled.
2025-11-25 17:23:19 +01:00
Antonin Delpeuch
f6d17fdb04 fix(node): bump tree-sitter dep to 0.25 in bindings
Sets the dependency `tree-sitter` to version 0.25 in
the NodeJS bindings generated by default, so that
`npm run test` passes.
2025-11-25 03:32:31 -05:00
Riley Bruins
829733a35e fix(query): prevent infinite loop with + and ? quantifiers
**Problem:** A query with a `?` quantifier followed by a `+` quantifier
would hang at 100% CPU usage while iterating through a tree, regardless
of the source content.

**Solution:** Collect all quantifiers in one step, and then add the
required repeat/optional step logic *after* we have determined the
composite quantifier we need to use for the current step.
2025-11-25 03:21:13 -05:00
Christian Clason
d64b863030 build(deps): bump wasi-sdk to v29 2025-11-25 07:41:04 +01:00
skewb1k
882aa867eb docs: remove manual bindings update steps for scanner
Since 66dab20462, bindings automatically
detect external scanner, making the instructions for manual updating
outdated. Avoids confusion about missing commented lines in Rust
bindings.
2025-11-24 23:00:40 +01:00
dependabot[bot]
de92a9b4c9 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 [indexmap](https://github.com/indexmap-rs/indexmap).


Updates `cc` from 1.2.46 to 1.2.47
- [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.46...cc-v1.2.47)

Updates `clap` from 4.5.51 to 4.5.53
- [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.51...clap_complete-v4.5.53)

Updates `clap_complete` from 4.5.60 to 4.5.61
- [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.60...clap_complete-v4.5.61)

Updates `indexmap` from 2.12.0 to 2.12.1
- [Changelog](https://github.com/indexmap-rs/indexmap/blob/main/RELEASES.md)
- [Commits](https://github.com/indexmap-rs/indexmap/compare/2.12.0...2.12.1)

---
updated-dependencies:
- dependency-name: cc
  dependency-version: 1.2.47
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: clap
  dependency-version: 4.5.53
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: clap_complete
  dependency-version: 4.5.61
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: indexmap
  dependency-version: 2.12.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-24 22:42:26 +01:00
dependabot[bot]
5880df47e2 ci: bump actions/checkout from 5 to 6 in the actions group
Bumps the actions group with 1 update: [actions/checkout](https://github.com/actions/checkout).


Updates `actions/checkout` from 5 to 6
- [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/v5...v6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-24 22:41:27 +01:00
Will Lillis
e92a7803eb fix(docs): final updates before 0.26.1
- Indicate where xtask looks for wasi-sdk
- Indicate where `build --wasm` looks for and downloads wasi-sdk binary
  to
- Mark native runtime as experimental, describe limitations
- Note ABI 13 support limitations
- Mention that `test --wasm` and `parse --wasm` require
  `--features=wasm` build
2025-11-24 15:18:12 +01:00
Will Lillis
0d656de98b feat(cli): update zig bindings with version command 2025-11-21 19:40:14 -05:00
Amaan Qureshi
b095968dff refactor(cli): clean up version updating code
This commit adds proper error types when updating the version across
files
2025-11-21 19:40:14 -05:00
Will Lillis
d592b16ac0 fix(docs): list dependencies on external tooling for version command 2025-11-21 19:40:14 -05:00
Will Lillis
320c0865e9 feat(cli): don't bail after first version update fails 2025-11-21 19:40:14 -05:00
ObserverOfTime
60635e0729 fix(generate): add node_modules to quickjs resolver 2025-11-21 19:39:53 -05:00
Antonin Delpeuch
120f74723e docs: fix typo in the page about ABI version
Of course I only catch that once they are already published…
2025-11-20 09:58:07 +01:00
Antonin Delpeuch
02508d5570 Apply suggestions from code review
Co-authored-by: Christian Clason <ch.clason+github@icloud.com>
2025-11-20 01:07:58 -05:00
Antonin Delpeuch
42e7e9c3e7 Integrate rewording suggestions 2025-11-20 01:07:58 -05:00
Antonin Delpeuch
55b9a25c84 docs: New page about ABI versions for parser users
Closes #374.

The statement about the intended backwards compatibility is purely
speculative and provided as a "straw man" to help reviewers come up with
a better description of the intended backwards compatibility.
2025-11-20 01:07:58 -05:00
Will Lillis
877782a8a4 fix(docs): update cli docs to reflect changes to various subcommand
arguments
2025-11-19 04:30:01 -05:00
Antonin Delpeuch
0e1f715ef1 Move PathsJSON method, reformat 2025-11-19 03:57:13 -05:00
Antonin Delpeuch
f3012a999d feat(bindings): expose the queries dynamically
Available in the Rust, Python, and Node bindings

Co-authored-by: ObserverOfTime <chronobserver@disroot.org>
2025-11-19 03:57:13 -05:00
dependabot[bot]
3072d35ed5 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 [wasmparser](https://github.com/bytecodealliance/wasm-tools).


Updates `cc` from 1.2.45 to 1.2.46
- [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.45...cc-v1.2.46)

Updates `wasmparser` from 0.240.0 to 0.241.2
- [Release notes](https://github.com/bytecodealliance/wasm-tools/releases)
- [Commits](https://github.com/bytecodealliance/wasm-tools/commits)

---
updated-dependencies:
- dependency-name: cc
  dependency-version: 1.2.46
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: wasmparser
  dependency-version: 0.241.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-17 22:29:19 +01:00
Valeriy Kosikhin
57e3a7b2ca fix(loader): set correct runtime host for cc while cross-compiling
Pass the BUILD_TARGET variable from the build environment as 'host' for
the cc crate. Otherwise, when cross-compiled, cc will keep looking for a
cross-compiler instead of the native one on the target system.

Signed-off-by: Valeriy Kosikhin <vkosikhin@gmail.com>
2025-11-17 18:19:34 +01:00
Christian Clason
0df2916920 bulild(deps): cargo update 2025-11-14 11:28:00 +01:00
WillLillis
61c21aa408 refactor(generate)!: include path when available in IO errors 2025-11-14 11:28:00 +01:00
WillLillis
7eb23d9f3c refactor(config)!: transition from anyhow to thiserror 2025-11-14 11:28:00 +01:00
WillLillis
db2d221ae9 fix(generate): remove leftover imports of anyhow 2025-11-14 11:28:00 +01:00
WillLillis
67cb3cb881 refactor(loader)!: transition from anyhow to thiserror 2025-11-14 11:28:00 +01:00
Marcono1234
12a31536e1 fix(docs): don't show mdbook help popup when using query editor 2025-11-12 23:43:03 -05:00
Will Lillis
7657cc9d35 fix(dsl): add ReservedRule to Rule type definition 2025-11-12 07:27:05 +01:00
dependabot[bot]
13ff3935ac build(deps): bump the cargo group with 3 updates
Bumps the cargo group with 3 updates: [cc](https://github.com/rust-lang/cc-rs), [libloading](https://github.com/nagisa/rust_libloading) and [schemars](https://github.com/GREsau/schemars).


Updates `cc` from 1.2.44 to 1.2.45
- [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.44...cc-v1.2.45)

Updates `libloading` from 0.8.9 to 0.9.0
- [Commits](https://github.com/nagisa/rust_libloading/compare/0.8.9...0.9.0)

Updates `schemars` from 1.0.4 to 1.0.5
- [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.0.4...v1.0.5)

---
updated-dependencies:
- dependency-name: cc
  dependency-version: 1.2.45
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: libloading
  dependency-version: 0.9.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: cargo
- dependency-name: schemars
  dependency-version: 1.0.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-10 23:04:54 +01:00
Will Lillis
361287fb56 fix(cli)!: deprecate --build flag for generate command 2025-11-07 08:51:24 +01:00
dependabot[bot]
13d4db8bb4 build(deps): bump the cargo group across 1 directory with 6 updates
Bumps the cargo group with 6 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [cc](https://github.com/rust-lang/cc-rs) | `1.2.43` | `1.2.44` |
| [clap](https://github.com/clap-rs/clap) | `4.5.50` | `4.5.51` |
| [clap_complete](https://github.com/clap-rs/clap) | `4.5.59` | `4.5.60` |
| [clap_complete_nushell](https://github.com/clap-rs/clap) | `4.5.9` | `4.5.10` |
| [etcetera](https://github.com/lunacookies/etcetera) | `0.10.0` | `0.11.0` |
| [wasmparser](https://github.com/bytecodealliance/wasm-tools) | `0.229.0` | `0.240.0` |



Updates `cc` from 1.2.43 to 1.2.44
- [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.43...cc-v1.2.44)

Updates `clap` from 4.5.50 to 4.5.51
- [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.50...clap_complete-v4.5.51)

Updates `clap_complete` from 4.5.59 to 4.5.60
- [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.59...clap_complete-v4.5.60)

Updates `clap_complete_nushell` from 4.5.9 to 4.5.10
- [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.5.9...clap_complete_nushell-v4.5.10)

Updates `etcetera` from 0.10.0 to 0.11.0
- [Release notes](https://github.com/lunacookies/etcetera/releases)
- [Commits](https://github.com/lunacookies/etcetera/compare/v0.10.0...v0.11.0)

Updates `wasmparser` from 0.229.0 to 0.240.0
- [Release notes](https://github.com/bytecodealliance/wasm-tools/releases)
- [Commits](https://github.com/bytecodealliance/wasm-tools/commits)

---
updated-dependencies:
- dependency-name: cc
  dependency-version: 1.2.44
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: clap
  dependency-version: 4.5.51
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: clap_complete
  dependency-version: 4.5.60
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: clap_complete_nushell
  dependency-version: 4.5.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: etcetera
  dependency-version: 0.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: cargo
- dependency-name: wasmparser
  dependency-version: 0.240.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-04 16:08:54 +01:00
Will Lillis
419a5a7305 fix(generate): don't short-circuit within extend_sorted 2025-11-03 01:22:29 -05:00
Will Lillis
c7b5f89392 feat(xtask): generate JSON schema for cli TestSummary 2025-11-02 21:08:55 -05:00
Will Lillis
d546e28abf fix(cli): mark report_states_for_rule and json/json_summary flags
for `generate` command as conflicting
2025-11-02 21:08:55 -05:00
WillLillis
86e2fd2337 fix(cli): correct behavior of parse --stat and --json-summary flags 2025-11-02 21:08:55 -05:00
WillLillis
ff255a2354 test: add coverage for new test aggregation method 2025-11-02 21:08:55 -05:00
WillLillis
fe67521b3d refactor(cli)!: deprecate json flags in favor of json-summary 2025-11-02 21:08:55 -05:00
WillLillis
f02d7e7e33 feat(test): display test results in JSON format 2025-11-02 21:08:55 -05:00
WillLillis
6a8676f335 refactor(test): generalize printing of test diff keys and diffs 2025-11-02 21:08:55 -05:00
WillLillis
944386d25f refactor(test): clean up test filtering logic
Also, only update the expected output of a case when it is skipped if
the `update` flag has been passed
2025-11-02 21:08:55 -05:00
Will Lillis
ef03a3f8fe fix(ci): correct mdbook release url 2025-11-02 18:37:09 -05:00
Christian Clason
18a5243933 ci(docs): pin mdbook to latest release 2025-11-02 18:07:47 -05:00
Christian Clason
8444cc3deb fix(docs): remove multilingual config field
Problem: "deploy docs" always pulls in the `latest` release of `mdbook`,
which now is a v0.5.0 prerelease with breaking changes -- including
removing an (apparently unused) `multilingual` config field in the TOML
that is now an error (another breaking change).

Solution: Delete the line. Add `workflow_dispatch` to the docs workflow
in case follow-up changes are needed; see
https://github.com/rust-lang/mdBook/blob/master/CHANGELOG.md#05-migration-guide
2025-11-02 18:07:47 -05:00
Will Lillis
097c2d4f05 fix(cli): remove --emit=lib generate option
This also replaces the `--emit` option with an `--no-parser` flag. The
default value is false, meaning a parser is still generated by default.
2025-11-02 10:26:28 +01:00
WillLillis
b8f52210f9 perf: reduce needless allocations 2025-10-30 18:24:42 +01:00
WillLillis
ecc787e221 fix(test): correct language typo in test name 2025-10-30 18:24:42 +01:00
Christian Clason
6188010f53 build(deps): bump rquickjs to v0.10.0 2025-10-29 18:30:25 -04:00
Christian Clason
70cde4a110 ci(dependabot): only update patch releases for cargo 2025-10-29 18:30:25 -04:00
Christian Clason
77363a65c2 build(deps): cargo update 2025-10-29 18:30:25 -04:00
dependabot[bot]
605e580063 ci: bump the actions group across 1 directory with 3 updates
Bumps the actions group with 3 updates in the / directory: [actions/upload-artifact](https://github.com/actions/upload-artifact), [actions/download-artifact](https://github.com/actions/download-artifact) and [actions/setup-node](https://github.com/actions/setup-node).


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

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

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

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: actions/download-artifact
  dependency-version: '6'
  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>
2025-10-28 09:23:50 +01:00
WillLillis
a2f2b16acb fix(xtask): require version argument for bump-version command 2025-10-24 18:13:59 -04:00
Will Lillis
87d778a1c6 fix(rust): apply Self usage in struct definition lint 2025-10-24 17:50:28 -04:00
Will Lillis
e344837e35 fix(rust): minor cleanup in generate code 2025-10-24 17:50:28 -04:00
ObserverOfTime
bdee2c2dd3 ci: use macos-15-intel runner
The macos-13 runner will soon be removed.
2025-10-24 10:14:52 +02:00
dependabot[bot]
da5926d6f5 build(deps): bump the cargo group across 1 directory with 4 updates
Bumps the cargo group with 4 updates in the / directory: [anstyle](https://github.com/rust-cli/anstyle), [cc](https://github.com/rust-lang/cc-rs), [thiserror](https://github.com/dtolnay/thiserror) and [widestring](https://github.com/VoidStarKat/widestring-rs).


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

Updates `cc` from 1.2.39 to 1.2.41
- [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.39...cc-v1.2.41)

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

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

---
updated-dependencies:
- dependency-name: anstyle
  dependency-version: 1.0.13
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: cc
  dependency-version: 1.2.41
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: thiserror
  dependency-version: 2.0.17
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: widestring
  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>
2025-10-14 11:00:03 +02:00
Will Lillis
b3bc7701cd refactor(generate): make AliasMap use BTreeMap over HashMap 2025-10-12 15:56:30 -04:00
Will Lillis
262f1782cc fix(generate): ensure deterministic iteration order for symbol aliases
while constructing node-types.json
2025-10-12 15:56:30 -04:00
WillLillis
00d172bf9f fix(generate): correct display of precedence for
`--report-states-for-rule`
2025-10-12 15:56:12 -04:00
Will Lillis
ae54350c76 fix(generate): Add missing fields to NodeInfoJson sorting
This ensures a deterministic ordering for node-types.json
2025-10-11 14:25:52 -04:00
Will Lillis
3355825a68 fix(cli): don't load languages for build command 2025-10-07 17:21:09 -04:00
ObserverOfTime
7d0e029e37 chore: add schema for node-types.json 2025-10-05 09:57:23 +03:00
Mihai-Daniel Potirniche
0f5ccc4aba Fix typo 2025-10-04 12:12:39 -04:00
ObserverOfTime
0cf6e7c507 fix(cli): prevent crash when parsing stdin
When we are parsing stdin via a pipe or heredoc, the source count is 0
(unsigned) so the XML output crashes while trying to subtract from it.
2025-10-03 19:24:58 +03:00
dependabot[bot]
1dc4804b6e build(deps): bump the cargo group with 2 updates
Bumps the cargo group with 2 updates: [memchr](https://github.com/BurntSushi/memchr) and [regex](https://github.com/rust-lang/regex).


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

Updates `regex` from 1.11.2 to 1.11.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.11.2...1.11.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-01 02:57:21 -04:00
Amaan Qureshi
c5b22a1dc6 ci: split cross compilation and emscripten tag read 2025-10-01 02:57:04 -04:00
Amaan Qureshi
92efd26380 fix(loader): allow parallel compilation on windows 2025-10-01 02:57:04 -04:00
Amaan Qureshi
24c8feba3e fix(bindings): fix root detection on windows 2025-09-28 08:40:02 -04:00
Amaan Qureshi
122493b717 style(cli): appease clippy 2025-09-28 05:54:28 -04:00
Amaan Qureshi
4edcca9850 style(loader): appease clippy 2025-09-28 05:54:28 -04:00
dependabot[bot]
be0c44f871 build(deps): bump the cargo group with 7 updates
Bumps the cargo group with 7 updates:

| Package | From | To |
| --- | --- | --- |
| [anyhow](https://github.com/dtolnay/anyhow) | `1.0.99` | `1.0.100` |
| [cc](https://github.com/rust-lang/cc-rs) | `1.2.37` | `1.2.39` |
| [clap](https://github.com/clap-rs/clap) | `4.5.47` | `4.5.48` |
| [clap_complete](https://github.com/clap-rs/clap) | `4.5.57` | `4.5.58` |
| [indexmap](https://github.com/indexmap-rs/indexmap) | `2.11.1` | `2.11.4` |
| [libloading](https://github.com/nagisa/rust_libloading) | `0.8.8` | `0.8.9` |
| [tempfile](https://github.com/Stebalien/tempfile) | `3.22.0` | `3.23.0` |


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

Updates `cc` from 1.2.37 to 1.2.39
- [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.37...cc-v1.2.39)

Updates `clap` from 4.5.47 to 4.5.48
- [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.47...clap_complete-v4.5.48)

Updates `clap_complete` 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 `indexmap` from 2.11.1 to 2.11.4
- [Changelog](https://github.com/indexmap-rs/indexmap/blob/main/RELEASES.md)
- [Commits](https://github.com/indexmap-rs/indexmap/compare/2.11.1...2.11.4)

Updates `libloading` from 0.8.8 to 0.8.9
- [Commits](https://github.com/nagisa/rust_libloading/compare/0.8.8...0.8.9)

Updates `tempfile` from 3.22.0 to 3.23.0
- [Changelog](https://github.com/Stebalien/tempfile/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Stebalien/tempfile/compare/v3.22.0...v3.23.0)

---
updated-dependencies:
- dependency-name: anyhow
  dependency-version: 1.0.100
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: cc
  dependency-version: 1.2.39
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: clap
  dependency-version: 4.5.48
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: clap_complete
  dependency-version: 4.5.58
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: indexmap
  dependency-version: 2.11.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: libloading
  dependency-version: 0.8.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo
- dependency-name: tempfile
  dependency-version: 3.23.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-28 05:54:28 -04:00
ObserverOfTime
35b1356e96 ci(dependabot): update package.json as well 2025-09-28 04:15:36 -04:00
ObserverOfTime
443acf080a ci(dependabot): enable cooldown period
This setting will delay package updates by 3 days which generally
should be enough time for supply chain attacks to be discovered
2025-09-28 03:58:23 -04:00
Amaan Qureshi
00e394f0f1 feat(lib)!: disallow whitespace in supertype syntax 2025-09-28 00:56:30 -04:00
Amaan Qureshi
341665824c fix(lib): validate subtypes in supertype queries 2025-09-28 00:56:30 -04:00
Amaan Qureshi
bd02be25d5 fix(lib): allow anonymous nodes in the supertype query syntax 2025-09-28 00:56:30 -04:00
WillLillis
12a6400c63 fix(test): trim trailing carriage return unconditionally in test
contents
2025-09-27 19:28:03 -04:00
ObserverOfTime
d86e1b4f5e feat(bindings): generate zig fingerprint 2025-09-26 18:02:35 -04:00
WillLillis
422866a437 fix(docs): update more broken links 2025-09-26 16:10:29 -05:00
Amaan Qureshi
5f7806f99e feat: add option to disable parse state optimizations 2025-09-26 02:40:53 -04:00
WillLillis
a9bce7c18a fix(generate): return error when generated grammar's state count exceeds
the maximum allowed value.

Co-authored-by: Amaan Qureshi <git@amaanq.com>
2025-09-25 22:29:04 -05:00
WillLillis
335bfabc60 feat(cli): include filenames in parsing xml output 2025-09-25 22:28:31 -05:00
Amaan Qureshi
e1b424c191 Revert "0.26.0" 2025-09-24 16:42:01 -04:00
Amaan Qureshi
ea9c318afb docs: update highlight crate link 2025-09-24 16:29:08 -04:00
ObserverOfTime
9d66dbc28f chore: remove CARGO_WORKSPACE_DIR var 2025-09-24 16:14:08 -04:00
Riley Bruins
8c22426223
feat(rust): add new_raw to create a raw, unchecked query pointer 2025-09-23 19:06:22 -04:00
Amaan Qureshi
90ee433c9b fix(lib): account for unreachable patterns with children
Co-authored-by: Will Lillis <will.lillis24@gmail.com>
2025-09-23 17:17:45 -04:00
Amaan Qureshi
f26bd44a43 flake: remove cross, add llvm-cov support 2025-09-23 01:19:14 -04:00
Amaan Qureshi
021d9c447d test: clean up async boundary test 2025-09-23 01:19:14 -04:00
Amaan Qureshi
ce56465197 test(rust): prefer asserts to panics 2025-09-23 01:19:14 -04:00
Amaan Qureshi
b0cdab85fe refactor(rust): avoid panics where possible 2025-09-23 01:19:14 -04:00
Amaan Qureshi
47c9256976 test: clean up parser hang test 2025-09-23 01:19:14 -04:00
dependabot[bot]
cf89840460 build(deps): bump the npm group across 1 directory with 6 updates
Bumps the npm group with 6 updates in the /lib/binding_web directory:

| Package | From | To |
| --- | --- | --- |
| [@eslint/js](https://github.com/eslint/eslint/tree/HEAD/packages/js) | `9.35.0` | `9.36.0` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `24.5.0` | `24.5.2` |
| [esbuild](https://github.com/evanw/esbuild) | `0.25.9` | `0.25.10` |
| [eslint](https://github.com/eslint/eslint) | `9.35.0` | `9.36.0` |
| [typescript](https://github.com/microsoft/TypeScript) | `5.8.3` | `5.9.2` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.44.0` | `8.44.1` |



Updates `@eslint/js` from 9.35.0 to 9.36.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/commits/v9.36.0/packages/js)

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

Updates `esbuild` from 0.25.9 to 0.25.10
- [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.25.9...v0.25.10)

Updates `eslint` from 9.35.0 to 9.36.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v9.35.0...v9.36.0)

Updates `typescript` from 5.8.3 to 5.9.2
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Changelog](https://github.com/microsoft/TypeScript/blob/main/azure-pipelines.release-publish.yml)
- [Commits](https://github.com/microsoft/TypeScript/compare/v5.8.3...v5.9.2)

Updates `typescript-eslint` from 8.44.0 to 8.44.1
- [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.44.1/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: "@eslint/js"
  dependency-version: 9.36.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm
- dependency-name: "@types/node"
  dependency-version: 24.5.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm
- dependency-name: esbuild
  dependency-version: 0.25.10
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm
- dependency-name: eslint
  dependency-version: 9.36.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm
- dependency-name: typescript
  dependency-version: 5.9.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm
- dependency-name: typescript-eslint
  dependency-version: 8.44.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-22 21:49:46 -04:00
Max Brunsfeld
e3294c3faf
build: bump tree-sitter-language to 0.1.5 2025-09-22 15:54:15 -04:00
Amaan Qureshi
95ab17e444 build: define _DARWIN_C_SOURCE 2025-09-22 19:50:43 +03:00
Max Brunsfeld
9b914885f1
Fix issues preventing releases from successfully publishing (#4867)
* Correct the path to the CLI npm package in release job

* Specify a version for tree-sitter-language

* Fix path to README in doc include
2025-09-22 09:24:30 -07:00
WillLillis
92678f0fc5 fix(rust): pass correct fd to C lib's ts_tree_print_dot_graph
Co-authored-by: Amaan Qureshi <git@amaanq.com>
2025-09-21 18:21:57 -04:00
ObserverOfTime
a1640e4fe4 chore: rebuild wasm stdlib 2025-09-21 16:28:08 -04:00
ObserverOfTime
1be51c2129 chore: upgrade emscripten to 4.0.15 2025-09-21 16:28:08 -04:00
Will Lillis
6214f95e7e
docs: correct new generate flag: "stage"->"emit" 2025-09-21 15:22:11 -04:00
164 changed files with 8013 additions and 4529 deletions

View file

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

View file

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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@v6
- name: Check directory changes
uses: actions/github-script@v8
with:
script: |
const scriptPath = `${process.env.GITHUB_WORKSPACE}/.github/scripts/wasm_stdlib.js`;
const script = require(scriptPath);
return script({ github, context, core });

View file

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

907
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1 +1 @@
4.0.12 4.0.15

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1 @@
29.0

View file

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

View file

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

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