Compare commits

..

151 commits

Author SHA1 Message Date
Christian Clason d97971e245 release v0.26.13 2026-08-23 10:48:19 +02:00
Will Lillis d2436dc57c fix(rust): remove must_use attribute from Tree::changed_ranges
`ExactSizeIterator` implements `std::iter::Iterator`, which is already
`must_use`.

(cherry picked from commit 54a46eb49b)
2026-08-23 10:05:49 +02:00
Christian Clason e323709b8d build(deps): bump wasmtime-c-api to v36.0.14 2026-08-21 10:11:35 +02:00
Will Lillis c7a65b67d1 fix(ci): run all workspace tests
Without `--workspace`, all generate tests were silently skipped.
2026-08-20 02:36:20 -04:00
Tim Vermeulen 9d1f31602c fix(parser): nest error children during recovery
(cherry picked from commit f30ee2b300)
2026-08-14 09:47:37 +02:00
Tim Vermeulen f837fc9813 fix(lib): accumulate error costs through hidden error nodes
(cherry picked from commit 869638f6cf)
2026-08-14 09:47:37 +02:00
Will Lillis 15adfa9e51 fix(generate): fold case-insensitive patterns at the AST leaves
A previous fix moved case folding for `/i` patterns out of `regex_syntax` and
into `expand_regex`, so folding could drop the two non-ASCII code points
Unicode simple folding maps onto ASCII letters: the long s `ſ` (U+017F)
onto `s`, and the Kelvin sign `K` (U+212A) onto `k`. Left in, they leak
into otherwise-ASCII tokens and stop those tokens from being extracted as
keywords.

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

`regex_syntax` folds each leaf of a class expression before applying that
leaf's negation and the set algebra above it. Keep that order and change
only the fold: walk the AST, replace each leaf with its fold, and translate
with `case_insensitive(false)`.
2026-08-14 01:16:45 -04:00
Will Lillis 063f8f886b fix(query): correctly set a state's skipped_quantifier flag when a
zero quantifier skip is performed.

The zero-skip branch currently sets skipped_quantifier unconditionally.
That flag is correct only when the quantified step and its skip target
are _siblings_ at the same query depth. Otherwise, setting it allows for
a "leak" and disables unrelated, "outer" anchors.

(cherry picked from commit 42f33fe2f8)
2026-08-11 00:26:25 -05:00
Will Lillis 6320165515 fix(query): correctly identify MISSING nodes in queries
(cherry picked from commit 5fae914f8f)
2026-08-09 23:07:51 +02:00
Christian Clason 808e4b1fc0 release v0.26.12 2026-08-08 15:04:48 +02:00
Newosko 7566ffacb7 fix(lib): continue search for later named siblings in
`ts_tree_cursor_current_status`

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

Also remove some dead branches inside `ts_tree_cursor_current_status`.

Co-authored-by: Will Lillis <will.lillis24@gmail.com>
(cherry picked from commit 308aee0c90)
2026-08-08 00:20:18 +02:00
Sjoerd Langkemper 4b7e2c956c fix(generate): honor right associativity despite a lower-precedence shift
A SHIFT/REDUCE conflict can bundle several shift interpretations with
different precedences against a single reduce. `handle_conflict` weighed them
with only `shift_is_less` and `shift_is_more`, so a lone lower-precedence
shift set `shift_is_less` and the REDUCE won outright, even when another
interpretation tied the REDUCE in precedence and the REDUCE was declared
right-associative. That tie's associativity should have won by shifting, so a
right-associative rule silently became left-associative as soon as an
unrelated lower-precedence rule was added to the grammar.

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

Co-authored-by: Will Lillis <will.lillis24@gmail.com>
(cherry picked from commit 0900f84eab)
2026-07-27 08:30:48 +02:00
Will Lillis 3ee7c639de fix(parser): restart recovery for invalid tokens in the error state
The early `ts_parser__recover` dispatch for `ERROR_STATE` in
`ts_parser__advance` was dropped in `201b41cf1`. Without it, tokens
with no valid action re-enter `ts_parser__handle_error` per token
and fragment the tree instead of extending one ERROR node. Restore
it to recover the old (pre-0.25) error recovery behavior.

(cherry picked from commit 15ea3328e1)
2026-07-25 18:11:25 +02:00
Alex Le Blanc fd0ccd65a5 fix(highlight): use std::sync::OnceCell for various loader fields
`std::unsync::OnceCell` allows for data races and requires an (incorrect) manual impl of the `Sync` trait.
2026-07-23 20:14:53 -04:00
Will Lillis 8df22e6f0e fix(init): don't lowercase repository url
(cherry picked from commit 78481a8dfc)
2026-07-18 22:58:47 +02:00
Will Lillis c02f32485a fix(cli): init --update should not update setup.py after replacing it (#5760)
Co-authored-by: Philipp <philipp.zander@tweag.io>
2026-07-17 21:32:18 -04:00
Will Lillis f9b9b39075 fix(templates): replace deprecated method in Package.swift
Co-authored-by: ObserverOfTime <chronobserver@disroot.org>
2026-07-17 21:32:18 -04:00
Will Lillis 09384230b4 fix(templates): add C source files to Python sdist
Co-authored-by: ObserverOfTime <chronobserver@disroot.org>
2026-07-17 21:32:18 -04:00
Will Lillis 5ccdbb6b84 fix(query): transfer a leading boundary anchor across a zero-matched quantifier
`(P . Q* Y)` with zero `Q` dropped the leading `.`, so `Y` matched at any
position instead of being pinned to the parent's first named child. On a `?`/`*`
zero-skip, when the skipped step is the parent's first child and carries a
leading anchor, transfer that first-child requirement to the skip target.

(cherry picked from commit 1ffd612be5)
2026-07-17 19:47:19 -04:00
Will Lillis fcb38e9dcf fix(query): keep the trailing anchor when a zero-matched quantifier is anchored
on both sides

In `A . Q* . B`, a zero-matched `Q` made both anchors vacuous, so `A` and `B`
were no longer required to be immediate siblings. Only relax the following
anchor on a `?`/`*` zero-skip when the skipped step has no leading anchor of
its own; otherwise the adjacency transfers through the empty run.

(cherry picked from commit dfcf73921c)
2026-07-17 19:47:19 -04:00
Will Lillis b6243a5234 fix(generate): fold case-insensitive patterns ourselves
Unicode simple case folding maps two non-ASCII code points onto ASCII
letters: the long s `ſ` (U+017F) onto `s`, and the Kelvin sign `K` (U+212A)
onto `k`. So `regex_syntax` pulls them into any case-insensitive pattern,
which is virtually never intended and has two bad effects:

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

Rather than let `regex_syntax` fold, parse patterns unfolded and fold them
ourselves in `case_fold_ascii_safe`: fold via `regex_syntax`, then drop
`ſ`/`K` only when folding introduced them (they were not already in the
base set). A class that already contains them keeps them.
2026-07-16 23:53:31 -04:00
Christian Clason 64402de285 release v0.26.11 2026-07-12 12:05:46 +02:00
Will Lillis 2194ac1a9c fix(ci): re-set executable permission on release artifacts before zipping 2026-07-11 10:44:46 +02:00
Will Lillis a2b8369c4a fix(generate): strip non-ASCII case folds
Regexes with the case-insensitive 'i' flag caused unicode simple case folding,
which maps two non-ASCII code points onto ASCII letters:

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

This pushed such tokens outside an ASCII `word` token, so they failed to
extract as keywords.

(cherry picked from commit 07c4ed220e)
2026-07-11 10:25:59 +02:00
Will Lillis 1ae3cc5753 fix(rust): address new clippy lints 2026-07-11 00:46:19 -04:00
Julia Hansbrough 90dd1a0cfb fix(templates): generated array macros do not compile with C++
Problem: A set of `array_*` macros (`array_push`, `array_extend`, etc) implicitly convert a `void*` into a different pointer type.  In environments that compile these headers as C++, this implicit conversion is an error.

Solution: This commit adds an `_array_cast` macro that uses `decltype` to cast the `void*` to the proper type when compiling as C++.
(cherry picked from commit cc7be1fd47)
2026-07-08 17:34:45 -04:00
Will Lillis fa6222a0d3 docs(queries): clarify behavior of first child anchor example
(cherry picked from commit b53a0fe622)
2026-07-01 20:04:13 -04:00
Will Lillis 77b741bb83 docs(queries): specify anchor behavior with quantifiers and groups
- An anchor between two patterns is vacuous when an adjacent quantifier
matches zero

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

- An anchor at the edge of a group or alternation is not allowed.

(cherry picked from commit 133d549b70)
2026-07-01 20:04:13 -04:00
Will Lillis 1c21ff33d6 fix(query): apply a trailing anchor when its optional node is skipped
A trailing `.` after an optional node, e.g. `(p (a)+ @a . (b)? @b .)`, sets
`is_last_child` on the `(b)?` step. When `(b)?` matched zero, the zero-skip
jumped past that step to completion, so the last-child requirement was never
enforced.

When a zero-skip would bypass a step carrying `is_last_child`, require that
the last matched node really is the last named child. Gated on the
`alternative_is_skip` edge marker.

(cherry picked from commit a8486ca239)
2026-07-01 20:04:13 -04:00
Will Lillis 5756ced350 fix(query): make an anchor after a zero-matched quantifier vacuous
A `.` anchor between a quantified pattern and a following node, e.g.
`(parent (comment)* @c . (decl))`, was treated as a leading anchor when the
quantifier matched zero. The zero-skip jumps a state directly onto the anchored
step, where `is_immediate` was enforced even though no sibling had matched before
it. Name that skip edge (`alternative_is_skip`) and, when a state follows it,
record that it skipped the quantifier (`skipped_quantifier`). The
immediate anchor is then supressoed for that state's next match.

(cherry picked from commit 7e7f2584d1)
2026-07-01 20:04:13 -04:00
Will Lillis eca86a7a6c fix(query): forbid an anchor at the end of a group
A `.` at the end of a group, such as `((comment)+ @c .)`, was silently dropped
during compilation. Reject rather than dropping.

(cherry picked from commit 966dc52557)
2026-07-01 20:04:13 -04:00
Will Lillis 306f868299 feat(query): enhanced step dumper
Extract the step dump into `ts_query__dump_steps` and enrich it with the control-flow
fields and analysis annotations.

(cherry picked from commit 6bfbd9d84d)
2026-07-01 20:04:13 -04:00
Will Lillis e7d6651336 fix(query): short-circuit longest-match dedup for disjoint states
An unanchored quantified sibling like `(program (comment)+ @doc (class))`
keeps one match state per matching sibling. A recent fix stopped over-pruning
them, but the per-node longest-match dedup is pairwise, so with n live states
matching became O(n^3).

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

Refs neovim/neovim#40517

(cherry picked from commit 91fd04f96a)
2026-07-01 19:01:52 -04:00
Will Lillis 074b6eb05b fix(xtask): eliminate silent errors in zig fetch 2026-06-30 20:06:29 -04:00
Christian Clason 4ac1514d94 fix(deps): zig manifest for wasmtime is missing windows hashes
Problem: `zig fetch` on 0.16 can't download zip archives, which wasmtime
uses for Windows. This makes `cargo xtask upgrade-wastime` fail silently
with empty hashes for these platforms.

Solution: Re-run xtask with Zig 0.17 nightly.
(cherry picked from commit ae2081989e)
2026-06-29 23:30:59 +02:00
Will Lillis 3fc4cd21bc release v0.26.10 2026-06-28 11:47:27 -04:00
Will Lillis 568aee0b7e test(query): regression test for anchored siblings inside a parent
(cherry picked from commit 41d3e16eac)
2026-06-28 14:09:05 +02:00
Lucas M. de Jong Larrarte 2efce10fe9 fix(query): skip passthrough steps when checking for fallible step splitting
Problem: In queries matching a parent node with anchored children
(siblings) where the first sibling has a quantifier and is not
captured, the check for fallible steps skips splitting the state because
the next node is a passthrough node (and not an is_immediate one). This
prevents the query from matching beyond the first occurrence.

Solution: In the check for fallible steps, skip the next steps if they
are passthrough steps.

(cherry picked from commit 99bceec689)
2026-06-28 00:10:08 +02:00
Lucas M. de Jong Larrarte e8b9639291 fix(query): avoid overriding states not seeking immediate match with states seeking immediate match
Problem: In queries matching a parent node with anchored children
(siblings) where the first anchored sibling has a quantifier, the
deduplication logic for states is overly aggressive. The logic causes
the state split on the first capture (with the parent) to be dropped in
favor of the loopback state. This results in the query not being able to
match beyond the first occurrence.

Solution: Prevent the deduplication logic from dropping a state that is
not seeking an immediate match for one that is seeking an immediate
match, since the one not seeking for an immediate match could still
match later nodes.

(cherry picked from commit 8b43f42dca)
2026-06-28 00:10:08 +02:00
Lucas M. de Jong Larrarte 576564d25a fix(query): take anchors between siblings into account for fallible step splitting
Problem: In queries matching a parent node with anchored children
(siblings), the check for fallible steps only considers nodes with
children, which results in no state split being made for anchored
siblings (node1) . (node2). This prevents the query from matching beyond
the first occurrence.

Solution: Make the check for fallible steps consider also the case where
the next step is at the same depth and must be matched immediately
after.

(cherry picked from commit 1b110f62dd)
2026-06-28 00:10:08 +02:00
Will Lillis fbf3049dc0 fix(dsl): forbid invalid field names 2026-06-27 00:22:57 -04:00
Christian Clason 99bc36b857 build(deps): bump wasmtime-c-api to v36.0.12 2026-06-26 14:25:24 +02:00
Will Lillis 1ef1048d05 fix(build): use std helper rather than passing -std=c11 flag
MSVC expects `/std:c11`
2026-06-17 00:29:06 -04:00
Will Lillis a61be4ac27 fix(lib): address strict aliasing violations in TreeCursor type
Omit the static assert from master to avoid a breaking change on the
release branch.
2026-06-17 00:29:06 -04:00
jannschu be8f380ad4 fix(cli): highlight test did not properly check for spans on later rows
The early exit condition to skip remaining highlights was incorrectly
checking the lexicographical order, which could lead to false
passes if a highlight was on a later row with smaller column number.
2026-06-08 18:09:21 -04:00
jannschu d4e89a4881 fix(cli): infinite loop in highlight test 2026-06-08 18:09:21 -04:00
Will Lillis e502c6c18e fix: add DESCRIPTION to grammar Makefile template 2026-06-07 18:03:19 -04:00
Will Lillis 2fddf5a1b4 fix(cli): display warning to user if parser test corpus dir isn't found
(cherry picked from commit d9acc99734)
2026-06-06 00:18:10 +02:00
Will Lillis 70068dd487 fix(lib): Consider subtree lookahead bytes when determining whether the
parser can reuse a node.

Lookahead bytes can be used to decide what a node is parsed as, so it's
resaonable to consider this as part of a node's "range" when deciding
which edits affect it.

(cherry picked from commit 15ddfb21ed)
2026-06-01 19:12:50 -04:00
Will Lillis 323d99ede3 fix(cli): improve soundness of cst range calculation 2026-05-31 23:05:37 +02:00
Georges Savoundararadj 3ab78c3c5b fix(lib): use correct TREE_SITTER_HIDE_SYMBOLS macro name in alloc.h
The alloc.h header checked for TREE_SITTER_HIDDEN_SYMBOLS, but the
canonical macro name used everywhere else (api.h, render.rs, setup.py)
is TREE_SITTER_HIDE_SYMBOLS. This mismatch meant that defining
TREE_SITTER_HIDE_SYMBOLS (as the Python binding build does) would not
actually hide the allocator symbols in alloc.h.

Fixes tree-sitter/tree-sitter#5625

Signed-off-by: Georges Savoundararadj <savoundg@amazon.com>
(cherry picked from commit 1da46327b3)
2026-05-30 23:00:00 -04:00
Will Lillis f4b1fe2ba7 fix(lib): update doc comment for ts_parser_parse 2026-05-30 19:15:38 -04:00
Christian Clason 7f534862c3 release v0.26.9 2026-05-19 19:51:53 +02:00
tree-sitter-ci-bot[bot] 77b96de523
fix(wasm): validate memory reads (#5569) (#5613)
Problem: Offsets such as parse_table, symbol_names, lex_modes, and the alias/supertype tables are used directly as indexes into the store's memory buffer. A malformed module can point one of those fields outside the current linear memory and make the host process read through an invalid pointer while loading the language.

Solution: Add a small checked-memory wrapper for Wasm language loading and routes descriptor reads, table copies, string reads, and the alias-map scan through it. Invalid descriptor addresses now fail loading with TSWasmErrorKindInstantiate.

(cherry picked from commit 21cfae7b56)

Co-authored-by: 𝙽!𝙻 <z_hakmi@estin.dz>
2026-05-19 19:40:15 +02:00
Christian Clason a082228e43 build(deps): bump wasmtime-c-api to v36.0.9 2026-05-19 19:07:01 +02:00
tree-sitter-ci-bot[bot] 7aea01521d
fix(wasm): load supertype tables for ABI 15 grammars (#5605) (#5606)
The wasm store gated supertype_symbols / supertype_map_slices /
supertype_map_entries copies on abi_version > LANGUAGE_VERSION_WITH_RESERVED_WORDS,
but every other consumer (language.c, query.c) treats those tables as
present when abi_version >= LANGUAGE_VERSION_WITH_RESERVED_WORDS.

A Wasm grammar built at ABI exactly 15 with supertype_count > 0 ends up
with supertype_count copied into the native TSLanguage but supertype_map_slices
left NULL. ts_query__analyze_patterns then calls ts_language_subtypes,
which dereferences self->supertype_map_slices[supertype] and crashes.

(cherry picked from commit a53c3b03a0)

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
2026-05-18 13:58:51 -07:00
Will Lillis 2cad8b8d47 fix(generate): consider reserved words when removing unused rules 2026-05-11 04:00:40 -04:00
Will Lillis ddbe46956f fix(generate): rewrite parse_grammar with forward DFS
The previous implementation of `InputGrammar::normalize` (inlined  in
`parse_grammar` iterated over every variable, checking whether  it was
reachable _backwards_ from teh root by recursing over rules that
referenced it.

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

Instead, seed a `used` set with the start rule, the word token, and an
names referenced from `extras`/`externals`. Then propagate via direct rule
references. This yields anywhere from a 2-~2200x speedup for
`parse_grammar`. This greatly speeds up `--no-parser` runs, but is
relatively unimportant for `parser.c` generation for _existing_
grammars. The important piece is eliminating the potential cliff.
2026-05-11 04:00:40 -04:00
Will Lillis 17f9796925 fix(generate): improve error message for nonterminals used in immediate token rule
(cherry picked from commit a376ad491f)
2026-05-10 11:39:25 -04:00
Will Lillis 17e4bf92c0 fix(cli): account for process versions > 5 in the parse command's pretty
debug output.

The initial implementation of `--debug pretty` assumed process version
was bounded by `MAX_VERSION_COUNT`. However, we also have to account for
`MAX_VERSION_COUNT_OVERFLOW` as well as `halted_version_count`.

(cherry picked from commit 5cac4316db)
2026-05-05 19:50:06 -04:00
Christian Clason ec120d06f2 build(deps): bump wasmtime-c-api to v36.0.8
(cherry picked from commit 85f985a778)
2026-05-05 08:38:44 +02:00
Will Lillis 7c3d842519 docs: note zero point unbounded behavior in query functions 2026-04-30 19:19:03 -04:00
Amaan Qureshi b7964b9b19 query: fix finished state heap invariants
(cherry picked from commit 43dc8eadbe)
2026-04-27 03:37:38 -04:00
Will Lillis b0ddae770e perf(query): use min-heap for finished_states in next_capture
`ts_query_cursor_next_capture` linearly scanned all finished states to
find the one with the earliest next capture byte offset. With deeply
nested code, this O(n) scan per capture caused the highlight crate to
hang for minutes on large files.

Replace the linear scan with a min-heap over the finished_states array,
keyed by (next_capture_byte_offset, pattern_index, id). The heap is
maintained lazily: ts_query_cursor__advance uses plain array_push
(preserving FIFO insertion order), and next_capture sifts new elements
into the heap on entry via a tracked heap_size boundary. This preserves
the documented "order found" guarantee for next_match while giving
next_capture O(log n) per call.

(cherry picked from commit 123fb1c13c)
2026-04-27 03:37:38 -04:00
Will Lillis 0b9a7f87ee fix(query): widen capture list pool from uint16_t to uint32_t
Commit 1f6eac55 ("query: Use uint32_t for capture list IDs") widened
QueryState.capture_list_id to uint32_t and removed the 65536 pool cap,
but left the pool function signatures as uint16_t. This caused silent
truncation when the pool exceeded 65535 entries, leading to a segfault.

(cherry picked from commit 361f293a73)
2026-04-27 03:37:38 -04:00
Daniel Jalkut e4ac513afd lexer: pass code-unit length to U16_NEXT in UTF-16 decoders
`ts_decode_utf16_le` and `ts_decode_utf16_be` passed a byte length to
`U16_NEXT_LE` and `U16_NEXT_BE`, but those macros count `uint16_t` code
units. If a lead surrogate was the last code unit in a chunk, the decoder
could peek past the chunk and combine it with adjacent memory.

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

Co-authored-by: Will Lillis <will.lillis24@gmail.com>
Co-authored-by: Amaan Qureshi <git@amaanq.com>
(cherry picked from commit 0f6780b9a3)
2026-04-26 03:40:09 -04:00
Antonin Delpeuch 2b00a9b7fc feat(dist): enable install via cargo binstall (#5533) 2026-04-24 09:36:10 +02:00
Will Lillis c64e7f0f7b fix(generate): pass default optimization level in
`generate_parser_for_grammar`

Passing `empty` here causes some grammars (i.e. tree-sitter-cpp) to fail
to generate.

(cherry picked from commit 457eb295b7)
2026-04-24 09:19:20 +02:00
Will Lillis 7952172109 fix(ci): include fixture lockfile in cache hash
(cherry picked from commit 15154504de)
2026-04-23 03:50:04 -04:00
Will Lillis 8185030b48 fix(rust): fix new clippy lints 2026-04-23 02:19:20 -04:00
Volker Mische 8850e11bd8 fix(loader): allow filenames with dots (#5529)
(cherry picked from commit 4cb11acd46)
2026-04-23 01:29:54 -04:00
Will Lillis a4bdd941d5 build(deps): bump wasmtime-c-api to v36.0.7
Co-authored-by: Christian Clason <c.clason@uni-graz.at>
2026-04-10 09:46:43 +02:00
Christian Clason 89f553c2e9 ci(actions): bump actions/cache to v5
Apparently dependabot doesn't cover the actions/cache/action.yml.

(cherry picked from commit 64698af1ac)
2026-04-01 01:54:16 -04:00
Christian Clason cd5b087cd9 release v0.26.8 2026-03-31 19:31:10 +02:00
Franklin Chen c0d1444118 generate: avoid panicking when a supertype only has hidden external token children
This commit skips adding entries to the subtype map when the subtypes
list is empty to avoid a lookup failure in the topological sort during
node type generation.

Co-authored-by: Amaan Qureshi <git@amaanq.com>
(cherry picked from commit 7c2e757c03)
2026-03-31 02:51:35 -04:00
Max Brunsfeld 0b04fd0533 Fix wasm loading of languages w/ multiple reserved word sets (#5475)
(cherry picked from commit d3ff0ce81d)
2026-03-31 02:01:50 -04:00
Will Lillis 05cf9a161a perf(cli): minor allocation and write call reductions
- Cache tree byte range length to avoid redundant FFI calls per test
- Combine 5 separate XML attribute write! calls into one
- Pre-allocate format_sexp output buffer to avoid repeated growth

(cherry picked from commit 791d7cead4)
2026-03-31 02:01:32 -04:00
Will Lillis bab48517d7 perf(cli): buffer stdout in parse and query output
Wrap stdout in a 64KB BufWriter when writing `parse` output. The tree
walking loop makes many small write calls (parentheses, indentation,
node kinds, ranges, etc.) which are expensive without buffering.

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

Also do the same for the `query` command's output.

(cherry picked from commit 827bcdabd9)
2026-03-31 02:01:32 -04:00
Will Lillis e28cb5ae74 fix(cli): correct typo in parse command's help text 2026-03-28 05:23:41 -04:00
Will Lillis 3839f6fcf5 fix(lib): document invariants that must be upheld for TSInputEdit 2026-03-23 00:16:34 -04:00
Will Lillis 001a926d56 fix(generate): allow disabling qjs-rt feature from CLI
The workspace dependency for `tree-sitter-generate` did not set
`default-features = false`, so Cargo always enabled its default
features (including `qjs-rt` and thus `rquickjs`) regardless of
the CLI's `--no-default-features` flag.

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

- Set `default-features = false` on the workspace `tree-sitter-generate`
  dependency so the CLI's feature forwarding actually takes effect.
- Explicitly enable the `load` feature in the CLI's dependency on
  `tree-sitter-generate`, since the CLI needs `load`-gated functions
  unconditionally.
- Gate necessary imports behind `#[cfg(feature = "load")]` to fix
  `tree-sitter-generate`'s build without the `load` feature.
2026-03-21 18:36:13 -04:00
Christian Clason 6f2e8a6cf4 release v0.26.7 2026-03-14 17:23:21 +01:00
Christian Clason 0ae615883e ci(release): publish zip archives 2026-03-13 22:53:24 +01:00
Will Lillis 9ce156713c docs: indicate that dashes are not permitted in parser names
(cherry picked from commit 30d9c675a4)
2026-03-13 20:45:45 +01:00
Will Lillis 365b1f0f91 Revert "feat: allow - in grammar names"
This reverts commit 7d3c321253.

(cherry picked from commit 30ed8da439)
2026-03-13 20:45:45 +01:00
Riley Bruins 8e87144b61 fix(query): don't add copies for quantifier steps outside alternations
(cherry picked from commit cf302b07d1)
2026-03-04 11:01:39 +01:00
Riley Bruins b61eabb4d2 refactor(query): remove alternative_is_immediate
This is implied by `is_pass_through`.

(cherry picked from commit a95fff5477)
2026-03-04 02:52:27 -05:00
Laurent Cheylus c802b44dff fix(loader): link with libc on OpenBSD to compile parser
Fix tree-sitter/tree-sitter#5333

Signed-off-by: Laurent Cheylus <foxy@free.fr>
(cherry picked from commit 2f747dc9b1)
2026-03-01 20:17:44 -05:00
Riley Bruins 16c7bfb48f chore(parser): return NULL, not false, for incomplete parse
Small nit; `NULL` is returned everywhere else in this function for an
incomplete parse.

(cherry picked from commit 4ae90615d1)
2026-03-01 05:13:28 -05:00
Marian Buschsieweke d01bd9b1e5 fix(wasm): pass target triple to clang (#5385)
Problem: The `clang` binary contained in the WASI-SDK releases downloaded from Github does not work on all platforms (e.g., Alpine/MUSL), but a custom (LLVM) `clang` built for the platform will default to that target, making it impossible to build wasm parsers.

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

Notes:
* This assumes the custom `clang` is (installed or linked) to `$TREE_SITTER_WASI_SDK_PATH/bin`.
* This requires a full LLVM clang; Apple clang does not support `wasm` targets.
* Tree-sitter expects a specified version of WASI-SDK, including a specific `clang` version. Other versions may but are not guaranteed to work.
2026-02-28 11:28:19 +01:00
MFS-code 594f9d5580 fix: skip missing Makefile in version command
(cherry picked from commit 146ea6e12b)
2026-02-27 10:09:28 +01:00
Christian Clason 534c4a074c 0.26.6 2026-02-25 17:28:48 +01:00
Christian Clason 0de6ea6edd build(deps): bump wasmtime to v36.0.6
(cherry picked from commit 4d7d35818b)
2026-02-24 22:55:55 +01:00
Will Lillis 6fdf7fdbc6 fix(cli): correct condition to perform __init__.py replacement for
`tree-sitter init -u` command
2026-02-24 02:00:45 -05:00
Will Lillis a6aabeb941 fix(cli): actually write updated Package.swift file 2026-02-24 02:00:45 -05:00
Christian Clason b1e10d5410 ci: retrigger crates check on PR updates
(cherry picked from commit e62bf4ee5f)
2026-02-22 15:30:53 +01:00
Will Lillis b3e86f75cc ci: add a dry-run workflow to test rust releases 2026-02-21 10:10:57 -05:00
lucasew a226c68720 fix(lib): cast NULL in ts_subtree_children macro
Explicitly casting NULL to (Subtree *) in the ternary expression ensures
type consistency. This resolves ambiguous type inference issues encountered
by strict static analysis tools and C-to-Go transpilers (like ccgo).

Signed-off-by: lucasew <lucas59356@gmail.com>
(cherry picked from commit 4e9fededff)
2026-02-21 04:34:07 -05:00
Christian Clason d03bb288e2 build(deps): bump wasmtime to v36.0.5 LTS
Wasmtime v34 introduced breaking ABI changes to `wasmtime_func_t` and
`wasmtime_table_t` structures. The `__private` field changed from
`size_t` to `void*`, requiring code updates to store complete
`wasmtime_func_t` structures instead of raw indices.

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

Co-Authored-By: nzinfo <li.monan@gmail.com>
(cherry picked from commit a21ee02710)
2026-02-20 10:52:27 +01:00
Sergey Reshetnikov ed06ca058b fix(lib): add RedoxOS support to portable/endian.h
(cherry picked from commit 5e23ccaac2)
2026-02-18 16:51:18 +01:00
Amaan Qureshi fc5f1b4526 ci: cache npm packages for the web bindings 2026-02-10 03:42:43 -05:00
Amaan Qureshi 054f892546 make: drop redundant cargo check 2026-02-10 03:42:43 -05:00
Amaan Qureshi 4bcc064ecc ci: skip release build on PRs 2026-02-10 03:42:43 -05:00
Amaan Qureshi 93a5767892 ci: don't update msys2 packages 2026-02-10 03:42:43 -05:00
Amaan Qureshi a5129d5bc0 ci: replace setup-emsdk action with manual install + actions/cache 2026-02-10 03:42:43 -05:00
Amaan Qureshi 9a22dff7e5 ci: download pre-built wasmtime C API instead of compiling from source
ci: cache npm
2026-02-10 03:42:43 -05:00
Amaan Qureshi fdc20d9c58 ci: only run wasm tests on linux x64 and mac arm64 2026-02-10 03:42:43 -05:00
Will Lillis 89e804b7e4 fix(query): prevent cross-branch capture contamination in alternations with quantifiers
When a branch inside an alternation has a + or * quantifier, the
quantifier's pass_through step loops back to the branch's first step.
The alternation linking also sets that step's alternative_index to
point to the next branch. This causes the quantifier loop-back to
incorrectly explore other branches in the case of a failed match that
follows a successful match, contaminating captures.

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

Co-authored-by: Riley Bruins <ribru17@hotmail.com>
(cherry picked from commit 596a4d69bb)
2026-02-09 22:55:40 -05:00
Chad McElligott 3b85287d6d web: add default export to CJS bundle
(cherry picked from commit 081c90b769)
2026-02-09 22:55:03 -05:00
Will Lillis 79b60a271c fix(cli): allow for both debug logs and graphs
(cherry picked from commit 73b2f981d5)
2026-02-09 22:26:08 -05:00
Jeremy Fleischman ad7a629a1b feat: allow - in grammar names
A number of grammars in the wild have this character in their name:

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

I read through https://github.com/tree-sitter/tree-sitter/pull/3700 and
https://github.com/tree-sitter/tree-sitter/issues/3637, and it doesn't
look like there was much discussion about this name regex, so hopefully
this is an acceptable change?

(cherry picked from commit 7d3c321253)
2026-02-09 21:48:46 -05:00
Amaan Qureshi 325bc50d6f lib: clean up strict aliasing fixes in array.h
(cherry picked from commit 22cda59a19)
2026-02-09 21:11:32 -05:00
Amaan Qureshi 932bde72b2 nix: fix clangd not working
(cherry picked from commit a234575b04)
2026-02-09 21:11:32 -05:00
Will Lillis 470813116b 0.26.5 2026-02-01 20:27:27 +01:00
Will Lillis 7ec1794d6b 0.26.4 2026-02-01 19:59:04 +01:00
Will Lillis fa8811f7f7 docs: include info on environment variables for fuzz command
(cherry picked from commit 7100767f64)
2026-02-01 19:07:59 +01:00
Will Lillis ef4999bf61 fix(cli): include default values for --edits and --iterations in
`fuzz` command help

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

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

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

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

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

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

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

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

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

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

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

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

(cherry picked from commit f4ca3d95ca)

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

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

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

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

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

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

This happens on concurrent builds of the the same grammar.

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

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

Solution:
Print whitespace only if node is not multiline.

(cherry picked from commit 4ac2d5d276)
2025-12-14 22:41:02 -05:00
313 changed files with 14095 additions and 24910 deletions

2
.dockerignore Normal file
View file

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

5
.envrc
View file

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

View file

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

View file

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

View file

@ -21,9 +21,6 @@ runs:
'lib/src/parser.h',
'lib/src/array.h',
'lib/src/alloc.h',
'lib/src/wasm-stdlib/external_scanner_stdlib.h',
'crates/loader/wasi-sdk-version',
'crates/loader/binaryen-version',
'test/fixtures/grammars/*/**/src/*.c',
'test/fixtures/fixtures.json',
'.github/actions/cache/action.yml') }}

20
.github/cliff.toml vendored
View file

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

View file

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

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

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

View file

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

View file

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

View file

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

View file

@ -26,7 +26,6 @@ jobs:
- windows-x86
- macos-arm64
- macos-x64
- illumos-x64
- wasm32
include:
@ -46,9 +45,6 @@ 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 }
@ -73,7 +69,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v7.0.1
uses: actions/checkout@v6
- name: Set up cross-compilation
if: matrix.cross
@ -95,7 +91,7 @@ jobs:
- name: Cache Emscripten SDK
if: contains(matrix.features, 'wasm') && (matrix.run-wasm-test || !inputs.run-test)
uses: actions/cache@v6
uses: actions/cache@v4
with:
path: emsdk
key: emsdk-${{ env.EMSCRIPTEN_VERSION }}-${{ runner.os }}-${{ runner.arch }}
@ -116,22 +112,17 @@ jobs:
- name: Set up Node.js
if: contains(matrix.features, 'wasm') && (matrix.run-wasm-test || !inputs.run-test)
uses: actions/setup-node@v7.0.0
uses: actions/setup-node@v4
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: |
@ -201,7 +192,7 @@ jobs:
WASMTIME_REPO: https://github.com/bytecodealliance/wasmtime
- name: Build C library (make)
if: runner.os != 'Windows' && !matrix.vm
if: runner.os != 'Windows'
run: |
if [[ $PLATFORM == linux-arm ]]; then
CC=arm-linux-gnueabihf-gcc; AR=arm-linux-gnueabihf-ar
@ -218,7 +209,7 @@ jobs:
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 && !matrix.vm"
if: "!matrix.cross"
run: |
cmake -S . -B build/static \
-DBUILD_SHARED_LIBS=OFF \
@ -237,20 +228,6 @@ 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
@ -269,7 +246,7 @@ jobs:
run: cargo check --no-default-features --target='${{ matrix.target }}'
- name: Build target
if: "!inputs.run-test && !matrix.vm"
if: "!inputs.run-test"
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' || '' }}
@ -299,13 +276,9 @@ jobs:
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@v7
uses: actions/upload-artifact@v6
with:
name: tree-sitter.${{ matrix.platform }}
path: target/${{ matrix.target }}/release/tree-sitter${{ contains(matrix.target, 'windows') && '.exe' || '' }}
@ -314,7 +287,7 @@ jobs:
- name: Upload Wasm artifacts
if: "!inputs.run-test && matrix.platform == 'linux-x64'"
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v6
with:
name: tree-sitter.wasm
path: |

View file

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

View file

@ -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@v7.0.1
uses: actions/checkout@v6
- name: Set up Rust
uses: actions-rust-lang/setup-rust-toolchain@v1

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,19 +0,0 @@
name: Check Wasm Stdlib build
on:
workflow_call:
jobs:
check:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v7.0.1
- name: Check directory changes
uses: actions/github-script@v9
with:
script: |
const scriptPath = `${process.env.GITHUB_WORKSPACE}/.github/scripts/wasm_stdlib.js`;
const script = require(scriptPath);
return script({ github, context, core });

View file

@ -1,19 +0,0 @@
[formatting]
column_width = 100
compact_arrays = false
reorder_inline_tables = true
reorder_keys = true
[[rule]]
include = [ "**/Cargo.toml" ]
keys = [ "package" ]
[rule.formatting]
reorder_keys = false
[[rule]]
include = [ "**/Cargo.toml" ]
keys = [ "profile" ]
[rule.formatting]
reorder_keys = false

11
.zed/settings.json Normal file
View file

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

View file

@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.13)
project(tree-sitter
VERSION "0.28.0"
VERSION "0.26.13"
DESCRIPTION "An incremental parsing system for programming tools"
HOMEPAGE_URL "https://tree-sitter.github.io/tree-sitter/"
LANGUAGES C)

BIN
Cargo.lock generated

Binary file not shown.

View file

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

10
Dockerfile Normal file
View file

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

View file

@ -1,4 +1,4 @@
VERSION := 0.28.0
VERSION := 0.26.13
DESCRIPTION := An incremental parsing system for programming tools
HOMEPAGE_URL := https://tree-sitter.github.io/tree-sitter/
@ -129,13 +129,8 @@ lint-web:
npm --prefix lib/binding_web ci
npm --prefix lib/binding_web run lint
lint-toml:
taplo check
taplo format --check --diff
format:
cargo fmt --all
taplo format
changelog:
@git-cliff --config .github/cliff.toml --prepend CHANGELOG.md --latest --github-token $(shell gh auth token)

35
Package.swift Normal file
View file

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

View file

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

View file

@ -1,8 +1,8 @@
.{
.name = .tree_sitter,
.fingerprint = 0x841224b447ac0d4f,
.version = "0.28.0",
.minimum_zig_version = "0.16.0",
.version = "0.26.13",
.minimum_zig_version = "0.14.1",
.paths = .{
"build.zig",
"build.zig.zon",
@ -13,83 +13,83 @@
},
.dependencies = .{
.wasmtime_c_api_aarch64_android = .{
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-aarch64-android-c-api.tar.xz",
.hash = "N-V-__8AAIp_mQVzQOITXcYcWxYLJkvB1W1SvLlrdiU2G7fj",
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v36.0.14/wasmtime-v36.0.14-aarch64-android-c-api.tar.xz",
.hash = "N-V-__8AAK1DGQSFd3QeAM37CBAgGjcO6SP4PTDitKUyv2FL",
.lazy = true,
},
.wasmtime_c_api_aarch64_linux = .{
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-aarch64-linux-c-api.tar.xz",
.hash = "N-V-__8AAMztsgU5Aj4oI3MRHXJVe5rW72op-kT_78I3kZVM",
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v36.0.14/wasmtime-v36.0.14-aarch64-linux-c-api.tar.xz",
.hash = "N-V-__8AADHpGQQi5Sb9gmPvUbAP3eDfMT7SloTyWHq0bULy",
.lazy = true,
},
.wasmtime_c_api_aarch64_macos = .{
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-aarch64-macos-c-api.tar.xz",
.hash = "N-V-__8AANZxOwT27sdrKxDDGGKsiwtcZlHy204xAWNIgDBH",
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v36.0.14/wasmtime-v36.0.14-aarch64-macos-c-api.tar.xz",
.hash = "N-V-__8AAA--3gIS9QGKTh_CdihUxebSyDLRgMyaSLErr0uU",
.lazy = true,
},
.wasmtime_c_api_aarch64_musl = .{
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-aarch64-musl-c-api.tar.xz",
.hash = "N-V-__8AAJL1zQW9yxC98uc60lSuVUHhH77QHTto0zwIQnBj",
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v36.0.14/wasmtime-v36.0.14-aarch64-musl-c-api.tar.xz",
.hash = "N-V-__8AALs2GATaXyZRY1eSj0MqcnCPA-rvwQKDLzNKoO7k",
.lazy = true,
},
.wasmtime_c_api_aarch64_windows = .{
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-aarch64-windows-c-api.zip",
.hash = "N-V-__8AAHRCtQU93hJcRFOgVcof3IQpRV9stT2Pp54wpJc2",
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v36.0.14/wasmtime-v36.0.14-aarch64-windows-c-api.zip",
.hash = "N-V-__8AACG7oQSQzo4Yqb2ROtmf0Te6z25qE2D1zZVOhOjT",
.lazy = true,
},
.wasmtime_c_api_armv7_linux = .{
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-armv7-linux-c-api.tar.xz",
.hash = "N-V-__8AAJaW6gT8QdULOU0jxX4a_DOCA5YD6cxWBC8IqhQF",
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v36.0.14/wasmtime-v36.0.14-armv7-linux-c-api.tar.xz",
.hash = "N-V-__8AACfgaQOJMJ60s9tEEnIGqCc5oCc8DjV6WCCU8jfv",
.lazy = true,
},
.wasmtime_c_api_i686_linux = .{
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-i686-linux-c-api.tar.xz",
.hash = "N-V-__8AANguMgVX4XMhdOVkdj4yfFKXrG8RTgZDs3nQB8J8",
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v36.0.14/wasmtime-v36.0.14-i686-linux-c-api.tar.xz",
.hash = "N-V-__8AAA0YpwPx6WET15rgQX6MuRYgJrBFFhGJ6T1o7bXd",
.lazy = true,
},
.wasmtime_c_api_i686_windows = .{
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-i686-windows-c-api.zip",
.hash = "N-V-__8AANY9ggXg4rK2_1o3EIlrCq124l5RfykPv-DPforq",
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v36.0.14/wasmtime-v36.0.14-i686-windows-c-api.zip",
.hash = "N-V-__8AAEuShgR9nTTtALm1vg7ydAa-XHeFJoCeS6EwWMYO",
.lazy = true,
},
.wasmtime_c_api_riscv64gc_linux = .{
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-riscv64gc-linux-c-api.tar.xz",
.hash = "N-V-__8AAPDtCAdQ0dD9Rs-qWl-kPr2c7L3PVsUVjwy12Iz1",
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v36.0.14/wasmtime-v36.0.14-riscv64gc-linux-c-api.tar.xz",
.hash = "N-V-__8AALE0UwVQ2O0JUsq-bvNiae9w9rF-DcoZReRwc3VO",
.lazy = true,
},
.wasmtime_c_api_s390x_linux = .{
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-s390x-linux-c-api.tar.xz",
.hash = "N-V-__8AANA3BwY1ZOoGCWCR_tTY9G1vfIX128RGVxufN3ov",
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v36.0.14/wasmtime-v36.0.14-s390x-linux-c-api.tar.xz",
.hash = "N-V-__8AADt6VAT4U8GJtiOuTxT_d--hYS7I8sOYeMDhsmYm",
.lazy = true,
},
.wasmtime_c_api_x86_64_android = .{
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-x86_64-android-c-api.tar.xz",
.hash = "N-V-__8AAF4AIgY0ltjevj1ybGfvMU1ErPRnNve5X1TmvCru",
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v36.0.14/wasmtime-v36.0.14-x86_64-android-c-api.tar.xz",
.hash = "N-V-__8AAOOKmQR9Gt_MNxWAdciCw9I0GLtvDhlcgC3dd8DU",
.lazy = true,
},
.wasmtime_c_api_x86_64_linux = .{
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-x86_64-linux-c-api.tar.xz",
.hash = "N-V-__8AAIR0cAbjf3DkrTbu81Oq_zociz-0lCpb5DR0lIC9",
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v36.0.14/wasmtime-v36.0.14-x86_64-linux-c-api.tar.xz",
.hash = "N-V-__8AAB_LmASYgb6KhwBhZV3_1VCPN_qIBCiLONea2VGO",
.lazy = true,
},
.wasmtime_c_api_x86_64_macos = .{
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-x86_64-macos-c-api.tar.xz",
.hash = "N-V-__8AAFJ4lgRgCBnYdz8-Yfc4hLve45Hv-0RICAEuCp4s",
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v36.0.14/wasmtime-v36.0.14-x86_64-macos-c-api.tar.xz",
.hash = "N-V-__8AAG9NVQNTKddBttoabwkDR_z9SjWpxC60kIzHiLZ5",
.lazy = true,
},
.wasmtime_c_api_x86_64_mingw = .{
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-x86_64-mingw-c-api.zip",
.hash = "N-V-__8AAMxZxQZUpp1cU8J5zgLiMNq4e4dy0hcchlQFy03J",
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v36.0.14/wasmtime-v36.0.14-x86_64-mingw-c-api.zip",
.hash = "N-V-__8AABqH-QT-RZQZvYQr6KKwTgDYN36-F3zEld6zltCe",
.lazy = true,
},
.wasmtime_c_api_x86_64_musl = .{
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-x86_64-musl-c-api.tar.xz",
.hash = "N-V-__8AAN5pWgZrZBt8VYWkN82WjyFe3DkGcE7uLn3jpt38",
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v36.0.14/wasmtime-v36.0.14-x86_64-musl-c-api.tar.xz",
.hash = "N-V-__8AAHE1nATox7TV8A9MUpHA3T7VMRW__JIGXvwtBBnd",
.lazy = true,
},
.wasmtime_c_api_x86_64_windows = .{
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v48.0.1/wasmtime-v48.0.1-x86_64-windows-c-api.zip",
.hash = "N-V-__8AAEIJkgaVHFETgakbognNUpFELuV17vpjw6NjsWhQ",
.url = "https://github.com/bytecodealliance/wasmtime/releases/download/v36.0.14/wasmtime-v36.0.14-x86_64-windows-c-api.zip",
.hash = "N-V-__8AAN9FXgVEOFW9RhCY3rKCYgg-HT7O8eBuFNOPI2Av",
.lazy = true,
},
},

View file

@ -5,13 +5,14 @@ description = "CLI tool for developing, testing, and using Tree-sitter parsers"
authors.workspace = true
edition.workspace = true
rust-version.workspace = true
readme = "README.md"
homepage.workspace = true
repository.workspace = true
documentation = "https://docs.rs/tree-sitter-cli"
license.workspace = true
keywords.workspace = true
categories.workspace = true
include = [ "build.rs", "README.md", "LICENSE", "benches/*", "src/**" ]
include = ["build.rs", "README.md", "LICENSE", "benches/*", "src/**"]
[lints]
workspace = true
@ -20,18 +21,18 @@ workspace = true
path = "src/tree_sitter_cli.rs"
[[bin]]
doc = false
name = "tree-sitter"
path = "src/main.rs"
doc = false
[[bench]]
harness = false
name = "benchmark"
harness = false
[features]
default = [ "qjs-rt" ]
qjs-rt = [ "tree-sitter-generate/qjs-rt" ]
wasm = [ "tree-sitter/wasm", "tree-sitter-loader/wasm" ]
default = ["qjs-rt"]
wasm = ["tree-sitter/wasm", "tree-sitter-loader/wasm"]
qjs-rt = ["tree-sitter-generate/qjs-rt"]
[dependencies]
ansi_colours.workspace = true
@ -66,19 +67,19 @@ wasmparser.workspace = true
webbrowser.workspace = true
tree-sitter.workspace = true
tree-sitter-generate = { workspace = true, features = ["load"] }
tree-sitter-config.workspace = true
tree-sitter-generate = { features = [ "load" ], workspace = true }
tree-sitter-highlight.workspace = true
tree-sitter-loader.workspace = true
tree-sitter-tags.workspace = true
[dev-dependencies]
encoding_rs = "0.8.35"
tree_sitter_proc_macro = { package = "tree-sitter-tests-proc-macro", path = "src/tests/proc_macro" }
widestring = "1.2.1"
tree_sitter_proc_macro = { path = "src/tests/proc_macro", package = "tree-sitter-tests-proc-macro" }
pretty_assertions.workspace = true
tempfile.workspace = true
pretty_assertions.workspace = true
unindent.workspace = true
[package.metadata.binstall]

View file

@ -7,12 +7,11 @@
[npmjs.com]: https://www.npmjs.org/package/tree-sitter-cli
[npmjs.com badge]: https://img.shields.io/npm/v/tree-sitter-cli.svg?color=%23BF4A4A
The Tree-sitter CLI allows you to develop, test, and use Tree-sitter grammars from the command line. It works on `MacOS`,
`Linux`, and `Windows`.
The Tree-sitter CLI allows you to develop, test, and use Tree-sitter grammars from the command line. It works on `MacOS`, `Linux`, and `Windows`.
### Installation
You can install the `tree-sitter-cli` with [`cargo-binstall`](https://github.com/cargo-bins/cargo-binstall):
You can install the `tree-sitter-cli` with `cargo-binstall`:
```sh
cargo binstall tree-sitter-cli
@ -34,11 +33,9 @@ The `tree-sitter` binary itself has no dependencies, but specific commands have
### Commands
* `generate` - The `tree-sitter generate` command will generate a Tree-sitter parser based on the grammar in the current
working directory. See [the documentation] for more information.
* `generate` - The `tree-sitter generate` command will generate a Tree-sitter parser based on the grammar in the current working directory. See [the documentation] for more information.
* `test` - The `tree-sitter test` command will run the unit tests for the Tree-sitter parser in the current working directory.
See [the documentation] for more information.
* `test` - The `tree-sitter test` command will run the unit tests for the Tree-sitter parser in the current working directory. See [the documentation] for more information.
* `parse` - The `tree-sitter parse` command will parse a file (or list of files) using Tree-sitter parsers.

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -6,7 +6,7 @@ use std::{
};
use log::{error, info};
use rand::RngExt;
use rand::Rng;
use regex::Regex;
use tree_sitter::{Language, Parser};
@ -25,7 +25,7 @@ use crate::{
random::Rand,
},
parse::perform_edit,
test::{DiffKey, TestDiff, TestEntry, TestExpectation, parse_tests, render_test_output},
test::{parse_tests, strip_sexp_fields, DiffKey, TestDiff, TestEntry},
};
pub static LOG_ENABLED: LazyLock<bool> = LazyLock::new(|| env::var("TREE_SITTER_LOG").is_ok());
@ -63,9 +63,9 @@ fn regex_env_var(name: &'static str) -> Option<Regex> {
#[must_use]
pub fn new_seed() -> usize {
int_env_var("TREE_SITTER_SEED").unwrap_or_else(|| {
let mut rng = rand::rng();
let seed = rng.random_range(0..=usize::MAX);
eprintln!("fuzz seed: {seed}");
let mut rng = rand::thread_rng();
let seed = rng.gen::<usize>();
info!("Seed: {seed}");
seed
})
}
@ -97,7 +97,9 @@ pub fn fuzz_language_corpus(
.iter()
.any(|lang| lang.as_ref() == language_name)
}
TestEntry::Group { children, .. } => {
TestEntry::Group {
ref mut children, ..
} => {
children.retain_mut(|child| retain(child, language_name));
!children.is_empty()
}
@ -109,16 +111,12 @@ pub fn fuzz_language_corpus(
let corpus_dir = grammar_dir.join(subdir).join("test").join("corpus");
if !corpus_dir.exists() || !corpus_dir.is_dir() {
error!(
"No corpus directory found, ensure that you have a `test/corpus` directory in your grammar directory with at least one test file."
);
error!("No corpus directory found, ensure that you have a `test/corpus` directory in your grammar directory with at least one test file.");
return;
}
if std::fs::read_dir(&corpus_dir).unwrap().count() == 0 {
error!(
"No corpus files found in `test/corpus`, ensure that you have at least one test file in your corpus directory."
);
error!("No corpus files found in `test/corpus`, ensure that you have at least one test file in your corpus directory.");
return;
}
@ -144,7 +142,7 @@ pub fn fuzz_language_corpus(
.take()
.unwrap_or_default()
.into_iter()
.chain(tests.iter().filter(|t| t.skip()).map(get_test_name))
.chain(tests.iter().filter(|x| x.skip).map(get_test_name))
.map(|x| (x, 0))
.collect::<HashMap<String, usize>>();
@ -169,8 +167,31 @@ pub fn fuzz_language_corpus(
println!(" {test_index}. {test_name}");
let passed = allocations::record_checked(|| {
let check_output = !test.error();
test.check_initial_parse(language, &test_name, check_output)
let mut log_session = None;
let mut parser = get_parser(&mut log_session, "log.html");
parser.set_language(language).unwrap();
set_included_ranges(&mut parser, &test.input, test.template_delimiters);
let tree = parser.parse(&test.input, None).unwrap();
if test.error {
return true;
}
let mut actual_output = tree.root_node().to_sexp();
if !test.has_fields {
actual_output = strip_sexp_fields(&actual_output);
}
if actual_output != test.output {
println!("Incorrect initial parse for {test_name}");
DiffKey::print();
println!("{}", TestDiff::new(&actual_output, &test.output));
println!();
return false;
}
true
})
.unwrap_or_else(|e| {
error!("{e}");
@ -250,9 +271,12 @@ pub fn fuzz_language_corpus(
let tree3 = parser.parse(&input, Some(&tree2)).unwrap();
// Verify that the final tree matches the expectation from the corpus.
let actual_output = render_test_output(&input, &tree3, test.cst, test.has_fields).unwrap();
let mut actual_output = tree3.root_node().to_sexp();
if !test.has_fields {
actual_output = strip_sexp_fields(&actual_output);
}
if actual_output != test.output && !test.error() {
if actual_output != test.output && !test.error {
println!("Incorrect parse for {test_name} - seed {seed}");
DiffKey::print();
println!("{}", TestDiff::new(&actual_output, &test.output));
@ -300,56 +324,12 @@ pub struct FlattenedTest {
pub input: Vec<u8>,
pub output: String,
pub languages: Vec<Box<str>>,
pub expectation: TestExpectation,
pub error: bool,
pub skip: bool,
pub has_fields: bool,
pub cst: bool,
pub template_delimiters: Option<(&'static str, &'static str)>,
}
impl FlattenedTest {
#[must_use]
fn skip(&self) -> bool {
self.expectation == TestExpectation::Skip
}
#[must_use]
fn error(&self) -> bool {
self.expectation == TestExpectation::Error
}
#[must_use]
pub(crate) fn check_initial_parse(
&self,
language: &Language,
display_name: &str,
check_output: bool,
) -> bool {
let mut log_session = None;
let mut parser = get_parser(&mut log_session, "log.html");
parser.set_language(language).unwrap();
set_included_ranges(&mut parser, &self.input, self.template_delimiters);
let tree = parser.parse(&self.input, None).unwrap();
if !check_output {
return true;
}
let actual_output =
render_test_output(&self.input, &tree, self.cst, self.has_fields).unwrap();
if actual_output == self.output {
true
} else {
println!("Incorrect initial parse for {display_name}");
DiffKey::print();
println!("{}", TestDiff::new(&actual_output, &self.output));
println!();
false
}
}
}
#[must_use]
pub fn flatten_tests(
test: TestEntry,
@ -382,20 +362,20 @@ pub fn flatten_tests(
if !include.is_match(&name) {
return;
}
} else if let Some(exclude) = exclude
&& exclude.is_match(&name)
{
return;
} else if let Some(exclude) = exclude {
if exclude.is_match(&name) {
return;
}
}
result.push(FlattenedTest {
name,
input,
output,
languages: attributes.languages,
expectation: attributes.expectation,
has_fields,
cst: attributes.cst,
languages: attributes.languages,
error: attributes.error,
skip: attributes.skip,
template_delimiters: None,
});
}

View file

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

View file

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

View file

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

View file

@ -1,13 +1,13 @@
use tree_sitter::{Point, Range, Tree};
#[derive(Debug)]
pub struct ScopeSequence<'a>(Vec<ScopeStack<'a>>);
pub struct ScopeSequence(Vec<ScopeStack>);
type ScopeStack<'a> = Vec<&'a str>;
type ScopeStack = Vec<&'static str>;
impl<'a> ScopeSequence<'a> {
impl ScopeSequence {
#[must_use]
pub fn new(tree: &'a Tree) -> Self {
pub fn new(tree: &Tree) -> Self {
let mut result = Self(Vec::new());
let mut scope_stack = Vec::new();

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -75,18 +75,18 @@ pub fn query_file_at_path(
if opts.ordered_captures {
let mut captures = query_cursor.captures(&query, tree.root_node(), source_code.as_slice());
while let Some((mat, capture_index)) = captures.next() {
let capture = mat.captures()[*capture_index];
let capture = mat.captures[*capture_index];
let capture_name = &query.capture_names()[capture.index as usize];
if !opts.quiet && !should_test {
writeln!(
&mut stdout,
" pattern: {:>2}, capture: {} - {capture_name}, start: {}, end: {}, text: `{}`",
mat.pattern_index,
capture.index,
capture.node.start_position(),
capture.node.end_position(),
capture.node.utf8_text(&source_code).unwrap_or("")
)?;
&mut stdout,
" pattern: {:>2}, capture: {} - {capture_name}, start: {}, end: {}, text: `{}`",
mat.pattern_index,
capture.index,
capture.node.start_position(),
capture.node.end_position(),
capture.node.utf8_text(&source_code).unwrap_or("")
)?;
}
if should_test {
results.push(query_testing::CaptureInfo {
@ -102,18 +102,18 @@ pub fn query_file_at_path(
if !opts.quiet && !should_test {
writeln!(&mut stdout, " pattern: {}", m.pattern_index)?;
}
for capture in m.captures() {
for capture in m.captures {
let start = capture.node.start_position();
let end = capture.node.end_position();
let capture_name = &query.capture_names()[capture.index as usize];
if !opts.quiet && !should_test {
if end.row == start.row {
writeln!(
&mut stdout,
" capture: {} - {capture_name}, start: {start}, end: {end}, text: `{}`",
capture.index,
capture.node.utf8_text(&source_code).unwrap_or("")
)?;
&mut stdout,
" capture: {} - {capture_name}, start: {start}, end: {end}, text: `{}`",
capture.index,
capture.node.utf8_text(&source_code).unwrap_or("")
)?;
} else {
writeln!(
&mut stdout,
@ -142,9 +142,7 @@ pub fn query_file_at_path(
};
// Invariant: `test_summary` will always be `Some` when `should_test` is true
let test_summary = test_summary.unwrap();
let assertions =
query_testing::parse_position_comments(&mut parser, language, source_code.as_slice())?;
match query_testing::assert_expected_captures(&results, &assertions) {
match query_testing::assert_expected_captures(&results, path, &mut parser, language) {
Ok(assertion_count) => {
test_summary.query_results.add_case(TestResult {
name: path_name.to_string(),

View file

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

View file

@ -2,7 +2,8 @@ use std::{
fs,
io::{self, Write},
path::Path,
sync::{Arc, atomic::AtomicUsize},
str,
sync::{atomic::AtomicUsize, Arc},
time::Instant,
};

View file

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

View file

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

View file

@ -11,8 +11,18 @@ fn main() {
let Ok(wasm_headers) = std::env::var("DEP_TREE_SITTER_LANGUAGE_WASM_HEADERS") else {
panic!("Environment variable DEP_TREE_SITTER_LANGUAGE_WASM_HEADERS must be set by the language crate");
};
let Ok(wasm_src) =
std::env::var("DEP_TREE_SITTER_LANGUAGE_WASM_SRC").map(std::path::PathBuf::from)
else {
panic!("Environment variable DEP_TREE_SITTER_LANGUAGE_WASM_SRC must be set by the language crate");
};
c_config.include(&wasm_headers);
c_config.files([
wasm_src.join("stdio.c"),
wasm_src.join("stdlib.c"),
wasm_src.join("string.c"),
]);
}
let parser_path = src_dir.join("parser.c");

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,11 +1,10 @@
// swift-tools-version:5.6
// swift-tools-version:5.3
import Foundation
import PackageDescription
let dir = Context.packageDirectory
var sources = ["src/parser.c"]
if FileManager.default.fileExists(atPath: "\(dir)/src/scanner.c") {
if FileManager.default.fileExists(atPath: "src/scanner.c") {
sources.append("src/scanner.c")
}

View file

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

File diff suppressed because it is too large Load diff

View file

@ -1,12 +1,12 @@
use std::{fs, path::Path};
use anyhow::{Result, anyhow};
use anyhow::{anyhow, Result};
use tree_sitter::Point;
use tree_sitter_highlight::{Highlight, HighlightConfiguration, HighlightEvent, Highlighter};
use tree_sitter_loader::{Config, Loader};
use crate::{
query_testing::{Assertion, Utf8Point, parse_position_comments, to_utf8_point},
query_testing::{parse_position_comments, to_utf8_point, Assertion, Utf8Point},
test::{TestInfo, TestOutcome, TestResult, TestSummary},
util,
};
@ -120,7 +120,11 @@ pub fn test_highlights(
}
}
if failed { Err(anyhow!("")) } else { Ok(()) }
if failed {
Err(anyhow!(""))
} else {
Ok(())
}
}
pub fn iterate_assertions(
@ -219,11 +223,9 @@ pub fn get_highlight_positions(
let mut highlight_stack = Vec::new();
let source = String::from_utf8_lossy(source);
let mut char_indices = source.char_indices();
for event in
highlighter.highlight(highlight_config, source.as_bytes(), None, None, |string| {
loader.highlight_config_for_injection_string(string)
})?
{
for event in highlighter.highlight(highlight_config, source.as_bytes(), None, |string| {
loader.highlight_config_for_injection_string(string)
})? {
match event? {
HighlightEvent::HighlightStart(h) => highlight_stack.push(h),
HighlightEvent::HighlightEnd => {

View file

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

View file

@ -17,12 +17,13 @@ mod tree_test;
#[cfg(feature = "wasm")]
mod wasm_language_test;
use tree_sitter_generate::{GenerateResult, OptLevel};
use tree_sitter_generate::GenerateResult;
pub use crate::fuzz::{
ITERATION_COUNT, allocations,
allocations,
edits::{get_random_edit, invert_edit},
random::Rand,
ITERATION_COUNT,
};
pub use helpers::fixtures::get_language;
@ -30,10 +31,5 @@ pub use helpers::fixtures::get_language;
/// This is a simple wrapper around [`tree_sitter_generate::generate_parser_for_grammar`], because
/// our tests do not need to pass in a version number, only the grammar JSON.
fn generate_parser(grammar_json: &str) -> GenerateResult<(String, String)> {
tree_sitter_generate::generate_parser_for_grammar(
grammar_json,
Some((0, 0, 0)),
OptLevel::default(),
&mut Vec::new(),
)
tree_sitter_generate::generate_parser_for_grammar(grammar_json, Some((0, 0, 0)))
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -66,13 +66,9 @@ fn test_parsing_with_logging() {
parser.set_language(&get_language("rust")).unwrap();
let mut messages = Vec::new();
// SAFETY: the logger borrows `messages` and is only invoked during the
// `parse` call below while `messages` is in scope.
unsafe {
parser.set_logger_unchecked(Some(Box::new(|log_type, message| {
messages.push((log_type, message.to_string()));
})));
}
parser.set_logger(Some(Box::new(|log_type, message| {
messages.push((log_type, message.to_string()));
})));
parser
.parse(
@ -253,21 +249,6 @@ fn test_parsing_with_custom_utf16_be_input() {
assert_eq!(root.child(0).unwrap().kind(), "function_item");
}
#[test]
fn test_utf16_decodes_surrogate_pairs() {
let mut parser = Parser::new();
let language = get_test_fixture_language("utf16_surrogate_oob");
parser.set_language(&language).unwrap();
let le = [0xD83D_u16.to_le(), 0xDE00_u16.to_le()];
let tree = parser.parse_utf16_le(le, None).unwrap();
assert_eq!(tree.root_node().to_sexp(), "(program (supplementary))");
let be = [0xD83D_u16.to_be(), 0xDE00_u16.to_be()];
let tree = parser.parse_utf16_be(be, None).unwrap();
assert_eq!(tree.root_node().to_sexp(), "(program (supplementary))");
}
#[test]
fn test_utf16_decode_does_not_read_oob() {
// Test for a buffer over-read in ts_decode_utf16_le/be when a lead surrogate
@ -822,7 +803,11 @@ fn test_parsing_cancelled_by_another_thread() {
&mut |offset, _| {
thread::yield_now();
thread::sleep(time::Duration::from_millis(10));
if offset == 0 { b" [" } else { b"0," }
if offset == 0 {
b" ["
} else {
b"0,"
}
},
None,
Some(ParseOptions::new().progress_callback(callback)),
@ -845,7 +830,11 @@ fn test_parsing_with_a_timeout() {
let start_time = time::Instant::now();
let tree = parser.parse_with_options(
&mut |offset, _| {
if offset == 0 { b" [" } else { b",0" }
if offset == 0 {
b" ["
} else {
b",0"
}
},
None,
Some(ParseOptions::new().progress_callback(&mut |_| {
@ -863,7 +852,11 @@ fn test_parsing_with_a_timeout() {
let start_time = time::Instant::now();
let tree = parser.parse_with_options(
&mut |offset, _| {
if offset == 0 { b" [" } else { b",0" }
if offset == 0 {
b" ["
} else {
b",0"
}
},
None,
Some(ParseOptions::new().progress_callback(&mut |_| {
@ -1060,9 +1053,9 @@ fn test_parsing_with_timeout_during_balancing() {
let mut parser = Parser::new();
parser.set_language(&get_language("javascript")).unwrap();
let function_count: u32 = 100;
let function_count = 100;
let code = "function() {}\n".repeat(function_count as usize);
let code = "function() {}\n".repeat(function_count);
let mut current_byte_offset = 0;
let mut in_balancing = false;
let tree = parser.parse_with_options(
@ -1804,15 +1797,11 @@ fn test_parsing_with_scanner_logging() {
.unwrap();
let mut found = false;
// SAFETY: the logger borrows `found` and is only invoked during the `parse`
// call below, while `found` is in scope.
unsafe {
parser.set_logger_unchecked(Some(Box::new(|log_type, message| {
if log_type == LogType::Lex && message == "Found a percent string" {
found = true;
}
})));
}
parser.set_logger(Some(Box::new(|log_type, message| {
if log_type == LogType::Lex && message == "Found a percent string" {
found = true;
}
})));
let source_code = "x + %(sup (external) scanner?)";
@ -1932,7 +1921,7 @@ fn test_decode_cp1252() {
fn decode(bytes: &[u8]) -> (i32, u32) {
if !bytes.is_empty() {
let byte = bytes[0];
(i32::from(byte), 1)
(byte as i32, 1)
} else {
(0, 0)
}
@ -1968,7 +1957,7 @@ fn test_decode_macintosh() {
fn decode(bytes: &[u8]) -> (i32, u32) {
if !bytes.is_empty() {
let byte = bytes[0];
(i32::from(byte), 1)
(byte as i32, 1)
} else {
(0, 0)
}
@ -2030,9 +2019,8 @@ fn test_decode_utf24le() {
#[test]
fn test_grammars_that_should_not_compile() {
assert!(
generate_parser(
r#"
assert!(generate_parser(
r#"
{
"name": "issue_1111",
"rules": {
@ -2040,13 +2028,11 @@ fn test_grammars_that_should_not_compile() {
},
}
"#
)
.is_err()
);
)
.is_err());
assert!(
generate_parser(
r#"
assert!(generate_parser(
r#"
{
"name": "issue_1271",
"rules": {
@ -2061,13 +2047,11 @@ fn test_grammars_that_should_not_compile() {
},
}
"#
)
.is_err()
);
)
.is_err());
assert!(
generate_parser(
r#"
assert!(generate_parser(
r#"
{
"name": "issue_1156_expl_1",
"rules": {
@ -2081,13 +2065,11 @@ fn test_grammars_that_should_not_compile() {
},
}
"#
)
.is_err()
);
)
.is_err());
assert!(
generate_parser(
r#"
assert!(generate_parser(
r#"
{
"name": "issue_1156_expl_2",
"rules": {
@ -2104,13 +2086,11 @@ fn test_grammars_that_should_not_compile() {
},
}
"#
)
.is_err()
);
)
.is_err());
assert!(
generate_parser(
r#"
assert!(generate_parser(
r#"
{
"name": "issue_1156_expl_3",
"rules": {
@ -2124,13 +2104,11 @@ fn test_grammars_that_should_not_compile() {
},
}
"#
)
.is_err()
);
)
.is_err());
assert!(
generate_parser(
r#"
assert!(generate_parser(
r#"
{
"name": "issue_1156_expl_4",
"rules": {
@ -2147,9 +2125,8 @@ fn test_grammars_that_should_not_compile() {
},
}
"#
)
.is_err()
);
)
.is_err());
}
const fn simple_range(start: usize, end: usize) -> Range {

View file

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

View file

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

View file

@ -1,12 +1,12 @@
use std::{env, fmt::Write, ops::ControlFlow, sync::LazyLock};
use indoc::indoc;
use rand::{SeedableRng, prelude::StdRng};
use rand::{prelude::StdRng, SeedableRng};
use streaming_iterator::StreamingIterator;
use tree_sitter::{
CaptureQuantifier, InputEdit, Language, Node, Parser, Point, Query, QueryCursor,
QueryCursorOptions, QueryCursorState, QueryError, QueryErrorKind, QueryPredicate,
QueryPredicateArg, QueryProperty, Range,
QueryCursorOptions, QueryError, QueryErrorKind, QueryPredicate, QueryPredicateArg,
QueryProperty, Range,
};
use tree_sitter_generate::load_grammar_file;
use unindent::Unindent;
@ -14,14 +14,15 @@ use unindent::Unindent;
use super::helpers::{
allocations,
fixtures::{get_language, get_test_language},
query_helpers::{Match, Pattern, assert_query_matches},
query_helpers::{assert_query_matches, Match, Pattern},
};
use crate::tests::{
ITERATION_COUNT, generate_parser,
generate_parser,
helpers::{
fixtures::get_test_fixture_language,
query_helpers::{collect_captures, collect_matches},
},
ITERATION_COUNT,
};
static EXAMPLE_FILTER: LazyLock<Option<String>> =
@ -33,13 +34,11 @@ 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!(
@ -1163,8 +1162,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.
// 3. Between two child nodes in a pattern, it specifies that there cannot be any named
// siblings between those two child nodes.
// 2. Between two child nodes in a pattern, it specifies that there cannot be any named
// siblings between those two child snodes.
let query = Query::new(
&language,
"
@ -2370,7 +2369,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 no crash or
// Just check that a subset of the results are returned, and crash or
// leak occurs.
assert_eq!(
collect_matches(matches, &query, source.as_str())[0],
@ -2597,7 +2596,7 @@ fn test_query_matches_with_supertypes() {
}
#[test]
#[expect(clippy::reversed_empty_ranges, reason = "testing empty range behavior")]
#[allow(clippy::reversed_empty_ranges)]
fn test_query_matches_within_byte_range() {
allocations::record(|| {
let language = get_language("javascript");
@ -2918,7 +2917,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());
}
}
@ -2934,7 +2933,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());
}
}
@ -2950,7 +2949,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());
}
}
@ -3008,7 +3007,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()],
@ -3031,7 +3030,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()],
@ -3330,7 +3329,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| {
(
@ -4311,7 +4310,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();
@ -4356,7 +4355,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();
@ -4397,18 +4396,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());
@ -4583,13 +4582,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();
cursor
let node = cursor
.matches(&query, node, source.as_bytes())
.next()
.unwrap()
.captures()[0]
.node
.captures[0]
.node;
node
}
let node = take_first_node_from_captures(source, query, tree.root_node());
@ -4603,14 +4602,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();
cursor
let node = cursor
.captures(&query, node, source.as_bytes())
.next()
.unwrap()
.0
.captures()[0]
.node
.captures[0]
.node;
node
}
let node = take_first_node_from_matches(source, query, tree.root_node());
@ -4623,7 +4622,7 @@ fn test_query_with_no_patterns() {
allocations::record(|| {
let language = get_language("javascript");
let query = Query::new(&language, "").unwrap();
assert_eq!(query.capture_names(), [] as [&str; 0]);
assert!(query.capture_names().is_empty());
assert_eq!(query.pattern_count(), 0);
});
}
@ -4696,54 +4695,6 @@ 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(|| {
@ -4819,7 +4770,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<_>>(),
@ -5122,10 +5073,10 @@ fn test_query_is_pattern_guaranteed_at_step() {
eprintln!();
for row in rows {
if let Some(filter) = EXAMPLE_FILTER.as_ref()
&& !row.description.contains(filter.as_str())
{
continue;
if let Some(filter) = EXAMPLE_FILTER.as_ref() {
if !row.description.contains(filter.as_str()) {
continue;
}
}
eprintln!(" query example: {:?}", row.description);
let query = Query::new(&row.language, row.pattern).unwrap();
@ -5218,10 +5169,10 @@ fn test_query_is_pattern_rooted() {
let language = get_language("python");
for row in &rows {
if let Some(filter) = EXAMPLE_FILTER.as_ref()
&& !row.description.contains(filter.as_str())
{
continue;
if let Some(filter) = EXAMPLE_FILTER.as_ref() {
if !row.description.contains(filter.as_str()) {
continue;
}
}
eprintln!(" query example: {:?}", row.description);
let query = Query::new(&language, row.pattern).unwrap();
@ -5315,10 +5266,10 @@ fn test_query_is_pattern_non_local() {
eprintln!();
for row in &rows {
if let Some(filter) = EXAMPLE_FILTER.as_ref()
&& !row.description.contains(filter.as_str())
{
continue;
if let Some(filter) = EXAMPLE_FILTER.as_ref() {
if !row.description.contains(filter.as_str()) {
continue;
}
}
eprintln!(" query example: {:?}", row.description);
let query = Query::new(&row.language, row.pattern).unwrap();
@ -5545,10 +5496,10 @@ fn test_capture_quantifiers() {
eprintln!();
for row in rows {
if let Some(filter) = EXAMPLE_FILTER.as_ref()
&& !row.description.contains(filter.as_str())
{
continue;
if let Some(filter) = EXAMPLE_FILTER.as_ref() {
if !row.description.contains(filter.as_str()) {
continue;
}
}
eprintln!(" query example: {:?}", row.description);
let query = Query::new(&row.language, row.pattern).unwrap();
@ -5833,10 +5784,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;
}
}
@ -5898,7 +5849,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 {
@ -6074,21 +6025,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 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 start_time = std::time::Instant::now();
let matches = cursor
.matches_with_options(
&query,
tree.root_node(),
source_code.as_bytes(),
QueryCursorOptions::new().progress_callback(&mut progress_callback),
QueryCursorOptions::new().progress_callback(&mut |_| {
if start_time.elapsed().as_micros() > 1000 {
ControlFlow::Break(())
} else {
ControlFlow::Continue(())
}
}),
)
.count();
assert!(matches < 1000);
@ -6099,43 +6050,13 @@ 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();
#[expect(
clippy::literal_string_with_formatting_args,
reason = "raw string contains format syntax as literal code"
)]
#[allow(clippy::literal_string_with_formatting_args)]
let code = r#"fn main() {
println!("{:?}", foo());
}"#;

View file

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

View file

@ -3,8 +3,8 @@ use tree_sitter_highlight::{Highlight, Highlighter};
use super::helpers::fixtures::{get_highlight_config, get_language, test_loader};
use crate::{
query_testing::{Assertion, Utf8Point, parse_position_comments},
test_highlight::{Failure, get_highlight_positions, iterate_assertions},
query_testing::{parse_position_comments, Assertion, Utf8Point},
test_highlight::{get_highlight_positions, iterate_assertions, Failure},
};
#[test]

View file

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

View file

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

View file

@ -1,3 +1,5 @@
use std::str;
use tree_sitter::{InputEdit, Parser, Point, Range, Tree};
use super::helpers::fixtures::get_language;

View file

@ -5,7 +5,7 @@ use tree_sitter::{Parser, Query, QueryCursor, WasmError, WasmErrorKind, WasmStor
use crate::tests::helpers::{
allocations,
fixtures::{ENGINE, WASM_DIR, get_test_fixture_language_wasm},
fixtures::{get_test_fixture_language_wasm, ENGINE, WASM_DIR},
};
#[test]
@ -73,10 +73,7 @@ fn test_load_wasm_rust_language() {
parser.set_wasm_store(store).unwrap();
parser.set_language(&language).unwrap();
let tree = parser.parse("fn main() {}", None).unwrap();
assert_eq!(
tree.root_node().to_sexp(),
"(source_file (function_item name: (identifier) parameters: (parameters) body: (block)))"
);
assert_eq!(tree.root_node().to_sexp(), "(source_file (function_item name: (identifier) parameters: (parameters) body: (block)))");
});
}
@ -90,10 +87,7 @@ fn test_load_wasm_javascript_language() {
parser.set_wasm_store(store).unwrap();
parser.set_language(&language).unwrap();
let tree = parser.parse("const a = b\nconst c = d", None).unwrap();
assert_eq!(
tree.root_node().to_sexp(),
"(program (lexical_declaration (variable_declarator name: (identifier) value: (identifier))) (lexical_declaration (variable_declarator name: (identifier) value: (identifier))))"
);
assert_eq!(tree.root_node().to_sexp(), "(program (lexical_declaration (variable_declarator name: (identifier) value: (identifier))) (lexical_declaration (variable_declarator name: (identifier) value: (identifier))))");
});
}
@ -107,10 +101,7 @@ fn test_load_wasm_python_language() {
parser.set_wasm_store(store).unwrap();
parser.set_language(&language).unwrap();
let tree = parser.parse("a = b\nc = d", None).unwrap();
assert_eq!(
tree.root_node().to_sexp(),
"(module (expression_statement (assignment left: (identifier) right: (identifier))) (expression_statement (assignment left: (identifier) right: (identifier))))"
);
assert_eq!(tree.root_node().to_sexp(), "(module (expression_statement (assignment left: (identifier) right: (identifier))) (expression_statement (assignment left: (identifier) right: (identifier))))");
});
}
@ -267,18 +258,12 @@ fn test_reset_wasm_store() {
parser.set_wasm_store(parser_store).unwrap();
parser.set_language(&language).unwrap();
let tree = parser.parse("fn main() {}", None).unwrap();
assert_eq!(
tree.root_node().to_sexp(),
"(source_file (function_item name: (identifier) parameters: (parameters) body: (block)))"
);
assert_eq!(tree.root_node().to_sexp(), "(source_file (function_item name: (identifier) parameters: (parameters) body: (block)))");
let parser_store = WasmStore::new(&ENGINE).unwrap();
parser.set_wasm_store(parser_store).unwrap();
let tree = parser.parse("fn main() {}", None).unwrap();
assert_eq!(
tree.root_node().to_sexp(),
"(source_file (function_item name: (identifier) parameters: (parameters) body: (block)))"
);
assert_eq!(tree.root_node().to_sexp(), "(source_file (function_item name: (identifier) parameters: (parameters) body: (block)))");
});
}
@ -387,18 +372,3 @@ fn test_wasm_oom() {
);
});
}
#[test]
fn test_lookahead_iterator_outlives_wasm_language() {
allocations::record(|| {
let mut store = WasmStore::new(&ENGINE).unwrap();
let wasm = fs::read(WASM_DIR.join("tree-sitter-ruby.wasm")).unwrap();
let language = store.load_language("ruby", &wasm).unwrap();
let mut lookahead = language.lookahead_iterator(0).unwrap();
drop(language);
// The iterator retains the language, so the names are still live.
assert!(lookahead.iter_names().count() > 0);
});
}

View file

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

View file

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

View file

@ -144,7 +144,7 @@ impl Version {
}
}
fn update_file_with<F>(path: &PathBuf, update_fn: F) -> Result<(), UpdateError>
fn update_file_with<F>(&self, path: &PathBuf, update_fn: F) -> Result<(), UpdateError>
where
F: Fn(&str) -> String,
{
@ -155,7 +155,7 @@ impl Version {
fn update_treesitter_json(&self) -> Result<(), UpdateError> {
let json_path = self.current_dir.join("tree-sitter.json");
Self::update_file_with(&json_path, |content| {
self.update_file_with(&json_path, |content| {
content
.lines()
.map(|line| {
@ -189,7 +189,7 @@ impl Version {
return Ok(());
}
Self::update_file_with(&cargo_toml_path, |content| {
self.update_file_with(&cargo_toml_path, |content| {
content
.lines()
.map(|line| {
@ -236,7 +236,7 @@ impl Version {
return Ok(());
}
Self::update_file_with(&package_json_path, |content| {
self.update_file_with(&package_json_path, |content| {
content
.lines()
.map(|line| {
@ -296,7 +296,7 @@ impl Version {
return Ok(());
}
Self::update_file_with(&makefile_path, |content| {
self.update_file_with(&makefile_path, |content| {
content
.lines()
.map(|line| {
@ -320,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(
@ -339,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| {
@ -363,7 +363,7 @@ impl Version {
return Ok(());
}
Self::update_file_with(&zig_zon_path, |content| {
self.update_file_with(&zig_zon_path, |content| {
let zig_version_prefix = ".version =";
content
.lines()

View file

@ -3,7 +3,7 @@ use std::{
path::{Path, PathBuf},
};
use anyhow::{Context, Result, anyhow};
use anyhow::{anyhow, Context, Result};
use tree_sitter::wasm_stdlib_symbols;
use tree_sitter_generate::{load_grammar_file, parse_grammar::GrammarJSON};
use tree_sitter_loader::Loader;
@ -83,17 +83,14 @@ pub fn compile_language_to_wasm(
let wasm_bytes = fs::read(&output_filename)?;
let parser = Parser::new(0);
for payload in parser.parse_all(&wasm_bytes) {
if let wasmparser::Payload::ImportSection(reader) = payload? {
for imports in reader {
for import in imports? {
let (_, import) = import?;
let name = import.name;
if !builtin_symbols.contains(&name)
&& !stdlib_symbols.contains(&name)
&& !dylink_symbols.contains(&name)
{
missing_symbols.push(name);
}
if let wasmparser::Payload::ImportSection(imports) = payload? {
for import in imports {
let import = import?.name;
if !builtin_symbols.contains(&import)
&& !stdlib_symbols.contains(&import)
&& !dylink_symbols.contains(&import)
{
missing_symbols.push(import);
}
}
}

View file

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

View file

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

View file

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

View file

@ -1,415 +0,0 @@
use std::{
hash::{Hash, Hasher},
ptr,
};
const ARENA_CHUNK_WORDS: usize = 128 * 1024 / std::mem::size_of::<u64>();
struct WordArena {
chunks: Vec<Vec<u64>>,
offset: usize,
/// Freed blocks grouped by size (number of words). Checked before
/// bump-allocating so that dropped `BitVec`s can be reused immediately.
free_list: Vec<(usize, Vec<*mut u64>)>,
}
impl WordArena {
const fn new() -> Self {
Self {
chunks: Vec::new(),
offset: ARENA_CHUNK_WORDS, // forces first alloc to create a chunk
free_list: Vec::new(),
}
}
#[inline]
fn alloc(&mut self, n_words: usize) -> *mut u64 {
if n_words == 0 {
return std::ptr::NonNull::<u64>::dangling().as_ptr();
}
// Check the free list before bump-allocating.
if let Some((_, bucket)) = self.free_list.iter_mut().find(|(s, _)| *s == n_words)
&& let Some(ptr) = bucket.pop()
{
return ptr;
}
if self.offset + n_words > ARENA_CHUNK_WORDS {
let size = ARENA_CHUNK_WORDS.max(n_words);
self.chunks.push(vec![0u64; size]);
self.offset = 0;
}
let chunk = self.chunks.last_mut().unwrap();
// SAFETY: Either a new chunk was just created with len = ARENA_CHUNK_WORDS.max(n_words),
// in which case offset = 0 and n_words <= len; or an existing chunk is reused, in which
// case offset + n_words <= ARENA_CHUNK_WORDS <= chunk.len().
let ptr = unsafe { chunk.as_mut_ptr().add(self.offset) };
self.offset += n_words;
ptr
}
#[inline]
fn free(&mut self, ptr: *mut u64, n_words: usize, used_words: usize) {
if n_words == 0 {
return;
}
// Zero only the in-use words; the rest are already zero by the BitVec
// invariant (data[words_in_use..capacity] is always zero).
// SAFETY: ptr was returned by alloc(n_words) and is valid for n_words
// words; used_words <= n_words.
if used_words > 0 {
unsafe { std::slice::from_raw_parts_mut(ptr, used_words).fill(0) };
}
if let Some((_, bucket)) = self.free_list.iter_mut().find(|(s, _)| *s == n_words) {
bucket.push(ptr);
} else {
self.free_list.push((n_words, vec![ptr]));
}
}
}
thread_local! {
static WORD_ARENA: std::cell::RefCell<WordArena> = const { std::cell::RefCell::new(WordArena::new()) };
}
#[inline]
fn arena_alloc(n_words: usize) -> *mut u64 {
WORD_ARENA.with(|a| a.borrow_mut().alloc(n_words))
}
#[inline]
fn arena_free(ptr: *mut u64, n_words: usize, used_words: usize) {
WORD_ARENA.with(|a| a.borrow_mut().free(ptr, n_words, used_words));
}
/// A bit vector whose backing `u64` words are bump-allocated from a global
/// arena. Token sets are OR'd together many times and doing this at the word
/// level rather than bit-by-bit is much faster.
pub struct BitVec {
/// Pointer into arena chunk data. Dangling when `capacity == 0`.
data: *mut u64,
num_bits: u32,
/// Number of allocated words (_not_ bytes) in the arena region.
capacity: u32,
}
impl BitVec {
#[must_use]
pub const fn new() -> Self {
Self {
data: ptr::NonNull::dangling().as_ptr(),
num_bits: 0,
capacity: 0,
}
}
#[must_use]
pub fn with_capacity(n_bits: usize) -> Self {
let n_words = n_bits.div_ceil(64);
if n_words == 0 {
return Self::new();
}
Self {
data: arena_alloc(n_words),
num_bits: 0,
capacity: n_words as u32,
}
}
#[inline]
const fn words_in_use(&self) -> usize {
(self.num_bits as usize).div_ceil(64)
}
/// View the in-use words as a slice.
#[inline]
#[must_use]
pub const fn as_slice(&self) -> &[u64] {
let n = self.words_in_use();
if n == 0 {
&[]
} else {
// SAFETY: data points to at least `capacity` valid words, and
// words_in_use() <= capacity.
unsafe { std::slice::from_raw_parts(self.data, n) }
}
}
/// View all `capacity` allocated words as a mutable slice.
#[inline]
const fn as_full_slice_mut(&mut self) -> &mut [u64] {
let n = self.capacity as usize;
if n == 0 {
return &mut [];
}
// SAFETY: data points to `capacity` valid words.
unsafe { std::slice::from_raw_parts_mut(self.data, n) }
}
#[must_use]
#[allow(clippy::len_without_is_empty)]
pub const fn len(&self) -> usize {
self.num_bits as usize
}
#[must_use]
pub const fn get(&self, index: usize) -> Option<bool> {
if index >= self.num_bits as usize {
return None;
}
Some(self.as_slice()[index / 64] >> (index % 64) & 1 != 0)
}
pub const fn set(&mut self, index: usize, val: bool) {
let word_idx = index / 64;
let bit_idx = index % 64;
let words = self.as_full_slice_mut();
if val {
words[word_idx] |= 1u64 << bit_idx;
} else {
words[word_idx] &= !(1u64 << bit_idx);
}
}
/// Grow the backing storage so that the arena region holds at least
/// `n_words` words, copying existing data into the new region.
fn ensure_words(&mut self, n_words: usize) {
if n_words > self.capacity as usize {
let new_cap = n_words.max((self.capacity as usize) * 2);
let new_data = arena_alloc(new_cap);
let old = self.words_in_use();
if old > 0 {
// SAFETY: new_data points to new_cap valid zeroed words; old <= capacity.
let dst = unsafe { std::slice::from_raw_parts_mut(new_data, old) };
dst.copy_from_slice(self.as_slice());
}
let old_cap = self.capacity;
let old_data = self.data;
self.data = new_data;
self.capacity = new_cap as u32;
arena_free(old_data, old_cap as usize, old);
}
}
pub fn resize(&mut self, new_len: usize, val: bool) {
let new_words = new_len.div_ceil(64);
let old_words = self.words_in_use();
self.ensure_words(new_words);
let fill = if val { !0u64 } else { 0u64 };
let words = self.as_full_slice_mut();
if new_words > old_words {
words[old_words..new_words].fill(fill);
// When setting bits to true, clear any bits in the last word that
// fall beyond new_len to preserve the invariant that bits >= num_bits
// are always zero.
if val && !new_len.is_multiple_of(64) {
let mask = (1u64 << (new_len % 64)) - 1;
words[new_words - 1] &= mask;
}
} else if new_words < old_words {
// Zero out truncated words so stale data is never visible.
words[new_words..old_words].fill(0);
}
self.num_bits = new_len as u32;
}
#[must_use]
pub const fn last(&self) -> Option<bool> {
if self.num_bits == 0 {
return None;
}
self.get(self.num_bits as usize - 1)
}
pub const fn pop(&mut self) -> Option<bool> {
if self.num_bits == 0 {
return None;
}
self.num_bits -= 1;
let word_idx = self.num_bits as usize / 64;
let bit_idx = self.num_bits as usize % 64;
let new_words_in_use = self.words_in_use();
let words = self.as_full_slice_mut();
let val = words[word_idx] >> bit_idx & 1 != 0;
if val {
words[word_idx] &= !(1u64 << bit_idx);
}
// Zero out the word if it's no longer in use.
if word_idx >= new_words_in_use {
words[word_idx] = 0;
}
Some(val)
}
/// Word-level OR: self |= other. Returns true if any new bits were set.
#[inline]
pub fn insert_all(&mut self, other: &Self) -> bool {
let other_words = other.words_in_use();
if other_words == 0 {
return false;
}
let self_words = self.words_in_use();
if other_words > self.capacity as usize {
// Need a larger arena region.
let new_data = arena_alloc(other_words);
if self_words > 0 {
// SAFETY: new_data points to other_words valid zeroed words; self_words <= capacity.
let dst = unsafe { std::slice::from_raw_parts_mut(new_data, self_words) };
dst.copy_from_slice(self.as_slice());
}
// Arena memory is pre-zeroed, so words self_words..other_words are already 0.
let old_cap = self.capacity;
let old_data = self.data;
self.data = new_data;
self.capacity = other_words as u32;
arena_free(old_data, old_cap as usize, self_words);
} else if other_words > self_words {
// Have capacity, but clear any stale data in the region we're about to OR into.
self.as_full_slice_mut()[self_words..other_words].fill(0);
}
if other.num_bits > self.num_bits {
self.num_bits = other.num_bits;
}
let other_slice = other.as_slice();
let self_slice = &mut self.as_full_slice_mut()[..other_words];
let mut any_new = 0u64;
for (sw, &ow) in self_slice.iter_mut().zip(other_slice) {
let new_bits = ow & !*sw;
*sw |= ow;
any_new |= new_bits;
}
any_new != 0
}
}
impl Drop for BitVec {
fn drop(&mut self) {
if self.capacity > 0 {
arena_free(self.data, self.capacity as usize, self.words_in_use());
}
}
}
impl Default for BitVec {
fn default() -> Self {
Self::new()
}
}
impl Clone for BitVec {
fn clone(&self) -> Self {
let words = self.words_in_use();
if words == 0 {
return Self::new();
}
let new_data = arena_alloc(words);
// SAFETY: new_data points to `words` valid zeroed words.
let dst = unsafe { std::slice::from_raw_parts_mut(new_data, words) };
dst.copy_from_slice(self.as_slice());
Self {
data: new_data,
num_bits: self.num_bits,
capacity: words as u32,
}
}
}
impl PartialEq for BitVec {
fn eq(&self, other: &Self) -> bool {
let a = self.as_slice();
let b = other.as_slice();
let max_len = a.len().max(b.len());
for i in 0..max_len {
if a.get(i).copied().unwrap_or(0) != b.get(i).copied().unwrap_or(0) {
return false;
}
}
true
}
}
impl Eq for BitVec {}
impl Hash for BitVec {
fn hash<H: Hasher>(&self, state: &mut H) {
let data = self.as_slice();
let effective_len = data.iter().rposition(|&w| w != 0).map_or(0, |i| i + 1);
data[..effective_len].hash(state);
}
}
impl Ord for BitVec {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
let a = self.as_slice();
let b = other.as_slice();
let max_len = a.len().max(b.len());
for i in 0..max_len {
let aw = a.get(i).copied().unwrap_or(0);
let bw = b.get(i).copied().unwrap_or(0);
if aw != bw {
let first_diff = (aw ^ bw).trailing_zeros();
return if (aw >> first_diff) & 1 != 0 {
std::cmp::Ordering::Greater
} else {
std::cmp::Ordering::Less
};
}
}
std::cmp::Ordering::Equal
}
}
impl PartialOrd for BitVec {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl std::ops::Index<usize> for BitVec {
type Output = bool;
fn index(&self, index: usize) -> &Self::Output {
static TRUE: bool = true;
static FALSE: bool = false;
if self.as_slice()[index / 64] >> (index % 64) & 1 != 0 {
&TRUE
} else {
&FALSE
}
}
}
/// Iterator that yields only the indices of set bits, skipping zero words
/// entirely and using `trailing_zeros()` within each word.
pub struct SetBitsIter<'a> {
data: &'a [u64],
word_idx: usize,
current_word: u64,
}
impl<'a> SetBitsIter<'a> {
#[must_use]
pub fn new(data: &'a [u64]) -> Self {
Self {
data,
word_idx: 0,
current_word: data.first().copied().unwrap_or(0),
}
}
}
impl Iterator for SetBitsIter<'_> {
type Item = usize;
fn next(&mut self) -> Option<usize> {
while self.current_word == 0 {
self.word_idx += 1;
if self.word_idx >= self.data.len() {
return None;
}
self.current_word = self.data[self.word_idx];
}
let bit = self.current_word.trailing_zeros() as usize;
self.current_word &= self.current_word - 1; // clear lowest set bit
Some(self.word_idx * 64 + bit)
}
}

View file

@ -6,31 +6,28 @@ mod item_set_builder;
mod minimize_parse_table;
mod token_conflicts;
use std::collections::BTreeSet;
use std::collections::{BTreeSet, HashMap};
pub use build_lex_table::LARGE_CHARACTER_RANGE_COUNT;
use build_parse_table::BuildTableResult;
pub use build_parse_table::ParseTableBuilderError;
use log::{debug, info};
use rustc_hash::FxHashMap;
use self::{
build_lex_table::build_lex_table,
build_parse_table::{ParseStateInfo, build_parse_table},
build_parse_table::{build_parse_table, ParseStateInfo},
coincident_tokens::CoincidentTokenIndex,
item::ItemKeyMap,
item_set_builder::ParseItemSetBuilder,
minimize_parse_table::minimize_parse_table,
token_conflicts::TokenConflictMap,
};
use crate::{
Diagnostic, OptLevel,
grammars::{InlinedProductionMap, LexicalGrammar, SyntaxGrammar},
nfa::{CharacterSet, NfaCursor},
node_types::VariableInfo,
rules::{AliasMap, Symbol, SymbolType, TokenSet},
strpool::StrPool,
tables::{ActionList, ActionListPool, LexTable, ParseAction, ParseTable, ParseTableEntry},
tables::{LexTable, ParseAction, ParseTable, ParseTableEntry},
OptLevel,
};
pub struct Tables {
@ -40,42 +37,32 @@ pub struct Tables {
pub large_character_sets: Vec<(Option<Symbol>, CharacterSet)>,
}
#[expect(
clippy::too_many_arguments,
reason = "all parameters are required for table building"
)]
pub fn build_tables(
syntax_grammar: &SyntaxGrammar,
lexical_grammar: &LexicalGrammar,
simple_aliases: &AliasMap,
variable_info: &[VariableInfo],
inlines: &InlinedProductionMap,
str_pool: &StrPool,
report_symbol_name: Option<&str>,
optimizations: OptLevel,
diagnostics: &mut Vec<Diagnostic>,
) -> BuildTableResult<Tables> {
let item_key_map = ItemKeyMap::new(syntax_grammar, str_pool);
let item_set_builder =
ParseItemSetBuilder::new(syntax_grammar, lexical_grammar, inlines, &item_key_map);
let following_tokens = get_following_tokens(syntax_grammar, lexical_grammar, &item_set_builder);
let item_set_builder = ParseItemSetBuilder::new(syntax_grammar, lexical_grammar, inlines);
let following_tokens =
get_following_tokens(syntax_grammar, lexical_grammar, inlines, &item_set_builder);
let (mut parse_table, parse_state_info) = build_parse_table(
syntax_grammar,
lexical_grammar,
item_set_builder,
variable_info,
str_pool,
diagnostics,
)?;
let token_conflict_map = TokenConflictMap::new(lexical_grammar, following_tokens);
let coincident_token_index =
CoincidentTokenIndex::new(&parse_table, lexical_grammar, syntax_grammar.word_token);
let coincident_token_index = CoincidentTokenIndex::new(&parse_table, lexical_grammar);
let keywords = identify_keywords(
lexical_grammar,
&parse_table,
syntax_grammar.word_token,
&token_conflict_map,
&coincident_token_index,
str_pool,
);
populate_error_state(
&mut parse_table,
@ -84,10 +71,8 @@ pub fn build_tables(
&coincident_token_index,
&token_conflict_map,
&keywords,
str_pool,
);
populate_used_symbols(&mut parse_table, syntax_grammar, lexical_grammar);
let mut parse_table = ActionListPool::intern_table(parse_table);
minimize_parse_table(
&mut parse_table,
syntax_grammar,
@ -95,7 +80,6 @@ pub fn build_tables(
simple_aliases,
&token_conflict_map,
&keywords,
str_pool,
optimizations,
);
let lex_tables = build_lex_table(
@ -107,10 +91,7 @@ pub fn build_tables(
&token_conflict_map,
);
populate_external_lex_states(&mut parse_table, syntax_grammar);
mark_fragile_tokens(&mut parse_table, &token_conflict_map);
parse_table
.action_lists
.canonicalize(&mut parse_table.states);
mark_fragile_tokens(&mut parse_table, lexical_grammar, &token_conflict_map);
if let Some(report_symbol_name) = report_symbol_name {
report_state_info(
@ -118,7 +99,6 @@ pub fn build_tables(
lexical_grammar,
&parse_table,
&parse_state_info,
str_pool,
report_symbol_name,
);
}
@ -138,25 +118,28 @@ pub fn build_tables(
fn get_following_tokens(
syntax_grammar: &SyntaxGrammar,
lexical_grammar: &LexicalGrammar,
inlines: &InlinedProductionMap,
builder: &ParseItemSetBuilder,
) -> Vec<TokenSet> {
let n_terminals = lexical_grammar.variables.len();
let n_externals = syntax_grammar.external_tokens.len();
let mut result = vec![TokenSet::with_capacity(n_terminals, n_externals); n_terminals];
let mut result = vec![TokenSet::new(); lexical_grammar.variables.len()];
let productions = syntax_grammar
.variables
.iter()
.flat_map(|v| &v.productions)
.chain(&inlines.productions);
let all_tokens = (0..result.len())
.map(Symbol::terminal)
.collect::<TokenSet>();
for production in &syntax_grammar.productions {
let steps = &syntax_grammar.steps[production.step_range()];
for i in 1..steps.len() {
let left_tokens = builder.last_set(steps[i - 1].symbol());
let right_tokens = builder.first_set(steps[i].symbol());
let right_reserved_tokens = builder.reserved_first_set(steps[i].symbol());
for production in productions {
for i in 1..production.steps.len() {
let left_tokens = builder.last_set(&production.steps[i - 1].symbol);
let right_tokens = builder.first_set(&production.steps[i].symbol);
let right_reserved_tokens = builder.reserved_first_set(&production.steps[i].symbol);
for left_token in left_tokens.iter() {
if left_token.is_terminal() {
result[left_token.index as usize].insert_all_terminals(right_tokens);
result[left_token.index].insert_all_terminals(right_tokens);
if let Some(reserved_tokens) = right_reserved_tokens {
result[left_token.index as usize].insert_all_terminals(reserved_tokens);
result[left_token.index].insert_all_terminals(reserved_tokens);
}
}
}
@ -167,20 +150,19 @@ fn get_following_tokens(
for entry in &mut result {
entry.insert(*extra);
}
result[extra.index as usize] = all_tokens.clone();
result[extra.index] = all_tokens.clone();
}
}
result
}
fn populate_error_state(
parse_table: &mut ParseTable<ParseTableEntry>,
parse_table: &mut ParseTable,
syntax_grammar: &SyntaxGrammar,
lexical_grammar: &LexicalGrammar,
coincident_token_index: &CoincidentTokenIndex,
token_conflict_map: &TokenConflictMap,
keywords: &TokenSet,
str_pool: &StrPool,
) {
let state = &mut parse_table.states[0];
let n = lexical_grammar.variables.len();
@ -199,7 +181,7 @@ fn populate_error_state(
} else {
debug!(
"error recovery - token {} has no conflicts",
str_pool.resolve(lexical_grammar.variables[i].name)
lexical_grammar.variables[i].name
);
Some(Symbol::terminal(i))
}
@ -208,31 +190,31 @@ fn populate_error_state(
let recover_entry = ParseTableEntry {
reusable: false,
actions: ActionList::One(ParseAction::Recover),
actions: vec![ParseAction::Recover],
};
// Exclude from the error-recovery state any token that conflicts with one of
// the *conflict-free tokens* identified above.
for i in 0..n {
let symbol = Symbol::terminal(i);
if !conflict_free_tokens.contains(symbol)
&& !keywords.contains(symbol)
if !conflict_free_tokens.contains(&symbol)
&& !keywords.contains(&symbol)
&& syntax_grammar.word_token != Some(symbol)
&& let Some(t) = conflict_free_tokens.iter().find(|t| {
!coincident_token_index.contains(symbol, *t)
&& token_conflict_map.does_conflict(symbol.index as usize, t.index as usize)
})
{
debug!(
"error recovery - exclude token {} because of conflict with {}",
str_pool.resolve(lexical_grammar.variables[i].name),
str_pool.resolve(lexical_grammar.variables[t.index as usize].name)
);
continue;
if let Some(t) = conflict_free_tokens.iter().find(|t| {
!coincident_token_index.contains(symbol, *t)
&& token_conflict_map.does_conflict(symbol.index, t.index)
}) {
debug!(
"error recovery - exclude token {} because of conflict with {}",
lexical_grammar.variables[i].name, lexical_grammar.variables[t.index].name
);
continue;
}
}
debug!(
"error recovery - include token {}",
str_pool.resolve(lexical_grammar.variables[i].name)
lexical_grammar.variables[i].name
);
state
.terminal_entries
@ -253,7 +235,7 @@ fn populate_error_state(
}
fn populate_used_symbols(
parse_table: &mut ParseTable<ParseTableEntry>,
parse_table: &mut ParseTable,
syntax_grammar: &SyntaxGrammar,
lexical_grammar: &LexicalGrammar,
) {
@ -263,13 +245,13 @@ fn populate_used_symbols(
for state in &parse_table.states {
for symbol in state.terminal_entries.keys() {
match symbol.kind {
SymbolType::Terminal => terminal_usages[symbol.index as usize] = true,
SymbolType::External => external_usages[symbol.index as usize] = true,
SymbolType::Terminal => terminal_usages[symbol.index] = true,
SymbolType::External => external_usages[symbol.index] = true,
_ => {}
}
}
for symbol in state.nonterminal_entries.keys() {
non_terminal_usages[symbol.index as usize] = true;
non_terminal_usages[symbol.index] = true;
}
}
parse_table.symbols.push(Symbol::end());
@ -281,10 +263,7 @@ fn populate_used_symbols(
// ensure that a subtree's symbol can be successfully reassigned to the word token
// without having to move the subtree to the heap.
// See https://github.com/tree-sitter/tree-sitter/issues/258
if syntax_grammar
.word_token
.is_some_and(|t| t.index as usize == i)
{
if syntax_grammar.word_token.is_some_and(|t| t.index == i) {
parse_table.symbols.insert(1, Symbol::terminal(i));
} else {
parse_table.symbols.push(Symbol::terminal(i));
@ -304,7 +283,7 @@ fn populate_used_symbols(
}
fn populate_external_lex_states(parse_table: &mut ParseTable, syntax_grammar: &SyntaxGrammar) {
let mut external_tokens_by_corresponding_internal_token = FxHashMap::default();
let mut external_tokens_by_corresponding_internal_token = HashMap::new();
for (i, external_token) in syntax_grammar.external_tokens.iter().enumerate() {
if let Some(symbol) = external_token.corresponding_internal_token {
external_tokens_by_corresponding_internal_token.insert(symbol.index, i);
@ -320,11 +299,12 @@ fn populate_external_lex_states(parse_table: &mut ParseTable, syntax_grammar: &S
for token in parse_table.states[i].terminal_entries.keys() {
if token.is_external() {
external_tokens.insert(*token);
} else if token.is_terminal()
&& let Some(index) =
} else if token.is_terminal() {
if let Some(index) =
external_tokens_by_corresponding_internal_token.get(&token.index)
{
external_tokens.insert(Symbol::external(*index));
{
external_tokens.insert(Symbol::external(*index));
}
}
}
@ -335,16 +315,16 @@ fn populate_external_lex_states(parse_table: &mut ParseTable, syntax_grammar: &S
.unwrap_or_else(|| {
parse_table.external_lex_states.push(external_tokens);
parse_table.external_lex_states.len() - 1
}) as u32;
});
}
}
fn identify_keywords(
lexical_grammar: &LexicalGrammar,
parse_table: &ParseTable,
word_token: Option<Symbol>,
token_conflict_map: &TokenConflictMap,
coincident_token_index: &CoincidentTokenIndex,
str_pool: &StrPool,
) -> TokenSet {
if word_token.is_none() {
return TokenSet::new();
@ -362,12 +342,12 @@ fn identify_keywords(
.filter_map(|(i, variable)| {
cursor.reset(vec![variable.start_state]);
if all_chars_are_alphabetical(&cursor)
&& token_conflict_map.does_match_same_string(i, word_token.index as usize)
&& !token_conflict_map.does_match_different_string(i, word_token.index as usize)
&& token_conflict_map.does_match_same_string(i, word_token.index)
&& !token_conflict_map.does_match_different_string(i, word_token.index)
{
debug!(
"Keywords - add candidate {}",
str_pool.resolve(lexical_grammar.variables[i].name)
lexical_grammar.variables[i].name
);
Some(Symbol::terminal(i))
} else {
@ -382,14 +362,12 @@ fn identify_keywords(
.filter(|token| {
for other_token in keyword_candidates.iter() {
if other_token != *token
&& token_conflict_map
.does_match_same_string(other_token.index as usize, token.index as usize)
&& token_conflict_map.does_match_same_string(other_token.index, token.index)
{
debug!(
"Keywords - exclude {} because it matches the same string as {}",
str_pool.resolve(lexical_grammar.variables[token.index as usize].name),
str_pool
.resolve(lexical_grammar.variables[other_token.index as usize].name)
lexical_grammar.variables[token.index].name,
lexical_grammar.variables[other_token.index].name
);
return false;
}
@ -400,12 +378,11 @@ fn identify_keywords(
// Exclude keyword candidates for which substituting the keyword capture
// token would introduce new lexical conflicts with other tokens.
keywords
let keywords = keywords
.iter()
.filter(|token| {
for other_index in 0..lexical_grammar.variables.len() {
if keyword_candidates.contains(Symbol::terminal(other_index)) {
if keyword_candidates.contains(&Symbol::terminal(other_index)) {
continue;
}
@ -413,20 +390,26 @@ fn identify_keywords(
// this keyword candidate, then substituting the word token won't
// introduce any new lexical conflicts.
if coincident_token_index
.all_coincident_states_have_word(*token, Symbol::terminal(other_index))
.states_with(*token, Symbol::terminal(other_index))
.iter()
.all(|state_id| {
parse_table.states[*state_id]
.terminal_entries
.contains_key(&word_token)
})
{
continue;
}
if !token_conflict_map.has_same_conflict_status(
token.index as usize,
word_token.index as usize,
token.index,
word_token.index,
other_index,
) {
debug!(
"Keywords - exclude {} because of conflict with {}",
str_pool.resolve(lexical_grammar.variables[token.index as usize].name),
str_pool.resolve(lexical_grammar.variables[other_index].name)
lexical_grammar.variables[token.index].name,
lexical_grammar.variables[other_index].name
);
return false;
}
@ -434,27 +417,35 @@ fn identify_keywords(
debug!(
"Keywords - include {}",
str_pool.resolve(lexical_grammar.variables[token.index as usize].name),
lexical_grammar.variables[token.index].name,
);
true
})
.collect()
.collect();
keywords
}
fn mark_fragile_tokens(parse_table: &mut ParseTable, token_conflict_map: &TokenConflictMap) {
let mut valid_terminal_indices = Vec::new();
fn mark_fragile_tokens(
parse_table: &mut ParseTable,
lexical_grammar: &LexicalGrammar,
token_conflict_map: &TokenConflictMap,
) {
let n = lexical_grammar.variables.len();
let mut valid_tokens_mask = Vec::with_capacity(n);
for state in &mut parse_table.states {
valid_terminal_indices.clear();
valid_tokens_mask.clear();
valid_tokens_mask.resize(n, false);
for token in state.terminal_entries.keys() {
if token.is_terminal() {
valid_terminal_indices.push(token.index);
valid_tokens_mask[token.index] = true;
}
}
for (token, id) in &mut state.terminal_entries {
for (token, entry) in &mut state.terminal_entries {
if token.is_terminal() {
for &i in &valid_terminal_indices {
if token_conflict_map.does_overlap(i as usize, token.index as usize) {
id.set_reusable(false);
for (i, is_valid) in valid_tokens_mask.iter().enumerate() {
if *is_valid && token_conflict_map.does_overlap(i, token.index) {
entry.reusable = false;
break;
}
}
@ -467,8 +458,7 @@ fn report_state_info<'a>(
syntax_grammar: &SyntaxGrammar,
lexical_grammar: &LexicalGrammar,
parse_table: &ParseTable,
parse_state_info: &ParseStateInfo<'a>,
str_pool: &StrPool,
parse_state_info: &[ParseStateInfo<'a>],
report_symbol_name: &'a str,
) {
let mut all_state_indices = BTreeSet::new();
@ -478,8 +468,8 @@ fn report_state_info<'a>(
for (i, state) in parse_table.states.iter().enumerate() {
all_state_indices.insert(i);
let item_set = parse_state_info.item_set(state.id);
for entry in &item_set.entries {
let item_set = &parse_state_info[state.id];
for entry in &item_set.1.entries {
if !entry.item.is_augmented() {
symbols_with_state_indices[entry.item.variable_index as usize]
.1
@ -493,13 +483,13 @@ fn report_state_info<'a>(
let max_symbol_name_length = syntax_grammar
.variables
.iter()
.map(|v| str_pool.resolve(v.name).len())
.map(|v| v.name.len())
.max()
.unwrap();
for (symbol, states) in &symbols_with_state_indices {
info!(
"{:width$}\t{}",
str_pool.resolve(syntax_grammar.variables[symbol.index as usize].name),
syntax_grammar.variables[symbol.index].name,
states.len(),
width = max_symbol_name_length
);
@ -512,9 +502,7 @@ fn report_state_info<'a>(
symbols_with_state_indices
.iter()
.find_map(|(symbol, state_indices)| {
if str_pool.resolve(syntax_grammar.variables[symbol.index as usize].name)
== report_symbol_name
{
if syntax_grammar.variables[symbol.index].name == report_symbol_name {
Some(state_indices)
} else {
None
@ -528,8 +516,7 @@ fn report_state_info<'a>(
for state_index in state_indices {
let id = parse_table.states[state_index].id;
let preceding_symbols = &parse_state_info.preceding_symbols_by_id[id as usize];
let item_set = parse_state_info.item_set(id);
let (preceding_symbols, item_set) = &parse_state_info[id];
info!("state index: {state_index}");
info!("state id: {id}");
info!(
@ -538,12 +525,11 @@ fn report_state_info<'a>(
.iter()
.map(|symbol| {
if symbol.is_terminal() {
str_pool.resolve(lexical_grammar.variables[symbol.index as usize].name)
lexical_grammar.variables[symbol.index].name.clone()
} else if symbol.is_external() {
str_pool
.resolve(syntax_grammar.external_tokens[symbol.index as usize].name)
syntax_grammar.external_tokens[symbol.index].name.clone()
} else {
str_pool.resolve(syntax_grammar.variables[symbol.index as usize].name)
syntax_grammar.variables[symbol.index].name.clone()
}
})
.collect::<Vec<_>>()
@ -551,13 +537,7 @@ fn report_state_info<'a>(
);
info!(
"\nitems:\n{}",
item::ParseItemSetDisplay(
item_set,
syntax_grammar,
lexical_grammar,
str_pool,
&parse_state_info.lookaheads
),
item::ParseItemSetDisplay(item_set, syntax_grammar, lexical_grammar),
);
}
}

View file

@ -1,10 +1,8 @@
use std::{
collections::{VecDeque, hash_map::Entry},
collections::{hash_map::Entry, HashMap, VecDeque},
mem,
};
use rustc_hash::FxHashMap;
use log::debug;
use super::{coincident_tokens::CoincidentTokenIndex, token_conflicts::TokenConflictMap};
@ -13,7 +11,7 @@ use crate::{
grammars::{LexicalGrammar, SyntaxGrammar},
nfa::{CharacterSet, NfaCursor},
rules::{Symbol, TokenSet},
tables::{AdvanceAction, LexState, LexStateId, LexTable, ParseStateId, ParseTable},
tables::{AdvanceAction, LexState, LexTable, ParseStateId, ParseTable},
};
pub const LARGE_CHARACTER_RANGE_COUNT: usize = 8;
@ -49,7 +47,7 @@ pub fn build_lex_table(
.chain(state.reserved_words.iter())
.filter_map(|token| {
if token.is_terminal() {
if keywords.contains(token) {
if keywords.contains(&token) {
syntax_grammar.word_token
} else {
Some(token)
@ -67,17 +65,18 @@ pub fn build_lex_table(
if merge_token_set(
&mut entry.0,
&tokens,
lexical_grammar,
token_conflict_map,
coincident_token_index,
) {
did_merge = true;
entry.1.push(i as u32);
entry.1.push(i);
break;
}
}
if !did_merge {
parse_state_ids_by_token_set.push((tokens, vec![i as u32]));
parse_state_ids_by_token_set.push((tokens, vec![i]));
}
}
@ -85,7 +84,7 @@ pub fn build_lex_table(
for (tokens, parse_state_ids) in parse_state_ids_by_token_set {
let lex_state_id = builder.add_state_for_tokens(&tokens);
for id in parse_state_ids {
parse_table.states[id as usize].lex_state_id = lex_state_id;
parse_table.states[id].lex_state_id = lex_state_id;
}
}
@ -131,7 +130,7 @@ pub fn build_lex_table(
}
struct QueueEntry {
state_id: LexStateId,
state_id: usize,
nfa_states: Vec<u32>,
eof_valid: bool,
}
@ -141,7 +140,7 @@ struct LexTableBuilder<'a> {
cursor: NfaCursor<'a>,
table: LexTable,
state_queue: VecDeque<QueueEntry>,
state_ids_by_nfa_state_set: FxHashMap<(Vec<u32>, bool), LexStateId>,
state_ids_by_nfa_state_set: HashMap<(Vec<u32>, bool), usize>,
}
impl<'a> LexTableBuilder<'a> {
@ -151,7 +150,7 @@ impl<'a> LexTableBuilder<'a> {
cursor: NfaCursor::new(&lexical_grammar.nfa, vec![]),
table: LexTable::default(),
state_queue: VecDeque::new(),
state_ids_by_nfa_state_set: FxHashMap::default(),
state_ids_by_nfa_state_set: HashMap::new(),
}
}
@ -161,13 +160,13 @@ impl<'a> LexTableBuilder<'a> {
self.state_ids_by_nfa_state_set.clear();
}
fn add_state_for_tokens(&mut self, tokens: &TokenSet) -> LexStateId {
fn add_state_for_tokens(&mut self, tokens: &TokenSet) -> usize {
let mut eof_valid = false;
let nfa_states = tokens
.iter()
.filter_map(|token| {
if token.is_terminal() {
Some(self.lexical_grammar.variables[token.index as usize].start_state)
Some(self.lexical_grammar.variables[token.index].start_state)
} else {
eof_valid = true;
None
@ -181,7 +180,7 @@ impl<'a> LexTableBuilder<'a> {
"entry point state: {state_id}, tokens: {:?}",
tokens
.iter()
.map(|t| &self.lexical_grammar.variables[t.index as usize].name)
.map(|t| &self.lexical_grammar.variables[t.index].name)
.collect::<Vec<_>>()
);
}
@ -197,7 +196,7 @@ impl<'a> LexTableBuilder<'a> {
state_id
}
fn add_state(&mut self, nfa_states: Vec<u32>, eof_valid: bool) -> (LexStateId, bool) {
fn add_state(&mut self, nfa_states: Vec<u32>, eof_valid: bool) -> (usize, bool) {
self.cursor.reset(nfa_states);
match self
.state_ids_by_nfa_state_set
@ -205,7 +204,7 @@ impl<'a> LexTableBuilder<'a> {
{
Entry::Occupied(o) => (*o.get(), false),
Entry::Vacant(v) => {
let state_id = self.table.states.len() as u32;
let state_id = self.table.states.len();
self.table.states.push(LexState::default());
self.state_queue.push_back(QueueEntry {
state_id,
@ -218,52 +217,53 @@ impl<'a> LexTableBuilder<'a> {
}
}
fn populate_state(&mut self, state_id: LexStateId, nfa_states: Vec<u32>, eof_valid: bool) {
fn populate_state(&mut self, state_id: usize, nfa_states: Vec<u32>, eof_valid: bool) {
self.cursor.force_reset(nfa_states);
// The EOF state is represented as an empty list of NFA states.
let mut completion = None;
for (id, prec) in self.cursor.completions() {
if let Some((prev_id, prev_precedence)) = completion
&& TokenConflictMap::prefer_token(
if let Some((prev_id, prev_precedence)) = completion {
if TokenConflictMap::prefer_token(
self.lexical_grammar,
(prev_precedence, prev_id),
(prec, id),
)
{
continue;
) {
continue;
}
}
completion = Some((id, prec));
}
let (transitions, has_sep) = self.cursor.transitions_and_any_sep();
let transitions = self.cursor.transitions();
let has_sep = self.cursor.transition_chars().any(|(_, sep)| sep);
// If EOF is a valid lookahead token, add a transition predicated on the null
// character that leads to the empty set of NFA states.
if eof_valid {
let (next_state_id, _) = self.add_state(Vec::new(), false);
self.table.states[state_id as usize].eof_action = Some(AdvanceAction {
self.table.states[state_id].eof_action = Some(AdvanceAction {
state: next_state_id,
in_main_token: true,
});
}
for transition in transitions {
if let Some((completed_id, completed_precedence)) = completion
&& !TokenConflictMap::prefer_transition(
if let Some((completed_id, completed_precedence)) = completion {
if !TokenConflictMap::prefer_transition(
self.lexical_grammar,
&transition,
completed_id,
completed_precedence,
has_sep,
)
{
continue;
) {
continue;
}
}
let (next_state_id, _) =
self.add_state(transition.states, eof_valid && transition.is_separator);
self.table.states[state_id as usize].advance_actions.push((
self.table.states[state_id].advance_actions.push((
transition.characters,
AdvanceAction {
state: next_state_id,
@ -273,78 +273,41 @@ impl<'a> LexTableBuilder<'a> {
}
if let Some((complete_id, _)) = completion {
self.table.states[state_id as usize].accept_action =
Some(Symbol::terminal(complete_id));
self.table.states[state_id].accept_action = Some(Symbol::terminal(complete_id));
} else if self.cursor.state_ids.is_empty() {
self.table.states[state_id as usize].accept_action = Some(Symbol::end());
self.table.states[state_id].accept_action = Some(Symbol::end());
}
}
}
fn check_token_conflicts(
i: usize,
set_without_terminal: &TokenSet,
token_conflict_map: &TokenConflictMap,
coincident_token_index: &CoincidentTokenIndex,
) -> bool {
let wpr = token_conflict_map.row_words;
let row_start = i * wpr;
let set_bits = set_without_terminal.terminal_bits_words();
// Does terminal i conflict with or match-prefix any terminal in the set?
let conflict_row = &token_conflict_map.conflict_or_prefix_bits[row_start..row_start + wpr];
for (&c, &s) in conflict_row.iter().zip(set_bits) {
if c & s != 0 {
return true;
}
}
// Does terminal i overlap (in either direction) with any non-coincident terminal in the set?
let overlap_row = &token_conflict_map.overlap_either_bits[row_start..row_start + wpr];
let coincident_row = &coincident_token_index.row_bits[row_start..row_start + wpr];
for ((&o, &s), &c) in overlap_row.iter().zip(set_bits).zip(coincident_row) {
if o & s & !c != 0 {
return true;
}
}
false
}
fn merge_token_set(
tokens: &mut TokenSet,
other: &TokenSet,
lexical_grammar: &LexicalGrammar,
token_conflict_map: &TokenConflictMap,
coincident_token_index: &CoincidentTokenIndex,
) -> bool {
if tokens
.terminals()
.filter(|terminal| !other.contains_terminal(terminal.index as usize))
.any(|terminal| {
check_token_conflicts(
terminal.index as usize,
other,
token_conflict_map,
coincident_token_index,
)
})
{
return false;
}
for i in 0..lexical_grammar.variables.len() {
let symbol = Symbol::terminal(i);
let set_without_terminal = match (tokens.contains_terminal(i), other.contains_terminal(i)) {
(true, false) => other,
(false, true) => tokens,
_ => continue,
};
if other
.terminals()
.filter(|terminal| !tokens.contains_terminal(terminal.index as usize))
.any(|terminal| {
check_token_conflicts(
terminal.index as usize,
tokens,
token_conflict_map,
coincident_token_index,
)
})
{
return false;
for existing_token in set_without_terminal.terminals() {
if token_conflict_map.does_conflict(i, existing_token.index)
|| token_conflict_map.does_match_prefix(i, existing_token.index)
{
return false;
}
if !coincident_token_index.contains(symbol, existing_token)
&& (token_conflict_map.does_overlap(existing_token.index, i)
|| token_conflict_map.does_overlap(i, existing_token.index))
{
return false;
}
}
}
tokens.insert_all(other);
@ -354,7 +317,7 @@ fn merge_token_set(
fn minimize_lex_table(table: &mut LexTable, parse_table: &mut ParseTable) {
// Initially group the states by their accept action and their
// valid lookahead characters.
let mut state_ids_by_signature = FxHashMap::default();
let mut state_ids_by_signature = HashMap::new();
for (i, state) in table.states.iter().enumerate() {
let signature = (
i == 0,
@ -369,7 +332,7 @@ fn minimize_lex_table(table: &mut LexTable, parse_table: &mut ParseTable) {
state_ids_by_signature
.entry(signature)
.or_insert(Vec::new())
.push(i as u32);
.push(i);
}
let mut state_ids_by_group_id = state_ids_by_signature
.into_iter()
@ -382,10 +345,10 @@ fn minimize_lex_table(table: &mut LexTable, parse_table: &mut ParseTable) {
.unwrap();
state_ids_by_group_id.swap(error_group_index, 0);
let mut group_ids_by_state_id = vec![0u32; table.states.len()];
let mut group_ids_by_state_id = vec![0; table.states.len()];
for (group_id, state_ids) in state_ids_by_group_id.iter().enumerate() {
for state_id in state_ids {
group_ids_by_state_id[*state_id as usize] = group_id as u32;
group_ids_by_state_id[*state_id] = group_id;
}
}
@ -400,35 +363,30 @@ fn minimize_lex_table(table: &mut LexTable, parse_table: &mut ParseTable) {
let mut new_states = Vec::with_capacity(state_ids_by_group_id.len());
for state_ids in &state_ids_by_group_id {
let mut new_state = LexState::default();
mem::swap(&mut new_state, &mut table.states[state_ids[0] as usize]);
mem::swap(&mut new_state, &mut table.states[state_ids[0]]);
for (_, advance_action) in &mut new_state.advance_actions {
advance_action.state = group_ids_by_state_id[advance_action.state as usize];
advance_action.state = group_ids_by_state_id[advance_action.state];
}
if let Some(eof_action) = &mut new_state.eof_action {
eof_action.state = group_ids_by_state_id[eof_action.state as usize];
eof_action.state = group_ids_by_state_id[eof_action.state];
}
new_states.push(new_state);
}
for state in &mut parse_table.states {
state.lex_state_id = group_ids_by_state_id[state.lex_state_id as usize];
state.lex_state_id = group_ids_by_state_id[state.lex_state_id];
}
table.states = new_states;
}
fn lex_states_differ(
left: &LexState,
right: &LexState,
group_ids_by_state_id: &[LexStateId],
) -> bool {
fn lex_states_differ(left: &LexState, right: &LexState, group_ids_by_state_id: &[usize]) -> bool {
left.advance_actions
.iter()
.zip(right.advance_actions.iter())
.any(|(left, right)| {
group_ids_by_state_id[left.1.state as usize]
!= group_ids_by_state_id[right.1.state as usize]
group_ids_by_state_id[left.1.state] != group_ids_by_state_id[right.1.state]
})
}
@ -438,9 +396,9 @@ fn sort_states(table: &mut LexTable, parse_table: &mut ParseTable) {
old_ids_by_new_id[1..].sort_by_key(|id| &table.states[*id]);
// Get the inverse mapping
let mut new_ids_by_old_id = vec![0u32; old_ids_by_new_id.len()];
let mut new_ids_by_old_id = vec![0; old_ids_by_new_id.len()];
for (id, old_id) in old_ids_by_new_id.iter().enumerate() {
new_ids_by_old_id[*old_id] = id as u32;
new_ids_by_old_id[*old_id] = id;
}
// Reorder the parse states and update their references to reflect
@ -451,10 +409,10 @@ fn sort_states(table: &mut LexTable, parse_table: &mut ParseTable) {
let mut state = LexState::default();
mem::swap(&mut state, &mut table.states[*old_id]);
for (_, advance_action) in &mut state.advance_actions {
advance_action.state = new_ids_by_old_id[advance_action.state as usize];
advance_action.state = new_ids_by_old_id[advance_action.state];
}
if let Some(eof_action) = &mut state.eof_action {
eof_action.state = new_ids_by_old_id[eof_action.state as usize];
eof_action.state = new_ids_by_old_id[eof_action.state];
}
state
})
@ -462,6 +420,6 @@ fn sort_states(table: &mut LexTable, parse_table: &mut ParseTable) {
// Update the parse table's lex state references
for state in &mut parse_table.states {
state.lex_state_id = new_ids_by_old_id[state.lex_state_id as usize];
state.lex_state_id = new_ids_by_old_id[state.lex_state_id];
}
}

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