mirror of
https://github.com/tree-sitter/tree-sitter.git
synced 2026-09-10 07:36:22 -04:00
Compare commits
454 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de98c6c970 | ||
|
|
8351896bea | ||
|
|
072f68c829 | ||
|
|
badf2882c6 | ||
|
|
9c2c5714cd | ||
|
|
f430449ab9 | ||
|
|
068db86ca9 | ||
|
|
ee0c20b1f5 | ||
|
|
702ba49a6a | ||
|
|
b9c5eb2b73 | ||
|
|
351bd71e52 | ||
|
|
c206ad1e6a | ||
|
|
51da0190f5 | ||
|
|
e5cbeb74eb | ||
|
|
ea9ddbe190 | ||
|
|
aa1b924997 | ||
|
|
1b64a459ff | ||
|
|
1e070eb28e | ||
|
|
91e875ad7b | ||
|
|
6070dbfefd | ||
|
|
f52e78cb9f | ||
|
|
49dbf70ebd | ||
|
|
9540ea3b10 | ||
|
|
3e7be91c46 | ||
|
|
f47170927d | ||
|
|
5ddf71b9f4 | ||
|
|
efa0dd9a36 | ||
|
|
16d5e9d227 | ||
|
|
3ac04a64f7 | ||
|
|
d9cb739430 | ||
|
|
7a2dd61a7c | ||
|
|
a520ad0fc3 | ||
|
|
da219f40f8 | ||
|
|
664e9a6786 | ||
|
|
fcef970e81 | ||
|
|
8933cb7b40 | ||
|
|
82639170a7 | ||
|
|
129d8cd141 | ||
|
|
f3a9521894 | ||
|
|
0be5f898ad | ||
|
|
ac17c8d003 | ||
|
|
c6c137a329 | ||
|
|
1cb91662a5 | ||
|
|
7ca101cb7e | ||
|
|
dad7d0bd88 | ||
|
|
f9dcb005cd | ||
|
|
78267a0a96 | ||
|
|
00beb27f83 | ||
|
|
7bd12334c4 | ||
|
|
54a46eb49b | ||
|
|
2452dc7a71 | ||
|
|
74b7d0c951 | ||
|
|
baad4174e5 | ||
|
|
cde84508ac | ||
|
|
43623ec9bf | ||
|
|
fe3fe327e2 | ||
|
|
03ae6710cd | ||
|
|
067f85e5a2 | ||
|
|
5800563fe7 | ||
|
|
14c158bbba | ||
|
|
f235c2f1c3 | ||
|
|
95eaaa2130 | ||
|
|
dff1fd868c | ||
|
|
0e2af0d8d1 | ||
|
|
21e3614b8d | ||
|
|
730946cfd5 | ||
|
|
277f53f886 | ||
|
|
f8e735212b | ||
|
|
f30ee2b300 | ||
|
|
869638f6cf | ||
|
|
c279989118 | ||
|
|
c68605f18d | ||
|
|
7be497a14e | ||
|
|
081929092f | ||
|
|
7bf4a10995 | ||
|
|
721b211168 | ||
|
|
2b15649f93 | ||
|
|
18cc71a6d9 | ||
|
|
816a2a0c15 | ||
|
|
42f33fe2f8 | ||
|
|
5c2533bb7b | ||
|
|
6ea9a7c031 | ||
|
|
543734d286 | ||
|
|
5fae914f8f | ||
|
|
1b916ab754 | ||
|
|
fad0a62a35 | ||
|
|
dbcdd0416b | ||
|
|
87d14ddfc6 | ||
|
|
142c60dca1 | ||
|
|
b14413e4f4 | ||
|
|
2a81995058 | ||
|
|
d6ae19cc6d | ||
|
|
6659ff57eb | ||
|
|
b304ac9e56 | ||
|
|
2ce7b409a8 | ||
|
|
110a8becb8 | ||
|
|
fb2052f02d | ||
|
|
e72c8594fb | ||
|
|
4f7ab22584 | ||
|
|
308aee0c90 | ||
|
|
003b10c280 | ||
|
|
7511e3adaa | ||
|
|
8966c69fcd | ||
|
|
97e2eeb83c | ||
|
|
fecce454cb | ||
|
|
36865d63bb | ||
|
|
963b5a5a97 | ||
|
|
4deef2d5ef | ||
|
|
049dee2634 | ||
|
|
0900f84eab | ||
|
|
4d0f126996 | ||
|
|
ee3fa37bc3 | ||
|
|
15ea3328e1 | ||
|
|
86e1fedfaf | ||
|
|
4777d410dd | ||
|
|
5d232ff77c | ||
|
|
9beff8a998 | ||
|
|
3a76a8c8ca | ||
|
|
d205dc92de | ||
|
|
65b5b03c94 | ||
|
|
0a0e77c375 | ||
|
|
5e37a97684 | ||
|
|
f45a488dea | ||
|
|
78481a8dfc | ||
|
|
1ffd612be5 | ||
|
|
dfcf73921c | ||
|
|
8fe4f218fb | ||
|
|
8b7c213443 | ||
|
|
0e76a0bd53 | ||
|
|
49dd7b4079 | ||
|
|
b23c9fceb7 | ||
|
|
db078ba439 | ||
|
|
da2838c39c | ||
|
|
d11d18f746 | ||
|
|
ee0847d605 | ||
|
|
08fcb7f2ef | ||
|
|
805a7cd0ae | ||
|
|
d2193f4dba | ||
|
|
07c4ed220e | ||
|
|
7027979c2b | ||
|
|
17a02fef5e | ||
|
|
cc7be1fd47 | ||
|
|
cce7768b27 | ||
|
|
15da6ddf12 | ||
|
|
e470c32c24 | ||
|
|
a1f55783e6 | ||
|
|
bf6f829c83 | ||
|
|
9fc2f486a8 | ||
|
|
ec872bca82 | ||
|
|
139b801cce | ||
|
|
a6bc724748 | ||
|
|
b69d00b0fe | ||
|
|
4915d1bd09 | ||
|
|
91fd04f96a | ||
|
|
57ea5340c6 | ||
|
|
5b591babd1 | ||
|
|
ae2081989e | ||
|
|
41d3e16eac | ||
|
|
98c25b9aa0 | ||
|
|
aec288f83c | ||
|
|
9991c0f46f | ||
|
|
99bceec689 | ||
|
|
8b43f42dca | ||
|
|
1b110f62dd | ||
|
|
cdae49be40 | ||
|
|
cbee467266 | ||
|
|
8668f2c74f | ||
|
|
cc85df9440 | ||
|
|
b360d655cc | ||
|
|
a8338eccef | ||
|
|
68316916e0 | ||
|
|
29e38c284e | ||
|
|
d861eab22b | ||
|
|
6b7e298549 | ||
|
|
04bac19343 | ||
|
|
6ad626c686 | ||
|
|
d3314cb62e | ||
|
|
d77f764dcd | ||
|
|
49f5027158 | ||
|
|
3ed12db8f0 | ||
|
|
de4b622223 | ||
|
|
2fb79a4325 | ||
|
|
519d511488 | ||
|
|
811b5308f8 | ||
|
|
4455da7102 | ||
|
|
4154ff23c0 | ||
|
|
d10b7272d2 | ||
|
|
d9acc99734 | ||
|
|
0617a59ed6 | ||
|
|
5742052982 | ||
|
|
94d419832e | ||
|
|
ff4dbc0589 | ||
|
|
15ddfb21ed | ||
|
|
f541fec5ad | ||
|
|
cd338a7a43 | ||
|
|
ceef7d179f | ||
|
|
797d2bddc4 | ||
|
|
e00bf7bc84 | ||
|
|
f1db12c31c | ||
|
|
4d362b4b98 | ||
|
|
1da46327b3 | ||
|
|
17125cefa6 | ||
|
|
e39ef3e700 | ||
|
|
30c00bc4fe | ||
|
|
a5c0d24749 | ||
|
|
b1fa9725cf | ||
|
|
edae7391ed | ||
|
|
a53c3b03a0 | ||
|
|
f535c3bb97 | ||
|
|
71040925fe | ||
|
|
ffea4e8e58 | ||
|
|
d4e7f9793c | ||
|
|
3a260021dd | ||
|
|
a858378ce7 | ||
|
|
aa83bf0402 | ||
|
|
50bb81484d | ||
|
|
3b328005c9 | ||
|
|
a376ad491f | ||
|
|
21cfae7b56 | ||
|
|
5cac4316db | ||
|
|
bc2802abed | ||
|
|
16e48018b3 | ||
|
|
85f985a778 | ||
|
|
8d737aa238 | ||
|
|
aedab6e0f2 | ||
|
|
fb3e029911 | ||
|
|
6c674cb5bd | ||
|
|
fb348c3493 | ||
|
|
43dc8eadbe | ||
|
|
123fb1c13c | ||
|
|
361f293a73 | ||
|
|
e24c109c82 | ||
|
|
77cb23fccc | ||
|
|
0f6780b9a3 | ||
|
|
475c48d1e3 | ||
|
|
457eb295b7 | ||
|
|
eccef997df | ||
|
|
15154504de | ||
|
|
d6e33d0722 | ||
|
|
4cb11acd46 | ||
|
|
4701df97c0 | ||
|
|
5c1dd38c45 | ||
|
|
20bdf3a8a0 | ||
|
|
aff9b9d92e | ||
|
|
24a64db7cc | ||
|
|
b3a752aa5b | ||
|
|
63b7d3b811 | ||
|
|
75fe13e7aa | ||
|
|
8178cfddd8 | ||
|
|
78714eb1b8 | ||
|
|
b018c20018 | ||
|
|
aa2c18895b | ||
|
|
ec0f2b59ee | ||
|
|
13a7a04e79 | ||
|
|
f1142b6e42 | ||
|
|
6c8ac80be9 | ||
|
|
d4db0a32f0 | ||
|
|
86d99e4f8e | ||
|
|
83d61ea20e | ||
|
|
35d4954030 | ||
|
|
bf37664420 | ||
|
|
422d7cc503 | ||
|
|
3a4245ba23 | ||
|
|
7c1e6861fb | ||
|
|
64698af1ac | ||
|
|
ba17ca14a0 | ||
|
|
8eafafaa16 | ||
|
|
f303a957a3 | ||
|
|
3294a64027 | ||
|
|
7c2e757c03 | ||
|
|
0d4f5cb13f | ||
|
|
d3ff0ce81d | ||
|
|
791d7cead4 | ||
|
|
827bcdabd9 | ||
|
|
1ec3b3aabe | ||
|
|
b53ddd3d54 | ||
|
|
1fcbacfff9 | ||
|
|
e65b7f2875 | ||
|
|
01dc3dba18 | ||
|
|
0535b0ca37 | ||
|
|
9a48405a87 | ||
|
|
cf8719f132 | ||
|
|
1a82df3cac | ||
|
|
c1379718ab | ||
|
|
cb01ffcc3f | ||
|
|
cc2ed5e85f | ||
|
|
e957e8ce0d | ||
|
|
6821fa7611 | ||
|
|
fd77bda97a | ||
|
|
8769254bda | ||
|
|
08d676b417 | ||
|
|
df3fa372a6 | ||
|
|
6011985a6c | ||
|
|
af67543764 | ||
|
|
94f48e3ebf | ||
|
|
0dd08ce747 | ||
|
|
30d9c675a4 | ||
|
|
30ed8da439 | ||
|
|
5f7326afd5 | ||
|
|
2c7375bdb2 | ||
|
|
a8845c066c | ||
|
|
8d656ed837 | ||
|
|
cf302b07d1 | ||
|
|
a95fff5477 | ||
|
|
495b069667 | ||
|
|
b27de1038f | ||
|
|
2f747dc9b1 | ||
|
|
1f9b274dfb | ||
|
|
4ae90615d1 | ||
|
|
5fa7514fee | ||
|
|
83fafba7f0 | ||
|
|
4d787db0b1 | ||
|
|
34706d3ef1 | ||
|
|
8e6dea673b | ||
|
|
603c61fcb9 | ||
|
|
f9bcce2e01 | ||
|
|
3a295c7493 | ||
|
|
6cefea704e | ||
|
|
a3bca3bc45 | ||
|
|
146ea6e12b | ||
|
|
0561079b6e | ||
|
|
4d7d35818b | ||
|
|
b75abfff09 | ||
|
|
6ae37120b3 | ||
|
|
5210ca0b51 | ||
|
|
f1f493e59e | ||
|
|
67f848cac9 | ||
|
|
9b4577c6f2 | ||
|
|
eeebf706cb | ||
|
|
787800dda6 | ||
|
|
e62bf4ee5f | ||
|
|
224598586c | ||
|
|
8e330c7f24 | ||
|
|
d0714e0828 | ||
|
|
cf98b9b61e | ||
|
|
4e9fededff | ||
|
|
034780a0c9 | ||
|
|
a21ee02710 | ||
|
|
5e23ccaac2 | ||
|
|
5784924c59 | ||
|
|
256df9304f | ||
|
|
27e1199387 | ||
|
|
e4aacc9a91 | ||
|
|
b8fcf27661 | ||
|
|
ebd2a56b5d | ||
|
|
921af361cf | ||
|
|
7deb5092c6 | ||
|
|
1c66a7f871 | ||
|
|
1aa2fec173 | ||
|
|
922bae62af | ||
|
|
26c52db75b | ||
|
|
77c8f075f3 | ||
|
|
0188262b4a | ||
|
|
792bba8dab | ||
|
|
957ee4e634 | ||
|
|
6952936eac | ||
|
|
f3a9b493c0 | ||
|
|
377c3d1c75 | ||
|
|
9a78267a7b | ||
|
|
a91efd56f8 | ||
|
|
e7389ccc2a | ||
|
|
b1b0ef9238 | ||
|
|
ad2f2ae76d | ||
|
|
8b72e64c3c | ||
|
|
68b6020e8e | ||
|
|
4eefcef0fc | ||
|
|
081c90b769 | ||
|
|
596a4d69bb | ||
|
|
7d3c321253 | ||
|
|
48e921883a | ||
|
|
9dd09fec1e | ||
|
|
22cda59a19 | ||
|
|
a234575b04 | ||
|
|
73b2f981d5 | ||
|
|
c4acd92930 | ||
|
|
953e3f2630 | ||
|
|
39f407e3ff | ||
|
|
73bf3febf7 | ||
|
|
eab567a02d | ||
|
|
2338c3d649 | ||
|
|
7e1d8add13 | ||
|
|
7100767f64 | ||
|
|
0b8f124453 | ||
|
|
c9d9ce6cbb | ||
|
|
337e20e0f2 | ||
|
|
8fe076695f | ||
|
|
439030503c | ||
|
|
18e2fb1e78 | ||
|
|
69c6ce8c17 | ||
|
|
1850706af0 | ||
|
|
94f4143ad6 | ||
|
|
e65dc4c3b5 | ||
|
|
4a2b8ed299 | ||
|
|
4f8983b89f | ||
|
|
7ad63a865e | ||
|
|
634ba3a1f7 | ||
|
|
2961ef5135 | ||
|
|
8816d4494f | ||
|
|
c0137208ec | ||
|
|
8f4df58983 | ||
|
|
5177b3dc26 | ||
|
|
55fdc50e81 | ||
|
|
d736b259de | ||
|
|
397452ead5 | ||
|
|
0ab73028d6 | ||
|
|
ecb854da3b | ||
|
|
400c2b5a3b | ||
|
|
148b4ca1b2 | ||
|
|
aea4ef2338 | ||
|
|
b1ec6a2586 | ||
|
|
6739742fb6 | ||
|
|
d251226a3c | ||
|
|
ae8184b8b9 | ||
|
|
470ecf8996 | ||
|
|
0cdb6bef7b | ||
|
|
cd603fa981 | ||
|
|
b12009a746 | ||
|
|
9f9a0bc410 | ||
|
|
5d290a2a75 | ||
|
|
5808350bfe | ||
|
|
e64e74d5ed | ||
|
|
1a88b26a10 | ||
|
|
6c05cdfb0c | ||
|
|
aefae11c0d | ||
|
|
630fa52717 | ||
|
|
eea85f4eff | ||
|
|
cd6672701b | ||
|
|
f4ca3d95ca | ||
|
|
17e3c7a5c5 | ||
|
|
dd60d5cff0 | ||
|
|
f1288ea5c9 | ||
|
|
47ae060966 | ||
|
|
a1893b4420 | ||
|
|
999e041d49 | ||
|
|
0d4d854809 | ||
|
|
93d793d249 | ||
|
|
82486d4b0a | ||
|
|
5d9605a91e | ||
|
|
5293dd683e | ||
|
|
62effdf128 | ||
|
|
8e4f21aba0 | ||
|
|
5208299bbb | ||
|
|
ba7350c7ee | ||
|
|
f96d518ebf | ||
|
|
d5b82fbbab | ||
|
|
a7d8c0cbb2 | ||
|
|
24007727d4 | ||
|
|
6aa63a7213 | ||
|
|
eacb95c85d | ||
|
|
6967640571 | ||
|
|
4ac2d5d276 | ||
|
|
642b56d9af | ||
|
|
0574fcf256 | ||
|
|
98de2bc1a8 |
|
|
@ -1,2 +0,0 @@
|
|||
target
|
||||
.git
|
||||
5
.envrc
5
.envrc
|
|
@ -1 +1,4 @@
|
|||
use flake
|
||||
source_up_if_exists
|
||||
if [ -z "${IN_NIX_SHELL:-}" ]; then
|
||||
use flake .
|
||||
fi
|
||||
|
|
|
|||
8
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
8
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
|
@ -2,6 +2,14 @@ name: Bug Report
|
|||
description: Report a problem
|
||||
type: Bug
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: AI Policy
|
||||
description: Review our [AI Policy](https://tree-sitter.github.io/tree-sitter/6-contributing.html#ai-policy).
|
||||
options:
|
||||
- label: I have read the AI Policy and this issue complies with it.
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: "Problem"
|
||||
|
|
|
|||
8
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
8
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
|
|
@ -2,6 +2,14 @@ name: Feature request
|
|||
description: Request an enhancement
|
||||
type: Feature
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: AI Policy
|
||||
description: Review our [AI Policy](https://tree-sitter.github.io/tree-sitter/6-contributing.html#ai-policy).
|
||||
options:
|
||||
- label: I have read the AI Policy and this issue complies with it.
|
||||
required: true
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
|
|
|
|||
6
.github/actions/cache/action.yml
vendored
6
.github/actions/cache/action.yml
vendored
|
|
@ -10,7 +10,7 @@ outputs:
|
|||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- uses: actions/cache@v4
|
||||
- uses: actions/cache@v5
|
||||
id: cache
|
||||
with:
|
||||
path: |
|
||||
|
|
@ -21,5 +21,9 @@ runs:
|
|||
'lib/src/parser.h',
|
||||
'lib/src/array.h',
|
||||
'lib/src/alloc.h',
|
||||
'lib/src/wasm-stdlib/external_scanner_stdlib.h',
|
||||
'crates/loader/wasi-sdk-version',
|
||||
'crates/loader/binaryen-version',
|
||||
'test/fixtures/grammars/*/**/src/*.c',
|
||||
'test/fixtures/fixtures.json',
|
||||
'.github/actions/cache/action.yml') }}
|
||||
|
|
|
|||
20
.github/cliff.toml
vendored
20
.github/cliff.toml
vendored
|
|
@ -43,16 +43,16 @@ commit_preprocessors = [
|
|||
]
|
||||
# regex for parsing and grouping commits
|
||||
commit_parsers = [
|
||||
{ message = "!:", group = "<!-- 0 -->Breaking" },
|
||||
{ message = "^feat", group = "<!-- 1 -->Features" },
|
||||
{ message = "^fix", group = "<!-- 2 -->Bug Fixes" },
|
||||
{ message = "^perf", group = "<!-- 3 -->Performance" },
|
||||
{ message = "^doc", group = "<!-- 4 -->Documentation" },
|
||||
{ message = "^refactor", group = "<!-- 5 -->Refactor" },
|
||||
{ message = "^test", group = "<!-- 6 -->Testing" },
|
||||
{ message = "^build", group = "<!-- 7 -->Build System and CI" },
|
||||
{ message = "^ci", group = "<!-- 7 -->Build System and CI" },
|
||||
{ message = ".*", group = "<!-- 8 -->Other" },
|
||||
{ group = "<!-- 0 -->Breaking", message = "!:" },
|
||||
{ group = "<!-- 1 -->Features", message = "^feat" },
|
||||
{ group = "<!-- 2 -->Bug Fixes", message = "^fix" },
|
||||
{ group = "<!-- 3 -->Performance", message = "^perf" },
|
||||
{ group = "<!-- 4 -->Documentation", message = "^doc" },
|
||||
{ group = "<!-- 5 -->Refactor", message = "^refactor" },
|
||||
{ group = "<!-- 6 -->Testing", message = "^test" },
|
||||
{ group = "<!-- 7 -->Build System and CI", message = "^build" },
|
||||
{ group = "<!-- 7 -->Build System and CI", message = "^ci" },
|
||||
{ group = "<!-- 8 -->Other", message = ".*" },
|
||||
]
|
||||
# filter out the commits that are not matched by commit parsers
|
||||
filter_commits = false
|
||||
|
|
|
|||
6
.github/pull_request_template.md
vendored
Normal file
6
.github/pull_request_template.md
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
### AI Policy
|
||||
|
||||
- [ ] I have read the [AI Policy](https://tree-sitter.github.io/tree-sitter/6-contributing.html#ai-policy) and this PR complies with it.
|
||||
- [ ] If AI tools were used: I have disclosed the tool and extent of usage below.
|
||||
|
||||
<!-- If you used AI tools, state which tool and how it was used. Delete this section if not applicable. -->
|
||||
16
.github/scripts/reviewers_remove.js
vendored
16
.github/scripts/reviewers_remove.js
vendored
|
|
@ -1,16 +0,0 @@
|
|||
module.exports = async ({ github, context }) => {
|
||||
const requestedReviewers = await github.rest.pulls.listRequestedReviewers({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: context.issue.number,
|
||||
});
|
||||
|
||||
const reviewers = requestedReviewers.data.users.map((e) => e.login);
|
||||
|
||||
github.rest.pulls.removeRequestedReviewers({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: context.issue.number,
|
||||
reviewers: reviewers,
|
||||
});
|
||||
};
|
||||
35
.github/scripts/wasm_stdlib.js
vendored
Normal file
35
.github/scripts/wasm_stdlib.js
vendored
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
module.exports = async ({ github, context, core }) => {
|
||||
if (context.eventName !== 'pull_request') return;
|
||||
|
||||
const prNumber = context.payload.pull_request.number;
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
|
||||
const files = await github.paginate(github.rest.pulls.listFiles, {
|
||||
owner,
|
||||
repo,
|
||||
pull_number: prNumber,
|
||||
per_page: 100
|
||||
});
|
||||
|
||||
const changedFiles = files.map(file => file.filename);
|
||||
|
||||
const wasmStdLibSources = [
|
||||
'lib/src/wasm-stdlib/external_scanner_allocator.c',
|
||||
'lib/src/wasm-stdlib/imports.txt',
|
||||
'lib/src/wasm-stdlib/libc.c',
|
||||
'lib/src/wasm-stdlib/stdio.c'
|
||||
];
|
||||
const dirChanged = changedFiles.some(file =>
|
||||
wasmStdLibSources.includes(file) ||
|
||||
file.startsWith('lib/src/wasm-stdlib/libc/ctype/') ||
|
||||
file.startsWith('lib/src/wasm-stdlib/libc/string/')
|
||||
);
|
||||
|
||||
if (!dirChanged) return;
|
||||
|
||||
const wasmStdLibHeader = 'lib/src/wasm-stdlib/external_scanner_stdlib.h';
|
||||
const requiredChanged = changedFiles.includes(wasmStdLibHeader);
|
||||
|
||||
if (!requiredChanged) core.setFailed(`Changes detected in the Wasm stdlib sources but ${wasmStdLibHeader} was not modified.`);
|
||||
};
|
||||
9
.github/workflows/backport.yml
vendored
9
.github/workflows/backport.yml
vendored
|
|
@ -14,17 +14,20 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7.0.1
|
||||
with:
|
||||
persist-credentials: true
|
||||
ref: ${{ github.event.pull_request.base.ref }}
|
||||
|
||||
- name: Create app token
|
||||
uses: actions/create-github-app-token@v2
|
||||
uses: actions/create-github-app-token@v3
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ vars.BACKPORT_APP }}
|
||||
private-key: ${{ secrets.BACKPORT_KEY }}
|
||||
|
||||
- name: Create backport PR
|
||||
uses: korthout/backport-action@v3
|
||||
uses: korthout/backport-action@v4.6.0
|
||||
with:
|
||||
pull_title: "${pull_title}"
|
||||
label_pattern: "^ci:backport ([^ ]+)$"
|
||||
|
|
|
|||
2
.github/workflows/bindgen.yml
vendored
2
.github/workflows/bindgen.yml
vendored
|
|
@ -16,7 +16,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7.0.1
|
||||
|
||||
- name: Set up stable Rust toolchain
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
|
|
|
|||
48
.github/workflows/build.yml
vendored
48
.github/workflows/build.yml
vendored
|
|
@ -26,12 +26,14 @@ jobs:
|
|||
- windows-x86
|
||||
- macos-arm64
|
||||
- macos-x64
|
||||
- illumos-x64
|
||||
- wasm32
|
||||
|
||||
include:
|
||||
# When adding a new `target`:
|
||||
# 1. Define a new platform alias above
|
||||
# 2. Add a new record to the matrix map in `crates/cli/npm/install.js`
|
||||
# 3. Consider adding the mapping at the end of 'crates/cli/Cargo.toml' for cargo-binstall support
|
||||
- { platform: linux-arm64 , target: aarch64-unknown-linux-gnu , os: ubuntu-24.04-arm }
|
||||
- { platform: linux-arm , target: armv7-unknown-linux-gnueabihf , os: ubuntu-24.04-arm }
|
||||
- { platform: linux-x64 , target: x86_64-unknown-linux-gnu , os: ubuntu-24.04 }
|
||||
|
|
@ -44,6 +46,9 @@ jobs:
|
|||
- { platform: macos-x64 , target: x86_64-apple-darwin , os: macos-15-intel }
|
||||
- { platform: wasm32 , target: wasm32-unknown-unknown , os: ubuntu-24.04 }
|
||||
|
||||
# illumos is not supported OOTB, it runs in a vm
|
||||
- { platform: illumos-x64 , target: x86_64-unknown-illumos , os: ubuntu-24.04 , vm: true , no-run: true }
|
||||
|
||||
# Extra features
|
||||
- { platform: linux-arm64 , features: wasm }
|
||||
- { platform: linux-x64 , features: wasm , run-wasm-test: true }
|
||||
|
|
@ -68,7 +73,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7.0.1
|
||||
|
||||
- name: Set up cross-compilation
|
||||
if: matrix.cross
|
||||
|
|
@ -90,7 +95,7 @@ jobs:
|
|||
|
||||
- name: Cache Emscripten SDK
|
||||
if: contains(matrix.features, 'wasm') && (matrix.run-wasm-test || !inputs.run-test)
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: emsdk
|
||||
key: emsdk-${{ env.EMSCRIPTEN_VERSION }}-${{ runner.os }}-${{ runner.arch }}
|
||||
|
|
@ -111,17 +116,22 @@ jobs:
|
|||
|
||||
- name: Set up Node.js
|
||||
if: contains(matrix.features, 'wasm') && (matrix.run-wasm-test || !inputs.run-test)
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v7.0.0
|
||||
with:
|
||||
node-version: 24
|
||||
cache: npm
|
||||
cache-dependency-path: lib/binding_web/package-lock.json
|
||||
|
||||
- name: Set up Rust
|
||||
if: ${{ !matrix.vm }}
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
|
||||
- name: Install Rust Wasm test target
|
||||
if: matrix.run-wasm-test
|
||||
run: rustup toolchain install nightly --profile minimal --component rust-src
|
||||
|
||||
- name: Install cross-compilation toolchain
|
||||
if: matrix.cross
|
||||
run: |
|
||||
|
|
@ -191,7 +201,7 @@ jobs:
|
|||
WASMTIME_REPO: https://github.com/bytecodealliance/wasmtime
|
||||
|
||||
- name: Build C library (make)
|
||||
if: runner.os != 'Windows'
|
||||
if: runner.os != 'Windows' && !matrix.vm
|
||||
run: |
|
||||
if [[ $PLATFORM == linux-arm ]]; then
|
||||
CC=arm-linux-gnueabihf-gcc; AR=arm-linux-gnueabihf-ar
|
||||
|
|
@ -205,10 +215,10 @@ jobs:
|
|||
make -j CFLAGS="$CFLAGS" CC=$CC AR=$AR
|
||||
env:
|
||||
PLATFORM: ${{ matrix.platform }}
|
||||
CFLAGS: -g -Werror -Wall -Wextra -Wshadow -Wpedantic -Werror=incompatible-pointer-types
|
||||
CFLAGS: -g -Werror -Wall -Wextra -Wshadow -Wpedantic -Werror=incompatible-pointer-types -Werror=strict-aliasing -Wstrict-aliasing=2
|
||||
|
||||
- name: Build C library (CMake)
|
||||
if: "!matrix.cross"
|
||||
if: "!matrix.cross && !matrix.vm"
|
||||
run: |
|
||||
cmake -S . -B build/static \
|
||||
-DBUILD_SHARED_LIBS=OFF \
|
||||
|
|
@ -227,6 +237,20 @@ jobs:
|
|||
CC: ${{ contains(matrix.platform, 'linux') && 'clang' || '' }}
|
||||
WASM: ${{ contains(matrix.features, 'wasm') && (matrix.run-wasm-test || !inputs.run-test) && 'ON' || 'OFF' }}
|
||||
|
||||
- name: Build C library and Rust crate (illumos gmake)
|
||||
if: matrix.platform == 'illumos-x64'
|
||||
uses: vmactions/omnios-vm@v1.3.6
|
||||
with:
|
||||
release: r151056-build
|
||||
copyback: false
|
||||
prepare: |
|
||||
pkg install -q build-essential || [ $? -eq 4 ]
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal
|
||||
run: |
|
||||
. "$HOME/.cargo/env"
|
||||
gmake -j
|
||||
cargo build -p tree-sitter
|
||||
|
||||
- name: Build Wasm library
|
||||
if: contains(matrix.features, 'wasm') && (matrix.run-wasm-test || !inputs.run-test)
|
||||
shell: bash
|
||||
|
|
@ -245,7 +269,7 @@ jobs:
|
|||
run: cargo check --no-default-features --target='${{ matrix.target }}'
|
||||
|
||||
- name: Build target
|
||||
if: "!inputs.run-test"
|
||||
if: "!inputs.run-test && !matrix.vm"
|
||||
run: cargo build --release --target='${{ matrix.target }}' --features='${{ (matrix.run-wasm-test || !inputs.run-test) && matrix.features || '' }}' $PACKAGE
|
||||
env:
|
||||
PACKAGE: ${{ matrix.platform == 'wasm32' && '-p tree-sitter' || '' }}
|
||||
|
|
@ -269,15 +293,19 @@ jobs:
|
|||
|
||||
- name: Run main tests
|
||||
if: inputs.run-test && !matrix.no-run
|
||||
run: cargo test --target='${{ matrix.target }}' --features='${{ (matrix.run-wasm-test || !inputs.run-test) && matrix.features || '' }}'
|
||||
run: cargo test --workspace --target='${{ matrix.target }}' --features='${{ (matrix.run-wasm-test || !inputs.run-test) && matrix.features || '' }}'
|
||||
|
||||
- name: Run Wasm tests
|
||||
if: inputs.run-test && !matrix.no-run && contains(matrix.features, 'wasm') && matrix.run-wasm-test
|
||||
run: cargo run -p xtask --target='${{ matrix.target }}' -- test-wasm
|
||||
|
||||
- name: Run Rust Wasm web test
|
||||
if: inputs.run-test && !matrix.no-run && contains(matrix.features, 'wasm') && matrix.run-wasm-test
|
||||
run: cargo run -p xtask --target='${{ matrix.target }}' -- test-rust-wasm-web
|
||||
|
||||
- name: Upload CLI artifact
|
||||
if: "!inputs.run-test && !matrix.no-run"
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: tree-sitter.${{ matrix.platform }}
|
||||
path: target/${{ matrix.target }}/release/tree-sitter${{ contains(matrix.target, 'windows') && '.exe' || '' }}
|
||||
|
|
@ -286,7 +314,7 @@ jobs:
|
|||
|
||||
- name: Upload Wasm artifacts
|
||||
if: "!inputs.run-test && matrix.platform == 'linux-x64'"
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: tree-sitter.wasm
|
||||
path: |
|
||||
|
|
|
|||
43
.github/workflows/ci.yml
vendored
43
.github/workflows/ci.yml
vendored
|
|
@ -1,21 +1,14 @@
|
|||
name: CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- docs/**
|
||||
- "**/README.md"
|
||||
- CONTRIBUTING.md
|
||||
- LICENSE
|
||||
- cli/src/templates
|
||||
push:
|
||||
branches: [master]
|
||||
paths-ignore:
|
||||
- docs/**
|
||||
- "**/README.md"
|
||||
- CONTRIBUTING.md
|
||||
- LICENSE
|
||||
- cli/src/templates
|
||||
branches:
|
||||
- 'master'
|
||||
- 'release-[0-9]+.[0-9]+'
|
||||
pull_request:
|
||||
branches:
|
||||
- 'master'
|
||||
- 'release-[0-9]+.[0-9]+'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
|
@ -26,7 +19,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7.0.1
|
||||
|
||||
- name: Set up stable Rust toolchain
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
|
|
@ -34,13 +27,25 @@ jobs:
|
|||
toolchain: stable
|
||||
components: clippy, rustfmt
|
||||
|
||||
- name: Lint files
|
||||
run: |
|
||||
make lint
|
||||
make lint-web
|
||||
- name: Lint Rust files
|
||||
run: make lint
|
||||
|
||||
- name: Install Taplo
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: taplo@0.10.0
|
||||
|
||||
- name: Lint TOML files
|
||||
run: make lint-toml
|
||||
|
||||
- name: Lint web files
|
||||
run: make lint-web
|
||||
|
||||
sanitize:
|
||||
uses: ./.github/workflows/sanitize.yml
|
||||
|
||||
build:
|
||||
uses: ./.github/workflows/build.yml
|
||||
|
||||
check-wasm-stdlib:
|
||||
uses: ./.github/workflows/wasm_stdlib.yml
|
||||
|
|
|
|||
2
.github/workflows/crate_versions.yml
vendored
2
.github/workflows/crate_versions.yml
vendored
|
|
@ -11,7 +11,7 @@ jobs:
|
|||
if: contains(github.event.pull_request.labels.*.name, 'ci:check release') || github.event_name == 'workflow_dispatch'
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7.0.1
|
||||
|
||||
- name: Set up Rust
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
|
|
|
|||
16
.github/workflows/docs.yml
vendored
16
.github/workflows/docs.yml
vendored
|
|
@ -16,35 +16,29 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Rust
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
uses: actions/checkout@v7.0.1
|
||||
|
||||
- name: Install mdbook
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
jq_expr='.assets[] | select(.name | contains("x86_64-unknown-linux-gnu")) | .browser_download_url'
|
||||
url=$(gh api repos/rust-lang/mdbook/releases/tags/v0.4.52 --jq "$jq_expr")
|
||||
url=$(gh api repos/rust-lang/mdbook/releases/tags/v0.5.4 --jq "$jq_expr")
|
||||
mkdir mdbook
|
||||
curl -sSL "$url" | tar -xz -C mdbook
|
||||
printf '%s/mdbook\n' "$PWD" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Install mdbook-admonish
|
||||
run: cargo install mdbook-admonish
|
||||
|
||||
- name: Build Book
|
||||
run: mdbook build docs
|
||||
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v5
|
||||
uses: actions/configure-pages@v6
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v4
|
||||
uses: actions/upload-pages-artifact@v5
|
||||
with:
|
||||
path: docs/book
|
||||
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
uses: actions/deploy-pages@v5
|
||||
|
|
|
|||
67
.github/workflows/nvim_ts.yml
vendored
67
.github/workflows/nvim_ts.yml
vendored
|
|
@ -3,6 +3,7 @@ name: nvim-treesitter parser tests
|
|||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'lib/**'
|
||||
- 'crates/cli/**'
|
||||
- 'crates/config/**'
|
||||
- 'crates/generate/**'
|
||||
|
|
@ -25,34 +26,54 @@ jobs:
|
|||
name: ${{ matrix.os }} - ${{ matrix.type }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
env:
|
||||
NVIM: ${{ matrix.os == 'windows-latest' && 'nvim-win64\\bin\\nvim.exe' || 'nvim' }}
|
||||
NVIM: ${{ matrix.os == 'windows-latest' && 'nvim.exe' || 'nvim' }}
|
||||
NVIM_TAG: stable
|
||||
NVIM_DIR: neovim
|
||||
NVIM_TS_DIR: nvim-treesitter
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7.0.1
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
- run: cargo build --profile optimize
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
- name: Clone Neovim
|
||||
uses: actions/checkout@v7.0.1
|
||||
with:
|
||||
repository: neovim/neovim
|
||||
ref: ${{ env.NVIM_TAG }}
|
||||
path: ${{ env.NVIM_DIR }}
|
||||
|
||||
- if: runner.os != 'Windows'
|
||||
name: Setup environment (Posix)
|
||||
run: |
|
||||
echo ${{ github.workspace }}/target/optimize >> "$GITHUB_PATH"
|
||||
echo ${{ github.workspace }}/neovim/build/bin >> "$GITHUB_PATH"
|
||||
echo "VIMRUNTIME=${{ github.workspace }}/neovim/runtime" >> "$GITHUB_ENV"
|
||||
|
||||
- if: runner.os == 'Windows'
|
||||
name: Setup environment (why can't you just be normal?!)
|
||||
run: |
|
||||
${{ env.NVIM_DIR }}/.github/scripts/env.ps1
|
||||
echo ${{ github.workspace }}/target/optimize >> "$env:GITHUB_PATH"
|
||||
echo ${{ github.workspace }}/neovim/build/bin >> "$env:GITHUB_PATH"
|
||||
echo "VIMRUNTIME=${{ github.workspace }}/neovim/runtime" >> "$env:GITHUB_ENV"
|
||||
|
||||
- name: Build Neovim
|
||||
working-directory: ${{ env.NVIM_DIR }}
|
||||
run: |
|
||||
cmake -S cmake.deps -B .deps -G Ninja -D CMAKE_BUILD_TYPE=Release -D TREESITTER_URL=https://github.com/tree-sitter/tree-sitter/archive/${{ github.event.pull_request.head.sha }}.tar.gz -D DEPS_IGNORE_SHA=TRUE
|
||||
cmake --build .deps --config Release
|
||||
cmake -B build -G Ninja -D CMAKE_BUILD_TYPE=Release
|
||||
cmake --build build --config Release
|
||||
|
||||
- name: Clone nvim-treesitter
|
||||
uses: actions/checkout@v7.0.1
|
||||
with:
|
||||
repository: nvim-treesitter/nvim-treesitter
|
||||
path: ${{ env.NVIM_TS_DIR }}
|
||||
ref: main
|
||||
|
||||
- if: runner.os != 'Windows'
|
||||
run: echo ${{ github.workspace }}/target/release >> $GITHUB_PATH
|
||||
|
||||
- if: runner.os == 'Windows'
|
||||
run: echo ${{ github.workspace }}/target/release >> $env:GITHUB_PATH
|
||||
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
- run: cargo build --release
|
||||
- uses: ilammy/msvc-dev-cmd@v1
|
||||
|
||||
- name: Install and prepare Neovim
|
||||
run: bash ./scripts/ci-install.sh
|
||||
working-directory: ${{ env.NVIM_TS_DIR }}
|
||||
|
||||
- if: matrix.type == 'generate'
|
||||
name: Generate and compile parsers
|
||||
run: $NVIM -l ./scripts/install-parsers.lua --generate --max-jobs=2
|
||||
run: $NVIM -l ./scripts/install-parsers.lua --generate --max-jobs=10
|
||||
working-directory: ${{ env.NVIM_TS_DIR }}
|
||||
shell: bash
|
||||
|
||||
|
|
@ -63,7 +84,13 @@ jobs:
|
|||
shell: bash
|
||||
|
||||
- if: "!cancelled()"
|
||||
name: Check query files
|
||||
name: Test parsers
|
||||
run: $NVIM -l ./scripts/check-parsers.lua
|
||||
working-directory: ${{ env.NVIM_TS_DIR }}
|
||||
shell: bash
|
||||
|
||||
- if: "!cancelled()"
|
||||
name: Test queries
|
||||
run: $NVIM -l ./scripts/check-queries.lua
|
||||
working-directory: ${{ env.NVIM_TS_DIR }}
|
||||
shell: bash
|
||||
|
|
|
|||
46
.github/workflows/release.yml
vendored
46
.github/workflows/release.yml
vendored
|
|
@ -1,7 +1,14 @@
|
|||
name: Release
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '5 5 * * *'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag_name:
|
||||
description: 'Tag name for release'
|
||||
required: false
|
||||
default: nightly
|
||||
push:
|
||||
tags:
|
||||
- v[0-9]+.[0-9]+.[0-9]+
|
||||
|
|
@ -21,11 +28,22 @@ jobs:
|
|||
attestations: write
|
||||
contents: write
|
||||
steps:
|
||||
- if: github.event_name == 'workflow_dispatch'
|
||||
env:
|
||||
TAG_NAME: ${{ github.event.inputs.tag_name }}
|
||||
run: echo "TAG_NAME=${TAG_NAME}" >> $GITHUB_ENV
|
||||
|
||||
- if: github.event_name == 'schedule'
|
||||
run: echo 'TAG_NAME=nightly' >> $GITHUB_ENV
|
||||
|
||||
- if: github.event_name == 'push'
|
||||
run: echo "TAG_NAME=${GITHUB_REF_NAME}" >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7.0.1
|
||||
|
||||
- name: Download build artifacts
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
path: artifacts
|
||||
|
||||
|
|
@ -44,28 +62,41 @@ jobs:
|
|||
|
||||
for platform in $(cd artifacts; ls | sed 's/^tree-sitter\.//'); do
|
||||
exe=$(ls artifacts/tree-sitter.$platform/tree-sitter*)
|
||||
chmod +x $exe
|
||||
gzip --stdout --name $exe > target/tree-sitter-$platform.gz
|
||||
zip -j9 target/tree-sitter-cli-$platform.zip $exe
|
||||
done
|
||||
rm -rf artifacts
|
||||
ls -l target/
|
||||
|
||||
- name: Generate attestations
|
||||
uses: actions/attest-build-provenance@v3
|
||||
uses: actions/attest-build-provenance@v4
|
||||
with:
|
||||
subject-path: |
|
||||
target/tree-sitter-*.gz
|
||||
target/tree-sitter-cli-*.zip
|
||||
target/web-tree-sitter.tar.gz
|
||||
|
||||
- if: env.TAG_NAME == 'nightly'
|
||||
run: |
|
||||
echo 'PRERELEASE=--prerelease' >> $GITHUB_ENV
|
||||
gh release delete nightly --yes || true
|
||||
git push https://${GITHUB_ACTOR}:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY} :nightly || true
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
|
||||
- name: Create release
|
||||
run: |-
|
||||
gh release create $GITHUB_REF_NAME \
|
||||
gh release create ${{ env.TAG_NAME }} $PRERELEASE \
|
||||
target/tree-sitter-*.gz \
|
||||
target/tree-sitter-cli-*.zip \
|
||||
target/web-tree-sitter.tar.gz
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
|
||||
crates_io:
|
||||
name: Publish packages to Crates.io
|
||||
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.tag_name != 'nightly')
|
||||
runs-on: ubuntu-latest
|
||||
environment: crates
|
||||
permissions:
|
||||
|
|
@ -74,7 +105,7 @@ jobs:
|
|||
needs: release
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7.0.1
|
||||
|
||||
- name: Set up Rust
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
|
|
@ -90,6 +121,7 @@ jobs:
|
|||
|
||||
npm:
|
||||
name: Publish packages to npmjs.com
|
||||
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.tag_name != 'nightly')
|
||||
runs-on: ubuntu-latest
|
||||
environment: npm
|
||||
permissions:
|
||||
|
|
@ -102,10 +134,10 @@ jobs:
|
|||
directory: [crates/cli/npm, lib/binding_web]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7.0.1
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v7.0.0
|
||||
with:
|
||||
node-version: 24
|
||||
registry-url: https://registry.npmjs.org
|
||||
|
|
|
|||
8
.github/workflows/response.yml
vendored
8
.github/workflows/response.yml
vendored
|
|
@ -17,13 +17,13 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout script
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7.0.1
|
||||
with:
|
||||
sparse-checkout: .github/scripts/close_unresponsive.js
|
||||
sparse-checkout-cone-mode: false
|
||||
|
||||
- name: Run script
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const script = require('./.github/scripts/close_unresponsive.js')
|
||||
|
|
@ -35,13 +35,13 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout script
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7.0.1
|
||||
with:
|
||||
sparse-checkout: .github/scripts/remove_response_label.js
|
||||
sparse-checkout-cone-mode: false
|
||||
|
||||
- name: Run script
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const script = require('./.github/scripts/remove_response_label.js')
|
||||
|
|
|
|||
26
.github/workflows/reviewers_remove.yml
vendored
26
.github/workflows/reviewers_remove.yml
vendored
|
|
@ -11,15 +11,21 @@ jobs:
|
|||
remove-reviewers:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout script
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
sparse-checkout: .github/scripts/reviewers_remove.js
|
||||
sparse-checkout-cone-mode: false
|
||||
|
||||
- name: Run script
|
||||
uses: actions/github-script@v8
|
||||
- name: Remove reviewers
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const script = require('./.github/scripts/reviewers_remove.js')
|
||||
await script({github, context})
|
||||
const requestedReviewers = await github.rest.pulls.listRequestedReviewers({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: context.issue.number,
|
||||
});
|
||||
|
||||
const reviewers = requestedReviewers.data.users.map((e) => e.login);
|
||||
|
||||
github.rest.pulls.removeRequestedReviewers({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: context.issue.number,
|
||||
reviewers: reviewers,
|
||||
});
|
||||
|
|
|
|||
2
.github/workflows/sanitize.yml
vendored
2
.github/workflows/sanitize.yml
vendored
|
|
@ -15,7 +15,7 @@ jobs:
|
|||
TREE_SITTER: ${{ github.workspace }}/target/release/tree-sitter
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7.0.1
|
||||
|
||||
- name: Install UBSAN library
|
||||
run: sudo apt-get update -y && sudo apt-get install -y libubsan1
|
||||
|
|
|
|||
4
.github/workflows/spam.yml
vendored
4
.github/workflows/spam.yml
vendored
|
|
@ -16,13 +16,13 @@ jobs:
|
|||
if: github.event.label.name == 'spam'
|
||||
steps:
|
||||
- name: Checkout script
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7.0.1
|
||||
with:
|
||||
sparse-checkout: .github/scripts/close_spam.js
|
||||
sparse-checkout-cone-mode: false
|
||||
|
||||
- name: Run script
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const script = require('./.github/scripts/close_spam.js')
|
||||
|
|
|
|||
4
.github/workflows/wasm_exports.yml
vendored
4
.github/workflows/wasm_exports.yml
vendored
|
|
@ -18,7 +18,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7.0.1
|
||||
|
||||
- name: Set up stable Rust toolchain
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
|
|
@ -31,7 +31,7 @@ jobs:
|
|||
- name: Build C library (make)
|
||||
run: make -j CFLAGS="$CFLAGS"
|
||||
env:
|
||||
CFLAGS: -g -Werror -Wall -Wextra -Wshadow -Wpedantic -Werror=incompatible-pointer-types
|
||||
CFLAGS: -g -Werror -Wall -Wextra -Wshadow -Wpedantic -Werror=incompatible-pointer-types -Werror=strict-aliasing -Wstrict-aliasing=2
|
||||
|
||||
- name: Build Wasm Library
|
||||
working-directory: lib/binding_web
|
||||
|
|
|
|||
19
.github/workflows/wasm_stdlib.yml
vendored
Normal file
19
.github/workflows/wasm_stdlib.yml
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
name: Check Wasm Stdlib build
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v7.0.1
|
||||
|
||||
- name: Check directory changes
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const scriptPath = `${process.env.GITHUB_WORKSPACE}/.github/scripts/wasm_stdlib.js`;
|
||||
const script = require(scriptPath);
|
||||
return script({ github, context, core });
|
||||
19
.taplo.toml
Normal file
19
.taplo.toml
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
[formatting]
|
||||
column_width = 100
|
||||
compact_arrays = false
|
||||
reorder_inline_tables = true
|
||||
reorder_keys = true
|
||||
|
||||
[[rule]]
|
||||
include = [ "**/Cargo.toml" ]
|
||||
keys = [ "package" ]
|
||||
|
||||
[rule.formatting]
|
||||
reorder_keys = false
|
||||
|
||||
[[rule]]
|
||||
include = [ "**/Cargo.toml" ]
|
||||
keys = [ "profile" ]
|
||||
|
||||
[rule.formatting]
|
||||
reorder_keys = false
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
{
|
||||
"lsp": {
|
||||
"rust-analyzer": {
|
||||
"initialization_options": {
|
||||
"cargo": {
|
||||
"features": "all"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
cmake_minimum_required(VERSION 3.13)
|
||||
|
||||
project(tree-sitter
|
||||
VERSION "0.26.6"
|
||||
VERSION "0.28.0"
|
||||
DESCRIPTION "An incremental parsing system for programming tools"
|
||||
HOMEPAGE_URL "https://tree-sitter.github.io/tree-sitter/"
|
||||
LANGUAGES C)
|
||||
|
|
@ -33,7 +33,8 @@ if(MSVC)
|
|||
else()
|
||||
target_compile_options(tree-sitter PRIVATE
|
||||
-Wall -Wextra -Wshadow -Wpedantic
|
||||
-Werror=incompatible-pointer-types)
|
||||
-Werror=incompatible-pointer-types
|
||||
-Werror=strict-aliasing -Wstrict-aliasing=2)
|
||||
endif()
|
||||
|
||||
if(TREE_SITTER_FEATURE_WASM)
|
||||
|
|
|
|||
BIN
Cargo.lock
generated
BIN
Cargo.lock
generated
Binary file not shown.
127
Cargo.toml
127
Cargo.toml
|
|
@ -1,5 +1,5 @@
|
|||
[workspace]
|
||||
default-members = ["crates/cli"]
|
||||
default-members = [ "crates/cli" ]
|
||||
members = [
|
||||
"crates/cli",
|
||||
"crates/config",
|
||||
|
|
@ -14,25 +14,22 @@ members = [
|
|||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
version = "0.26.6"
|
||||
authors = [
|
||||
"Max Brunsfeld <maxbrunsfeld@gmail.com>",
|
||||
"Amaan Qureshi <amaanq12@gmail.com>",
|
||||
]
|
||||
edition = "2021"
|
||||
rust-version = "1.84"
|
||||
authors = [ "Max Brunsfeld <maxbrunsfeld@gmail.com>", "Amaan Qureshi <amaanq12@gmail.com>" ]
|
||||
categories = [ "command-line-utilities", "parsing" ]
|
||||
edition = "2024"
|
||||
homepage = "https://tree-sitter.github.io/tree-sitter"
|
||||
repository = "https://github.com/tree-sitter/tree-sitter"
|
||||
keywords = [ "incremental", "parsing" ]
|
||||
license = "MIT"
|
||||
keywords = ["incremental", "parsing"]
|
||||
categories = ["command-line-utilities", "parsing"]
|
||||
repository = "https://github.com/tree-sitter/tree-sitter"
|
||||
rust-version = "1.90"
|
||||
version = "0.28.0"
|
||||
|
||||
[workspace.lints.clippy]
|
||||
dbg_macro = "deny"
|
||||
todo = "deny"
|
||||
pedantic = { level = "warn", priority = -1 }
|
||||
nursery = { level = "warn", priority = -1 }
|
||||
cargo = { level = "warn", priority = -1 }
|
||||
dbg_macro = "deny"
|
||||
nursery = { level = "warn", priority = -1 }
|
||||
pedantic = { level = "warn", priority = -1 }
|
||||
todo = "deny"
|
||||
|
||||
# The lints below are a specific subset of the pedantic+nursery lints
|
||||
# that we explicitly allow in the tree-sitter codebase because they either:
|
||||
|
|
@ -41,52 +38,31 @@ cargo = { level = "warn", priority = -1 }
|
|||
# 2. Are unnecessary, or
|
||||
# 3. Worsen the code
|
||||
|
||||
branches_sharing_code = "allow"
|
||||
cast_lossless = "allow"
|
||||
cast_possible_truncation = "allow"
|
||||
cast_possible_wrap = "allow"
|
||||
cast_precision_loss = "allow"
|
||||
cast_sign_loss = "allow"
|
||||
checked_conversions = "allow"
|
||||
cognitive_complexity = "allow"
|
||||
collection_is_never_read = "allow"
|
||||
fallible_impl_from = "allow"
|
||||
fn_params_excessive_bools = "allow"
|
||||
inline_always = "allow"
|
||||
if_not_else = "allow"
|
||||
items_after_statements = "allow"
|
||||
match_wildcard_for_single_variants = "allow"
|
||||
missing_errors_doc = "allow"
|
||||
missing_panics_doc = "allow"
|
||||
module_name_repetitions = "allow"
|
||||
multiple_crate_versions = "allow"
|
||||
needless_for_each = "allow"
|
||||
obfuscated_if_else = "allow"
|
||||
option_if_let_else = "allow"
|
||||
or_fun_call = "allow"
|
||||
range_plus_one = "allow"
|
||||
redundant_clone = "allow"
|
||||
redundant_closure_for_method_calls = "allow"
|
||||
ref_option = "allow"
|
||||
similar_names = "allow"
|
||||
string_lit_as_bytes = "allow"
|
||||
struct_excessive_bools = "allow"
|
||||
struct_field_names = "allow"
|
||||
transmute_undefined_repr = "allow"
|
||||
too_many_lines = "allow"
|
||||
unnecessary_wraps = "allow"
|
||||
unused_self = "allow"
|
||||
used_underscore_items = "allow"
|
||||
tuple_array_conversions = "allow"
|
||||
|
||||
[workspace.lints.rust]
|
||||
mismatched_lifetime_syntaxes = "allow"
|
||||
|
||||
[profile.optimize]
|
||||
inherits = "release"
|
||||
strip = true # Automatically strip symbols from the binary.
|
||||
codegen-units = 1 # Maximum size reduction optimizations.
|
||||
lto = true # Link-time optimization.
|
||||
opt-level = 3 # Optimization level 3.
|
||||
codegen-units = 1 # Maximum size reduction optimizations.
|
||||
strip = true # Automatically strip symbols from the binary.
|
||||
|
||||
[profile.size]
|
||||
inherits = "optimize"
|
||||
|
|
@ -94,70 +70,69 @@ opt-level = "s" # Optimize for size.
|
|||
|
||||
[profile.release-dev]
|
||||
inherits = "release"
|
||||
lto = false
|
||||
codegen-units = 256
|
||||
debug = true
|
||||
debug-assertions = true
|
||||
overflow-checks = true
|
||||
incremental = true
|
||||
codegen-units = 256
|
||||
lto = false
|
||||
overflow-checks = true
|
||||
|
||||
[workspace.dependencies]
|
||||
ansi_colours = "1.2.3"
|
||||
anstyle = "1.0.13"
|
||||
anyhow = "1.0.100"
|
||||
bstr = "1.12.0"
|
||||
cc = "1.2.48"
|
||||
clap = { version = "4.5.53", features = [
|
||||
anstyle = "1.0.14"
|
||||
anyhow = "1.0.102"
|
||||
bstr = "1.12.1"
|
||||
cc = "1.2.63"
|
||||
clap = { features = [
|
||||
"cargo",
|
||||
"derive",
|
||||
"env",
|
||||
"help",
|
||||
"string",
|
||||
"unstable-styles",
|
||||
] }
|
||||
clap_complete = "4.5.61"
|
||||
], version = "4.5.58" }
|
||||
clap_complete = "4.6.3"
|
||||
clap_complete_nushell = "4.5.10"
|
||||
crc32fast = "1.5.0"
|
||||
ctor = "0.2.9"
|
||||
ctrlc = { version = "3.5.0", features = ["termination"] }
|
||||
dialoguer = { version = "0.11.0", features = ["fuzzy-select"] }
|
||||
ctor = "0.6.3"
|
||||
ctrlc = { features = [ "termination" ], version = "3.5.2" }
|
||||
dialoguer = { features = [ "fuzzy-select" ], version = "0.12.0" }
|
||||
etcetera = "0.11.0"
|
||||
fs4 = "0.12.0"
|
||||
glob = "0.3.3"
|
||||
hashbrown = { default-features = false, version = "0.17.1" }
|
||||
heck = "0.5.0"
|
||||
html-escape = "0.2.13"
|
||||
indexmap = "2.12.1"
|
||||
indoc = "2.0.6"
|
||||
indexmap = "2.13.0"
|
||||
indoc = "2.0.7"
|
||||
libloading = "0.9.0"
|
||||
log = { version = "0.4.28", features = ["std"] }
|
||||
memchr = "2.7.6"
|
||||
log = { features = [ "std" ], version = "0.4.30" }
|
||||
memchr = "2.8.1"
|
||||
once_cell = "1.21.3"
|
||||
pretty_assertions = "1.4.1"
|
||||
rand = "0.8.5"
|
||||
regex = "1.11.3"
|
||||
regex-syntax = "0.8.6"
|
||||
rand = "0.10.1"
|
||||
regex = "1.12.3"
|
||||
regex-syntax = "0.8.9"
|
||||
rustc-hash = "2.1.1"
|
||||
schemars = "1.0.5"
|
||||
semver = { version = "1.0.27", features = ["serde"] }
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
serde_json = { version = "1.0.145", features = ["preserve_order"] }
|
||||
schemars = "1.2.1"
|
||||
semver = { features = [ "serde" ], version = "1.0.27" }
|
||||
serde = { features = [ "derive" ], version = "1.0.228" }
|
||||
serde_json = { features = [ "preserve_order" ], version = "1.0.150" }
|
||||
similar = "2.7.0"
|
||||
smallbitvec = "2.6.0"
|
||||
streaming-iterator = "0.1.9"
|
||||
tempfile = "3.23.0"
|
||||
thiserror = "2.0.17"
|
||||
tempfile = "3.25.0"
|
||||
thiserror = "2.0.18"
|
||||
tiny_http = "0.12.0"
|
||||
topological-sort = "0.2.2"
|
||||
unindent = "0.2.4"
|
||||
walkdir = "2.5.0"
|
||||
wasmparser = "0.243.0"
|
||||
webbrowser = "1.0.5"
|
||||
wasmparser = "0.244.0"
|
||||
webbrowser = "1.2.1"
|
||||
|
||||
tree-sitter = { version = "0.26.6", path = "./lib" }
|
||||
tree-sitter-generate = { version = "0.26.6", path = "./crates/generate" }
|
||||
tree-sitter-loader = { version = "0.26.6", path = "./crates/loader" }
|
||||
tree-sitter-config = { version = "0.26.6", path = "./crates/config" }
|
||||
tree-sitter-highlight = { version = "0.26.6", path = "./crates/highlight" }
|
||||
tree-sitter-tags = { version = "0.26.6", path = "./crates/tags" }
|
||||
tree-sitter = { path = "./lib", version = "0.28.0" }
|
||||
tree-sitter-config = { path = "./crates/config", version = "0.28.0" }
|
||||
tree-sitter-generate = { default-features = false, path = "./crates/generate", version = "0.28.0" }
|
||||
tree-sitter-highlight = { path = "./crates/highlight", version = "0.28.0" }
|
||||
tree-sitter-loader = { path = "./crates/loader", version = "0.28.0" }
|
||||
tree-sitter-tags = { path = "./crates/tags", version = "0.28.0" }
|
||||
|
||||
tree-sitter-language = { version = "0.1", path = "./crates/language" }
|
||||
tree-sitter-language = { path = "./crates/language", version = "0.1.8" }
|
||||
|
|
|
|||
10
Dockerfile
10
Dockerfile
|
|
@ -1,10 +0,0 @@
|
|||
FROM rust:1.76-buster
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y nodejs
|
||||
|
||||
COPY . .
|
||||
|
||||
CMD cargo test --all-features
|
||||
9
Makefile
9
Makefile
|
|
@ -1,4 +1,4 @@
|
|||
VERSION := 0.26.6
|
||||
VERSION := 0.28.0
|
||||
DESCRIPTION := An incremental parsing system for programming tools
|
||||
HOMEPAGE_URL := https://tree-sitter.github.io/tree-sitter/
|
||||
|
||||
|
|
@ -22,7 +22,7 @@ OBJ := $(SRC:.c=.o)
|
|||
|
||||
# define default flags, and override to append mandatory flags
|
||||
ARFLAGS := rcs
|
||||
CFLAGS ?= -O3 -Wall -Wextra -Wshadow -Wpedantic -Werror=incompatible-pointer-types
|
||||
CFLAGS ?= -O3 -Wall -Wextra -Wshadow -Wpedantic -Werror=incompatible-pointer-types -Werror=strict-aliasing -Wstrict-aliasing=2
|
||||
override CFLAGS += -std=c11 -fPIC -fvisibility=hidden
|
||||
override CFLAGS += -D_POSIX_C_SOURCE=200112L -D_DEFAULT_SOURCE -D_BSD_SOURCE -D_DARWIN_C_SOURCE
|
||||
override CFLAGS += -Ilib/src -Ilib/src/wasm -Ilib/include
|
||||
|
|
@ -129,8 +129,13 @@ lint-web:
|
|||
npm --prefix lib/binding_web ci
|
||||
npm --prefix lib/binding_web run lint
|
||||
|
||||
lint-toml:
|
||||
taplo check
|
||||
taplo format --check --diff
|
||||
|
||||
format:
|
||||
cargo fmt --all
|
||||
taplo format
|
||||
|
||||
changelog:
|
||||
@git-cliff --config .github/cliff.toml --prepend CHANGELOG.md --latest --github-token $(shell gh auth token)
|
||||
|
|
|
|||
|
|
@ -1,35 +0,0 @@
|
|||
// swift-tools-version: 5.8
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "TreeSitter",
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||
.library(
|
||||
name: "TreeSitter",
|
||||
targets: ["TreeSitter"]),
|
||||
],
|
||||
targets: [
|
||||
.target(name: "TreeSitter",
|
||||
path: "lib",
|
||||
exclude: [
|
||||
"src/unicode/ICU_SHA",
|
||||
"src/unicode/README.md",
|
||||
"src/unicode/LICENSE",
|
||||
"src/wasm/stdlib-symbols.txt",
|
||||
"src/lib.c",
|
||||
],
|
||||
sources: ["src"],
|
||||
publicHeadersPath: "include",
|
||||
cSettings: [
|
||||
.headerSearchPath("src"),
|
||||
.define("_POSIX_C_SOURCE", to: "200112L"),
|
||||
.define("_DEFAULT_SOURCE"),
|
||||
.define("_BSD_SOURCE"),
|
||||
.define("_DARWIN_C_SOURCE"),
|
||||
]),
|
||||
],
|
||||
cLanguageStandard: .c11
|
||||
)
|
||||
53
build.zig
53
build.zig
|
|
@ -4,51 +4,56 @@ pub fn build(b: *std.Build) !void {
|
|||
const target = b.standardTargetOptions(.{});
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
|
||||
var threaded: std.Io.Threaded = .init(b.allocator, .{});
|
||||
defer threaded.deinit();
|
||||
const io = threaded.io();
|
||||
|
||||
const wasm = b.option(bool, "enable-wasm", "Enable Wasm support") orelse false;
|
||||
const shared = b.option(bool, "build-shared", "Build a shared library") orelse false;
|
||||
const amalgamated = b.option(bool, "amalgamated", "Build using an amalgamated source") orelse false;
|
||||
|
||||
var tree_sitter = b.createModule(.{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.link_libc = true,
|
||||
.pic = if (shared) true else null,
|
||||
});
|
||||
const lib: *std.Build.Step.Compile = b.addLibrary(.{
|
||||
.name = "tree-sitter",
|
||||
.linkage = if (shared) .dynamic else .static,
|
||||
.root_module = b.createModule(.{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.link_libc = true,
|
||||
.pic = if (shared) true else null,
|
||||
}),
|
||||
.root_module = tree_sitter,
|
||||
});
|
||||
|
||||
if (amalgamated) {
|
||||
lib.addCSourceFile(.{
|
||||
tree_sitter.addCSourceFile(.{
|
||||
.file = b.path("lib/src/lib.c"),
|
||||
.flags = &.{"-std=c11"},
|
||||
});
|
||||
} else {
|
||||
const files = try findSourceFiles(b);
|
||||
const files = try findSourceFiles(b, io);
|
||||
defer b.allocator.free(files);
|
||||
lib.addCSourceFiles(.{
|
||||
tree_sitter.addCSourceFiles(.{
|
||||
.root = b.path("lib/src"),
|
||||
.files = files,
|
||||
.flags = &.{"-std=c11"},
|
||||
});
|
||||
}
|
||||
|
||||
lib.addIncludePath(b.path("lib/include"));
|
||||
lib.addIncludePath(b.path("lib/src"));
|
||||
lib.addIncludePath(b.path("lib/src/wasm"));
|
||||
tree_sitter.addIncludePath(b.path("lib/include"));
|
||||
tree_sitter.addIncludePath(b.path("lib/src"));
|
||||
tree_sitter.addIncludePath(b.path("lib/src/wasm"));
|
||||
|
||||
lib.root_module.addCMacro("_POSIX_C_SOURCE", "200112L");
|
||||
lib.root_module.addCMacro("_DEFAULT_SOURCE", "");
|
||||
lib.root_module.addCMacro("_BSD_SOURCE", "");
|
||||
lib.root_module.addCMacro("_DARWIN_C_SOURCE", "");
|
||||
tree_sitter.addCMacro("_POSIX_C_SOURCE", "200112L");
|
||||
tree_sitter.addCMacro("_DEFAULT_SOURCE", "");
|
||||
tree_sitter.addCMacro("_BSD_SOURCE", "");
|
||||
tree_sitter.addCMacro("_DARWIN_C_SOURCE", "");
|
||||
|
||||
if (wasm) {
|
||||
if (b.lazyDependency(wasmtimeDep(target.result), .{})) |wasmtime| {
|
||||
lib.root_module.addCMacro("TREE_SITTER_FEATURE_WASM", "");
|
||||
lib.addSystemIncludePath(wasmtime.path("include"));
|
||||
lib.addLibraryPath(wasmtime.path("lib"));
|
||||
if (shared) lib.linkSystemLibrary("wasmtime");
|
||||
tree_sitter.addCMacro("TREE_SITTER_FEATURE_WASM", "");
|
||||
tree_sitter.addSystemIncludePath(wasmtime.path("include"));
|
||||
tree_sitter.addLibraryPath(wasmtime.path("lib"));
|
||||
if (shared) tree_sitter.linkSystemLibrary("wasmtime", .{});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -122,14 +127,14 @@ pub fn wasmtimeDep(target: std.Target) []const u8 {
|
|||
);
|
||||
}
|
||||
|
||||
fn findSourceFiles(b: *std.Build) ![]const []const u8 {
|
||||
fn findSourceFiles(b: *std.Build, io: std.Io) ![]const []const u8 {
|
||||
var sources: std.ArrayListUnmanaged([]const u8) = .empty;
|
||||
|
||||
var dir = try b.build_root.handle.openDir("lib/src", .{ .iterate = true });
|
||||
var dir = try b.build_root.handle.openDir(io, "lib/src", .{ .iterate = true });
|
||||
var iter = dir.iterate();
|
||||
defer dir.close();
|
||||
defer dir.close(io);
|
||||
|
||||
while (try iter.next()) |entry| {
|
||||
while (try iter.next(io)) |entry| {
|
||||
if (entry.kind != .file) continue;
|
||||
const file = entry.name;
|
||||
const ext = std.fs.path.extension(file);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
.{
|
||||
.name = .tree_sitter,
|
||||
.fingerprint = 0x841224b447ac0d4f,
|
||||
.version = "0.26.6",
|
||||
.minimum_zig_version = "0.14.1",
|
||||
.version = "0.28.0",
|
||||
.minimum_zig_version = "0.16.0",
|
||||
.paths = .{
|
||||
"build.zig",
|
||||
"build.zig.zon",
|
||||
|
|
@ -13,83 +13,83 @@
|
|||
},
|
||||
.dependencies = .{
|
||||
.wasmtime_c_api_aarch64_android = .{
|
||||
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v33.0.2/wasmtime-v33.0.2-aarch64-android-c-api.tar.xz",
|
||||
.hash = "N-V-__8AAIfPIgdw2YnV3QyiFQ2NHdrxrXzzCdjYJyxJDOta",
|
||||
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-aarch64-android-c-api.tar.xz",
|
||||
.hash = "N-V-__8AAIp_mQVzQOITXcYcWxYLJkvB1W1SvLlrdiU2G7fj",
|
||||
.lazy = true,
|
||||
},
|
||||
.wasmtime_c_api_aarch64_linux = .{
|
||||
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v33.0.2/wasmtime-v33.0.2-aarch64-linux-c-api.tar.xz",
|
||||
.hash = "N-V-__8AAIt97QZi7Pf7nNJ2mVY6uxA80Klyuvvtop3pLMRK",
|
||||
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-aarch64-linux-c-api.tar.xz",
|
||||
.hash = "N-V-__8AAMztsgU5Aj4oI3MRHXJVe5rW72op-kT_78I3kZVM",
|
||||
.lazy = true,
|
||||
},
|
||||
.wasmtime_c_api_aarch64_macos = .{
|
||||
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v33.0.2/wasmtime-v33.0.2-aarch64-macos-c-api.tar.xz",
|
||||
.hash = "N-V-__8AAAO48QQf91w9RmmUDHTja8DrXZA1n6Bmc8waW3qe",
|
||||
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-aarch64-macos-c-api.tar.xz",
|
||||
.hash = "N-V-__8AANZxOwT27sdrKxDDGGKsiwtcZlHy204xAWNIgDBH",
|
||||
.lazy = true,
|
||||
},
|
||||
.wasmtime_c_api_aarch64_musl = .{
|
||||
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v33.0.2/wasmtime-v33.0.2-aarch64-musl-c-api.tar.xz",
|
||||
.hash = "N-V-__8AAI196wa9pwADoA2RbCDp5F7bKQg1iOPq6gIh8-FH",
|
||||
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-aarch64-musl-c-api.tar.xz",
|
||||
.hash = "N-V-__8AAJL1zQW9yxC98uc60lSuVUHhH77QHTto0zwIQnBj",
|
||||
.lazy = true,
|
||||
},
|
||||
.wasmtime_c_api_aarch64_windows = .{
|
||||
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v33.0.2/wasmtime-v33.0.2-aarch64-windows-c-api.zip",
|
||||
.hash = "N-V-__8AAC9u4wXfqd1Q6XyQaC8_DbQZClXux60Vu5743N05",
|
||||
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-aarch64-windows-c-api.zip",
|
||||
.hash = "N-V-__8AAHRCtQU93hJcRFOgVcof3IQpRV9stT2Pp54wpJc2",
|
||||
.lazy = true,
|
||||
},
|
||||
.wasmtime_c_api_armv7_linux = .{
|
||||
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v33.0.2/wasmtime-v33.0.2-armv7-linux-c-api.tar.xz",
|
||||
.hash = "N-V-__8AAHXe8gWs3s83Cc5G6SIq0_jWxj8fGTT5xG4vb6-x",
|
||||
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-armv7-linux-c-api.tar.xz",
|
||||
.hash = "N-V-__8AAJaW6gT8QdULOU0jxX4a_DOCA5YD6cxWBC8IqhQF",
|
||||
.lazy = true,
|
||||
},
|
||||
.wasmtime_c_api_i686_linux = .{
|
||||
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v33.0.2/wasmtime-v33.0.2-i686-linux-c-api.tar.xz",
|
||||
.hash = "N-V-__8AAN2pzgUUfulRCYnipSfis9IIYHoTHVlieLRmKuct",
|
||||
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-i686-linux-c-api.tar.xz",
|
||||
.hash = "N-V-__8AANguMgVX4XMhdOVkdj4yfFKXrG8RTgZDs3nQB8J8",
|
||||
.lazy = true,
|
||||
},
|
||||
.wasmtime_c_api_i686_windows = .{
|
||||
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v33.0.2/wasmtime-v33.0.2-i686-windows-c-api.zip",
|
||||
.hash = "N-V-__8AAJu0YAUUTFBLxFIOi-MSQVezA6MMkpoFtuaf2Quf",
|
||||
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-i686-windows-c-api.zip",
|
||||
.hash = "N-V-__8AANY9ggXg4rK2_1o3EIlrCq124l5RfykPv-DPforq",
|
||||
.lazy = true,
|
||||
},
|
||||
.wasmtime_c_api_riscv64gc_linux = .{
|
||||
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v33.0.2/wasmtime-v33.0.2-riscv64gc-linux-c-api.tar.xz",
|
||||
.hash = "N-V-__8AAG8m-gc3E3AIImtTZ3l1c7HC6HUWazQ9OH5KACX4",
|
||||
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-riscv64gc-linux-c-api.tar.xz",
|
||||
.hash = "N-V-__8AAPDtCAdQ0dD9Rs-qWl-kPr2c7L3PVsUVjwy12Iz1",
|
||||
.lazy = true,
|
||||
},
|
||||
.wasmtime_c_api_s390x_linux = .{
|
||||
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v33.0.2/wasmtime-v33.0.2-s390x-linux-c-api.tar.xz",
|
||||
.hash = "N-V-__8AAH314gd-gE4IBp2uvAL3gHeuW1uUZjMiLLeUdXL_",
|
||||
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-s390x-linux-c-api.tar.xz",
|
||||
.hash = "N-V-__8AANA3BwY1ZOoGCWCR_tTY9G1vfIX128RGVxufN3ov",
|
||||
.lazy = true,
|
||||
},
|
||||
.wasmtime_c_api_x86_64_android = .{
|
||||
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v33.0.2/wasmtime-v33.0.2-x86_64-android-c-api.tar.xz",
|
||||
.hash = "N-V-__8AAIPNRwfNkznebrcGb0IKUe7f35bkuZEYOjcx6q3f",
|
||||
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-x86_64-android-c-api.tar.xz",
|
||||
.hash = "N-V-__8AAF4AIgY0ltjevj1ybGfvMU1ErPRnNve5X1TmvCru",
|
||||
.lazy = true,
|
||||
},
|
||||
.wasmtime_c_api_x86_64_linux = .{
|
||||
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v33.0.2/wasmtime-v33.0.2-x86_64-linux-c-api.tar.xz",
|
||||
.hash = "N-V-__8AAI8EDwcyTtk_Afhk47SEaqfpoRqGkJeZpGs69ChF",
|
||||
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-x86_64-linux-c-api.tar.xz",
|
||||
.hash = "N-V-__8AAIR0cAbjf3DkrTbu81Oq_zociz-0lCpb5DR0lIC9",
|
||||
.lazy = true,
|
||||
},
|
||||
.wasmtime_c_api_x86_64_macos = .{
|
||||
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v33.0.2/wasmtime-v33.0.2-x86_64-macos-c-api.tar.xz",
|
||||
.hash = "N-V-__8AAGtGNgVaOpHSxC22IjrampbRIy6lLwscdcAE8nG1",
|
||||
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-x86_64-macos-c-api.tar.xz",
|
||||
.hash = "N-V-__8AAFJ4lgRgCBnYdz8-Yfc4hLve45Hv-0RICAEuCp4s",
|
||||
.lazy = true,
|
||||
},
|
||||
.wasmtime_c_api_x86_64_mingw = .{
|
||||
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v33.0.2/wasmtime-v33.0.2-x86_64-mingw-c-api.zip",
|
||||
.hash = "N-V-__8AAPS2PAbVix50L6lnddlgazCPTz3whLUFk1qnRtnZ",
|
||||
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-x86_64-mingw-c-api.zip",
|
||||
.hash = "N-V-__8AAMxZxQZUpp1cU8J5zgLiMNq4e4dy0hcchlQFy03J",
|
||||
.lazy = true,
|
||||
},
|
||||
.wasmtime_c_api_x86_64_musl = .{
|
||||
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v33.0.2/wasmtime-v33.0.2-x86_64-musl-c-api.tar.xz",
|
||||
.hash = "N-V-__8AAF-WEQe0nzvi09PgusM5i46FIuCKJmIDWUleWgQ3",
|
||||
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-x86_64-musl-c-api.tar.xz",
|
||||
.hash = "N-V-__8AAN5pWgZrZBt8VYWkN82WjyFe3DkGcE7uLn3jpt38",
|
||||
.lazy = true,
|
||||
},
|
||||
.wasmtime_c_api_x86_64_windows = .{
|
||||
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v33.0.2/wasmtime-v33.0.2-x86_64-windows-c-api.zip",
|
||||
.hash = "N-V-__8AAKGNXwbpJQsn0_6kwSIVDDWifSg8cBzf7T2RzsC9",
|
||||
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-x86_64-windows-c-api.zip",
|
||||
.hash = "N-V-__8AAEIJkgaVHFETgakbognNUpFELuV17vpjw6NjsWhQ",
|
||||
.lazy = true,
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -5,14 +5,13 @@ description = "CLI tool for developing, testing, and using Tree-sitter parsers"
|
|||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
readme = "README.md"
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
documentation = "https://docs.rs/tree-sitter-cli"
|
||||
license.workspace = true
|
||||
keywords.workspace = true
|
||||
categories.workspace = true
|
||||
include = ["build.rs", "README.md", "LICENSE", "benches/*", "src/**"]
|
||||
include = [ "build.rs", "README.md", "LICENSE", "benches/*", "src/**" ]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
|
@ -21,18 +20,18 @@ workspace = true
|
|||
path = "src/tree_sitter_cli.rs"
|
||||
|
||||
[[bin]]
|
||||
doc = false
|
||||
name = "tree-sitter"
|
||||
path = "src/main.rs"
|
||||
doc = false
|
||||
|
||||
[[bench]]
|
||||
name = "benchmark"
|
||||
harness = false
|
||||
name = "benchmark"
|
||||
|
||||
[features]
|
||||
default = ["qjs-rt"]
|
||||
wasm = ["tree-sitter/wasm", "tree-sitter-loader/wasm"]
|
||||
qjs-rt = ["tree-sitter-generate/qjs-rt"]
|
||||
default = [ "qjs-rt" ]
|
||||
qjs-rt = [ "tree-sitter-generate/qjs-rt" ]
|
||||
wasm = [ "tree-sitter/wasm", "tree-sitter-loader/wasm" ]
|
||||
|
||||
[dependencies]
|
||||
ansi_colours.workspace = true
|
||||
|
|
@ -67,17 +66,50 @@ wasmparser.workspace = true
|
|||
webbrowser.workspace = true
|
||||
|
||||
tree-sitter.workspace = true
|
||||
tree-sitter-generate.workspace = true
|
||||
tree-sitter-config.workspace = true
|
||||
tree-sitter-generate = { features = [ "load" ], workspace = true }
|
||||
tree-sitter-highlight.workspace = true
|
||||
tree-sitter-loader.workspace = true
|
||||
tree-sitter-tags.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
encoding_rs = "0.8.35"
|
||||
tree_sitter_proc_macro = { package = "tree-sitter-tests-proc-macro", path = "src/tests/proc_macro" }
|
||||
widestring = "1.2.1"
|
||||
tree_sitter_proc_macro = { path = "src/tests/proc_macro", package = "tree-sitter-tests-proc-macro" }
|
||||
|
||||
tempfile.workspace = true
|
||||
pretty_assertions.workspace = true
|
||||
tempfile.workspace = true
|
||||
unindent.workspace = true
|
||||
|
||||
[package.metadata.binstall]
|
||||
pkg-fmt = "zip"
|
||||
|
||||
[package.metadata.binstall.overrides.aarch64-unknown-linux-gnu]
|
||||
pkg-url = "{ repo }/releases/download/v{ version }/{ name }-linux-arm64{ archive-suffix }"
|
||||
|
||||
[package.metadata.binstall.overrides.armv7-unknown-linux-gnueabihf]
|
||||
pkg-url = "{ repo }/releases/download/v{ version }/{ name }-linux-arm{ archive-suffix }"
|
||||
|
||||
[package.metadata.binstall.overrides.x86_64-unknown-linux-gnu]
|
||||
pkg-url = "{ repo }/releases/download/v{ version }/{ name }-linux-x64{ archive-suffix }"
|
||||
|
||||
[package.metadata.binstall.overrides.i686-unknown-linux-gnu]
|
||||
pkg-url = "{ repo }/releases/download/v{ version }/{ name }-linux-x86{ archive-suffix }"
|
||||
|
||||
[package.metadata.binstall.overrides.powerpc64-unknown-linux-gnu]
|
||||
pkg-url = "{ repo }/releases/download/v{ version }/{ name }-linux-powerpc64{ archive-suffix }"
|
||||
|
||||
[package.metadata.binstall.overrides.aarch64-pc-windows-msvc]
|
||||
pkg-url = "{ repo }/releases/download/v{ version }/{ name }-windows-arm64{ archive-suffix }"
|
||||
|
||||
[package.metadata.binstall.overrides.x86_64-pc-windows-msvc]
|
||||
pkg-url = "{ repo }/releases/download/v{ version }/{ name }-windows-x64{ archive-suffix }"
|
||||
|
||||
[package.metadata.binstall.overrides.i686-pc-windows-msvc]
|
||||
pkg-url = "{ repo }/releases/download/v{ version }/{ name }-windows-x86{ archive-suffix }"
|
||||
|
||||
[package.metadata.binstall.overrides.aarch64-apple-darwin]
|
||||
pkg-url = "{ repo }/releases/download/v{ version }/{ name }-macos-arm64{ archive-suffix }"
|
||||
|
||||
[package.metadata.binstall.overrides.x86_64-apple-darwin]
|
||||
pkg-url = "{ repo }/releases/download/v{ version }/{ name }-macos-x64{ archive-suffix }"
|
||||
|
|
|
|||
|
|
@ -7,20 +7,20 @@
|
|||
[npmjs.com]: https://www.npmjs.org/package/tree-sitter-cli
|
||||
[npmjs.com badge]: https://img.shields.io/npm/v/tree-sitter-cli.svg?color=%23BF4A4A
|
||||
|
||||
The Tree-sitter CLI allows you to develop, test, and use Tree-sitter grammars from the command line. It works on `MacOS`, `Linux`, and `Windows`.
|
||||
The Tree-sitter CLI allows you to develop, test, and use Tree-sitter grammars from the command line. It works on `MacOS`,
|
||||
`Linux`, and `Windows`.
|
||||
|
||||
### Installation
|
||||
|
||||
You can install the `tree-sitter-cli` with `cargo`:
|
||||
You can install the `tree-sitter-cli` with [`cargo-binstall`](https://github.com/cargo-bins/cargo-binstall):
|
||||
|
||||
```sh
|
||||
cargo install --locked tree-sitter-cli
|
||||
cargo binstall tree-sitter-cli
|
||||
```
|
||||
|
||||
or with `npm`:
|
||||
|
||||
or you can build it from source:
|
||||
```sh
|
||||
npm install tree-sitter-cli
|
||||
cargo install --locked tree-sitter-cli
|
||||
```
|
||||
|
||||
You can also download a pre-built binary for your platform from [the releases page].
|
||||
|
|
@ -34,9 +34,11 @@ The `tree-sitter` binary itself has no dependencies, but specific commands have
|
|||
|
||||
### Commands
|
||||
|
||||
* `generate` - The `tree-sitter generate` command will generate a Tree-sitter parser based on the grammar in the current working directory. See [the documentation] for more information.
|
||||
* `generate` - The `tree-sitter generate` command will generate a Tree-sitter parser based on the grammar in the current
|
||||
working directory. See [the documentation] for more information.
|
||||
|
||||
* `test` - The `tree-sitter test` command will run the unit tests for the Tree-sitter parser in the current working directory. See [the documentation] for more information.
|
||||
* `test` - The `tree-sitter test` command will run the unit tests for the Tree-sitter parser in the current working directory.
|
||||
See [the documentation] for more information.
|
||||
|
||||
* `parse` - The `tree-sitter parse` command will parse a file (or list of files) using Tree-sitter parsers.
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ use std::{
|
|||
collections::BTreeMap,
|
||||
env, fs,
|
||||
path::{Path, PathBuf},
|
||||
str,
|
||||
sync::LazyLock,
|
||||
time::Instant,
|
||||
};
|
||||
|
|
@ -10,6 +9,8 @@ use std::{
|
|||
use anyhow::Context;
|
||||
use log::info;
|
||||
use tree_sitter::{Language, Parser, Query};
|
||||
#[cfg(feature = "wasm")]
|
||||
use tree_sitter::{WasmStore, wasmtime};
|
||||
use tree_sitter_loader::{CompileConfig, Loader};
|
||||
|
||||
include!("../src/tests/helpers/dirs.rs");
|
||||
|
|
@ -21,10 +22,16 @@ static EXAMPLE_FILTER: LazyLock<Option<String>> =
|
|||
static REPETITION_COUNT: LazyLock<usize> = LazyLock::new(|| {
|
||||
env::var("TREE_SITTER_BENCHMARK_REPETITION_COUNT").map_or(5, |s| s.parse::<usize>().unwrap())
|
||||
});
|
||||
static WASM: LazyLock<bool> = LazyLock::new(|| env::var_os("TREE_SITTER_BENCHMARK_WASM").is_some());
|
||||
static TEST_LOADER: LazyLock<Loader> =
|
||||
LazyLock::new(|| Loader::with_parser_lib_path(SCRATCH_DIR.clone()));
|
||||
#[cfg(feature = "wasm")]
|
||||
static WASM_ENGINE: LazyLock<wasmtime::Engine> = LazyLock::new(Default::default);
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
#[expect(
|
||||
clippy::type_complexity,
|
||||
reason = "complex map type reflects benchmark data structure"
|
||||
)]
|
||||
static EXAMPLE_AND_QUERY_PATHS_BY_LANGUAGE_DIR: LazyLock<
|
||||
BTreeMap<PathBuf, (Vec<PathBuf>, Vec<PathBuf>)>,
|
||||
> = LazyLock::new(|| {
|
||||
|
|
@ -36,22 +43,14 @@ static EXAMPLE_AND_QUERY_PATHS_BY_LANGUAGE_DIR: LazyLock<
|
|||
if let Ok(example_files) = fs::read_dir(dir.join("examples")) {
|
||||
example_paths.extend(example_files.filter_map(|p| {
|
||||
let p = p.unwrap().path();
|
||||
if p.is_file() {
|
||||
Some(p)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
if p.is_file() { Some(p) } else { None }
|
||||
}));
|
||||
}
|
||||
|
||||
if let Ok(query_files) = fs::read_dir(dir.join("queries")) {
|
||||
query_paths.extend(query_files.filter_map(|p| {
|
||||
let p = p.unwrap().path();
|
||||
if p.is_file() {
|
||||
Some(p)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
if p.is_file() { Some(p) } else { None }
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
|
|
@ -93,22 +92,26 @@ fn main() {
|
|||
{
|
||||
let language_name = language_path.file_name().unwrap().to_str().unwrap();
|
||||
|
||||
if let Some(filter) = LANGUAGE_FILTER.as_ref() {
|
||||
if language_name != filter.as_str() {
|
||||
continue;
|
||||
}
|
||||
if let Some(filter) = LANGUAGE_FILTER.as_ref()
|
||||
&& language_name != filter.as_str()
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
info!("\nLanguage: {language_name}");
|
||||
let language = get_language(language_path);
|
||||
let language = if *WASM {
|
||||
get_wasm_language(language_name, &mut parser)
|
||||
} else {
|
||||
get_language(language_path)
|
||||
};
|
||||
parser.set_language(&language).unwrap();
|
||||
|
||||
info!(" Constructing Queries");
|
||||
for path in query_paths {
|
||||
if let Some(filter) = EXAMPLE_FILTER.as_ref() {
|
||||
if !path.to_str().unwrap().contains(filter.as_str()) {
|
||||
continue;
|
||||
}
|
||||
if let Some(filter) = EXAMPLE_FILTER.as_ref()
|
||||
&& !path.to_str().unwrap().contains(filter.as_str())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
parse(path, max_path_length, |source| {
|
||||
|
|
@ -121,10 +124,10 @@ fn main() {
|
|||
info!(" Parsing Valid Code:");
|
||||
let mut normal_speeds = Vec::new();
|
||||
for example_path in example_paths {
|
||||
if let Some(filter) = EXAMPLE_FILTER.as_ref() {
|
||||
if !example_path.to_str().unwrap().contains(filter.as_str()) {
|
||||
continue;
|
||||
}
|
||||
if let Some(filter) = EXAMPLE_FILTER.as_ref()
|
||||
&& !example_path.to_str().unwrap().contains(filter.as_str())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
normal_speeds.push(parse(example_path, max_path_length, |code| {
|
||||
|
|
@ -139,10 +142,10 @@ fn main() {
|
|||
{
|
||||
if other_language_path != language_path {
|
||||
for example_path in example_paths {
|
||||
if let Some(filter) = EXAMPLE_FILTER.as_ref() {
|
||||
if !example_path.to_str().unwrap().contains(filter.as_str()) {
|
||||
continue;
|
||||
}
|
||||
if let Some(filter) = EXAMPLE_FILTER.as_ref()
|
||||
&& !example_path.to_str().unwrap().contains(filter.as_str())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
error_speeds.push(parse(example_path, max_path_length, |code| {
|
||||
|
|
@ -220,3 +223,32 @@ fn get_language(path: &Path) -> Language {
|
|||
.with_context(|| format!("Failed to load language at path {}", src_path.display()))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[cfg(feature = "wasm")]
|
||||
fn get_wasm_language(language_name: &str, parser: &mut Parser) -> Language {
|
||||
let wasm_language_name = language_name.replace('-', "_");
|
||||
let wasm_path = ROOT_DIR
|
||||
.join("target")
|
||||
.join("release")
|
||||
.join(format!("tree-sitter-{language_name}.wasm"));
|
||||
let wasm = fs::read(&wasm_path)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Failed to read {}. Generate Wasm fixtures with `cargo xtask generate-fixtures --wasm`",
|
||||
wasm_path.display()
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
let mut store = WasmStore::new(&WASM_ENGINE).expect("Failed to create Wasm store");
|
||||
let language = store
|
||||
.load_language(&wasm_language_name, &wasm)
|
||||
.with_context(|| format!("Failed to load Wasm language at {}", wasm_path.display()))
|
||||
.unwrap();
|
||||
parser.set_wasm_store(store).unwrap();
|
||||
language
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "wasm"))]
|
||||
fn get_wasm_language(_language_name: &str, _parser: &mut Parser) -> Language {
|
||||
panic!("Wasm benchmarking requires the `wasm` feature");
|
||||
}
|
||||
|
|
|
|||
840
crates/cli/eslint/package-lock.json
generated
840
crates/cli/eslint/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -17,7 +17,7 @@
|
|||
"tree-sitter"
|
||||
],
|
||||
"dependencies": {
|
||||
"eslint-plugin-jsdoc": "^50.2.4"
|
||||
"eslint-plugin-jsdoc": "^62.7.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": ">= 9"
|
||||
|
|
|
|||
83
crates/cli/npm/dsl.d.ts
vendored
83
crates/cli/npm/dsl.d.ts
vendored
|
|
@ -3,18 +3,22 @@ type BlankRule = { type: 'BLANK' };
|
|||
type ChoiceRule = { type: 'CHOICE'; members: Rule[] };
|
||||
type FieldRule = { type: 'FIELD'; name: string; content: Rule };
|
||||
type ImmediateTokenRule = { type: 'IMMEDIATE_TOKEN'; content: Rule };
|
||||
type PatternRule = { type: 'PATTERN'; value: string };
|
||||
type PatternRule = { type: 'PATTERN'; value: string; flags?: string };
|
||||
type PrecedenceValue = string | number;
|
||||
type PrecDynamicRule = { type: 'PREC_DYNAMIC'; content: Rule; value: number };
|
||||
type PrecLeftRule = { type: 'PREC_LEFT'; content: Rule; value: number };
|
||||
type PrecRightRule = { type: 'PREC_RIGHT'; content: Rule; value: number };
|
||||
type PrecRule = { type: 'PREC'; content: Rule; value: number };
|
||||
type PrecLeftRule = { type: 'PREC_LEFT'; content: Rule; value: PrecedenceValue };
|
||||
type PrecRightRule = { type: 'PREC_RIGHT'; content: Rule; value: PrecedenceValue };
|
||||
type PrecRule = { type: 'PREC'; content: Rule; value: PrecedenceValue };
|
||||
type Repeat1Rule = { type: 'REPEAT1'; content: Rule };
|
||||
type RepeatRule = { type: 'REPEAT'; content: Rule };
|
||||
type ReservedRule = { type: 'RESERVED'; content: Rule; context_name: string };
|
||||
type SeqRule = { type: 'SEQ'; members: Rule[] };
|
||||
type StringRule = { type: 'STRING'; value: string };
|
||||
type SymbolRule<Name extends string> = { type: 'SYMBOL'; name: Name };
|
||||
type PrecedenceEntry = StringRule | SymbolRule<string>;
|
||||
type TokenRule = { type: 'TOKEN'; content: Rule };
|
||||
type EOFRule = { type: 'EOF' };
|
||||
|
||||
|
||||
type Rule =
|
||||
| AliasRule
|
||||
|
|
@ -33,7 +37,8 @@ type Rule =
|
|||
| SeqRule
|
||||
| StringRule
|
||||
| SymbolRule<string>
|
||||
| TokenRule;
|
||||
| TokenRule
|
||||
| EOFRule;
|
||||
|
||||
declare class RustRegex {
|
||||
value: string;
|
||||
|
|
@ -86,8 +91,8 @@ interface Grammar<
|
|||
*/
|
||||
precedences?: (
|
||||
$: GrammarSymbols<RuleName | BaseGrammarRuleName>,
|
||||
previous: Rule[][],
|
||||
) => RuleOrLiteral[][],
|
||||
previous: PrecedenceEntry[][],
|
||||
) => (string | PrecedenceEntry)[][],
|
||||
|
||||
/**
|
||||
* An array of arrays of rule names. Each inner array represents a set of
|
||||
|
|
@ -101,8 +106,8 @@ interface Grammar<
|
|||
*/
|
||||
conflicts?: (
|
||||
$: GrammarSymbols<RuleName | BaseGrammarRuleName>,
|
||||
previous: Rule[][],
|
||||
) => RuleOrLiteral[][];
|
||||
previous: SymbolRule<string>[][],
|
||||
) => SymbolRule<string>[][];
|
||||
|
||||
/**
|
||||
* An array of token names which can be returned by an _external scanner_.
|
||||
|
|
@ -127,9 +132,11 @@ interface Grammar<
|
|||
* specify extras: `$ => []` in your grammar.
|
||||
*
|
||||
* @param $ grammar rules
|
||||
* @param previous array of extras from the base grammar
|
||||
*/
|
||||
extras?: (
|
||||
$: GrammarSymbols<RuleName | BaseGrammarRuleName>,
|
||||
previous: Rule[],
|
||||
) => RuleOrLiteral[];
|
||||
|
||||
/**
|
||||
|
|
@ -142,8 +149,8 @@ interface Grammar<
|
|||
*/
|
||||
inline?: (
|
||||
$: GrammarSymbols<RuleName | BaseGrammarRuleName>,
|
||||
previous: Rule[],
|
||||
) => RuleOrLiteral[];
|
||||
previous: SymbolRule<string>[],
|
||||
) => SymbolRule<string>[];
|
||||
|
||||
/**
|
||||
* A list of hidden rule names that should be considered supertypes in the
|
||||
|
|
@ -155,8 +162,8 @@ interface Grammar<
|
|||
*/
|
||||
supertypes?: (
|
||||
$: GrammarSymbols<RuleName | BaseGrammarRuleName>,
|
||||
previous: Rule[],
|
||||
) => RuleOrLiteral[];
|
||||
previous: SymbolRule<string>[],
|
||||
) => SymbolRule<string>[];
|
||||
|
||||
/**
|
||||
* The name of a token that will match keywords for the purpose of the
|
||||
|
|
@ -166,24 +173,47 @@ interface Grammar<
|
|||
*
|
||||
* @see https://tree-sitter.github.io/tree-sitter/creating-parsers/3-writing-the-grammar#keyword-extraction
|
||||
*/
|
||||
word?: ($: GrammarSymbols<RuleName | BaseGrammarRuleName>) => RuleOrLiteral;
|
||||
word?: (
|
||||
$: GrammarSymbols<RuleName | BaseGrammarRuleName>,
|
||||
) => SymbolRule<string>;
|
||||
|
||||
|
||||
/**
|
||||
* Mapping of names to reserved word sets. The first reserved word set is the
|
||||
* global word set, meaning it applies to every rule in every parse state.
|
||||
* The other word sets can be used with the `reserved` function.
|
||||
* The other word sets can be used with the `reserved` function. Each callback
|
||||
* receives the base grammar's reserved word set of the same name as its second
|
||||
* argument, or `undefined` if no matching set exists.
|
||||
*/
|
||||
reserved?: Record<
|
||||
string,
|
||||
($: GrammarSymbols<RuleName | BaseGrammarRuleName>) => RuleOrLiteral[]
|
||||
(
|
||||
$: GrammarSymbols<RuleName | BaseGrammarRuleName>,
|
||||
previous: Rule[] | undefined,
|
||||
) => RuleOrLiteral[]
|
||||
>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return type of grammar(). The runtime evaluates and normalizes the grammar
|
||||
* beneath a "grammar" key. Optional input fields become required output fields
|
||||
* with default values when not provided.
|
||||
*/
|
||||
type GrammarSchema<RuleName extends string> = {
|
||||
[K in keyof Grammar<RuleName>]: K extends 'rules'
|
||||
? Record<RuleName, Rule>
|
||||
: Grammar<RuleName>[K];
|
||||
grammar: {
|
||||
name: string;
|
||||
/** Base grammar name when extending; undefined for root grammars. */
|
||||
inherits: string | undefined;
|
||||
rules: Record<RuleName, Rule>;
|
||||
precedences: PrecedenceEntry[][];
|
||||
conflicts: string[][];
|
||||
externals: Rule[];
|
||||
extras: Rule[];
|
||||
inline: string[];
|
||||
supertypes: string[];
|
||||
word: string | undefined;
|
||||
reserved: Record<string, Rule[]>;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -311,7 +341,7 @@ declare const prec: {
|
|||
*
|
||||
* @see https://www.gnu.org/software/bison/manual/html_node/Generalized-LR-Parsing.html
|
||||
*/
|
||||
dynamic(value: string | number, rule: RuleOrLiteral): PrecDynamicRule;
|
||||
dynamic(value: number, rule: RuleOrLiteral): PrecDynamicRule;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -382,6 +412,19 @@ declare const token: {
|
|||
immediate(rule: RuleOrLiteral): ImmediateTokenRule;
|
||||
};
|
||||
|
||||
/**
|
||||
* Matches the end of input. May only appear as the final symbol of a
|
||||
* (possibly nested) sequence; a production ending in `eof()` reduces only
|
||||
* when the lookahead is end-of-input, rather than shifting a token.
|
||||
*
|
||||
* Choice branches that continue past `eof()` are dropped as unreachable,
|
||||
* and `eof()` is not allowed inside `token()`.
|
||||
*
|
||||
* Useful when a rule should match either an explicit terminator (e.g. a
|
||||
* newline) or the end of the file.
|
||||
*/
|
||||
declare function eof(): EOFRule;
|
||||
|
||||
/**
|
||||
* Creates a new language grammar with the provided schema.
|
||||
*
|
||||
|
|
|
|||
4
crates/cli/npm/package-lock.json
generated
4
crates/cli/npm/package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "tree-sitter-cli",
|
||||
"version": "0.26.6",
|
||||
"version": "0.28.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "tree-sitter-cli",
|
||||
"version": "0.26.6",
|
||||
"version": "0.28.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "tree-sitter-cli",
|
||||
"version": "0.26.6",
|
||||
"version": "0.28.0",
|
||||
"author": {
|
||||
"name": "Max Brunsfeld",
|
||||
"email": "maxbrunsfeld@gmail.com"
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
installShellFiles,
|
||||
}:
|
||||
let
|
||||
isCross = stdenv.targetPlatform == stdenv.buildPlatform;
|
||||
canRunHost = stdenv.buildPlatform.canExecute stdenv.hostPlatform;
|
||||
in
|
||||
rustPlatform.buildRustPackage {
|
||||
pname = "tree-sitter-cli";
|
||||
|
|
@ -28,7 +28,7 @@ rustPlatform.buildRustPackage {
|
|||
pkg-config
|
||||
nodejs_22
|
||||
]
|
||||
++ lib.optionals (!isCross) [ installShellFiles ];
|
||||
++ lib.optionals canRunHost [ installShellFiles ];
|
||||
|
||||
cargoLock.lockFile = ../../Cargo.lock;
|
||||
|
||||
|
|
@ -42,9 +42,9 @@ rustPlatform.buildRustPackage {
|
|||
'';
|
||||
|
||||
preCheck = "export HOME=$TMPDIR";
|
||||
doCheck = !isCross;
|
||||
doCheck = canRunHost;
|
||||
|
||||
postInstall = lib.optionalString (!isCross) ''
|
||||
postInstall = lib.optionalString canRunHost ''
|
||||
installShellCompletion --cmd tree-sitter \
|
||||
--bash <($out/bin/tree-sitter complete --shell bash) \
|
||||
--zsh <($out/bin/tree-sitter complete --shell zsh) \
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use std::{
|
|||
};
|
||||
|
||||
use log::{error, info};
|
||||
use rand::Rng;
|
||||
use rand::RngExt;
|
||||
use regex::Regex;
|
||||
use tree_sitter::{Language, Parser};
|
||||
|
||||
|
|
@ -25,7 +25,7 @@ use crate::{
|
|||
random::Rand,
|
||||
},
|
||||
parse::perform_edit,
|
||||
test::{parse_tests, strip_sexp_fields, DiffKey, TestDiff, TestEntry},
|
||||
test::{DiffKey, TestDiff, TestEntry, TestExpectation, parse_tests, render_test_output},
|
||||
};
|
||||
|
||||
pub static LOG_ENABLED: LazyLock<bool> = LazyLock::new(|| env::var("TREE_SITTER_LOG").is_ok());
|
||||
|
|
@ -63,9 +63,9 @@ fn regex_env_var(name: &'static str) -> Option<Regex> {
|
|||
#[must_use]
|
||||
pub fn new_seed() -> usize {
|
||||
int_env_var("TREE_SITTER_SEED").unwrap_or_else(|| {
|
||||
let mut rng = rand::thread_rng();
|
||||
let seed = rng.gen::<usize>();
|
||||
info!("Seed: {seed}");
|
||||
let mut rng = rand::rng();
|
||||
let seed = rng.random_range(0..=usize::MAX);
|
||||
eprintln!("fuzz seed: {seed}");
|
||||
seed
|
||||
})
|
||||
}
|
||||
|
|
@ -97,9 +97,7 @@ pub fn fuzz_language_corpus(
|
|||
.iter()
|
||||
.any(|lang| lang.as_ref() == language_name)
|
||||
}
|
||||
TestEntry::Group {
|
||||
ref mut children, ..
|
||||
} => {
|
||||
TestEntry::Group { children, .. } => {
|
||||
children.retain_mut(|child| retain(child, language_name));
|
||||
!children.is_empty()
|
||||
}
|
||||
|
|
@ -111,12 +109,16 @@ pub fn fuzz_language_corpus(
|
|||
let corpus_dir = grammar_dir.join(subdir).join("test").join("corpus");
|
||||
|
||||
if !corpus_dir.exists() || !corpus_dir.is_dir() {
|
||||
error!("No corpus directory found, ensure that you have a `test/corpus` directory in your grammar directory with at least one test file.");
|
||||
error!(
|
||||
"No corpus directory found, ensure that you have a `test/corpus` directory in your grammar directory with at least one test file."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if std::fs::read_dir(&corpus_dir).unwrap().count() == 0 {
|
||||
error!("No corpus files found in `test/corpus`, ensure that you have at least one test file in your corpus directory.");
|
||||
error!(
|
||||
"No corpus files found in `test/corpus`, ensure that you have at least one test file in your corpus directory."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -142,7 +144,7 @@ pub fn fuzz_language_corpus(
|
|||
.take()
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.chain(tests.iter().filter(|x| x.skip).map(get_test_name))
|
||||
.chain(tests.iter().filter(|t| t.skip()).map(get_test_name))
|
||||
.map(|x| (x, 0))
|
||||
.collect::<HashMap<String, usize>>();
|
||||
|
||||
|
|
@ -167,31 +169,8 @@ pub fn fuzz_language_corpus(
|
|||
println!(" {test_index}. {test_name}");
|
||||
|
||||
let passed = allocations::record_checked(|| {
|
||||
let mut log_session = None;
|
||||
let mut parser = get_parser(&mut log_session, "log.html");
|
||||
parser.set_language(language).unwrap();
|
||||
set_included_ranges(&mut parser, &test.input, test.template_delimiters);
|
||||
|
||||
let tree = parser.parse(&test.input, None).unwrap();
|
||||
|
||||
if test.error {
|
||||
return true;
|
||||
}
|
||||
|
||||
let mut actual_output = tree.root_node().to_sexp();
|
||||
if !test.has_fields {
|
||||
actual_output = strip_sexp_fields(&actual_output);
|
||||
}
|
||||
|
||||
if actual_output != test.output {
|
||||
println!("Incorrect initial parse for {test_name}");
|
||||
DiffKey::print();
|
||||
println!("{}", TestDiff::new(&actual_output, &test.output));
|
||||
println!();
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
let check_output = !test.error();
|
||||
test.check_initial_parse(language, &test_name, check_output)
|
||||
})
|
||||
.unwrap_or_else(|e| {
|
||||
error!("{e}");
|
||||
|
|
@ -255,7 +234,7 @@ pub fn fuzz_language_corpus(
|
|||
// Check that the new tree is consistent.
|
||||
check_consistent_sizes(&tree2, &input);
|
||||
if let Err(message) = check_changed_ranges(&tree, &tree2, &input) {
|
||||
error!("\nUnexpected scope change in seed {seed} with start seed {start_seed}\n{message}\n\n",);
|
||||
error!("\nUnexpected scope change in seed {seed} with start seed {start_seed}\n{message}\n\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -271,12 +250,9 @@ pub fn fuzz_language_corpus(
|
|||
let tree3 = parser.parse(&input, Some(&tree2)).unwrap();
|
||||
|
||||
// Verify that the final tree matches the expectation from the corpus.
|
||||
let mut actual_output = tree3.root_node().to_sexp();
|
||||
if !test.has_fields {
|
||||
actual_output = strip_sexp_fields(&actual_output);
|
||||
}
|
||||
let actual_output = render_test_output(&input, &tree3, test.cst, test.has_fields).unwrap();
|
||||
|
||||
if actual_output != test.output && !test.error {
|
||||
if actual_output != test.output && !test.error() {
|
||||
println!("Incorrect parse for {test_name} - seed {seed}");
|
||||
DiffKey::print();
|
||||
println!("{}", TestDiff::new(&actual_output, &test.output));
|
||||
|
|
@ -324,12 +300,56 @@ pub struct FlattenedTest {
|
|||
pub input: Vec<u8>,
|
||||
pub output: String,
|
||||
pub languages: Vec<Box<str>>,
|
||||
pub error: bool,
|
||||
pub skip: bool,
|
||||
pub expectation: TestExpectation,
|
||||
pub has_fields: bool,
|
||||
pub cst: bool,
|
||||
pub template_delimiters: Option<(&'static str, &'static str)>,
|
||||
}
|
||||
|
||||
impl FlattenedTest {
|
||||
#[must_use]
|
||||
fn skip(&self) -> bool {
|
||||
self.expectation == TestExpectation::Skip
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
fn error(&self) -> bool {
|
||||
self.expectation == TestExpectation::Error
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub(crate) fn check_initial_parse(
|
||||
&self,
|
||||
language: &Language,
|
||||
display_name: &str,
|
||||
check_output: bool,
|
||||
) -> bool {
|
||||
let mut log_session = None;
|
||||
let mut parser = get_parser(&mut log_session, "log.html");
|
||||
parser.set_language(language).unwrap();
|
||||
set_included_ranges(&mut parser, &self.input, self.template_delimiters);
|
||||
|
||||
let tree = parser.parse(&self.input, None).unwrap();
|
||||
|
||||
if !check_output {
|
||||
return true;
|
||||
}
|
||||
|
||||
let actual_output =
|
||||
render_test_output(&self.input, &tree, self.cst, self.has_fields).unwrap();
|
||||
|
||||
if actual_output == self.output {
|
||||
true
|
||||
} else {
|
||||
println!("Incorrect initial parse for {display_name}");
|
||||
DiffKey::print();
|
||||
println!("{}", TestDiff::new(&actual_output, &self.output));
|
||||
println!();
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn flatten_tests(
|
||||
test: TestEntry,
|
||||
|
|
@ -362,20 +382,20 @@ pub fn flatten_tests(
|
|||
if !include.is_match(&name) {
|
||||
return;
|
||||
}
|
||||
} else if let Some(exclude) = exclude {
|
||||
if exclude.is_match(&name) {
|
||||
return;
|
||||
}
|
||||
} else if let Some(exclude) = exclude
|
||||
&& exclude.is_match(&name)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
result.push(FlattenedTest {
|
||||
name,
|
||||
input,
|
||||
output,
|
||||
has_fields,
|
||||
languages: attributes.languages,
|
||||
error: attributes.error,
|
||||
skip: attributes.skip,
|
||||
expectation: attributes.expectation,
|
||||
has_fields,
|
||||
cst: attributes.cst,
|
||||
template_delimiters: None,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,19 +2,21 @@ use std::{
|
|||
collections::HashMap,
|
||||
os::raw::c_void,
|
||||
sync::{
|
||||
atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst},
|
||||
Mutex,
|
||||
atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst},
|
||||
},
|
||||
};
|
||||
|
||||
#[ctor::ctor]
|
||||
unsafe fn initialize_allocation_recording() {
|
||||
tree_sitter::set_allocator(
|
||||
Some(ts_record_malloc),
|
||||
Some(ts_record_calloc),
|
||||
Some(ts_record_realloc),
|
||||
Some(ts_record_free),
|
||||
);
|
||||
unsafe {
|
||||
tree_sitter::set_allocator(Some(tree_sitter::Allocator {
|
||||
malloc: ts_record_malloc,
|
||||
calloc: ts_record_calloc,
|
||||
realloc: ts_record_realloc,
|
||||
free: ts_record_free,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
|
|
@ -33,7 +35,7 @@ thread_local! {
|
|||
static RECORDER: AllocationRecorder = AllocationRecorder::default();
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
unsafe extern "C" {
|
||||
fn malloc(size: usize) -> *mut c_void;
|
||||
fn calloc(count: usize, size: usize) -> *mut c_void;
|
||||
fn realloc(ptr: *mut c_void, size: usize) -> *mut c_void;
|
||||
|
|
@ -103,9 +105,11 @@ fn record_dealloc(ptr: *mut c_void) {
|
|||
/// freed by calling `ts_record_free`.
|
||||
#[must_use]
|
||||
pub unsafe extern "C" fn ts_record_malloc(size: usize) -> *mut c_void {
|
||||
let result = malloc(size);
|
||||
record_alloc(result);
|
||||
result
|
||||
unsafe {
|
||||
let result = malloc(size);
|
||||
record_alloc(result);
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
|
|
@ -114,9 +118,11 @@ pub unsafe extern "C" fn ts_record_malloc(size: usize) -> *mut c_void {
|
|||
/// freed by calling `ts_record_free`.
|
||||
#[must_use]
|
||||
pub unsafe extern "C" fn ts_record_calloc(count: usize, size: usize) -> *mut c_void {
|
||||
let result = calloc(count, size);
|
||||
record_alloc(result);
|
||||
result
|
||||
unsafe {
|
||||
let result = calloc(count, size);
|
||||
record_alloc(result);
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
|
|
@ -125,14 +131,16 @@ pub unsafe extern "C" fn ts_record_calloc(count: usize, size: usize) -> *mut c_v
|
|||
/// freed by calling `ts_record_free`.
|
||||
#[must_use]
|
||||
pub unsafe extern "C" fn ts_record_realloc(ptr: *mut c_void, size: usize) -> *mut c_void {
|
||||
let result = realloc(ptr, size);
|
||||
if ptr.is_null() {
|
||||
record_alloc(result);
|
||||
} else if !core::ptr::eq(ptr, result) {
|
||||
record_dealloc(ptr);
|
||||
record_alloc(result);
|
||||
unsafe {
|
||||
let result = realloc(ptr, size);
|
||||
if ptr.is_null() {
|
||||
record_alloc(result);
|
||||
} else if !core::ptr::eq(ptr, result) {
|
||||
record_dealloc(ptr);
|
||||
record_alloc(result);
|
||||
}
|
||||
result
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
|
|
@ -140,6 +148,8 @@ pub unsafe extern "C" fn ts_record_realloc(ptr: *mut c_void, size: usize) -> *mu
|
|||
/// The caller must ensure that `ptr` was allocated by a previous call
|
||||
/// to `ts_record_malloc`, `ts_record_calloc`, or `ts_record_realloc`.
|
||||
pub unsafe extern "C" fn ts_record_free(ptr: *mut c_void) {
|
||||
record_dealloc(ptr);
|
||||
free(ptr);
|
||||
unsafe {
|
||||
record_dealloc(ptr);
|
||||
free(ptr);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,22 @@
|
|||
use tree_sitter::{LogType, Node, Parser, Point, Range, Tree};
|
||||
|
||||
use super::{scope_sequence::ScopeSequence, LOG_ENABLED, LOG_GRAPH_ENABLED};
|
||||
use super::{LOG_ENABLED, LOG_GRAPH_ENABLED, scope_sequence::ScopeSequence};
|
||||
use crate::util;
|
||||
|
||||
pub fn check_consistent_sizes(tree: &Tree, input: &[u8]) {
|
||||
fn check(node: Node, line_offsets: &[usize]) {
|
||||
struct SizeCheckFrame<'a> {
|
||||
node: Node<'a>,
|
||||
end_byte: usize,
|
||||
end_point: Point,
|
||||
child_count: u32,
|
||||
child_index: u32,
|
||||
last_child_end_byte: usize,
|
||||
last_child_end_point: Point,
|
||||
some_child_has_changes: bool,
|
||||
actual_named_child_count: usize,
|
||||
}
|
||||
|
||||
impl SizeCheckFrame<'_> {
|
||||
fn new<'a>(node: Node<'a>, line_offsets: &[usize]) -> SizeCheckFrame<'a> {
|
||||
let start_byte = node.start_byte();
|
||||
let end_byte = node.end_byte();
|
||||
let start_point = node.start_position();
|
||||
|
|
@ -18,37 +30,21 @@ pub fn check_consistent_sizes(tree: &Tree, input: &[u8]) {
|
|||
);
|
||||
assert_eq!(end_byte, line_offsets[end_point.row] + end_point.column);
|
||||
|
||||
let mut last_child_end_byte = start_byte;
|
||||
let mut last_child_end_point = start_point;
|
||||
let mut some_child_has_changes = false;
|
||||
let mut actual_named_child_count = 0;
|
||||
for i in 0..node.child_count() {
|
||||
let child = node.child(i as u32).unwrap();
|
||||
assert!(child.start_byte() >= last_child_end_byte);
|
||||
assert!(child.start_position() >= last_child_end_point);
|
||||
check(child, line_offsets);
|
||||
if child.has_changes() {
|
||||
some_child_has_changes = true;
|
||||
}
|
||||
if child.is_named() {
|
||||
actual_named_child_count += 1;
|
||||
}
|
||||
last_child_end_byte = child.end_byte();
|
||||
last_child_end_point = child.end_position();
|
||||
}
|
||||
|
||||
assert_eq!(actual_named_child_count, node.named_child_count());
|
||||
|
||||
if node.child_count() > 0 {
|
||||
assert!(end_byte >= last_child_end_byte);
|
||||
assert!(end_point >= last_child_end_point);
|
||||
}
|
||||
|
||||
if some_child_has_changes {
|
||||
assert!(node.has_changes());
|
||||
SizeCheckFrame {
|
||||
node,
|
||||
end_byte,
|
||||
end_point,
|
||||
child_count: node.child_count(),
|
||||
child_index: 0,
|
||||
last_child_end_byte: start_byte,
|
||||
last_child_end_point: start_point,
|
||||
some_child_has_changes: false,
|
||||
actual_named_child_count: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check_consistent_sizes(tree: &Tree, input: &[u8]) {
|
||||
let mut line_offsets = vec![0];
|
||||
for (i, c) in input.iter().enumerate() {
|
||||
if *c == b'\n' {
|
||||
|
|
@ -56,7 +52,41 @@ pub fn check_consistent_sizes(tree: &Tree, input: &[u8]) {
|
|||
}
|
||||
}
|
||||
|
||||
check(tree.root_node(), &line_offsets);
|
||||
let mut stack: Vec<SizeCheckFrame> = vec![SizeCheckFrame::new(tree.root_node(), &line_offsets)];
|
||||
while let Some(top) = stack.last_mut() {
|
||||
if top.child_index < top.child_count {
|
||||
let i = top.child_index;
|
||||
let child = top.node.child(i).unwrap();
|
||||
|
||||
assert!(child.start_byte() >= top.last_child_end_byte);
|
||||
assert!(child.start_position() >= top.last_child_end_point);
|
||||
if child.has_changes() {
|
||||
top.some_child_has_changes = true;
|
||||
}
|
||||
if child.is_named() {
|
||||
top.actual_named_child_count += 1;
|
||||
}
|
||||
top.last_child_end_byte = child.end_byte();
|
||||
top.last_child_end_point = child.end_position();
|
||||
top.child_index += 1;
|
||||
|
||||
stack.push(SizeCheckFrame::new(child, &line_offsets));
|
||||
continue;
|
||||
}
|
||||
|
||||
let frame = stack.pop().unwrap();
|
||||
assert_eq!(
|
||||
frame.actual_named_child_count,
|
||||
frame.node.named_child_count()
|
||||
);
|
||||
if frame.child_count > 0 {
|
||||
assert!(frame.end_byte >= frame.last_child_end_byte);
|
||||
assert!(frame.end_point >= frame.last_child_end_point);
|
||||
}
|
||||
if frame.some_child_has_changes {
|
||||
assert!(frame.node.has_changes());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check_changed_ranges(old_tree: &Tree, new_tree: &Tree, input: &[u8]) -> Result<(), String> {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,4 @@
|
|||
use rand::{
|
||||
distributions::Alphanumeric,
|
||||
prelude::{Rng, SeedableRng, StdRng},
|
||||
};
|
||||
use rand::{RngExt, SeedableRng, distr::Alphanumeric, rngs::StdRng};
|
||||
|
||||
const OPERATORS: &[char] = &[
|
||||
'+', '-', '<', '>', '(', ')', '*', '/', '&', '|', '!', ',', '.', '%',
|
||||
|
|
@ -16,7 +13,7 @@ impl Rand {
|
|||
}
|
||||
|
||||
pub fn unsigned(&mut self, max: usize) -> usize {
|
||||
self.0.gen_range(0..=max)
|
||||
self.0.random_range(0..=max)
|
||||
}
|
||||
|
||||
pub fn words(&mut self, max_count: usize) -> Vec<u8> {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
use tree_sitter::{Point, Range, Tree};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ScopeSequence(Vec<ScopeStack>);
|
||||
pub struct ScopeSequence<'a>(Vec<ScopeStack<'a>>);
|
||||
|
||||
type ScopeStack = Vec<&'static str>;
|
||||
type ScopeStack<'a> = Vec<&'a str>;
|
||||
|
||||
impl ScopeSequence {
|
||||
impl<'a> ScopeSequence<'a> {
|
||||
#[must_use]
|
||||
pub fn new(tree: &Tree) -> Self {
|
||||
pub fn new(tree: &'a Tree) -> Self {
|
||||
let mut result = Self(Vec::new());
|
||||
let mut scope_stack = Vec::new();
|
||||
|
||||
|
|
@ -49,7 +49,7 @@ impl ScopeSequence {
|
|||
for i in 0..(self.0.len().max(other.0.len())) {
|
||||
let stack = &self.0.get(i);
|
||||
let other_stack = &other.0.get(i);
|
||||
if *stack != *other_stack && ![b'\r', b'\n'].contains(&text[i]) {
|
||||
if *stack != *other_stack && !b"\r\n".contains(&text[i]) {
|
||||
let containing_range = known_changed_ranges
|
||||
.iter()
|
||||
.find(|range| range.start_point <= position && position < range.end_point);
|
||||
|
|
|
|||
|
|
@ -4,17 +4,18 @@ use std::{
|
|||
fs,
|
||||
io::{self, Write as _},
|
||||
path::{self, Path, PathBuf},
|
||||
str,
|
||||
sync::{atomic::AtomicUsize, Arc},
|
||||
sync::{Arc, atomic::AtomicUsize},
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
use ansi_colours::{ansi256_from_rgb, rgb_from_ansi256};
|
||||
use anstyle::{Ansi256Color, AnsiColor, Color, Effects, RgbColor};
|
||||
use anyhow::Result;
|
||||
use clap::ValueEnum;
|
||||
use log::{info, warn};
|
||||
use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer};
|
||||
use serde_json::{json, Value};
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer, ser::SerializeMap};
|
||||
use serde_json::{Value, json};
|
||||
use tree_sitter::ffi::{self, TSInputEncoding};
|
||||
use tree_sitter_highlight::{HighlightConfiguration, HighlightEvent, Highlighter, HtmlRenderer};
|
||||
use tree_sitter_loader::Loader;
|
||||
|
||||
|
|
@ -25,8 +26,9 @@ pub const HTML_HEAD_HEADER: &str = "
|
|||
<style>
|
||||
body {
|
||||
font-family: monospace
|
||||
}
|
||||
.line-number {
|
||||
}";
|
||||
|
||||
pub const HTML_LINE_NUMBER_STYLE: &str = " .line-number {
|
||||
user-select: none;
|
||||
text-align: right;
|
||||
color: rgba(27,31,35,.3);
|
||||
|
|
@ -34,8 +36,7 @@ pub const HTML_HEAD_HEADER: &str = "
|
|||
}
|
||||
.line {
|
||||
white-space: pre;
|
||||
}
|
||||
</style>";
|
||||
}";
|
||||
|
||||
pub const HTML_BODY_HEADER: &str = "
|
||||
</head>
|
||||
|
|
@ -190,20 +191,14 @@ fn parse_style(style: &mut Style, json: Value) {
|
|||
if let Value::Object(entries) = json {
|
||||
for (property_name, value) in entries {
|
||||
match property_name.as_str() {
|
||||
"bold" => {
|
||||
if value == Value::Bool(true) {
|
||||
style.ansi = style.ansi.bold();
|
||||
}
|
||||
"bold" if value == Value::Bool(true) => {
|
||||
style.ansi = style.ansi.bold();
|
||||
}
|
||||
"italic" => {
|
||||
if value == Value::Bool(true) {
|
||||
style.ansi = style.ansi.italic();
|
||||
}
|
||||
"italic" if value == Value::Bool(true) => {
|
||||
style.ansi = style.ansi.italic();
|
||||
}
|
||||
"underline" => {
|
||||
if value == Value::Bool(true) {
|
||||
style.ansi = style.ansi.underline();
|
||||
}
|
||||
"underline" if value == Value::Bool(true) => {
|
||||
style.ansi = style.ansi.underline();
|
||||
}
|
||||
"color" => {
|
||||
if let Some(color) = parse_color(value) {
|
||||
|
|
@ -221,11 +216,11 @@ fn parse_style(style: &mut Style, json: Value) {
|
|||
style.css = None;
|
||||
}
|
||||
|
||||
if let Some(Color::Rgb(RgbColor(red, green, blue))) = style.ansi.get_fg_color() {
|
||||
if !terminal_supports_truecolor() {
|
||||
let ansi256 = Color::Ansi256(Ansi256Color(ansi256_from_rgb((red, green, blue))));
|
||||
style.ansi = style.ansi.fg_color(Some(ansi256));
|
||||
}
|
||||
if let Some(Color::Rgb(RgbColor(red, green, blue))) = style.ansi.get_fg_color()
|
||||
&& !terminal_supports_truecolor()
|
||||
{
|
||||
let ansi256 = Color::Ansi256(Ansi256Color(ansi256_from_rgb((red, green, blue))));
|
||||
style.ansi = style.ansi.fg_color(Some(ansi256));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -313,15 +308,40 @@ fn terminal_supports_truecolor() -> bool {
|
|||
.is_ok_and(|truecolor| truecolor == "truecolor" || truecolor == "24bit")
|
||||
}
|
||||
|
||||
/// The kind of HTML emitted when highlighting to HTML.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
|
||||
pub enum HtmlOutput {
|
||||
/// A complete, self-contained document wrapping a plain
|
||||
/// `<div class="highlight"><pre><code>` block.
|
||||
Document,
|
||||
/// A complete document with a line-number column (a `<table>` layout).
|
||||
#[value(name = "line-numbers")]
|
||||
NumberedDocument,
|
||||
/// Only the code markup, without the surrounding document.
|
||||
Fragment,
|
||||
}
|
||||
|
||||
/// How token colors are applied in HTML output.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
|
||||
pub enum HtmlStyling {
|
||||
/// `class="..."` spans plus a generated `<style>` carrying the theme's colors.
|
||||
Classes,
|
||||
/// `style="..."` spans with the colors inlined.
|
||||
Inline,
|
||||
/// `class="..."` spans with no colors emitted (supply your own stylesheet).
|
||||
Minimal,
|
||||
}
|
||||
|
||||
pub struct HighlightOptions {
|
||||
pub theme: Theme,
|
||||
pub check: bool,
|
||||
pub captures_path: Option<PathBuf>,
|
||||
pub inline_styles: bool,
|
||||
pub html: bool,
|
||||
/// `None` for regular output, `Some((layout, style))` when emitting HTML.
|
||||
pub html: Option<(HtmlOutput, HtmlStyling)>,
|
||||
pub quiet: bool,
|
||||
pub print_time: bool,
|
||||
pub cancellation_flag: Arc<AtomicUsize>,
|
||||
pub encoding: Option<TSInputEncoding>,
|
||||
}
|
||||
|
||||
pub fn highlight(
|
||||
|
|
@ -364,29 +384,60 @@ pub fn highlight(
|
|||
}
|
||||
|
||||
let source = fs::read(path)?;
|
||||
|
||||
fn is_utf16_le_bom(bom_bytes: &[u8]) -> bool {
|
||||
bom_bytes == [0xFF, 0xFE]
|
||||
}
|
||||
|
||||
fn is_utf16_be_bom(bom_bytes: &[u8]) -> bool {
|
||||
bom_bytes == [0xFE, 0xFF]
|
||||
}
|
||||
|
||||
let encoding = match opts.encoding {
|
||||
None if source.len() >= 2 => {
|
||||
if is_utf16_le_bom(&source[0..2]) {
|
||||
Some(ffi::TSInputEncodingUTF16LE)
|
||||
} else if is_utf16_be_bom(&source[0..2]) {
|
||||
Some(ffi::TSInputEncodingUTF16BE)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => opts.encoding,
|
||||
};
|
||||
|
||||
let stdout = io::stdout();
|
||||
let mut stdout = stdout.lock();
|
||||
let time = Instant::now();
|
||||
let mut highlighter = Highlighter::new();
|
||||
let events =
|
||||
highlighter.highlight(config, &source, Some(&opts.cancellation_flag), |string| {
|
||||
loader.highlight_config_for_injection_string(string)
|
||||
})?;
|
||||
let events = highlighter.highlight(
|
||||
config,
|
||||
&source,
|
||||
encoding,
|
||||
Some(&opts.cancellation_flag),
|
||||
|string| loader.highlight_config_for_injection_string(string),
|
||||
)?;
|
||||
let theme = &opts.theme;
|
||||
|
||||
if !opts.quiet && print_name {
|
||||
// A fragment is pure code markup, so it must not be prefixed with the filename.
|
||||
let html_fragment = opts
|
||||
.html
|
||||
.is_some_and(|(layout, _)| layout == HtmlOutput::Fragment);
|
||||
if !opts.quiet && print_name && !html_fragment {
|
||||
writeln!(&mut stdout, "{name}")?;
|
||||
}
|
||||
|
||||
if opts.html {
|
||||
if !opts.quiet {
|
||||
if let Some((layout, style)) = opts.html {
|
||||
if !opts.quiet && layout != HtmlOutput::Fragment {
|
||||
writeln!(&mut stdout, "{HTML_HEAD_HEADER}")?;
|
||||
writeln!(&mut stdout, " <style>")?;
|
||||
let names = theme.highlight_names.iter();
|
||||
let styles = theme.styles.iter();
|
||||
for (name, style) in names.zip(styles) {
|
||||
if let Some(css) = &style.css {
|
||||
writeln!(&mut stdout, " .{name} {{ {css}; }}")?;
|
||||
if layout == HtmlOutput::NumberedDocument {
|
||||
writeln!(&mut stdout, "{HTML_LINE_NUMBER_STYLE}")?;
|
||||
}
|
||||
if style == HtmlStyling::Classes {
|
||||
for (name, style) in theme.highlight_names.iter().zip(&theme.styles) {
|
||||
if let Some(css) = &style.css {
|
||||
writeln!(&mut stdout, " .{name} {{ {css}; }}")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
writeln!(&mut stdout, " </style>")?;
|
||||
|
|
@ -395,7 +446,7 @@ pub fn highlight(
|
|||
|
||||
let mut renderer = HtmlRenderer::new();
|
||||
renderer.render(events, &source, &move |highlight, output| {
|
||||
if opts.inline_styles {
|
||||
if style == HtmlStyling::Inline {
|
||||
output.extend(b"style='");
|
||||
output.extend(
|
||||
theme.styles[highlight.0]
|
||||
|
|
@ -403,7 +454,6 @@ pub fn highlight(
|
|||
.as_ref()
|
||||
.map_or_else(|| "".as_bytes(), |css_style| css_style.as_bytes()),
|
||||
);
|
||||
output.extend(b"'");
|
||||
} else {
|
||||
output.extend(b"class='");
|
||||
let mut parts = theme.highlight_names[highlight.0].split('.').peekable();
|
||||
|
|
@ -413,21 +463,34 @@ pub fn highlight(
|
|||
output.extend(b" ");
|
||||
}
|
||||
}
|
||||
output.extend(b"'");
|
||||
}
|
||||
output.extend(b"'");
|
||||
})?;
|
||||
|
||||
if !opts.quiet {
|
||||
writeln!(&mut stdout, "<table>")?;
|
||||
for (i, line) in renderer.lines().enumerate() {
|
||||
if layout == HtmlOutput::NumberedDocument {
|
||||
writeln!(&mut stdout, "<table>")?;
|
||||
for (i, line) in renderer.lines().enumerate() {
|
||||
writeln!(
|
||||
&mut stdout,
|
||||
"<tr><td class=line-number>{}</td><td class=line>{line}</td></tr>",
|
||||
i + 1,
|
||||
)?;
|
||||
}
|
||||
writeln!(&mut stdout, "</table>")?;
|
||||
} else {
|
||||
let mut body = renderer.lines().collect::<String>();
|
||||
if body.ends_with('\n') {
|
||||
body.pop();
|
||||
}
|
||||
writeln!(
|
||||
&mut stdout,
|
||||
"<tr><td class=line-number>{}</td><td class=line>{line}</td></tr>",
|
||||
i + 1,
|
||||
"<div class=\"highlight\">\n<pre><code>{body}</code></pre>\n</div>",
|
||||
)?;
|
||||
}
|
||||
writeln!(&mut stdout, "</table>")?;
|
||||
writeln!(&mut stdout, "{HTML_FOOTER}")?;
|
||||
if layout != HtmlOutput::Fragment {
|
||||
writeln!(&mut stdout, "{HTML_FOOTER}")?;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let mut style_stack = vec![theme.default_style().ansi];
|
||||
|
|
@ -474,7 +537,7 @@ mod tests {
|
|||
assert_eq!(style.css, None);
|
||||
|
||||
// darkcyan is an ANSI color and is preserved
|
||||
env::set_var("COLORTERM", "");
|
||||
unsafe { env::set_var("COLORTERM", "") };
|
||||
parse_style(&mut style, Value::String(DARK_CYAN.to_string()));
|
||||
assert_eq!(
|
||||
style.ansi.get_fg_color(),
|
||||
|
|
@ -483,7 +546,7 @@ mod tests {
|
|||
assert_eq!(style.css, Some("color: #00af87".to_string()));
|
||||
|
||||
// junglegreen is not an ANSI color and is preserved when the terminal supports it
|
||||
env::set_var("COLORTERM", "truecolor");
|
||||
unsafe { env::set_var("COLORTERM", "truecolor") };
|
||||
parse_style(&mut style, Value::String(JUNGLE_GREEN.to_string()));
|
||||
assert_eq!(
|
||||
style.ansi.get_fg_color(),
|
||||
|
|
@ -492,7 +555,7 @@ mod tests {
|
|||
assert_eq!(style.css, Some("color: #26a69a".to_string()));
|
||||
|
||||
// junglegreen gets approximated as cadetblue when the terminal does not support it
|
||||
env::set_var("COLORTERM", "");
|
||||
unsafe { env::set_var("COLORTERM", "") };
|
||||
parse_style(&mut style, Value::String(JUNGLE_GREEN.to_string()));
|
||||
assert_eq!(
|
||||
style.ansi.get_fg_color(),
|
||||
|
|
@ -501,9 +564,9 @@ mod tests {
|
|||
assert_eq!(style.css, Some("color: #26a69a".to_string()));
|
||||
|
||||
if let Ok(environment_variable) = original_environment_variable {
|
||||
env::set_var("COLORTERM", environment_variable);
|
||||
unsafe { env::set_var("COLORTERM", environment_variable) };
|
||||
} else {
|
||||
env::remove_var("COLORTERM");
|
||||
unsafe { env::remove_var("COLORTERM") };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -3,15 +3,16 @@ use std::{
|
|||
io::{Read, Write},
|
||||
path::{Path, PathBuf},
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
mpsc, Arc,
|
||||
mpsc,
|
||||
},
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use anyhow::{Context, Result, anyhow, bail};
|
||||
use glob::glob;
|
||||
|
||||
use crate::test::{parse_tests, TestEntry};
|
||||
use crate::test::{TestEntry, parse_tests};
|
||||
|
||||
pub enum CliInput {
|
||||
Paths(Vec<PathBuf>),
|
||||
|
|
@ -146,7 +147,6 @@ pub fn get_input(
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn get_test_info(
|
||||
test_entry: &TestEntry,
|
||||
target_test: u32,
|
||||
|
|
|
|||
|
|
@ -1,12 +1,8 @@
|
|||
use std::io::Write;
|
||||
|
||||
use anstyle::{AnsiColor, Color, Style};
|
||||
use log::{Level, LevelFilter, Log, Metadata, Record};
|
||||
|
||||
pub fn paint(color: Option<impl Into<Color>>, text: &str) -> String {
|
||||
let style = Style::new().fg_color(color.map(Into::into));
|
||||
format!("{style}{text}{style:#}")
|
||||
}
|
||||
use crate::paint::{Paint, RED, YELLOW};
|
||||
|
||||
struct Logger;
|
||||
|
||||
|
|
@ -17,16 +13,8 @@ impl Log for Logger {
|
|||
|
||||
fn log(&self, record: &Record) {
|
||||
match record.level() {
|
||||
Level::Error => eprintln!(
|
||||
"{} {}",
|
||||
paint(Some(AnsiColor::Red), "Error:"),
|
||||
record.args()
|
||||
),
|
||||
Level::Warn => eprintln!(
|
||||
"{} {}",
|
||||
paint(Some(AnsiColor::Yellow), "Warning:"),
|
||||
record.args()
|
||||
),
|
||||
Level::Error => eprintln!("{} {}", Paint(RED, "Error:"), record.args()),
|
||||
Level::Warn => eprintln!("{} {}", Paint(YELLOW, "Warning:"), record.args()),
|
||||
Level::Info | Level::Debug => eprintln!("{}", record.args()),
|
||||
Level::Trace => eprintln!(
|
||||
"[{}] {}",
|
||||
|
|
|
|||
|
|
@ -5,24 +5,24 @@ use std::{
|
|||
};
|
||||
|
||||
use anstyle::{AnsiColor, Color, Style};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use clap::{crate_authors, Args, Command, FromArgMatches as _, Subcommand, ValueEnum};
|
||||
use anyhow::{Context, Result, anyhow};
|
||||
use clap::{ArgGroup, Args, Command, FromArgMatches as _, Subcommand, ValueEnum, crate_authors};
|
||||
use clap_complete::generate;
|
||||
use dialoguer::{theme::ColorfulTheme, Confirm, FuzzySelect, Input, MultiSelect};
|
||||
use dialoguer::{Confirm, FuzzySelect, Input, MultiSelect, theme::ColorfulTheme};
|
||||
use heck::ToUpperCamelCase;
|
||||
use log::{error, info, warn};
|
||||
use regex::Regex;
|
||||
use semver::Version as SemverVersion;
|
||||
use tree_sitter::{ffi, Parser, Point};
|
||||
use tree_sitter::{Parser, Point, ffi};
|
||||
use tree_sitter_cli::{
|
||||
fuzz::{
|
||||
fuzz_language_corpus, FuzzOptions, DEFAULT_EDIT_COUNT, DEFAULT_ITERATION_COUNT, EDIT_COUNT,
|
||||
ITERATION_COUNT, LOG_ENABLED, LOG_GRAPH_ENABLED, START_SEED,
|
||||
DEFAULT_EDIT_COUNT, DEFAULT_ITERATION_COUNT, EDIT_COUNT, FuzzOptions, ITERATION_COUNT,
|
||||
LOG_ENABLED, LOG_GRAPH_ENABLED, START_SEED, fuzz_language_corpus,
|
||||
},
|
||||
highlight::{self, HighlightOptions},
|
||||
init::{generate_grammar_files, JsonConfigOpts, TREE_SITTER_JSON_SCHEMA},
|
||||
input::{get_input, get_tmp_source_file, CliInput},
|
||||
logger,
|
||||
highlight::{self, HighlightOptions, HtmlOutput, HtmlStyling},
|
||||
init::{JsonConfigOpts, TREE_SITTER_JSON_SCHEMA, generate_grammar_files},
|
||||
input::{CliInput, get_input, get_tmp_source_file},
|
||||
logger, paint,
|
||||
parse::{self, ParseDebugType, ParseFileOptions, ParseOutput, ParseTheme},
|
||||
playground,
|
||||
query::{self, QueryFileOptions},
|
||||
|
|
@ -33,7 +33,7 @@ use tree_sitter_cli::{
|
|||
wasm,
|
||||
};
|
||||
use tree_sitter_config::Config;
|
||||
use tree_sitter_generate::OptLevel;
|
||||
use tree_sitter_generate::{Diagnostic, GenerateError, OptLevel};
|
||||
use tree_sitter_highlight::Highlighter;
|
||||
use tree_sitter_loader::{self as loader, Bindings, TreeSitterJSON};
|
||||
use tree_sitter_tags::TagsContext;
|
||||
|
|
@ -188,10 +188,14 @@ struct Build {
|
|||
/// Compile a parser in debug mode
|
||||
#[arg(long, short = '0')]
|
||||
pub debug: bool,
|
||||
/// Display verbose build information
|
||||
#[arg(short, long)]
|
||||
pub verbose: bool,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
#[command(alias = "p")]
|
||||
#[command(group(ArgGroup::new("graph_output").multiple(true)))]
|
||||
struct Parse {
|
||||
/// The path to a file with paths to source file(s)
|
||||
#[arg(long = "paths")]
|
||||
|
|
@ -207,26 +211,29 @@ struct Parse {
|
|||
pub lib_path: Option<PathBuf>,
|
||||
/// If `--lib-path` is used, the name of the language used to extract the
|
||||
/// library's language function
|
||||
#[arg(long)]
|
||||
#[arg(long, requires = "lib_path")]
|
||||
pub lang_name: Option<String>,
|
||||
/// Select a language by the scope instead of a file extension
|
||||
#[arg(long)]
|
||||
pub scope: Option<String>,
|
||||
/// Show parsing debug log
|
||||
#[arg(long, short = 'd')] // TODO: Rework once clap adds `default_missing_value_t`
|
||||
#[allow(clippy::option_option)]
|
||||
#[expect(
|
||||
clippy::option_option,
|
||||
reason = "required by clap for optional flag with optional value"
|
||||
)]
|
||||
pub debug: Option<Option<ParseDebugType>>,
|
||||
/// Compile a parser in debug mode
|
||||
#[arg(long, short = '0')]
|
||||
pub debug_build: bool,
|
||||
/// Produce the log.html file with debug graphs
|
||||
#[arg(long, short = 'D')]
|
||||
#[arg(long, short = 'D', group = "graph_output")]
|
||||
pub debug_graph: bool,
|
||||
/// Compile parsers to Wasm instead of native dynamic libraries
|
||||
#[arg(long, hide = cfg!(not(feature = "wasm")))]
|
||||
pub wasm: bool,
|
||||
/// Output the parse data with graphviz dot
|
||||
#[arg(long = "dot")]
|
||||
#[arg(long = "dot", group = "graph_output")]
|
||||
pub output_dot: bool,
|
||||
/// Output the parse data in XML format
|
||||
#[arg(long = "xml", short = 'x')]
|
||||
|
|
@ -234,7 +241,7 @@ struct Parse {
|
|||
/// Output the parse data in a pretty-printed CST format
|
||||
#[arg(long = "cst", short = 'c')]
|
||||
pub output_cst: bool,
|
||||
/// Show parsing statistic
|
||||
/// Show parsing statistics
|
||||
#[arg(long, short, conflicts_with = "json", conflicts_with = "json_summary")]
|
||||
pub stat: bool,
|
||||
/// Interrupt the parsing process by timeout (µs)
|
||||
|
|
@ -246,7 +253,10 @@ struct Parse {
|
|||
/// Suppress main output
|
||||
#[arg(long, short)]
|
||||
pub quiet: bool,
|
||||
#[allow(clippy::doc_markdown)]
|
||||
#[expect(
|
||||
clippy::doc_markdown,
|
||||
reason = "doc string contains format syntax, not code identifiers"
|
||||
)]
|
||||
/// Apply edits in the format: \"row,col|position delcount insert_text\", can be supplied
|
||||
/// multiple times
|
||||
#[arg(
|
||||
|
|
@ -257,8 +267,8 @@ struct Parse {
|
|||
/// The encoding of the input files
|
||||
#[arg(long)]
|
||||
pub encoding: Option<Encoding>,
|
||||
/// Open `log.html` in the default browser, if `--debug-graph` is supplied
|
||||
#[arg(long)]
|
||||
/// Open `log.html` in the default browser, if `--debug-graph` or `--dot` is supplied
|
||||
#[arg(long, requires = "graph_output")]
|
||||
pub open_log: bool,
|
||||
/// Deprecated: use --json-summary
|
||||
#[arg(long, conflicts_with = "json_summary", conflicts_with = "stat")]
|
||||
|
|
@ -283,9 +293,9 @@ struct Parse {
|
|||
|
||||
#[derive(ValueEnum, Clone)]
|
||||
pub enum Encoding {
|
||||
Utf8,
|
||||
Utf16LE,
|
||||
Utf16BE,
|
||||
Utf8 = 0,
|
||||
Utf16LE = 1,
|
||||
Utf16BE = 2,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
|
|
@ -308,7 +318,7 @@ struct Test {
|
|||
pub lib_path: Option<PathBuf>,
|
||||
/// If `--lib-path` is used, the name of the language used to extract the
|
||||
/// library's language function
|
||||
#[arg(long)]
|
||||
#[arg(long, requires = "lib_path")]
|
||||
pub lang_name: Option<String>,
|
||||
/// Update all syntax trees in corpus files with current parser output
|
||||
#[arg(long, short)]
|
||||
|
|
@ -326,7 +336,7 @@ struct Test {
|
|||
#[arg(long, hide = cfg!(not(feature = "wasm")))]
|
||||
pub wasm: bool,
|
||||
/// Open `log.html` in the default browser, if `--debug-graph` is supplied
|
||||
#[arg(long)]
|
||||
#[arg(long, requires = "debug_graph")]
|
||||
pub open_log: bool,
|
||||
/// The path to an alternative config.json file
|
||||
#[arg(long)]
|
||||
|
|
@ -334,6 +344,9 @@ struct Test {
|
|||
/// Force showing fields in test diffs
|
||||
#[arg(long)]
|
||||
pub show_fields: bool,
|
||||
/// Force showing '+' and '-' in test diffs
|
||||
#[arg(long)]
|
||||
pub show_diff_markers: bool,
|
||||
/// Show parsing statistics
|
||||
#[arg(long)]
|
||||
pub stat: Option<TestStats>,
|
||||
|
|
@ -350,6 +363,10 @@ struct Test {
|
|||
|
||||
#[derive(Args)]
|
||||
#[command(alias = "publish")]
|
||||
#[expect(
|
||||
clippy::struct_field_names,
|
||||
reason = "field names map to CLI arguments"
|
||||
)]
|
||||
/// Display or increment the version of a grammar
|
||||
struct Version {
|
||||
/// The version to bump to
|
||||
|
|
@ -389,7 +406,7 @@ struct Fuzz {
|
|||
pub lib_path: Option<PathBuf>,
|
||||
/// If `--lib-path` is used, the name of the language used to extract the
|
||||
/// library's language function
|
||||
#[arg(long)]
|
||||
#[arg(long, requires = "lib_path")]
|
||||
pub lang_name: Option<String>,
|
||||
#[arg(
|
||||
long,
|
||||
|
|
@ -420,6 +437,10 @@ struct Fuzz {
|
|||
|
||||
#[derive(Args)]
|
||||
#[command(alias = "q")]
|
||||
#[expect(
|
||||
clippy::struct_field_names,
|
||||
reason = "field names map to CLI arguments"
|
||||
)]
|
||||
struct Query {
|
||||
/// Path to a file with queries
|
||||
#[arg(index = 1, required = true)]
|
||||
|
|
@ -432,7 +453,7 @@ struct Query {
|
|||
pub lib_path: Option<PathBuf>,
|
||||
/// If `--lib-path` is used, the name of the language used to extract the
|
||||
/// library's language function
|
||||
#[arg(long)]
|
||||
#[arg(long, requires = "lib_path")]
|
||||
pub lang_name: Option<String>,
|
||||
/// Measure execution time
|
||||
#[arg(long, short)]
|
||||
|
|
@ -487,14 +508,20 @@ struct Highlight {
|
|||
/// Generate highlighting as an HTML document
|
||||
#[arg(long, short = 'H')]
|
||||
pub html: bool,
|
||||
/// When generating HTML, use css classes rather than inline styles
|
||||
#[arg(long)]
|
||||
/// Deprecated: use `--style classes`
|
||||
#[arg(long, requires = "html", conflicts_with = "style")]
|
||||
pub css_classes: bool,
|
||||
/// When generating HTML, the document structure to emit
|
||||
#[arg(long, requires = "html", value_enum, default_value = "document")]
|
||||
pub layout: HtmlOutput,
|
||||
/// When generating HTML, how token colors are applied
|
||||
#[arg(long, requires = "html", value_enum, default_value = "classes")]
|
||||
pub style: HtmlStyling,
|
||||
/// Check that highlighting captures conform strictly to standards
|
||||
#[arg(long)]
|
||||
pub check: bool,
|
||||
/// The path to a file with captures
|
||||
#[arg(long)]
|
||||
#[arg(long, requires = "check")]
|
||||
pub captures_path: Option<PathBuf>,
|
||||
/// The paths to files with queries
|
||||
#[arg(long, num_args = 1..)]
|
||||
|
|
@ -527,6 +554,9 @@ struct Highlight {
|
|||
/// Force rebuild the parser
|
||||
#[arg(short, long)]
|
||||
pub rebuild: bool,
|
||||
/// The encoding of the input files
|
||||
#[arg(long)]
|
||||
pub encoding: Option<Encoding>,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
|
|
@ -789,7 +819,7 @@ impl Init {
|
|||
|
||||
let enabled = MultiSelect::new()
|
||||
.with_prompt("Bindings")
|
||||
.items_checked(&languages)
|
||||
.items_checked(languages.iter().copied())
|
||||
.interact()?
|
||||
.into_iter()
|
||||
.map(|i| languages[i].0);
|
||||
|
|
@ -863,7 +893,7 @@ impl Init {
|
|||
|
||||
let idx = FuzzySelect::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt("Which field would you like to change?")
|
||||
.items(&choices)
|
||||
.items(choices)
|
||||
.interact()?;
|
||||
|
||||
set_choice!(choices[idx]);
|
||||
|
|
@ -881,7 +911,7 @@ impl Init {
|
|||
|
||||
let new_config = format!("{}\n", serde_json::to_string_pretty(&json)?);
|
||||
// Write the re-serialized config back, as newly added optional boolean fields
|
||||
// will be included with explicit `false`s rather than implict `null`s
|
||||
// will be included with explicit `false`s rather than implicit `null`s
|
||||
if self.update && !old_config.trim().eq(new_config.trim()) {
|
||||
info!("Updating tree-sitter.json");
|
||||
fs::write(
|
||||
|
|
@ -928,7 +958,8 @@ impl Generate {
|
|||
self.json_summary
|
||||
};
|
||||
|
||||
if let Err(err) = tree_sitter_generate::generate_parser_in_directory(
|
||||
let mut diagnostics = Vec::new();
|
||||
let result = tree_sitter_generate::generate_parser_in_directory(
|
||||
current_dir,
|
||||
self.output.as_deref(),
|
||||
self.grammar_path.as_deref(),
|
||||
|
|
@ -941,16 +972,33 @@ impl Generate {
|
|||
} else {
|
||||
OptLevel::default()
|
||||
},
|
||||
) {
|
||||
if json_summary {
|
||||
eprintln!("{}", serde_json::to_string_pretty(&err)?);
|
||||
&mut diagnostics,
|
||||
);
|
||||
if json_summary {
|
||||
#[derive(serde::Serialize)]
|
||||
struct Envelope<'a> {
|
||||
diagnostics: &'a [Diagnostic],
|
||||
error: Option<&'a GenerateError>,
|
||||
}
|
||||
let envelope = Envelope {
|
||||
diagnostics: &diagnostics,
|
||||
error: result.as_ref().err(),
|
||||
};
|
||||
eprintln!("{}", serde_json::to_string_pretty(&envelope)?);
|
||||
if result.is_err() {
|
||||
// Exit early to prevent errors from being printed a second time in the caller
|
||||
std::process::exit(1);
|
||||
} else {
|
||||
}
|
||||
} else {
|
||||
for d in &diagnostics {
|
||||
warn!("{d}");
|
||||
}
|
||||
if let Err(err) = result {
|
||||
// Removes extra context associated with the error
|
||||
Err(anyhow!(err.to_string())).with_context(|| "Error when generating parser")?;
|
||||
}
|
||||
}
|
||||
|
||||
if self.build {
|
||||
warn!("--build is deprecated, use the `build` command");
|
||||
if let Some(path) = self.libdir {
|
||||
|
|
@ -968,6 +1016,7 @@ impl Build {
|
|||
let grammar_path = current_dir.join(self.path.unwrap_or_default());
|
||||
|
||||
loader.debug_build(self.debug);
|
||||
loader.verbose_build(self.verbose);
|
||||
|
||||
if self.wasm {
|
||||
let output_path = self.output.map(|path| current_dir.join(path));
|
||||
|
|
@ -985,7 +1034,7 @@ impl Build {
|
|||
.context("Output path must have a parent")?;
|
||||
let name = full_path
|
||||
.file_name()
|
||||
.context("Ouput path must have a filename")?;
|
||||
.context("Output path must have a filename")?;
|
||||
fs::create_dir_all(parent_path).context("Failed to create output path")?;
|
||||
let mut canon_path = parent_path.canonicalize().context("Invalid output path")?;
|
||||
canon_path.push(name);
|
||||
|
|
@ -1023,7 +1072,6 @@ impl Build {
|
|||
impl Parse {
|
||||
fn run(self, mut loader: loader::Loader, current_dir: &Path) -> Result<()> {
|
||||
let config = Config::load(self.config_path)?;
|
||||
let color = env::var("NO_COLOR").map_or(true, |v| v != "1");
|
||||
let json_summary = if self.json {
|
||||
warn!("--json is deprecated, use --json-summary instead");
|
||||
true
|
||||
|
|
@ -1042,7 +1090,7 @@ impl Parse {
|
|||
ParseOutput::Normal
|
||||
};
|
||||
|
||||
let parse_theme = if color {
|
||||
let parse_theme = if paint::color_enabled() {
|
||||
config
|
||||
.get::<parse::Config>()
|
||||
.with_context(|| "Failed to parse CST theme")?
|
||||
|
|
@ -1080,9 +1128,6 @@ impl Parse {
|
|||
let timeout = self.timeout.unwrap_or_default();
|
||||
|
||||
let mut has_error = false;
|
||||
let loader_config = config.get()?;
|
||||
loader.find_all_languages(&loader_config)?;
|
||||
|
||||
let should_track_stats = self.stat;
|
||||
let mut stats = parse::ParseStats::default();
|
||||
let debug: ParseDebugType = match self.debug {
|
||||
|
|
@ -1125,10 +1170,11 @@ impl Parse {
|
|||
has_error |= !parse_result.successful;
|
||||
};
|
||||
|
||||
if self.lib_path.is_none() && self.lang_name.is_some() {
|
||||
warn!("--lang-name` specified without --lib-path. This argument will be ignored.");
|
||||
}
|
||||
let lib_info = get_lib_info(self.lib_path.as_ref(), self.lang_name.as_ref(), current_dir);
|
||||
if lib_info.is_none() {
|
||||
let loader_config = config.get()?;
|
||||
loader.find_all_languages(&loader_config)?;
|
||||
}
|
||||
|
||||
let input = get_input(
|
||||
self.paths_file.as_deref(),
|
||||
|
|
@ -1279,7 +1325,6 @@ fn check_test(
|
|||
impl Test {
|
||||
fn run(self, mut loader: loader::Loader, current_dir: &Path) -> Result<()> {
|
||||
let config = Config::load(self.config_path)?;
|
||||
let color = env::var("NO_COLOR").map_or(true, |v| v != "1");
|
||||
let stat = self.stat.unwrap_or_default();
|
||||
|
||||
loader.debug_build(self.debug_build);
|
||||
|
|
@ -1297,9 +1342,6 @@ impl Test {
|
|||
});
|
||||
}
|
||||
|
||||
if self.lib_path.is_none() && self.lang_name.is_some() {
|
||||
warn!("--lang-name` specified without --lib-path. This argument will be ignored.");
|
||||
}
|
||||
let languages = loader.languages_at_path(current_dir)?;
|
||||
let language = if let Some(ref lib_path) = self.lib_path {
|
||||
let lib_info =
|
||||
|
|
@ -1321,13 +1363,9 @@ impl Test {
|
|||
parser.set_language(language)?;
|
||||
|
||||
let test_dir = current_dir.join("test");
|
||||
let mut test_summary = TestSummary::new(
|
||||
color,
|
||||
stat,
|
||||
self.update,
|
||||
self.overview_only,
|
||||
self.json_summary,
|
||||
);
|
||||
let mut test_summary =
|
||||
TestSummary::new(stat, self.update, self.overview_only, self.json_summary);
|
||||
test_summary.use_markers = self.show_diff_markers;
|
||||
|
||||
// Run the corpus tests. Look for them in `test/corpus`.
|
||||
let test_corpus_dir = test_dir.join("corpus");
|
||||
|
|
@ -1342,7 +1380,6 @@ impl Test {
|
|||
update: self.update,
|
||||
open_log: self.open_log,
|
||||
languages: languages.iter().map(|(l, n)| (n.as_str(), l)).collect(),
|
||||
color,
|
||||
show_fields: self.show_fields,
|
||||
overview_only: self.overview_only,
|
||||
};
|
||||
|
|
@ -1353,16 +1390,20 @@ impl Test {
|
|||
self.json_summary,
|
||||
)?;
|
||||
test_summary.test_num = 1;
|
||||
} else {
|
||||
warn!("Test corpus not found at {}", test_corpus_dir.display());
|
||||
}
|
||||
|
||||
// Check that all of the queries are valid.
|
||||
let query_dir = current_dir.join("queries");
|
||||
check_test(
|
||||
test::check_queries_at_path(language, &query_dir),
|
||||
&test_summary,
|
||||
self.json_summary,
|
||||
)?;
|
||||
test_summary.test_num = 1;
|
||||
if query_dir.is_dir() {
|
||||
check_test(
|
||||
test::check_queries_at_path(language, &query_dir),
|
||||
&test_summary,
|
||||
self.json_summary,
|
||||
)?;
|
||||
test_summary.test_num = 1;
|
||||
}
|
||||
|
||||
// Run the syntax highlighting tests.
|
||||
let test_highlight_dir = test_dir.join("highlight");
|
||||
|
|
@ -1405,7 +1446,7 @@ impl Test {
|
|||
// For the rest of the queries, find their tests and run them
|
||||
for entry in walkdir::WalkDir::new(&query_dir)
|
||||
.into_iter()
|
||||
.filter_map(|e| e.ok())
|
||||
.filter_map(std::result::Result::ok)
|
||||
.filter(|e| e.file_type().is_file())
|
||||
{
|
||||
let stem = entry
|
||||
|
|
@ -1476,9 +1517,6 @@ impl Fuzz {
|
|||
loader.sanitize_build(true);
|
||||
loader.force_rebuild(self.rebuild || self.grammar_path.is_some());
|
||||
|
||||
if self.lib_path.is_none() && self.lang_name.is_some() {
|
||||
warn!("--lang-name` specified without --lib-path. This argument will be ignored.");
|
||||
}
|
||||
let languages = loader.languages_at_path(current_dir)?;
|
||||
let (language, language_name) = if let Some(ref lib_path) = self.lib_path {
|
||||
let lib_info = get_lib_info(Some(lib_path), self.lang_name.as_ref(), current_dir)
|
||||
|
|
@ -1526,24 +1564,23 @@ impl Fuzz {
|
|||
impl Query {
|
||||
fn run(self, mut loader: loader::Loader, current_dir: &Path) -> Result<()> {
|
||||
let config = Config::load(self.config_path)?;
|
||||
let loader_config = config.get()?;
|
||||
let lib_info = get_lib_info(self.lib_path.as_ref(), self.lang_name.as_ref(), current_dir);
|
||||
if lib_info.is_none() {
|
||||
let loader_config = config.get()?;
|
||||
loader.find_all_languages(&loader_config)?;
|
||||
}
|
||||
loader.force_rebuild(self.rebuild || self.grammar_path.is_some());
|
||||
loader.find_all_languages(&loader_config)?;
|
||||
let query_path = Path::new(&self.query_path);
|
||||
|
||||
let byte_range = parse_range(&self.byte_range, |x| x)?;
|
||||
let point_range = parse_range(&self.row_range, |row| Point::new(row, 0))?;
|
||||
let containing_byte_range = parse_range(&self.containing_byte_range, |x| x)?;
|
||||
let containing_point_range =
|
||||
parse_range(&self.containing_row_range, |row| Point::new(row, 0))?;
|
||||
let byte_range = parse_range(self.byte_range.as_deref(), |x| x)?;
|
||||
let point_range = parse_range(self.row_range.as_deref(), |row| Point::new(row, 0))?;
|
||||
let containing_byte_range = parse_range(self.containing_byte_range.as_deref(), |x| x)?;
|
||||
let containing_point_range = parse_range(self.containing_row_range.as_deref(), |row| {
|
||||
Point::new(row, 0)
|
||||
})?;
|
||||
|
||||
let cancellation_flag = util::cancel_on_signal();
|
||||
|
||||
if self.lib_path.is_none() && self.lang_name.is_some() {
|
||||
warn!("--lang-name specified without --lib-path. This argument will be ignored.");
|
||||
}
|
||||
let lib_info = get_lib_info(self.lib_path.as_ref(), self.lang_name.as_ref(), current_dir);
|
||||
|
||||
let input = get_input(
|
||||
self.paths_file.as_deref(),
|
||||
self.paths,
|
||||
|
|
@ -1667,15 +1704,29 @@ impl Highlight {
|
|||
}
|
||||
}
|
||||
|
||||
let encoding = self.encoding.map(|e| match e {
|
||||
Encoding::Utf8 => ffi::TSInputEncodingUTF8,
|
||||
Encoding::Utf16LE => ffi::TSInputEncodingUTF16LE,
|
||||
Encoding::Utf16BE => ffi::TSInputEncodingUTF16BE,
|
||||
});
|
||||
|
||||
let style = if self.css_classes {
|
||||
// TODO: Remove during the 0.28 release cycle
|
||||
warn!("--css-classes is deprecated, use --style classes instead");
|
||||
HtmlStyling::Classes
|
||||
} else {
|
||||
self.style
|
||||
};
|
||||
|
||||
let options = HighlightOptions {
|
||||
theme: theme_config.theme,
|
||||
check: self.check,
|
||||
captures_path: self.captures_path,
|
||||
inline_styles: !self.css_classes,
|
||||
html: self.html,
|
||||
html: self.html.then_some((self.layout, style)),
|
||||
quiet: self.quiet,
|
||||
print_time: self.time,
|
||||
cancellation_flag: cancellation_flag.clone(),
|
||||
encoding,
|
||||
};
|
||||
|
||||
let input = get_input(
|
||||
|
|
@ -1760,7 +1811,7 @@ impl Highlight {
|
|||
let path = get_tmp_source_file(&contents)?;
|
||||
|
||||
let (language, language_config) =
|
||||
if let (Some(l), Some(lc)) = (language.clone(), language_configuration) {
|
||||
if let (Some(l), Some(lc)) = (language, language_configuration) {
|
||||
(l, lc)
|
||||
} else {
|
||||
let language = languages
|
||||
|
|
@ -1902,7 +1953,7 @@ impl Tags {
|
|||
let path = get_tmp_source_file(&contents)?;
|
||||
|
||||
let (language, language_config) =
|
||||
if let (Some(l), Some(lc)) = (language.clone(), language_configuration) {
|
||||
if let (Some(l), Some(lc)) = (language, language_configuration) {
|
||||
(l, lc)
|
||||
} else {
|
||||
let languages = loader.languages_at_path(current_dir)?;
|
||||
|
|
@ -1956,7 +2007,7 @@ impl DumpLanguages {
|
|||
concat!(
|
||||
"name: {}\n",
|
||||
"scope: {}\n",
|
||||
"parser: {:?}\n",
|
||||
"parser: {}\n",
|
||||
"highlights: {:?}\n",
|
||||
"file_types: {:?}\n",
|
||||
"content_regex: {:?}\n",
|
||||
|
|
@ -1964,7 +2015,7 @@ impl DumpLanguages {
|
|||
),
|
||||
configuration.language_name,
|
||||
configuration.scope.as_ref().unwrap_or(&String::new()),
|
||||
language_path,
|
||||
language_path.display(),
|
||||
configuration.highlights_filenames,
|
||||
configuration.file_types,
|
||||
configuration.content_regex,
|
||||
|
|
@ -1997,10 +2048,10 @@ fn main() {
|
|||
let result = run();
|
||||
if let Err(err) = &result {
|
||||
// Ignore BrokenPipe errors
|
||||
if let Some(error) = err.downcast_ref::<std::io::Error>() {
|
||||
if error.kind() == std::io::ErrorKind::BrokenPipe {
|
||||
return;
|
||||
}
|
||||
if let Some(error) = err.downcast_ref::<std::io::Error>()
|
||||
&& error.kind() == std::io::ErrorKind::BrokenPipe
|
||||
{
|
||||
return;
|
||||
}
|
||||
if !err.to_string().is_empty() {
|
||||
error!("{err:?}");
|
||||
|
|
@ -2053,7 +2104,7 @@ fn run() -> Result<()> {
|
|||
| Commands::Complete(_) => &None,
|
||||
}
|
||||
.as_ref()
|
||||
.map_or_else(|| env::current_dir().unwrap(), |p| p.clone());
|
||||
.map_or_else(|| env::current_dir().unwrap(), std::clone::Clone::clone);
|
||||
|
||||
let loader = loader::Loader::new()?;
|
||||
|
||||
|
|
@ -2125,7 +2176,7 @@ fn get_lib_info<'a>(
|
|||
// Use the user-specified name if present, otherwise try to derive it from
|
||||
// the lib path
|
||||
match (
|
||||
language_name.map(|s| s.as_str()),
|
||||
language_name.map(std::string::String::as_str),
|
||||
lib_path.file_stem().and_then(|s| s.to_str()),
|
||||
) {
|
||||
(Some(name), _) | (None, Some(name)) => Some((absolute_lib_path, name)),
|
||||
|
|
@ -2138,10 +2189,10 @@ fn get_lib_info<'a>(
|
|||
|
||||
/// Parse a range string of the form "start:end" into an optional Range<T>.
|
||||
fn parse_range<T>(
|
||||
range_str: &Option<String>,
|
||||
range_str: Option<&str>,
|
||||
make: impl Fn(usize) -> T,
|
||||
) -> Result<Option<std::ops::Range<T>>> {
|
||||
if let Some(range) = range_str.as_ref() {
|
||||
if let Some(range) = range_str {
|
||||
let err_msg = format!("Invalid range '{range}', expected 'start:end'");
|
||||
let mut parts = range.split(':');
|
||||
|
||||
|
|
|
|||
27
crates/cli/src/paint.rs
Normal file
27
crates/cli/src/paint.rs
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
use anstyle::{AnsiColor, Color, Style};
|
||||
|
||||
pub const RED: Style = Style::new().fg_color(Some(Color::Ansi(AnsiColor::Red)));
|
||||
pub const YELLOW: Style = Style::new().fg_color(Some(Color::Ansi(AnsiColor::Yellow)));
|
||||
|
||||
/// Wraps a `Display` value with a style; emits ANSI codes only when
|
||||
/// [`color_enabled`] is true.
|
||||
pub struct Paint<T>(pub Style, pub T);
|
||||
|
||||
pub fn color_enabled() -> bool {
|
||||
static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
|
||||
*ENABLED.get_or_init(|| std::env::var_os("NO_COLOR").is_none_or(|v| v.is_empty()))
|
||||
}
|
||||
|
||||
pub fn paint<T>(color: Option<impl Into<Color>>, text: T) -> Paint<T> {
|
||||
Paint(Style::new().fg_color(color.map(Into::into)), text)
|
||||
}
|
||||
|
||||
impl<T: std::fmt::Display> std::fmt::Display for Paint<T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
if color_enabled() {
|
||||
write!(f, "{}{}{:#}", self.0, self.1, self.0)
|
||||
} else {
|
||||
self.1.fmt(f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,17 +8,17 @@ use std::{
|
|||
};
|
||||
|
||||
use anstyle::{AnsiColor, Color, RgbColor};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use anyhow::{Context, Result, anyhow};
|
||||
use clap::ValueEnum;
|
||||
use log::info;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tree_sitter::{
|
||||
ffi, InputEdit, Language, LogType, ParseOptions, ParseState, Parser, Point, Range, Tree,
|
||||
TreeCursor,
|
||||
InputEdit, Language, LogType, ParseOptions, ParseState, Parser, Point, Range, Tree, TreeCursor,
|
||||
ffi,
|
||||
};
|
||||
|
||||
use crate::{fuzz::edits::Edit, logger::paint, util};
|
||||
use crate::{fuzz::edits::Edit, paint::paint, util};
|
||||
|
||||
#[derive(Debug, Default, Serialize, JsonSchema)]
|
||||
pub struct Stats {
|
||||
|
|
@ -286,6 +286,10 @@ pub fn parse_file_at_path(
|
|||
max_path_length: usize,
|
||||
opts: &mut ParseFileOptions,
|
||||
) -> Result<()> {
|
||||
#[expect(
|
||||
clippy::collection_is_never_read,
|
||||
reason = "value is held for its Drop side effect"
|
||||
)]
|
||||
let mut _log_session = None;
|
||||
parser.set_language(language)?;
|
||||
let mut source_code = fs::read(path).with_context(|| format!("Error reading {name:?}"))?;
|
||||
|
|
@ -297,7 +301,6 @@ pub fn parse_file_at_path(
|
|||
// Log to stderr if `--debug` was passed
|
||||
else if opts.debug != ParseDebugType::Quiet {
|
||||
let mut curr_version: usize = 0;
|
||||
let use_color = std::env::var("NO_COLOR").map_or(true, |v| v != "1");
|
||||
let debug = opts.debug;
|
||||
parser.set_logger(Some(Box::new(move |log_type, message| {
|
||||
if debug == ParseDebugType::Normal {
|
||||
|
|
@ -306,13 +309,13 @@ pub fn parse_file_at_path(
|
|||
}
|
||||
writeln!(&mut io::stderr(), "{message}").unwrap();
|
||||
} else {
|
||||
#[rustfmt::skip]
|
||||
let colors = &[
|
||||
AnsiColor::White,
|
||||
AnsiColor::Red,
|
||||
AnsiColor::Blue,
|
||||
AnsiColor::Green,
|
||||
AnsiColor::Cyan,
|
||||
AnsiColor::Yellow,
|
||||
AnsiColor::White, AnsiColor::Red, AnsiColor::Blue, AnsiColor::Green,
|
||||
AnsiColor::Cyan, AnsiColor::Yellow, AnsiColor::Magenta,
|
||||
AnsiColor::BrightWhite, AnsiColor::BrightRed, AnsiColor::BrightBlue,
|
||||
AnsiColor::BrightGreen, AnsiColor::BrightCyan, AnsiColor::BrightYellow,
|
||||
AnsiColor::BrightMagenta,
|
||||
];
|
||||
if message.starts_with("process version:") {
|
||||
let comma_idx = message.find(',').unwrap();
|
||||
|
|
@ -320,30 +323,21 @@ pub fn parse_file_at_path(
|
|||
.parse()
|
||||
.unwrap();
|
||||
}
|
||||
let color = if use_color {
|
||||
Some(colors[curr_version])
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let mut out = if log_type == LogType::Lex {
|
||||
" ".to_string()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
out += &paint(color, message);
|
||||
writeln!(&mut io::stderr(), "{out}").unwrap();
|
||||
let color = Some(colors[curr_version % colors.len()]);
|
||||
let prefix = if log_type == LogType::Lex { " " } else { "" };
|
||||
writeln!(&mut io::stderr(), "{prefix}{}", paint(color, message)).unwrap();
|
||||
}
|
||||
})));
|
||||
}
|
||||
|
||||
let parse_time = Instant::now();
|
||||
|
||||
#[inline(always)]
|
||||
#[inline]
|
||||
fn is_utf16_le_bom(bom_bytes: &[u8]) -> bool {
|
||||
bom_bytes == [0xFF, 0xFE]
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
#[inline]
|
||||
fn is_utf16_be_bom(bom_bytes: &[u8]) -> bool {
|
||||
bom_bytes == [0xFE, 0xFF]
|
||||
}
|
||||
|
|
@ -368,13 +362,13 @@ pub fn parse_file_at_path(
|
|||
// after the specified number of microseconds.
|
||||
let start_time = Instant::now();
|
||||
let progress_callback = &mut |_: &ParseState| {
|
||||
if let Some(cancellation_flag) = opts.cancellation_flag {
|
||||
if cancellation_flag.load(Ordering::SeqCst) != 0 {
|
||||
return ControlFlow::Break(());
|
||||
}
|
||||
if let Some(cancellation_flag) = opts.cancellation_flag
|
||||
&& cancellation_flag.load(Ordering::SeqCst) != 0
|
||||
{
|
||||
return ControlFlow::Break(());
|
||||
}
|
||||
|
||||
if opts.timeout > 0 && start_time.elapsed().as_micros() > opts.timeout as u128 {
|
||||
if opts.timeout > 0 && start_time.elapsed().as_micros() > u128::from(opts.timeout) {
|
||||
return ControlFlow::Break(());
|
||||
}
|
||||
|
||||
|
|
@ -386,8 +380,10 @@ pub fn parse_file_at_path(
|
|||
let tree = match encoding {
|
||||
Some(encoding) if encoding == ffi::TSInputEncodingUTF16LE => {
|
||||
let source_code_utf16 = source_code
|
||||
.chunks_exact(2)
|
||||
.map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]]))
|
||||
.as_chunks::<2>()
|
||||
.0
|
||||
.iter()
|
||||
.map(|&chunk| u16::from_le_bytes(chunk))
|
||||
.collect::<Vec<_>>();
|
||||
parser.parse_utf16_le_with_options(
|
||||
&mut |i, _| {
|
||||
|
|
@ -403,8 +399,10 @@ pub fn parse_file_at_path(
|
|||
}
|
||||
Some(encoding) if encoding == ffi::TSInputEncodingUTF16BE => {
|
||||
let source_code_utf16 = source_code
|
||||
.chunks_exact(2)
|
||||
.map(|chunk| u16::from_be_bytes([chunk[0], chunk[1]]))
|
||||
.as_chunks::<2>()
|
||||
.0
|
||||
.iter()
|
||||
.map(|&chunk| u16::from_be_bytes(chunk))
|
||||
.collect::<Vec<_>>();
|
||||
parser.parse_utf16_be_with_options(
|
||||
&mut |i, _| {
|
||||
|
|
@ -433,7 +431,7 @@ pub fn parse_file_at_path(
|
|||
let parse_duration = parse_time.elapsed();
|
||||
|
||||
let stdout = io::stdout();
|
||||
let mut stdout = stdout.lock();
|
||||
let mut stdout = io::BufWriter::with_capacity(64 * 1024, stdout.lock());
|
||||
|
||||
if let Some(mut tree) = tree {
|
||||
if opts.debug_graph && !opts.edits.is_empty() {
|
||||
|
|
@ -510,7 +508,7 @@ pub fn parse_file_at_path(
|
|||
}
|
||||
}
|
||||
cursor.reset(tree.root_node());
|
||||
println!();
|
||||
writeln!(&mut stdout)?;
|
||||
}
|
||||
|
||||
if opts.output == ParseOutput::Cst {
|
||||
|
|
@ -544,10 +542,10 @@ pub fn parse_file_at_path(
|
|||
}
|
||||
write!(&mut stdout, "</{}>", tag.expect("there is a tag"))?;
|
||||
// we only write a line in the case where it's the last sibling
|
||||
if let Some(parent) = node.parent() {
|
||||
if parent.child(parent.child_count() as u32 - 1).unwrap() == node {
|
||||
stdout.write_all(b"\n")?;
|
||||
}
|
||||
if let Some(parent) = node.parent()
|
||||
&& parent.child(parent.child_count() - 1).unwrap() == node
|
||||
{
|
||||
stdout.write_all(b"\n")?;
|
||||
}
|
||||
needs_newline = true;
|
||||
}
|
||||
|
|
@ -581,11 +579,11 @@ pub fn parse_file_at_path(
|
|||
}
|
||||
let start = node.start_position();
|
||||
let end = node.end_position();
|
||||
write!(&mut stdout, " srow=\"{}\"", start.row)?;
|
||||
write!(&mut stdout, " scol=\"{}\"", start.column)?;
|
||||
write!(&mut stdout, " erow=\"{}\"", end.row)?;
|
||||
write!(&mut stdout, " ecol=\"{}\"", end.column)?;
|
||||
write!(&mut stdout, ">")?;
|
||||
write!(
|
||||
&mut stdout,
|
||||
" srow=\"{}\" scol=\"{}\" erow=\"{}\" ecol=\"{}\">",
|
||||
start.row, start.column, end.row, end.column
|
||||
)?;
|
||||
tags.push(node.kind());
|
||||
needs_newline = true;
|
||||
}
|
||||
|
|
@ -776,12 +774,16 @@ pub fn render_cst<'a, 'b: 'a>(
|
|||
cursor: &mut TreeCursor<'a>,
|
||||
opts: &ParseFileOptions,
|
||||
out: &mut impl Write,
|
||||
) -> Result<()> {
|
||||
) -> io::Result<()> {
|
||||
let lossy_source_code = String::from_utf8_lossy(source_code);
|
||||
let total_width = lossy_source_code
|
||||
.lines()
|
||||
.enumerate()
|
||||
.map(|(row, col)| (row as f64).log10() as usize + (col.len() as f64).log10() as usize + 1)
|
||||
.map(|(row, col)| {
|
||||
row.checked_ilog10().unwrap_or(0) as usize
|
||||
+ col.len().checked_ilog10().unwrap_or(0) as usize
|
||||
+ 1
|
||||
})
|
||||
.max()
|
||||
.unwrap_or(1);
|
||||
let mut indent_level = usize::from(!opts.no_ranges);
|
||||
|
|
@ -825,19 +827,19 @@ pub fn render_cst<'a, 'b: 'a>(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn render_node_text(source: &str) -> String {
|
||||
source
|
||||
.chars()
|
||||
.fold(String::with_capacity(source.len()), |mut acc, c| {
|
||||
if let Some(esc) = escape_invisible(c) {
|
||||
acc.push_str(esc);
|
||||
} else if let Some(esc) = escape_delimiter(c) {
|
||||
acc.push_str(esc);
|
||||
} else {
|
||||
acc.push(c);
|
||||
struct CstNodeText<'a>(&'a str);
|
||||
|
||||
impl std::fmt::Display for CstNodeText<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
use std::fmt::Write as _;
|
||||
for c in self.0.chars() {
|
||||
match escape_invisible(c).or_else(|| escape_delimiter(c)) {
|
||||
Some(esc) => f.write_str(esc)?,
|
||||
None => f.write_char(c)?,
|
||||
}
|
||||
acc
|
||||
})
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn write_node_text(
|
||||
|
|
@ -848,21 +850,21 @@ fn write_node_text(
|
|||
source: &str,
|
||||
color: Option<impl Into<Color> + Copy>,
|
||||
text_info: (usize, usize),
|
||||
) -> Result<()> {
|
||||
) -> io::Result<()> {
|
||||
let (total_width, indent_level) = text_info;
|
||||
let (quote, quote_color) = if is_named {
|
||||
('`', opts.parse_theme.backtick)
|
||||
} else {
|
||||
('\"', color.map(|c| c.into()))
|
||||
('\"', color.map(std::convert::Into::into))
|
||||
};
|
||||
|
||||
if !is_named {
|
||||
write!(
|
||||
out,
|
||||
"{}{}{}",
|
||||
paint(quote_color, &String::from(quote)),
|
||||
paint(color, &render_node_text(source)),
|
||||
paint(quote_color, &String::from(quote)),
|
||||
paint(quote_color, quote),
|
||||
paint(color, CstNodeText(source)),
|
||||
paint(quote_color, quote),
|
||||
)?;
|
||||
} else {
|
||||
let multiline = source.contains('\n');
|
||||
|
|
@ -881,24 +883,34 @@ fn write_node_text(
|
|||
} else {
|
||||
0
|
||||
};
|
||||
let formatted_line = render_line_feed(line, opts);
|
||||
if multiline {
|
||||
writeln!(out)?;
|
||||
if !opts.no_ranges {
|
||||
write!(
|
||||
out,
|
||||
"{}",
|
||||
CstNodeRange {
|
||||
opts,
|
||||
has_field_name: cursor.field_name().is_some(),
|
||||
is_named,
|
||||
is_multiline: true,
|
||||
total_width,
|
||||
range: node_range,
|
||||
}
|
||||
)?;
|
||||
}
|
||||
for _ in 0..=indent_level {
|
||||
write!(out, " ")?;
|
||||
}
|
||||
} else {
|
||||
write!(out, " ")?;
|
||||
}
|
||||
write!(
|
||||
out,
|
||||
"{}{}{}{}{}{}",
|
||||
if multiline { "\n" } else { " " },
|
||||
if multiline && !opts.no_ranges {
|
||||
render_node_range(opts, cursor, is_named, true, total_width, node_range)
|
||||
} else {
|
||||
String::new()
|
||||
},
|
||||
if multiline {
|
||||
" ".repeat(indent_level + 1)
|
||||
} else {
|
||||
String::new()
|
||||
},
|
||||
paint(quote_color, &String::from(quote)),
|
||||
paint(color, &render_node_text(&formatted_line)),
|
||||
paint(quote_color, &String::from(quote)),
|
||||
"{}{}{}",
|
||||
paint(quote_color, quote),
|
||||
paint(color, CstLineFeed { source: line, opts }),
|
||||
paint(quote_color, quote),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
|
@ -906,67 +918,93 @@ fn write_node_text(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn render_line_feed(source: &str, opts: &ParseFileOptions) -> String {
|
||||
if cfg!(windows) {
|
||||
source.replace("\r\n", &paint(opts.parse_theme.line_feed, "\r\n"))
|
||||
} else {
|
||||
source.replace('\n', &paint(opts.parse_theme.line_feed, "\n"))
|
||||
struct CstLineFeed<'src, 'opt> {
|
||||
source: &'src str,
|
||||
opts: &'src ParseFileOptions<'opt>,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for CstLineFeed<'_, '_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
#[cfg(windows)]
|
||||
let lf = "\r\n";
|
||||
#[cfg(not(windows))]
|
||||
let lf = "\n";
|
||||
let painted = paint(self.opts.parse_theme.line_feed, CstNodeText(lf));
|
||||
let mut parts = self.source.split(lf);
|
||||
if let Some(first) = parts.next() {
|
||||
write!(f, "{}", CstNodeText(first))?;
|
||||
}
|
||||
for part in parts {
|
||||
write!(f, "{painted}{}", CstNodeText(part))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn render_node_range(
|
||||
opts: &ParseFileOptions,
|
||||
cursor: &TreeCursor,
|
||||
struct CstNodeRange<'src, 'opt> {
|
||||
opts: &'src ParseFileOptions<'opt>,
|
||||
has_field_name: bool,
|
||||
is_named: bool,
|
||||
is_multiline: bool,
|
||||
total_width: usize,
|
||||
range: Range,
|
||||
) -> String {
|
||||
let has_field_name = cursor.field_name().is_some();
|
||||
let range_color = if is_named && !is_multiline && !has_field_name {
|
||||
opts.parse_theme.row_color_named
|
||||
} else {
|
||||
opts.parse_theme.row_color
|
||||
};
|
||||
}
|
||||
|
||||
let remaining_width_start = (total_width
|
||||
- (range.start_point.row as f64).log10() as usize
|
||||
- (range.start_point.column as f64).log10() as usize)
|
||||
.max(1);
|
||||
let remaining_width_end = (total_width
|
||||
- (range.end_point.row as f64).log10() as usize
|
||||
- (range.end_point.column as f64).log10() as usize)
|
||||
.max(1);
|
||||
paint(
|
||||
range_color,
|
||||
&format!(
|
||||
"{}:{}{:remaining_width_start$}- {}:{}{:remaining_width_end$}",
|
||||
range.start_point.row,
|
||||
range.start_point.column,
|
||||
' ',
|
||||
range.end_point.row,
|
||||
range.end_point.column,
|
||||
' ',
|
||||
),
|
||||
)
|
||||
impl std::fmt::Display for CstNodeRange<'_, '_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let start = self.range.start_point;
|
||||
let end = self.range.end_point;
|
||||
let range_color = if self.is_named && !self.is_multiline && !self.has_field_name {
|
||||
self.opts.parse_theme.row_color_named
|
||||
} else {
|
||||
self.opts.parse_theme.row_color
|
||||
};
|
||||
let remaining_width = |row: usize, col: usize| {
|
||||
(self
|
||||
.total_width
|
||||
.saturating_sub(row.checked_ilog10().unwrap_or(0) as usize)
|
||||
.saturating_sub(col.checked_ilog10().unwrap_or(0) as usize))
|
||||
.max(1)
|
||||
};
|
||||
let remaining_width_start = remaining_width(start.row, start.column);
|
||||
let remaining_width_end = remaining_width(end.row, end.column);
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
paint(
|
||||
range_color,
|
||||
format_args!(
|
||||
"{}:{}{:remaining_width_start$}- {}:{}{:remaining_width_end$}",
|
||||
start.row, start.column, ' ', end.row, end.column, ' ',
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn cst_render_node(
|
||||
opts: &ParseFileOptions,
|
||||
cursor: &mut TreeCursor,
|
||||
cursor: &TreeCursor,
|
||||
source_code: &[u8],
|
||||
out: &mut impl Write,
|
||||
total_width: usize,
|
||||
indent_level: usize,
|
||||
in_error: bool,
|
||||
) -> Result<()> {
|
||||
) -> io::Result<()> {
|
||||
let node = cursor.node();
|
||||
let is_named = node.is_named();
|
||||
if !opts.no_ranges {
|
||||
write!(
|
||||
out,
|
||||
"{}",
|
||||
render_node_range(opts, cursor, is_named, false, total_width, node.range())
|
||||
CstNodeRange {
|
||||
opts,
|
||||
has_field_name: cursor.field_name().is_some(),
|
||||
is_named,
|
||||
is_multiline: false,
|
||||
total_width,
|
||||
range: node.range(),
|
||||
}
|
||||
)?;
|
||||
}
|
||||
write!(
|
||||
|
|
@ -984,7 +1022,7 @@ fn cst_render_node(
|
|||
write!(
|
||||
out,
|
||||
"{}",
|
||||
paint(opts.parse_theme.field, &format!("{field_name}: "))
|
||||
paint(opts.parse_theme.field, format_args!("{field_name}: "))
|
||||
)?;
|
||||
}
|
||||
|
||||
|
|
@ -1055,10 +1093,13 @@ pub fn perform_edit(tree: &mut Tree, input: &mut Vec<u8>, edit: &Edit) -> Result
|
|||
|
||||
fn parse_edit_flag(source_code: &[u8], flag: &str) -> Result<Edit> {
|
||||
let error = || {
|
||||
anyhow!(concat!(
|
||||
"Invalid edit string '{}'. ",
|
||||
"Edit strings must match the pattern '<START_BYTE_OR_POSITION> <REMOVED_LENGTH> <NEW_TEXT>'"
|
||||
), flag)
|
||||
anyhow!(
|
||||
concat!(
|
||||
"Invalid edit string '{}'. ",
|
||||
"Edit strings must match the pattern '<START_BYTE_OR_POSITION> <REMOVED_LENGTH> <NEW_TEXT>'"
|
||||
),
|
||||
flag
|
||||
)
|
||||
};
|
||||
|
||||
// Three whitespace-separated parts:
|
||||
|
|
@ -1096,30 +1137,25 @@ fn parse_edit_flag(source_code: &[u8], flag: &str) -> Result<Edit> {
|
|||
|
||||
pub fn offset_for_position(input: &[u8], position: Point) -> Result<usize> {
|
||||
let mut row = 0;
|
||||
let mut offset = 0;
|
||||
let mut iter = memchr::memchr_iter(b'\n', input);
|
||||
loop {
|
||||
if let Some(pos) = iter.next() {
|
||||
if row < position.row {
|
||||
row += 1;
|
||||
offset = pos;
|
||||
continue;
|
||||
let mut line_start = 0;
|
||||
for line_end in memchr::memchr_iter(b'\n', input) {
|
||||
if row == position.row {
|
||||
if position.column > line_end - line_start {
|
||||
return Err(anyhow!("Failed to address a column: {}", position.column));
|
||||
}
|
||||
return Ok(line_start + position.column);
|
||||
}
|
||||
offset += 1;
|
||||
break;
|
||||
row += 1;
|
||||
line_start = line_end + 1;
|
||||
}
|
||||
if position.row - row > 0 {
|
||||
|
||||
if row != position.row {
|
||||
return Err(anyhow!("Failed to address a row: {}", position.row));
|
||||
}
|
||||
if let Some(pos) = iter.next() {
|
||||
if (pos - offset < position.column) || (input[offset] == b'\n' && position.column > 0) {
|
||||
return Err(anyhow!("Failed to address a column: {}", position.column));
|
||||
}
|
||||
} else if input.len() - offset < position.column {
|
||||
if position.column > input.len() - line_start {
|
||||
return Err(anyhow!("Failed to address a column over the end"));
|
||||
}
|
||||
Ok(offset + position.column)
|
||||
Ok(line_start + position.column)
|
||||
}
|
||||
|
||||
pub fn position_for_offset(input: &[u8], offset: usize) -> Result<Point> {
|
||||
|
|
@ -1139,3 +1175,45 @@ pub fn position_for_offset(input: &[u8], offset: usize) -> Result<Point> {
|
|||
};
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{offset_for_position, parse_edit_flag};
|
||||
use tree_sitter::Point;
|
||||
|
||||
#[test]
|
||||
fn offset_for_position_uses_zero_based_line_and_column_coordinates() {
|
||||
let input = b"abc\n";
|
||||
assert_eq!(
|
||||
offset_for_position(input, Point { row: 0, column: 0 }).unwrap(),
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
offset_for_position(input, Point { row: 0, column: 1 }).unwrap(),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
offset_for_position(input, Point { row: 0, column: 3 }).unwrap(),
|
||||
3
|
||||
);
|
||||
assert_eq!(
|
||||
offset_for_position(input, Point { row: 1, column: 0 }).unwrap(),
|
||||
4
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offset_for_position_rejects_out_of_bounds_coordinates() {
|
||||
let input = b"abc\ndef";
|
||||
assert!(offset_for_position(input, Point { row: 0, column: 4 }).is_err());
|
||||
assert!(offset_for_position(input, Point { row: 2, column: 0 }).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_edit_flag_resolves_first_line_positions() {
|
||||
let edit = parse_edit_flag(b"abc\n", "0,0 0 X").unwrap();
|
||||
assert_eq!(edit.position, 0);
|
||||
assert_eq!(edit.deleted_length, 0);
|
||||
assert_eq!(edit.inserted_text, b"X");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,8 @@
|
|||
--light-scrollbar-track: #f1f1f1;
|
||||
--light-scrollbar-thumb: #c1c1c1;
|
||||
--light-scrollbar-thumb-hover: #a8a8a8;
|
||||
|
||||
--light-tree-row-bg: #e3f2fd;
|
||||
|
||||
--dark-bg: #1d1f21;
|
||||
--dark-border: #2d2d2d;
|
||||
--dark-text: #c5c8c6;
|
||||
|
|
@ -28,6 +29,7 @@
|
|||
--dark-scrollbar-track: #25282c;
|
||||
--dark-scrollbar-thumb: #4a4d51;
|
||||
--dark-scrollbar-thumb-hover: #5a5d61;
|
||||
--dark-tree-row-bg: #373737;
|
||||
|
||||
--primary-color: #0550ae;
|
||||
--primary-color-alpha: rgba(5, 80, 174, 0.1);
|
||||
|
|
@ -42,6 +44,7 @@
|
|||
--text-color: var(--dark-text);
|
||||
--panel-bg: var(--dark-panel-bg);
|
||||
--code-bg: var(--dark-code-bg);
|
||||
--tree-row-bg: var(--dark-tree-row-bg);
|
||||
}
|
||||
|
||||
[data-theme="light"] {
|
||||
|
|
@ -50,6 +53,7 @@
|
|||
--text-color: var(--light-text);
|
||||
--panel-bg: white;
|
||||
--code-bg: white;
|
||||
--tree-row-bg: var(--light-tree-row-bg);
|
||||
}
|
||||
|
||||
/* Base Styles */
|
||||
|
|
@ -275,7 +279,7 @@
|
|||
}
|
||||
|
||||
#output-container a.highlighted {
|
||||
background-color: #d9d9d9;
|
||||
background-color: #cae2ff;
|
||||
color: red;
|
||||
border-radius: 3px;
|
||||
text-decoration: underline;
|
||||
|
|
@ -346,7 +350,7 @@
|
|||
}
|
||||
|
||||
& #output-container a.highlighted {
|
||||
background-color: #373b41;
|
||||
background-color: #656669;
|
||||
color: red;
|
||||
}
|
||||
|
||||
|
|
@ -373,6 +377,9 @@
|
|||
color: var(--dark-text);
|
||||
}
|
||||
}
|
||||
.tree-row:has(.highlighted) {
|
||||
background-color: var(--tree-row-bg);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
|
|
@ -463,7 +470,7 @@
|
|||
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/codemirror.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/clusterize.js/0.19.0/clusterize.min.js"></script>
|
||||
|
||||
<script>LANGUAGE_BASE_URL = "";</script>
|
||||
<script>LANGUAGE_BASE_URL = ".";</script>
|
||||
<script type="module" src="playground.js"></script>
|
||||
<script type="module">
|
||||
import * as TreeSitter from './web-tree-sitter.js';
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@ use std::{
|
|||
env, fs,
|
||||
net::TcpListener,
|
||||
path::{Path, PathBuf},
|
||||
str::{self, FromStr as _},
|
||||
str::FromStr as _,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use anyhow::{Context, Result, anyhow};
|
||||
use log::{error, info};
|
||||
use tiny_http::{Header, Response, Server};
|
||||
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ pub fn query_file_at_path(
|
|||
test_summary: Option<&mut TestSummary>,
|
||||
) -> Result<()> {
|
||||
let stdout = io::stdout();
|
||||
let mut stdout = stdout.lock();
|
||||
let mut stdout = io::BufWriter::with_capacity(64 * 1024, stdout.lock());
|
||||
|
||||
let query_source = fs::read_to_string(query_path)
|
||||
.with_context(|| format!("Error reading query file {}", query_path.display()))?;
|
||||
|
|
@ -75,18 +75,18 @@ pub fn query_file_at_path(
|
|||
if opts.ordered_captures {
|
||||
let mut captures = query_cursor.captures(&query, tree.root_node(), source_code.as_slice());
|
||||
while let Some((mat, capture_index)) = captures.next() {
|
||||
let capture = mat.captures[*capture_index];
|
||||
let capture = mat.captures()[*capture_index];
|
||||
let capture_name = &query.capture_names()[capture.index as usize];
|
||||
if !opts.quiet && !should_test {
|
||||
writeln!(
|
||||
&mut stdout,
|
||||
" pattern: {:>2}, capture: {} - {capture_name}, start: {}, end: {}, text: `{}`",
|
||||
mat.pattern_index,
|
||||
capture.index,
|
||||
capture.node.start_position(),
|
||||
capture.node.end_position(),
|
||||
capture.node.utf8_text(&source_code).unwrap_or("")
|
||||
)?;
|
||||
&mut stdout,
|
||||
" pattern: {:>2}, capture: {} - {capture_name}, start: {}, end: {}, text: `{}`",
|
||||
mat.pattern_index,
|
||||
capture.index,
|
||||
capture.node.start_position(),
|
||||
capture.node.end_position(),
|
||||
capture.node.utf8_text(&source_code).unwrap_or("")
|
||||
)?;
|
||||
}
|
||||
if should_test {
|
||||
results.push(query_testing::CaptureInfo {
|
||||
|
|
@ -102,18 +102,18 @@ pub fn query_file_at_path(
|
|||
if !opts.quiet && !should_test {
|
||||
writeln!(&mut stdout, " pattern: {}", m.pattern_index)?;
|
||||
}
|
||||
for capture in m.captures {
|
||||
for capture in m.captures() {
|
||||
let start = capture.node.start_position();
|
||||
let end = capture.node.end_position();
|
||||
let capture_name = &query.capture_names()[capture.index as usize];
|
||||
if !opts.quiet && !should_test {
|
||||
if end.row == start.row {
|
||||
writeln!(
|
||||
&mut stdout,
|
||||
" capture: {} - {capture_name}, start: {start}, end: {end}, text: `{}`",
|
||||
capture.index,
|
||||
capture.node.utf8_text(&source_code).unwrap_or("")
|
||||
)?;
|
||||
&mut stdout,
|
||||
" capture: {} - {capture_name}, start: {start}, end: {end}, text: `{}`",
|
||||
capture.index,
|
||||
capture.node.utf8_text(&source_code).unwrap_or("")
|
||||
)?;
|
||||
} else {
|
||||
writeln!(
|
||||
&mut stdout,
|
||||
|
|
@ -142,7 +142,9 @@ pub fn query_file_at_path(
|
|||
};
|
||||
// Invariant: `test_summary` will always be `Some` when `should_test` is true
|
||||
let test_summary = test_summary.unwrap();
|
||||
match query_testing::assert_expected_captures(&results, path, &mut parser, language) {
|
||||
let assertions =
|
||||
query_testing::parse_position_comments(&mut parser, language, source_code.as_slice())?;
|
||||
match query_testing::assert_expected_captures(&results, &assertions) {
|
||||
Ok(assertion_count) => {
|
||||
test_summary.query_results.add_case(TestResult {
|
||||
name: path_name.to_string(),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use std::{fs, path::Path, sync::LazyLock};
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use anyhow::{Result, anyhow};
|
||||
use bstr::{BStr, ByteSlice};
|
||||
use regex::Regex;
|
||||
use tree_sitter::{Language, Parser, Point};
|
||||
|
|
@ -106,66 +106,66 @@ pub fn parse_position_comments(
|
|||
let node = cursor.node();
|
||||
|
||||
// Find every comment node.
|
||||
if node.kind().to_lowercase().contains("comment") {
|
||||
if let Ok(text) = node.utf8_text(source) {
|
||||
let mut position = node.start_position();
|
||||
if position.row > 0 {
|
||||
// Find the arrow character ("^" or "<-") in the comment. A left arrow
|
||||
// refers to the column where the comment node starts. An up arrow refers
|
||||
// to its own column.
|
||||
let mut has_left_caret = false;
|
||||
let mut has_arrow = false;
|
||||
let mut negative = false;
|
||||
let mut arrow_end = 0;
|
||||
let mut arrow_count = 1;
|
||||
for (i, c) in text.char_indices() {
|
||||
arrow_end = i + 1;
|
||||
if c == '-' && has_left_caret {
|
||||
has_arrow = true;
|
||||
break;
|
||||
}
|
||||
if c == '^' {
|
||||
has_arrow = true;
|
||||
position.column += i;
|
||||
// Continue counting remaining arrows and update their end column
|
||||
for (_, c) in text[arrow_end..].char_indices() {
|
||||
if c != '^' {
|
||||
arrow_end += arrow_count - 1;
|
||||
break;
|
||||
}
|
||||
arrow_count += 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
has_left_caret = c == '<';
|
||||
if node.kind().to_lowercase().contains("comment")
|
||||
&& let Ok(text) = node.utf8_text(source)
|
||||
{
|
||||
let mut position = node.start_position();
|
||||
if position.row > 0 {
|
||||
// Find the arrow character ("^" or "<-") in the comment. A left arrow
|
||||
// refers to the column where the comment node starts. An up arrow refers
|
||||
// to its own column.
|
||||
let mut has_left_caret = false;
|
||||
let mut has_arrow = false;
|
||||
let mut negative = false;
|
||||
let mut arrow_end = 0;
|
||||
let mut arrow_count = 1;
|
||||
for (i, c) in text.char_indices() {
|
||||
arrow_end = i + 1;
|
||||
if c == '-' && has_left_caret {
|
||||
has_arrow = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// find any ! after arrows but before capture name
|
||||
if has_arrow {
|
||||
for (i, c) in text[arrow_end..].char_indices() {
|
||||
if c == '!' {
|
||||
negative = true;
|
||||
arrow_end += i + 1;
|
||||
break;
|
||||
} else if !c.is_whitespace() {
|
||||
if c == '^' {
|
||||
has_arrow = true;
|
||||
position.column += i;
|
||||
// Continue counting remaining arrows and update their end column
|
||||
for (_, c) in text[arrow_end..].char_indices() {
|
||||
if c != '^' {
|
||||
arrow_end += arrow_count - 1;
|
||||
break;
|
||||
}
|
||||
arrow_count += 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
has_left_caret = c == '<';
|
||||
}
|
||||
|
||||
// find any ! after arrows but before capture name
|
||||
if has_arrow {
|
||||
for (i, c) in text[arrow_end..].char_indices() {
|
||||
if c == '!' {
|
||||
negative = true;
|
||||
arrow_end += i + 1;
|
||||
break;
|
||||
} else if !c.is_whitespace() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the comment node contains an arrow and a highlight name, record the
|
||||
// highlight name and the position.
|
||||
if let (true, Some(mat)) =
|
||||
(has_arrow, CAPTURE_NAME_REGEX.find(&text[arrow_end..]))
|
||||
{
|
||||
assertion_ranges.push((node.start_position(), node.end_position()));
|
||||
result.push(Assertion {
|
||||
position: to_utf8_point(position, source),
|
||||
length: arrow_count,
|
||||
negative,
|
||||
expected_capture_name: mat.as_str().to_string(),
|
||||
});
|
||||
}
|
||||
// If the comment node contains an arrow and a highlight name, record the
|
||||
// highlight name and the position.
|
||||
if let (true, Some(mat)) =
|
||||
(has_arrow, CAPTURE_NAME_REGEX.find(&text[arrow_end..]))
|
||||
{
|
||||
assertion_ranges.push((node.start_position(), node.end_position()));
|
||||
result.push(Assertion {
|
||||
position: to_utf8_point(position, source),
|
||||
length: arrow_count,
|
||||
negative,
|
||||
expected_capture_name: mat.as_str().to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -219,19 +219,14 @@ pub fn parse_position_comments(
|
|||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn assert_expected_captures(
|
||||
infos: &[CaptureInfo],
|
||||
path: &Path,
|
||||
parser: &mut Parser,
|
||||
language: &Language,
|
||||
) -> Result<usize> {
|
||||
let contents = fs::read_to_string(path)?;
|
||||
let pairs = parse_position_comments(parser, language, contents.as_bytes())?;
|
||||
for assertion in &pairs {
|
||||
pub fn assert_expected_captures(infos: &[CaptureInfo], assertions: &[Assertion]) -> Result<usize> {
|
||||
for assertion in assertions {
|
||||
if let Some(found) = &infos.iter().find(|p| {
|
||||
assertion.position >= p.start
|
||||
&& (assertion.position.row < p.end.row
|
||||
|| assertion.position.column + assertion.length - 1 < p.end.column)
|
||||
let assertion_end = Utf8Point::new(
|
||||
assertion.position.row,
|
||||
assertion.position.column + assertion.length - 1,
|
||||
);
|
||||
assertion.position >= p.start && assertion_end < p.end
|
||||
}) {
|
||||
if assertion.expected_capture_name != found.name && found.name != "name" {
|
||||
return Err(anyhow!(
|
||||
|
|
@ -250,5 +245,24 @@ pub fn assert_expected_captures(
|
|||
));
|
||||
}
|
||||
}
|
||||
Ok(pairs.len())
|
||||
Ok(assertions.len())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{Assertion, CaptureInfo, Utf8Point, assert_expected_captures};
|
||||
|
||||
#[test]
|
||||
fn test_assertion_after_multiline_capture_does_not_match() {
|
||||
let captures = [CaptureInfo {
|
||||
name: "foo".to_string(),
|
||||
start: Utf8Point::new(0, 0),
|
||||
end: Utf8Point::new(1, 1),
|
||||
}];
|
||||
let assertions = [Assertion::new(2, 0, 1, false, "foo".to_string())];
|
||||
|
||||
let result = assert_expected_captures(&captures, &assertions);
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,7 @@ use std::{
|
|||
fs,
|
||||
io::{self, Write},
|
||||
path::Path,
|
||||
str,
|
||||
sync::{atomic::AtomicUsize, Arc},
|
||||
sync::{Arc, atomic::AtomicUsize},
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
|
|
@ -49,7 +48,7 @@ pub fn generate_tags(
|
|||
&mut stdout,
|
||||
"{indent_str}{:<10}\t | {:<8}\t{} {} - {} `{}`",
|
||||
str::from_utf8(&source[tag.name_range]).unwrap_or(""),
|
||||
&config.syntax_type_name(tag.syntax_type_id),
|
||||
config.syntax_type_name(tag.syntax_type_id),
|
||||
if tag.is_definition { "def" } else { "ref" },
|
||||
tag.span.start,
|
||||
tag.span.end,
|
||||
|
|
@ -59,7 +58,7 @@ pub fn generate_tags(
|
|||
if docs.len() > 120 {
|
||||
write!(&mut stdout, "\t{:?}...", docs.get(0..120).unwrap_or(""))?;
|
||||
} else {
|
||||
write!(&mut stdout, "\t{:?}", &docs)?;
|
||||
write!(&mut stdout, "\t{docs:?}")?;
|
||||
}
|
||||
}
|
||||
writeln!(&mut stdout)?;
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
"""PARSER_DESCRIPTION"""
|
||||
|
||||
from importlib.resources import files as _files
|
||||
|
||||
from ._binding import language
|
||||
|
||||
|
||||
def _get_query(name, file):
|
||||
files = globals().get("_files")
|
||||
if files is None:
|
||||
from importlib.resources import files
|
||||
globals()["_files"] = files
|
||||
try:
|
||||
query = _files(f"{__package__}") / file
|
||||
query = files(f"{__package__}") / file
|
||||
globals()[name] = query.read_text()
|
||||
except FileNotFoundError:
|
||||
globals()[name] = None
|
||||
|
|
|
|||
|
|
@ -1,25 +1,18 @@
|
|||
[package]
|
||||
name = "tree-sitter-PARSER_NAME"
|
||||
description = "PARSER_DESCRIPTION"
|
||||
version = "PARSER_VERSION"
|
||||
authors = ["PARSER_AUTHOR_NAME PARSER_AUTHOR_EMAIL"]
|
||||
license = "PARSER_LICENSE"
|
||||
readme = "README.md"
|
||||
keywords = ["incremental", "parsing", "tree-sitter", "PARSER_NAME"]
|
||||
categories = ["parser-implementations", "parsing", "text-editors"]
|
||||
repository = "PARSER_URL"
|
||||
edition = "2021"
|
||||
authors = [ "PARSER_AUTHOR_NAME PARSER_AUTHOR_EMAIL" ]
|
||||
autoexamples = false
|
||||
categories = [ "parser-implementations", "parsing", "text-editors" ]
|
||||
description = "PARSER_DESCRIPTION"
|
||||
edition = "2024"
|
||||
keywords = [ "incremental", "parsing", "tree-sitter", "PARSER_NAME" ]
|
||||
license = "PARSER_LICENSE"
|
||||
name = "tree-sitter-PARSER_NAME"
|
||||
readme = "README.md"
|
||||
repository = "PARSER_URL"
|
||||
version = "PARSER_VERSION"
|
||||
|
||||
build = "bindings/rust/build.rs"
|
||||
include = [
|
||||
"bindings/rust/*",
|
||||
"grammar.js",
|
||||
"queries/*",
|
||||
"src/*",
|
||||
"tree-sitter.json",
|
||||
"/LICENSE",
|
||||
]
|
||||
include = [ "bindings/rust/*", "grammar.js", "queries/*", "src/*", "tree-sitter.json", "/LICENSE" ]
|
||||
|
||||
[lib]
|
||||
path = "bindings/rust/lib.rs"
|
||||
|
|
|
|||
|
|
@ -11,18 +11,8 @@ fn main() {
|
|||
let Ok(wasm_headers) = std::env::var("DEP_TREE_SITTER_LANGUAGE_WASM_HEADERS") else {
|
||||
panic!("Environment variable DEP_TREE_SITTER_LANGUAGE_WASM_HEADERS must be set by the language crate");
|
||||
};
|
||||
let Ok(wasm_src) =
|
||||
std::env::var("DEP_TREE_SITTER_LANGUAGE_WASM_SRC").map(std::path::PathBuf::from)
|
||||
else {
|
||||
panic!("Environment variable DEP_TREE_SITTER_LANGUAGE_WASM_SRC must be set by the language crate");
|
||||
};
|
||||
|
||||
c_config.include(&wasm_headers);
|
||||
c_config.files([
|
||||
wasm_src.join("stdio.c"),
|
||||
wasm_src.join("stdlib.c"),
|
||||
wasm_src.join("string.c"),
|
||||
]);
|
||||
}
|
||||
|
||||
let parser_path = src_dir.join("parser.c");
|
||||
|
|
|
|||
|
|
@ -4,46 +4,51 @@ pub fn build(b: *std.Build) !void {
|
|||
const target = b.standardTargetOptions(.{});
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
|
||||
var threaded: std.Io.Threaded = .init(b.allocator, .{});
|
||||
defer threaded.deinit();
|
||||
const io = threaded.io();
|
||||
|
||||
const shared = b.option(bool, "build-shared", "Build a shared library") orelse true;
|
||||
const reuse_alloc = b.option(bool, "reuse-allocator", "Reuse the library allocator") orelse false;
|
||||
|
||||
const library_name = "tree-sitter-PARSER_NAME";
|
||||
|
||||
var grammar = b.createModule(.{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.link_libc = true,
|
||||
.pic = if (shared) true else null,
|
||||
});
|
||||
const lib: *std.Build.Step.Compile = b.addLibrary(.{
|
||||
.name = library_name,
|
||||
.linkage = if (shared) .dynamic else .static,
|
||||
.root_module = b.createModule(.{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.link_libc = true,
|
||||
.pic = if (shared) true else null,
|
||||
}),
|
||||
.root_module = grammar,
|
||||
});
|
||||
|
||||
lib.addCSourceFile(.{
|
||||
grammar.addCSourceFile(.{
|
||||
.file = b.path("src/parser.c"),
|
||||
.flags = &.{"-std=c11"},
|
||||
});
|
||||
if (fileExists(b, "src/scanner.c")) {
|
||||
lib.addCSourceFile(.{
|
||||
if (fileExists(b, io, "src/scanner.c")) {
|
||||
grammar.addCSourceFile(.{
|
||||
.file = b.path("src/scanner.c"),
|
||||
.flags = &.{"-std=c11"},
|
||||
});
|
||||
}
|
||||
|
||||
if (reuse_alloc) {
|
||||
lib.root_module.addCMacro("TREE_SITTER_REUSE_ALLOCATOR", "");
|
||||
grammar.addCMacro("TREE_SITTER_REUSE_ALLOCATOR", "");
|
||||
}
|
||||
if (optimize == .Debug) {
|
||||
lib.root_module.addCMacro("TREE_SITTER_DEBUG", "");
|
||||
grammar.addCMacro("TREE_SITTER_DEBUG", "");
|
||||
}
|
||||
|
||||
lib.addIncludePath(b.path("src"));
|
||||
grammar.addIncludePath(b.path("src"));
|
||||
|
||||
b.installArtifact(lib);
|
||||
b.installFile("src/node-types.json", "node-types.json");
|
||||
|
||||
if (fileExists(b, "queries")) {
|
||||
if (fileExists(b, io, "queries")) {
|
||||
b.installDirectory(.{
|
||||
.source_dir = b.path("queries"),
|
||||
.install_dir = .prefix,
|
||||
|
|
@ -69,16 +74,10 @@ pub fn build(b: *std.Build) !void {
|
|||
tests.root_module.addImport(library_name, module);
|
||||
|
||||
// HACK: fetch tree-sitter dependency only when testing this module
|
||||
if (b.pkg_hash.len == 0) {
|
||||
var args = try std.process.argsWithAllocator(b.allocator);
|
||||
defer args.deinit();
|
||||
while (args.next()) |a| {
|
||||
if (std.mem.eql(u8, a, "test")) {
|
||||
const ts_dep = b.lazyDependency("tree_sitter", .{}) orelse continue;
|
||||
tests.root_module.addImport("tree-sitter", ts_dep.module("tree-sitter"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (b.option(bool, "test", "Fetch test dependencies") orelse false) {
|
||||
const ts_dep = b.lazyDependency("tree_sitter", .{});
|
||||
if (ts_dep) |dep|
|
||||
tests.root_module.addImport("tree-sitter", dep.module("tree_sitter"));
|
||||
}
|
||||
|
||||
const run_tests = b.addRunArtifact(tests);
|
||||
|
|
@ -86,8 +85,8 @@ pub fn build(b: *std.Build) !void {
|
|||
test_step.dependOn(&run_tests.step);
|
||||
}
|
||||
|
||||
inline fn fileExists(b: *std.Build, filename: []const u8) bool {
|
||||
inline fn fileExists(b: *std.Build, io: std.Io, filename: []const u8) bool {
|
||||
const dir = b.build_root.handle;
|
||||
dir.access(filename, .{}) catch return false;
|
||||
dir.access(io, filename, .{}) catch return false;
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
.{
|
||||
.name = .tree_sitter_PARSER_NAME,
|
||||
.fingerprint = PARSER_FINGERPRINT,
|
||||
.minimum_zig_version = "0.16.0",
|
||||
.version = "PARSER_VERSION",
|
||||
.dependencies = .{
|
||||
.tree_sitter = .{
|
||||
.url = "git+https://github.com/tree-sitter/zig-tree-sitter#b4b72c903e69998fc88e27e154a5e3cc9166551b",
|
||||
.hash = "tree_sitter-0.25.0-8heIf51vAQConvVIgvm-9mVIbqh7yabZYqPXfOpS3YoG",
|
||||
.url = "git+https://github.com/tree-sitter/zig-tree-sitter#0cf58172e61f6fdd16f681cde42b4acb531a23db",
|
||||
.hash = "tree_sitter-0.26.0-8heIf3CaAQDeVTQc0DMSBhbQAEx5aF-dTen4_LPxMgrv",
|
||||
.lazy = true,
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ endif()
|
|||
|
||||
include(GNUInstallDirs)
|
||||
|
||||
find_program(TREE_SITTER_CLI tree-sitter DOC "Tree-sitter CLI")
|
||||
find_program(TREE_SITTER_CLI tree-sitter DOC "Tree-sitter CLI" REQUIRED)
|
||||
|
||||
add_custom_command(OUTPUT "${CMAKE_CURRENT_SOURCE_DIR}/src/grammar.json"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/node-types.json"
|
||||
|
|
|
|||
2
crates/cli/src/templates/index.d.ts
vendored
2
crates/cli/src/templates/index.d.ts
vendored
|
|
@ -21,7 +21,7 @@ type NodeInfo =
|
|||
/**
|
||||
* The tree-sitter language object for this grammar.
|
||||
*
|
||||
* @see {@linkcode https://tree-sitter.github.io/node-tree-sitter/interfaces/Parser.Language.html Parser.Language}
|
||||
* @see {@linkcode https://tree-sitter.github.io/node-tree-sitter/interfaces/Language.html Parser.Language}
|
||||
*
|
||||
* @example
|
||||
* import Parser from "tree-sitter";
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
|
||||
use tree_sitter_language::LanguageFn;
|
||||
|
||||
extern "C" {
|
||||
unsafe extern "C" {
|
||||
fn tree_sitter_PARSER_NAME() -> *const ();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
LANGUAGE_NAME := tree-sitter-KEBAB_PARSER_NAME
|
||||
HOMEPAGE_URL := PARSER_URL
|
||||
VERSION := PARSER_VERSION
|
||||
DESCRIPTION := PARSER_DESCRIPTION
|
||||
|
||||
# repository
|
||||
SRC_DIR := src
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
// swift-tools-version:5.3
|
||||
// swift-tools-version:5.6
|
||||
|
||||
import Foundation
|
||||
import PackageDescription
|
||||
|
||||
let dir = Context.packageDirectory
|
||||
var sources = ["src/parser.c"]
|
||||
if FileManager.default.fileExists(atPath: "src/scanner.c") {
|
||||
if FileManager.default.fileExists(atPath: "\(dir)/src/scanner.c") {
|
||||
sources.append("src/scanner.c")
|
||||
}
|
||||
|
||||
|
|
@ -14,7 +15,7 @@ let package = Package(
|
|||
.library(name: "PARSER_CLASS_NAME", targets: ["PARSER_CLASS_NAME"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(name: "SwiftTreeSitter", url: "https://github.com/tree-sitter/swift-tree-sitter", from: "0.9.0"),
|
||||
.package(url: "https://github.com/tree-sitter/swift-tree-sitter", from: "0.10.0"),
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
|
|
@ -31,7 +32,7 @@ let package = Package(
|
|||
.testTarget(
|
||||
name: "PARSER_CLASS_NAMETests",
|
||||
dependencies: [
|
||||
"SwiftTreeSitter",
|
||||
.product(name: "SwiftTreeSitter", package: "swift-tree-sitter"),
|
||||
"PARSER_CLASS_NAME",
|
||||
],
|
||||
path: "bindings/swift/PARSER_CLASS_NAMETests"
|
||||
|
|
|
|||
|
|
@ -1,29 +1,29 @@
|
|||
[build-system]
|
||||
requires = ["setuptools>=62.4.0", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
requires = [ "setuptools>=62.4.0", "wheel" ]
|
||||
|
||||
[project]
|
||||
name = "tree-sitter-PARSER_NAME"
|
||||
description = "PARSER_DESCRIPTION"
|
||||
version = "PARSER_VERSION"
|
||||
keywords = ["incremental", "parsing", "tree-sitter", "PARSER_NAME"]
|
||||
authors = [ { email = "PARSER_AUTHOR_EMAIL", name = "PARSER_AUTHOR_NAME" } ]
|
||||
classifiers = [
|
||||
"Intended Audience :: Developers",
|
||||
"Topic :: Software Development :: Compilers",
|
||||
"Topic :: Text Processing :: Linguistic",
|
||||
"Typing :: Typed",
|
||||
]
|
||||
authors = [{ name = "PARSER_AUTHOR_NAME", email = "PARSER_AUTHOR_EMAIL" }]
|
||||
requires-python = ">=3.10"
|
||||
description = "PARSER_DESCRIPTION"
|
||||
keywords = [ "incremental", "parsing", "tree-sitter", "PARSER_NAME" ]
|
||||
license.text = "PARSER_LICENSE"
|
||||
name = "tree-sitter-PARSER_NAME"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "PARSER_VERSION"
|
||||
|
||||
[project.urls]
|
||||
Homepage = "PARSER_URL"
|
||||
Funding = "FUNDING_URL"
|
||||
Homepage = "PARSER_URL"
|
||||
|
||||
[project.optional-dependencies]
|
||||
core = ["tree-sitter~=0.24"]
|
||||
core = [ "tree-sitter~=0.24" ]
|
||||
|
||||
[tool.cibuildwheel]
|
||||
build = "cp310-*"
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ class EggInfo(egg_info):
|
|||
super().find_sources()
|
||||
self.filelist.recursive_include("queries", "*.scm")
|
||||
self.filelist.include("src/tree_sitter/*.h")
|
||||
self.filelist.include("src/*.c")
|
||||
|
||||
|
||||
setup(
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,22 +1,22 @@
|
|||
use std::{fs, path::Path};
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use anyhow::{Result, anyhow};
|
||||
use tree_sitter::Point;
|
||||
use tree_sitter_highlight::{Highlight, HighlightConfiguration, HighlightEvent, Highlighter};
|
||||
use tree_sitter_loader::{Config, Loader};
|
||||
|
||||
use crate::{
|
||||
query_testing::{parse_position_comments, to_utf8_point, Assertion, Utf8Point},
|
||||
query_testing::{Assertion, Utf8Point, parse_position_comments, to_utf8_point},
|
||||
test::{TestInfo, TestOutcome, TestResult, TestSummary},
|
||||
util,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Failure {
|
||||
row: usize,
|
||||
column: usize,
|
||||
expected_highlight: String,
|
||||
actual_highlights: Vec<String>,
|
||||
pub(crate) row: usize,
|
||||
pub(crate) column: usize,
|
||||
pub(crate) expected_highlight: String,
|
||||
pub(crate) actual_highlights: Vec<String>,
|
||||
}
|
||||
|
||||
impl std::error::Error for Failure {}
|
||||
|
|
@ -120,12 +120,9 @@ pub fn test_highlights(
|
|||
}
|
||||
}
|
||||
|
||||
if failed {
|
||||
Err(anyhow!(""))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
if failed { Err(anyhow!("")) } else { Ok(()) }
|
||||
}
|
||||
|
||||
pub fn iterate_assertions(
|
||||
assertions: &[Assertion],
|
||||
highlights: &[(Utf8Point, Utf8Point, Highlight)],
|
||||
|
|
@ -142,49 +139,48 @@ pub fn iterate_assertions(
|
|||
expected_capture_name: expected_highlight,
|
||||
} in assertions
|
||||
{
|
||||
let mut passed = false;
|
||||
let mut end_column = position.column + length - 1;
|
||||
// Iterate through all of the highlights that start at or before this assertion's
|
||||
// position, looking for one that matches the assertion.
|
||||
actual_highlights.clear();
|
||||
|
||||
// The assertions are ordered by position, so skip past all of the highlights that
|
||||
// end at or before this assertion's position.
|
||||
'highlight_loop: while let Some(highlight) = highlights.get(i) {
|
||||
let mut passed = false;
|
||||
let end_column = position.column + length - 1;
|
||||
for highlight in &highlights[i..] {
|
||||
// The assertions are ordered by position, so skip past all of the highlights that
|
||||
// end at or before this assertion's position.
|
||||
if highlight.1 <= *position {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if (highlight.0.row > position.row)
|
||||
|| (highlight.0.row == position.row && highlight.0.column > end_column)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// Iterate through all of the highlights that start at or before this assertion's
|
||||
// position, looking for one that matches the assertion.
|
||||
let mut j = i;
|
||||
while let (false, Some(highlight)) = (passed, highlights.get(j)) {
|
||||
end_column = position.column + length - 1;
|
||||
if highlight.0.row >= position.row && highlight.0.column > end_column {
|
||||
break 'highlight_loop;
|
||||
}
|
||||
|
||||
// If the highlight matches the assertion, or if the highlight doesn't
|
||||
// match the assertion but it's negative, this test passes. Otherwise,
|
||||
// add this highlight to the list of actual highlights that span the
|
||||
// assertion's position, in order to generate an error message in the event
|
||||
// of a failure.
|
||||
let highlight_name = &highlight_names[(highlight.2).0];
|
||||
if (*highlight_name == *expected_highlight) == *negative {
|
||||
actual_highlights.push(highlight_name);
|
||||
} else {
|
||||
passed = true;
|
||||
break 'highlight_loop;
|
||||
}
|
||||
|
||||
j += 1;
|
||||
// If the highlight matches the assertion, or if the highlight doesn't
|
||||
// match the assertion but it's negative, this test passes. Otherwise,
|
||||
// add this highlight to the list of actual highlights that span the
|
||||
// assertion's position, in order to generate an error message in the event
|
||||
// of a failure.
|
||||
let highlight_name = &highlight_names[(highlight.2).0];
|
||||
if (*highlight_name == *expected_highlight) == *negative {
|
||||
actual_highlights.push(highlight_name);
|
||||
} else {
|
||||
passed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if !passed {
|
||||
let mut expected = String::with_capacity(expected_highlight.len() + 1);
|
||||
if *negative {
|
||||
expected.push('!');
|
||||
}
|
||||
expected.push_str(expected_highlight);
|
||||
return Err(Failure {
|
||||
row: position.row,
|
||||
column: end_column,
|
||||
expected_highlight: expected_highlight.clone(),
|
||||
expected_highlight: expected,
|
||||
actual_highlights: actual_highlights.into_iter().cloned().collect(),
|
||||
}
|
||||
.into());
|
||||
|
|
@ -223,9 +219,11 @@ pub fn get_highlight_positions(
|
|||
let mut highlight_stack = Vec::new();
|
||||
let source = String::from_utf8_lossy(source);
|
||||
let mut char_indices = source.char_indices();
|
||||
for event in highlighter.highlight(highlight_config, source.as_bytes(), None, |string| {
|
||||
loader.highlight_config_for_injection_string(string)
|
||||
})? {
|
||||
for event in
|
||||
highlighter.highlight(highlight_config, source.as_bytes(), None, None, |string| {
|
||||
loader.highlight_config_for_injection_string(string)
|
||||
})?
|
||||
{
|
||||
match event? {
|
||||
HighlightEvent::HighlightStart(h) => highlight_stack.push(h),
|
||||
HighlightEvent::HighlightEnd => {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
use std::{fs, path::Path};
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use anyhow::{Result, anyhow};
|
||||
use tree_sitter_loader::{Config, Loader};
|
||||
use tree_sitter_tags::{TagsConfiguration, TagsContext};
|
||||
|
||||
use crate::{
|
||||
query_testing::{parse_position_comments, to_utf8_point, Assertion, Utf8Point},
|
||||
query_testing::{Assertion, Utf8Point, parse_position_comments, to_utf8_point},
|
||||
test::{TestInfo, TestOutcome, TestResult, TestSummary},
|
||||
util,
|
||||
};
|
||||
|
|
@ -113,11 +113,7 @@ pub fn test_tags(
|
|||
}
|
||||
}
|
||||
|
||||
if failed {
|
||||
Err(anyhow!(""))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
if failed { Err(anyhow!("")) } else { Ok(()) }
|
||||
}
|
||||
|
||||
pub fn test_tag(
|
||||
|
|
|
|||
|
|
@ -17,13 +17,12 @@ mod tree_test;
|
|||
#[cfg(feature = "wasm")]
|
||||
mod wasm_language_test;
|
||||
|
||||
use tree_sitter_generate::GenerateResult;
|
||||
use tree_sitter_generate::{GenerateResult, OptLevel};
|
||||
|
||||
pub use crate::fuzz::{
|
||||
allocations,
|
||||
ITERATION_COUNT, allocations,
|
||||
edits::{get_random_edit, invert_edit},
|
||||
random::Rand,
|
||||
ITERATION_COUNT,
|
||||
};
|
||||
|
||||
pub use helpers::fixtures::get_language;
|
||||
|
|
@ -31,5 +30,10 @@ pub use helpers::fixtures::get_language;
|
|||
/// This is a simple wrapper around [`tree_sitter_generate::generate_parser_for_grammar`], because
|
||||
/// our tests do not need to pass in a version number, only the grammar JSON.
|
||||
fn generate_parser(grammar_json: &str) -> GenerateResult<(String, String)> {
|
||||
tree_sitter_generate::generate_parser_for_grammar(grammar_json, Some((0, 0, 0)))
|
||||
tree_sitter_generate::generate_parser_for_grammar(
|
||||
grammar_json,
|
||||
Some((0, 0, 0)),
|
||||
OptLevel::default(),
|
||||
&mut Vec::new(),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,24 +2,25 @@ use std::{collections::HashMap, env, fs};
|
|||
|
||||
use anyhow::Context;
|
||||
use tree_sitter::Parser;
|
||||
use tree_sitter_generate::OptLevel;
|
||||
use tree_sitter_proc_macro::test_with_seed;
|
||||
|
||||
use crate::{
|
||||
fuzz::{
|
||||
EDIT_COUNT, EXAMPLE_EXCLUDE, EXAMPLE_INCLUDE, ITERATION_COUNT, LANGUAGE_FILTER,
|
||||
LOG_GRAPH_ENABLED, START_SEED,
|
||||
corpus_test::{
|
||||
check_changed_ranges, check_consistent_sizes, get_parser, set_included_ranges,
|
||||
},
|
||||
edits::{get_random_edit, invert_edit},
|
||||
flatten_tests, new_seed,
|
||||
random::Rand,
|
||||
EDIT_COUNT, EXAMPLE_EXCLUDE, EXAMPLE_INCLUDE, ITERATION_COUNT, LANGUAGE_FILTER,
|
||||
LOG_GRAPH_ENABLED, START_SEED,
|
||||
},
|
||||
parse::perform_edit,
|
||||
test::{parse_tests, strip_sexp_fields, DiffKey, TestDiff},
|
||||
test::{DiffKey, TestDiff, parse_tests, render_test_output},
|
||||
tests::{
|
||||
allocations,
|
||||
helpers::fixtures::{fixtures_dir, get_language, get_test_language, SCRATCH_BASE_DIR},
|
||||
helpers::fixtures::{SCRATCH_BASE_DIR, fixtures_dir, get_language, get_test_language},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -120,10 +121,10 @@ pub fn test_language_corpus(
|
|||
skipped: Option<&[&str]>,
|
||||
language_dir: Option<&str>,
|
||||
) {
|
||||
if let Some(filter) = LANGUAGE_FILTER.as_ref() {
|
||||
if language_name != filter {
|
||||
return;
|
||||
}
|
||||
if let Some(filter) = LANGUAGE_FILTER.as_ref()
|
||||
&& language_name != filter
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let language_dir = language_dir.unwrap_or_default();
|
||||
|
|
@ -185,38 +186,17 @@ pub fn test_language_corpus(
|
|||
println!();
|
||||
for (test_index, test) in tests.iter().enumerate() {
|
||||
let test_name = format!("{language_name} - {}", test.name);
|
||||
if let Some(skipped) = skipped.as_mut() {
|
||||
if let Some(counter) = skipped.get_mut(test_name.as_str()) {
|
||||
println!(" {test_index}. {test_name} - SKIPPED");
|
||||
*counter += 1;
|
||||
continue;
|
||||
}
|
||||
if let Some(skipped) = skipped.as_mut()
|
||||
&& let Some(counter) = skipped.get_mut(test_name.as_str())
|
||||
{
|
||||
println!(" {test_index}. {test_name} - SKIPPED");
|
||||
*counter += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
println!(" {test_index}. {test_name}");
|
||||
|
||||
let passed = allocations::record(|| {
|
||||
let mut log_session = None;
|
||||
let mut parser = get_parser(&mut log_session, "log.html");
|
||||
parser.set_language(&language).unwrap();
|
||||
set_included_ranges(&mut parser, &test.input, test.template_delimiters);
|
||||
|
||||
let tree = parser.parse(&test.input, None).unwrap();
|
||||
let mut actual_output = tree.root_node().to_sexp();
|
||||
if !test.has_fields {
|
||||
actual_output = strip_sexp_fields(&actual_output);
|
||||
}
|
||||
|
||||
if actual_output != test.output {
|
||||
println!("Incorrect initial parse for {test_name}");
|
||||
DiffKey::print();
|
||||
println!("{}", TestDiff::new(&actual_output, &test.output));
|
||||
println!();
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
});
|
||||
let passed = allocations::record(|| test.check_initial_parse(&language, &test_name, true));
|
||||
|
||||
if !passed {
|
||||
failure_count += 1;
|
||||
|
|
@ -274,7 +254,9 @@ pub fn test_language_corpus(
|
|||
// Check that the new tree is consistent.
|
||||
check_consistent_sizes(&tree2, &input);
|
||||
if let Err(message) = check_changed_ranges(&tree, &tree2, &input) {
|
||||
println!("\nUnexpected scope change in seed {seed} with start seed {start_seed}\n{message}\n\n",);
|
||||
println!(
|
||||
"\nUnexpected scope change in seed {seed} with start seed {start_seed}\n{message}\n\n",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -290,10 +272,8 @@ pub fn test_language_corpus(
|
|||
let tree3 = parser.parse(&input, Some(&tree2)).unwrap();
|
||||
|
||||
// Verify that the final tree matches the expectation from the corpus.
|
||||
let mut actual_output = tree3.root_node().to_sexp();
|
||||
if !test.has_fields {
|
||||
actual_output = strip_sexp_fields(&actual_output);
|
||||
}
|
||||
let actual_output =
|
||||
render_test_output(&input, &tree3, test.cst, test.has_fields).unwrap();
|
||||
|
||||
if actual_output != test.output {
|
||||
println!("Incorrect parse for {test_name} - seed {seed}");
|
||||
|
|
@ -306,7 +286,9 @@ pub fn test_language_corpus(
|
|||
// Check that the edited tree is consistent.
|
||||
check_consistent_sizes(&tree3, &input);
|
||||
if let Err(message) = check_changed_ranges(&tree2, &tree3, &input) {
|
||||
println!("Unexpected scope change in seed {seed} with start seed {start_seed}\n{message}\n\n");
|
||||
println!(
|
||||
"Unexpected scope change in seed {seed} with start seed {start_seed}\n{message}\n\n"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -351,10 +333,10 @@ fn test_feature_corpus_files() {
|
|||
let language_name = entry.file_name();
|
||||
let language_name = language_name.to_str().unwrap();
|
||||
|
||||
if let Some(filter) = LANGUAGE_FILTER.as_ref() {
|
||||
if language_name != filter {
|
||||
continue;
|
||||
}
|
||||
if let Some(filter) = LANGUAGE_FILTER.as_ref()
|
||||
&& language_name != filter
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let test_path = entry.path();
|
||||
|
|
@ -371,8 +353,12 @@ fn test_feature_corpus_files() {
|
|||
)
|
||||
})
|
||||
.unwrap();
|
||||
let generate_result =
|
||||
tree_sitter_generate::generate_parser_for_grammar(&grammar_json, Some((0, 0, 0)));
|
||||
let generate_result = tree_sitter_generate::generate_parser_for_grammar(
|
||||
&grammar_json,
|
||||
Some((0, 0, 0)),
|
||||
OptLevel::default(),
|
||||
&mut Vec::new(),
|
||||
);
|
||||
|
||||
if error_message_path.exists() {
|
||||
if EXAMPLE_INCLUDE.is_some() || EXAMPLE_EXCLUDE.is_some() {
|
||||
|
|
@ -393,12 +379,12 @@ fn test_feature_corpus_files() {
|
|||
failure_count += 1;
|
||||
}
|
||||
} else {
|
||||
eprintln!("Expected error message but got none for test grammar '{language_name}'",);
|
||||
eprintln!("Expected error message but got none for test grammar '{language_name}'");
|
||||
failure_count += 1;
|
||||
}
|
||||
} else {
|
||||
if let Err(e) = &generate_result {
|
||||
eprintln!("Unexpected error for test grammar '{language_name}':\n{e}",);
|
||||
eprintln!("Unexpected error for test grammar '{language_name}':\n{e}");
|
||||
failure_count += 1;
|
||||
continue;
|
||||
}
|
||||
|
|
@ -416,24 +402,8 @@ fn test_feature_corpus_files() {
|
|||
for test in tests {
|
||||
eprintln!(" example: {:?}", test.name);
|
||||
|
||||
let passed = allocations::record(|| {
|
||||
let mut log_session = None;
|
||||
let mut parser = get_parser(&mut log_session, "log.html");
|
||||
parser.set_language(&language).unwrap();
|
||||
let tree = parser.parse(&test.input, None).unwrap();
|
||||
let mut actual_output = tree.root_node().to_sexp();
|
||||
if !test.has_fields {
|
||||
actual_output = strip_sexp_fields(&actual_output);
|
||||
}
|
||||
if actual_output == test.output {
|
||||
true
|
||||
} else {
|
||||
DiffKey::print();
|
||||
print!("{}", TestDiff::new(&actual_output, &test.output));
|
||||
println!();
|
||||
false
|
||||
}
|
||||
});
|
||||
let passed =
|
||||
allocations::record(|| test.check_initial_parse(&language, &test.name, true));
|
||||
|
||||
if !passed {
|
||||
failure_count += 1;
|
||||
|
|
|
|||
|
|
@ -127,6 +127,52 @@ fn detect_language_by_double_barrel_file_extension() {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detect_language_with_dots_in_filename() {
|
||||
let blade_dir = tree_sitter_dir(
|
||||
r#"{
|
||||
"grammars": [
|
||||
{
|
||||
"name": "blade_dots",
|
||||
"path": ".",
|
||||
"scope": "source.blade",
|
||||
"file-types": [
|
||||
"blade.php"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "php_dots",
|
||||
"path": ".",
|
||||
"scope": "source.php",
|
||||
"file-types": [
|
||||
"php"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"version": "0.0.1"
|
||||
}
|
||||
}
|
||||
"#,
|
||||
"blade_dots",
|
||||
);
|
||||
|
||||
let mut loader = Loader::with_parser_lib_path(scratch_dir().to_path_buf());
|
||||
let config = loader
|
||||
.find_language_configurations_at_path(blade_dir.path(), false)
|
||||
.unwrap();
|
||||
|
||||
// this is just to validate that we can read the tree-sitter.json correctly
|
||||
assert_eq!(config[0].scope.as_ref().unwrap(), "source.blade");
|
||||
|
||||
let file_name = blade_dir.path().join("foo.bar.baz.blade.php");
|
||||
fs::write(&file_name, "").unwrap();
|
||||
assert_eq!(
|
||||
get_lang_scope(&loader, &file_name),
|
||||
Some("source.blade".into())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detect_language_without_filename() {
|
||||
let gitignore_dir = tree_sitter_dir(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use std::{ops::Range, str};
|
||||
use std::ops::Range;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ReadRecorder<'a> {
|
||||
|
|
@ -20,7 +20,7 @@ impl<'a> ReadRecorder<'a> {
|
|||
if let Err(i) = self.indices_read.binary_search(&offset) {
|
||||
self.indices_read.insert(i, offset);
|
||||
}
|
||||
&self.content[offset..(offset + 1)]
|
||||
&self.content[offset..=offset]
|
||||
} else {
|
||||
&[]
|
||||
}
|
||||
|
|
@ -30,7 +30,7 @@ impl<'a> ReadRecorder<'a> {
|
|||
let mut result = Vec::new();
|
||||
let mut last_range = Option::<Range<usize>>::None;
|
||||
for index in &self.indices_read {
|
||||
if let Some(ref mut range) = &mut last_range {
|
||||
if let Some(range) = &mut last_range {
|
||||
if range.end == *index {
|
||||
range.end += 1;
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
use std::{
|
||||
collections::HashSet,
|
||||
env, fs,
|
||||
path::{Path, PathBuf},
|
||||
sync::LazyLock,
|
||||
sync::{LazyLock, Mutex},
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use tree_sitter::Language;
|
||||
use tree_sitter_generate::{load_grammar_file, ALLOC_HEADER, ARRAY_HEADER};
|
||||
use tree_sitter_generate::{ALLOC_HEADER, ARRAY_HEADER, load_grammar_file};
|
||||
use tree_sitter_highlight::HighlightConfiguration;
|
||||
use tree_sitter_loader::{CompileConfig, Loader};
|
||||
use tree_sitter_tags::TagsConfiguration;
|
||||
|
|
@ -23,6 +24,10 @@ static TEST_LOADER: LazyLock<Loader> = LazyLock::new(|| {
|
|||
loader
|
||||
});
|
||||
|
||||
// Prevents parallel tests from racing on the same per-grammar
|
||||
// `src_dir/tree_sitter/` and observing a half-rewritten header.
|
||||
static WRITTEN_HEADER_DIRS: LazyLock<Mutex<HashSet<PathBuf>>> = LazyLock::new(Default::default);
|
||||
|
||||
#[cfg(feature = "wasm")]
|
||||
pub static ENGINE: LazyLock<tree_sitter::wasmtime::Engine> = LazyLock::new(Default::default);
|
||||
|
||||
|
|
@ -134,17 +139,22 @@ fn get_test_language_internal(
|
|||
};
|
||||
|
||||
let header_path = src_dir.join("tree_sitter");
|
||||
fs::create_dir_all(&header_path).unwrap();
|
||||
|
||||
for (file, content) in [
|
||||
("alloc.h", ALLOC_HEADER),
|
||||
("array.h", ARRAY_HEADER),
|
||||
("parser.h", tree_sitter::PARSER_HEADER),
|
||||
] {
|
||||
let file = header_path.join(file);
|
||||
fs::write(&file, content)
|
||||
.with_context(|| format!("Failed to write {:?}", file.file_name().unwrap()))
|
||||
.unwrap();
|
||||
if WRITTEN_HEADER_DIRS
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(header_path.clone())
|
||||
{
|
||||
fs::create_dir_all(&header_path).unwrap();
|
||||
for (file, content) in [
|
||||
("alloc.h", ALLOC_HEADER),
|
||||
("array.h", ARRAY_HEADER),
|
||||
("parser.h", tree_sitter::PARSER_HEADER),
|
||||
] {
|
||||
let path = header_path.join(file);
|
||||
fs::write(&path, content)
|
||||
.with_context(|| format!("Failed to write {}", path.display()))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
let paths_to_check = if let Some(scanner_path) = &scanner_path {
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
use std::{cmp::Ordering, fmt::Write, ops::Range};
|
||||
|
||||
use rand::prelude::Rng;
|
||||
use rand::{Rng, RngExt};
|
||||
use streaming_iterator::{IntoStreamingIterator, StreamingIterator};
|
||||
use tree_sitter::{
|
||||
Language, Node, Parser, Point, Query, QueryCapture, QueryCursor, QueryMatch, Tree, TreeCursor,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Pattern {
|
||||
kind: Option<&'static str>,
|
||||
pub struct Pattern<'a> {
|
||||
kind: Option<&'a str>,
|
||||
named: bool,
|
||||
field: Option<&'static str>,
|
||||
field: Option<&'a str>,
|
||||
capture: Option<String>,
|
||||
children: Vec<Self>,
|
||||
}
|
||||
|
|
@ -25,17 +25,17 @@ const CAPTURE_NAMES: &[&str] = &[
|
|||
"one", "two", "three", "four", "five", "six", "seven", "eight",
|
||||
];
|
||||
|
||||
impl Pattern {
|
||||
pub fn random_pattern_in_tree(tree: &Tree, rng: &mut impl Rng) -> (Self, Range<Point>) {
|
||||
impl<'a> Pattern<'a> {
|
||||
pub fn random_pattern_in_tree(tree: &'a Tree, rng: &mut impl Rng) -> (Self, Range<Point>) {
|
||||
let mut cursor = tree.walk();
|
||||
|
||||
// Descend to the node at a random byte offset and depth.
|
||||
let mut max_depth = 0;
|
||||
let byte_offset = rng.gen_range(0..cursor.node().end_byte());
|
||||
let byte_offset = rng.random_range(0..cursor.node().end_byte());
|
||||
while cursor.goto_first_child_for_byte(byte_offset).is_some() {
|
||||
max_depth += 1;
|
||||
}
|
||||
let depth = rng.gen_range(0..=max_depth);
|
||||
let depth = rng.random_range(0..=max_depth);
|
||||
for _ in 0..depth {
|
||||
cursor.goto_parent();
|
||||
}
|
||||
|
|
@ -45,7 +45,7 @@ impl Pattern {
|
|||
let pattern_start = cursor.node().start_position();
|
||||
let mut roots = vec![Self::random_pattern_for_node(&mut cursor, rng)];
|
||||
while roots.len() < 5 && cursor.goto_next_sibling() {
|
||||
if rng.gen_bool(0.2) {
|
||||
if rng.random_bool(0.2) {
|
||||
roots.push(Self::random_pattern_for_node(&mut cursor, rng));
|
||||
}
|
||||
}
|
||||
|
|
@ -75,26 +75,26 @@ impl Pattern {
|
|||
(pattern, pattern_start..pattern_end)
|
||||
}
|
||||
|
||||
fn random_pattern_for_node(cursor: &mut TreeCursor, rng: &mut impl Rng) -> Self {
|
||||
fn random_pattern_for_node(cursor: &mut TreeCursor<'a>, rng: &mut impl Rng) -> Self {
|
||||
let node = cursor.node();
|
||||
|
||||
// Sometimes specify the node's type, sometimes use a wildcard.
|
||||
let (kind, named) = if rng.gen_bool(0.9) {
|
||||
let (kind, named) = if rng.random_bool(0.9) {
|
||||
(Some(node.kind()), node.is_named())
|
||||
} else {
|
||||
(Some("_"), node.is_named() && rng.gen_bool(0.8))
|
||||
(Some("_"), node.is_named() && rng.random_bool(0.8))
|
||||
};
|
||||
|
||||
// Sometimes specify the node's field.
|
||||
let field = if rng.gen_bool(0.75) {
|
||||
let field = if rng.random_bool(0.75) {
|
||||
cursor.field_name()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Sometimes capture the node.
|
||||
let capture = if rng.gen_bool(0.7) {
|
||||
Some(CAPTURE_NAMES[rng.gen_range(0..CAPTURE_NAMES.len())].to_string())
|
||||
let capture = if rng.random_bool(0.7) {
|
||||
Some(CAPTURE_NAMES[rng.random_range(0..CAPTURE_NAMES.len())].to_string())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
|
@ -102,9 +102,9 @@ impl Pattern {
|
|||
// Walk the children and include child patterns for some of them.
|
||||
let mut children = Vec::new();
|
||||
if named && cursor.goto_first_child() {
|
||||
let max_children = rng.gen_range(0..4);
|
||||
let max_children = rng.random_range(0..4);
|
||||
while cursor.goto_next_sibling() {
|
||||
if rng.gen_bool(0.6) {
|
||||
if rng.random_bool(0.6) {
|
||||
let child_ast = Self::random_pattern_for_node(cursor, rng);
|
||||
children.push(child_ast);
|
||||
if children.len() >= max_children {
|
||||
|
|
@ -204,10 +204,10 @@ impl Pattern {
|
|||
}
|
||||
|
||||
// If a field is specified, check that it matches the node.
|
||||
if let Some(field) = self.field {
|
||||
if cursor.field_name() != Some(field) {
|
||||
return Vec::new();
|
||||
}
|
||||
if let Some(field) = self.field
|
||||
&& cursor.field_name() != Some(field)
|
||||
{
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
// Create a match for the current node.
|
||||
|
|
@ -225,7 +225,7 @@ impl Pattern {
|
|||
}
|
||||
|
||||
// Find every matching combination of child patterns and child nodes.
|
||||
let mut finished_matches = Vec::<Match>::new();
|
||||
let mut finished_matches = Vec::<Match<'_, 'tree>>::new();
|
||||
if cursor.goto_first_child() {
|
||||
let mut match_states = vec![(0, mat)];
|
||||
loop {
|
||||
|
|
@ -268,7 +268,7 @@ impl Pattern {
|
|||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Pattern {
|
||||
impl std::fmt::Display for Pattern<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut result = String::new();
|
||||
self.write_to_string(&mut result, 0);
|
||||
|
|
@ -336,7 +336,7 @@ pub fn collect_matches<'a>(
|
|||
while let Some(m) = matches.next() {
|
||||
result.push((
|
||||
m.pattern_index,
|
||||
format_captures(m.captures.iter().into_streaming_iter_ref(), query, source),
|
||||
format_captures(m.captures().iter().into_streaming_iter_ref(), query, source),
|
||||
));
|
||||
}
|
||||
result
|
||||
|
|
@ -347,7 +347,7 @@ pub fn collect_captures<'a>(
|
|||
query: &'a Query,
|
||||
source: &'a str,
|
||||
) -> Vec<(&'a str, &'a str)> {
|
||||
format_captures(captures.map(|(m, i)| m.captures[*i]), query, source)
|
||||
format_captures(captures.map(|(m, i)| m.captures()[*i]), query, source)
|
||||
}
|
||||
|
||||
fn format_captures<'a>(
|
||||
|
|
|
|||
|
|
@ -2,15 +2,15 @@ use std::{
|
|||
ffi::CString,
|
||||
fs,
|
||||
os::raw::c_char,
|
||||
ptr, slice, str,
|
||||
ptr, slice,
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
LazyLock,
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
},
|
||||
};
|
||||
|
||||
use tree_sitter_highlight::{
|
||||
c, Error, Highlight, HighlightConfiguration, HighlightEvent, Highlighter, HtmlRenderer,
|
||||
Error, Highlight, HighlightConfiguration, HighlightEvent, Highlighter, HtmlRenderer, c,
|
||||
};
|
||||
|
||||
use super::helpers::fixtures::{get_highlight_config, get_language, get_language_queries_path};
|
||||
|
|
@ -485,6 +485,7 @@ fn test_highlighting_cancellation() {
|
|||
.highlight(
|
||||
&HTML_HIGHLIGHT,
|
||||
source.as_bytes(),
|
||||
None,
|
||||
Some(&cancellation_flag),
|
||||
injection_callback,
|
||||
)
|
||||
|
|
@ -495,7 +496,7 @@ fn test_highlighting_cancellation() {
|
|||
let found_cancellation_error = events.any(|event| match event {
|
||||
Ok(_) => false,
|
||||
Err(Error::Cancelled) => true,
|
||||
Err(Error::InvalidLanguage | Error::Unknown) => {
|
||||
Err(Error::InvalidLanguage(_) | Error::Unknown) => {
|
||||
unreachable!("Unexpected error type while iterating events")
|
||||
}
|
||||
});
|
||||
|
|
@ -727,6 +728,7 @@ fn to_html<'a>(
|
|||
language_config,
|
||||
src,
|
||||
None,
|
||||
None,
|
||||
&test_language_for_injection_string,
|
||||
)?;
|
||||
|
||||
|
|
@ -747,7 +749,10 @@ fn to_html<'a>(
|
|||
.collect())
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
#[expect(
|
||||
clippy::type_complexity,
|
||||
reason = "return type represents structured highlight tokens"
|
||||
)]
|
||||
fn to_token_vector<'a>(
|
||||
src: &'a str,
|
||||
language_config: &'a HighlightConfiguration,
|
||||
|
|
@ -761,6 +766,7 @@ fn to_token_vector<'a>(
|
|||
language_config,
|
||||
src,
|
||||
None,
|
||||
None,
|
||||
&test_language_for_injection_string,
|
||||
)?;
|
||||
for event in events {
|
||||
|
|
|
|||
|
|
@ -31,14 +31,17 @@ fn test_lookahead_iterator() {
|
|||
let mut lookahead = language.lookahead_iterator(next_state).unwrap();
|
||||
assert_eq!(*lookahead.language(), language);
|
||||
assert!(lookahead.iter_names().eq(expected_symbols));
|
||||
assert_eq!(lookahead.iter_names().count(), 0);
|
||||
|
||||
lookahead.reset_state(next_state);
|
||||
assert!(lookahead.reset_state(next_state));
|
||||
assert!(lookahead.iter_names().eq(expected_symbols));
|
||||
|
||||
lookahead.reset(&language, next_state);
|
||||
assert!(lookahead
|
||||
.map(|s| language.node_kind_for_id(s).unwrap())
|
||||
.eq(expected_symbols));
|
||||
assert!(lookahead.reset(&language, next_state));
|
||||
assert!(
|
||||
lookahead
|
||||
.map(|s| language.node_kind_for_id(s).unwrap())
|
||||
.eq(expected_symbols)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -64,6 +67,33 @@ fn test_lookahead_iterator_modifiable_only_by_mut() {
|
|||
let _ = names.next();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lookahead_iterator_exhaustion() {
|
||||
let language = get_language("json");
|
||||
|
||||
for state in 0..language.parse_state_count() {
|
||||
let state = u16::try_from(state).unwrap();
|
||||
let mut lookahead = language.lookahead_iterator(state).unwrap();
|
||||
|
||||
// A fresh iterator is not positioned on a symbol.
|
||||
assert_eq!(lookahead.current_symbol(), None);
|
||||
assert_eq!(lookahead.current_symbol_name(), None);
|
||||
|
||||
let count = lookahead.by_ref().count();
|
||||
|
||||
// An exhausted iterator is not positioned on a symbol, and stays exhausted.
|
||||
assert_eq!(lookahead.current_symbol(), None);
|
||||
assert_eq!(lookahead.current_symbol_name(), None);
|
||||
assert_eq!(lookahead.by_ref().count(), 0);
|
||||
assert_eq!(lookahead.iter_names().count(), 0);
|
||||
|
||||
// Resetting restores it exactly.
|
||||
assert!(lookahead.reset_state(state));
|
||||
assert_eq!(lookahead.current_symbol(), None);
|
||||
assert_eq!(lookahead.by_ref().count(), count);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_symbol_metadata_checks() {
|
||||
let language = get_language("rust");
|
||||
|
|
@ -110,7 +140,7 @@ fn test_supertypes() {
|
|||
supertypes
|
||||
.iter()
|
||||
.filter_map(|&s| language.node_kind_for_id(s))
|
||||
.map(|s| s.to_string())
|
||||
.map(std::string::ToString::to_string)
|
||||
.collect::<Vec<String>>(),
|
||||
vec![
|
||||
"_expression",
|
||||
|
|
|
|||
|
|
@ -2,9 +2,8 @@ use tree_sitter::{InputEdit, Node, Parser, Point, Tree};
|
|||
use tree_sitter_generate::load_grammar_file;
|
||||
|
||||
use super::{
|
||||
get_random_edit,
|
||||
Rand, get_random_edit,
|
||||
helpers::fixtures::{fixtures_dir, get_language, get_test_language},
|
||||
Rand,
|
||||
};
|
||||
use crate::{
|
||||
parse::perform_edit,
|
||||
|
|
@ -286,7 +285,10 @@ fn test_parent_of_zero_width_node() {
|
|||
|
||||
assert_eq!(block.to_string(), "(block)");
|
||||
assert_eq!(block_parent.kind(), "function_definition");
|
||||
assert_eq!(block_parent.to_string(), "(function_definition name: (identifier) parameters: (parameters (identifier)) body: (block))");
|
||||
assert_eq!(
|
||||
block_parent.to_string(),
|
||||
"(function_definition name: (identifier) parameters: (parameters (identifier)) body: (block))"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
root.child_with_descendant(block).unwrap(),
|
||||
|
|
@ -452,7 +454,7 @@ fn test_node_child_by_field_name_with_extra_hidden_children() {
|
|||
// In the Python grammar, some fields are applied to `suite` nodes,
|
||||
// which consist of an invisible `indent` token followed by a block.
|
||||
// Check that when searching for a child with a field name, we don't
|
||||
//
|
||||
// return a hidden child node.
|
||||
let tree = parser.parse("while a:\n pass", None).unwrap();
|
||||
let while_node = tree.root_node().child(0).unwrap();
|
||||
assert_eq!(while_node.kind(), "while_statement");
|
||||
|
|
@ -948,6 +950,13 @@ fn test_node_sexp() {
|
|||
|
||||
#[test]
|
||||
fn test_node_field_names() {
|
||||
// - "x":
|
||||
// This isn't used in the test, but prevents `_hidden_rule1` from being eliminated as a
|
||||
// unit reduction.
|
||||
// - "_hidden_rule1":
|
||||
// Fields pointing to hidden nodes with a single child resolve to the child.
|
||||
// - "_hidden_rule2":
|
||||
// Fields within hidden nodes can be referenced through the parent node.
|
||||
let (parser_name, parser_code) = generate_parser(
|
||||
r#"
|
||||
{
|
||||
|
|
@ -970,8 +979,6 @@ fn test_node_field_names() {
|
|||
{"type": "STRING", "value": "child-1"},
|
||||
{"type": "BLANK"},
|
||||
|
||||
// This isn't used in the test, but prevents `_hidden_rule1`
|
||||
// from being eliminated as a unit reduction.
|
||||
{
|
||||
"type": "ALIAS",
|
||||
"value": "x",
|
||||
|
|
@ -992,7 +999,6 @@ fn test_node_field_names() {
|
|||
]
|
||||
},
|
||||
|
||||
// Fields pointing to hidden nodes with a single child resolve to the child.
|
||||
"_hidden_rule1": {
|
||||
"type": "CHOICE",
|
||||
"members": [
|
||||
|
|
@ -1001,7 +1007,6 @@ fn test_node_field_names() {
|
|||
]
|
||||
},
|
||||
|
||||
// Fields within hidden nodes can be referenced through the parent node.
|
||||
"_hidden_rule2": {
|
||||
"type": "SEQ",
|
||||
"members": [
|
||||
|
|
|
|||
|
|
@ -66,9 +66,13 @@ fn test_parsing_with_logging() {
|
|||
parser.set_language(&get_language("rust")).unwrap();
|
||||
|
||||
let mut messages = Vec::new();
|
||||
parser.set_logger(Some(Box::new(|log_type, message| {
|
||||
messages.push((log_type, message.to_string()));
|
||||
})));
|
||||
// SAFETY: the logger borrows `messages` and is only invoked during the
|
||||
// `parse` call below while `messages` is in scope.
|
||||
unsafe {
|
||||
parser.set_logger_unchecked(Some(Box::new(|log_type, message| {
|
||||
messages.push((log_type, message.to_string()));
|
||||
})));
|
||||
}
|
||||
|
||||
parser
|
||||
.parse(
|
||||
|
|
@ -249,6 +253,79 @@ fn test_parsing_with_custom_utf16_be_input() {
|
|||
assert_eq!(root.child(0).unwrap().kind(), "function_item");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_utf16_decodes_surrogate_pairs() {
|
||||
let mut parser = Parser::new();
|
||||
let language = get_test_fixture_language("utf16_surrogate_oob");
|
||||
parser.set_language(&language).unwrap();
|
||||
|
||||
let le = [0xD83D_u16.to_le(), 0xDE00_u16.to_le()];
|
||||
let tree = parser.parse_utf16_le(le, None).unwrap();
|
||||
assert_eq!(tree.root_node().to_sexp(), "(program (supplementary))");
|
||||
|
||||
let be = [0xD83D_u16.to_be(), 0xDE00_u16.to_be()];
|
||||
let tree = parser.parse_utf16_be(be, None).unwrap();
|
||||
assert_eq!(tree.root_node().to_sexp(), "(program (supplementary))");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_utf16_decode_does_not_read_oob() {
|
||||
// Test for a buffer over-read in ts_decode_utf16_le/be when a lead surrogate
|
||||
// is the last code unit in a chunk. The test grammar's external scanner
|
||||
// distinguishes surrogate code points from supplementary-plane characters,
|
||||
// making the over-read directly observable in the parse tree.
|
||||
//
|
||||
// Buffer layout:
|
||||
// buf[0] = 0xD83E (lead surrogate)
|
||||
// buf[1] = 0xDD8B (POISON: fake trail surrogate, adjacent in memory)
|
||||
//
|
||||
// The callback returns only buf[0..1] (one code unit = 2 bytes).
|
||||
//
|
||||
// When functioning correctly, this test passes a length of 2 bytes, which is
|
||||
// interpreted as 2/2 = 1 code unit, and thus doesn't over-read into the "poison"
|
||||
// fake trail surrogate. If an over-read does occur, the scanner sees a
|
||||
// supplementary token.
|
||||
let mut parser = Parser::new();
|
||||
let language = get_test_fixture_language("utf16_surrogate_oob");
|
||||
parser.set_language(&language).unwrap();
|
||||
|
||||
let buf = vec![
|
||||
0xD83E, // lead surrogate (the only "visible" code unit)
|
||||
0xDD8B, // POISON: adjacent in Vec memory, past the chunk
|
||||
];
|
||||
assert_eq!("🦋", String::from_utf16(&buf).unwrap());
|
||||
|
||||
let mut callback = |offset: usize, _position: Point| -> &[u16] {
|
||||
// only expose buf[0], never buf[1]
|
||||
if offset >= 1 {
|
||||
return [].as_slice();
|
||||
}
|
||||
&buf[0..1]
|
||||
};
|
||||
|
||||
// Use the parse function matching the host endianness, since the
|
||||
// buffer contains native u16 values.
|
||||
#[cfg(target_endian = "little")]
|
||||
let tree = parser
|
||||
.parse_utf16_le_with_options(&mut callback, None, None)
|
||||
.unwrap();
|
||||
#[cfg(target_endian = "big")]
|
||||
let tree = parser
|
||||
.parse_utf16_be_with_options(&mut callback, None, None)
|
||||
.unwrap();
|
||||
|
||||
let root = tree.root_node();
|
||||
|
||||
// Correct: scanner sees raw surrogate (0xD83E) -> `surrogate` node
|
||||
// Incorrect: scanner sees supplementary (U+1F98B, aka 🦋) -> `supplementary` node
|
||||
assert_eq!(
|
||||
root.to_sexp(),
|
||||
"(program (surrogate))",
|
||||
"buffer over-read: decoder read past chunk boundary and formed a \
|
||||
supplementary character from OOB adjacent memory"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parsing_with_callback_returning_owned_strings() {
|
||||
let mut parser = Parser::new();
|
||||
|
|
@ -745,11 +822,7 @@ fn test_parsing_cancelled_by_another_thread() {
|
|||
&mut |offset, _| {
|
||||
thread::yield_now();
|
||||
thread::sleep(time::Duration::from_millis(10));
|
||||
if offset == 0 {
|
||||
b" ["
|
||||
} else {
|
||||
b"0,"
|
||||
}
|
||||
if offset == 0 { b" [" } else { b"0," }
|
||||
},
|
||||
None,
|
||||
Some(ParseOptions::new().progress_callback(callback)),
|
||||
|
|
@ -772,11 +845,7 @@ fn test_parsing_with_a_timeout() {
|
|||
let start_time = time::Instant::now();
|
||||
let tree = parser.parse_with_options(
|
||||
&mut |offset, _| {
|
||||
if offset == 0 {
|
||||
b" ["
|
||||
} else {
|
||||
b",0"
|
||||
}
|
||||
if offset == 0 { b" [" } else { b",0" }
|
||||
},
|
||||
None,
|
||||
Some(ParseOptions::new().progress_callback(&mut |_| {
|
||||
|
|
@ -794,11 +863,7 @@ fn test_parsing_with_a_timeout() {
|
|||
let start_time = time::Instant::now();
|
||||
let tree = parser.parse_with_options(
|
||||
&mut |offset, _| {
|
||||
if offset == 0 {
|
||||
b" ["
|
||||
} else {
|
||||
b",0"
|
||||
}
|
||||
if offset == 0 { b" [" } else { b",0" }
|
||||
},
|
||||
None,
|
||||
Some(ParseOptions::new().progress_callback(&mut |_| {
|
||||
|
|
@ -995,9 +1060,9 @@ fn test_parsing_with_timeout_during_balancing() {
|
|||
let mut parser = Parser::new();
|
||||
parser.set_language(&get_language("javascript")).unwrap();
|
||||
|
||||
let function_count = 100;
|
||||
let function_count: u32 = 100;
|
||||
|
||||
let code = "function() {}\n".repeat(function_count);
|
||||
let code = "function() {}\n".repeat(function_count as usize);
|
||||
let mut current_byte_offset = 0;
|
||||
let mut in_balancing = false;
|
||||
let tree = parser.parse_with_options(
|
||||
|
|
@ -1071,7 +1136,7 @@ fn test_parsing_with_timeout_during_balancing() {
|
|||
Some(ParseOptions::new().progress_callback(&mut |state| {
|
||||
// Because we've already finished parsing, we should only be resuming the
|
||||
// balancing phase.
|
||||
assert!(state.current_byte_offset() == current_byte_offset);
|
||||
assert_eq!(state.current_byte_offset(), current_byte_offset);
|
||||
ControlFlow::Continue(())
|
||||
})),
|
||||
)
|
||||
|
|
@ -1739,11 +1804,15 @@ fn test_parsing_with_scanner_logging() {
|
|||
.unwrap();
|
||||
|
||||
let mut found = false;
|
||||
parser.set_logger(Some(Box::new(|log_type, message| {
|
||||
if log_type == LogType::Lex && message == "Found a percent string" {
|
||||
found = true;
|
||||
}
|
||||
})));
|
||||
// SAFETY: the logger borrows `found` and is only invoked during the `parse`
|
||||
// call below, while `found` is in scope.
|
||||
unsafe {
|
||||
parser.set_logger_unchecked(Some(Box::new(|log_type, message| {
|
||||
if log_type == LogType::Lex && message == "Found a percent string" {
|
||||
found = true;
|
||||
}
|
||||
})));
|
||||
}
|
||||
|
||||
let source_code = "x + %(sup (external) scanner?)";
|
||||
|
||||
|
|
@ -1863,7 +1932,7 @@ fn test_decode_cp1252() {
|
|||
fn decode(bytes: &[u8]) -> (i32, u32) {
|
||||
if !bytes.is_empty() {
|
||||
let byte = bytes[0];
|
||||
(byte as i32, 1)
|
||||
(i32::from(byte), 1)
|
||||
} else {
|
||||
(0, 0)
|
||||
}
|
||||
|
|
@ -1899,7 +1968,7 @@ fn test_decode_macintosh() {
|
|||
fn decode(bytes: &[u8]) -> (i32, u32) {
|
||||
if !bytes.is_empty() {
|
||||
let byte = bytes[0];
|
||||
(byte as i32, 1)
|
||||
(i32::from(byte), 1)
|
||||
} else {
|
||||
(0, 0)
|
||||
}
|
||||
|
|
@ -1961,8 +2030,9 @@ fn test_decode_utf24le() {
|
|||
|
||||
#[test]
|
||||
fn test_grammars_that_should_not_compile() {
|
||||
assert!(generate_parser(
|
||||
r#"
|
||||
assert!(
|
||||
generate_parser(
|
||||
r#"
|
||||
{
|
||||
"name": "issue_1111",
|
||||
"rules": {
|
||||
|
|
@ -1970,11 +2040,13 @@ fn test_grammars_that_should_not_compile() {
|
|||
},
|
||||
}
|
||||
"#
|
||||
)
|
||||
.is_err());
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
|
||||
assert!(generate_parser(
|
||||
r#"
|
||||
assert!(
|
||||
generate_parser(
|
||||
r#"
|
||||
{
|
||||
"name": "issue_1271",
|
||||
"rules": {
|
||||
|
|
@ -1989,11 +2061,13 @@ fn test_grammars_that_should_not_compile() {
|
|||
},
|
||||
}
|
||||
"#
|
||||
)
|
||||
.is_err());
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
|
||||
assert!(generate_parser(
|
||||
r#"
|
||||
assert!(
|
||||
generate_parser(
|
||||
r#"
|
||||
{
|
||||
"name": "issue_1156_expl_1",
|
||||
"rules": {
|
||||
|
|
@ -2007,11 +2081,13 @@ fn test_grammars_that_should_not_compile() {
|
|||
},
|
||||
}
|
||||
"#
|
||||
)
|
||||
.is_err());
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
|
||||
assert!(generate_parser(
|
||||
r#"
|
||||
assert!(
|
||||
generate_parser(
|
||||
r#"
|
||||
{
|
||||
"name": "issue_1156_expl_2",
|
||||
"rules": {
|
||||
|
|
@ -2028,11 +2104,13 @@ fn test_grammars_that_should_not_compile() {
|
|||
},
|
||||
}
|
||||
"#
|
||||
)
|
||||
.is_err());
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
|
||||
assert!(generate_parser(
|
||||
r#"
|
||||
assert!(
|
||||
generate_parser(
|
||||
r#"
|
||||
{
|
||||
"name": "issue_1156_expl_3",
|
||||
"rules": {
|
||||
|
|
@ -2046,11 +2124,13 @@ fn test_grammars_that_should_not_compile() {
|
|||
},
|
||||
}
|
||||
"#
|
||||
)
|
||||
.is_err());
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
|
||||
assert!(generate_parser(
|
||||
r#"
|
||||
assert!(
|
||||
generate_parser(
|
||||
r#"
|
||||
{
|
||||
"name": "issue_1156_expl_4",
|
||||
"rules": {
|
||||
|
|
@ -2067,8 +2147,9 @@ fn test_grammars_that_should_not_compile() {
|
|||
},
|
||||
}
|
||||
"#
|
||||
)
|
||||
.is_err());
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
const fn simple_range(start: usize, end: usize) -> Range {
|
||||
|
|
|
|||
|
|
@ -14,4 +14,4 @@ proc-macro = true
|
|||
[dependencies]
|
||||
proc-macro2 = "1.0.93"
|
||||
quote = "1.0.38"
|
||||
syn = { version = "2.0.96", features = ["full"] }
|
||||
syn = { features = [ "full" ], version = "2.0.96" }
|
||||
|
|
|
|||
|
|
@ -2,8 +2,9 @@ use proc_macro::TokenStream;
|
|||
use proc_macro2::Span;
|
||||
use quote::quote;
|
||||
use syn::{
|
||||
Error, Expr, Ident, ItemFn, LitInt, Token,
|
||||
parse::{Parse, ParseStream},
|
||||
parse_macro_input, Error, Expr, Ident, ItemFn, LitInt, Token,
|
||||
parse_macro_input,
|
||||
};
|
||||
|
||||
#[proc_macro_attribute]
|
||||
|
|
@ -68,7 +69,7 @@ pub fn test_with_seed(args: TokenStream, input: TokenStream) -> TokenStream {
|
|||
return Err(Error::new(
|
||||
name.span(),
|
||||
format!("Unsupported parameter `{x}`"),
|
||||
))
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
use std::{env, fmt::Write, ops::ControlFlow, sync::LazyLock};
|
||||
|
||||
use indoc::indoc;
|
||||
use rand::{prelude::StdRng, SeedableRng};
|
||||
use rand::{SeedableRng, prelude::StdRng};
|
||||
use streaming_iterator::StreamingIterator;
|
||||
use tree_sitter::{
|
||||
CaptureQuantifier, InputEdit, Language, Node, Parser, Point, Query, QueryCursor,
|
||||
QueryCursorOptions, QueryError, QueryErrorKind, QueryPredicate, QueryPredicateArg,
|
||||
QueryProperty, Range,
|
||||
QueryCursorOptions, QueryCursorState, QueryError, QueryErrorKind, QueryPredicate,
|
||||
QueryPredicateArg, QueryProperty, Range,
|
||||
};
|
||||
use tree_sitter_generate::load_grammar_file;
|
||||
use unindent::Unindent;
|
||||
|
|
@ -14,15 +14,14 @@ use unindent::Unindent;
|
|||
use super::helpers::{
|
||||
allocations,
|
||||
fixtures::{get_language, get_test_language},
|
||||
query_helpers::{assert_query_matches, Match, Pattern},
|
||||
query_helpers::{Match, Pattern, assert_query_matches},
|
||||
};
|
||||
use crate::tests::{
|
||||
generate_parser,
|
||||
ITERATION_COUNT, generate_parser,
|
||||
helpers::{
|
||||
fixtures::get_test_fixture_language,
|
||||
query_helpers::{collect_captures, collect_matches},
|
||||
},
|
||||
ITERATION_COUNT,
|
||||
};
|
||||
|
||||
static EXAMPLE_FILTER: LazyLock<Option<String>> =
|
||||
|
|
@ -34,11 +33,13 @@ fn test_query_errors_on_invalid_syntax() {
|
|||
let language = get_language("javascript");
|
||||
|
||||
assert!(Query::new(&language, "(if_statement)").is_ok());
|
||||
assert!(Query::new(
|
||||
&language,
|
||||
"(if_statement condition:(parenthesized_expression (identifier)))"
|
||||
)
|
||||
.is_ok());
|
||||
assert!(
|
||||
Query::new(
|
||||
&language,
|
||||
"(if_statement condition:(parenthesized_expression (identifier)))"
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
|
||||
// Mismatched parens
|
||||
assert_eq!(
|
||||
|
|
@ -255,6 +256,51 @@ fn test_query_errors_on_invalid_syntax() {
|
|||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_query_errors_on_anchor_at_group_edge() {
|
||||
allocations::record(|| {
|
||||
let language = get_language("javascript");
|
||||
|
||||
// Anchors between siblings, or at the first/last position of a *node*
|
||||
// pattern, are valid.
|
||||
assert!(Query::new(&language, "((_) . (_))").is_ok());
|
||||
assert!(Query::new(&language, "(program (_) (_) .)").is_ok());
|
||||
assert!(Query::new(&language, "(program (_)* @x . (_))").is_ok());
|
||||
|
||||
// A `.` at the edge of a *group* is rejected. A group is not a node, so it
|
||||
// has no last child to anchor, and there is no sibling within the group to
|
||||
// anchor to.
|
||||
assert_eq!(
|
||||
Query::new(&language, "((_) .)").unwrap_err(),
|
||||
QueryError {
|
||||
row: 0,
|
||||
offset: 5,
|
||||
column: 5,
|
||||
kind: QueryErrorKind::Syntax,
|
||||
message: [
|
||||
"((_) .)", //
|
||||
" ^"
|
||||
]
|
||||
.join("\n")
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
Query::new(&language, "(program ((_)+ .)? (_))").unwrap_err(),
|
||||
QueryError {
|
||||
row: 0,
|
||||
offset: 15,
|
||||
column: 15,
|
||||
kind: QueryErrorKind::Syntax,
|
||||
message: [
|
||||
"(program ((_)+ .)? (_))", //
|
||||
" ^"
|
||||
]
|
||||
.join("\n")
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_query_errors_on_invalid_symbols() {
|
||||
allocations::record(|| {
|
||||
|
|
@ -370,6 +416,16 @@ fn test_query_errors_on_invalid_symbols() {
|
|||
message: "\"fakefield\"".to_string()
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
Query::new(&language, "(MISS)").unwrap_err(),
|
||||
QueryError {
|
||||
row: 0,
|
||||
offset: 1,
|
||||
column: 1,
|
||||
kind: QueryErrorKind::NodeType,
|
||||
message: "\"MISS\"".to_string(),
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -1107,8 +1163,8 @@ fn test_query_matches_with_immediate_siblings() {
|
|||
// siblings before that child node.
|
||||
// 2. After the last child node in a pattern, it means that there cannot be any named
|
||||
// sibling after that child node.
|
||||
// 2. Between two child nodes in a pattern, it specifies that there cannot be any named
|
||||
// siblings between those two child snodes.
|
||||
// 3. Between two child nodes in a pattern, it specifies that there cannot be any named
|
||||
// siblings between those two child nodes.
|
||||
let query = Query::new(
|
||||
&language,
|
||||
"
|
||||
|
|
@ -1176,6 +1232,209 @@ fn test_query_matches_with_immediate_siblings() {
|
|||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_query_matches_with_anchor_after_zero_quantifier() {
|
||||
allocations::record(|| {
|
||||
let language = get_language("javascript");
|
||||
let query = Query::new(
|
||||
&language,
|
||||
"(program (comment)* @doc . (function_declaration name: (identifier) @name))",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// No comments and the function is not the first child. An anchor after a
|
||||
// zero-matched quantifier is vacuous, so the function still matches.
|
||||
assert_query_matches(
|
||||
&language,
|
||||
&query,
|
||||
"
|
||||
class X {}
|
||||
function foo() {}
|
||||
",
|
||||
&[(0, vec![("name", "foo")])],
|
||||
);
|
||||
|
||||
// With at least one comment the anchor applies, so the comments must
|
||||
// immediately precede the function.
|
||||
assert_query_matches(
|
||||
&language,
|
||||
&query,
|
||||
"
|
||||
// c
|
||||
function foo() {}
|
||||
",
|
||||
&[(0, vec![("doc", "// c"), ("name", "foo")])],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_query_matches_with_anchor_after_nested_zero_quantifier() {
|
||||
allocations::record(|| {
|
||||
let language = get_language("javascript");
|
||||
let query = Query::new(
|
||||
&language,
|
||||
r#"
|
||||
(_
|
||||
(field_definition
|
||||
property: (_) @name
|
||||
value: (_)? @value
|
||||
) @field
|
||||
.
|
||||
";" @semicolon
|
||||
)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_query_matches(
|
||||
&language,
|
||||
&query,
|
||||
"class Foo { bar; baz = 0; }",
|
||||
&[
|
||||
(
|
||||
0,
|
||||
vec![("field", "bar"), ("name", "bar"), ("semicolon", ";")],
|
||||
),
|
||||
(
|
||||
0,
|
||||
vec![
|
||||
("field", "baz = 0"),
|
||||
("name", "baz"),
|
||||
("value", "0"),
|
||||
("semicolon", ";"),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_query_matches_with_last_child_anchor_after_optional() {
|
||||
allocations::record(|| {
|
||||
let language = get_language("c");
|
||||
let query = Query::new(
|
||||
&language,
|
||||
"(preproc_if (preproc_def)+ @def . (preproc_else)? @else .)",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// The optional `(preproc_else)?` is absent, so the trailing anchor's
|
||||
// last-child requirement transfers to the last `preproc_def`. A trailing
|
||||
// comment means the def is not the last child, so nothing matches.
|
||||
assert_query_matches(
|
||||
&language,
|
||||
&query,
|
||||
"
|
||||
#if X
|
||||
#define A
|
||||
// c
|
||||
#endif
|
||||
",
|
||||
&[],
|
||||
);
|
||||
|
||||
// With the def as the last child, the (else-less) match is allowed.
|
||||
assert_query_matches(
|
||||
&language,
|
||||
&query,
|
||||
"
|
||||
#if X
|
||||
#define A
|
||||
#endif
|
||||
",
|
||||
&[(0, vec![("def", "#define A\n")])],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_query_matches_with_anchors_on_both_sides_of_zero_quantifier() {
|
||||
allocations::record(|| {
|
||||
let language = get_language("javascript");
|
||||
let query = Query::new(
|
||||
&language,
|
||||
"(program (lexical_declaration) @a . (comment)* . (function_declaration) @b)",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Anchors on both sides of a zero-matched quantifier collapse into a single
|
||||
// adjacency constraint: with no comments, the declaration must be immediately
|
||||
// followed by the function.
|
||||
assert_query_matches(
|
||||
&language,
|
||||
&query,
|
||||
"
|
||||
const a = 1;
|
||||
const b = 2;
|
||||
function foo() {}
|
||||
",
|
||||
&[(0, vec![("a", "const b = 2;"), ("b", "function foo() {}")])],
|
||||
);
|
||||
|
||||
// With a comment present the quantifier is non-zero, so the anchors apply
|
||||
// normally: the comment must sit immediately between the declaration and the
|
||||
// function.
|
||||
assert_query_matches(
|
||||
&language,
|
||||
&query,
|
||||
"
|
||||
const b = 2;
|
||||
// c
|
||||
function foo() {}
|
||||
",
|
||||
&[(0, vec![("a", "const b = 2;"), ("b", "function foo() {}")])],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_query_matches_with_leading_anchor_before_zero_quantifier() {
|
||||
allocations::record(|| {
|
||||
let language = get_language("c");
|
||||
let query = Query::new(
|
||||
&language,
|
||||
"(translation_unit . (comment)* (function_definition) @f)",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// The leading `.` anchors the comment run to the parent's first child. When the
|
||||
// run matches zero comments, that first-child requirement transfers to the
|
||||
// function, so it matches only when it is itself the first child.
|
||||
assert_query_matches(
|
||||
&language,
|
||||
&query,
|
||||
"
|
||||
int main() {}
|
||||
",
|
||||
&[(0, vec![("f", "int main() {}")])],
|
||||
);
|
||||
|
||||
// The function is the second child, so with no leading comments it must not match.
|
||||
assert_query_matches(
|
||||
&language,
|
||||
&query,
|
||||
"
|
||||
int a;
|
||||
int main() {}
|
||||
",
|
||||
&[],
|
||||
);
|
||||
|
||||
// With a leading comment the run starts at the first child and the function follows.
|
||||
assert_query_matches(
|
||||
&language,
|
||||
&query,
|
||||
"
|
||||
// c
|
||||
int main() {}
|
||||
",
|
||||
&[(0, vec![("f", "int main() {}")])],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_query_matches_with_last_named_child() {
|
||||
allocations::record(|| {
|
||||
|
|
@ -1348,6 +1607,35 @@ fn test_query_matches_with_repeated_leaf_nodes() {
|
|||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_query_matches_optional_capture_before_uncaptured_required_sibling() {
|
||||
allocations::record(|| {
|
||||
let language = get_language("rust");
|
||||
let query = Query::new(&language, "(block (line_comment)? @doc (line_comment))").unwrap();
|
||||
|
||||
// The optional `(line_comment)? @doc` before an *uncaptured* required
|
||||
// `(line_comment)` yields two candidate completions at the block:
|
||||
// - one where the optional captured `// a` (the required node is then `// b`),
|
||||
// - one where the optional matched zero (the required node absorbs `// a`,
|
||||
// leaving `@doc` unbound).
|
||||
// The zero-match completion's captures are a strict subset of the other's, so the
|
||||
// longest-match rule must drop it (there is exactly one match). This regressed when
|
||||
// the dedup pass gained an early-break. Without the accompanying capture-position
|
||||
// sort, the break skips the subset "loser" and it leaks as an extra empty match.
|
||||
assert_query_matches(
|
||||
&language,
|
||||
&query,
|
||||
"
|
||||
fn f() {
|
||||
// a
|
||||
// b
|
||||
}
|
||||
",
|
||||
&[(0, vec![("doc", "// a")])],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_query_matches_with_optional_nodes_inside_of_repetitions() {
|
||||
allocations::record(|| {
|
||||
|
|
@ -1592,6 +1880,148 @@ fn test_query_matches_with_leading_zero_or_more_repeated_leaf_nodes() {
|
|||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_matches_with_anchor_sibling_inside_parent() {
|
||||
allocations::record(|| {
|
||||
let language = get_language("rust");
|
||||
|
||||
let query = Query::new(
|
||||
&language,
|
||||
"
|
||||
(source_file
|
||||
(line_comment)
|
||||
.
|
||||
(function_item
|
||||
name: (identifier) @name)
|
||||
)",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_query_matches(
|
||||
&language,
|
||||
&query,
|
||||
"
|
||||
// A
|
||||
fn a() {}
|
||||
|
||||
// B
|
||||
fn b() {}
|
||||
",
|
||||
&[(0, vec![("name", "a")]), (0, vec![("name", "b")])],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_matches_with_anchor_sibling_with_quantifier_inside_parent() {
|
||||
allocations::record(|| {
|
||||
let language = get_language("rust");
|
||||
|
||||
let query = Query::new(
|
||||
&language,
|
||||
"
|
||||
(source_file
|
||||
(line_comment)+
|
||||
.
|
||||
(function_item
|
||||
name: (identifier) @name)
|
||||
)",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_query_matches(
|
||||
&language,
|
||||
&query,
|
||||
"
|
||||
// A
|
||||
fn a() {}
|
||||
|
||||
// B
|
||||
fn b() {}
|
||||
",
|
||||
&[(0, vec![("name", "a")]), (0, vec![("name", "b")])],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_matches_with_anchor_sibling_with_quantifier_captured_inside_parent() {
|
||||
allocations::record(|| {
|
||||
let language = get_language("rust");
|
||||
|
||||
let query = Query::new(
|
||||
&language,
|
||||
"
|
||||
(source_file
|
||||
(line_comment)+ @doc
|
||||
.
|
||||
(function_item
|
||||
name: (identifier) @name)
|
||||
)",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_query_matches(
|
||||
&language,
|
||||
&query,
|
||||
"
|
||||
// A
|
||||
fn a() {}
|
||||
|
||||
// B
|
||||
fn b() {}
|
||||
",
|
||||
&[
|
||||
(0, vec![("doc", "// A"), ("name", "a")]),
|
||||
(0, vec![("doc", "// B"), ("name", "b")]),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_matches_anchored_quantified_sibling_inside_parent() {
|
||||
allocations::record(|| {
|
||||
let language = get_language("c");
|
||||
let query = Query::new(
|
||||
&language,
|
||||
"(translation_unit (comment)* @comment . (declaration) @decl)",
|
||||
)
|
||||
.unwrap();
|
||||
assert_query_matches(
|
||||
&language,
|
||||
&query,
|
||||
"
|
||||
void foo() {}
|
||||
|
||||
// this one has
|
||||
// two comments
|
||||
extern int baz;
|
||||
|
||||
// this one has a comment
|
||||
extern int bar;
|
||||
",
|
||||
&[
|
||||
(
|
||||
0,
|
||||
vec![
|
||||
("comment", "// this one has"),
|
||||
("comment", "// two comments"),
|
||||
("decl", "extern int baz;"),
|
||||
],
|
||||
),
|
||||
(
|
||||
0,
|
||||
vec![
|
||||
("comment", "// this one has a comment"),
|
||||
("decl", "extern int bar;"),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_query_matches_with_trailing_optional_nodes() {
|
||||
allocations::record(|| {
|
||||
|
|
@ -1940,7 +2370,7 @@ fn test_query_matches_with_too_many_permutations_to_track() {
|
|||
let matches = cursor.matches(&query, tree.root_node(), source.as_bytes());
|
||||
|
||||
// For this pathological query, some match permutations will be dropped.
|
||||
// Just check that a subset of the results are returned, and crash or
|
||||
// Just check that a subset of the results are returned, and no crash or
|
||||
// leak occurs.
|
||||
assert_eq!(
|
||||
collect_matches(matches, &query, source.as_str())[0],
|
||||
|
|
@ -2167,7 +2597,7 @@ fn test_query_matches_with_supertypes() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::reversed_empty_ranges)]
|
||||
#[expect(clippy::reversed_empty_ranges, reason = "testing empty range behavior")]
|
||||
fn test_query_matches_within_byte_range() {
|
||||
allocations::record(|| {
|
||||
let language = get_language("javascript");
|
||||
|
|
@ -2488,7 +2918,7 @@ fn test_query_matches_with_wildcard_at_root_intersecting_byte_range() {
|
|||
);
|
||||
|
||||
while let Some(mat) = match_iter.next() {
|
||||
if let Some(capture) = mat.captures.first() {
|
||||
if let Some(capture) = mat.captures().first() {
|
||||
matches.push(capture.node.kind());
|
||||
}
|
||||
}
|
||||
|
|
@ -2504,7 +2934,7 @@ fn test_query_matches_with_wildcard_at_root_intersecting_byte_range() {
|
|||
);
|
||||
|
||||
while let Some(mat) = match_iter.next() {
|
||||
if let Some(capture) = mat.captures.first() {
|
||||
if let Some(capture) = mat.captures().first() {
|
||||
matches.push(capture.node.kind());
|
||||
}
|
||||
}
|
||||
|
|
@ -2520,7 +2950,7 @@ fn test_query_matches_with_wildcard_at_root_intersecting_byte_range() {
|
|||
);
|
||||
|
||||
while let Some(mat) = match_iter.next() {
|
||||
if let Some(capture) = mat.captures.first() {
|
||||
if let Some(capture) = mat.captures().first() {
|
||||
matches.push(capture.node.kind());
|
||||
}
|
||||
}
|
||||
|
|
@ -2578,7 +3008,7 @@ fn test_query_captures_within_byte_range_assigned_after_iterating() {
|
|||
let mut results = Vec::new();
|
||||
let mut first_five = captures.by_ref().take(5);
|
||||
while let Some((mat, capture_ix)) = first_five.next() {
|
||||
let capture = mat.captures[*capture_ix];
|
||||
let capture = mat.captures()[*capture_ix];
|
||||
results.push((
|
||||
query.capture_names()[capture.index as usize],
|
||||
&source[capture.node.byte_range()],
|
||||
|
|
@ -2601,7 +3031,7 @@ fn test_query_captures_within_byte_range_assigned_after_iterating() {
|
|||
results.clear();
|
||||
captures.set_byte_range(source.find("Ok").unwrap()..source.len());
|
||||
while let Some((mat, capture_ix)) = captures.next() {
|
||||
let capture = mat.captures[*capture_ix];
|
||||
let capture = mat.captures()[*capture_ix];
|
||||
results.push((
|
||||
query.capture_names()[capture.index as usize],
|
||||
&source[capture.node.byte_range()],
|
||||
|
|
@ -2900,7 +3330,7 @@ fn test_query_matches_with_captured_wildcard_at_root() {
|
|||
|
||||
while let Some(m) = match_iter.next() {
|
||||
let captures = m
|
||||
.captures
|
||||
.captures()
|
||||
.iter()
|
||||
.map(|c| {
|
||||
(
|
||||
|
|
@ -3110,6 +3540,39 @@ fn test_query_alternation_with_inner_quantifier() {
|
|||
assert_query_matches(&language, &query, source_code, matches);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_query_alternation_with_outer_quantifier() {
|
||||
let language = get_language("c");
|
||||
let source_code = "#include <foo>
|
||||
#include <bar>
|
||||
#include <baz>
|
||||
|
||||
// comment";
|
||||
let matches = &[(
|
||||
0,
|
||||
vec![
|
||||
("capture", "#include <foo>\n"),
|
||||
("capture", "#include <bar>\n"),
|
||||
("capture", "#include <baz>\n"),
|
||||
("capture", "// comment"),
|
||||
],
|
||||
)];
|
||||
|
||||
let query = "[
|
||||
(preproc_include)
|
||||
(comment)
|
||||
]+ @capture";
|
||||
let query = Query::new(&language, query).unwrap();
|
||||
assert_query_matches(&language, &query, source_code, matches);
|
||||
|
||||
let query = "([
|
||||
(preproc_include)
|
||||
(comment)
|
||||
] (_)?)+ @capture";
|
||||
let query = Query::new(&language, query).unwrap();
|
||||
assert_query_matches(&language, &query, source_code, matches);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_query_matches_with_alternations_and_predicates() {
|
||||
allocations::record(|| {
|
||||
|
|
@ -3848,7 +4311,7 @@ fn test_query_captures_with_matches_removed() {
|
|||
|
||||
let mut captures = cursor.captures(&query, tree.root_node(), source.as_bytes());
|
||||
while let Some((m, i)) = captures.next() {
|
||||
let capture = m.captures[*i];
|
||||
let capture = m.captures()[*i];
|
||||
let text = capture.node.utf8_text(source.as_bytes()).unwrap();
|
||||
if text == "a" {
|
||||
m.remove();
|
||||
|
|
@ -3893,7 +4356,7 @@ fn test_query_captures_with_matches_removed_before_they_finish() {
|
|||
let mut captured_strings = Vec::new();
|
||||
let mut captures = cursor.captures(&query, tree.root_node(), source.as_bytes());
|
||||
while let Some((m, i)) = captures.next() {
|
||||
let capture = m.captures[*i];
|
||||
let capture = m.captures()[*i];
|
||||
let text = capture.node.utf8_text(source.as_bytes()).unwrap();
|
||||
if text == "as" {
|
||||
m.remove();
|
||||
|
|
@ -3934,18 +4397,18 @@ fn test_query_captures_and_matches_iterators_are_fused() {
|
|||
let mut cursor = QueryCursor::new();
|
||||
let mut captures = cursor.captures(&query, tree.root_node(), source.as_bytes());
|
||||
|
||||
assert_eq!(captures.next().unwrap().0.captures[0].index, 0);
|
||||
assert_eq!(captures.next().unwrap().0.captures[0].index, 0);
|
||||
assert_eq!(captures.next().unwrap().0.captures[0].index, 0);
|
||||
assert_eq!(captures.next().unwrap().0.captures()[0].index, 0);
|
||||
assert_eq!(captures.next().unwrap().0.captures()[0].index, 0);
|
||||
assert_eq!(captures.next().unwrap().0.captures()[0].index, 0);
|
||||
assert!(captures.next().is_none());
|
||||
assert!(captures.next().is_none());
|
||||
assert!(captures.next().is_none());
|
||||
drop(captures);
|
||||
|
||||
let mut matches = cursor.matches(&query, tree.root_node(), source.as_bytes());
|
||||
assert_eq!(matches.next().unwrap().captures[0].index, 0);
|
||||
assert_eq!(matches.next().unwrap().captures[0].index, 0);
|
||||
assert_eq!(matches.next().unwrap().captures[0].index, 0);
|
||||
assert_eq!(matches.next().unwrap().captures()[0].index, 0);
|
||||
assert_eq!(matches.next().unwrap().captures()[0].index, 0);
|
||||
assert_eq!(matches.next().unwrap().captures()[0].index, 0);
|
||||
assert!(matches.next().is_none());
|
||||
assert!(matches.next().is_none());
|
||||
assert!(matches.next().is_none());
|
||||
|
|
@ -4120,13 +4583,13 @@ fn test_query_lifetime_is_separate_from_nodes_lifetime() {
|
|||
let language = get_language("javascript");
|
||||
let query = Query::new(&language, query).unwrap();
|
||||
let mut cursor = QueryCursor::new();
|
||||
let node = cursor
|
||||
|
||||
cursor
|
||||
.matches(&query, node, source.as_bytes())
|
||||
.next()
|
||||
.unwrap()
|
||||
.captures[0]
|
||||
.node;
|
||||
node
|
||||
.captures()[0]
|
||||
.node
|
||||
}
|
||||
|
||||
let node = take_first_node_from_captures(source, query, tree.root_node());
|
||||
|
|
@ -4140,14 +4603,14 @@ fn test_query_lifetime_is_separate_from_nodes_lifetime() {
|
|||
let language = get_language("javascript");
|
||||
let query = Query::new(&language, query).unwrap();
|
||||
let mut cursor = QueryCursor::new();
|
||||
let node = cursor
|
||||
|
||||
cursor
|
||||
.captures(&query, node, source.as_bytes())
|
||||
.next()
|
||||
.unwrap()
|
||||
.0
|
||||
.captures[0]
|
||||
.node;
|
||||
node
|
||||
.captures()[0]
|
||||
.node
|
||||
}
|
||||
|
||||
let node = take_first_node_from_matches(source, query, tree.root_node());
|
||||
|
|
@ -4160,7 +4623,7 @@ fn test_query_with_no_patterns() {
|
|||
allocations::record(|| {
|
||||
let language = get_language("javascript");
|
||||
let query = Query::new(&language, "").unwrap();
|
||||
assert!(query.capture_names().is_empty());
|
||||
assert_eq!(query.capture_names(), [] as [&str; 0]);
|
||||
assert_eq!(query.pattern_count(), 0);
|
||||
});
|
||||
}
|
||||
|
|
@ -4233,6 +4696,54 @@ fn test_query_disable_pattern() {
|
|||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_query_deep_clone() {
|
||||
allocations::record(|| {
|
||||
let language = get_language("javascript");
|
||||
let query = Query::new(
|
||||
&language,
|
||||
"
|
||||
(function_declaration
|
||||
name: (identifier) @name)
|
||||
(function_declaration
|
||||
body: (statement_block) @body)
|
||||
",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut clone = query.deep_clone();
|
||||
clone.disable_pattern(1);
|
||||
|
||||
let source = "function foo() { return 1; }";
|
||||
let mut parser = Parser::new();
|
||||
parser.set_language(&language).unwrap();
|
||||
let tree = parser.parse(source, None).unwrap();
|
||||
let mut cursor = QueryCursor::new();
|
||||
|
||||
// The clone with pattern 1 disabled only produces the @name match.
|
||||
let clone_matches = collect_matches(
|
||||
cursor.matches(&clone, tree.root_node(), source.as_bytes()),
|
||||
&clone,
|
||||
source,
|
||||
);
|
||||
assert_eq!(clone_matches, &[(0, vec![("name", "foo")])]);
|
||||
|
||||
// The original is unaffected and still produces both @name and @body.
|
||||
let original_matches = collect_matches(
|
||||
cursor.matches(&query, tree.root_node(), source.as_bytes()),
|
||||
&query,
|
||||
source,
|
||||
);
|
||||
assert_eq!(
|
||||
original_matches,
|
||||
&[
|
||||
(0, vec![("name", "foo")]),
|
||||
(1, vec![("body", "{ return 1; }")]),
|
||||
]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_query_alternative_predicate_prefix() {
|
||||
allocations::record(|| {
|
||||
|
|
@ -4308,7 +4819,7 @@ fn test_query_random() {
|
|||
let transformed_match = Match {
|
||||
last_node: None,
|
||||
captures: mat
|
||||
.captures
|
||||
.captures()
|
||||
.iter()
|
||||
.map(|c| (query.capture_names()[c.index as usize], c.node))
|
||||
.collect::<Vec<_>>(),
|
||||
|
|
@ -4611,10 +5122,10 @@ fn test_query_is_pattern_guaranteed_at_step() {
|
|||
eprintln!();
|
||||
|
||||
for row in rows {
|
||||
if let Some(filter) = EXAMPLE_FILTER.as_ref() {
|
||||
if !row.description.contains(filter.as_str()) {
|
||||
continue;
|
||||
}
|
||||
if let Some(filter) = EXAMPLE_FILTER.as_ref()
|
||||
&& !row.description.contains(filter.as_str())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
eprintln!(" query example: {:?}", row.description);
|
||||
let query = Query::new(&row.language, row.pattern).unwrap();
|
||||
|
|
@ -4707,10 +5218,10 @@ fn test_query_is_pattern_rooted() {
|
|||
|
||||
let language = get_language("python");
|
||||
for row in &rows {
|
||||
if let Some(filter) = EXAMPLE_FILTER.as_ref() {
|
||||
if !row.description.contains(filter.as_str()) {
|
||||
continue;
|
||||
}
|
||||
if let Some(filter) = EXAMPLE_FILTER.as_ref()
|
||||
&& !row.description.contains(filter.as_str())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
eprintln!(" query example: {:?}", row.description);
|
||||
let query = Query::new(&language, row.pattern).unwrap();
|
||||
|
|
@ -4804,10 +5315,10 @@ fn test_query_is_pattern_non_local() {
|
|||
eprintln!();
|
||||
|
||||
for row in &rows {
|
||||
if let Some(filter) = EXAMPLE_FILTER.as_ref() {
|
||||
if !row.description.contains(filter.as_str()) {
|
||||
continue;
|
||||
}
|
||||
if let Some(filter) = EXAMPLE_FILTER.as_ref()
|
||||
&& !row.description.contains(filter.as_str())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
eprintln!(" query example: {:?}", row.description);
|
||||
let query = Query::new(&row.language, row.pattern).unwrap();
|
||||
|
|
@ -5034,10 +5545,10 @@ fn test_capture_quantifiers() {
|
|||
eprintln!();
|
||||
|
||||
for row in rows {
|
||||
if let Some(filter) = EXAMPLE_FILTER.as_ref() {
|
||||
if !row.description.contains(filter.as_str()) {
|
||||
continue;
|
||||
}
|
||||
if let Some(filter) = EXAMPLE_FILTER.as_ref()
|
||||
&& !row.description.contains(filter.as_str())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
eprintln!(" query example: {:?}", row.description);
|
||||
let query = Query::new(&row.language, row.pattern).unwrap();
|
||||
|
|
@ -5322,10 +5833,10 @@ fn test_consecutive_zero_or_modifiers() {
|
|||
let mut len_1 = false;
|
||||
|
||||
while let Some(m) = matches.next() {
|
||||
if m.captures.len() == 3 {
|
||||
if m.captures().len() == 3 {
|
||||
len_3 = true;
|
||||
}
|
||||
if m.captures.len() == 1 {
|
||||
if m.captures().len() == 1 {
|
||||
len_1 = true;
|
||||
}
|
||||
}
|
||||
|
|
@ -5387,7 +5898,7 @@ fn test_query_max_start_depth_more() {
|
|||
let query = Query::new(&language, "(compound_statement) @capture").unwrap();
|
||||
|
||||
let mut matches = cursor.matches(&query, tree.root_node(), source.as_bytes());
|
||||
let node = matches.next().unwrap().captures[0].node;
|
||||
let node = matches.next().unwrap().captures()[0].node;
|
||||
assert_eq!(node.kind(), "compound_statement");
|
||||
|
||||
for row in rows {
|
||||
|
|
@ -5563,21 +6074,21 @@ fn test_query_execution_with_timeout() {
|
|||
let tree = parser.parse(&source_code, None).unwrap();
|
||||
|
||||
let query = Query::new(&language, "(function_declaration) @function").unwrap();
|
||||
let mut cursor = QueryCursor::new();
|
||||
|
||||
let start_time = std::time::Instant::now();
|
||||
let mut progress_callback = |_: &QueryCursorState| {
|
||||
if start_time.elapsed().as_micros() > 1000 {
|
||||
ControlFlow::Break(())
|
||||
} else {
|
||||
ControlFlow::Continue(())
|
||||
}
|
||||
};
|
||||
let mut cursor = QueryCursor::new();
|
||||
let matches = cursor
|
||||
.matches_with_options(
|
||||
&query,
|
||||
tree.root_node(),
|
||||
source_code.as_bytes(),
|
||||
QueryCursorOptions::new().progress_callback(&mut |_| {
|
||||
if start_time.elapsed().as_micros() > 1000 {
|
||||
ControlFlow::Break(())
|
||||
} else {
|
||||
ControlFlow::Continue(())
|
||||
}
|
||||
}),
|
||||
QueryCursorOptions::new().progress_callback(&mut progress_callback),
|
||||
)
|
||||
.count();
|
||||
assert!(matches < 1000);
|
||||
|
|
@ -5588,13 +6099,43 @@ fn test_query_execution_with_timeout() {
|
|||
assert_eq!(matches, 1000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_query_progress_callback_lives_as_long_as_matches() {
|
||||
let language = get_language("javascript");
|
||||
let mut parser = Parser::new();
|
||||
parser.set_language(&language).unwrap();
|
||||
|
||||
let source_code = "function foo() {}\n".repeat(1000);
|
||||
let tree = parser.parse(&source_code, None).unwrap();
|
||||
let query = Query::new(&language, "(function_declaration) @function").unwrap();
|
||||
|
||||
let mut cursor = QueryCursor::new();
|
||||
let mut callback_was_called = false;
|
||||
let mut progress_callback = |_: &QueryCursorState| {
|
||||
callback_was_called = true;
|
||||
ControlFlow::Continue(())
|
||||
};
|
||||
let matches = cursor.matches_with_options(
|
||||
&query,
|
||||
tree.root_node(),
|
||||
source_code.as_bytes(),
|
||||
QueryCursorOptions::new().progress_callback(&mut progress_callback),
|
||||
);
|
||||
|
||||
assert_eq!(matches.count(), 1000);
|
||||
assert!(callback_was_called);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_query_execution_with_points_causing_underflow() {
|
||||
let language = get_language("rust");
|
||||
let mut parser = Parser::new();
|
||||
parser.set_language(&language).unwrap();
|
||||
|
||||
#[allow(clippy::literal_string_with_formatting_args)]
|
||||
#[expect(
|
||||
clippy::literal_string_with_formatting_args,
|
||||
reason = "raw string contains format syntax as literal code"
|
||||
)]
|
||||
let code = r#"fn main() {
|
||||
println!("{:?}", foo());
|
||||
}"#;
|
||||
|
|
@ -5996,3 +6537,64 @@ export default grammar({
|
|||
|
||||
assert_query_matches(&language, &query, source, &[(0, vec![("tuple", "()")])]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_last_child_anchor_looks_past_hidden_repeat() {
|
||||
let language = get_test_fixture_language("last_child_anchor_past_hidden_repeat");
|
||||
|
||||
let source = "T a.b.c\nL a.b.c\nN a!.b!.c\n";
|
||||
|
||||
let query = Query::new(
|
||||
&language,
|
||||
"
|
||||
(trailing_sep (name) @last .)
|
||||
(leading_sep (name) @last .)
|
||||
(trailing_named (name) @last .)
|
||||
",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_query_matches(
|
||||
&language,
|
||||
&query,
|
||||
source,
|
||||
&[
|
||||
(0, vec![("last", "c")]),
|
||||
(1, vec![("last", "c")]),
|
||||
(2, vec![("last", "c")]),
|
||||
],
|
||||
);
|
||||
|
||||
let query = Query::new(
|
||||
&language,
|
||||
"
|
||||
(trailing_sep . (name) @first)
|
||||
(trailing_sep (name) @a . (name) @b)
|
||||
",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_query_matches(
|
||||
&language,
|
||||
&query,
|
||||
source,
|
||||
&[
|
||||
(0, vec![("first", "a")]),
|
||||
(1, vec![("a", "a"), ("b", "b")]),
|
||||
(1, vec![("a", "b"), ("b", "c")]),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_last_child_anchor_looks_past_hidden_node() {
|
||||
allocations::record(|| {
|
||||
let language = get_language("c");
|
||||
|
||||
let query = Query::new(&language, "(translation_unit (_) @last .)").unwrap();
|
||||
|
||||
let source = "enum E { A };\nint x;\nint y;\n";
|
||||
|
||||
assert_query_matches(&language, &query, source, &[(0, vec![("last", "int y;")])]);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
use std::{
|
||||
ffi::{CStr, CString},
|
||||
fs, ptr, slice, str,
|
||||
fs, ptr, slice,
|
||||
sync::atomic::{AtomicUsize, Ordering},
|
||||
};
|
||||
|
||||
use tree_sitter::Point;
|
||||
use tree_sitter_tags::{c_lib as c, Error, TagsConfiguration, TagsContext};
|
||||
use tree_sitter_tags::{Error, TagsConfiguration, TagsContext, c_lib as c};
|
||||
|
||||
use super::helpers::{
|
||||
allocations,
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ use tree_sitter_highlight::{Highlight, Highlighter};
|
|||
|
||||
use super::helpers::fixtures::{get_highlight_config, get_language, test_loader};
|
||||
use crate::{
|
||||
query_testing::{parse_position_comments, Assertion, Utf8Point},
|
||||
test_highlight::get_highlight_positions,
|
||||
query_testing::{Assertion, Utf8Point, parse_position_comments},
|
||||
test_highlight::{Failure, get_highlight_positions, iterate_assertions},
|
||||
};
|
||||
|
||||
#[test]
|
||||
|
|
@ -68,3 +68,195 @@ fn test_highlight_test_with_basic_test() {
|
|||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_assertion_with_non_matching_highlight_at_same_position() {
|
||||
// Test that an assertion fails when the highlight at the position does not match
|
||||
let highlight_names = vec!["keyword".to_string(), "variable".to_string()];
|
||||
let assertions = vec![Assertion::new(1, 0, 1, false, String::from("keyword"))];
|
||||
let highlights = vec![
|
||||
(Utf8Point::new(1, 0), Utf8Point::new(1, 5), Highlight(1)), // "variable" highlight
|
||||
];
|
||||
let result = iterate_assertions(&assertions, &highlights, &highlight_names);
|
||||
|
||||
assert!(result.is_err());
|
||||
let err = result.unwrap_err().downcast::<Failure>().unwrap();
|
||||
assert_eq!(err.row, 1);
|
||||
assert_eq!(err.column, 0);
|
||||
assert_eq!(err.expected_highlight, "keyword");
|
||||
assert_eq!(err.actual_highlights, vec!["variable".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_assertion_with_exact_matching_highlight() {
|
||||
// Test exact match: assertion and highlight have same start and end
|
||||
let highlight_names = vec!["keyword".to_string()];
|
||||
let assertions = vec![Assertion::new(0, 5, 3, false, String::from("keyword"))];
|
||||
let highlights = vec![(Utf8Point::new(0, 5), Utf8Point::new(0, 8), Highlight(0))];
|
||||
let result = iterate_assertions(&assertions, &highlights, &highlight_names);
|
||||
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_assertion_contained_within_highlight() {
|
||||
// Test where assertion is fully contained within a larger highlight
|
||||
let highlight_names = vec!["keyword".to_string()];
|
||||
let assertions = vec![Assertion::new(0, 3, 2, false, String::from("keyword"))];
|
||||
let highlights = vec![(Utf8Point::new(0, 0), Utf8Point::new(0, 10), Highlight(0))];
|
||||
let result = iterate_assertions(&assertions, &highlights, &highlight_names);
|
||||
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_assertion_overlapping_highlight_start() {
|
||||
// Test where assertion starts before highlight but overlaps with it
|
||||
let highlight_names = vec!["keyword".to_string()];
|
||||
let assertions = vec![Assertion::new(0, 3, 4, false, String::from("keyword"))];
|
||||
let highlights = vec![(Utf8Point::new(0, 5), Utf8Point::new(0, 10), Highlight(0))];
|
||||
let result = iterate_assertions(&assertions, &highlights, &highlight_names);
|
||||
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_assertion_with_no_highlights() {
|
||||
// Test that an assertion fails when there are no highlights at all
|
||||
let highlight_names = vec!["keyword".to_string()];
|
||||
let assertions = vec![Assertion::new(0, 0, 1, false, String::from("keyword"))];
|
||||
let highlights = vec![];
|
||||
let result = iterate_assertions(&assertions, &highlights, &highlight_names);
|
||||
|
||||
assert!(result.is_err());
|
||||
let err = result.unwrap_err().downcast::<Failure>().unwrap();
|
||||
assert_eq!(err.row, 0);
|
||||
assert_eq!(err.column, 0);
|
||||
assert_eq!(err.expected_highlight, "keyword");
|
||||
assert_eq!(err.actual_highlights, Vec::<String>::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_assertion_with_highlight_ending_before() {
|
||||
// Test where highlight ends before the assertion starts
|
||||
let highlight_names = vec!["keyword".to_string()];
|
||||
let assertions = vec![Assertion::new(0, 10, 1, false, String::from("keyword"))];
|
||||
let highlights = vec![(Utf8Point::new(0, 0), Utf8Point::new(0, 5), Highlight(0))];
|
||||
let result = iterate_assertions(&assertions, &highlights, &highlight_names);
|
||||
|
||||
assert!(result.is_err());
|
||||
let err = result.unwrap_err().downcast::<Failure>().unwrap();
|
||||
assert_eq!(err.row, 0);
|
||||
assert_eq!(err.column, 10);
|
||||
assert_eq!(err.expected_highlight, "keyword");
|
||||
assert_eq!(err.actual_highlights, Vec::<String>::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_negative_assertion_with_non_matching_highlight() {
|
||||
// Test that a negative assertion passes when the specified highlight is NOT present
|
||||
let highlight_names = vec!["keyword".to_string(), "variable".to_string()];
|
||||
let assertions = vec![Assertion::new(0, 0, 1, true, String::from("keyword"))];
|
||||
let highlights = vec![
|
||||
(Utf8Point::new(0, 0), Utf8Point::new(0, 5), Highlight(1)), // "variable" highlight
|
||||
];
|
||||
let result = iterate_assertions(&assertions, &highlights, &highlight_names);
|
||||
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_negative_assertion_with_matching_highlight() {
|
||||
// Test that a negative assertion fails when the specified highlight IS present
|
||||
let highlight_names = vec!["keyword".to_string()];
|
||||
let assertions = vec![Assertion::new(0, 0, 1, true, String::from("keyword"))];
|
||||
let highlights = vec![
|
||||
(Utf8Point::new(0, 0), Utf8Point::new(0, 5), Highlight(0)), // "keyword" highlight
|
||||
];
|
||||
let result = iterate_assertions(&assertions, &highlights, &highlight_names);
|
||||
|
||||
assert!(result.is_err());
|
||||
let err = result.unwrap_err().downcast::<Failure>().unwrap();
|
||||
assert_eq!(err.row, 0);
|
||||
assert_eq!(err.column, 0);
|
||||
assert_eq!(err.expected_highlight, "!keyword");
|
||||
assert_eq!(err.actual_highlights, vec!["keyword".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_assertions_sequential() {
|
||||
// Test multiple assertions in sequence with non-overlapping highlights
|
||||
let highlight_names = vec!["keyword".to_string(), "variable".to_string()];
|
||||
let assertions = vec![
|
||||
Assertion::new(0, 0, 3, false, String::from("keyword")),
|
||||
Assertion::new(0, 10, 1, false, String::from("variable")),
|
||||
];
|
||||
let highlights = vec![
|
||||
(Utf8Point::new(0, 0), Utf8Point::new(0, 3), Highlight(0)), // "keyword"
|
||||
(Utf8Point::new(0, 10), Utf8Point::new(0, 11), Highlight(1)), // "variable"
|
||||
];
|
||||
let result = iterate_assertions(&assertions, &highlights, &highlight_names);
|
||||
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_highlights_at_same_position() {
|
||||
// Test where multiple highlights overlap at the assertion position
|
||||
let highlight_names = vec![
|
||||
"keyword".to_string(),
|
||||
"variable".to_string(),
|
||||
"function".to_string(),
|
||||
];
|
||||
let assertions = vec![Assertion::new(0, 5, 1, false, String::from("variable"))];
|
||||
let highlights = vec![
|
||||
(Utf8Point::new(0, 0), Utf8Point::new(0, 10), Highlight(0)), // "keyword" spans entire range
|
||||
(Utf8Point::new(0, 5), Utf8Point::new(0, 8), Highlight(1)), // "variable" at assertion position
|
||||
(Utf8Point::new(0, 7), Utf8Point::new(0, 12), Highlight(2)), // "function" overlaps
|
||||
];
|
||||
let result = iterate_assertions(&assertions, &highlights, &highlight_names);
|
||||
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_assertions_across_multiple_rows() {
|
||||
// Test assertions on different rows
|
||||
let highlight_names = vec!["keyword".to_string(), "variable".to_string()];
|
||||
let assertions = vec![
|
||||
Assertion::new(0, 5, 3, false, String::from("keyword")),
|
||||
Assertion::new(2, 10, 1, false, String::from("variable")),
|
||||
];
|
||||
let highlights = vec![
|
||||
(Utf8Point::new(0, 5), Utf8Point::new(0, 8), Highlight(0)), // "keyword" on row 0
|
||||
(Utf8Point::new(2, 10), Utf8Point::new(2, 11), Highlight(1)), // "variable" on row 2
|
||||
];
|
||||
let result = iterate_assertions(&assertions, &highlights, &highlight_names);
|
||||
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_assertion_should_not_match_highlight_on_later_row() {
|
||||
// Test logic for early exit when highlight is on a later row than the assertion
|
||||
let highlight_names = vec!["keyword".to_string()];
|
||||
let assertions = vec![Assertion::new(0, 5, 3, false, String::from("keyword"))];
|
||||
let highlights = vec![
|
||||
(Utf8Point::new(1, 0), Utf8Point::new(1, 5), Highlight(0)), // wrong row
|
||||
];
|
||||
let result = iterate_assertions(&assertions, &highlights, &highlight_names);
|
||||
|
||||
assert!(result.is_err());
|
||||
let err = result.unwrap_err().downcast::<Failure>().unwrap();
|
||||
assert_eq!(err.row, 0);
|
||||
assert_eq!(err.column, 7); // end_column
|
||||
assert_eq!(err.expected_highlight, "keyword");
|
||||
assert_eq!(err.actual_highlights, Vec::<String>::new());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use tree_sitter_tags::TagsContext;
|
|||
|
||||
use super::helpers::fixtures::{get_language, get_tags_config};
|
||||
use crate::{
|
||||
query_testing::{parse_position_comments, Assertion, Utf8Point},
|
||||
query_testing::{Assertion, Utf8Point, parse_position_comments},
|
||||
test_tags::get_tag_positions,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ fn tree_query<I: AsRef<[u8]>>(tree: &Tree, text: impl TextProvider<I>, language:
|
|||
let mut cursor = QueryCursor::new();
|
||||
let mut captures = cursor.captures(&query, tree.root_node(), text);
|
||||
let (match_, idx) = captures.next().unwrap();
|
||||
let capture = match_.captures[*idx];
|
||||
let capture = match_.captures()[*idx];
|
||||
assert_eq!(capture.index as usize, *idx);
|
||||
assert_eq!("comment", capture.node.kind());
|
||||
}
|
||||
|
|
@ -126,9 +126,11 @@ fn test_text_provider_callback_with_str_slice() {
|
|||
check_parsing(text, |_node: Node<'_>| iter::once(text));
|
||||
check_parsing_callback(
|
||||
&mut |offset, _point| {
|
||||
(offset < text.len())
|
||||
.then_some(text.as_bytes())
|
||||
.unwrap_or_default()
|
||||
if offset < text.len() {
|
||||
text.as_bytes()
|
||||
} else {
|
||||
Default::default()
|
||||
}
|
||||
},
|
||||
|_node: Node<'_>| iter::once(text),
|
||||
);
|
||||
|
|
@ -140,9 +142,11 @@ fn test_text_provider_callback_with_owned_string_slice() {
|
|||
|
||||
check_parsing_callback(
|
||||
&mut |offset, _point| {
|
||||
(offset < text.len())
|
||||
.then_some(text.as_bytes())
|
||||
.unwrap_or_default()
|
||||
if offset < text.len() {
|
||||
text.as_bytes()
|
||||
} else {
|
||||
Default::default()
|
||||
}
|
||||
},
|
||||
|_node: Node<'_>| {
|
||||
let slice: String = text.to_owned();
|
||||
|
|
@ -157,9 +161,11 @@ fn test_text_provider_callback_with_owned_bytes_vec_slice() {
|
|||
|
||||
check_parsing_callback(
|
||||
&mut |offset, _point| {
|
||||
(offset < text.len())
|
||||
.then_some(text.as_bytes())
|
||||
.unwrap_or_default()
|
||||
if offset < text.len() {
|
||||
text.as_bytes()
|
||||
} else {
|
||||
Default::default()
|
||||
}
|
||||
},
|
||||
|_node: Node<'_>| {
|
||||
let slice = text.to_owned().into_bytes();
|
||||
|
|
@ -174,9 +180,11 @@ fn test_text_provider_callback_with_owned_arc_of_bytes_slice() {
|
|||
|
||||
check_parsing_callback(
|
||||
&mut |offset, _point| {
|
||||
(offset < text.len())
|
||||
.then_some(text.as_bytes())
|
||||
.unwrap_or_default()
|
||||
if offset < text.len() {
|
||||
text.as_bytes()
|
||||
} else {
|
||||
Default::default()
|
||||
}
|
||||
},
|
||||
|_node: Node<'_>| {
|
||||
let slice: Arc<[u8]> = text.to_owned().into_bytes().into();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
use std::str;
|
||||
|
||||
use tree_sitter::{InputEdit, Parser, Point, Range, Tree};
|
||||
|
||||
use super::helpers::fixtures::get_language;
|
||||
|
|
@ -795,3 +793,44 @@ fn get_changed_ranges(
|
|||
*tree = new_tree;
|
||||
result
|
||||
}
|
||||
|
||||
// Regression test for an incremental reparse bug where an external
|
||||
// scanner's choice depends on lexer->eof() (and thus on the parser's
|
||||
// current included ranges). The cached token at byte 0 was emitted when
|
||||
// the included range stopped just after the opener. Widening the range
|
||||
// to include a later matching delimiter must invalidate that cached
|
||||
// token so the scanner re-runs and emits the open form instead of the
|
||||
// unclosed form.
|
||||
#[test]
|
||||
fn test_reuse_invalidates_scanner_token_when_included_range_expands() {
|
||||
let language = get_test_fixture_language("external_lookahead_eof_boundary");
|
||||
let mut parser = Parser::new();
|
||||
parser.set_language(&language).unwrap();
|
||||
|
||||
let source = "``";
|
||||
|
||||
parser
|
||||
.set_included_ranges(&[Range {
|
||||
start_byte: 0,
|
||||
end_byte: 1,
|
||||
start_point: Point::new(0, 0),
|
||||
end_point: Point::new(0, 1),
|
||||
}])
|
||||
.unwrap();
|
||||
let tree1 = parser.parse(source, None).unwrap();
|
||||
assert_eq!(tree1.root_node().to_sexp(), "(document (unclosed_delim))");
|
||||
|
||||
parser
|
||||
.set_included_ranges(&[Range {
|
||||
start_byte: 0,
|
||||
end_byte: 2,
|
||||
start_point: Point::new(0, 0),
|
||||
end_point: Point::new(0, 2),
|
||||
}])
|
||||
.unwrap();
|
||||
let tree2 = parser.parse(source, Some(&tree1)).unwrap();
|
||||
assert_eq!(
|
||||
tree2.root_node().to_sexp(),
|
||||
"(document (span (open_delim) (close_delim)))"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use tree_sitter::{Parser, Query, QueryCursor, WasmError, WasmErrorKind, WasmStor
|
|||
|
||||
use crate::tests::helpers::{
|
||||
allocations,
|
||||
fixtures::{get_test_fixture_language_wasm, ENGINE, WASM_DIR},
|
||||
fixtures::{ENGINE, WASM_DIR, get_test_fixture_language_wasm},
|
||||
};
|
||||
|
||||
#[test]
|
||||
|
|
@ -73,7 +73,10 @@ fn test_load_wasm_rust_language() {
|
|||
parser.set_wasm_store(store).unwrap();
|
||||
parser.set_language(&language).unwrap();
|
||||
let tree = parser.parse("fn main() {}", None).unwrap();
|
||||
assert_eq!(tree.root_node().to_sexp(), "(source_file (function_item name: (identifier) parameters: (parameters) body: (block)))");
|
||||
assert_eq!(
|
||||
tree.root_node().to_sexp(),
|
||||
"(source_file (function_item name: (identifier) parameters: (parameters) body: (block)))"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -87,7 +90,10 @@ fn test_load_wasm_javascript_language() {
|
|||
parser.set_wasm_store(store).unwrap();
|
||||
parser.set_language(&language).unwrap();
|
||||
let tree = parser.parse("const a = b\nconst c = d", None).unwrap();
|
||||
assert_eq!(tree.root_node().to_sexp(), "(program (lexical_declaration (variable_declarator name: (identifier) value: (identifier))) (lexical_declaration (variable_declarator name: (identifier) value: (identifier))))");
|
||||
assert_eq!(
|
||||
tree.root_node().to_sexp(),
|
||||
"(program (lexical_declaration (variable_declarator name: (identifier) value: (identifier))) (lexical_declaration (variable_declarator name: (identifier) value: (identifier))))"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -101,7 +107,10 @@ fn test_load_wasm_python_language() {
|
|||
parser.set_wasm_store(store).unwrap();
|
||||
parser.set_language(&language).unwrap();
|
||||
let tree = parser.parse("a = b\nc = d", None).unwrap();
|
||||
assert_eq!(tree.root_node().to_sexp(), "(module (expression_statement (assignment left: (identifier) right: (identifier))) (expression_statement (assignment left: (identifier) right: (identifier))))");
|
||||
assert_eq!(
|
||||
tree.root_node().to_sexp(),
|
||||
"(module (expression_statement (assignment left: (identifier) right: (identifier))) (expression_statement (assignment left: (identifier) right: (identifier))))"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -258,12 +267,18 @@ fn test_reset_wasm_store() {
|
|||
parser.set_wasm_store(parser_store).unwrap();
|
||||
parser.set_language(&language).unwrap();
|
||||
let tree = parser.parse("fn main() {}", None).unwrap();
|
||||
assert_eq!(tree.root_node().to_sexp(), "(source_file (function_item name: (identifier) parameters: (parameters) body: (block)))");
|
||||
assert_eq!(
|
||||
tree.root_node().to_sexp(),
|
||||
"(source_file (function_item name: (identifier) parameters: (parameters) body: (block)))"
|
||||
);
|
||||
|
||||
let parser_store = WasmStore::new(&ENGINE).unwrap();
|
||||
parser.set_wasm_store(parser_store).unwrap();
|
||||
let tree = parser.parse("fn main() {}", None).unwrap();
|
||||
assert_eq!(tree.root_node().to_sexp(), "(source_file (function_item name: (identifier) parameters: (parameters) body: (block)))");
|
||||
assert_eq!(
|
||||
tree.root_node().to_sexp(),
|
||||
"(source_file (function_item name: (identifier) parameters: (parameters) body: (block)))"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -299,6 +314,55 @@ fn test_load_wasm_errors() {
|
|||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_wasm_language_with_reserved_words() {
|
||||
// This test exercises a grammar with multiple reserved word sets loaded via WASM.
|
||||
allocations::record(|| {
|
||||
let store = WasmStore::new(&ENGINE).unwrap();
|
||||
let language = get_test_fixture_language_wasm("reserved_words");
|
||||
|
||||
let mut parser = Parser::new();
|
||||
parser.set_wasm_store(store).unwrap();
|
||||
parser.set_language(&language).unwrap();
|
||||
|
||||
// "if" and "while" are globally reserved, so using them as identifiers
|
||||
// should produce an error recovery.
|
||||
let tree = parser
|
||||
.parse("var a =\n\nif (something) {\n c();\n}", None)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
tree.root_node().to_sexp(),
|
||||
concat!(
|
||||
"(program ",
|
||||
"(ERROR (identifier)) ",
|
||||
"(if_statement (parenthesized_expression (identifier)) ",
|
||||
"(block (expression_statement (call_expression (identifier))))))",
|
||||
)
|
||||
);
|
||||
|
||||
// "if" and "while" are NOT reserved in the 'property' context, so they
|
||||
// can appear as object keys without error.
|
||||
let tree = parser
|
||||
.parse("var x = {\n if: a,\n while: b,\n};", None)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
tree.root_node().to_sexp(),
|
||||
concat!(
|
||||
"(program (var_declaration (identifier) (object ",
|
||||
"(pair (identifier) (identifier)) (pair (identifier) (identifier)))))"
|
||||
)
|
||||
);
|
||||
|
||||
// "var" IS reserved in the 'property' context, so using it as a property
|
||||
// key triggers error recovery.
|
||||
let tree = parser.parse("var x = {\nvar y = z;", None).unwrap();
|
||||
assert_eq!(
|
||||
tree.root_node().to_sexp(),
|
||||
"(program (ERROR (identifier)) (var_declaration (identifier) (identifier)))"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wasm_oom() {
|
||||
allocations::record(|| {
|
||||
|
|
@ -323,3 +387,18 @@ fn test_wasm_oom() {
|
|||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lookahead_iterator_outlives_wasm_language() {
|
||||
allocations::record(|| {
|
||||
let mut store = WasmStore::new(&ENGINE).unwrap();
|
||||
let wasm = fs::read(WASM_DIR.join("tree-sitter-ruby.wasm")).unwrap();
|
||||
let language = store.load_language("ruby", &wasm).unwrap();
|
||||
|
||||
let mut lookahead = language.lookahead_iterator(0).unwrap();
|
||||
drop(language);
|
||||
|
||||
// The iterator retains the language, so the names are still live.
|
||||
assert!(lookahead.iter_names().count() > 0);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ pub mod highlight;
|
|||
pub mod init;
|
||||
pub mod input;
|
||||
pub mod logger;
|
||||
pub mod paint;
|
||||
pub mod parse;
|
||||
pub mod playground;
|
||||
pub mod query;
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@ use std::{
|
|||
path::{Path, PathBuf},
|
||||
process::{Child, ChildStdin, Command, Stdio},
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
Arc,
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
},
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use anyhow::{Context, Result, anyhow};
|
||||
use indoc::indoc;
|
||||
use log::error;
|
||||
use tree_sitter::{Parser, Tree};
|
||||
|
|
@ -71,8 +71,8 @@ pub struct LogSession {
|
|||
open_log: bool,
|
||||
}
|
||||
|
||||
pub fn print_tree_graph(tree: &Tree, path: &str, quiet: bool) -> Result<()> {
|
||||
let session = LogSession::new(path, quiet)?;
|
||||
pub fn print_tree_graph(tree: &Tree, path: &str, open_log: bool) -> Result<()> {
|
||||
let session = LogSession::new(path, open_log)?;
|
||||
tree.print_dot_graph(session.dot_process_stdin.as_ref().unwrap());
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -94,9 +94,9 @@ impl LogSession {
|
|||
.stdin(Stdio::piped())
|
||||
.stdout(dot_file)
|
||||
.spawn()
|
||||
.with_context(|| {
|
||||
"Failed to run the `dot` command. Check that graphviz is installed."
|
||||
})?;
|
||||
.with_context(
|
||||
|| "Failed to run the `dot` command. Check that graphviz is installed.",
|
||||
)?;
|
||||
let dot_stdin = dot_process
|
||||
.stdin
|
||||
.take()
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ impl Version {
|
|||
}
|
||||
}
|
||||
|
||||
fn update_file_with<F>(&self, path: &PathBuf, update_fn: F) -> Result<(), UpdateError>
|
||||
fn update_file_with<F>(path: &PathBuf, update_fn: F) -> Result<(), UpdateError>
|
||||
where
|
||||
F: Fn(&str) -> String,
|
||||
{
|
||||
|
|
@ -155,7 +155,7 @@ impl Version {
|
|||
|
||||
fn update_treesitter_json(&self) -> Result<(), UpdateError> {
|
||||
let json_path = self.current_dir.join("tree-sitter.json");
|
||||
self.update_file_with(&json_path, |content| {
|
||||
Self::update_file_with(&json_path, |content| {
|
||||
content
|
||||
.lines()
|
||||
.map(|line| {
|
||||
|
|
@ -189,7 +189,7 @@ impl Version {
|
|||
return Ok(());
|
||||
}
|
||||
|
||||
self.update_file_with(&cargo_toml_path, |content| {
|
||||
Self::update_file_with(&cargo_toml_path, |content| {
|
||||
content
|
||||
.lines()
|
||||
.map(|line| {
|
||||
|
|
@ -236,7 +236,7 @@ impl Version {
|
|||
return Ok(());
|
||||
}
|
||||
|
||||
self.update_file_with(&package_json_path, |content| {
|
||||
Self::update_file_with(&package_json_path, |content| {
|
||||
content
|
||||
.lines()
|
||||
.map(|line| {
|
||||
|
|
@ -292,8 +292,11 @@ impl Version {
|
|||
} else {
|
||||
self.current_dir.join("Makefile")
|
||||
};
|
||||
if !makefile_path.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.update_file_with(&makefile_path, |content| {
|
||||
Self::update_file_with(&makefile_path, |content| {
|
||||
content
|
||||
.lines()
|
||||
.map(|line| {
|
||||
|
|
@ -317,7 +320,7 @@ impl Version {
|
|||
return Ok(());
|
||||
}
|
||||
|
||||
self.update_file_with(&cmake_lists_path, |content| {
|
||||
Self::update_file_with(&cmake_lists_path, |content| {
|
||||
let re = Regex::new(r#"(\s*VERSION\s+)"[0-9]+\.[0-9]+\.[0-9]+""#)
|
||||
.expect("Failed to compile regex");
|
||||
re.replace(
|
||||
|
|
@ -336,7 +339,7 @@ impl Version {
|
|||
return Ok(());
|
||||
}
|
||||
|
||||
self.update_file_with(&pyproject_toml_path, |content| {
|
||||
Self::update_file_with(&pyproject_toml_path, |content| {
|
||||
content
|
||||
.lines()
|
||||
.map(|line| {
|
||||
|
|
@ -360,7 +363,7 @@ impl Version {
|
|||
return Ok(());
|
||||
}
|
||||
|
||||
self.update_file_with(&zig_zon_path, |content| {
|
||||
Self::update_file_with(&zig_zon_path, |content| {
|
||||
let zig_version_prefix = ".version =";
|
||||
content
|
||||
.lines()
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use std::{
|
|||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use anyhow::{Context, Result, anyhow};
|
||||
use tree_sitter::wasm_stdlib_symbols;
|
||||
use tree_sitter_generate::{load_grammar_file, parse_grammar::GrammarJSON};
|
||||
use tree_sitter_loader::Loader;
|
||||
|
|
@ -15,7 +15,7 @@ pub fn load_language_wasm_file(language_dir: &Path) -> Result<(String, Vec<u8>)>
|
|||
.unwrap();
|
||||
let wasm_filename = format!("tree-sitter-{grammar_name}.wasm");
|
||||
let contents = fs::read(language_dir.join(&wasm_filename)).with_context(|| {
|
||||
format!("Failed to read {wasm_filename}. Run `tree-sitter build --wasm` first.",)
|
||||
format!("Failed to read {wasm_filename}. Run `tree-sitter build --wasm` first.")
|
||||
})?;
|
||||
Ok((grammar_name, contents))
|
||||
}
|
||||
|
|
@ -83,14 +83,17 @@ pub fn compile_language_to_wasm(
|
|||
let wasm_bytes = fs::read(&output_filename)?;
|
||||
let parser = Parser::new(0);
|
||||
for payload in parser.parse_all(&wasm_bytes) {
|
||||
if let wasmparser::Payload::ImportSection(imports) = payload? {
|
||||
for import in imports {
|
||||
let import = import?.name;
|
||||
if !builtin_symbols.contains(&import)
|
||||
&& !stdlib_symbols.contains(&import)
|
||||
&& !dylink_symbols.contains(&import)
|
||||
{
|
||||
missing_symbols.push(import);
|
||||
if let wasmparser::Payload::ImportSection(reader) = payload? {
|
||||
for imports in reader {
|
||||
for import in imports? {
|
||||
let (_, import) = import?;
|
||||
let name = import.name;
|
||||
if !builtin_symbols.contains(&name)
|
||||
&& !stdlib_symbols.contains(&name)
|
||||
&& !dylink_symbols.contains(&name)
|
||||
{
|
||||
missing_symbols.push(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ description = "User configuration of tree-sitter's command line programs"
|
|||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
readme = "README.md"
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
documentation = "https://docs.rs/tree-sitter-config"
|
||||
|
|
|
|||
|
|
@ -28,14 +28,24 @@ pub enum ConfigError {
|
|||
#[derive(Debug, Error)]
|
||||
pub struct IoError {
|
||||
pub error: std::io::Error,
|
||||
pub path: Option<String>,
|
||||
pub path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl PartialEq for IoError {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.path == other.path
|
||||
&& self.error.kind() == other.error.kind()
|
||||
&& self.error.raw_os_error() == other.error.raw_os_error()
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for IoError {}
|
||||
|
||||
impl IoError {
|
||||
fn new(error: std::io::Error, path: Option<&Path>) -> Self {
|
||||
Self {
|
||||
error,
|
||||
path: path.map(|p| p.to_string_lossy().to_string()),
|
||||
path: path.map(Path::to_path_buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -44,7 +54,7 @@ impl std::fmt::Display for IoError {
|
|||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.error)?;
|
||||
if let Some(ref path) = self.path {
|
||||
write!(f, " ({path})")?;
|
||||
write!(f, " ({})", path.display())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ description = "Library for generating C source code from a tree-sitter grammar"
|
|||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
readme = "README.md"
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
documentation = "https://docs.rs/tree-sitter-generate"
|
||||
|
|
@ -20,30 +19,24 @@ path = "src/generate.rs"
|
|||
workspace = true
|
||||
|
||||
[features]
|
||||
default = ["qjs-rt"]
|
||||
load = ["dep:semver"]
|
||||
qjs-rt = ["load", "rquickjs", "pathdiff"]
|
||||
default = [ "qjs-rt" ]
|
||||
load = [ "dep:semver" ]
|
||||
qjs-rt = [ "load", "rquickjs", "pathdiff" ]
|
||||
|
||||
[dependencies]
|
||||
bitflags = "2.9.4"
|
||||
bitflags = "2.11.1"
|
||||
dunce = "1.0.5"
|
||||
hashbrown.workspace = true
|
||||
indexmap.workspace = true
|
||||
indoc.workspace = true
|
||||
log.workspace = true
|
||||
pathdiff = { version = "0.2.3", optional = true }
|
||||
pathdiff = { optional = true, version = "0.2.3" }
|
||||
regex.workspace = true
|
||||
regex-syntax.workspace = true
|
||||
rquickjs = { version = "0.10.0", optional = true, features = [
|
||||
"bindgen",
|
||||
"loader",
|
||||
"macro",
|
||||
"phf",
|
||||
] }
|
||||
rquickjs = { features = [ "bindgen", "loader", "macro", "phf" ], optional = true, version = "0.13" }
|
||||
rustc-hash.workspace = true
|
||||
semver = { workspace = true, optional = true }
|
||||
semver = { optional = true, workspace = true }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
smallbitvec.workspace = true
|
||||
thiserror.workspace = true
|
||||
topological-sort.workspace = true
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue