tree_haver 5.0.4 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/lib/tree_haver/backend_context.rb +28 -0
  4. data/lib/tree_haver/backend_registry.rb +19 -432
  5. data/lib/tree_haver/contracts.rb +460 -0
  6. data/lib/tree_haver/kaitai_backend.rb +30 -0
  7. data/lib/tree_haver/language_pack.rb +190 -0
  8. data/lib/tree_haver/peg_backends.rb +76 -0
  9. data/lib/tree_haver/version.rb +1 -12
  10. data/lib/tree_haver.rb +7 -1316
  11. data.tar.gz.sig +0 -0
  12. metadata +34 -245
  13. metadata.gz.sig +0 -0
  14. data/CHANGELOG.md +0 -1366
  15. data/CITATION.cff +0 -20
  16. data/CODE_OF_CONDUCT.md +0 -134
  17. data/CONTRIBUTING.md +0 -359
  18. data/FUNDING.md +0 -74
  19. data/LICENSE.txt +0 -21
  20. data/README.md +0 -2347
  21. data/REEK +0 -0
  22. data/RUBOCOP.md +0 -71
  23. data/SECURITY.md +0 -21
  24. data/lib/tree_haver/backend_api.rb +0 -349
  25. data/lib/tree_haver/backends/citrus.rb +0 -487
  26. data/lib/tree_haver/backends/ffi.rb +0 -1009
  27. data/lib/tree_haver/backends/java.rb +0 -893
  28. data/lib/tree_haver/backends/mri.rb +0 -362
  29. data/lib/tree_haver/backends/parslet.rb +0 -560
  30. data/lib/tree_haver/backends/prism.rb +0 -471
  31. data/lib/tree_haver/backends/psych.rb +0 -375
  32. data/lib/tree_haver/backends/rust.rb +0 -239
  33. data/lib/tree_haver/base/language.rb +0 -98
  34. data/lib/tree_haver/base/node.rb +0 -322
  35. data/lib/tree_haver/base/parser.rb +0 -24
  36. data/lib/tree_haver/base/point.rb +0 -48
  37. data/lib/tree_haver/base/tree.rb +0 -128
  38. data/lib/tree_haver/base.rb +0 -12
  39. data/lib/tree_haver/citrus_grammar_finder.rb +0 -218
  40. data/lib/tree_haver/compat.rb +0 -43
  41. data/lib/tree_haver/grammar_finder.rb +0 -374
  42. data/lib/tree_haver/language.rb +0 -295
  43. data/lib/tree_haver/language_registry.rb +0 -190
  44. data/lib/tree_haver/library_path_utils.rb +0 -80
  45. data/lib/tree_haver/node.rb +0 -579
  46. data/lib/tree_haver/parser.rb +0 -438
  47. data/lib/tree_haver/parslet_grammar_finder.rb +0 -224
  48. data/lib/tree_haver/path_validator.rb +0 -353
  49. data/lib/tree_haver/point.rb +0 -27
  50. data/lib/tree_haver/rspec/dependency_tags.rb +0 -1392
  51. data/lib/tree_haver/rspec/testable_node.rb +0 -217
  52. data/lib/tree_haver/rspec.rb +0 -33
  53. data/lib/tree_haver/tree.rb +0 -258
  54. data/sig/tree_haver/backends.rbs +0 -352
  55. data/sig/tree_haver/grammar_finder.rbs +0 -29
  56. data/sig/tree_haver/path_validator.rbs +0 -32
  57. data/sig/tree_haver.rbs +0 -234
data/README.md DELETED
@@ -1,2347 +0,0 @@
1
- | 📍 NOTE |
2
- |---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
3
- | RubyGems (the [GitHub org][rubygems-org], not the website) [suffered][draper-security] a [hostile takeover][ellen-takeover] in September 2025. |
4
- | Ultimately [4 maintainers][simi-removed] were [hard removed][martin-removed] and a reason has been given for only 1 of those, while 2 others resigned in protest. |
5
- | It is a [complicated story][draper-takeover] which is difficult to [parse quickly][draper-lies]. |
6
- | Simply put - there was active policy for adding or removing maintainers/owners of [rubygems][rubygems-maint-policy] and [bundler][bundler-maint-policy], and those [policies were not followed][policy-fail]. |
7
- | I'm adding notes like this to gems because I [don't condone theft][draper-theft] of repositories or gems from their rightful owners. |
8
- | If a similar theft happened with my repos/gems, I'd hope some would stand up for me. |
9
- | Disenfranchised former-maintainers have started [gem.coop][gem-coop]. |
10
- | Once available I will publish there exclusively; unless RubyCentral makes amends with the community. |
11
- | The ["Technology for Humans: Joel Draper"][reinteractive-podcast] podcast episode by [reinteractive][reinteractive] is the most cogent summary I'm aware of. |
12
- | See [here][gem-naming], [here][gem-coop] and [here][martin-ann] for more info on what comes next. |
13
- | What I'm doing: A (WIP) proposal for [bundler/gem scopes][gem-scopes], and a (WIP) proposal for a federated [gem server][gem-server]. |
14
-
15
- [rubygems-org]: https://github.com/rubygems/
16
- [draper-security]: https://joel.drapper.me/p/ruby-central-security-measures/
17
- [draper-takeover]: https://joel.drapper.me/p/ruby-central-takeover/
18
- [ellen-takeover]: https://pup-e.com/blog/goodbye-rubygems/
19
- [simi-removed]: https://www.reddit.com/r/ruby/s/gOk42POCaV
20
- [martin-removed]: https://bsky.app/profile/martinemde.com/post/3m3occezxxs2q
21
- [draper-lies]: https://joel.drapper.me/p/ruby-central-fact-check/
22
- [draper-theft]: https://joel.drapper.me/p/ruby-central/
23
- [reinteractive]: https://reinteractive.com/ruby-on-rails
24
- [gem-coop]: https://gem.coop
25
- [gem-naming]: https://github.com/gem-coop/gem.coop/issues/12
26
- [martin-ann]: https://martinemde.com/2025/10/05/announcing-gem-coop.html
27
- [gem-scopes]: https://github.com/galtzo-floss/bundle-namespace
28
- [gem-server]: https://github.com/galtzo-floss/gem-server
29
- [reinteractive-podcast]: https://youtu.be/_H4qbtC5qzU?si=BvuBU90R2wAqD2E6
30
- [bundler-maint-policy]: https://github.com/ruby/rubygems/blob/b1ab33a3d52310a84d16b193991af07f5a6a07c0/doc/bundler/playbooks/TEAM_CHANGES.md
31
- [rubygems-maint-policy]: https://github.com/ruby/rubygems/blob/b1ab33a3d52310a84d16b193991af07f5a6a07c0/doc/rubygems/POLICIES.md?plain=1#L187-L196
32
- [policy-fail]: https://www.reddit.com/r/ruby/comments/1ove9vp/rubycentral_hates_this_one_fact/
33
-
34
- [![Galtzo FLOSS Logo by Aboling0, CC BY-SA 4.0][🖼️galtzo-i]][🖼️galtzo-discord] [![ruby-lang Logo, Yukihiro Matsumoto, Ruby Visual Identity Team, CC BY-SA 2.5][🖼️ruby-lang-i]][🖼️ruby-lang] [![kettle-rb Logo by Aboling0, CC BY-SA 4.0][🖼️kettle-rb-i]][🖼️kettle-rb]
35
-
36
- [🖼️galtzo-i]: https://logos.galtzo.com/assets/images/galtzo-floss/avatar-192px.svg
37
- [🖼️galtzo-discord]: https://discord.gg/3qme4XHNKN
38
- [🖼️ruby-lang-i]: https://logos.galtzo.com/assets/images/ruby-lang/avatar-192px.svg
39
- [🖼️ruby-lang]: https://www.ruby-lang.org/
40
- [🖼️kettle-rb-i]: https://logos.galtzo.com/assets/images/kettle-rb/avatar-192px.svg
41
- [🖼️kettle-rb]: https://github.com/kettle-rb
42
-
43
- # 🌴 TreeHaver
44
-
45
- [![Version][👽versioni]][👽dl-rank] [![GitHub tag (latest SemVer)][⛳️tag-img]][⛳️tag] [![License: MIT][📄license-img]][📄license-ref] [![Downloads Rank][👽dl-ranki]][👽dl-rank] [![Open Source Helpers][👽oss-helpi]][👽oss-help] [![CodeCov Test Coverage][🏀codecovi]][🏀codecov] [![Coveralls Test Coverage][🏀coveralls-img]][🏀coveralls] [![QLTY Test Coverage][🏀qlty-covi]][🏀qlty-cov] [![QLTY Maintainability][🏀qlty-mnti]][🏀qlty-mnt] [![CI Heads][🚎3-hd-wfi]][🚎3-hd-wf] [![CI Runtime Dependencies @ HEAD][🚎12-crh-wfi]][🚎12-crh-wf] [![CI Current][🚎11-c-wfi]][🚎11-c-wf] [![CI Truffle Ruby][🚎9-t-wfi]][🚎9-t-wf] [![Deps Locked][🚎13-🔒️-wfi]][🚎13-🔒️-wf] [![Deps Unlocked][🚎14-🔓️-wfi]][🚎14-🔓️-wf] [![CI Supported][🚎6-s-wfi]][🚎6-s-wf] [![CI Test Coverage][🚎2-cov-wfi]][🚎2-cov-wf] [![CI Style][🚎5-st-wfi]][🚎5-st-wf] [![CodeQL][🖐codeQL-img]][🖐codeQL] [![Apache SkyWalking Eyes License Compatibility Check][🚎15-🪪-wfi]][🚎15-🪪-wf]
46
-
47
- `if ci_badges.map(&:color).detect { it != "green"}` ☝️ [let me know][🖼️galtzo-discord], as I may have missed the [discord notification][🖼️galtzo-discord].
48
-
49
- -----
50
-
51
- `if ci_badges.map(&:color).all? { it == "green"}` 👇️ send money so I can do more of this. FLOSS maintenance is now my full-time job.
52
-
53
- [![OpenCollective Backers][🖇osc-backers-i]][🖇osc-backers] [![OpenCollective Sponsors][🖇osc-sponsors-i]][🖇osc-sponsors] [![Sponsor Me on Github][🖇sponsor-img]][🖇sponsor] [![Liberapay Goal Progress][⛳liberapay-img]][⛳liberapay] [![Donate on PayPal][🖇paypal-img]][🖇paypal] [![Buy me a coffee][🖇buyme-small-img]][🖇buyme] [![Donate on Polar][🖇polar-img]][🖇polar] [![Donate at ko-fi.com][🖇kofi-img]][🖇kofi]
54
-
55
- ## 🌻 Synopsis
56
-
57
- TreeHaver is a cross-Ruby adapter for the [tree-sitter](https://tree-sitter.github.io/tree-sitter/), [Citrus][citrus], and [Parslet][parslet] parsing libraries and other dedicated parsing tools that works seamlessly across MRI Ruby, JRuby, and TruffleRuby. It provides a unified API for parsing source code using grammars, regardless of your Ruby implementation.
58
-
59
- ### The Adapter Pattern: Like Faraday, but for Parsing
60
-
61
- If you've used [Faraday](https://github.com/lostisland/faraday), [multi\_json](https://github.com/intridea/multi_json), or [multi\_xml](https://github.com/sferik/multi_xml), you'll feel right at home with TreeHaver. These gems share a common philosophy:
62
-
63
- | Gem | Unified API for | Backend Examples |
64
- |-----------------|-----------------|---------------------------------------------------------------------------|
65
- | **Faraday** | HTTP requests | Net::HTTP, Typhoeus, Patron, Excon |
66
- | **multi\_json** | JSON parsing | Oj, Yajl, JSON gem |
67
- | **multi\_xml** | XML parsing | Nokogiri, LibXML, Ox |
68
- | **TreeHaver** | Code parsing | MRI, Rust, FFI, Java, Prism, Psych, Commonmarker, Markly, Citrus, Parslet |
69
-
70
- **Learn once, write anywhere.**
71
-
72
- **Write once, run anywhere.**
73
-
74
- Just as Faraday lets you swap HTTP adapters without changing your code, TreeHaver lets you swap tree-sitter backends. Your parsing code remains the same whether you're running on MRI with native C extensions, JRuby with FFI, or TruffleRuby.
75
-
76
- ```ruby
77
- # Your code stays the same regardless of backend
78
- parser = TreeHaver::Parser.new
79
- parser.language = TreeHaver::Language.from_library("/path/to/grammar.so")
80
- tree = parser.parse(source_code)
81
-
82
- # TreeHaver automatically picks the best available backend:
83
- # - MRI: ruby_tree_sitter, tree_stump, ffi, prism, psych, commonmarker, markly, citrus, parslet
84
- # - JRuby: ffi, java-tree-sitter (not a gem, but the jtreesitter maven package), prism, psych, commonmarker, markly, citrus, parslet
85
- # - TruffleRuby: prism, psych, commonmarker, markly, citrus, parslet
86
- # (tree-sitter backends don't work on Truffleruby with ffi gem due to FFI STRUCT_BY_VALUE limitation)
87
- ```
88
-
89
- ### Key Features
90
-
91
- - **Universal Ruby Support**: Works on MRI Ruby, JRuby, and TruffleRuby
92
- - **10 Parsing Backends** - Choose the right backend for your needs:
93
- - **Tree-sitter Backends** (high-performance, incremental parsing):
94
- - **MRI Backend**: Leverages [`ruby_tree_sitter`][ruby_tree_sitter] gem (C extension, fastest on MRI)
95
- - **Note**: `ruby_tree_sitter` currently requires unreleased fixes in the `pboling` fork, `tree_haver` branch.
96
- - **Rust Backend**: Uses [`tree_stump`][tree_stump] gem (Rust with precompiled binaries)
97
- - **Note**: `tree_stump` currently requires unreleased fixes in the `pboling` fork, `tree_haver` branch.
98
- - **FFI Backend**: Pure Ruby FFI bindings to `libtree-sitter` (JRuby only; TruffleRuby's FFI doesn't support tree-sitter's struct-by-value returns)
99
- - **Java Backend**: Native Java integration for JRuby with [`java-tree-sitter`](https://github.com/tree-sitter/java-tree-sitter) / [`jtreesitter`][jtreesitter] grammar JARs
100
- - **Language-Specific Backends** (native parser integration):
101
- - **Prism Backend**: Ruby's official parser ([Prism][prism], stdlib in Ruby 3.4+)
102
- - **Psych Backend**: Ruby's YAML parser ([Psych][psych], stdlib)
103
- - **Commonmarker Backend**: Fast Markdown parser ([Commonmarker][commonmarker], comrak Rust)
104
- - **Markly Backend**: GitHub Flavored Markdown ([Markly][markly], cmark-gfm C)
105
- - **Pure Ruby Fallback**:
106
- - **Citrus Backend**: Pure Ruby PEG parsing via [`citrus`][citrus] (no native dependencies)
107
- - **Parslet Backend**: Pure Ruby PEG parsing via [`parslet`][parslet] (no native dependencies)
108
- - **Automatic Backend Selection**: Intelligently selects the best backend for your Ruby implementation
109
- - **Language Agnostic**: Parse any language - Ruby, Markdown, YAML, JSON, Bash, TOML, JavaScript, etc.
110
- - **Grammar Discovery**: Built-in `GrammarFinder` utility for platform-aware grammar library discovery
111
- - **Unified Position API**: Consistent `start_line`, `end_line`, `source_position` across all backends
112
- - **Thread-Safe**: Built-in language registry with thread-safe caching
113
- - **Minimal API Surface**: Simple, focused API that covers the most common use cases
114
-
115
- ### Backend Requirements
116
-
117
- TreeHaver has minimal dependencies and automatically selects the best backend for your Ruby implementation. Each backend has specific version requirements:
118
-
119
- #### MRI Backend (ruby\_tree\_sitter, C extensions)
120
-
121
- **Requires `ruby_tree_sitter` v2.0+**
122
-
123
- In ruby\_tree\_sitter v2.0, all TreeSitter exceptions were changed to inherit from `Exception` (not `StandardError`). This was an intentional breaking change made for thread-safety and signal handling reasons.
124
-
125
- **Exception Mapping**: TreeHaver catches `TreeSitter::TreeSitterError` and its subclasses, converting them to `TreeHaver::NotAvailable` while preserving the original error message. This provides a consistent exception API across all backends:
126
-
127
- | ruby\_tree\_sitter Exception | TreeHaver Exception | When It Occurs |
128
- |-----------------------------------|---------------------------|----------------------------------------------|
129
- | `TreeSitter::ParserNotFoundError` | `TreeHaver::NotAvailable` | Parser library file cannot be loaded |
130
- | `TreeSitter::LanguageLoadError` | `TreeHaver::NotAvailable` | Language symbol loads but returns nothing |
131
- | `TreeSitter::SymbolNotFoundError` | `TreeHaver::NotAvailable` | Symbol not found in library |
132
- | `TreeSitter::ParserVersionError` | `TreeHaver::NotAvailable` | Parser version incompatible with tree-sitter |
133
- | `TreeSitter::QueryCreationError` | `TreeHaver::NotAvailable` | Query creation fails |
134
-
135
- ```ruby
136
- # MRI tree-sitter Backend
137
- gem "ruby_tree_sitter",
138
- github: "pboling/ruby-tree-sitter",
139
- branch: "tree_haver",
140
- require: false # DO NOT LOAD, because conflicts with FFI
141
- ```
142
-
143
- #### Rust Backend (tree\_stump)
144
-
145
- **MRI Ruby only** - Does not work on JRuby or TruffleRuby.
146
-
147
- The Rust backend uses [tree\_stump][tree_stump], which is a Rust native extension built with [magnus](https://github.com/matsadler/magnus) and [rb-sys](https://github.com/oxidize-rb/rb-sys). These libraries are only compatible with MRI Ruby's C API.
148
-
149
- - **JRuby**: Cannot load native `.so` extensions (runs on JVM)
150
- - **TruffleRuby**: magnus/rb-sys are incompatible with TruffleRuby's C API emulation
151
- NOTE: `tree_stump` currently requires unreleased fixes in the `main` branch.
152
-
153
- ```ruby
154
- # Rust tree-sitter backend (MRI only)
155
- gem "tree_stump",
156
- # path: "../../vendor/tree_stump"
157
- github: "pboling/tree_stump",
158
- branch: "tree_haver"
159
- ```
160
-
161
- #### FFI Backend
162
-
163
- **MRI and JRuby only** - Does not work on TruffleRuby.
164
-
165
- Requires the `ffi` gem and a system installation of `libtree-sitter`.
166
-
167
- - **TruffleRuby**: TruffleRuby's FFI implementation doesn't support `STRUCT_BY_VALUE` return types, which tree-sitter's C API uses for functions like `ts_tree_root_node` and `ts_node_child`.
168
-
169
- ```ruby
170
- # Add to your Gemfile for FFI backend (MRI and JRuby)
171
- gem "ffi", ">= 1.15", "< 2.0"
172
- ```
173
-
174
- ```bash
175
- # Install libtree-sitter on your system:
176
- # macOS
177
- brew install tree-sitter
178
-
179
- # Ubuntu/Debian
180
- apt-get install libtree-sitter0 libtree-sitter-dev
181
-
182
- # Fedora
183
- dnf install tree-sitter tree-sitter-devel
184
- ```
185
-
186
- #### Citrus Backend
187
-
188
- Pure Ruby PEG parser with no native dependencies:
189
-
190
- ```ruby
191
- # Add to your Gemfile for Citrus backend
192
- gem "citrus", "~> 3.0"
193
- ```
194
-
195
- #### Parslet Backend
196
-
197
- Pure Ruby PEG parser with no native dependencies:
198
-
199
- ```ruby
200
- # Add to your Gemfile for Parslet backend
201
- gem "parslet", "~> 2.0"
202
- ```
203
-
204
- #### Java Backend (JRuby only)
205
-
206
- **Requires jtreesitter \>= 0.26.0** from Maven Central. Older versions are not supported due to breaking API changes.
207
-
208
- ```ruby
209
- # No gem dependency - uses JRuby's built-in Java integration
210
- # Download the JAR:
211
- # curl -L -o jtreesitter-0.26.0.jar \
212
- # "https://repo1.maven.org/maven2/io/github/tree-sitter/jtreesitter/0.26.0/jtreesitter-0.26.0.jar"
213
-
214
- # Set environment variable:
215
- # export TREE_SITTER_JAVA_JARS_DIR=/path/to/jars
216
- ```
217
-
218
- **Also requires**:
219
-
220
- - Tree-sitter runtime library (`libtree-sitter.so`) version 0.26+ (must match jtreesitter version)
221
- - Grammar `.so` files built against tree-sitter 0.26+ (or rebuilt with `tree-sitter generate`)
222
-
223
- ### Version Requirements for Tree-Sitter Backends
224
-
225
- #### tree-sitter Runtime Library
226
-
227
- All tree-sitter backends (MRI, Rust, FFI, Java) require the tree-sitter runtime library. **Version 0.26+ is required** for the Java backend (to match jtreesitter 0.26.0). Other backends may work with 0.24+, but 0.26+ is recommended for consistency.
228
-
229
- ```bash
230
- # Check your tree-sitter version
231
- tree-sitter --version # Should be 0.26.0 or newer for Java backend
232
-
233
- # macOS
234
- brew install tree-sitter
235
-
236
- # Ubuntu/Debian
237
- apt-get install libtree-sitter0 libtree-sitter-dev
238
-
239
- # Fedora
240
- dnf install tree-sitter tree-sitter-devel
241
- ```
242
-
243
- #### jtreesitter (Java Backend)
244
-
245
- **The Java backend requires jtreesitter \>= 0.26.0.** This version introduced breaking API changes:
246
-
247
- - `Parser.parse()` returns `Optional<Tree>` instead of `Tree`
248
- - `Tree.getRootNode()` returns `Node` directly (not `Optional<Node>`)
249
- - `Node.getChild()`, `getParent()`, `getNextSibling()`, `getPrevSibling()` return `Optional<Node>`
250
- - `Language.load(name)` was removed; use `SymbolLookup` API instead
251
- Older versions of jtreesitter are **NOT supported**.
252
-
253
- ```bash
254
- # Download jtreesitter 0.26.0 from Maven Central
255
- curl -L -o jtreesitter-0.26.0.jar \
256
- "https://repo1.maven.org/maven2/io/github/tree-sitter/jtreesitter/0.26.0/jtreesitter-0.26.0.jar"
257
-
258
- # Or use the provided setup script
259
- bin/setup-jtreesitter
260
- ```
261
-
262
- Set the environment variable to point to your JAR directory:
263
-
264
- ```bash
265
- export TREE_SITTER_JAVA_JARS_DIR=/path/to/jars
266
- ```
267
-
268
- #### Grammar ABI Compatibility
269
-
270
- **CRITICAL**: Grammars must be built against a compatible tree-sitter version.
271
-
272
- Tree-sitter 0.24+ changed how language ABI versions are reported (from `ts_language_version()` to `ts_language_abi_version()`). For the Java backend with jtreesitter 0.26.0, grammars must be built against tree-sitter 0.26+. If you get errors like:
273
-
274
- Failed to load tree_sitter_toml
275
- Version mismatch detected: The grammar was built against tree-sitter < 0.26
276
-
277
- You need to rebuild the grammar from source:
278
-
279
- ```bash
280
- # Use the provided build script
281
- bin/build-grammar toml
282
-
283
- # Or manually:
284
- git clone https://github.com/tree-sitter-grammars/tree-sitter-toml
285
- cd tree-sitter-toml
286
- tree-sitter generate # Regenerates parser.c for your tree-sitter version
287
- cc -shared -fPIC -o libtree-sitter-toml.so src/parser.c src/scanner.c -I src
288
- ```
289
-
290
- **Grammar sources for common languages:**
291
-
292
- | Language | Repository |
293
- |----------|--------------------------------------------------|
294
- | TOML | [tree-sitter-grammars/tree-sitter-toml][ts-toml] |
295
- | JSON | [tree-sitter/tree-sitter-json][ts-json] |
296
- | JSONC | [WhyNotHugo/tree-sitter-jsonc][ts-jsonc] |
297
- | Bash | [tree-sitter/tree-sitter-bash][ts-bash] |
298
-
299
- #### TruffleRuby Limitations
300
-
301
- TruffleRuby has **no working tree-sitter backend**:
302
-
303
- - **FFI**: TruffleRuby's FFI doesn't support `STRUCT_BY_VALUE` return types (used by `ts_tree_root_node`, `ts_node_child`, etc.)
304
- - **MRI/Rust**: C and Rust extensions require MRI's C API internals (`RBasic.flags`, `rb_gc_writebarrier`, etc.) that TruffleRuby doesn't expose
305
- TruffleRuby users should use: **Prism** (Ruby), **Psych** (YAML), **Citrus/Parslet** (e.g., TOML via toml-rb/toml), or potentially **Commonmarker/Markly** (Markdown).
306
-
307
- #### JRuby Limitations
308
-
309
- JRuby runs on the JVM and **cannot load native `.so` extensions via Ruby's C API**:
310
-
311
- - **MRI/Rust**: C and Rust extensions simply cannot be loaded
312
- - **FFI**: Works\! JRuby has excellent FFI support
313
- - **Java**: Works\! The Java backend uses jtreesitter (requires \>= 0.26.0)
314
- JRuby users should use: **Java backend** (best performance, full API) or **FFI backend** for tree-sitter, plus **Prism**, **Psych**, **Citrus/Parslet** for other formats.
315
-
316
- ### Why TreeHaver?
317
-
318
- tree-sitter is a powerful parser generator that creates incremental parsers for many programming languages. However, integrating it into Ruby applications can be challenging:
319
-
320
- - MRI-based C extensions don't work on JRuby
321
- - FFI-based solutions may not be optimal for MRI
322
- - Managing different backends for different Ruby implementations is cumbersome
323
- TreeHaver solves these problems by providing a unified API that automatically selects the appropriate backend for your Ruby implementation, allowing you to write code once and run it anywhere.
324
-
325
- ### The `*-merge` Gem Family
326
-
327
- The `*-merge` gem family provides intelligent, AST-based merging for various file formats. At the foundation is [tree_haver][tree_haver], which provides a unified cross-Ruby parsing API that works seamlessly across MRI, JRuby, and TruffleRuby.
328
-
329
- | Gem | Version / CI | Language<br>/ Format | Parser Backend(s) | Description |
330
- |------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------:|----------------------|-------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------|
331
- | [tree_haver][tree_haver] | [![Version][tree_haver-gem-i]][tree_haver-gem] <br/> [![CI][tree_haver-ci-i]][tree_haver-ci] | Multi | Supported Backends: MRI C, Rust, FFI, Java, Prism, Psych, Commonmarker, Markly, Citrus, Parslet | **Foundation**: Cross-Ruby adapter for parsing libraries (like Faraday for HTTP) |
332
- | [ast-merge][ast-merge] | [![Version][ast-merge-gem-i]][ast-merge-gem] <br/> [![CI][ast-merge-ci-i]][ast-merge-ci] | Text | internal | **Infrastructure**: Shared base classes and merge logic for all `*-merge` gems |
333
- | [bash-merge][bash-merge] | [![Version][bash-merge-gem-i]][bash-merge-gem] <br/> [![CI][bash-merge-ci-i]][bash-merge-ci] | Bash | [tree-sitter-bash][ts-bash] (via tree_haver) | Smart merge for Bash scripts |
334
- | [commonmarker-merge][commonmarker-merge] | [![Version][commonmarker-merge-gem-i]][commonmarker-merge-gem] <br/> [![CI][commonmarker-merge-ci-i]][commonmarker-merge-ci] | Markdown | [Commonmarker][commonmarker] (via tree_haver) | Smart merge for Markdown (CommonMark via comrak Rust) |
335
- | [dotenv-merge][dotenv-merge] | [![Version][dotenv-merge-gem-i]][dotenv-merge-gem] <br/> [![CI][dotenv-merge-ci-i]][dotenv-merge-ci] | Dotenv | internal | Smart merge for `.env` files |
336
- | [json-merge][json-merge] | [![Version][json-merge-gem-i]][json-merge-gem] <br/> [![CI][json-merge-ci-i]][json-merge-ci] | JSON | [tree-sitter-json][ts-json] (via tree_haver) | Smart merge for JSON files |
337
- | [jsonc-merge][jsonc-merge] | [![Version][jsonc-merge-gem-i]][jsonc-merge-gem] <br/> [![CI][jsonc-merge-ci-i]][jsonc-merge-ci] | JSONC | [tree-sitter-jsonc][ts-jsonc] (via tree_haver) | ⚠️ Proof of concept; Smart merge for JSON with Comments |
338
- | [markdown-merge][markdown-merge] | [![Version][markdown-merge-gem-i]][markdown-merge-gem] <br/> [![CI][markdown-merge-ci-i]][markdown-merge-ci] | Markdown | [Commonmarker][commonmarker] / [Markly][markly] (via tree_haver), [Parslet][parslet] | **Foundation**: Shared base for Markdown mergers with inner code block merging |
339
- | [markly-merge][markly-merge] | [![Version][markly-merge-gem-i]][markly-merge-gem] <br/> [![CI][markly-merge-ci-i]][markly-merge-ci] | Markdown | [Markly][markly] (via tree_haver) | Smart merge for Markdown (CommonMark via cmark-gfm C) |
340
- | [prism-merge][prism-merge] | [![Version][prism-merge-gem-i]][prism-merge-gem] <br/> [![CI][prism-merge-ci-i]][prism-merge-ci] | Ruby | [Prism][prism] (`prism` std lib gem) | Smart merge for Ruby source files |
341
- | [psych-merge][psych-merge] | [![Version][psych-merge-gem-i]][psych-merge-gem] <br/> [![CI][psych-merge-ci-i]][psych-merge-ci] | YAML | [Psych][psych] (`psych` std lib gem) | Smart merge for YAML files |
342
- | [rbs-merge][rbs-merge] | [![Version][rbs-merge-gem-i]][rbs-merge-gem] <br/> [![CI][rbs-merge-ci-i]][rbs-merge-ci] | RBS | [tree-sitter-bash][ts-rbs] (via tree_haver), [RBS][rbs] (`rbs` std lib gem) | Smart merge for Ruby type signatures |
343
- | [toml-merge][toml-merge] | [![Version][toml-merge-gem-i]][toml-merge-gem] <br/> [![CI][toml-merge-ci-i]][toml-merge-ci] | TOML | [Parslet + toml][toml], [Citrus + toml-rb][toml-rb], [tree-sitter-toml][ts-toml] (all via tree_haver) | Smart merge for TOML files |
344
-
345
- #### Backend Platform Compatibility
346
-
347
- tree_haver supports multiple parsing backends, but not all backends work on all Ruby platforms:
348
-
349
- | Platform 👉️<br> TreeHaver Backend 👇️ | MRI | JRuby | TruffleRuby | Notes |
350
- |-------------------------------------------------|:---:|:-----:|:-----------:|----------------------------------------------------------------------------|
351
- | **MRI** ([ruby_tree_sitter][ruby_tree_sitter]) | ✅ | ❌ | ❌ | C extension, MRI only |
352
- | **Rust** ([tree_stump][tree_stump]) | ✅ | ❌ | ❌ | Rust extension via magnus/rb-sys, MRI only |
353
- | **FFI** ([ffi][ffi]) | ✅ | ✅ | ❌ | TruffleRuby's FFI doesn't support `STRUCT_BY_VALUE` |
354
- | **Java** ([jtreesitter][jtreesitter]) | ❌ | ✅ | ❌ | JRuby only, requires grammar JARs |
355
- | **Prism** ([prism][prism]) | ✅ | ✅ | ✅ | Ruby parsing, stdlib in Ruby 3.4+ |
356
- | **Psych** ([psych][psych]) | ✅ | ✅ | ✅ | YAML parsing, stdlib |
357
- | **Citrus** ([citrus][citrus]) | ✅ | ✅ | ✅ | Pure Ruby PEG parser, no native dependencies |
358
- | **Parslet** ([parslet][parslet]) | ✅ | ✅ | ✅ | Pure Ruby PEG parser, no native dependencies |
359
- | **Commonmarker** ([commonmarker][commonmarker]) | ✅ | ❌ | ❓ | Rust extension for Markdown (via [commonmarker-merge][commonmarker-merge]) |
360
- | **Markly** ([markly][markly]) | ✅ | ❌ | ❓ | C extension for Markdown (via [markly-merge][markly-merge]) |
361
-
362
- **Legend**: ✅ = Works, ❌ = Does not work, ❓ = Untested
363
-
364
- **Why some backends don't work on certain platforms**:
365
-
366
- - **JRuby**: Runs on the JVM; cannot load native C/Rust extensions (`.so` files)
367
- - **TruffleRuby**: Has C API emulation via Sulong/LLVM, but it doesn't expose all MRI internals that native extensions require (e.g., `RBasic.flags`, `rb_gc_writebarrier`)
368
- - **FFI on TruffleRuby**: TruffleRuby's FFI implementation doesn't support returning structs by value, which tree-sitter's C API requires
369
-
370
- **Example implementations** for the gem templating use case:
371
-
372
- | Gem | Purpose | Description |
373
- |--------------------------|-----------------|-----------------------------------------------|
374
- | [kettle-dev][kettle-dev] | Gem Development | Gem templating tool using `*-merge` gems |
375
- | [kettle-jem][kettle-jem] | Gem Templating | Gem template library with smart merge support |
376
-
377
- [tree_haver]: https://github.com/kettle-rb/tree_haver
378
- [ast-merge]: https://github.com/kettle-rb/ast-merge
379
- [prism-merge]: https://github.com/kettle-rb/prism-merge
380
- [psych-merge]: https://github.com/kettle-rb/psych-merge
381
- [json-merge]: https://github.com/kettle-rb/json-merge
382
- [jsonc-merge]: https://github.com/kettle-rb/jsonc-merge
383
- [bash-merge]: https://github.com/kettle-rb/bash-merge
384
- [rbs-merge]: https://github.com/kettle-rb/rbs-merge
385
- [dotenv-merge]: https://github.com/kettle-rb/dotenv-merge
386
- [toml-merge]: https://github.com/kettle-rb/toml-merge
387
- [markdown-merge]: https://github.com/kettle-rb/markdown-merge
388
- [markly-merge]: https://github.com/kettle-rb/markly-merge
389
- [commonmarker-merge]: https://github.com/kettle-rb/commonmarker-merge
390
- [kettle-dev]: https://github.com/kettle-rb/kettle-dev
391
- [kettle-jem]: https://github.com/kettle-rb/kettle-jem
392
- [tree_haver-gem]: https://bestgems.org/gems/tree_haver
393
- [ast-merge-gem]: https://bestgems.org/gems/ast-merge
394
- [prism-merge-gem]: https://bestgems.org/gems/prism-merge
395
- [psych-merge-gem]: https://bestgems.org/gems/psych-merge
396
- [json-merge-gem]: https://bestgems.org/gems/json-merge
397
- [jsonc-merge-gem]: https://bestgems.org/gems/jsonc-merge
398
- [bash-merge-gem]: https://bestgems.org/gems/bash-merge
399
- [rbs-merge-gem]: https://bestgems.org/gems/rbs-merge
400
- [dotenv-merge-gem]: https://bestgems.org/gems/dotenv-merge
401
- [toml-merge-gem]: https://bestgems.org/gems/toml-merge
402
- [markdown-merge-gem]: https://bestgems.org/gems/markdown-merge
403
- [markly-merge-gem]: https://bestgems.org/gems/markly-merge
404
- [commonmarker-merge-gem]: https://bestgems.org/gems/commonmarker-merge
405
- [kettle-dev-gem]: https://bestgems.org/gems/kettle-dev
406
- [kettle-jem-gem]: https://bestgems.org/gems/kettle-jem
407
- [tree_haver-gem-i]: https://img.shields.io/gem/v/tree_haver.svg
408
- [ast-merge-gem-i]: https://img.shields.io/gem/v/ast-merge.svg
409
- [prism-merge-gem-i]: https://img.shields.io/gem/v/prism-merge.svg
410
- [psych-merge-gem-i]: https://img.shields.io/gem/v/psych-merge.svg
411
- [json-merge-gem-i]: https://img.shields.io/gem/v/json-merge.svg
412
- [jsonc-merge-gem-i]: https://img.shields.io/gem/v/jsonc-merge.svg
413
- [bash-merge-gem-i]: https://img.shields.io/gem/v/bash-merge.svg
414
- [rbs-merge-gem-i]: https://img.shields.io/gem/v/rbs-merge.svg
415
- [dotenv-merge-gem-i]: https://img.shields.io/gem/v/dotenv-merge.svg
416
- [toml-merge-gem-i]: https://img.shields.io/gem/v/toml-merge.svg
417
- [markdown-merge-gem-i]: https://img.shields.io/gem/v/markdown-merge.svg
418
- [markly-merge-gem-i]: https://img.shields.io/gem/v/markly-merge.svg
419
- [commonmarker-merge-gem-i]: https://img.shields.io/gem/v/commonmarker-merge.svg
420
- [kettle-dev-gem-i]: https://img.shields.io/gem/v/kettle-dev.svg
421
- [kettle-jem-gem-i]: https://img.shields.io/gem/v/kettle-jem.svg
422
- [tree_haver-ci-i]: https://github.com/kettle-rb/tree_haver/actions/workflows/current.yml/badge.svg
423
- [ast-merge-ci-i]: https://github.com/kettle-rb/ast-merge/actions/workflows/current.yml/badge.svg
424
- [prism-merge-ci-i]: https://github.com/kettle-rb/prism-merge/actions/workflows/current.yml/badge.svg
425
- [psych-merge-ci-i]: https://github.com/kettle-rb/psych-merge/actions/workflows/current.yml/badge.svg
426
- [json-merge-ci-i]: https://github.com/kettle-rb/json-merge/actions/workflows/current.yml/badge.svg
427
- [jsonc-merge-ci-i]: https://github.com/kettle-rb/jsonc-merge/actions/workflows/current.yml/badge.svg
428
- [bash-merge-ci-i]: https://github.com/kettle-rb/bash-merge/actions/workflows/current.yml/badge.svg
429
- [rbs-merge-ci-i]: https://github.com/kettle-rb/rbs-merge/actions/workflows/current.yml/badge.svg
430
- [dotenv-merge-ci-i]: https://github.com/kettle-rb/dotenv-merge/actions/workflows/current.yml/badge.svg
431
- [toml-merge-ci-i]: https://github.com/kettle-rb/toml-merge/actions/workflows/current.yml/badge.svg
432
- [markdown-merge-ci-i]: https://github.com/kettle-rb/markdown-merge/actions/workflows/current.yml/badge.svg
433
- [markly-merge-ci-i]: https://github.com/kettle-rb/markly-merge/actions/workflows/current.yml/badge.svg
434
- [commonmarker-merge-ci-i]: https://github.com/kettle-rb/commonmarker-merge/actions/workflows/current.yml/badge.svg
435
- [kettle-dev-ci-i]: https://github.com/kettle-rb/kettle-dev/actions/workflows/current.yml/badge.svg
436
- [kettle-jem-ci-i]: https://github.com/kettle-rb/kettle-jem/actions/workflows/current.yml/badge.svg
437
- [tree_haver-ci]: https://github.com/kettle-rb/tree_haver/actions/workflows/current.yml
438
- [ast-merge-ci]: https://github.com/kettle-rb/ast-merge/actions/workflows/current.yml
439
- [prism-merge-ci]: https://github.com/kettle-rb/prism-merge/actions/workflows/current.yml
440
- [psych-merge-ci]: https://github.com/kettle-rb/psych-merge/actions/workflows/current.yml
441
- [json-merge-ci]: https://github.com/kettle-rb/json-merge/actions/workflows/current.yml
442
- [jsonc-merge-ci]: https://github.com/kettle-rb/jsonc-merge/actions/workflows/current.yml
443
- [bash-merge-ci]: https://github.com/kettle-rb/bash-merge/actions/workflows/current.yml
444
- [rbs-merge-ci]: https://github.com/kettle-rb/rbs-merge/actions/workflows/current.yml
445
- [dotenv-merge-ci]: https://github.com/kettle-rb/dotenv-merge/actions/workflows/current.yml
446
- [toml-merge-ci]: https://github.com/kettle-rb/toml-merge/actions/workflows/current.yml
447
- [markdown-merge-ci]: https://github.com/kettle-rb/markdown-merge/actions/workflows/current.yml
448
- [markly-merge-ci]: https://github.com/kettle-rb/markly-merge/actions/workflows/current.yml
449
- [commonmarker-merge-ci]: https://github.com/kettle-rb/commonmarker-merge/actions/workflows/current.yml
450
- [kettle-dev-ci]: https://github.com/kettle-rb/kettle-dev/actions/workflows/current.yml
451
- [kettle-jem-ci]: https://github.com/kettle-rb/kettle-jem/actions/workflows/current.yml
452
- [prism]: https://github.com/ruby/prism
453
- [psych]: https://github.com/ruby/psych
454
- [ffi]: https://github.com/ffi/ffi
455
- [ts-json]: https://github.com/tree-sitter/tree-sitter-json
456
- [ts-jsonc]: https://gitlab.com/WhyNotHugo/tree-sitter-jsonc
457
- [ts-bash]: https://github.com/tree-sitter/tree-sitter-bash
458
- [ts-rbs]: https://github.com/joker1007/tree-sitter-rbs
459
- [ts-toml]: https://github.com/tree-sitter-grammars/tree-sitter-toml
460
- [dotenv]: https://github.com/bkeepers/dotenv
461
- [rbs]: https://github.com/ruby/rbs
462
- [toml-rb]: https://github.com/emancu/toml-rb
463
- [toml]: https://github.com/jm/toml
464
- [markly]: https://github.com/ioquatix/markly
465
- [commonmarker]: https://github.com/gjtorikian/commonmarker
466
- [ruby_tree_sitter]: https://github.com/Faveod/ruby-tree-sitter
467
- [tree_stump]: https://github.com/joker1007/tree_stump
468
- [jtreesitter]: https://central.sonatype.com/artifact/io.github.tree-sitter/jtreesitter
469
- [citrus]: https://github.com/mjackson/citrus
470
- [parslet]: https://github.com/kschiess/parslet
471
-
472
- ### Comparison with Other Ruby AST / Parser Bindings
473
-
474
- | Feature | [tree\_haver][📜src-gh] (this gem) | [ruby\_tree\_sitter][ruby_tree_sitter] | [tree\_stump][tree_stump] | [citrus][citrus] | [parslet][parslet] |
475
- |---------------------------|-------------------------------------------------|----------------------------------------|---------------------------|------------------|--------------------|
476
- | **MRI Ruby** | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes |
477
- | **JRuby** | ✅ Yes (FFI, Java, Citrus, or Parslet backend) | ❌ No | ❌ No | ✅ Yes | ✅ Yes |
478
- | **TruffleRuby** | ✅ Yes (FFI, Citrus, or Parslet) | ❌ No | ❓ Unknown | ✅ Yes | ✅ Yes |
479
- | **Backend** | Multi (MRI C, Rust, FFI, Java, Citrus, Parslet) | C extension only | Rust extension | Pure Ruby | Pure Ruby |
480
- | **Incremental Parsing** | ✅ Via MRI C/Rust/Java backend | ✅ Yes | ✅ Yes | ❌ No | ❌ No |
481
- | **Query API** | ⚡ Via MRI/Rust/Java backend | ✅ Yes | ✅ Yes | ❌ No | ❌ No |
482
- | **Grammar Discovery** | ✅ Built-in `GrammarFinder` | ❌ Manual | ❌ Manual | ❌ Manual | ❌ Manual |
483
- | **Security Validations** | ✅ `PathValidator` | ❌ No | ❌ No | ❌ No | ❌ No |
484
- | **Language Registration** | ✅ Thread-safe registry | ❌ No | ❌ No | ❌ No | ❌ No |
485
- | **Native Performance** | ⚡ Backend-dependent | ✅ Native C | ✅ Native Rust | ❌ Pure Ruby | ❌ Pure Ruby |
486
- | **Precompiled Binaries** | ⚡ Via Rust backend | ✅ Yes | ✅ Yes | ✅ Pure Ruby | ✅ Pure Ruby |
487
- | **Zero Native Deps** | ⚡ Via Citrus/Parslet backend | ❌ No | ❌ No | ✅ Yes | ✅ Yes |
488
- | **Minimum Ruby** | 3.2+ | 3.0+ | 3.1+ | 0+ | 0+ |
489
-
490
- **Note:** Java backend works with grammar `.so` files built against tree-sitter 0.24+. The grammars must be rebuilt with `tree-sitter generate` if they were compiled against older tree-sitter versions. FFI is recommended for JRuby as it's easier to set up.
491
-
492
- **Note:** TreeHaver can use `ruby_tree_sitter` (MRI) or `tree_stump` (MRI) as backends, or `java-tree-sitter` / `jtreesitter` \>= 0.26.0 ([docs](https://tree-sitter.github.io/java-tree-sitter/), [maven][jtreesitter], [source](https://github.com/tree-sitter/java-tree-sitter), JRuby), or FFI on any backend, giving you TreeHaver's unified API, grammar discovery, and security features, plus full access to incremental parsing when using those backends.
493
-
494
- **Note:** `tree_stump` currently requires unreleased fixes in the `main` branch.
495
-
496
- #### When to Use Each
497
-
498
- **Choose TreeHaver when:**
499
-
500
- - You need JRuby or TruffleRuby support
501
- - You're building a library that should work across Ruby implementations
502
- - You want automatic grammar discovery and security validations
503
- - You want flexibility to switch backends without code changes
504
- - You need incremental parsing with a unified API
505
-
506
- **Choose ruby\_tree\_sitter directly when:**
507
-
508
- - You only target MRI Ruby
509
- - You need the full Query API without abstraction
510
- - You want the most battle-tested C bindings
511
- - You don't need TreeHaver's grammar discovery
512
-
513
- **Choose tree\_stump directly when:**
514
-
515
- - You only target MRI Ruby
516
- - You prefer Rust-based native extensions
517
- - You want precompiled binaries without system dependencies
518
- - You don't need TreeHaver's grammar discovery
519
- - **Note:** `tree_stump` currently requires unreleased fixes in the `main` branch.
520
-
521
- **Choose citrus or parslet directly when:**
522
-
523
- - You need zero native dependencies (pure Ruby)
524
- - You're using a Citrus or Parslet grammar (not tree-sitter grammars)
525
- - Performance is less critical than portability
526
- - You don't need TreeHaver's unified API
527
-
528
- ## 💡 Info you can shake a stick at
529
-
530
- | Tokens to Remember | [![Gem name][⛳️name-img]][👽dl-rank] [![Gem namespace][⛳️namespace-img]][📜src-gh] |
531
- |-------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
532
- | Works with JRuby | [![JRuby 10.0 Compat][💎jruby-c-i]][🚎11-c-wf] [![JRuby HEAD Compat][💎jruby-headi]][🚎3-hd-wf] |
533
- | Works with Truffle Ruby | [![Truffle Ruby 23.1 Compat][💎truby-23.1i]][🚎9-t-wf] [![Truffle Ruby 24.1 Compat][💎truby-c-i]][🚎11-c-wf] |
534
- | Works with MRI Ruby 3 | [![Ruby 3.2 Compat][💎ruby-3.2i]][🚎6-s-wf] [![Ruby 3.3 Compat][💎ruby-3.3i]][🚎6-s-wf] [![Ruby 3.4 Compat][💎ruby-c-i]][🚎11-c-wf] [![Ruby HEAD Compat][💎ruby-headi]][🚎3-hd-wf] |
535
- | Support & Community | [![Join Me on Daily.dev's RubyFriends][✉️ruby-friends-img]][✉️ruby-friends] [![Live Chat on Discord][✉️discord-invite-img-ftb]][🖼️galtzo-discord] [![Get help from me on Upwork][👨🏼‍🏫expsup-upwork-img]][👨🏼‍🏫expsup-upwork] [![Get help from me on Codementor][👨🏼‍🏫expsup-codementor-img]][👨🏼‍🏫expsup-codementor] |
536
- | Source | [![Source on GitLab.com][📜src-gl-img]][📜src-gl] [![Source on CodeBerg.org][📜src-cb-img]][📜src-cb] [![Source on Github.com][📜src-gh-img]][📜src-gh] [![The best SHA: dQw4w9WgXcQ\!](https://img.shields.io/badge/KLOC-2.484-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue)][🧮kloc] |
537
- | Documentation | [![Current release on RubyDoc.info][📜docs-cr-rd-img]][🚎yard-current] [![YARD on Galtzo.com][📜docs-head-rd-img]][🚎yard-head] [![Maintainer Blog][🚂maint-blog-img]][🚂maint-blog] [![GitLab Wiki][📜gl-wiki-img]][📜gl-wiki] [![GitHub Wiki][📜gh-wiki-img]][📜gh-wiki] |
538
- | Compliance | [![License: MIT][📄license-img]][📄license-ref] [![Compatible with Apache Software Projects: Verified by SkyWalking Eyes][📄license-compat-img]][📄license-compat] [![📄ilo-declaration-img][📄ilo-declaration-img]][📄ilo-declaration] [![Security Policy][🔐security-img]][🔐security] [![Contributor Covenant 2.1][🪇conduct-img]][🪇conduct] [![SemVer 2.0.0][📌semver-img]][📌semver] |
539
- | Style | [![Enforced Code Style Linter][💎rlts-img]][💎rlts] [![Keep-A-Changelog 1.0.0][📗keep-changelog-img]][📗keep-changelog] [![Gitmoji Commits][📌gitmoji-img]][📌gitmoji] [![Compatibility appraised by: appraisal2][💎appraisal2-img]][💎appraisal2] |
540
- | Maintainer 🎖️ | [![Follow Me on LinkedIn][💖🖇linkedin-img]][💖🖇linkedin] [![Follow Me on Ruby.Social][💖🐘ruby-mast-img]][💖🐘ruby-mast] [![Follow Me on Bluesky][💖🦋bluesky-img]][💖🦋bluesky] [![Contact Maintainer][🚂maint-contact-img]][🚂maint-contact] [![My technical writing][💖💁🏼‍♂️devto-img]][💖💁🏼‍♂️devto] |
541
- | `...` 💖 | [![Find Me on WellFound:][💖✌️wellfound-img]][💖✌️wellfound] [![Find Me on CrunchBase][💖💲crunchbase-img]][💖💲crunchbase] [![My LinkTree][💖🌳linktree-img]][💖🌳linktree] [![More About Me][💖💁🏼‍♂️aboutme-img]][💖💁🏼‍♂️aboutme] [🧊][💖🧊berg] [🐙][💖🐙hub] [🛖][💖🛖hut] [🧪][💖🧪lab] |
542
-
543
- ### Compatibility
544
-
545
- Compatible with MRI Ruby 3.2.0+, and concordant releases of JRuby, and TruffleRuby.
546
-
547
- | 🚚 *Amazing* test matrix was brought to you by | 🔎 appraisal2 🔎 and the color 💚 green 💚 |
548
- |------------------------------------------------|--------------------------------------------------------|
549
- | 👟 Check it out\! | ✨ [github.com/appraisal-rb/appraisal2][💎appraisal2] ✨ |
550
-
551
- ### Federated DVCS
552
-
553
- <details markdown="1">
554
- <summary>Find this repo on federated forges (Coming soon!)</summary>
555
-
556
- | Federated [DVCS][💎d-in-dvcs] Repository | Status | Issues | PRs | Wiki | CI | Discussions |
557
- |--------------------------------------------------|------------------------------------------------------------------------------------|----------------------------|---------------------------|----------------------------|---------------------------|--------------------------------|
558
- | 🧪 [kettle-rb/tree\_haver on GitLab][📜src-gl] | The Truth | [💚][🤝gl-issues] | [💚][🤝gl-pulls] | [💚][📜gl-wiki] | 🐭 Tiny Matrix | ➖ |
559
- | 🧊 [kettle-rb/tree\_haver on CodeBerg][📜src-cb] | An Ethical Mirror ([Donate][🤝cb-donate]) | [💚][🤝cb-issues] | [💚][🤝cb-pulls] | ➖ | ⭕️ No Matrix | ➖ |
560
- | 🐙 [kettle-rb/tree\_haver on GitHub][📜src-gh] | Another Mirror | [💚][🤝gh-issues] | [💚][🤝gh-pulls] | [💚][📜gh-wiki] | 💯 Full Matrix | [💚][gh-discussions] |
561
- | 🎮️ [Discord Server][🖼️galtzo-discord] | [![Live Chat on Discord][✉️discord-invite-img-ftb]][🖼️galtzo-discord] | [Let's][🖼️galtzo-discord] | [talk][🖼️galtzo-discord] | [about][🖼️galtzo-discord] | [this][🖼️galtzo-discord] | [library\!][🖼️galtzo-discord] |
562
-
563
- </details>
564
-
565
- [gh-discussions]: https://github.com/kettle-rb/tree_haver/discussions
566
-
567
- ### Enterprise Support [![Tidelift](https://tidelift.com/badges/package/rubygems/tree_haver)][🏙️entsup-tidelift]
568
-
569
- Available as part of the Tidelift Subscription.
570
-
571
- <details markdown="1">
572
- <summary>Need enterprise-level guarantees?</summary>
573
-
574
- The maintainers of this and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source packages you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact packages you use.
575
-
576
- [![Get help from me on Tidelift][🏙️entsup-tidelift-img]][🏙️entsup-tidelift]
577
-
578
- - 💡Subscribe for support guarantees covering *all* your FLOSS dependencies
579
-
580
- - 💡Tidelift is part of [Sonar][🏙️entsup-tidelift-sonar]
581
-
582
- - 💡Tidelift pays maintainers to maintain the software you depend on\!<br/>📊`@`Pointy Haired Boss: An [enterprise support][🏙️entsup-tidelift] subscription is "[never gonna let you down][🧮kloc]", and *supports* open source maintainers
583
- Alternatively:
584
-
585
- - [![Live Chat on Discord][✉️discord-invite-img-ftb]][🖼️galtzo-discord]
586
-
587
- - [![Get help from me on Upwork][👨🏼‍🏫expsup-upwork-img]][👨🏼‍🏫expsup-upwork]
588
-
589
- - [![Get help from me on Codementor][👨🏼‍🏫expsup-codementor-img]][👨🏼‍🏫expsup-codementor]
590
-
591
- </details>
592
-
593
- ## ✨ Installation
594
-
595
- Install the gem and add to the application's Gemfile by executing:
596
-
597
- ```console
598
- bundle add tree_haver
599
- ```
600
-
601
- If bundler is not being used to manage dependencies, install the gem by executing:
602
-
603
- ```console
604
- gem install tree_haver
605
- ```
606
-
607
- ### 🔒 Secure Installation
608
-
609
- <details markdown="1">
610
- <summary>For Medium or High Security Installations</summary>
611
-
612
- This gem is cryptographically signed, and has verifiable [SHA-256 and SHA-512][💎SHA_checksums] checksums by
613
- [stone\_checksums][💎stone_checksums]. Be sure the gem you install hasn’t been tampered with
614
- by following the instructions below.
615
-
616
- Add my public key (if you haven’t already, expires 2045-04-29) as a trusted certificate:
617
-
618
- ```console
619
- gem cert --add <(curl -Ls https://raw.github.com/galtzo-floss/certs/main/pboling.pem)
620
- ```
621
-
622
- You only need to do that once. Then proceed to install with:
623
-
624
- ```console
625
- gem install tree_haver -P HighSecurity
626
- ```
627
-
628
- The `HighSecurity` trust profile will verify signed gems, and not allow the installation of unsigned dependencies.
629
-
630
- If you want to up your security game full-time:
631
-
632
- ```console
633
- bundle config set --global trust-policy MediumSecurity
634
- ```
635
-
636
- `MediumSecurity` instead of `HighSecurity` is necessary if not all the gems you use are signed.
637
-
638
- NOTE: Be prepared to track down certs for signed gems and add them the same way you added mine.
639
-
640
- </details>
641
-
642
- ## ⚙️ Configuration
643
-
644
- ### Available Backends
645
-
646
- TreeHaver supports 10 parsing backends, each with different trade-offs. The `auto` backend automatically selects the best available option.
647
-
648
- #### Tree-sitter Backends (Universal Parsing)
649
-
650
- | Backend | Description | Performance | Portability | Examples |
651
- |----------|---------------------------------------|-------------|-------------|---------------------------------------------------------------------------------------------------------------------------------|
652
- | **Auto** | Auto-selects best backend | Varies | ✅ Universal | [JSON](examples/auto_json.rb) · [JSONC](examples/auto_jsonc.rb) · [Bash](examples/auto_bash.rb) · [TOML](examples/auto_toml.rb) |
653
- | **MRI** | C extension via ruby\_tree\_sitter | ⚡ Fastest | MRI only | [JSON](examples/mri_json.rb) · [JSONC](examples/mri_jsonc.rb) · \~\~Bash\~\~\* · [TOML](examples/mri_toml.rb) |
654
- | **Rust** | Precompiled via tree\_stump | ⚡ Very Fast | ✅ Good | [JSON](examples/rust_json.rb) · [JSONC](examples/rust_jsonc.rb) · \~\~Bash\~\~\* · [TOML](examples/rust_toml.rb) |
655
- | **FFI** | Dynamic linking via FFI | 🔵 Fast | ✅ Universal | [JSON](examples/ffi_json.rb) · [JSONC](examples/ffi_jsonc.rb) · [Bash](examples/ffi_bash.rb) · [TOML](examples/ffi_toml.rb) |
656
- | **Java** | JNI bindings (jtreesitter \>= 0.26.0) | ⚡ Very Fast | JRuby only | [JSON](examples/java_json.rb) · [JSONC](examples/java_jsonc.rb) · [Bash](examples/java_bash.rb) · [TOML](examples/java_toml.rb) |
657
-
658
- #### Language-Specific Backends (Native Parser Integration)
659
-
660
- | Backend | Description | Performance | Portability | Examples |
661
- |------------------|-----------------------------|-------------|-------------|--------------------------------------------------------------------------------------------------------------|
662
- | **Prism** | Ruby's official parser | ⚡ Very Fast | ✅ Universal | [Ruby](examples/prism_ruby.rb) |
663
- | **Psych** | Ruby's YAML parser (stdlib) | ⚡ Very Fast | ✅ Universal | [YAML](examples/psych_yaml.rb) |
664
- | **Commonmarker** | Markdown via comrak (Rust) | ⚡ Very Fast | ✅ Good | [Markdown](examples/commonmarker_markdown.rb) · [commonmarker-merge](examples/commonmarker_merge_example.rb) |
665
- | **Markly** | GFM via cmark-gfm (C) | ⚡ Very Fast | ✅ Good | [Markdown](examples/markly_markdown.rb) · [Merge](examples/markly_merge_example.rb) |
666
- | **Citrus** | Pure Ruby parsing | 🟡 Slower | ✅ Universal | [TOML](examples/citrus_toml.rb) · [Finitio](examples/citrus_finitio.rb) · [Dhall](examples/citrus_dhall.rb) |
667
- | **Parslet** | Pure Ruby parsing | 🟡 Slower | ✅ Universal | [TOML](examples/parslet_toml.rb) |
668
-
669
- **Selection Priority (Auto mode):** MRI → Rust → FFI → Java → Prism → Psych → Commonmarker → Markly → Citrus → Parslet
670
-
671
- **Known Issues:**
672
-
673
- - \*MRI + Bash: ABI incompatibility (use FFI instead)
674
- - \*Rust + Bash: Version mismatch (use FFI instead)
675
- **Backend Requirements:**
676
-
677
- ```ruby
678
- # Tree-sitter backends
679
- gem "ruby_tree_sitter", "~> 2.0" # MRI backend
680
- gem "tree_stump" # Rust backend
681
- gem "ffi", ">= 1.15", "< 2.0" # FFI backend
682
- # Java backend: no gem required (uses JRuby's built-in JNI)
683
-
684
- # Language-specific backends
685
- gem "prism", "~> 1.0" # Ruby parsing (stdlib in Ruby 3.4+)
686
- # Psych: no gem required (Ruby stdlib)
687
- gem "commonmarker", ">= 0.23" # Markdown parsing (comrak)
688
- gem "markly", "~> 0.11" # GFM parsing (cmark-gfm)
689
-
690
- # Pure Ruby fallbacks
691
- gem "citrus", "~> 3.0" # Citrus backend
692
- gem "parslet", "~> 2.0" # Parslet backend
693
- # Plus grammar gems: toml-rb (citrus), toml (parslet), dhall, finitio, etc.
694
- ```
695
-
696
- **Force Specific Backend:**
697
-
698
- ```ruby
699
- # Tree-sitter backends
700
- TreeHaver.backend = :mri # Force MRI backend (ruby_tree_sitter)
701
- TreeHaver.backend = :rust # Force Rust backend (tree_stump)
702
- TreeHaver.backend = :ffi # Force FFI backend
703
- TreeHaver.backend = :java # Force Java backend (JRuby only)
704
-
705
- # Language-specific backends
706
- TreeHaver.backend = :prism # Force Prism (Ruby parsing)
707
- TreeHaver.backend = :psych # Force Psych (YAML parsing)
708
- TreeHaver.backend = :commonmarker # Force Commonmarker (Markdown)
709
- TreeHaver.backend = :markly # Force Markly (GFM Markdown)
710
- TreeHaver.backend = :citrus # Force Citrus (Pure Ruby PEG)
711
- TreeHaver.backend = :parslet # Force Parslet (Pure Ruby PEG)
712
-
713
- # Auto-selection (default)
714
- TreeHaver.backend = :auto # Let TreeHaver choose
715
- ```
716
-
717
- **Block-based Backend Switching:**
718
-
719
- Use `with_backend` to temporarily switch backends for a specific block of code.
720
- This is thread-safe and supports nesting—the previous backend is automatically
721
- restored when the block exits (even if an exception is raised).
722
-
723
- ```ruby
724
- # Temporarily use a specific backend
725
- TreeHaver.with_backend(:mri) do
726
- parser = TreeHaver::Parser.new
727
- tree = parser.parse(source)
728
- # All operations in this block use the MRI backend
729
- end
730
- # Backend is restored to its previous value here
731
-
732
- # Nested blocks work correctly
733
- TreeHaver.with_backend(:rust) do
734
- # Uses :rust
735
- TreeHaver.with_backend(:citrus) do
736
- # Uses :citrus
737
- parser = TreeHaver::Parser.new
738
- end
739
- # Back to :rust
740
- TreeHaver.with_backend(:parslet) do
741
- # Uses :parslet
742
- parser = TreeHaver::Parser.new
743
- end
744
- # Back to :rust
745
- end
746
- # Back to original backend
747
- ```
748
-
749
- This is particularly useful for:
750
-
751
- - **Testing**: Test the same code with different backends
752
- - **Performance comparison**: Benchmark different backends
753
- - **Fallback scenarios**: Try one backend, fall back to another
754
- - **Thread isolation**: Each thread can use a different backend safely
755
-
756
- ```ruby
757
- # Example: Testing with multiple backends
758
- [:mri, :rust, :citrus, :parslet].each do |backend_name|
759
- TreeHaver.with_backend(backend_name) do
760
- parser = TreeHaver::Parser.new
761
- result = parser.parse(source)
762
- puts "#{backend_name}: #{result.root_node.type}"
763
- end
764
- end
765
- ```
766
-
767
- **Check Backend Capabilities:**
768
-
769
- ```ruby
770
- TreeHaver.backend # => :ffi
771
- TreeHaver.backend_module # => TreeHaver::Backends::FFI
772
- TreeHaver.capabilities # => { backend: :ffi, parse: true, query: false, ... }
773
- ```
774
-
775
- See [examples/](examples/) directory for **26 complete working examples** demonstrating all 10 backends with multiple languages (JSON, JSONC, Bash, TOML, Ruby, YAML, Markdown) plus markdown-merge integration examples.
776
-
777
- ### Security Considerations
778
-
779
- **⚠️ Loading shared libraries (.so/.dylib/.dll) executes arbitrary native code.**
780
-
781
- TreeHaver provides defense-in-depth validations, but you should understand the risks:
782
-
783
- #### Attack Vectors Mitigated
784
-
785
- TreeHaver's `PathValidator` module protects against:
786
-
787
- - **Path traversal**: Paths containing `/../` or `/./` are rejected
788
- - **Null byte injection**: Paths containing null bytes are rejected
789
- - **Non-absolute paths**: Relative paths are rejected to prevent CWD-based attacks
790
- - **Invalid extensions**: Only `.so`, `.dylib`, and `.dll` files are accepted
791
- - **Malicious filenames**: Filenames must match a safe pattern (alphanumeric, hyphens, underscores)
792
- - **Invalid language names**: Language names must be lowercase alphanumeric with underscores
793
- - **Invalid symbol names**: Symbol names must be valid C identifiers
794
-
795
- #### Secure Usage
796
-
797
- ```ruby
798
- # Standard usage - paths from ENV are validated
799
- finder = TreeHaver::GrammarFinder.new(:toml)
800
- path = finder.find_library_path # Validates ENV path before returning
801
-
802
- # Maximum security - only trusted system directories
803
- path = finder.find_library_path_safe # Ignores ENV, only /usr/lib etc.
804
-
805
- # Manual validation
806
- if TreeHaver::PathValidator.safe_library_path?(user_provided_path)
807
- language = TreeHaver::Language.from_library(user_provided_path)
808
- end
809
-
810
- # Get validation errors for debugging
811
- errors = TreeHaver::PathValidator.validation_errors(path)
812
- # => ["Path is not absolute", "Path contains traversal sequence"]
813
- ```
814
-
815
- #### Trusted Directories
816
-
817
- The `find_library_path_safe` method only returns paths in trusted directories.
818
-
819
- **Default trusted directories:**
820
-
821
- - `/usr/lib`, `/usr/lib64`
822
- - `/usr/lib/x86_64-linux-gnu`, `/usr/lib/aarch64-linux-gnu`
823
- - `/usr/local/lib`
824
- - `/opt/homebrew/lib`, `/opt/local/lib`
825
- **Adding custom trusted directories:**
826
- For non-standard installations (Homebrew on Linux, luarocks, mise, asdf, etc.), register additional trusted directories:
827
-
828
- ```ruby
829
- # Programmatically at application startup
830
- TreeHaver::PathValidator.add_trusted_directory("/home/linuxbrew/.linuxbrew/Cellar")
831
- TreeHaver::PathValidator.add_trusted_directory("~/.local/share/mise/installs/lua")
832
-
833
- # Or via environment variable (comma-separated, in your shell profile)
834
- export TREE_HAVER_TRUSTED_DIRS = "/home/linuxbrew/.linuxbrew/Cellar,~/.local/share/mise/installs/lua"
835
- ```
836
-
837
- **Example: Fedora Silverblue with Homebrew and luarocks**
838
-
839
- ```bash
840
- # In ~/.bashrc or ~/.zshrc
841
- export TREE_HAVER_TRUSTED_DIRS="/home/linuxbrew/.linuxbrew/Cellar,~/.local/share/mise/installs/lua"
842
-
843
- # tree-sitter runtime library
844
- export TREE_SITTER_RUNTIME_LIB=/home/linuxbrew/.linuxbrew/Cellar/tree-sitter/0.26.3/lib/libtree-sitter.so
845
-
846
- # Language grammar (luarocks-installed)
847
- export TREE_SITTER_TOML_PATH=~/.local/share/mise/installs/lua/5.4.8/luarocks/lib/luarocks/rocks-5.4/tree-sitter-toml/0.0.31-1/parser/toml.so
848
- ```
849
-
850
- #### Recommendations
851
-
852
- 1. **Production**: Consider using `find_library_path_safe` to ignore ENV overrides
853
- 2. **Development**: Standard `find_library_path` is convenient for testing
854
- 3. **User Input**: Always validate paths before passing to `Language.from_library`
855
- 4. **CI/CD**: Be cautious of ENV vars that could be set by untrusted sources
856
- 5. **Custom installs**: Register trusted directories via `TREE_HAVER_TRUSTED_DIRS` or `add_trusted_directory`
857
-
858
- ### Backend Selection
859
-
860
- TreeHaver automatically selects the best backend for your Ruby implementation, but you can override this behavior:
861
-
862
- ```ruby
863
- # Automatic backend selection (default)
864
- TreeHaver.backend = :auto
865
-
866
- # Force a specific backend
867
- TreeHaver.backend = :mri # Use ruby_tree_sitter (MRI only, C extension)
868
- TreeHaver.backend = :rust # Use tree_stump (MRI, Rust extension with precompiled binaries)
869
- # Note: `tree_stump` currently requires unreleased fixes in the `main` branch.
870
- # See: https://github.com/joker1007/tree_stump
871
- TreeHaver.backend = :ffi # Use FFI bindings (works on MRI and JRuby)
872
- TreeHaver.backend = :java # Use Java bindings (JRuby only, coming soon)
873
- TreeHaver.backend = :citrus # Use Citrus pure Ruby parser
874
- # NOTE: Portable, all Ruby implementations
875
- # CAVEAT: few major language grammars, but many esoteric grammars
876
- TreeHaver.backend = :parslet # Use Parslet pure Ruby parser
877
- # NOTE: Portable, all Ruby implementations
878
- # CAVEAT: few major language grammars, but many esoteric grammars
879
- ```
880
-
881
- **Auto-selection priority on MRI:** MRI → Rust → FFI → Citrus → Parslet
882
-
883
- You can also set the backend via environment variable:
884
-
885
- ```bash
886
- export TREE_HAVER_BACKEND=rust
887
- ```
888
-
889
- ### Backend Registry
890
-
891
- TreeHaver provides a `BackendRegistry` module that allows external gems to register their backend availability checkers. This enables dynamic backend detection without hardcoding dependencies.
892
-
893
- #### Registering a Backend Availability Checker
894
-
895
- External gems (like `commonmarker-merge`, `markly-merge`, `rbs-merge`) can register their availability checker when loaded:
896
-
897
- ```ruby
898
- # In your gem's backend module
899
- TreeHaver::BackendRegistry.register_availability_checker(:my_backend) do
900
- # Return true if backend is available
901
- require "my_backend_gem"
902
- true
903
- rescue LoadError
904
- false
905
- end
906
- ```
907
-
908
- #### Checking Backend Availability
909
-
910
- ```ruby
911
- # Check if a backend is available
912
- TreeHaver::BackendRegistry.available?(:commonmarker) # => true/false
913
- TreeHaver::BackendRegistry.available?(:markly) # => true/false
914
- TreeHaver::BackendRegistry.available?(:rbs) # => true/false
915
-
916
- # Check if a checker is registered
917
- TreeHaver::BackendRegistry.registered?(:my_backend) # => true/false
918
-
919
- # Get all registered backend names
920
- TreeHaver::BackendRegistry.registered_backends # => [:mri, :rust, :ffi, ...]
921
- ```
922
-
923
- #### How It Works
924
-
925
- 1. Built-in backends (MRI, Rust, FFI, Java, Prism, Psych, Citrus, Parslet) automatically register their checkers when loaded
926
- 2. External gems register their checkers when their backend module is loaded
927
- 3. `TreeHaver::RSpec::DependencyTags` uses the registry to dynamically detect available backends
928
- 4. Results are cached for performance (use `clear_cache!` to reset)
929
-
930
- #### RSpec Integration
931
-
932
- The `BackendRegistry` is used by `TreeHaver::RSpec::DependencyTags` to configure RSpec exclusion filters:
933
-
934
- ```ruby
935
- # In your spec_helper.rb
936
- require "tree_haver/rspec/dependency_tags"
937
-
938
- # Then in specs, use tags to skip tests when backends aren't available
939
- it "requires commonmarker", :commonmarker_backend do
940
- # This test only runs when commonmarker is available
941
- end
942
-
943
- it "requires markly", :markly_backend do
944
- # This test only runs when markly is available
945
- end
946
- ```
947
-
948
- ### Environment Variables
949
-
950
- TreeHaver recognizes several environment variables for configuration:
951
-
952
- **Note**: All path-based environment variables are validated before use. Invalid paths are ignored.
953
-
954
- #### Security Configuration
955
-
956
- - **`TREE_HAVER_TRUSTED_DIRS`**: Comma-separated list of additional trusted directories for grammar libraries
957
-
958
- ```bash
959
- # For Homebrew on Linux and luarocks
960
- export TREE_HAVER_TRUSTED_DIRS="/home/linuxbrew/.linuxbrew/Cellar,~/.local/share/mise/installs/lua"
961
- ```
962
-
963
- Tilde (`~`) is expanded to the user's home directory. Directories listed here are considered safe for `find_library_path_safe`.
964
-
965
- #### Core Runtime Library
966
-
967
- - **`TREE_SITTER_RUNTIME_LIB`**: Absolute path to the core `libtree-sitter` shared library
968
- ```bash
969
- export TREE_SITTER_RUNTIME_LIB=/usr/local/lib/libtree-sitter.so
970
- ```
971
-
972
- If not set, TreeHaver tries these names in order:
973
-
974
- - `tree-sitter`
975
- - `libtree-sitter.so.0`
976
- - `libtree-sitter.so`
977
- - `libtree-sitter.dylib`
978
- - `libtree-sitter.dll`
979
-
980
- #### Language Symbol Resolution
981
-
982
- When loading a language grammar, if you don't specify the `symbol:` parameter, TreeHaver resolves it in this precedence:
983
-
984
- 1. **`TREE_SITTER_LANG_SYMBOL`**: Explicit symbol override
985
- 2. Guessed from filename (e.g., `libtree-sitter-toml.so` → `tree_sitter_toml`)
986
- 3. Default fallback (`tree_sitter_toml`)
987
-
988
- ```bash
989
- export TREE_SITTER_LANG_SYMBOL=tree_sitter_toml
990
- ```
991
-
992
- #### Language Library Paths
993
-
994
- For specific languages, you can set environment variables to point to grammar libraries:
995
-
996
- ```bash
997
- export TREE_SITTER_TOML_PATH=/usr/local/lib/libtree-sitter-toml.so
998
- export TREE_SITTER_JSON_PATH=/usr/local/lib/libtree-sitter-json.so
999
- ```
1000
-
1001
- #### JRuby-Specific: Java Backend Configuration
1002
-
1003
- For the Java backend on JRuby, you need:
1004
-
1005
- 1. **jtreesitter \>= 0.26.0** JAR from Maven Central
1006
- 2. **Tree-sitter runtime library** (`libtree-sitter.so`) version 0.26+
1007
- 3. **Grammar `.so` files** built against tree-sitter 0.26+
1008
-
1009
- ```bash
1010
- # Download jtreesitter JAR (or use bin/setup-jtreesitter)
1011
- export TREE_SITTER_JAVA_JARS_DIR=/path/to/java-tree-sitter/jars
1012
-
1013
- # Point to tree-sitter runtime (must be 0.26+)
1014
- export TREE_SITTER_RUNTIME_LIB=/usr/local/lib/libtree-sitter.so
1015
-
1016
- # Point to grammar libraries (must be built for tree-sitter 0.26+)
1017
- export TREE_SITTER_TOML_PATH=/path/to/libtree-sitter-toml.so
1018
- ```
1019
-
1020
- **Building grammars for Java backend:**
1021
-
1022
- If you get "version mismatch" errors, rebuild the grammar:
1023
-
1024
- ```bash
1025
- # Use the provided build script
1026
- bin/build-grammar toml
1027
-
1028
- # This regenerates parser.c for your tree-sitter version and compiles it
1029
- ```
1030
-
1031
- For more see [docs](https://tree-sitter.github.io/java-tree-sitter/), [maven][jtreesitter], and [source](https://github.com/tree-sitter/java-tree-sitter).
1032
-
1033
- ### Language Registration
1034
-
1035
- Register languages once at application startup for convenient access:
1036
-
1037
- ```ruby
1038
- # Register a TOML grammar
1039
- TreeHaver.register_language(
1040
- :toml,
1041
- path: "/usr/local/lib/libtree-sitter-toml.so",
1042
- symbol: "tree_sitter_toml", # optional, will be inferred if omitted
1043
- )
1044
-
1045
- # Now you can use the convenient helper
1046
- language = TreeHaver::Language.toml
1047
-
1048
- # Or still override path/symbol per-call
1049
- language = TreeHaver::Language.toml(
1050
- path: "/custom/path/libtree-sitter-toml.so",
1051
- )
1052
- ```
1053
-
1054
- ### Grammar Discovery with GrammarFinder
1055
-
1056
- For libraries that need to automatically locate tree-sitter grammars (like the `*-merge` family of gems), TreeHaver provides the `GrammarFinder` utility class. It handles platform-aware grammar discovery without requiring language-specific code in TreeHaver itself.
1057
-
1058
- ```ruby
1059
- # Create a finder for any language
1060
- finder = TreeHaver::GrammarFinder.new(:toml)
1061
-
1062
- # Check if the grammar is available
1063
- if finder.available?
1064
- puts "TOML grammar found at: #{finder.find_library_path}"
1065
- else
1066
- puts finder.not_found_message
1067
- # => "tree-sitter toml grammar not found. Searched: /usr/lib/libtree-sitter-toml.so, ..."
1068
- end
1069
-
1070
- # Register the language if available
1071
- finder.register! if finder.available?
1072
-
1073
- # Now use the registered language
1074
- language = TreeHaver::Language.toml
1075
- ```
1076
-
1077
- #### GrammarFinder Automatic Derivation
1078
-
1079
- Given just the language name, `GrammarFinder` automatically derives:
1080
-
1081
- | Property | Derived Value (for `:toml`) |
1082
- |------------------|------------------------------------------------------|
1083
- | ENV var | `TREE_SITTER_TOML_PATH` |
1084
- | Library filename | `libtree-sitter-toml.so` (Linux) or `.dylib` (macOS) |
1085
- | Symbol name | `tree_sitter_toml` |
1086
-
1087
- #### Search Order
1088
-
1089
- `GrammarFinder` searches for grammars in this order:
1090
-
1091
- 1. **Environment variable**: `TREE_SITTER_<LANG>_PATH` (highest priority)
1092
- 2. **Extra paths**: Custom paths provided at initialization
1093
- 3. **System paths**: Common installation directories (`/usr/lib`, `/usr/local/lib`, `/opt/homebrew/lib`, etc.)
1094
-
1095
- #### Usage in \*-merge Gems
1096
-
1097
- The `GrammarFinder` pattern enables clean integration in language-specific merge gems:
1098
-
1099
- ```ruby
1100
- # In toml-merge
1101
- finder = TreeHaver::GrammarFinder.new(:toml)
1102
- finder.register! if finder.available?
1103
-
1104
- # In json-merge
1105
- finder = TreeHaver::GrammarFinder.new(:json)
1106
- finder.register! if finder.available?
1107
-
1108
- # In bash-merge
1109
- finder = TreeHaver::GrammarFinder.new(:bash)
1110
- finder.register! if finder.available?
1111
- ```
1112
-
1113
- Each gem uses the same API—only the language name changes.
1114
-
1115
- #### Adding Custom Search Paths
1116
-
1117
- For non-standard installations, provide extra search paths:
1118
-
1119
- ```ruby
1120
- finder = TreeHaver::GrammarFinder.new(:toml, extra_paths: [
1121
- "/opt/custom/lib",
1122
- "/home/user/.local/lib",
1123
- ])
1124
- ```
1125
-
1126
- #### Debug Information
1127
-
1128
- Get detailed information about the grammar search:
1129
-
1130
- ```ruby
1131
- finder = TreeHaver::GrammarFinder.new(:toml)
1132
- puts finder.search_info
1133
- # => {
1134
- # language: :toml,
1135
- # env_var: "TREE_SITTER_TOML_PATH",
1136
- # env_value: nil,
1137
- # symbol: "tree_sitter_toml",
1138
- # library_filename: "libtree-sitter-toml.so",
1139
- # search_paths: ["/usr/lib/libtree-sitter-toml.so", ...],
1140
- # found_path: "/usr/lib/libtree-sitter-toml.so",
1141
- # available: true
1142
- # }
1143
- ```
1144
-
1145
- ### Checking Capabilities
1146
-
1147
- Different backends may support different features:
1148
-
1149
- ```ruby
1150
- TreeHaver.capabilities
1151
- # => { backend: :mri, query: true, bytes_field: true }
1152
- # or
1153
- # => { backend: :ffi, parse: true, query: false, bytes_field: true }
1154
- # or
1155
- # => { backend: :citrus, parse: true, query: false, bytes_field: false }
1156
- # or
1157
- # => { backend: :parslet, parse: true, query: false, bytes_field: false }
1158
- ```
1159
-
1160
- ### Compatibility Mode
1161
-
1162
- For codebases migrating from `ruby_tree_sitter`, TreeHaver provides a compatibility shim:
1163
-
1164
- ```ruby
1165
- require "tree_haver/compat"
1166
-
1167
- # Now TreeSitter constants map to TreeHaver
1168
- parser = TreeSitter::Parser.new # Actually creates TreeHaver::Parser
1169
- ```
1170
-
1171
- This is safe and idempotent—if the real `TreeSitter` module is already loaded, the shim does nothing.
1172
-
1173
- #### ⚠️ Important: Exception Hierarchy
1174
-
1175
- **Both ruby\_tree\_sitter v2+ and TreeHaver exceptions inherit from `Exception` (not `StandardError`).**
1176
-
1177
- This design decision follows ruby\_tree\_sitter's lead for thread-safety and signal handling reasons. See [ruby\_tree\_sitter PR \#83](https://github.com/Faveod/ruby-tree-sitter/pull/83) for the rationale.
1178
-
1179
- **What this means for exception handling:**
1180
-
1181
- ```ruby
1182
- # ⚠️ This will NOT catch TreeHaver errors
1183
- begin
1184
- TreeHaver::Language.from_library("/nonexistent.so")
1185
- rescue => e
1186
- puts "Caught!" # Never reached - TreeHaver::Error inherits Exception
1187
- end
1188
-
1189
- # ✅ Explicit rescue is required
1190
- begin
1191
- TreeHaver::Language.from_library("/nonexistent.so")
1192
- rescue TreeHaver::Error => e
1193
- puts "Caught!" # This works
1194
- end
1195
-
1196
- # ✅ Or rescue specific exceptions
1197
- begin
1198
- TreeHaver::Language.from_library("/nonexistent.so")
1199
- rescue TreeHaver::NotAvailable => e
1200
- puts "Grammar not available: #{e.message}"
1201
- end
1202
- ```
1203
-
1204
- **TreeHaver Exception Hierarchy:**
1205
-
1206
- Exception
1207
- └── TreeHaver::Error # Base error class
1208
- ├── TreeHaver::NotAvailable # Backend/grammar not available
1209
- └── TreeHaver::BackendConflict # Backend incompatibility detected
1210
-
1211
- **Compatibility Mode Behavior:**
1212
-
1213
- The compat mode (`require "tree_haver/compat"`) creates aliases but **does not change the exception hierarchy**:
1214
-
1215
- ```ruby
1216
- require "tree_haver/compat"
1217
-
1218
- # TreeSitter constants are now aliases to TreeHaver
1219
- TreeSitter::Error # => TreeHaver::Error (still inherits Exception)
1220
- TreeSitter::Parser # => TreeHaver::Parser
1221
- TreeSitter::Language # => TreeHaver::Language
1222
-
1223
- # Exception handling remains the same
1224
- begin
1225
- TreeSitter::Language.load("missing", "/nonexistent.so")
1226
- rescue TreeSitter::Error => e # Still requires explicit rescue
1227
- puts "Error: #{e.message}"
1228
- end
1229
- ```
1230
-
1231
- **Best Practices:**
1232
-
1233
- 1. **Always use explicit rescue** for TreeHaver errors:
1234
-
1235
- ```ruby
1236
- begin
1237
- finder = TreeHaver::GrammarFinder.new(:toml)
1238
- finder.register! if finder.available?
1239
- language = TreeHaver::Language.toml
1240
- rescue TreeHaver::NotAvailable => e
1241
- warn("TOML grammar not available: #{e.message}")
1242
- # Fallback to another backend or fail gracefully
1243
- end
1244
- ```
1245
-
1246
- 2. **Never rely on `rescue => e`** to catch TreeHaver errors (it won't work)
1247
- **Why inherit from Exception?**
1248
- Following ruby\_tree\_sitter's reasoning:
1249
-
1250
- - **Thread safety**: Prevents accidental catching in thread cleanup code
1251
- - **Signal handling**: Ensures parsing errors don't interfere with SIGTERM/SIGINT
1252
- - **Intentional handling**: Forces developers to explicitly handle parsing errors
1253
- See `lib/tree_haver/compat.rb` for compatibility layer documentation.
1254
-
1255
- ## 🔧 Basic Usage
1256
-
1257
- ### Quick Start
1258
-
1259
- The simplest way to parse code is with `TreeHaver.parser_for`, which handles all the complexity of language loading, grammar discovery, and backend selection:
1260
-
1261
- ```ruby
1262
- require "tree_haver"
1263
-
1264
- # Parse TOML - auto-discovers grammar and falls back to Citrus if needed
1265
- parser = TreeHaver.parser_for(:toml)
1266
- tree = parser.parse("[package]\nname = \"my-app\"")
1267
-
1268
- # Parse JSON
1269
- parser = TreeHaver.parser_for(:json)
1270
- tree = parser.parse('{"key": "value"}')
1271
-
1272
- # Parse Bash
1273
- parser = TreeHaver.parser_for(:bash)
1274
- tree = parser.parse("#!/bin/bash\necho hello")
1275
-
1276
- # With explicit library path
1277
- parser = TreeHaver.parser_for(:toml, library_path: "/custom/path/libtree-sitter-toml.so")
1278
-
1279
- # With Citrus fallback configuration
1280
- parser = TreeHaver.parser_for(
1281
- :toml,
1282
- citrus_config: {gem_name: "toml-rb", grammar_const: "TomlRB::Document"},
1283
- )
1284
- ```
1285
-
1286
- `TreeHaver.parser_for` handles:
1287
-
1288
- 1. Checking if the language is already registered
1289
- 2. Auto-discovering tree-sitter grammar via `GrammarFinder`
1290
- 3. Falling back to Citrus grammar if tree-sitter is unavailable
1291
- 4. Creating and configuring the parser
1292
- 5. Raising `NotAvailable` with a helpful message if nothing works
1293
-
1294
- ### Manual Parser Setup
1295
-
1296
- For more control, you can create parsers manually:
1297
-
1298
- TreeHaver works with any language through its 10 backends. Here are examples for different parsing needs:
1299
-
1300
- #### Parsing with Tree-sitter (Universal Languages)
1301
-
1302
- ```ruby
1303
- require "tree_haver"
1304
-
1305
- # Load a tree-sitter grammar (works with MRI, Rust, FFI, or Java backend)
1306
- language = TreeHaver::Language.from_library(
1307
- "/usr/local/lib/libtree-sitter-toml.so",
1308
- symbol: "tree_sitter_toml",
1309
- )
1310
-
1311
- # Create a parser
1312
- parser = TreeHaver::Parser.new
1313
- parser.language = language
1314
-
1315
- # Parse source code
1316
- source = <<~TOML
1317
- [package]
1318
- name = "my-app"
1319
- version = "1.0.0"
1320
- TOML
1321
-
1322
- tree = parser.parse(source)
1323
-
1324
- # Access the unified Position API (works across all backends)
1325
- root = tree.root_node
1326
- puts "Root type: #{root.type}" # => "document"
1327
- puts "Start line: #{root.start_line}" # => 1 (1-based)
1328
- puts "End line: #{root.end_line}" # => 3
1329
- puts "Position: #{root.source_position}" # => {start_line: 1, end_line: 3, ...}
1330
-
1331
- # Traverse the tree
1332
- root.each do |child|
1333
- puts "Child: #{child.type} at line #{child.start_line}"
1334
- end
1335
- ```
1336
-
1337
- #### Parsing Ruby with Prism
1338
-
1339
- ```ruby
1340
- require "tree_haver"
1341
-
1342
- TreeHaver.backend = :prism
1343
- parser = TreeHaver::Parser.new
1344
- parser.language = TreeHaver::Backends::Prism::Language.ruby
1345
-
1346
- source = <<~RUBY
1347
- class Example
1348
- def hello
1349
- puts "Hello, world!"
1350
- end
1351
- end
1352
- RUBY
1353
-
1354
- tree = parser.parse(source)
1355
- root = tree.root_node
1356
-
1357
- # Find all method definitions
1358
- def find_methods(node, results = [])
1359
- results << node if node.type == "def_node"
1360
- node.children.each { |child| find_methods(child, results) }
1361
- results
1362
- end
1363
-
1364
- methods = find_methods(root)
1365
- methods.each do |method_node|
1366
- pos = method_node.source_position
1367
- puts "Method at lines #{pos[:start_line]}-#{pos[:end_line]}"
1368
- end
1369
- ```
1370
-
1371
- #### Parsing YAML with Psych
1372
-
1373
- ```ruby
1374
- require "tree_haver"
1375
-
1376
- TreeHaver.backend = :psych
1377
- parser = TreeHaver::Parser.new
1378
- parser.language = TreeHaver::Backends::Psych::Language.yaml
1379
-
1380
- source = <<~YAML
1381
- database:
1382
- host: localhost
1383
- port: 5432
1384
- YAML
1385
-
1386
- tree = parser.parse(source)
1387
- root = tree.root_node
1388
-
1389
- # Navigate YAML structure
1390
- def show_structure(node, indent = 0)
1391
- prefix = " " * indent
1392
- puts "#{prefix}#{node.type} (line #{node.start_line})"
1393
- node.children.each { |child| show_structure(child, indent + 1) }
1394
- end
1395
-
1396
- show_structure(root)
1397
- ```
1398
-
1399
- #### Parsing Markdown with Commonmarker or Markly
1400
-
1401
- ```ruby
1402
- require "tree_haver"
1403
-
1404
- # Choose your backend
1405
- TreeHaver.backend = :commonmarker # or :markly for GFM
1406
-
1407
- parser = TreeHaver::Parser.new
1408
- parser.language = TreeHaver::Backends::Commonmarker::Language.markdown
1409
-
1410
- source = <<~MARKDOWN
1411
- # My Document
1412
-
1413
- ## Section
1414
-
1415
- - Item 1
1416
- - Item 2
1417
- MARKDOWN
1418
-
1419
- tree = parser.parse(source)
1420
- root = tree.root_node
1421
-
1422
- # Find all headings
1423
- def find_headings(node, results = [])
1424
- results << node if node.type == "heading"
1425
- node.children.each { |child| find_headings(child, results) }
1426
- results
1427
- end
1428
-
1429
- headings = find_headings(root)
1430
- headings.each do |heading|
1431
- level = heading.header_level
1432
- text = heading.children.map(&:text).join
1433
- puts "H#{level}: #{text} (line #{heading.start_line})"
1434
- end
1435
- ```
1436
-
1437
- ### Using Language Registration
1438
-
1439
- For cleaner code, register languages at startup:
1440
-
1441
- ```ruby
1442
- # At application initialization
1443
- TreeHaver.register_language(
1444
- :toml,
1445
- path: "/usr/local/lib/libtree-sitter-toml.so",
1446
- )
1447
-
1448
- TreeHaver.register_language(
1449
- :json,
1450
- path: "/usr/local/lib/libtree-sitter-json.so",
1451
- )
1452
-
1453
- # Later in your code
1454
- toml_language = TreeHaver::Language.toml
1455
- json_language = TreeHaver::Language.json
1456
-
1457
- parser = TreeHaver::Parser.new
1458
- parser.language = toml_language
1459
- tree = parser.parse(toml_source)
1460
- ```
1461
-
1462
- #### Flexible Language Names
1463
-
1464
- The `name` parameter in `register_language` is an arbitrary identifier you choose—it doesn't
1465
- need to match the actual language name. The actual grammar identity comes from the `path`
1466
- and `symbol` parameters (for tree-sitter) or `grammar_module` (for Citrus/Parslet).
1467
-
1468
- This flexibility is useful for:
1469
-
1470
- - **Aliasing**: Register the same grammar under multiple names
1471
- - **Versioning**: Register different grammar versions (e.g., `:ruby_2`, `:ruby_3`)
1472
- - **Testing**: Use unique names to avoid collisions between tests
1473
- - **Context-specific naming**: Use names that make sense for your application
1474
-
1475
- ```ruby
1476
- # Register the same TOML grammar under different names for different purposes
1477
- TreeHaver.register_language(
1478
- :config_parser, # Custom name for your app
1479
- path: "/usr/local/lib/libtree-sitter-toml.so",
1480
- symbol: "tree_sitter_toml",
1481
- )
1482
-
1483
- TreeHaver.register_language(
1484
- :toml_v1, # Version-specific name
1485
- path: "/usr/local/lib/libtree-sitter-toml.so",
1486
- symbol: "tree_sitter_toml",
1487
- )
1488
-
1489
- # Use your custom names
1490
- config_lang = TreeHaver::Language.config_parser
1491
- versioned_lang = TreeHaver::Language.toml_v1
1492
- ```
1493
-
1494
- ### Parsing Different Languages
1495
-
1496
- TreeHaver works with any tree-sitter grammar:
1497
-
1498
- ```ruby
1499
- # Parse Ruby code
1500
- ruby_lang = TreeHaver::Language.from_library(
1501
- "/path/to/libtree-sitter-ruby.so",
1502
- )
1503
- parser = TreeHaver::Parser.new
1504
- parser.language = ruby_lang
1505
- tree = parser.parse("class Foo; end")
1506
-
1507
- # Parse JavaScript
1508
- js_lang = TreeHaver::Language.from_library(
1509
- "/path/to/libtree-sitter-javascript.so",
1510
- )
1511
- parser.language = js_lang # Reuse the same parser
1512
- tree = parser.parse("const x = 42;")
1513
- ```
1514
-
1515
- ### Walking the AST
1516
-
1517
- TreeHaver provides simple node traversal:
1518
-
1519
- ```ruby
1520
- tree = parser.parse(source)
1521
- root = tree.root_node
1522
-
1523
- # Recursive tree walk
1524
- def walk_tree(node, depth = 0)
1525
- puts "#{" " * depth}#{node.type}"
1526
- node.each { |child| walk_tree(child, depth + 1) }
1527
- end
1528
-
1529
- walk_tree(root)
1530
- ```
1531
-
1532
- ### Incremental Parsing
1533
-
1534
- TreeHaver supports incremental parsing when using the MRI or Rust backends. This is a major performance optimization for editors and IDEs that need to re-parse on every keystroke.
1535
-
1536
- ```ruby
1537
- # Check if current backend supports incremental parsing
1538
- if TreeHaver.capabilities[:incremental]
1539
- puts "Incremental parsing is available!"
1540
- end
1541
-
1542
- # Initial parse
1543
- parser = TreeHaver::Parser.new
1544
- parser.language = language
1545
- tree = parser.parse_string(nil, "x = 1")
1546
-
1547
- # User edits the source: "x = 1" -> "x = 42"
1548
- # Mark the tree as edited (tell tree-sitter what changed)
1549
- tree.edit(
1550
- start_byte: 4, # edit starts at byte 4
1551
- old_end_byte: 5, # old text "1" ended at byte 5
1552
- new_end_byte: 6, # new text "42" ends at byte 6
1553
- start_point: {row: 0, column: 4},
1554
- old_end_point: {row: 0, column: 5},
1555
- new_end_point: {row: 0, column: 6},
1556
- )
1557
-
1558
- # Re-parse incrementally - tree-sitter reuses unchanged nodes
1559
- new_tree = parser.parse_string(tree, "x = 42")
1560
- ```
1561
-
1562
- **Note:** Incremental parsing requires the MRI (`ruby_tree_sitter`), Rust (`tree_stump`), or Java (`java-tree-sitter` / `jtreesitter`) backend. The FFI, Citrus, and Parslet backends do not currently support incremental parsing. You can check support with:
1563
-
1564
- **Note:** `tree_stump` currently requires unreleased fixes in the `main` branch.
1565
-
1566
- ```ruby
1567
- tree.supports_editing? # => true if edit() is available
1568
- ```
1569
-
1570
- ### Error Handling
1571
-
1572
- ```ruby
1573
- begin
1574
- language = TreeHaver::Language.from_library("/path/to/grammar.so")
1575
- rescue TreeHaver::NotAvailable => e
1576
- puts "Failed to load grammar: #{e.message}"
1577
- end
1578
-
1579
- # Check if a backend is available
1580
- if TreeHaver.backend_module.nil?
1581
- puts "No TreeHaver backend is available!"
1582
- puts "Install ruby_tree_sitter (MRI), ffi gem with libtree-sitter, citrus gem, or parslet gem"
1583
- end
1584
- ```
1585
-
1586
- ### Platform-Specific Examples
1587
-
1588
- #### MRI Ruby
1589
-
1590
- On MRI, TreeHaver uses `ruby_tree_sitter` by default:
1591
-
1592
- ```ruby
1593
- # Gemfile
1594
- gem "tree_haver"
1595
- gem "ruby_tree_sitter" # MRI backend
1596
-
1597
- # Code - no changes needed, TreeHaver auto-selects MRI backend
1598
- parser = TreeHaver::Parser.new
1599
- ```
1600
-
1601
- #### JRuby
1602
-
1603
- On JRuby, TreeHaver can use the FFI backend, Java backend, Citrus backend, or Parslet backend:
1604
-
1605
- ##### Option 1: FFI Backend (recommended for tree-sitter grammars)
1606
-
1607
- ```ruby
1608
- # Gemfile
1609
- gem "tree_haver"
1610
- gem "ffi" # Required for FFI backend
1611
-
1612
- # Ensure libtree-sitter is installed on your system
1613
- # On macOS with Homebrew:
1614
- # brew install tree-sitter
1615
-
1616
- # On Ubuntu/Debian:
1617
- # sudo apt-get install libtree-sitter0 libtree-sitter-dev
1618
-
1619
- # Code - TreeHaver auto-selects FFI backend on JRuby
1620
- parser = TreeHaver::Parser.new
1621
- ```
1622
-
1623
- ##### Option 2: Java Backend (native JVM performance)
1624
-
1625
- ```bash
1626
- # 1. Download java-tree-sitter JAR from Maven Central
1627
- mkdir -p vendor/jars
1628
- curl -fSL -o vendor/jars/jtreesitter-0.23.2.jar \
1629
- "https://repo1.maven.org/maven2/io/github/tree-sitter/jtreesitter/0.23.2/jtreesitter-0.23.2.jar"
1630
-
1631
- # 2. Set environment variables
1632
- export CLASSPATH="$(pwd)/vendor/jars:$CLASSPATH"
1633
- export LD_LIBRARY_PATH="/path/to/libtree-sitter/lib:$LD_LIBRARY_PATH"
1634
-
1635
- # 3. Run with JRuby (requires Java 22+ for Foreign Function API)
1636
- JAVA_OPTS="--enable-native-access=ALL-UNNAMED" jruby your_script.rb
1637
- ```
1638
-
1639
- ```ruby
1640
- # Force Java backend
1641
- TreeHaver.backend = :java
1642
-
1643
- # Check if Java backend is available
1644
- if TreeHaver::Backends::Java.available?
1645
- puts "Java backend is ready!"
1646
- puts TreeHaver.capabilities
1647
- # => { backend: :java, parse: true, query: true, bytes_field: true, incremental: true }
1648
- end
1649
- ```
1650
-
1651
- **⚠️ Java Backend Limitation: Symbol Resolution**
1652
-
1653
- The Java backend uses Java's Foreign Function & Memory (FFM) API which loads libraries in isolation. Unlike the system's dynamic linker (`dlopen`), FFM's `SymbolLookup.or()` chains symbol lookups but doesn't resolve dynamic library dependencies.
1654
-
1655
- This means grammar `.so` files with unresolved references to `libtree-sitter.so` symbols won't load correctly. Most grammars from luarocks, npm, or other sources have these dependencies.
1656
-
1657
- **Recommended approach for JRuby:** Use the **FFI backend**:
1658
-
1659
- ```ruby
1660
- # On JRuby, use FFI backend (recommended)
1661
- TreeHaver.backend = :ffi
1662
- ```
1663
-
1664
- The FFI backend uses Ruby's FFI gem which relies on the system's dynamic linker, correctly resolving symbol dependencies between `libtree-sitter.so` and grammar libraries.
1665
-
1666
- The Java backend will work with:
1667
-
1668
- - Grammar JARs built specifically for java-tree-sitter / jtreesitter (self-contained, [docs](https://tree-sitter.github.io/java-tree-sitter/), [maven][jtreesitter], [source](https://github.com/tree-sitter/java-tree-sitter))
1669
- - Grammar `.so` files that statically link tree-sitter
1670
-
1671
- ##### Option 3: Citrus Backend (pure Ruby, portable)
1672
-
1673
- ```ruby
1674
- # Gemfile
1675
- gem "tree_haver"
1676
- gem "citrus" # Pure Ruby parser, zero native dependencies
1677
-
1678
- # Code - Force Citrus backend for maximum portability
1679
- TreeHaver.backend = :citrus
1680
-
1681
- # Check if Citrus backend is available
1682
- if TreeHaver::Backends::Citrus.available?
1683
- puts "Citrus backend is ready!"
1684
- puts TreeHaver.capabilities
1685
- # => { backend: :citrus, parse: true, query: false, bytes_field: false }
1686
- end
1687
- ```
1688
-
1689
- **⚠️ Citrus Backend Limitations:**
1690
-
1691
- - Uses Citrus grammars (not tree-sitter grammars)
1692
- - No incremental parsing support
1693
- - No query API
1694
- - Pure Ruby performance (slower than native backends)
1695
- - Best for: prototyping, environments without native extension support, teaching
1696
-
1697
- ##### Option 4: Parslet Backend (pure Ruby, portable)
1698
-
1699
- ```ruby
1700
- # Gemfile
1701
- gem "tree_haver"
1702
- gem "parslet" # Pure Ruby parser, zero native dependencies
1703
-
1704
- # Code - Force Parslet backend for maximum portability
1705
- TreeHaver.backend = :parslet
1706
-
1707
- # Check if Parslet backend is available
1708
- if TreeHaver::Backends::Parslet.available?
1709
- puts "Parslet backend is ready!"
1710
- puts TreeHaver.capabilities
1711
- # => { backend: :parslet, parse: true, query: false, bytes_field: false }
1712
- end
1713
- ```
1714
-
1715
- **⚠️ Parslet Backend Limitations:**
1716
-
1717
- - Uses Parslet grammars (not tree-sitter grammars)
1718
- - No incremental parsing support
1719
- - No query API
1720
- - Pure Ruby performance (slower than native backends)
1721
- - Best for: prototyping, environments without native extension support, teaching
1722
-
1723
- #### TruffleRuby
1724
-
1725
- TruffleRuby can use the MRI, FFI, Citrus, or Parslet backend:
1726
-
1727
- ```ruby
1728
- # Use FFI backend (recommended for tree-sitter grammars)
1729
- TreeHaver.backend = :ffi
1730
-
1731
- # Or try MRI backend if ruby_tree_sitter compiles on your TruffleRuby version
1732
- TreeHaver.backend = :mri
1733
-
1734
- # Or use Citrus backend for zero native dependencies
1735
- TreeHaver.backend = :citrus
1736
-
1737
- # Or use Parslet backend for zero native dependencies
1738
- TreeHaver.backend = :parslet
1739
- ```
1740
-
1741
- ### Advanced: Thread-Safe Backend Switching
1742
-
1743
- TreeHaver provides `with_backend` for thread-safe, temporary backend switching. This is
1744
- essential for testing, benchmarking, and applications that need different backends in
1745
- different contexts.
1746
-
1747
- #### Testing with Multiple Backends
1748
-
1749
- Test the same code path with different backends using `with_backend`:
1750
-
1751
- ```ruby
1752
- # In your test setup
1753
- RSpec.describe("MyParser") do
1754
- # Test with each available backend
1755
- [:mri, :rust, :citrus, :parslet].each do |backend_name|
1756
- context "with #{backend_name} backend" do
1757
- it "parses correctly" do
1758
- TreeHaver.with_backend(backend_name) do
1759
- parser = TreeHaver::Parser.new
1760
- result = parser.parse("x = 42")
1761
- expect(result.root_node.type).to(eq("document"))
1762
- end
1763
- # Backend automatically restored after block
1764
- end
1765
- end
1766
- end
1767
- end
1768
- ```
1769
-
1770
- #### Thread Isolation
1771
-
1772
- Each thread can use a different backend safely—`with_backend` uses thread-local storage:
1773
-
1774
- ```ruby
1775
- threads = []
1776
-
1777
- threads << Thread.new do
1778
- TreeHaver.with_backend(:mri) do
1779
- # This thread uses MRI backend
1780
- parser = TreeHaver::Parser.new
1781
- 100.times { parser.parse("x = 1") }
1782
- end
1783
- end
1784
-
1785
- threads << Thread.new do
1786
- TreeHaver.with_backend(:citrus) do
1787
- # This thread uses Citrus backend simultaneously
1788
- parser = TreeHaver::Parser.new
1789
- 100.times { parser.parse("x = 1") }
1790
- end
1791
- end
1792
-
1793
- threads << Thread.new do
1794
- TreeHaver.with_backend(:parslet) do
1795
- # This thread uses Parslet backend simultaneously
1796
- parser = TreeHaver::Parser.new
1797
- 100.times { parser.parse("x = 1") }
1798
- end
1799
- end
1800
-
1801
- threads.each(&:join)
1802
- ```
1803
-
1804
- #### Nested Blocks
1805
-
1806
- `with_backend` supports nesting—inner blocks override outer blocks:
1807
-
1808
- ```ruby
1809
- TreeHaver.with_backend(:rust) do
1810
- puts TreeHaver.effective_backend # => :rust
1811
-
1812
- TreeHaver.with_backend(:citrus) do
1813
- puts TreeHaver.effective_backend # => :citrus
1814
- end
1815
-
1816
- TreeHaver.with_backend(:parslet) do
1817
- puts TreeHaver.effective_backend # => :parslet
1818
- end
1819
-
1820
- puts TreeHaver.effective_backend # => :rust (restored)
1821
- end
1822
- ```
1823
-
1824
- #### Fallback Pattern
1825
-
1826
- Try one backend, fall back to another on failure:
1827
-
1828
- ```ruby
1829
- def parse_with_fallback(source)
1830
- TreeHaver.with_backend(:mri) do
1831
- TreeHaver::Parser.new.tap { |p| p.language = load_language }.parse(source)
1832
- end
1833
- rescue TreeHaver::NotAvailable
1834
- # Fall back to Citrus if MRI backend unavailable
1835
- TreeHaver.with_backend(:citrus) do
1836
- TreeHaver::Parser.new.tap { |p| p.language = load_language }.parse(source)
1837
- end
1838
- rescue TreeHaver::NotAvailable
1839
- # Fall back to Parslet if Citrus backend unavailable
1840
- TreeHaver.with_backend(:parslet) do
1841
- TreeHaver::Parser.new.tap { |p| p.language = load_language }.parse(source)
1842
- end
1843
- end
1844
- ```
1845
-
1846
- ### Complete Real-World Example
1847
-
1848
- Here's a practical example that extracts package names from a TOML file:
1849
-
1850
- ```ruby
1851
- require "tree_haver"
1852
-
1853
- # Setup
1854
- TreeHaver.register_language(
1855
- :toml,
1856
- path: "/usr/local/lib/libtree-sitter-toml.so",
1857
- )
1858
-
1859
- def extract_package_name(toml_content)
1860
- # Create parser
1861
- parser = TreeHaver::Parser.new
1862
- parser.language = TreeHaver::Language.toml
1863
-
1864
- # Parse
1865
- tree = parser.parse(toml_content)
1866
- root = tree.root_node
1867
-
1868
- # Find [package] table
1869
- root.each do |child|
1870
- next unless child.type == "table"
1871
-
1872
- child.each do |table_elem|
1873
- if table_elem.type == "pair"
1874
- # Look for name = "..." pair
1875
- key = table_elem.each.first&.type
1876
- # In a real implementation, you'd extract the text value
1877
- # This is simplified for demonstration
1878
- end
1879
- end
1880
- end
1881
- end
1882
-
1883
- # Usage
1884
- toml = <<~TOML
1885
- [package]
1886
- name = "awesome-app"
1887
- version = "2.0.0"
1888
- TOML
1889
-
1890
- package_name = extract_package_name(toml)
1891
- ```
1892
-
1893
- ### 🧪 RSpec Integration
1894
-
1895
- TreeHaver provides shared RSpec helpers for conditional test execution based on dependency availability. This is useful for testing code that uses optional backends.
1896
-
1897
- ```ruby
1898
- # In your spec_helper.rb
1899
- require "tree_haver/rspec"
1900
- ```
1901
-
1902
- This automatically configures RSpec with exclusion filters for all TreeHaver dependencies. Use tags to conditionally run tests:
1903
-
1904
- ```ruby
1905
- # Runs only when FFI backend is available
1906
- it "parses with FFI", :ffi do
1907
- # ...
1908
- end
1909
-
1910
- # Runs only when ruby_tree_sitter gem is available
1911
- it "uses MRI backend", :mri_backend do
1912
- # ...
1913
- end
1914
-
1915
- # Runs only when tree-sitter-toml grammar works
1916
- it "parses TOML", :tree_sitter_toml do
1917
- # ...
1918
- end
1919
-
1920
- # Runs only when any markdown backend is available
1921
- it "parses markdown", :markdown_backend do
1922
- # ...
1923
- end
1924
- ```
1925
-
1926
- **Available Tags:**
1927
-
1928
- Tags follow a naming convention:
1929
-
1930
- - `*_backend` = TreeHaver backends (mri, rust, ffi, java, prism, psych, commonmarker, markly, citrus, parslet, rbs)
1931
- - `*_engine` = Ruby engines (mri, jruby, truffleruby)
1932
- - `*_grammar` = tree-sitter grammar files (.so)
1933
- - `*_parsing` = any parsing capability for a language (combines multiple backends/grammars)
1934
- - `*_gem` = specific library gems
1935
-
1936
- | Tag | Description |
1937
- |-------------------------|---------------------------------------------------------------------------|
1938
- | **Backend Tags** | |
1939
- | `:ffi_backend` | FFI backend available (dynamic check, legacy alias: `:ffi`) |
1940
- | `:ffi_backend_only` | FFI backend in isolation (won't trigger MRI check) |
1941
- | `:mri_backend` | ruby\_tree\_sitter gem available |
1942
- | `:mri_backend_only` | MRI backend in isolation (won't trigger FFI check) |
1943
- | `:rust_backend` | tree\_stump gem available |
1944
- | `:java_backend` | Java backend available (JRuby + jtreesitter) |
1945
- | `:prism_backend` | Prism gem available |
1946
- | `:psych_backend` | Psych available (stdlib) |
1947
- | `:commonmarker_backend` | commonmarker gem available |
1948
- | `:markly_backend` | markly gem available |
1949
- | `:citrus_backend` | Citrus gem available |
1950
- | `:parslet_backend` | Parslet gem available |
1951
- | `:rbs_backend` | RBS gem available (official RBS parser, MRI only) |
1952
- | **Engine Tags** | |
1953
- | `:mri_engine` | Running on MRI (CRuby) |
1954
- | `:jruby_engine` | Running on JRuby |
1955
- | `:truffleruby_engine` | Running on TruffleRuby |
1956
- | **Grammar Tags** | |
1957
- | `:libtree_sitter` | libtree-sitter.so is loadable via FFI |
1958
- | `:bash_grammar` | tree-sitter-bash grammar available and parsing works |
1959
- | `:toml_grammar` | tree-sitter-toml grammar available and parsing works |
1960
- | `:json_grammar` | tree-sitter-json grammar available and parsing works |
1961
- | `:jsonc_grammar` | tree-sitter-jsonc grammar available and parsing works |
1962
- | `:rbs_grammar` | tree-sitter-rbs grammar available and parsing works |
1963
- | **Parsing Tags** | |
1964
- | `:toml_parsing` | Any TOML parser available (tree-sitter OR toml-rb/Citrus OR toml/Parslet) |
1965
- | `:markdown_parsing` | Any markdown parser available (commonmarker OR markly) |
1966
- | `:rbs_parsing` | Any RBS parser available (rbs gem OR tree-sitter-rbs) |
1967
- | `:native_parsing` | Native tree-sitter backend and grammar available |
1968
- | **Library Tags** | |
1969
- | `:toml_rb_gem` | toml-rb gem available (Citrus backend for TOML) |
1970
- | `:toml_gem` | toml gem available (Parslet backend for TOML) |
1971
- | `:rbs_gem` | rbs gem available (official RBS parser) |
1972
-
1973
- All tags have negated versions (e.g., `:not_mri_backend`, `:not_jruby_engine`, `:not_toml_parsing`) for testing fallback behavior.
1974
-
1975
- **Debug Output:**
1976
-
1977
- Set `TREE_HAVER_DEBUG=1` to print a dependency summary at the start of your test suite:
1978
-
1979
- ```bash
1980
- TREE_HAVER_DEBUG=1 bundle exec rspec
1981
- ```
1982
-
1983
- ## 🦷 FLOSS Funding
1984
-
1985
- While kettle-rb tools are free software and will always be, the project would benefit immensely from some funding.
1986
- Raising a monthly budget of... "dollars" would make the project more sustainable.
1987
-
1988
- We welcome both individual and corporate sponsors\! We also offer a
1989
- wide array of funding channels to account for your preferences
1990
- (although currently [Open Collective][🖇osc] is our preferred funding platform).
1991
-
1992
- **If you're working in a company that's making significant use of kettle-rb tools we'd
1993
- appreciate it if you suggest to your company to become a kettle-rb sponsor.**
1994
-
1995
- You can support the development of kettle-rb tools via
1996
- [GitHub Sponsors][🖇sponsor],
1997
- [Liberapay][⛳liberapay],
1998
- [PayPal][🖇paypal],
1999
- [Open Collective][🖇osc]
2000
- and [Tidelift][🏙️entsup-tidelift].
2001
-
2002
- | 📍 NOTE |
2003
- |----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
2004
- | If doing a sponsorship in the form of donation is problematic for your company <br/> from an accounting standpoint, we'd recommend the use of Tidelift, <br/> where you can get a support-like subscription instead. |
2005
-
2006
- ### Open Collective for Individuals
2007
-
2008
- Support us with a monthly donation and help us continue our activities. \[[Become a backer][🖇osc-backers]\]
2009
-
2010
- NOTE: [kettle-readme-backers][kettle-readme-backers] updates this list every day, automatically.
2011
-
2012
- <!-- OPENCOLLECTIVE-INDIVIDUALS:START -->
2013
- No backers yet. Be the first!
2014
- <!-- OPENCOLLECTIVE-INDIVIDUALS:END -->
2015
-
2016
- ### Open Collective for Organizations
2017
-
2018
- Become a sponsor and get your logo on our README on GitHub with a link to your site. \[[Become a sponsor][🖇osc-sponsors]\]
2019
-
2020
- NOTE: [kettle-readme-backers][kettle-readme-backers] updates this list every day, automatically.
2021
-
2022
- <!-- OPENCOLLECTIVE-ORGANIZATIONS:START -->
2023
- No sponsors yet. Be the first!
2024
- <!-- OPENCOLLECTIVE-ORGANIZATIONS:END -->
2025
-
2026
- [kettle-readme-backers]: https://github.com/kettle-rb/tree_haver/blob/main/exe/kettle-readme-backers
2027
-
2028
- ### Another way to support open-source
2029
-
2030
- I’m driven by a passion to foster a thriving open-source community – a space where people can tackle complex problems, no matter how small. Revitalizing libraries that have fallen into disrepair, and building new libraries focused on solving real-world challenges, are my passions. I was recently affected by layoffs, and the tech jobs market is unwelcoming. I’m reaching out here because your support would significantly aid my efforts to provide for my family, and my farm (11 🐔 chickens, 2 🐶 dogs, 3 🐰 rabbits, 8 🐈‍ cats).
2031
-
2032
- If you work at a company that uses my work, please encourage them to support me as a corporate sponsor. My work on gems you use might show up in `bundle fund`.
2033
-
2034
- I’m developing a new library, [floss\_funding][🖇floss-funding-gem], designed to empower open-source developers like myself to get paid for the work we do, in a sustainable way. Please give it a look.
2035
-
2036
- **[Floss-Funding.dev][🖇floss-funding.dev]: 👉️ No network calls. 👉️ No tracking. 👉️ No oversight. 👉️ Minimal crypto hashing. 💡 Easily disabled nags**
2037
-
2038
- [![OpenCollective Backers][🖇osc-backers-i]][🖇osc-backers] [![OpenCollective Sponsors][🖇osc-sponsors-i]][🖇osc-sponsors] [![Sponsor Me on Github][🖇sponsor-img]][🖇sponsor] [![Liberapay Goal Progress][⛳liberapay-img]][⛳liberapay] [![Donate on PayPal][🖇paypal-img]][🖇paypal] [![Buy me a coffee][🖇buyme-small-img]][🖇buyme] [![Donate on Polar][🖇polar-img]][🖇polar] [![Donate to my FLOSS efforts at ko-fi.com][🖇kofi-img]][🖇kofi] [![Donate to my FLOSS efforts using Patreon][🖇patreon-img]][🖇patreon]
2039
-
2040
- ## 🔐 Security
2041
-
2042
- See [SECURITY.md][🔐security].
2043
-
2044
- ## 🤝 Contributing
2045
-
2046
- If you need some ideas of where to help, you could work on adding more code coverage,
2047
- or if it is already 💯 (see [below](#code-coverage)) check [reek](REEK), [issues][🤝gh-issues], or [PRs][🤝gh-pulls],
2048
- or use the gem and think about how it could be better.
2049
-
2050
- We [![Keep A Changelog][📗keep-changelog-img]][📗keep-changelog] so if you make changes, remember to update it.
2051
-
2052
- See [CONTRIBUTING.md][🤝contributing] for more detailed instructions.
2053
-
2054
- ### 🚀 Release Instructions
2055
-
2056
- See [CONTRIBUTING.md][🤝contributing].
2057
-
2058
- ### Code Coverage
2059
-
2060
- [![Coverage Graph][🏀codecov-g]][🏀codecov]
2061
-
2062
- [![Coveralls Test Coverage][🏀coveralls-img]][🏀coveralls]
2063
-
2064
- [![QLTY Test Coverage][🏀qlty-covi]][🏀qlty-cov]
2065
-
2066
- ### 🪇 Code of Conduct
2067
-
2068
- Everyone interacting with this project's codebases, issue trackers,
2069
- chat rooms and mailing lists agrees to follow the [![Contributor Covenant 2.1][🪇conduct-img]][🪇conduct].
2070
-
2071
- ## 🌈 Contributors
2072
-
2073
- [![Contributors][🖐contributors-img]][🖐contributors]
2074
-
2075
- Made with [contributors-img][🖐contrib-rocks].
2076
-
2077
- Also see GitLab Contributors: <https://gitlab.com/kettle-rb/tree_haver/-/graphs/main>
2078
-
2079
- <details>
2080
- <summary>⭐️ Star History</summary>
2081
-
2082
- <a href="https://star-history.com/#kettle-rb/tree_haver&Date">
2083
- <picture>
2084
- <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=kettle-rb/tree_haver&type=Date&theme=dark" />
2085
- <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=kettle-rb/tree_haver&type=Date" />
2086
- <img alt="Star History Chart" src="https://api.star-history.com/svg?repos=kettle-rb/tree_haver&type=Date" />
2087
- </picture>
2088
- </a>
2089
-
2090
- </details>
2091
-
2092
- ## 📌 Versioning
2093
-
2094
- This Library adheres to [![Semantic Versioning 2.0.0][📌semver-img]][📌semver].
2095
- Violations of this scheme should be reported as bugs.
2096
- Specifically, if a minor or patch version is released that breaks backward compatibility,
2097
- a new version should be immediately released that restores compatibility.
2098
- Breaking changes to the public API will only be introduced with new major versions.
2099
-
2100
- > dropping support for a platform is both obviously and objectively a breaking change <br/>
2101
- > —Jordan Harband ([@ljharb](https://github.com/ljharb), maintainer of SemVer) [in SemVer issue 716][📌semver-breaking]
2102
-
2103
- I understand that policy doesn't work universally ("exceptions to every rule\!"),
2104
- but it is the policy here.
2105
- As such, in many cases it is good to specify a dependency on this library using
2106
- the [Pessimistic Version Constraint][📌pvc] with two digits of precision.
2107
-
2108
- For example:
2109
-
2110
- ```ruby
2111
- spec.add_dependency("tree_haver", "~> 5.0")
2112
- ```
2113
-
2114
- <details markdown="1">
2115
- <summary>📌 Is "Platform Support" part of the public API? More details inside.</summary>
2116
-
2117
- SemVer should, IMO, but doesn't explicitly, say that dropping support for specific Platforms
2118
- is a *breaking change* to an API, and for that reason the bike shedding is endless.
2119
-
2120
- To get a better understanding of how SemVer is intended to work over a project's lifetime,
2121
- read this article from the creator of SemVer:
2122
-
2123
- - ["Major Version Numbers are Not Sacred"][📌major-versions-not-sacred]
2124
-
2125
- </details>
2126
-
2127
- See [CHANGELOG.md][📌changelog] for a list of releases.
2128
-
2129
- ## 📄 License
2130
-
2131
- The gem is available as open source under the terms of
2132
- the [MIT License][📄license] [![License: MIT][📄license-img]][📄license-ref].
2133
- See [LICENSE.txt][📄license] for the official [Copyright Notice][📄copyright-notice-explainer].
2134
-
2135
- ### © Copyright
2136
-
2137
- <ul>
2138
- <li>
2139
- Copyright (c) 2025-2026 Peter H. Boling, of
2140
- <a href="https://discord.gg/3qme4XHNKN">
2141
- Galtzo.com
2142
- <picture>
2143
- <img src="https://logos.galtzo.com/assets/images/galtzo-floss/avatar-128px-blank.svg" alt="Galtzo.com Logo (Wordless) by Aboling0, CC BY-SA 4.0" width="24">
2144
- </picture>
2145
- </a>, and tree_haver contributors.
2146
- </li>
2147
- </ul>
2148
-
2149
- ## 🤑 A request for help
2150
-
2151
- Maintainers have teeth and need to pay their dentists.
2152
- After getting laid off in an RIF in March, and encountering difficulty finding a new one,
2153
- I began spending most of my time building open source tools.
2154
- I'm hoping to be able to pay for my kids' health insurance this month,
2155
- so if you value the work I am doing, I need your support.
2156
- Please consider sponsoring me or the project.
2157
-
2158
- To join the community or get help 👇️ Join the Discord.
2159
-
2160
- [![Live Chat on Discord][✉️discord-invite-img-ftb]][🖼️galtzo-discord]
2161
-
2162
- To say "thanks\!" ☝️ Join the Discord or 👇️ send money.
2163
-
2164
- [![Sponsor kettle-rb/tree\_haver on Open Source Collective][🖇osc-all-bottom-img]][🖇osc] 💌 [![Sponsor me on GitHub Sponsors][🖇sponsor-bottom-img]][🖇sponsor] 💌 [![Sponsor me on Liberapay][⛳liberapay-bottom-img]][⛳liberapay] 💌 [![Donate on PayPal][🖇paypal-bottom-img]][🖇paypal]
2165
-
2166
- ### Please give the project a star ⭐ ♥.
2167
-
2168
- Thanks for RTFM. ☺️
2169
-
2170
- [⛳liberapay-img]: https://img.shields.io/liberapay/goal/pboling.svg?logo=liberapay&color=a51611&style=flat
2171
- [⛳liberapay-bottom-img]: https://img.shields.io/liberapay/goal/pboling.svg?style=for-the-badge&logo=liberapay&color=a51611
2172
- [⛳liberapay]: https://liberapay.com/pboling/donate
2173
- [🖇osc-all-img]: https://img.shields.io/opencollective/all/kettle-rb
2174
- [🖇osc-sponsors-img]: https://img.shields.io/opencollective/sponsors/kettle-rb
2175
- [🖇osc-backers-img]: https://img.shields.io/opencollective/backers/kettle-rb
2176
- [🖇osc-backers]: https://opencollective.com/kettle-rb#backer
2177
- [🖇osc-backers-i]: https://opencollective.com/kettle-rb/backers/badge.svg?style=flat
2178
- [🖇osc-sponsors]: https://opencollective.com/kettle-rb#sponsor
2179
- [🖇osc-sponsors-i]: https://opencollective.com/kettle-rb/sponsors/badge.svg?style=flat
2180
- [🖇osc-all-bottom-img]: https://img.shields.io/opencollective/all/kettle-rb?style=for-the-badge
2181
- [🖇osc-sponsors-bottom-img]: https://img.shields.io/opencollective/sponsors/kettle-rb?style=for-the-badge
2182
- [🖇osc-backers-bottom-img]: https://img.shields.io/opencollective/backers/kettle-rb?style=for-the-badge
2183
- [🖇osc]: https://opencollective.com/kettle-rb
2184
- [🖇sponsor-img]: https://img.shields.io/badge/Sponsor_Me!-pboling.svg?style=social&logo=github
2185
- [🖇sponsor-bottom-img]: https://img.shields.io/badge/Sponsor_Me!-pboling-blue?style=for-the-badge&logo=github
2186
- [🖇sponsor]: https://github.com/sponsors/pboling
2187
- [🖇polar-img]: https://img.shields.io/badge/polar-donate-a51611.svg?style=flat
2188
- [🖇polar]: https://polar.sh/pboling
2189
- [🖇kofi-img]: https://img.shields.io/badge/ko--fi-%E2%9C%93-a51611.svg?style=flat
2190
- [🖇kofi]: https://ko-fi.com/O5O86SNP4
2191
- [🖇patreon-img]: https://img.shields.io/badge/patreon-donate-a51611.svg?style=flat
2192
- [🖇patreon]: https://patreon.com/galtzo
2193
- [🖇buyme-small-img]: https://img.shields.io/badge/buy_me_a_coffee-%E2%9C%93-a51611.svg?style=flat
2194
- [🖇buyme-img]: https://img.buymeacoffee.com/button-api/?text=Buy%20me%20a%20latte&emoji=&slug=pboling&button_colour=FFDD00&font_colour=000000&font_family=Cookie&outline_colour=000000&coffee_colour=ffffff
2195
- [🖇buyme]: https://www.buymeacoffee.com/pboling
2196
- [🖇paypal-img]: https://img.shields.io/badge/donate-paypal-a51611.svg?style=flat&logo=paypal
2197
- [🖇paypal-bottom-img]: https://img.shields.io/badge/donate-paypal-a51611.svg?style=for-the-badge&logo=paypal&color=0A0A0A
2198
- [🖇paypal]: https://www.paypal.com/paypalme/peterboling
2199
- [🖇floss-funding.dev]: https://floss-funding.dev
2200
- [🖇floss-funding-gem]: https://github.com/galtzo-floss/floss_funding
2201
- [✉️discord-invite]: https://discord.gg/3qme4XHNKN
2202
- [✉️discord-invite-img-ftb]: https://img.shields.io/discord/1373797679469170758?style=for-the-badge&logo=discord
2203
- [✉️ruby-friends-img]: https://img.shields.io/badge/daily.dev-%F0%9F%92%8E_Ruby_Friends-0A0A0A?style=for-the-badge&logo=dailydotdev&logoColor=white
2204
- [✉️ruby-friends]: https://app.daily.dev/squads/rubyfriends
2205
- [✇bundle-group-pattern]: https://gist.github.com/pboling/4564780
2206
- [⛳️gem-namespace]: https://github.com/kettle-rb/tree_haver
2207
- [⛳️namespace-img]: https://img.shields.io/badge/namespace-TreeHaver-3C2D2D.svg?style=square&logo=ruby&logoColor=white
2208
- [⛳️gem-name]: https://bestgems.org/gems/tree_haver
2209
- [⛳️name-img]: https://img.shields.io/badge/name-tree__haver-3C2D2D.svg?style=square&logo=rubygems&logoColor=red
2210
- [⛳️tag-img]: https://img.shields.io/github/tag/kettle-rb/tree_haver.svg
2211
- [⛳️tag]: http://github.com/kettle-rb/tree_haver/releases
2212
- [🚂maint-blog]: http://www.railsbling.com/tags/tree_haver
2213
- [🚂maint-blog-img]: https://img.shields.io/badge/blog-railsbling-0093D0.svg?style=for-the-badge&logo=rubyonrails&logoColor=orange
2214
- [🚂maint-contact]: http://www.railsbling.com/contact
2215
- [🚂maint-contact-img]: https://img.shields.io/badge/Contact-Maintainer-0093D0.svg?style=flat&logo=rubyonrails&logoColor=red
2216
- [💖🖇linkedin]: http://www.linkedin.com/in/peterboling
2217
- [💖🖇linkedin-img]: https://img.shields.io/badge/PeterBoling-LinkedIn-0B66C2?style=flat&logo=newjapanprowrestling
2218
- [💖✌️wellfound]: https://wellfound.com/u/peter-boling
2219
- [💖✌️wellfound-img]: https://img.shields.io/badge/peter--boling-orange?style=flat&logo=wellfound
2220
- [💖💲crunchbase]: https://www.crunchbase.com/person/peter-boling
2221
- [💖💲crunchbase-img]: https://img.shields.io/badge/peter--boling-purple?style=flat&logo=crunchbase
2222
- [💖🐘ruby-mast]: https://ruby.social/@galtzo
2223
- [💖🐘ruby-mast-img]: https://img.shields.io/mastodon/follow/109447111526622197?domain=https://ruby.social&style=flat&logo=mastodon&label=Ruby%20@galtzo
2224
- [💖🦋bluesky]: https://bsky.app/profile/galtzo.com
2225
- [💖🦋bluesky-img]: https://img.shields.io/badge/@galtzo.com-0285FF?style=flat&logo=bluesky&logoColor=white
2226
- [💖🌳linktree]: https://linktr.ee/galtzo
2227
- [💖🌳linktree-img]: https://img.shields.io/badge/galtzo-purple?style=flat&logo=linktree
2228
- [💖💁🏼‍♂️devto]: https://dev.to/galtzo
2229
- [💖💁🏼‍♂️devto-img]: https://img.shields.io/badge/dev.to-0A0A0A?style=flat&logo=devdotto&logoColor=white
2230
- [💖💁🏼‍♂️aboutme]: https://about.me/peter.boling
2231
- [💖💁🏼‍♂️aboutme-img]: https://img.shields.io/badge/about.me-0A0A0A?style=flat&logo=aboutme&logoColor=white
2232
- [💖🧊berg]: https://codeberg.org/pboling
2233
- [💖🐙hub]: https://github.org/pboling
2234
- [💖🛖hut]: https://sr.ht/~galtzo/
2235
- [💖🧪lab]: https://gitlab.com/pboling
2236
- [👨🏼‍🏫expsup-upwork]: https://www.upwork.com/freelancers/~014942e9b056abdf86?mp_source=share
2237
- [👨🏼‍🏫expsup-upwork-img]: https://img.shields.io/badge/UpWork-13544E?style=for-the-badge&logo=Upwork&logoColor=white
2238
- [👨🏼‍🏫expsup-codementor]: https://www.codementor.io/peterboling?utm_source=github&utm_medium=button&utm_term=peterboling&utm_campaign=github
2239
- [👨🏼‍🏫expsup-codementor-img]: https://img.shields.io/badge/CodeMentor-Get_Help-1abc9c?style=for-the-badge&logo=CodeMentor&logoColor=white
2240
- [🏙️entsup-tidelift]: https://tidelift.com/subscription/pkg/rubygems-tree_haver?utm_source=rubygems-tree_haver&utm_medium=referral&utm_campaign=readme
2241
- [🏙️entsup-tidelift-img]: https://img.shields.io/badge/Tidelift_and_Sonar-Enterprise_Support-FD3456?style=for-the-badge&logo=sonar&logoColor=white
2242
- [🏙️entsup-tidelift-sonar]: https://blog.tidelift.com/tidelift-joins-sonar
2243
- [💁🏼‍♂️peterboling]: http://www.peterboling.com
2244
- [🚂railsbling]: http://www.railsbling.com
2245
- [📜src-gl-img]: https://img.shields.io/badge/GitLab-FBA326?style=for-the-badge&logo=Gitlab&logoColor=orange
2246
- [📜src-gl]: https://gitlab.com/kettle-rb/tree_haver/
2247
- [📜src-cb-img]: https://img.shields.io/badge/CodeBerg-4893CC?style=for-the-badge&logo=CodeBerg&logoColor=blue
2248
- [📜src-cb]: https://codeberg.org/kettle-rb/tree_haver
2249
- [📜src-gh-img]: https://img.shields.io/badge/GitHub-238636?style=for-the-badge&logo=Github&logoColor=green
2250
- [📜src-gh]: https://github.com/kettle-rb/tree_haver
2251
- [📜docs-cr-rd-img]: https://img.shields.io/badge/RubyDoc-Current_Release-943CD2?style=for-the-badge&logo=readthedocs&logoColor=white
2252
- [📜docs-head-rd-img]: https://img.shields.io/badge/YARD_on_Galtzo.com-HEAD-943CD2?style=for-the-badge&logo=readthedocs&logoColor=white
2253
- [📜gl-wiki]: https://gitlab.com/kettle-rb/tree_haver/-/wikis/home
2254
- [📜gh-wiki]: https://github.com/kettle-rb/tree_haver/wiki
2255
- [📜gl-wiki-img]: https://img.shields.io/badge/wiki-examples-943CD2.svg?style=for-the-badge&logo=gitlab&logoColor=white
2256
- [📜gh-wiki-img]: https://img.shields.io/badge/wiki-examples-943CD2.svg?style=for-the-badge&logo=github&logoColor=white
2257
- [👽dl-rank]: https://bestgems.org/gems/tree_haver
2258
- [👽dl-ranki]: https://img.shields.io/gem/rd/tree_haver.svg
2259
- [👽oss-help]: https://www.codetriage.com/kettle-rb/tree_haver
2260
- [👽oss-helpi]: https://www.codetriage.com/kettle-rb/tree_haver/badges/users.svg
2261
- [👽version]: https://bestgems.org/gems/tree_haver
2262
- [👽versioni]: https://img.shields.io/gem/v/tree_haver.svg
2263
- [🏀qlty-mnt]: https://qlty.sh/gh/kettle-rb/projects/tree_haver
2264
- [🏀qlty-mnti]: https://qlty.sh/gh/kettle-rb/projects/tree_haver/maintainability.svg
2265
- [🏀qlty-cov]: https://qlty.sh/gh/kettle-rb/projects/tree_haver/metrics/code?sort=coverageRating
2266
- [🏀qlty-covi]: https://qlty.sh/gh/kettle-rb/projects/tree_haver/coverage.svg
2267
- [🏀codecov]: https://codecov.io/gh/kettle-rb/tree_haver
2268
- [🏀codecovi]: https://codecov.io/gh/kettle-rb/tree_haver/graph/badge.svg
2269
- [🏀coveralls]: https://coveralls.io/github/kettle-rb/tree_haver?branch=main
2270
- [🏀coveralls-img]: https://coveralls.io/repos/github/kettle-rb/tree_haver/badge.svg?branch=main
2271
- [🖐codeQL]: https://github.com/kettle-rb/tree_haver/security/code-scanning
2272
- [🖐codeQL-img]: https://github.com/kettle-rb/tree_haver/actions/workflows/codeql-analysis.yml/badge.svg
2273
- [🚎2-cov-wf]: https://github.com/kettle-rb/tree_haver/actions/workflows/coverage.yml
2274
- [🚎2-cov-wfi]: https://github.com/kettle-rb/tree_haver/actions/workflows/coverage.yml/badge.svg
2275
- [🚎3-hd-wf]: https://github.com/kettle-rb/tree_haver/actions/workflows/heads.yml
2276
- [🚎3-hd-wfi]: https://github.com/kettle-rb/tree_haver/actions/workflows/heads.yml/badge.svg
2277
- [🚎5-st-wf]: https://github.com/kettle-rb/tree_haver/actions/workflows/style.yml
2278
- [🚎5-st-wfi]: https://github.com/kettle-rb/tree_haver/actions/workflows/style.yml/badge.svg
2279
- [🚎6-s-wf]: https://github.com/kettle-rb/tree_haver/actions/workflows/supported.yml
2280
- [🚎6-s-wfi]: https://github.com/kettle-rb/tree_haver/actions/workflows/supported.yml/badge.svg
2281
- [🚎9-t-wf]: https://github.com/kettle-rb/tree_haver/actions/workflows/truffle.yml
2282
- [🚎9-t-wfi]: https://github.com/kettle-rb/tree_haver/actions/workflows/truffle.yml/badge.svg
2283
- [🚎11-c-wf]: https://github.com/kettle-rb/tree_haver/actions/workflows/current.yml
2284
- [🚎11-c-wfi]: https://github.com/kettle-rb/tree_haver/actions/workflows/current.yml/badge.svg
2285
- [🚎12-crh-wf]: https://github.com/kettle-rb/tree_haver/actions/workflows/dep-heads.yml
2286
- [🚎12-crh-wfi]: https://github.com/kettle-rb/tree_haver/actions/workflows/dep-heads.yml/badge.svg
2287
- [🚎13-🔒️-wf]: https://github.com/kettle-rb/tree_haver/actions/workflows/locked_deps.yml
2288
- [🚎13-🔒️-wfi]: https://github.com/kettle-rb/tree_haver/actions/workflows/locked_deps.yml/badge.svg
2289
- [🚎14-🔓️-wf]: https://github.com/kettle-rb/tree_haver/actions/workflows/unlocked_deps.yml
2290
- [🚎14-🔓️-wfi]: https://github.com/kettle-rb/tree_haver/actions/workflows/unlocked_deps.yml/badge.svg
2291
- [🚎15-🪪-wf]: https://github.com/kettle-rb/tree_haver/actions/workflows/license-eye.yml
2292
- [🚎15-🪪-wfi]: https://github.com/kettle-rb/tree_haver/actions/workflows/license-eye.yml/badge.svg
2293
- [💎ruby-3.2i]: https://img.shields.io/badge/Ruby-3.2-CC342D?style=for-the-badge&logo=ruby&logoColor=white
2294
- [💎ruby-3.3i]: https://img.shields.io/badge/Ruby-3.3-CC342D?style=for-the-badge&logo=ruby&logoColor=white
2295
- [💎ruby-c-i]: https://img.shields.io/badge/Ruby-current-CC342D?style=for-the-badge&logo=ruby&logoColor=green
2296
- [💎ruby-headi]: https://img.shields.io/badge/Ruby-HEAD-CC342D?style=for-the-badge&logo=ruby&logoColor=blue
2297
- [💎truby-23.1i]: https://img.shields.io/badge/Truffle_Ruby-23.1-34BCB1?style=for-the-badge&logo=ruby&logoColor=pink
2298
- [💎truby-c-i]: https://img.shields.io/badge/Truffle_Ruby-current-34BCB1?style=for-the-badge&logo=ruby&logoColor=green
2299
- [💎truby-headi]: https://img.shields.io/badge/Truffle_Ruby-HEAD-34BCB1?style=for-the-badge&logo=ruby&logoColor=blue
2300
- [💎jruby-c-i]: https://img.shields.io/badge/JRuby-current-FBE742?style=for-the-badge&logo=ruby&logoColor=green
2301
- [💎jruby-headi]: https://img.shields.io/badge/JRuby-HEAD-FBE742?style=for-the-badge&logo=ruby&logoColor=blue
2302
- [🤝gh-issues]: https://github.com/kettle-rb/tree_haver/issues
2303
- [🤝gh-pulls]: https://github.com/kettle-rb/tree_haver/pulls
2304
- [🤝gl-issues]: https://gitlab.com/kettle-rb/tree_haver/-/issues
2305
- [🤝gl-pulls]: https://gitlab.com/kettle-rb/tree_haver/-/merge_requests
2306
- [🤝cb-issues]: https://codeberg.org/kettle-rb/tree_haver/issues
2307
- [🤝cb-pulls]: https://codeberg.org/kettle-rb/tree_haver/pulls
2308
- [🤝cb-donate]: https://donate.codeberg.org/
2309
- [🤝contributing]: CONTRIBUTING.md
2310
- [🏀codecov-g]: https://codecov.io/gh/kettle-rb/tree_haver/graphs/tree.svg
2311
- [🖐contrib-rocks]: https://contrib.rocks
2312
- [🖐contributors]: https://github.com/kettle-rb/tree_haver/graphs/contributors
2313
- [🖐contributors-img]: https://contrib.rocks/image?repo=kettle-rb/tree_haver
2314
- [🚎contributors-gl]: https://gitlab.com/kettle-rb/tree_haver/-/graphs/main
2315
- [🪇conduct]: CODE_OF_CONDUCT.md
2316
- [🪇conduct-img]: https://img.shields.io/badge/Contributor_Covenant-2.1-259D6C.svg
2317
- [📌pvc]: http://guides.rubygems.org/patterns/#pessimistic-version-constraint
2318
- [📌semver]: https://semver.org/spec/v2.0.0.html
2319
- [📌semver-img]: https://img.shields.io/badge/semver-2.0.0-259D6C.svg?style=flat
2320
- [📌semver-breaking]: https://github.com/semver/semver/issues/716#issuecomment-869336139
2321
- [📌major-versions-not-sacred]: https://tom.preston-werner.com/2022/05/23/major-version-numbers-are-not-sacred.html
2322
- [📌changelog]: CHANGELOG.md
2323
- [📗keep-changelog]: https://keepachangelog.com/en/1.0.0/
2324
- [📗keep-changelog-img]: https://img.shields.io/badge/keep--a--changelog-1.0.0-34495e.svg?style=flat
2325
- [📌gitmoji]: https://gitmoji.dev
2326
- [📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
2327
- [🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
2328
- [🧮kloc-img]: https://img.shields.io/badge/KLOC-2.543-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
2329
- [🔐security]: SECURITY.md
2330
- [🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
2331
- [📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
2332
- [📄license]: LICENSE.txt
2333
- [📄license-ref]: https://opensource.org/licenses/MIT
2334
- [📄license-img]: https://img.shields.io/badge/License-MIT-259D6C.svg
2335
- [📄license-compat]: https://dev.to/galtzo/how-to-check-license-compatibility-41h0
2336
- [📄license-compat-img]: https://img.shields.io/badge/Apache_Compatible:_Category_A-%E2%9C%93-259D6C.svg?style=flat&logo=Apache
2337
- [📄ilo-declaration]: https://www.ilo.org/declaration/lang--en/index.htm
2338
- [📄ilo-declaration-img]: https://img.shields.io/badge/ILO_Fundamental_Principles-✓-259D6C.svg?style=flat
2339
- [🚎yard-current]: http://rubydoc.info/gems/tree_haver
2340
- [🚎yard-head]: https://tree-haver.galtzo.com
2341
- [💎stone_checksums]: https://github.com/galtzo-floss/stone_checksums
2342
- [💎SHA_checksums]: https://gitlab.com/kettle-rb/tree_haver/-/tree/main/checksums
2343
- [💎rlts]: https://github.com/rubocop-lts/rubocop-lts
2344
- [💎rlts-img]: https://img.shields.io/badge/code_style_&_linting-rubocop--lts-34495e.svg?plastic&logo=ruby&logoColor=white
2345
- [💎appraisal2]: https://github.com/appraisal-rb/appraisal2
2346
- [💎appraisal2-img]: https://img.shields.io/badge/appraised_by-appraisal2-34495e.svg?plastic&logo=ruby&logoColor=white
2347
- [💎d-in-dvcs]: https://railsbling.com/posts/dvcs/put_the_d_in_dvcs/