tree_haver 7.0.0 → 7.1.1

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 (44) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/LICENSE.md +13 -0
  4. data/README.md +1959 -0
  5. data/lib/tree_haver/backend_api.rb +392 -0
  6. data/lib/tree_haver/backend_registry.rb +153 -3
  7. data/lib/tree_haver/backends/citrus.rb +490 -0
  8. data/lib/tree_haver/backends/ffi.rb +1013 -0
  9. data/lib/tree_haver/backends/java.rb +909 -0
  10. data/lib/tree_haver/backends/mri.rb +367 -0
  11. data/lib/tree_haver/backends/parslet.rb +565 -0
  12. data/lib/tree_haver/backends/prism.rb +568 -0
  13. data/lib/tree_haver/backends/psych.rb +379 -0
  14. data/lib/tree_haver/backends/rust.rb +243 -0
  15. data/lib/tree_haver/backends/tslp.rb +274 -0
  16. data/lib/tree_haver/base/comment.rb +320 -0
  17. data/lib/tree_haver/base/language.rb +98 -0
  18. data/lib/tree_haver/base/node.rb +330 -0
  19. data/lib/tree_haver/base/parser.rb +28 -0
  20. data/lib/tree_haver/base/point.rb +48 -0
  21. data/lib/tree_haver/base/tree.rb +128 -0
  22. data/lib/tree_haver/citrus_grammar_finder.rb +213 -0
  23. data/lib/tree_haver/contracts.rb +661 -96
  24. data/lib/tree_haver/grammar_finder.rb +429 -0
  25. data/lib/tree_haver/kaitai_backend.rb +2 -2
  26. data/lib/tree_haver/language.rb +294 -0
  27. data/lib/tree_haver/language_pack.rb +17 -166
  28. data/lib/tree_haver/language_registry.rb +221 -0
  29. data/lib/tree_haver/library_path_utils.rb +80 -0
  30. data/lib/tree_haver/node.rb +588 -0
  31. data/lib/tree_haver/parser.rb +445 -0
  32. data/lib/tree_haver/parslet_grammar_finder.rb +217 -0
  33. data/lib/tree_haver/path_validator.rb +356 -0
  34. data/lib/tree_haver/peg_backends.rb +8 -8
  35. data/lib/tree_haver/point.rb +27 -0
  36. data/lib/tree_haver/rspec/dependency_tags.rb +56 -0
  37. data/lib/tree_haver/rspec.rb +3 -0
  38. data/lib/tree_haver/tree.rb +267 -0
  39. data/lib/tree_haver/version.rb +5 -3
  40. data/lib/tree_haver.rb +613 -8
  41. data/sig/tree_haver.rbs +6 -0
  42. data.tar.gz.sig +0 -0
  43. metadata +314 -13
  44. metadata.gz.sig +0 -0
data/README.md ADDED
@@ -0,0 +1,1959 @@
1
+ <a href="https://github.com/structuredmerge"><img alt="structuredmerge Logo by GitHub" src="https://github.com/structuredmerge.png?size=192" width="12%" align="right"/></a> <a href="https://github.com/structuredmerge/structuredmerge-ruby/tree/main/gems/tree_haver"><img alt="tree_haver Logo by Aboling0, CC BY-SA 4.0" src="https://logos.galtzo.com/assets/images/structuredmerge/structuredmerge-ruby/tree_haver/avatar-128px.svg" width="12%" align="right"/></a>
2
+
3
+ # 🌴 TreeHaver
4
+
5
+ [![Version][👽versioni]][👽version] [![Ruby Users Forum][✉️ruby-forum-top-img]][✉️ruby-forum] [![GitHub tag (latest SemVer)][⛳️tag-img]][⛳️tag] [![License: AGPL-3.0-only OR PolyForm-Small-Business-1.0.0][📄license-img]][📄license] [![Total downloads][👽dl-ranki]][👽dl-rank] [![CI Current][🚎11-c-wfi]][🚎11-c-wf]
6
+
7
+ `if ci_badges.map(&:color).detect { it != "green"}` ☝️ [let me know on Discord][✉️discord-invite] or [RubyForum][✉️ruby-forum], as I may have missed the notification.
8
+
9
+ ---
10
+
11
+ `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.
12
+
13
+ [![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 at ko-fi.com][🖇kofi-img]][🖇kofi]
14
+
15
+ <details markdown="1">
16
+ <summary>👣 How will this project approach the September 2025 hostile takeover of RubyGems? 🚑️</summary>
17
+
18
+ I've summarized my thoughts in [this blog post](https://dev.to/galtzo/hostile-takeover-of-rubygems-my-thoughts-5hlo).
19
+
20
+ </details>
21
+
22
+ ## 🌻 Synopsis <a href="https://discord.gg/3qme4XHNKN"><img alt="Galtzo FLOSS Logo by Aboling0, CC BY-SA 4.0" src="https://logos.galtzo.com/assets/images/galtzo-floss/avatar-128px.svg" width="8%" align="right"/></a> <a href="https://ruby-toolbox.com"><img alt="ruby-lang Logo, Yukihiro Matsumoto, Ruby Visual Identity Team, CC BY-SA 2.5" src="https://logos.galtzo.com/assets/images/ruby-lang/avatar-128px.svg" width="8%" align="right"/></a>
23
+
24
+ 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.
25
+
26
+ ### The Adapter Pattern: Like Faraday, but for Parsing
27
+
28
+ 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:
29
+
30
+ | Gem | Unified API for | Backend Examples |
31
+ |-----------------|-----------------|---------------------------------------------------------------------------|
32
+ | **Faraday** | HTTP requests | Net::HTTP, Typhoeus, Patron, Excon |
33
+ | **multi\_json** | JSON parsing | Oj, Yajl, JSON gem |
34
+ | **multi\_xml** | XML parsing | Nokogiri, LibXML, Ox |
35
+ | **TreeHaver** | Code parsing | TSLP, MRI, Rust, FFI, Java, Prism, Psych, Commonmarker, Markly, RBS, Citrus, Parslet, Kaitai |
36
+
37
+ **Learn once, write anywhere.**
38
+
39
+ **Write once, run anywhere.**
40
+
41
+ Just as Faraday lets you swap HTTP adapters without changing your code, TreeHaver lets merge providers report and select parsing backends through one registry. The high-level StructuredMerge providers default to the tree-sitter language-pack path when it supports the requested language, while the lower-level `TreeHaver::Parser` facade can still use native tree-sitter adapters and Ruby parser backends directly.
42
+
43
+ ```ruby
44
+ # Your code stays the same regardless of backend
45
+ parser = TreeHaver::Parser.new
46
+ parser.language = TreeHaver::Language.from_library("/path/to/grammar.so")
47
+ tree = parser.parse(source_code)
48
+
49
+ # TreeHaver::Parser automatically picks the best available parser facade backend:
50
+ # - MRI: ruby_tree_sitter, tree_stump, ffi, prism, psych, citrus, parslet
51
+ # - JRuby: java-tree-sitter / jtreesitter, ffi, prism, psych, citrus, parslet
52
+ # - TruffleRuby: prism, psych, citrus, parslet
53
+ # (the FFI tree-sitter adapter is not used on TruffleRuby because of struct-by-value limits)
54
+ ```
55
+
56
+ ### Key Features
57
+
58
+ - **Universal Ruby Support**: Works on MRI Ruby, JRuby, and TruffleRuby
59
+ - **Backend Registry** - Choose the right backend for your needs:
60
+ - **Tree-sitter Backends** (high-performance, incremental parsing):
61
+ - **Tree-sitter Language Pack (TSLP)**: Default provider path for StructuredMerge language-family gems that use `tree-sitter-language-pack`
62
+ - **MRI Backend**: Leverages [`ruby_tree_sitter`][ruby_tree_sitter] gem (C extension, fastest on MRI)
63
+ - **Rust Backend**: Uses [`tree_stump`][tree_stump] gem (Rust with precompiled binaries)
64
+ - **Note**: Use `tree_stump` v0.2.0 or newer (fixes are released).
65
+ - **FFI Backend**: Pure Ruby FFI bindings to `libtree-sitter` (JRuby only; TruffleRuby's FFI doesn't support tree-sitter's struct-by-value returns)
66
+ - **Java Backend**: Native Java integration for JRuby with [`java-tree-sitter`](https://github.com/tree-sitter/java-tree-sitter) / [`jtreesitter`][jtreesitter] grammar JARs
67
+ - **Language-Specific Backends** (native parser integration):
68
+ - **Prism Backend**: Ruby's official parser ([Prism][prism], stdlib in Ruby 3.4+)
69
+ - **Psych Backend**: Ruby's YAML parser ([Psych][psych], stdlib)
70
+ - **Commonmarker Backend**: Fast Markdown parser ([Commonmarker][commonmarker], comrak Rust)
71
+ - **Markly Backend**: GitHub Flavored Markdown ([Markly][markly], cmark-gfm C)
72
+ - **RBS Backend**: Official RBS parser integration registered by `rbs-merge`
73
+ - **Pure Ruby Provider Backends**:
74
+ - **Citrus Backend**: Pure Ruby PEG parsing via [`citrus`][citrus] (no native dependencies)
75
+ - **Parslet Backend**: Pure Ruby PEG parsing via [`parslet`][parslet] (no native dependencies)
76
+ - **Binary Schema Support**:
77
+ - **Kaitai Struct Backend**: Backend reference and capability profile for binary schema analysis
78
+ - **Automatic Backend Selection**: Intelligently selects the best backend for your Ruby implementation
79
+ - **Language Agnostic**: Parse any language - Ruby, Markdown, YAML, JSON, Bash, TOML, JavaScript, etc.
80
+ - **Grammar Discovery**: Built-in `GrammarFinder` utility for registration-first tree-sitter grammar resolution
81
+ - **Unified Position API**: Consistent `start_line`, `end_line`, `source_position` across all backends
82
+ - **Thread-Safe**: Built-in language registry with thread-safe caching
83
+ - **Minimal API Surface**: Simple, focused API that covers the most common use cases
84
+
85
+ ### Backend Requirements
86
+
87
+ TreeHaver has minimal dependencies and automatically selects the best backend for your Ruby implementation. Each backend has specific version requirements:
88
+
89
+ #### MRI Backend (ruby\_tree\_sitter, C extensions)
90
+
91
+ **Requires `ruby_tree_sitter` v2.0+**
92
+
93
+ TreeHaver normalizes backend failures behind `TreeHaver::Error` subclasses, which inherit from `StandardError`.
94
+
95
+ **Exception Mapping**: TreeHaver catches `TreeSitter::TreeSitterError` and its subclasses where the MRI adapter exposes them, converting them to `TreeHaver::NotAvailable` while preserving the original error message. This provides a consistent exception API across backends:
96
+
97
+ | ruby\_tree\_sitter Exception | TreeHaver Exception | When It Occurs |
98
+ |-----------------------------------|---------------------------|----------------------------------------------|
99
+ | `TreeSitter::ParserNotFoundError` | `TreeHaver::NotAvailable` | Parser library file cannot be loaded |
100
+ | `TreeSitter::LanguageLoadError` | `TreeHaver::NotAvailable` | Language symbol loads but returns nothing |
101
+ | `TreeSitter::SymbolNotFoundError` | `TreeHaver::NotAvailable` | Symbol not found in library |
102
+ | `TreeSitter::ParserVersionError` | `TreeHaver::NotAvailable` | Parser version incompatible with tree-sitter |
103
+ | `TreeSitter::QueryCreationError` | `TreeHaver::NotAvailable` | Query creation fails |
104
+
105
+ ```ruby
106
+ # MRI tree-sitter backend
107
+ gem "ruby_tree_sitter", "~> 2.0", require: false
108
+ ```
109
+
110
+ #### Rust Backend (tree\_stump)
111
+
112
+ **MRI Ruby only** - Does not work on JRuby or TruffleRuby.
113
+
114
+ 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.
115
+
116
+ - **JRuby**: Cannot load native `.so` extensions (runs on JVM)
117
+ - **TruffleRuby**: magnus/rb-sys are incompatible with TruffleRuby's C API emulation
118
+
119
+ ```ruby
120
+ # Rust tree-sitter backend (MRI only)
121
+ gem "tree_stump", "~> 0.2.0"
122
+ ```
123
+
124
+ #### FFI Backend
125
+
126
+ **MRI and JRuby only** - Does not work on TruffleRuby.
127
+
128
+ Requires the `ffi` gem and a system installation of `libtree-sitter`.
129
+
130
+ - **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`.
131
+
132
+ ```ruby
133
+ # Add to your Gemfile for FFI backend (MRI and JRuby)
134
+ gem "ffi", ">= 1.15", "< 2.0"
135
+ ```
136
+
137
+ ```bash
138
+ # Install libtree-sitter on your system:
139
+ # macOS
140
+ brew install tree-sitter
141
+
142
+ # Ubuntu/Debian
143
+ apt-get install libtree-sitter0 libtree-sitter-dev
144
+
145
+ # Fedora
146
+ dnf install tree-sitter tree-sitter-devel
147
+ ```
148
+
149
+ #### Citrus Backend
150
+
151
+ Pure Ruby PEG parser with no native dependencies:
152
+
153
+ ```ruby
154
+ # Add to your Gemfile for Citrus backend
155
+ gem "citrus", "~> 3.0"
156
+ ```
157
+
158
+ #### Parslet Backend
159
+
160
+ Pure Ruby PEG parser with no native dependencies:
161
+
162
+ ```ruby
163
+ # Add to your Gemfile for Parslet backend
164
+ gem "parslet", "~> 2.0"
165
+ ```
166
+
167
+ #### Java Backend (JRuby only)
168
+
169
+ **Requires jtreesitter \>= 0.26.0** from Maven Central. Older versions are not supported due to breaking API changes.
170
+
171
+ ```ruby
172
+ # No gem dependency - uses JRuby's built-in Java integration
173
+ # Download the JAR:
174
+ # curl -L -o jtreesitter-0.26.0.jar \
175
+ # "https://repo1.maven.org/maven2/io/github/tree-sitter/jtreesitter/0.26.0/jtreesitter-0.26.0.jar"
176
+
177
+ # Set environment variable:
178
+ # export TREE_SITTER_JAVA_JARS_DIR=/path/to/jars
179
+ ```
180
+
181
+ **Also requires**:
182
+
183
+ - Tree-sitter runtime library (`libtree-sitter.so`) version 0.26+ (must match jtreesitter version)
184
+ - Grammar `.so` files built against tree-sitter 0.26+ (or rebuilt with `tree-sitter generate`)
185
+
186
+ ### Version Requirements for Tree-Sitter Backends
187
+
188
+ #### tree-sitter Runtime Library
189
+
190
+ 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.
191
+
192
+ ```bash
193
+ # Check your tree-sitter version
194
+ tree-sitter --version # Should be 0.26.0 or newer for Java backend
195
+
196
+ # macOS
197
+ brew install tree-sitter
198
+
199
+ # Ubuntu/Debian
200
+ apt-get install libtree-sitter0 libtree-sitter-dev
201
+
202
+ # Fedora
203
+ dnf install tree-sitter tree-sitter-devel
204
+ ```
205
+
206
+ #### jtreesitter (Java Backend)
207
+
208
+ **The Java backend requires jtreesitter \>= 0.26.0.** This version introduced breaking API changes:
209
+
210
+ - `Parser.parse()` returns `Optional<Tree>` instead of `Tree`
211
+ - `Tree.getRootNode()` returns `Node` directly (not `Optional<Node>`)
212
+ - `Node.getChild()`, `getParent()`, `getNextSibling()`, `getPrevSibling()` return `Optional<Node>`
213
+ - `Language.load(name)` was removed; use `SymbolLookup` API instead
214
+ Older versions of jtreesitter are **NOT supported**.
215
+
216
+ ```bash
217
+ # Download jtreesitter 0.26.0 from Maven Central
218
+ curl -L -o jtreesitter-0.26.0.jar \
219
+ "https://repo1.maven.org/maven2/io/github/tree-sitter/jtreesitter/0.26.0/jtreesitter-0.26.0.jar"
220
+
221
+ # Or use the provided setup script
222
+ bin/setup-jtreesitter
223
+ ```
224
+
225
+ Set the environment variable to point to your JAR directory:
226
+
227
+ ```bash
228
+ export TREE_SITTER_JAVA_JARS_DIR=/path/to/jars
229
+ ```
230
+
231
+ #### Grammar ABI Compatibility
232
+
233
+ **CRITICAL**: Grammars must be built against a compatible tree-sitter version.
234
+
235
+ 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:
236
+
237
+ Failed to load tree_sitter_toml
238
+ Version mismatch detected: The grammar was built against tree-sitter < 0.26
239
+
240
+ You need to rebuild the grammar from source:
241
+
242
+ ```bash
243
+ # Use the provided build script
244
+ bin/build-grammar toml
245
+
246
+ # Or manually:
247
+ git clone https://github.com/tree-sitter-grammars/tree-sitter-toml
248
+ cd tree-sitter-toml
249
+ tree-sitter generate # Regenerates parser.c for your tree-sitter version
250
+ cc -shared -fPIC -o libtree-sitter-toml.so src/parser.c src/scanner.c -I src
251
+ ```
252
+
253
+ **Grammar sources for common languages:**
254
+
255
+ | Language | Repository |
256
+ |----------|--------------------------------------------------|
257
+ | TOML | [tree-sitter-grammars/tree-sitter-toml][ts-toml] |
258
+ | JSON | [tree-sitter/tree-sitter-json][ts-json] |
259
+ | JSONC | [WhyNotHugo/tree-sitter-jsonc][ts-jsonc] |
260
+ | Bash | [tree-sitter/tree-sitter-bash][ts-bash] |
261
+
262
+ #### TruffleRuby Limitations
263
+
264
+ TruffleRuby has **no working tree-sitter backend**:
265
+
266
+ - **FFI**: TruffleRuby's FFI doesn't support `STRUCT_BY_VALUE` return types (used by `ts_tree_root_node`, `ts_node_child`, etc.)
267
+ - **MRI/Rust**: C and Rust extensions require MRI's C API internals (`RBasic.flags`, `rb_gc_writebarrier`, etc.) that TruffleRuby doesn't expose
268
+ TruffleRuby users should use: **Prism** (Ruby), **Psych** (YAML), **Citrus/Parslet** (e.g., TOML via toml-rb/toml), or potentially **Commonmarker/Markly** (Markdown).
269
+
270
+ #### JRuby Limitations
271
+
272
+ JRuby runs on the JVM and **cannot load native `.so` extensions via Ruby's C API**:
273
+
274
+ - **MRI/Rust**: C and Rust extensions simply cannot be loaded
275
+ - **FFI**: Works\! JRuby has excellent FFI support
276
+ - **Java**: Works\! The Java backend uses jtreesitter (requires \>= 0.26.0)
277
+ JRuby users should use: **Java backend** (best performance, full API) or **FFI backend** for tree-sitter, plus **Prism**, **Psych**, **Citrus/Parslet** for other formats.
278
+
279
+ ### Why TreeHaver?
280
+
281
+ tree-sitter is a powerful parser generator that creates incremental parsers for many programming languages. However, integrating it into Ruby applications can be challenging:
282
+
283
+ - MRI-based C extensions don't work on JRuby
284
+ - FFI-based solutions may not be optimal for MRI
285
+ - Managing different backends for different Ruby implementations is cumbersome
286
+ 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.
287
+
288
+ ### Comparison with Other Ruby AST / Parser Bindings
289
+
290
+ | Feature | [tree\_haver][📜src-gh] (this gem) | [ruby\_tree\_sitter][ruby_tree_sitter] | [tree\_stump][tree_stump] | [citrus][citrus] | [parslet][parslet] |
291
+ |---------------------------|-------------------------------------------------|----------------------------------------|---------------------------|------------------|--------------------|
292
+ | **MRI Ruby** | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes |
293
+ | **JRuby** | ✅ Yes (FFI, Java, Citrus, or Parslet backend) | ❌ No | ❌ No | ✅ Yes | ✅ Yes |
294
+ | **TruffleRuby** | ✅ Yes (FFI, Citrus, or Parslet) | ❌ No | ❓ Unknown | ✅ Yes | ✅ Yes |
295
+ | **Backend** | Multi (MRI C, Rust, FFI, Java, Citrus, Parslet) | C extension only | Rust extension | Pure Ruby | Pure Ruby |
296
+ | **Incremental Parsing** | ✅ Via MRI C/Rust/Java backend | ✅ Yes | ✅ Yes | ❌ No | ❌ No |
297
+ | **Query API** | ⚡ Via MRI/Rust/Java backend | ✅ Yes | ✅ Yes | ❌ No | ❌ No |
298
+ | **Grammar Discovery** | ✅ Built-in `GrammarFinder` | ❌ Manual | ❌ Manual | ❌ Manual | ❌ Manual |
299
+ | **Security Validations** | ✅ `PathValidator` | ❌ No | ❌ No | ❌ No | ❌ No |
300
+ | **Language Registration** | ✅ Thread-safe registry | ❌ No | ❌ No | ❌ No | ❌ No |
301
+ | **Native Performance** | ⚡ Backend-dependent | ✅ Native C | ✅ Native Rust | ❌ Pure Ruby | ❌ Pure Ruby |
302
+ | **Precompiled Binaries** | ⚡ Via Rust backend | ✅ Yes | ✅ Yes | ✅ Pure Ruby | ✅ Pure Ruby |
303
+ | **Zero Native Deps** | ⚡ Via Citrus/Parslet backend | ❌ No | ❌ No | ✅ Yes | ✅ Yes |
304
+ | **Minimum Ruby** | 3.2+ | 3.0+ | 3.1+ | 0+ | 0+ |
305
+
306
+ **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.
307
+
308
+ **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.
309
+
310
+ **Note:** Use `tree_stump` v0.2.0 or newer (fixes are released).
311
+
312
+ #### When to Use Each
313
+
314
+ **Choose TreeHaver when:**
315
+
316
+ - You need JRuby or TruffleRuby support
317
+ - You're building a library that should work across Ruby implementations
318
+ - You want automatic grammar discovery and security validations
319
+ - You want flexibility to switch backends without code changes
320
+ - You need incremental parsing with a unified API
321
+
322
+ **Choose ruby\_tree\_sitter directly when:**
323
+
324
+ - You only target MRI Ruby
325
+ - You need the full Query API without abstraction
326
+ - You want the most battle-tested C bindings
327
+ - You don't need TreeHaver's grammar discovery
328
+
329
+ **Choose tree\_stump directly when:**
330
+
331
+ - You only target MRI Ruby
332
+ - You prefer Rust-based native extensions
333
+ - You want precompiled binaries without system dependencies
334
+ - You don't need TreeHaver's grammar discovery
335
+ - **Note:** Use `tree_stump` v0.2.0 or newer (fixes are released).
336
+
337
+ **Choose citrus or parslet directly when:**
338
+
339
+ - You need zero native dependencies (pure Ruby)
340
+ - You're using a Citrus or Parslet grammar (not tree-sitter grammars)
341
+ - Performance is less critical than portability
342
+ - You don't need TreeHaver's unified API
343
+
344
+ [citrus]: https://github.com/mjackson/citrus
345
+ [parslet]: https://github.com/kschiess/parslet
346
+ [ruby_tree_sitter]: https://github.com/Faveod/ruby-tree-sitter
347
+ [tree_stump]: https://github.com/joker1007/tree_stump
348
+ [jtreesitter]: https://central.sonatype.com/artifact/io.github.tree-sitter/jtreesitter
349
+ [prism]: https://github.com/ruby/prism
350
+ [psych]: https://github.com/ruby/psych
351
+ [commonmarker]: https://github.com/gjtorikian/commonmarker
352
+ [markly]: https://github.com/ioquatix/markly
353
+ [ts-toml]: https://github.com/tree-sitter-grammars/tree-sitter-toml
354
+ [ts-json]: https://github.com/tree-sitter/tree-sitter-json
355
+ [ts-jsonc]: https://gitlab.com/WhyNotHugo/tree-sitter-jsonc
356
+ [ts-bash]: https://github.com/tree-sitter/tree-sitter-bash
357
+
358
+ ## 💡 Info you can shake a stick at
359
+
360
+ | Tokens to Remember | [![Gem name][⛳️name-img]][⛳️gem-name] [![Gem namespace][⛳️namespace-img]][⛳️gem-namespace] |
361
+ |-------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
362
+ | Works with MRI Ruby 4 | [![Ruby current Compat][💎ruby-c-i]][🚎11-c-wf]|
363
+ | Support & Community | [![Join Me on Daily.dev's RubyFriends][✉️ruby-friends-img]][✉️ruby-friends] [![Get help from RubyForum][✉️ruby-forum-img]][✉️ruby-forum] [![Live Chat on Discord][✉️discord-invite-img-ftb]][✉️discord-invite] [![Get help from me on Upwork][👨🏼‍🏫expsup-upwork-img]][👨🏼‍🏫expsup-upwork] [![Get help from me on Codementor][👨🏼‍🏫expsup-codementor-img]][👨🏼‍🏫expsup-codementor] |
364
+ | 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!][🧮kloc-img]][🧮kloc] |
365
+ | 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] |
366
+ | Compliance | [![License: AGPL-3.0-only OR PolyForm-Small-Business-1.0.0][📄license-img]][📄license] [![Apache license compatibility: Category X][📄license-compat-img]][📄license-compat] [![📄ilo-declaration-img]][📄ilo-declaration] [![Security Policy][🔐security-img]][🔐security] [![Contributor Covenant 2.1][🪇conduct-img]][🪇conduct] [![SemVer 2.0.0][📌semver-img]][📌semver] |
367
+ | 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] |
368
+ | 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] |
369
+ | `...` 💖 | [![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] |
370
+
371
+ ### Compatibility
372
+
373
+ Compatible with MRI Ruby 4.0.0+, and concordant releases of JRuby, and TruffleRuby.
374
+ CI workflows and Appraisals are generated for MRI Ruby 4.0.0+.
375
+ This test floor is configured by `ruby.test_minimum` in `.kettle-jem.yml` and
376
+ may be higher than the gem's runtime compatibility floor when legacy Rubies are
377
+ not practical for the current toolchain.
378
+
379
+ <a href="https://github.com/kettle-dev"><img alt="kettle-dev Logo by Aboling0, CC BY-SA 4.0" src="https://logos.galtzo.com/assets/images/kettle-dev/avatar-128px.svg" width="14%" align="right"/></a>
380
+
381
+ The _amazing_ test matrix is powered by the kettle-dev stack.
382
+
383
+ <details markdown="1">
384
+ <summary>How kettle-dev manages complexity in tests</summary>
385
+
386
+ | Gem | Source | Role | Total downloads |
387
+ |-----|--------|------|---------------------|
388
+ | [appraisal2](https://clickgems.clickhouse.com/dashboard/appraisal2) | [GitHub](https://github.com/appraisal-rb/appraisal2) | multi-dependency Appraisal matrix generation | [![Total downloads for appraisal2](https://img.shields.io/gem/dt/appraisal2.svg?style=flat-square)](https://clickgems.clickhouse.com/dashboard/appraisal2) |
389
+ | [appraisal2-rubocop](https://clickgems.clickhouse.com/dashboard/appraisal2-rubocop) | [GitHub](https://github.com/appraisal-rb/appraisal2-rubocop) | RuboCop Appraisal generator integration | [![Total downloads for appraisal2-rubocop](https://img.shields.io/gem/dt/appraisal2-rubocop.svg?style=flat-square)](https://clickgems.clickhouse.com/dashboard/appraisal2-rubocop) |
390
+ | [kettle-dev](https://clickgems.clickhouse.com/dashboard/kettle-dev) | [GitHub](https://github.com/kettle-dev/kettle-dev) | development, release, and CI workflow tooling | [![Total downloads for kettle-dev](https://img.shields.io/gem/dt/kettle-dev.svg?style=flat-square)](https://clickgems.clickhouse.com/dashboard/kettle-dev) |
391
+ | [kettle-jem](https://clickgems.clickhouse.com/dashboard/kettle-jem) | [GitHub](https://github.com/kettle-dev/kettle-jem) | Appraisals & CI workflow templates | [![Total downloads for kettle-jem](https://img.shields.io/gem/dt/kettle-jem.svg?style=flat-square)](https://clickgems.clickhouse.com/dashboard/kettle-jem) |
392
+ | [kettle-soup-cover](https://clickgems.clickhouse.com/dashboard/kettle-soup-cover) | [GitHub](https://github.com/kettle-dev/kettle-soup-cover) | SimpleCov coverage policy and reporting | [![Total downloads for kettle-soup-cover](https://img.shields.io/gem/dt/kettle-soup-cover.svg?style=flat-square)](https://clickgems.clickhouse.com/dashboard/kettle-soup-cover) |
393
+ | [kettle-test](https://clickgems.clickhouse.com/dashboard/kettle-test) | [GitHub](https://github.com/kettle-dev/kettle-test) | standard test runner and coverage harness | [![Total downloads for kettle-test](https://img.shields.io/gem/dt/kettle-test.svg?style=flat-square)](https://clickgems.clickhouse.com/dashboard/kettle-test) |
394
+ | [rubocop-lts](https://clickgems.clickhouse.com/dashboard/rubocop-lts) | [GitHub](https://github.com/rubocop-lts/rubocop-lts) | Ruby-version-aware linting | [![Total downloads for rubocop-lts](https://img.shields.io/gem/dt/rubocop-lts.svg?style=flat-square)](https://clickgems.clickhouse.com/dashboard/rubocop-lts) |
395
+ | [turbo_tests2](https://clickgems.clickhouse.com/dashboard/turbo_tests2) | [GitHub](https://github.com/galtzo-floss/turbo_tests2) | parallel test execution | [![Total downloads for turbo_tests2](https://img.shields.io/gem/dt/turbo_tests2.svg?style=flat-square)](https://clickgems.clickhouse.com/dashboard/turbo_tests2) |
396
+
397
+ </details>
398
+
399
+ ## ✨ Installation
400
+
401
+ Install the gem and add to the application's Gemfile by executing:
402
+
403
+ ```console
404
+ bundle add tree_haver
405
+ ```
406
+
407
+ If bundler is not being used to manage dependencies, install the gem by executing:
408
+
409
+ ```console
410
+ gem install tree_haver
411
+ ```
412
+
413
+ ## ⚙️ Configuration
414
+
415
+ ### Available Backends
416
+
417
+ TreeHaver exposes a backend registry for parser facades, language-family providers, and binary schema support. The high-level StructuredMerge providers default to `tslp` / `kreuzberg-language-pack` when the language pack supports the requested format; the low-level `TreeHaver::Parser` facade uses `auto` to select among parser modules that expose the Parser/Language/Tree API.
418
+
419
+ #### Tree-sitter Backends (Universal Parsing)
420
+
421
+ | Backend | Description | Performance | Portability |
422
+ |---------|-------------|-------------|-------------|
423
+ | **TSLP** | `tree-sitter-language-pack`; default StructuredMerge provider path | Fast | Universal where the gem supports the language |
424
+ | **Kreuzberg Language Pack** | Stable provider id alias used by StructuredMerge family gems | Fast | Universal where `tree-sitter-language-pack` is available |
425
+ | **Auto** | Auto-selects the best `TreeHaver::Parser` facade backend | Varies | Universal |
426
+ | **MRI** | C extension via ruby\_tree\_sitter | Fastest | MRI only |
427
+ | **Rust** | Precompiled via tree\_stump | Very fast | MRI only |
428
+ | **FFI** | Dynamic linking via FFI | Fast | MRI/JRuby where `libtree-sitter` is available |
429
+ | **Java** | JNI bindings (jtreesitter \>= 0.26.0) | Very fast | JRuby only |
430
+
431
+ #### Language-Specific Backends (Native Parser Integration)
432
+
433
+ | Backend | Description | Performance | Portability |
434
+ |---------|-------------|-------------|-------------|
435
+ | **Prism** | Ruby's official parser | Very fast | Universal |
436
+ | **Psych** | Ruby's YAML parser (stdlib) | Very fast | Universal |
437
+ | **Commonmarker** | Markdown via comrak (Rust) | Very fast | MRI/JRuby/TruffleRuby where the gem is available |
438
+ | **Markly** | GFM via cmark-gfm (C) | Very fast | MRI/JRuby/TruffleRuby where the gem is available |
439
+ | **RBS** | Official RBS parser integration | Fast | Universal where the `rbs` gem is available |
440
+ | **Citrus** | Pure Ruby parsing | Slower | Universal |
441
+ | **Parslet** | Pure Ruby parsing | Slower | Universal |
442
+
443
+ #### Binary Schema Support
444
+
445
+ | Backend | Description | Performance | Portability |
446
+ |---------|-------------|-------------|-------------|
447
+ | **Kaitai Struct** | Backend reference and feature profile for binary schema analysis | Varies | Universal once a schema adapter is supplied |
448
+
449
+ **`TreeHaver::Parser` Auto-selection contract:** `:auto` never means
450
+ unregistered parser discovery. It selects the first registered TreeHaver backend
451
+ module whose availability check passes. Built-in tree-sitter facade priority is
452
+ MRI/Rust/FFI on MRI and Java/FFI on JRuby, followed by registered provider
453
+ modules such as Prism, Psych, Citrus, and Parslet when those providers have been
454
+ registered for the requested language. Explicit backend requests fail closed if
455
+ that backend is unavailable or has no parser registered for the requested
456
+ language.
457
+
458
+ **Known Issues:**
459
+
460
+ - \*MRI + Bash: ABI incompatibility (use FFI instead)
461
+ - \*Rust + Bash: Version mismatch (use FFI instead)
462
+ **Backend Requirements:**
463
+
464
+ ```ruby
465
+ # Tree-sitter backends
466
+ gem "tree-sitter-language-pack" # TSLP / Kreuzberg language-pack provider
467
+ gem "ruby_tree_sitter", "~> 2.0" # MRI backend
468
+ gem "tree_stump" # Rust backend
469
+ gem "ffi", ">= 1.15", "< 2.0" # FFI backend
470
+ # Java backend: no gem required (uses JRuby's built-in JNI)
471
+
472
+ # Language-specific backends
473
+ gem "prism", "~> 1.0" # Ruby parsing (stdlib in Ruby 3.4+)
474
+ # Psych: no gem required (Ruby stdlib)
475
+ gem "commonmarker", ">= 0.23" # Markdown parsing (comrak)
476
+ gem "markly", "~> 0.11" # GFM parsing (cmark-gfm)
477
+ gem "rbs" # RBS parsing
478
+
479
+ # Pure Ruby fallbacks
480
+ gem "citrus", "~> 3.0" # Citrus backend
481
+ gem "parslet", "~> 2.0" # Parslet backend
482
+ # Plus grammar gems: toml-rb (citrus), toml (parslet), dhall, finitio, etc.
483
+ ```
484
+
485
+ **Force Specific Backend:**
486
+
487
+ ```ruby
488
+ # Tree-sitter backends
489
+ TreeHaver.backend = :tslp # Force tree-sitter-language-pack where supported
490
+ TreeHaver.backend = :mri # Force MRI backend (ruby_tree_sitter)
491
+ TreeHaver.backend = :rust # Force Rust backend (tree_stump)
492
+ TreeHaver.backend = :ffi # Force FFI backend
493
+ TreeHaver.backend = :java # Force Java backend (JRuby only)
494
+
495
+ # Language-specific backends
496
+ TreeHaver.backend = :prism # Force Prism (Ruby parsing)
497
+ TreeHaver.backend = :psych # Force Psych (YAML parsing)
498
+ TreeHaver.backend = :commonmarker # Force Commonmarker (Markdown)
499
+ TreeHaver.backend = :markly # Force Markly (GFM Markdown)
500
+ TreeHaver.backend = :rbs # Force RBS parser integration
501
+ TreeHaver.backend = :citrus # Force Citrus (Pure Ruby PEG)
502
+ TreeHaver.backend = :parslet # Force Parslet (Pure Ruby PEG)
503
+
504
+ # Auto-selection (default)
505
+ TreeHaver.backend = :auto # Let TreeHaver choose
506
+ ```
507
+
508
+ **Block-based Backend Switching:**
509
+
510
+ Use `with_backend` to temporarily switch backends for a specific block of code.
511
+ This is thread-safe and supports nesting—the previous backend is automatically
512
+ restored when the block exits (even if an exception is raised).
513
+
514
+ ```ruby
515
+ # Temporarily use a specific backend
516
+ TreeHaver.with_backend(:mri) do
517
+ parser = TreeHaver::Parser.new
518
+ tree = parser.parse(source)
519
+ # All operations in this block use the MRI backend
520
+ end
521
+ # Backend is restored to its previous value here
522
+
523
+ # Nested blocks work correctly
524
+ TreeHaver.with_backend(:rust) do
525
+ # Uses :rust
526
+ TreeHaver.with_backend(:citrus) do
527
+ # Uses :citrus
528
+ parser = TreeHaver::Parser.new
529
+ end
530
+ # Back to :rust
531
+ TreeHaver.with_backend(:parslet) do
532
+ # Uses :parslet
533
+ parser = TreeHaver::Parser.new
534
+ end
535
+ # Back to :rust
536
+ end
537
+ # Back to original backend
538
+ ```
539
+
540
+ This is particularly useful for:
541
+
542
+ - **Testing**: Test the same code with different backends
543
+ - **Performance comparison**: Benchmark different backends
544
+ - **Backend-selection scenarios**: Exercise each registered backend explicitly
545
+ - **Thread isolation**: Each thread can use a different backend safely
546
+
547
+ ```ruby
548
+ # Example: Testing with multiple backends
549
+ [:mri, :rust, :citrus, :parslet].each do |backend_name|
550
+ TreeHaver.with_backend(backend_name) do
551
+ parser = TreeHaver::Parser.new
552
+ result = parser.parse(source)
553
+ puts "#{backend_name}: #{result.root_node.type}"
554
+ end
555
+ end
556
+ ```
557
+
558
+ **Check Backend Capabilities:**
559
+
560
+ ```ruby
561
+ TreeHaver.backend # => :ffi
562
+ TreeHaver.backend_module # => TreeHaver::Backends::FFI
563
+ TreeHaver.capabilities # => { backend: :ffi, parse: true, query: false, ... }
564
+ ```
565
+
566
+ For runnable scenario examples, see the implementation-level [examples directory](../../examples/). Those examples are user-level scripts rather than backend-count smoke tests.
567
+
568
+ ### Security Considerations
569
+
570
+ **⚠️ Loading shared libraries (.so/.dylib/.dll) executes arbitrary native code.**
571
+
572
+ TreeHaver provides defense-in-depth validations, but you should understand the risks:
573
+
574
+ #### Attack Vectors Mitigated
575
+
576
+ TreeHaver's `PathValidator` module protects against:
577
+
578
+ - **Path traversal**: Paths containing `/../` or `/./` are rejected
579
+ - **Null byte injection**: Paths containing null bytes are rejected
580
+ - **Non-absolute paths**: Relative paths are rejected to prevent CWD-based attacks
581
+ - **Invalid extensions**: Only `.so`, `.dylib`, and `.dll` files are accepted
582
+ - **Malicious filenames**: Filenames must match a safe pattern (alphanumeric, hyphens, underscores)
583
+ - **Invalid language names**: Language names must be lowercase alphanumeric with underscores
584
+ - **Invalid symbol names**: Symbol names must be valid C identifiers
585
+
586
+ #### Secure Usage
587
+
588
+ ```ruby
589
+ # Standard usage - paths from ENV are validated
590
+ finder = TreeHaver::GrammarFinder.new(:toml)
591
+ path = finder.find_library_path # Validates ENV path before returning
592
+
593
+ # Maximum security - only trusted system directories
594
+ path = finder.find_library_path_safe # Ignores ENV, only /usr/lib etc.
595
+
596
+ # Manual validation
597
+ if TreeHaver::PathValidator.safe_library_path?(user_provided_path)
598
+ language = TreeHaver::Language.from_library(user_provided_path)
599
+ end
600
+
601
+ # Get validation errors for debugging
602
+ errors = TreeHaver::PathValidator.validation_errors(path)
603
+ # => ["Path is not absolute", "Path contains traversal sequence"]
604
+ ```
605
+
606
+ #### Trusted Directories
607
+
608
+ The `find_library_path_safe` method only returns paths in trusted directories.
609
+
610
+ **Default trusted directories:**
611
+
612
+ - `/usr/lib`, `/usr/lib64`
613
+ - `/usr/lib/x86_64-linux-gnu`, `/usr/lib/aarch64-linux-gnu`
614
+ - `/usr/local/lib`
615
+ - `/opt/homebrew/lib`, `/opt/local/lib`
616
+ **Adding custom trusted directories:**
617
+ For non-standard installations (Homebrew on Linux, luarocks, mise, asdf, etc.), register additional trusted directories:
618
+
619
+ ```ruby
620
+ # Programmatically at application startup
621
+ TreeHaver::PathValidator.add_trusted_directory("/home/linuxbrew/.linuxbrew/Cellar")
622
+ TreeHaver::PathValidator.add_trusted_directory("~/.local/share/mise/installs/lua")
623
+
624
+ # Or via environment variable (comma-separated, in your shell profile)
625
+ export TREE_HAVER_TRUSTED_DIRS = "/home/linuxbrew/.linuxbrew/Cellar,~/.local/share/mise/installs/lua"
626
+ ```
627
+
628
+ **Example: Fedora Silverblue with Homebrew and luarocks**
629
+
630
+ ```bash
631
+ # In ~/.bashrc or ~/.zshrc
632
+ export TREE_HAVER_TRUSTED_DIRS="/home/linuxbrew/.linuxbrew/Cellar,~/.local/share/mise/installs/lua"
633
+
634
+ # tree-sitter runtime library
635
+ export TREE_SITTER_RUNTIME_LIB=/home/linuxbrew/.linuxbrew/Cellar/tree-sitter/0.26.3/lib/libtree-sitter.so
636
+
637
+ # Language grammar (luarocks-installed)
638
+ 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
639
+ ```
640
+
641
+ #### Recommendations
642
+
643
+ 1. **Production**: Consider using `find_library_path_safe` to ignore ENV overrides
644
+ 2. **Development**: Standard `find_library_path` is convenient for testing
645
+ 3. **User Input**: Always validate paths before passing to `Language.from_library`
646
+ 4. **CI/CD**: Be cautious of ENV vars that could be set by untrusted sources
647
+ 5. **Custom installs**: Register trusted directories via `TREE_HAVER_TRUSTED_DIRS` or `add_trusted_directory`
648
+
649
+ ### Backend Selection
650
+
651
+ TreeHaver can select an available registered backend for you, but you can
652
+ override this behavior:
653
+
654
+ ```ruby
655
+ # Automatic backend selection (default)
656
+ TreeHaver.backend = :auto
657
+
658
+ # Force a specific backend
659
+ TreeHaver.backend = :mri # Use ruby_tree_sitter (MRI only, C extension)
660
+ TreeHaver.backend = :rust # Use tree_stump (MRI, Rust extension with precompiled binaries)
661
+ # Note: Use tree_stump v0.2.0 or newer (fixes are released).
662
+ TreeHaver.backend = :ffi # Use FFI bindings (works on MRI and JRuby)
663
+ TreeHaver.backend = :java # Use Java bindings (JRuby only)
664
+ TreeHaver.backend = :citrus # Use Citrus pure Ruby parser
665
+ # NOTE: Portable, all Ruby implementations
666
+ # CAVEAT: few major language grammars, but many esoteric grammars
667
+ TreeHaver.backend = :parslet # Use Parslet pure Ruby parser
668
+ # NOTE: Portable, all Ruby implementations
669
+ # CAVEAT: few major language grammars, but many esoteric grammars
670
+ ```
671
+
672
+ **Auto-selection rule:** `:auto` uses the registered backend list and chooses
673
+ the first backend module that is allowed by environment configuration and reports
674
+ itself available. It does not call parser libraries directly, search for
675
+ language-specific fallback parsers outside the registry, or hide an explicit
676
+ backend failure by silently switching to a different backend.
677
+
678
+ **Built-in facade priority on MRI:** MRI → Rust → FFI → Citrus → Parslet
679
+
680
+ You can also set the backend via environment variable:
681
+
682
+ ```bash
683
+ export TREE_HAVER_BACKEND=rust
684
+ ```
685
+
686
+ ### Backend Registry
687
+
688
+ TreeHaver provides a `BackendRegistry` module that allows external gems to register their backend availability checkers. This enables dynamic backend detection without hardcoding dependencies.
689
+
690
+ #### Registering a Backend Availability Checker
691
+
692
+ External gems (like `commonmarker-merge`, `markly-merge`, `rbs-merge`) can register their availability checker when loaded:
693
+
694
+ ```ruby
695
+ # In your gem's backend module
696
+ TreeHaver::BackendRegistry.register_availability_checker(:my_backend) do
697
+ # Return true if backend is available
698
+ require "my_backend_gem"
699
+ true
700
+ rescue LoadError
701
+ false
702
+ end
703
+ ```
704
+
705
+ #### Checking Backend Availability
706
+
707
+ ```ruby
708
+ # Check if a backend is available
709
+ TreeHaver::BackendRegistry.available?(:commonmarker) # => true/false
710
+ TreeHaver::BackendRegistry.available?(:markly) # => true/false
711
+ TreeHaver::BackendRegistry.available?(:rbs) # => true/false
712
+
713
+ # Check if a checker is registered
714
+ TreeHaver::BackendRegistry.registered?(:my_backend) # => true/false
715
+
716
+ # Get all registered backend names
717
+ TreeHaver::BackendRegistry.registered_backends # => [:mri, :rust, :ffi, ...]
718
+ ```
719
+
720
+ #### How It Works
721
+
722
+ 1. Built-in backends (MRI, Rust, FFI, Java, Prism, Psych, Citrus, Parslet) automatically register their checkers when loaded
723
+ 2. External gems register their checkers when their backend module is loaded
724
+ 3. `TreeHaver::RSpec::DependencyTags` uses the registry to dynamically detect available backends
725
+ 4. Results are cached for performance (use `clear_cache!` to reset)
726
+
727
+ #### RSpec Integration
728
+
729
+ The `BackendRegistry` is used by `TreeHaver::RSpec::DependencyTags` to configure RSpec exclusion filters:
730
+
731
+ ```ruby
732
+ # In your spec_helper.rb
733
+ require "tree_haver/rspec/dependency_tags"
734
+
735
+ # Then in specs, use tags to skip tests when backends aren't available
736
+ it "requires commonmarker", :commonmarker_backend do
737
+ # This test only runs when commonmarker is available
738
+ end
739
+
740
+ it "requires markly", :markly_backend do
741
+ # This test only runs when markly is available
742
+ end
743
+ ```
744
+
745
+ ### Environment Variables
746
+
747
+ TreeHaver recognizes several environment variables for configuration:
748
+
749
+ **Note**: All path-based environment variables are validated before use. Invalid paths are ignored.
750
+
751
+ #### Security Configuration
752
+
753
+ - **`TREE_HAVER_TRUSTED_DIRS`**: Comma-separated list of additional trusted directories for grammar libraries
754
+
755
+ ```bash
756
+ # For Homebrew on Linux and luarocks
757
+ export TREE_HAVER_TRUSTED_DIRS="/home/linuxbrew/.linuxbrew/Cellar,~/.local/share/mise/installs/lua"
758
+ ```
759
+
760
+ Tilde (`~`) is expanded to the user's home directory. Directories listed here are considered safe for `find_library_path_safe`.
761
+
762
+ #### Core Runtime Library
763
+
764
+ - **`TREE_SITTER_RUNTIME_LIB`**: Absolute path to the core `libtree-sitter` shared library
765
+ ```bash
766
+ export TREE_SITTER_RUNTIME_LIB=/usr/local/lib/libtree-sitter.so
767
+ ```
768
+
769
+ If not set, TreeHaver tries these names in order:
770
+
771
+ - `tree-sitter`
772
+ - `libtree-sitter.so.0`
773
+ - `libtree-sitter.so`
774
+ - `libtree-sitter.dylib`
775
+ - `libtree-sitter.dll`
776
+
777
+ #### Language Symbol Resolution
778
+
779
+ When loading a language grammar, if you don't specify the `symbol:` parameter, TreeHaver resolves it in this precedence:
780
+
781
+ 1. **`TREE_SITTER_LANG_SYMBOL`**: Explicit symbol override
782
+ 2. Guessed from filename (e.g., `libtree-sitter-toml.so` → `tree_sitter_toml`)
783
+ 3. Default fallback (`tree_sitter_toml`)
784
+
785
+ ```bash
786
+ export TREE_SITTER_LANG_SYMBOL=tree_sitter_toml
787
+ ```
788
+
789
+ #### Language Library Paths
790
+
791
+ For specific languages, you can set environment variables to point to grammar libraries:
792
+
793
+ ```bash
794
+ export TREE_SITTER_TOML_PATH=/usr/local/lib/libtree-sitter-toml.so
795
+ export TREE_SITTER_JSON_PATH=/usr/local/lib/libtree-sitter-json.so
796
+ ```
797
+
798
+ #### JRuby-Specific: Java Backend Configuration
799
+
800
+ For the Java backend on JRuby, you need:
801
+
802
+ 1. **jtreesitter \>= 0.26.0** JAR from Maven Central
803
+ 2. **Tree-sitter runtime library** (`libtree-sitter.so`) version 0.26+
804
+ 3. **Grammar `.so` files** built against tree-sitter 0.26+
805
+
806
+ ```bash
807
+ # Download jtreesitter JAR (or use bin/setup-jtreesitter)
808
+ export TREE_SITTER_JAVA_JARS_DIR=/path/to/java-tree-sitter/jars
809
+
810
+ # Point to tree-sitter runtime (must be 0.26+)
811
+ export TREE_SITTER_RUNTIME_LIB=/usr/local/lib/libtree-sitter.so
812
+
813
+ # Point to grammar libraries (must be built for tree-sitter 0.26+)
814
+ export TREE_SITTER_TOML_PATH=/path/to/libtree-sitter-toml.so
815
+ ```
816
+
817
+ **Building grammars for Java backend:**
818
+
819
+ If you get "version mismatch" errors, rebuild the grammar:
820
+
821
+ ```bash
822
+ # Use the provided build script
823
+ bin/build-grammar toml
824
+
825
+ # This regenerates parser.c for your tree-sitter version and compiles it
826
+ ```
827
+
828
+ For more see [docs](https://tree-sitter.github.io/java-tree-sitter/), [maven][jtreesitter], and [source](https://github.com/tree-sitter/java-tree-sitter).
829
+
830
+ ### Language Registration
831
+
832
+ Register languages once at application startup for convenient access:
833
+
834
+ ```ruby
835
+ # Register a TOML grammar
836
+ TreeHaver.register_language(
837
+ :toml,
838
+ path: "/usr/local/lib/libtree-sitter-toml.so",
839
+ symbol: "tree_sitter_toml", # optional, will be inferred if omitted
840
+ )
841
+
842
+ # Now you can use the convenient helper
843
+ language = TreeHaver::Language.toml
844
+
845
+ # Or still override path/symbol per-call
846
+ language = TreeHaver::Language.toml(
847
+ path: "/custom/path/libtree-sitter-toml.so",
848
+ )
849
+ ```
850
+
851
+ ### Grammar Discovery with GrammarFinder
852
+
853
+ For libraries that need to automatically locate tree-sitter grammars (like the
854
+ `*-merge` family of gems), TreeHaver provides the `GrammarFinder` utility
855
+ class. It resolves explicit registrations first and then uses
856
+ `tree_sitter_language_pack` as the normalized on-demand provisioning path for
857
+ tree-sitter grammars. Parser-specific non-tree-sitter backends should be
858
+ registered by the owning merge gem rather than hardcoded in TreeHaver.
859
+
860
+ ```ruby
861
+ # Create a finder for any language
862
+ finder = TreeHaver::GrammarFinder.new(:toml)
863
+
864
+ # Check if the grammar is available
865
+ if finder.available?
866
+ puts "TOML grammar found at: #{finder.find_library_path}"
867
+ else
868
+ puts finder.not_found_message
869
+ # => "tree-sitter toml grammar not found. Searched: /.../libtree_sitter_toml.so, ..."
870
+ end
871
+
872
+ # Register the language if available
873
+ finder.register! if finder.available?
874
+
875
+ # Now use the registered language
876
+ language = TreeHaver::Language.toml
877
+ ```
878
+
879
+ #### Registration Bootstrap
880
+
881
+ TreeHaver is the shared registry. It is not the owner of parser-family policy.
882
+
883
+ - Tree-sitter grammars should be normalized through `GrammarFinder` and
884
+ `tree_sitter_language_pack` or an explicit registration.
885
+ - Non-tree-sitter backends should be registered by the merge gem that owns
886
+ that parser family.
887
+ - Tools that load multiple merge gems should invoke each gem's registration
888
+ bootstrap so TreeHaver sees the full set of available grammars before
889
+ `parser_for` is called.
890
+
891
+ ```ruby
892
+ # In a tool that uses several merge gems
893
+ require "tree_haver"
894
+ require "toml-merge"
895
+ require "markdown-merge"
896
+
897
+ Toml::Merge.register_backend!
898
+ Markdown::Merge.register_backend!
899
+
900
+ parser = TreeHaver.parser_for(:toml)
901
+ ```
902
+
903
+ Once those registrations have run, `TreeHaver.parser_for` can resolve any
904
+ registered tree-sitter grammar plus any registered backend-specific grammar for
905
+ the active backend mode. If a merge depends on a grammar that has not been
906
+ registered and cannot be provisioned through `tree_sitter_language_pack`,
907
+ TreeHaver raises `TreeHaver::NotAvailable`.
908
+
909
+ #### GrammarFinder Automatic Derivation
910
+
911
+ Given just the language name, `GrammarFinder` automatically derives:
912
+
913
+ | Property | Derived Value (for `:toml`) |
914
+ |------------------|-----------------------------------------------------|
915
+ | ENV var | `TREE_SITTER_TOML_PATH` |
916
+ | Library filename | `libtree_sitter_toml.so` (Linux) or `.dylib` (macOS) |
917
+ | Symbol name | `tree_sitter_toml` |
918
+
919
+ #### Search Order
920
+
921
+ `GrammarFinder` searches for grammars in this order:
922
+
923
+ 1. **Environment variable**: `TREE_SITTER_<LANG>_PATH` (highest priority)
924
+ 2. **Existing TreeHaver registration**: previously-registered tree-sitter grammar path
925
+ 3. **Extra paths**: explicit paths provided at initialization
926
+ 4. **`tree_sitter_language_pack`**: parser API availability through its on-demand grammar loader
927
+
928
+ #### TSLP Cold-Cache Troubleshooting
929
+
930
+ `tree_sitter_language_pack` loads bundled grammars on demand. A cold install may
931
+ not have a populated shared-library cache before the first parse, so
932
+ `GrammarFinder` must not treat cache contents as the source of truth for TSLP
933
+ availability.
934
+
935
+ For TSLP-backed languages, check availability through the TSLP parser API and
936
+ then register the language with the TreeHaver TSLP backend:
937
+
938
+ ```ruby
939
+ finder = TreeHaver::GrammarFinder.new(:toml)
940
+ finder.register! if finder.available?
941
+
942
+ TreeHaver.with_backend("kreuzberg-language-pack") do
943
+ TreeHaver.parser_for(:toml).parse("title = \"example\"\n")
944
+ end
945
+ ```
946
+
947
+ If this fails from a cold start, inspect `finder.search_info` and confirm that
948
+ the `tree-sitter-language-pack` gem is in the bundle being used. Do not add a
949
+ merge-gem parser fallback to hide the failure; fix the TreeHaver registration
950
+ or the TSLP grammar binding instead.
951
+
952
+ #### Usage in \*-merge Gems
953
+
954
+ The `GrammarFinder` pattern enables clean integration in language-specific
955
+ merge gems:
956
+
957
+ ```ruby
958
+ # In a substrate merge gem
959
+ finder = TreeHaver::GrammarFinder.new(:toml)
960
+ finder.register! if finder.available?
961
+ ```
962
+
963
+ Parser-specific gems should register their own TreeHaver backend wrappers:
964
+
965
+ ```ruby
966
+ # In a TOML parser provider gem
967
+ TreeHaver.register_language(
968
+ :toml,
969
+ backend_module: Citrus::Toml::Merge::Backend,
970
+ backend_type: :citrus,
971
+ gem_name: "toml-rb",
972
+ )
973
+ ```
974
+
975
+ Each gem uses the same API. TreeHaver owns the shared registration surface;
976
+ merge gems own parser-specific backend registrations and any explicit bootstrap
977
+ hook they expose to register them.
978
+
979
+ #### Adding Custom Search Paths
980
+
981
+ For non-standard standalone grammar builds, provide extra search paths:
982
+
983
+ ```ruby
984
+ finder = TreeHaver::GrammarFinder.new(:toml, extra_paths: [
985
+ "/opt/custom/lib",
986
+ "/home/user/.local/lib",
987
+ ])
988
+ ```
989
+
990
+ #### Debug Information
991
+
992
+ Get detailed information about the grammar search:
993
+
994
+ ```ruby
995
+ finder = TreeHaver::GrammarFinder.new(:toml)
996
+ puts finder.search_info
997
+ # => {
998
+ # language: :toml,
999
+ # env_var: "TREE_SITTER_TOML_PATH",
1000
+ # env_value: nil,
1001
+ # symbol: "tree_sitter_toml",
1002
+ # library_filename: "libtree_sitter_toml.so",
1003
+ # search_paths: ["/custom/lib/libtree_sitter_toml.so", "/.../tree-sitter-language-pack/..."],
1004
+ # found_path: "/.../libtree_sitter_toml.so",
1005
+ # available: true
1006
+ # }
1007
+ ```
1008
+
1009
+ ### Checking Capabilities
1010
+
1011
+ Different backends may support different features:
1012
+
1013
+ ```ruby
1014
+ TreeHaver.capabilities
1015
+ # => { backend: :mri, query: true, bytes_field: true }
1016
+ # or
1017
+ # => { backend: :ffi, parse: true, query: false, bytes_field: true }
1018
+ # or
1019
+ # => { backend: :citrus, parse: true, query: false, bytes_field: false }
1020
+ # or
1021
+ # => { backend: :parslet, parse: true, query: false, bytes_field: false }
1022
+ ```
1023
+
1024
+ ### Error Handling Model
1025
+
1026
+ TreeHaver does not ship a `TreeSitter::*` compatibility namespace. Use the `TreeHaver::*` API directly, and rescue `TreeHaver::Error` or a more specific subclass when parser setup can fail.
1027
+
1028
+ **TreeHaver Exception Hierarchy:**
1029
+
1030
+ StandardError
1031
+ └── TreeHaver::Error # Base error class
1032
+ ├── TreeHaver::NotAvailable # Backend/grammar not available
1033
+ └── TreeHaver::BackendConflict # Backend incompatibility detected
1034
+
1035
+ **Best Practices:**
1036
+
1037
+ 1. **Rescue TreeHaver errors explicitly when backend setup can fail:**
1038
+
1039
+ ```ruby
1040
+ begin
1041
+ finder = TreeHaver::GrammarFinder.new(:toml)
1042
+ finder.register! if finder.available?
1043
+ language = TreeHaver::Language.toml
1044
+ rescue TreeHaver::NotAvailable => e
1045
+ warn("TOML grammar not available: #{e.message}")
1046
+ # Select another registered TreeHaver backend explicitly, or fail gracefully
1047
+ end
1048
+ ```
1049
+
1050
+ 2. **Prefer specific handling** for `TreeHaver::NotAvailable` when a backend or grammar is optional.
1051
+
1052
+ ## 🔧 Basic Usage
1053
+
1054
+ ### Quick Start
1055
+
1056
+ The simplest way to parse code is with `TreeHaver.parser_for`, which handles
1057
+ language loading, grammar resolution, and backend selection:
1058
+
1059
+ ```ruby
1060
+ require "tree_haver"
1061
+
1062
+ # Parse TOML - resolves any registered tree-sitter grammar and any registered
1063
+ # non-tree-sitter backend for the active backend mode
1064
+ parser = TreeHaver.parser_for(:toml)
1065
+ tree = parser.parse("[package]\nname = \"my-app\"")
1066
+
1067
+ # Parse JSON
1068
+ parser = TreeHaver.parser_for(:json)
1069
+ tree = parser.parse('{"key": "value"}')
1070
+
1071
+ # Parse Bash
1072
+ parser = TreeHaver.parser_for(:bash)
1073
+ tree = parser.parse("#!/bin/bash\necho hello")
1074
+
1075
+ # With explicit library path
1076
+ parser = TreeHaver.parser_for(:toml, library_path: "/custom/path/libtree-sitter-toml.so")
1077
+
1078
+ # With explicit Citrus provider configuration
1079
+ parser = TreeHaver.parser_for(
1080
+ :toml,
1081
+ citrus_config: {gem_name: "toml-rb", grammar_const: "TomlRB::Document"},
1082
+ )
1083
+ ```
1084
+
1085
+ `TreeHaver.parser_for` handles:
1086
+
1087
+ 1. Checking if the language is already registered
1088
+ 2. Auto-discovering tree-sitter grammar via `GrammarFinder`
1089
+ 3. Using any registered backend-specific grammar for the active backend
1090
+ 4. Creating and configuring the parser
1091
+ 5. Raising `NotAvailable` with a helpful message if nothing works
1092
+
1093
+ ### Manual Parser Setup
1094
+
1095
+ For more control, you can create parsers manually:
1096
+
1097
+ TreeHaver works with many languages through its registry of tree-sitter adapters, language-pack providers, native parsers, PEG parsers, and binary schema support. Here are examples for different parsing needs:
1098
+
1099
+ #### Parsing with Tree-sitter (Universal Languages)
1100
+
1101
+ ```ruby
1102
+ require "tree_haver"
1103
+
1104
+ # Load a tree-sitter grammar (works with MRI, Rust, FFI, or Java backend)
1105
+ language = TreeHaver::Language.from_library(
1106
+ "/usr/local/lib/libtree-sitter-toml.so",
1107
+ symbol: "tree_sitter_toml",
1108
+ )
1109
+
1110
+ # Create a parser
1111
+ parser = TreeHaver::Parser.new
1112
+ parser.language = language
1113
+
1114
+ # Parse source code
1115
+ source = <<~TOML
1116
+ [package]
1117
+ name = "my-app"
1118
+ version = "1.0.0"
1119
+ TOML
1120
+
1121
+ tree = parser.parse(source)
1122
+
1123
+ # Access the unified Position API (works across all backends)
1124
+ root = tree.root_node
1125
+ puts "Root type: #{root.type}" # => "document"
1126
+ puts "Start line: #{root.start_line}" # => 1 (1-based)
1127
+ puts "End line: #{root.end_line}" # => 3
1128
+ puts "Position: #{root.source_position}" # => {start_line: 1, end_line: 3, ...}
1129
+
1130
+ # Traverse the tree
1131
+ root.each do |child|
1132
+ puts "Child: #{child.type} at line #{child.start_line}"
1133
+ end
1134
+ ```
1135
+
1136
+ #### Parsing Ruby with Prism
1137
+
1138
+ ```ruby
1139
+ require "tree_haver"
1140
+
1141
+ TreeHaver.backend = :prism
1142
+ parser = TreeHaver::Parser.new
1143
+ parser.language = TreeHaver::Backends::Prism::Language.ruby
1144
+
1145
+ source = <<~RUBY
1146
+ class Example
1147
+ def hello
1148
+ puts "Hello, world!"
1149
+ end
1150
+ end
1151
+ RUBY
1152
+
1153
+ tree = parser.parse(source)
1154
+ root = tree.root_node
1155
+
1156
+ # Find all method definitions
1157
+ def find_methods(node, results = [])
1158
+ results << node if node.type == "def_node"
1159
+ node.children.each { |child| find_methods(child, results) }
1160
+ results
1161
+ end
1162
+
1163
+ methods = find_methods(root)
1164
+ methods.each do |method_node|
1165
+ pos = method_node.source_position
1166
+ puts "Method at lines #{pos[:start_line]}-#{pos[:end_line]}"
1167
+ end
1168
+ ```
1169
+
1170
+ #### Parsing YAML with Psych
1171
+
1172
+ ```ruby
1173
+ require "tree_haver"
1174
+
1175
+ TreeHaver.backend = :psych
1176
+ parser = TreeHaver::Parser.new
1177
+ parser.language = TreeHaver::Backends::Psych::Language.yaml
1178
+
1179
+ source = <<~YAML
1180
+ database:
1181
+ host: localhost
1182
+ port: 5432
1183
+ YAML
1184
+
1185
+ tree = parser.parse(source)
1186
+ root = tree.root_node
1187
+
1188
+ # Navigate YAML structure
1189
+ def show_structure(node, indent = 0)
1190
+ prefix = " " * indent
1191
+ puts "#{prefix}#{node.type} (line #{node.start_line})"
1192
+ node.children.each { |child| show_structure(child, indent + 1) }
1193
+ end
1194
+
1195
+ show_structure(root)
1196
+ ```
1197
+
1198
+ #### Parsing Markdown with Commonmarker or Markly
1199
+
1200
+ ```ruby
1201
+ require "tree_haver"
1202
+
1203
+ # Choose your backend
1204
+ TreeHaver.backend = :commonmarker # or :markly for GFM
1205
+
1206
+ parser = TreeHaver::Parser.new
1207
+ parser.language = TreeHaver::Backends::Commonmarker::Language.markdown
1208
+
1209
+ source = <<~MARKDOWN
1210
+ # My Document
1211
+
1212
+ ## Section
1213
+
1214
+ - Item 1
1215
+ - Item 2
1216
+ MARKDOWN
1217
+
1218
+ tree = parser.parse(source)
1219
+ root = tree.root_node
1220
+
1221
+ # Find all headings
1222
+ def find_headings(node, results = [])
1223
+ results << node if node.type == "heading"
1224
+ node.children.each { |child| find_headings(child, results) }
1225
+ results
1226
+ end
1227
+
1228
+ headings = find_headings(root)
1229
+ headings.each do |heading|
1230
+ level = heading.header_level
1231
+ text = heading.children.map(&:text).join
1232
+ puts "H#{level}: #{text} (line #{heading.start_line})"
1233
+ end
1234
+ ```
1235
+
1236
+ ### Using Language Registration
1237
+
1238
+ For cleaner code, register languages at startup:
1239
+
1240
+ ```ruby
1241
+ # At application initialization
1242
+ TreeHaver.register_language(
1243
+ :toml,
1244
+ path: "/usr/local/lib/libtree-sitter-toml.so",
1245
+ )
1246
+
1247
+ TreeHaver.register_language(
1248
+ :json,
1249
+ path: "/usr/local/lib/libtree-sitter-json.so",
1250
+ )
1251
+
1252
+ # Later in your code
1253
+ toml_language = TreeHaver::Language.toml
1254
+ json_language = TreeHaver::Language.json
1255
+
1256
+ parser = TreeHaver::Parser.new
1257
+ parser.language = toml_language
1258
+ tree = parser.parse(toml_source)
1259
+ ```
1260
+
1261
+ #### Flexible Language Names
1262
+
1263
+ The `name` parameter in `register_language` is an arbitrary identifier you choose—it doesn't
1264
+ need to match the actual language name. The actual grammar identity comes from the `path`
1265
+ and `symbol` parameters (for tree-sitter) or `grammar_module` (for Citrus/Parslet).
1266
+
1267
+ This flexibility is useful for:
1268
+
1269
+ - **Aliasing**: Register the same grammar under multiple names
1270
+ - **Versioning**: Register different grammar versions (e.g., `:ruby_2`, `:ruby_3`)
1271
+ - **Testing**: Use unique names to avoid collisions between tests
1272
+ - **Context-specific naming**: Use names that make sense for your application
1273
+
1274
+ ```ruby
1275
+ # Register the same TOML grammar under different names for different purposes
1276
+ TreeHaver.register_language(
1277
+ :config_parser, # Custom name for your app
1278
+ path: "/usr/local/lib/libtree-sitter-toml.so",
1279
+ symbol: "tree_sitter_toml",
1280
+ )
1281
+
1282
+ TreeHaver.register_language(
1283
+ :toml_v1, # Version-specific name
1284
+ path: "/usr/local/lib/libtree-sitter-toml.so",
1285
+ symbol: "tree_sitter_toml",
1286
+ )
1287
+
1288
+ # Use your custom names
1289
+ config_lang = TreeHaver::Language.config_parser
1290
+ versioned_lang = TreeHaver::Language.toml_v1
1291
+ ```
1292
+
1293
+ ### Parsing Different Languages
1294
+
1295
+ TreeHaver works with any tree-sitter grammar:
1296
+
1297
+ ```ruby
1298
+ # Parse Ruby code
1299
+ ruby_lang = TreeHaver::Language.from_library(
1300
+ "/path/to/libtree-sitter-ruby.so",
1301
+ )
1302
+ parser = TreeHaver::Parser.new
1303
+ parser.language = ruby_lang
1304
+ tree = parser.parse("class Foo; end")
1305
+
1306
+ # Parse JavaScript
1307
+ js_lang = TreeHaver::Language.from_library(
1308
+ "/path/to/libtree-sitter-javascript.so",
1309
+ )
1310
+ parser.language = js_lang # Reuse the same parser
1311
+ tree = parser.parse("const x = 42;")
1312
+ ```
1313
+
1314
+ ### Walking the AST
1315
+
1316
+ TreeHaver provides simple node traversal:
1317
+
1318
+ ```ruby
1319
+ tree = parser.parse(source)
1320
+ root = tree.root_node
1321
+
1322
+ # Recursive tree walk
1323
+ def walk_tree(node, depth = 0)
1324
+ puts "#{" " * depth}#{node.type}"
1325
+ node.each { |child| walk_tree(child, depth + 1) }
1326
+ end
1327
+
1328
+ walk_tree(root)
1329
+ ```
1330
+
1331
+ ### Incremental Parsing
1332
+
1333
+ 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.
1334
+
1335
+ ```ruby
1336
+ # Check if current backend supports incremental parsing
1337
+ if TreeHaver.capabilities[:incremental]
1338
+ puts "Incremental parsing is available!"
1339
+ end
1340
+
1341
+ # Initial parse
1342
+ parser = TreeHaver::Parser.new
1343
+ parser.language = language
1344
+ tree = parser.parse_string(nil, "x = 1")
1345
+
1346
+ # User edits the source: "x = 1" -> "x = 42"
1347
+ # Mark the tree as edited (tell tree-sitter what changed)
1348
+ tree.edit(
1349
+ start_byte: 4, # edit starts at byte 4
1350
+ old_end_byte: 5, # old text "1" ended at byte 5
1351
+ new_end_byte: 6, # new text "42" ends at byte 6
1352
+ start_point: {row: 0, column: 4},
1353
+ old_end_point: {row: 0, column: 5},
1354
+ new_end_point: {row: 0, column: 6},
1355
+ )
1356
+
1357
+ # Re-parse incrementally - tree-sitter reuses unchanged nodes
1358
+ new_tree = parser.parse_string(tree, "x = 42")
1359
+ ```
1360
+
1361
+ **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 support incremental parsing. You can check support with:
1362
+
1363
+ ```ruby
1364
+ tree.supports_editing? # => true if edit() is available
1365
+ ```
1366
+
1367
+ ### Error Handling
1368
+
1369
+ ```ruby
1370
+ begin
1371
+ language = TreeHaver::Language.from_library("/path/to/grammar.so")
1372
+ rescue TreeHaver::NotAvailable => e
1373
+ puts "Failed to load grammar: #{e.message}"
1374
+ end
1375
+
1376
+ # Check if a backend is available
1377
+ if TreeHaver.backend_module.nil?
1378
+ puts "No TreeHaver backend is available!"
1379
+ puts "Install tree-sitter-language-pack, ruby_tree_sitter, tree_stump, ffi with libtree-sitter, or a Ruby parser backend such as prism, psych, citrus, or parslet"
1380
+ end
1381
+ ```
1382
+
1383
+ ### Platform-Specific Examples
1384
+
1385
+ #### MRI Ruby
1386
+
1387
+ On MRI, TreeHaver uses `ruby_tree_sitter` by default:
1388
+
1389
+ ```ruby
1390
+ # Gemfile
1391
+ gem "tree_haver"
1392
+ gem "ruby_tree_sitter" # MRI backend
1393
+
1394
+ # Code - no changes needed, TreeHaver auto-selects MRI backend
1395
+ parser = TreeHaver::Parser.new
1396
+ ```
1397
+
1398
+ #### JRuby
1399
+
1400
+ On JRuby, TreeHaver can use the FFI backend, Java backend, Citrus backend, or Parslet backend:
1401
+
1402
+ ##### Option 1: FFI Backend (recommended for tree-sitter grammars)
1403
+
1404
+ ```ruby
1405
+ # Gemfile
1406
+ gem "tree_haver"
1407
+ gem "ffi" # Required for FFI backend
1408
+
1409
+ # Ensure libtree-sitter is installed on your system
1410
+ # On macOS with Homebrew:
1411
+ # brew install tree-sitter
1412
+
1413
+ # On Ubuntu/Debian:
1414
+ # sudo apt-get install libtree-sitter0 libtree-sitter-dev
1415
+
1416
+ # Code - TreeHaver auto-selects FFI backend on JRuby
1417
+ parser = TreeHaver::Parser.new
1418
+ ```
1419
+
1420
+ ##### Option 2: Java Backend (native JVM performance)
1421
+
1422
+ ```bash
1423
+ # 1. Download java-tree-sitter JAR from Maven Central
1424
+ mkdir -p vendor/jars
1425
+ curl -fSL -o vendor/jars/jtreesitter-0.26.0.jar \
1426
+ "https://repo1.maven.org/maven2/io/github/tree-sitter/jtreesitter/0.26.0/jtreesitter-0.26.0.jar"
1427
+
1428
+ # 2. Set environment variables
1429
+ export CLASSPATH="$(pwd)/vendor/jars:$CLASSPATH"
1430
+ export LD_LIBRARY_PATH="/path/to/libtree-sitter/lib:$LD_LIBRARY_PATH"
1431
+
1432
+ # 3. Run with JRuby (requires Java 22+ for Foreign Function API)
1433
+ JAVA_OPTS="--enable-native-access=ALL-UNNAMED" jruby your_script.rb
1434
+ ```
1435
+
1436
+ ```ruby
1437
+ # Force Java backend
1438
+ TreeHaver.backend = :java
1439
+
1440
+ # Check if Java backend is available
1441
+ if TreeHaver::Backends::Java.available?
1442
+ puts "Java backend is ready!"
1443
+ puts TreeHaver.capabilities
1444
+ # => { backend: :java, parse: true, query: true, bytes_field: true, incremental: true }
1445
+ end
1446
+ ```
1447
+
1448
+ **⚠️ Java Backend Limitation: Symbol Resolution**
1449
+
1450
+ 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.
1451
+
1452
+ 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.
1453
+
1454
+ **Recommended approach for JRuby:** Use the **FFI backend**:
1455
+
1456
+ ```ruby
1457
+ # On JRuby, use FFI backend (recommended)
1458
+ TreeHaver.backend = :ffi
1459
+ ```
1460
+
1461
+ 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.
1462
+
1463
+ The Java backend will work with:
1464
+
1465
+ - 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))
1466
+ - Grammar `.so` files that statically link tree-sitter
1467
+
1468
+ ##### Option 3: Citrus Backend (pure Ruby, portable)
1469
+
1470
+ ```ruby
1471
+ # Gemfile
1472
+ gem "tree_haver"
1473
+ gem "citrus" # Pure Ruby parser, zero native dependencies
1474
+
1475
+ # Code - Force Citrus backend for maximum portability
1476
+ TreeHaver.backend = :citrus
1477
+
1478
+ # Check if Citrus backend is available
1479
+ if TreeHaver::Backends::Citrus.available?
1480
+ puts "Citrus backend is ready!"
1481
+ puts TreeHaver.capabilities
1482
+ # => { backend: :citrus, parse: true, query: false, bytes_field: false }
1483
+ end
1484
+ ```
1485
+
1486
+ **⚠️ Citrus Backend Limitations:**
1487
+
1488
+ - Uses Citrus grammars (not tree-sitter grammars)
1489
+ - No incremental parsing support
1490
+ - No query API
1491
+ - Pure Ruby performance (slower than native backends)
1492
+ - Best for: prototyping, environments without native extension support, teaching
1493
+
1494
+ ##### Option 4: Parslet Backend (pure Ruby, portable)
1495
+
1496
+ ```ruby
1497
+ # Gemfile
1498
+ gem "tree_haver"
1499
+ gem "parslet" # Pure Ruby parser, zero native dependencies
1500
+
1501
+ # Code - Force Parslet backend for maximum portability
1502
+ TreeHaver.backend = :parslet
1503
+
1504
+ # Check if Parslet backend is available
1505
+ if TreeHaver::Backends::Parslet.available?
1506
+ puts "Parslet backend is ready!"
1507
+ puts TreeHaver.capabilities
1508
+ # => { backend: :parslet, parse: true, query: false, bytes_field: false }
1509
+ end
1510
+ ```
1511
+
1512
+ **⚠️ Parslet Backend Limitations:**
1513
+
1514
+ - Uses Parslet grammars (not tree-sitter grammars)
1515
+ - No incremental parsing support
1516
+ - No query API
1517
+ - Pure Ruby performance (slower than native backends)
1518
+ - Best for: prototyping, environments without native extension support, teaching
1519
+
1520
+ #### TruffleRuby
1521
+
1522
+ TruffleRuby can use Ruby-native parser backends such as Prism, Psych, Citrus, or Parslet. The FFI tree-sitter backend is not selected on TruffleRuby because tree-sitter's struct-by-value API is incompatible with the current FFI path.
1523
+
1524
+ ```ruby
1525
+ # Use Prism for Ruby source
1526
+ TreeHaver.backend = :prism
1527
+
1528
+ # Or use Psych for YAML
1529
+ TreeHaver.backend = :psych
1530
+
1531
+ # Use Citrus backend for zero native dependencies
1532
+ TreeHaver.backend = :citrus
1533
+
1534
+ # Or use Parslet backend for zero native dependencies
1535
+ TreeHaver.backend = :parslet
1536
+ ```
1537
+
1538
+ ### Advanced: Thread-Safe Backend Switching
1539
+
1540
+ TreeHaver provides `with_backend` for thread-safe, temporary backend switching. This is
1541
+ essential for testing, benchmarking, and applications that need different backends in
1542
+ different contexts.
1543
+
1544
+ #### Testing with Multiple Backends
1545
+
1546
+ Test the same code path with different backends using `with_backend`:
1547
+
1548
+ ```ruby
1549
+ # In your test setup
1550
+ RSpec.describe("MyParser") do
1551
+ # Test with each available backend
1552
+ [:mri, :rust, :citrus, :parslet].each do |backend_name|
1553
+ context "with #{backend_name} backend" do
1554
+ it "parses correctly" do
1555
+ TreeHaver.with_backend(backend_name) do
1556
+ parser = TreeHaver::Parser.new
1557
+ result = parser.parse("x = 42")
1558
+ expect(result.root_node.type).to(eq("document"))
1559
+ end
1560
+ # Backend automatically restored after block
1561
+ end
1562
+ end
1563
+ end
1564
+ end
1565
+ ```
1566
+
1567
+ #### Thread Isolation
1568
+
1569
+ Each thread can use a different backend safely—`with_backend` uses thread-local storage:
1570
+
1571
+ ```ruby
1572
+ threads = []
1573
+
1574
+ threads << Thread.new do
1575
+ TreeHaver.with_backend(:mri) do
1576
+ # This thread uses MRI backend
1577
+ parser = TreeHaver::Parser.new
1578
+ 100.times { parser.parse("x = 1") }
1579
+ end
1580
+ end
1581
+
1582
+ threads << Thread.new do
1583
+ TreeHaver.with_backend(:citrus) do
1584
+ # This thread uses Citrus backend simultaneously
1585
+ parser = TreeHaver::Parser.new
1586
+ 100.times { parser.parse("x = 1") }
1587
+ end
1588
+ end
1589
+
1590
+ threads << Thread.new do
1591
+ TreeHaver.with_backend(:parslet) do
1592
+ # This thread uses Parslet backend simultaneously
1593
+ parser = TreeHaver::Parser.new
1594
+ 100.times { parser.parse("x = 1") }
1595
+ end
1596
+ end
1597
+
1598
+ threads.each(&:join)
1599
+ ```
1600
+
1601
+ #### Nested Blocks
1602
+
1603
+ `with_backend` supports nesting—inner blocks override outer blocks:
1604
+
1605
+ ```ruby
1606
+ TreeHaver.with_backend(:rust) do
1607
+ puts TreeHaver.effective_backend # => :rust
1608
+
1609
+ TreeHaver.with_backend(:citrus) do
1610
+ puts TreeHaver.effective_backend # => :citrus
1611
+ end
1612
+
1613
+ TreeHaver.with_backend(:parslet) do
1614
+ puts TreeHaver.effective_backend # => :parslet
1615
+ end
1616
+
1617
+ puts TreeHaver.effective_backend # => :rust (restored)
1618
+ end
1619
+ ```
1620
+
1621
+ #### Explicit Backend Selection Pattern
1622
+
1623
+ Try registered TreeHaver backends explicitly when you want controlled provider
1624
+ selection. Structured merge gems should fail closed when no registered TreeHaver
1625
+ backend can parse the requested language.
1626
+
1627
+ ```ruby
1628
+ def parse_with_backend(source, backend_name)
1629
+ TreeHaver.with_backend(backend_name) do
1630
+ TreeHaver::Parser.new.tap { |p| p.language = load_language }.parse(source)
1631
+ end
1632
+ end
1633
+ ```
1634
+
1635
+ ### Complete Real-World Example
1636
+
1637
+ Here's a practical example that extracts package names from a TOML file:
1638
+
1639
+ ```ruby
1640
+ require "tree_haver"
1641
+
1642
+ # Setup
1643
+ TreeHaver.register_language(
1644
+ :toml,
1645
+ path: "/usr/local/lib/libtree-sitter-toml.so",
1646
+ )
1647
+
1648
+ def extract_package_name(toml_content)
1649
+ # Create parser
1650
+ parser = TreeHaver::Parser.new
1651
+ parser.language = TreeHaver::Language.toml
1652
+
1653
+ # Parse
1654
+ tree = parser.parse(toml_content)
1655
+ root = tree.root_node
1656
+
1657
+ # Find [package] table
1658
+ root.each do |child|
1659
+ next unless child.type == "table"
1660
+
1661
+ child.each do |table_elem|
1662
+ if table_elem.type == "pair"
1663
+ # Look for name = "..." pair
1664
+ key = table_elem.each.first&.type
1665
+ # In a real implementation, you'd extract the text value
1666
+ # This is simplified for demonstration
1667
+ end
1668
+ end
1669
+ end
1670
+ end
1671
+
1672
+ # Usage
1673
+ toml = <<~TOML
1674
+ [package]
1675
+ name = "awesome-app"
1676
+ version = "2.0.0"
1677
+ TOML
1678
+
1679
+ package_name = extract_package_name(toml)
1680
+ ```
1681
+
1682
+ ### 🧪 RSpec Integration
1683
+
1684
+ TreeHaver provides shared RSpec helpers for conditional test execution based on dependency availability. This is useful for testing code that uses optional backends.
1685
+
1686
+ ```ruby
1687
+ # In your spec_helper.rb
1688
+ require "tree_haver/rspec"
1689
+ ```
1690
+
1691
+ This automatically configures RSpec with exclusion filters for all TreeHaver dependencies. Use tags to conditionally run tests:
1692
+
1693
+ ```ruby
1694
+ # Runs only when FFI backend is available
1695
+ it "parses with FFI", :ffi do
1696
+ # ...
1697
+ end
1698
+
1699
+ # Runs only when ruby_tree_sitter gem is available
1700
+ it "uses MRI backend", :mri_backend do
1701
+ # ...
1702
+ end
1703
+
1704
+ # Runs only when tree-sitter-toml grammar works
1705
+ it "parses TOML", :tree_sitter_toml do
1706
+ # ...
1707
+ end
1708
+
1709
+ # Runs only when any markdown backend is available
1710
+ it "parses markdown", :markdown_backend do
1711
+ # ...
1712
+ end
1713
+ ```
1714
+
1715
+ **Available Tags:**
1716
+
1717
+ Tags follow a naming convention:
1718
+
1719
+ - `*_backend` = TreeHaver backend availability checks (tslp, mri, rust, ffi, java, prism, psych, commonmarker, markly, citrus, parslet, rbs)
1720
+ - `*_engine` = Ruby engines (mri, jruby, truffleruby)
1721
+ - `*_grammar` = tree-sitter grammar files (.so)
1722
+ - `*_parsing` = any parsing capability for a language (combines multiple backends/grammars)
1723
+ - `*_gem` = specific library gems
1724
+
1725
+ | Tag | Description |
1726
+ |-------------------------|---------------------------------------------------------------------------|
1727
+ | **Backend Tags** | |
1728
+ | `:ffi_backend` | FFI backend available (dynamic check) |
1729
+ | `:ffi_backend_only` | FFI backend in isolation (won't trigger MRI check) |
1730
+ | `:mri_backend` | ruby\_tree\_sitter gem available |
1731
+ | `:mri_backend_only` | MRI backend in isolation (won't trigger FFI check) |
1732
+ | `:rust_backend` | tree\_stump gem available |
1733
+ | `:java_backend` | Java backend available (JRuby + jtreesitter) |
1734
+ | `:prism_backend` | Prism gem available |
1735
+ | `:psych_backend` | Psych available (stdlib) |
1736
+ | `:commonmarker_backend` | commonmarker gem available |
1737
+ | `:markly_backend` | markly gem available |
1738
+ | `:citrus_backend` | Citrus gem available |
1739
+ | `:parslet_backend` | Parslet gem available |
1740
+ | `:tslp_backend` | tree-sitter-language-pack available |
1741
+ | `:rbs_backend` | RBS gem available (official RBS parser) |
1742
+ | **Engine Tags** | |
1743
+ | `:mri_engine` | Running on MRI (CRuby) |
1744
+ | `:jruby_engine` | Running on JRuby |
1745
+ | `:truffleruby_engine` | Running on TruffleRuby |
1746
+ | **Grammar Tags** | |
1747
+ | `:libtree_sitter` | libtree-sitter.so is loadable via FFI |
1748
+ | `:bash_grammar` | tree-sitter-bash grammar available and parsing works |
1749
+ | `:toml_grammar` | tree-sitter-toml grammar available and parsing works |
1750
+ | `:json_grammar` | tree-sitter-json grammar available and parsing works |
1751
+ | `:jsonc_grammar` | tree-sitter-jsonc grammar available and parsing works |
1752
+ | `:rbs_grammar` | tree-sitter-rbs grammar available and parsing works |
1753
+ | **Parsing Tags** | |
1754
+ | `:toml_parsing` | Any TOML parser available (tree-sitter OR toml-rb/Citrus OR toml/Parslet) |
1755
+ | `:markdown_parsing` | Any markdown parser available (commonmarker OR markly) |
1756
+ | `:rbs_parsing` | Any RBS parser available (rbs gem OR tree-sitter-rbs) |
1757
+ | `:native_parsing` | Native tree-sitter backend and grammar available |
1758
+ | **Library Tags** | |
1759
+ | `:toml_rb_gem` | toml-rb gem available (Citrus backend for TOML) |
1760
+ | `:toml_gem` | toml gem available (Parslet backend for TOML) |
1761
+ | `:rbs_gem` | rbs gem available (official RBS parser) |
1762
+
1763
+ All tags have negated versions (e.g., `:not_mri_backend`, `:not_jruby_engine`, `:not_toml_parsing`) for testing fallback behavior.
1764
+
1765
+ **Debug Output:**
1766
+
1767
+ Set `TREE_HAVER_DEBUG=1` to print a dependency summary at the start of your test suite:
1768
+
1769
+ ```bash
1770
+ TREE_HAVER_DEBUG=1 bundle exec rspec
1771
+ ```
1772
+
1773
+ ## 🔐 Security
1774
+
1775
+ See [SECURITY.md][🔐security].
1776
+
1777
+ ## 🤝 Contributing
1778
+
1779
+ If you need some ideas of where to help, you could work on adding more code coverage,
1780
+ or if it is already 💯 (see [below](#code-coverage)) check [issues][🤝gh-issues] or [PRs][🤝gh-pulls],
1781
+ or use the gem and think about how it could be better.
1782
+
1783
+ We [![Keep A Changelog][📗keep-changelog-img]][📗keep-changelog] so if you make changes, remember to update it.
1784
+
1785
+ See [CONTRIBUTING.md][🤝contributing] for more detailed instructions.
1786
+
1787
+ ### Code Coverage
1788
+
1789
+ Run the test suite with coverage enabled:
1790
+
1791
+ ```bash
1792
+ bundle exec kettle-test
1793
+ ```
1794
+
1795
+ Inspect the generated report with:
1796
+
1797
+ ```bash
1798
+ kettle-soup-cover -d
1799
+ ```
1800
+
1801
+ ## 📌 Versioning
1802
+
1803
+ This library follows [![Semantic Versioning 2.0.0][📌semver-img]][📌semver] for its public API where practical.
1804
+ For most applications, prefer the [Pessimistic Version Constraint][📌pvc] with two digits of precision.
1805
+
1806
+ For example:
1807
+
1808
+ ```ruby
1809
+ spec.add_dependency("tree_haver", "~> 7.0")
1810
+ ```
1811
+
1812
+ <details markdown="1">
1813
+ <summary>📌 Is "Platform Support" part of the public API? More details inside.</summary>
1814
+
1815
+ Dropping support for a platform can be a breaking change for affected users.
1816
+ If a release changes supported platforms, it should be called out clearly in the changelog and versioned with that impact in mind.
1817
+
1818
+ To get a better understanding of how SemVer is intended to work over a project's lifetime,
1819
+ read this article from the creator of SemVer:
1820
+
1821
+ - ["Major Version Numbers are Not Sacred"][📌major-versions-not-sacred]
1822
+
1823
+ </details>
1824
+
1825
+ See [CHANGELOG.md][📌changelog] for a list of releases.
1826
+
1827
+ ## 📄 License
1828
+
1829
+ The gem is available under the following licenses: [AGPL-3.0-only](https://github.com/structuredmerge/structuredmerge-ruby/blob/main/AGPL-3.0-only.md), [PolyForm-Small-Business-1.0.0](https://github.com/structuredmerge/structuredmerge-ruby/blob/main/PolyForm-Small-Business-1.0.0.md).
1830
+ See [LICENSE.md][📄license] for details.
1831
+
1832
+ If none of the available licenses suit your use case, please [contact us](mailto:floss@galtzo.com) to discuss a custom commercial license.
1833
+
1834
+ [⛳liberapay-img]: https://img.shields.io/liberapay/goal/pboling.svg?logo=liberapay&color=a51611&style=flat
1835
+ [⛳liberapay-bottom-img]: https://img.shields.io/liberapay/goal/pboling.svg?style=for-the-badge&logo=liberapay&color=a51611
1836
+ [⛳liberapay]: https://liberapay.com/pboling/donate
1837
+ [🖇sponsor-img]: https://img.shields.io/badge/Sponsor_Me!-pboling.svg?style=social&logo=github
1838
+ [🖇sponsor-bottom-img]: https://img.shields.io/badge/Sponsor_Me!-pboling-blue?style=for-the-badge&logo=github
1839
+ [🖇sponsor]: https://github.com/sponsors/pboling
1840
+ [🖇kofi-img]: https://img.shields.io/badge/ko--fi-%E2%9C%93-a51611.svg?style=flat
1841
+ [🖇kofi]: https://ko-fi.com/pboling
1842
+ [🖇buyme-small-img]: https://img.shields.io/badge/buy_me_a_coffee-%E2%9C%93-a51611.svg?style=flat
1843
+ [🖇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
1844
+ [🖇buyme]: https://www.buymeacoffee.com/pboling
1845
+ [🖇paypal-img]: https://img.shields.io/badge/donate-paypal-a51611.svg?style=flat&logo=paypal
1846
+ [🖇paypal-bottom-img]: https://img.shields.io/badge/donate-paypal-a51611.svg?style=for-the-badge&logo=paypal&color=0A0A0A
1847
+ [🖇paypal]: https://www.paypal.com/paypalme/peterboling
1848
+ [🖇floss-funding.dev]: https://floss-funding.dev
1849
+ [🖇floss-funding-gem]: https://github.com/galtzo-floss/floss_funding
1850
+ [✉️discord-invite]: https://discord.gg/3qme4XHNKN
1851
+ [✉️discord-invite-img-ftb]: https://img.shields.io/discord/1373797679469170758?style=for-the-badge&logo=discord
1852
+ [✉️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
1853
+ [✉️ruby-friends]: https://app.daily.dev/squads/rubyfriends
1854
+ [✉️ruby-forum-top-img]: https://img.shields.io/discourse/topics?server=https%3A%2F%2Fwww.rubyforum.org&style=flat&logo=discourse&label=Ruby%20Users%20Forum
1855
+ [✉️ruby-forum-img]: https://img.shields.io/discourse/topics?server=https%3A%2F%2Fwww.rubyforum.org&style=for-the-badge&logo=discourse&label=Ruby%20Users%20Forum
1856
+ [✉️ruby-forum]: https://www.rubyforum.org/tag/tree_haver
1857
+ [✇bundle-group-pattern]: https://gist.github.com/pboling/4564780
1858
+ [⛳️gem-namespace]: https://github.com/structuredmerge/structuredmerge-ruby/tree/main/gems/tree_haver
1859
+ [⛳️namespace-img]: https://img.shields.io/badge/namespace-TreeHaver-3C2D2D.svg?style=square&logo=ruby&logoColor=white
1860
+ [⛳️gem-name]: https://clickgems.clickhouse.com/dashboard/tree_haver
1861
+ [⛳️name-img]: https://img.shields.io/badge/name-tree__haver-3C2D2D.svg?style=square&logo=rubygems&logoColor=red
1862
+ [⛳️tag-img]: https://img.shields.io/github/tag/structuredmerge/structuredmerge-ruby.svg
1863
+ [⛳️tag]: https://github.com/structuredmerge/structuredmerge-ruby/releases
1864
+ [🚂maint-blog]: http://www.railsbling.com/tags/tree_haver
1865
+ [🚂maint-blog-img]: https://img.shields.io/badge/blog-railsbling-0093D0.svg?style=for-the-badge&logo=rubyonrails&logoColor=orange
1866
+ [🚂maint-contact]: http://www.railsbling.com/contact
1867
+ [🚂maint-contact-img]: https://img.shields.io/badge/Contact-Maintainer-0093D0.svg?style=flat&logo=rubyonrails&logoColor=red
1868
+ [💖🖇linkedin]: http://www.linkedin.com/in/peterboling
1869
+ [💖🖇linkedin-img]: https://img.shields.io/badge/LinkedIn-Profile-0B66C2?style=flat&logo=newjapanprowrestling
1870
+ [💖✌️wellfound]: https://wellfound.com/u/peter-boling
1871
+ [💖✌️wellfound-img]: https://img.shields.io/badge/peter--boling-orange?style=flat&logo=wellfound
1872
+ [💖💲crunchbase]: https://www.crunchbase.com/person/peter-boling
1873
+ [💖💲crunchbase-img]: https://img.shields.io/badge/peter--boling-purple?style=flat&logo=crunchbase
1874
+ [💖🐘ruby-mast]: https://ruby.social/@galtzo
1875
+ [💖🐘ruby-mast-img]: https://img.shields.io/mastodon/follow/109447111526622197?domain=https://ruby.social&style=flat&logo=mastodon&label=Ruby%20@galtzo
1876
+ [💖🦋bluesky]: https://bsky.app/profile/galtzo.com
1877
+ [💖🦋bluesky-img]: https://img.shields.io/badge/@galtzo.com-0285FF?style=flat&logo=bluesky&logoColor=white
1878
+ [💖🌳linktree]: https://linktr.ee/galtzo
1879
+ [💖🌳linktree-img]: https://img.shields.io/badge/galtzo-purple?style=flat&logo=linktree
1880
+ [💖💁🏼‍♂️devto]: https://dev.to/galtzo
1881
+ [💖💁🏼‍♂️devto-img]: https://img.shields.io/badge/dev.to-0A0A0A?style=flat&logo=devdotto&logoColor=white
1882
+ [💖💁🏼‍♂️aboutme]: https://about.me/peter.boling
1883
+ [💖💁🏼‍♂️aboutme-img]: https://img.shields.io/badge/about.me-0A0A0A?style=flat&logo=aboutme&logoColor=white
1884
+ [💖🧊berg]: https://codeberg.org/pboling
1885
+ [💖🐙hub]: https://github.org/pboling
1886
+ [💖🛖hut]: https://sr.ht/~galtzo/
1887
+ [💖🧪lab]: https://gitlab.com/pboling
1888
+ [👨🏼‍🏫expsup-upwork]: https://www.upwork.com/freelancers/~014942e9b056abdf86?mp_source=share
1889
+ [👨🏼‍🏫expsup-upwork-img]: https://img.shields.io/badge/UpWork-13544E?style=for-the-badge&logo=Upwork&logoColor=white
1890
+ [👨🏼‍🏫expsup-codementor]: https://www.codementor.io/peterboling?utm_source=github&utm_medium=button&utm_term=peterboling&utm_campaign=github
1891
+ [👨🏼‍🏫expsup-codementor-img]: https://img.shields.io/badge/CodeMentor-Get_Help-1abc9c?style=for-the-badge&logo=CodeMentor&logoColor=white
1892
+ [🏙️entsup-tidelift]: https://tidelift.com/subscription/pkg/rubygems-tree_haver?utm_source=rubygems-tree_haver&utm_medium=referral&utm_campaign=readme
1893
+ [🏙️entsup-tidelift-img]: https://img.shields.io/badge/Tidelift_and_Sonar-Enterprise_Support-FD3456?style=for-the-badge&logo=sonar&logoColor=white
1894
+ [🏙️entsup-tidelift-sonar]: https://blog.tidelift.com/tidelift-joins-sonar
1895
+ [💁🏼‍♂️peterboling]: http://www.peterboling.com
1896
+ [🚂railsbling]: http://www.railsbling.com
1897
+ [📜src-gl-img]: https://img.shields.io/badge/GitLab-FBA326?style=for-the-badge&logo=Gitlab&logoColor=orange
1898
+ [📜src-gl]: https://gitlab.com/structuredmerge/structuredmerge-ruby/-/tree/main/gems/tree_haver
1899
+ [📜src-cb-img]: https://img.shields.io/badge/CodeBerg-4893CC?style=for-the-badge&logo=CodeBerg&logoColor=blue
1900
+ [📜src-cb]: https://codeberg.org/structuredmerge/structuredmerge-ruby/src/branch/main/gems/tree_haver
1901
+ [📜src-gh-img]: https://img.shields.io/badge/GitHub-238636?style=for-the-badge&logo=GitHub&logoColor=green
1902
+ [📜src-gh]: https://github.com/structuredmerge/structuredmerge-ruby/tree/main/gems/tree_haver
1903
+ [📜docs-cr-rd-img]: https://img.shields.io/badge/RubyDoc-Current_Release-943CD2?style=for-the-badge&logo=readthedocs&logoColor=white
1904
+ [📜docs-head-rd-img]: https://img.shields.io/badge/YARD_on_Galtzo.com-HEAD-943CD2?style=for-the-badge&logo=readthedocs&logoColor=white
1905
+ [📜gl-wiki]: https://gitlab.com/structuredmerge/structuredmerge-ruby/-/wikis/home
1906
+ [📜gh-wiki]: https://github.com/structuredmerge/structuredmerge-ruby/wiki
1907
+ [📜gl-wiki-img]: https://img.shields.io/badge/wiki-gitlab-943CD2.svg?style=for-the-badge&logo=gitlab&logoColor=white
1908
+ [📜gh-wiki-img]: https://img.shields.io/badge/wiki-github-943CD2.svg?style=for-the-badge&logo=github&logoColor=white
1909
+ [👽dl-rank]: https://clickgems.clickhouse.com/dashboard/tree_haver
1910
+ [👽dl-ranki]: https://img.shields.io/gem/dt/tree_haver.svg
1911
+ [👽version]: https://clickgems.clickhouse.com/dashboard/tree_haver
1912
+ [👽versioni]: https://img.shields.io/gem/v/tree_haver.svg
1913
+ [🚎11-c-wf]: https://github.com/structuredmerge/structuredmerge-ruby/actions/workflows/current.yml
1914
+ [🚎11-c-wfi]: https://github.com/structuredmerge/structuredmerge-ruby/actions/workflows/current.yml/badge.svg
1915
+ [💎ruby-c-i]: https://img.shields.io/badge/Ruby-current-CC342D?style=for-the-badge&logo=ruby&logoColor=green
1916
+ [🤝gh-issues]: https://github.com/structuredmerge/structuredmerge-ruby/issues
1917
+ [🤝gh-pulls]: https://github.com/structuredmerge/structuredmerge-ruby/pulls
1918
+ [🤝gl-issues]: https://gitlab.com/structuredmerge/structuredmerge-ruby/-/issues
1919
+ [🤝gl-pulls]: https://gitlab.com/structuredmerge/structuredmerge-ruby/-/merge_requests
1920
+ [🤝cb-issues]: https://codeberg.org/structuredmerge/structuredmerge-ruby/issues
1921
+ [🤝cb-pulls]: https://codeberg.org/structuredmerge/structuredmerge-ruby/pulls
1922
+ [🤝cb-donate]: https://donate.codeberg.org/
1923
+ [🤝contributing]: https://github.com/structuredmerge/structuredmerge-ruby/blob/main/CONTRIBUTING.md
1924
+ [🖐contrib-rocks]: https://contrib.rocks
1925
+ [🖐contributors]: https://github.com/structuredmerge/structuredmerge-ruby/graphs/contributors
1926
+ [🖐contributors-img]: https://contrib.rocks/image?repo=structuredmerge/structuredmerge-ruby
1927
+ [🪇conduct]: https://github.com/structuredmerge/structuredmerge-ruby/blob/main/CODE_OF_CONDUCT.md
1928
+ [🪇conduct-img]: https://img.shields.io/badge/Contributor_Covenant-2.1-259D6C.svg
1929
+ [📌pvc]: http://guides.rubygems.org/patterns/#pessimistic-version-constraint
1930
+ [📌semver]: https://semver.org/spec/v2.0.0.html
1931
+ [📌semver-img]: https://img.shields.io/badge/semver-2.0.0-259D6C.svg?style=flat
1932
+ [📌semver-breaking]: https://github.com/semver/semver/issues/716#issuecomment-869336139
1933
+ [📌major-versions-not-sacred]: https://tom.preston-werner.com/2022/05/23/major-version-numbers-are-not-sacred.html
1934
+ [📌changelog]: https://github.com/structuredmerge/structuredmerge-ruby/blob/main/CHANGELOG.md
1935
+ [📗keep-changelog]: https://keepachangelog.com/en/1.0.0/
1936
+ [📗keep-changelog-img]: https://img.shields.io/badge/keep--a--changelog-1.0.0-34495e.svg?style=flat
1937
+ [📌gitmoji]: https://gitmoji.dev
1938
+ [📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
1939
+ [🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
1940
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-5.053-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
1941
+ [🔐security]: https://github.com/structuredmerge/structuredmerge-ruby/blob/main/SECURITY.md
1942
+ [🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
1943
+ [📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
1944
+ [📄license]: LICENSE.md
1945
+ [📄license-ref]: LICENSE.md
1946
+ [📄license-img]: https://img.shields.io/badge/License-AGPL--3.0--only_OR_PolyForm--Small--Business--1.0.0-259D6C.svg
1947
+ [📄license-compat]: https://www.apache.org/legal/resolved.html#category-x
1948
+ [📄license-compat-img]: https://img.shields.io/badge/Apache_Incompatible:_Category_X-%E2%9C%97-C0392B.svg?style=flat&logo=Apache
1949
+ [📄ilo-declaration]: https://www.ilo.org/declaration/lang--en/index.htm
1950
+ [📄ilo-declaration-img]: https://img.shields.io/badge/ILO_Fundamental_Principles-✓-259D6C.svg?style=flat
1951
+ [🚎yard-current]: http://rubydoc.info/gems/tree_haver
1952
+ [🚎yard-head]: https://tree-haver.galtzo.com
1953
+ [💎stone_checksums]: https://github.com/galtzo-floss/stone_checksums
1954
+ [💎SHA_checksums]: https://gitlab.com/structuredmerge/structuredmerge-ruby/-/tree/main/checksums
1955
+ [💎rlts]: https://github.com/rubocop-lts/rubocop-lts
1956
+ [💎rlts-img]: https://img.shields.io/badge/code_style_&_linting-rubocop--lts-34495e.svg?plastic&logo=ruby&logoColor=white
1957
+ [💎appraisal2]: https://github.com/appraisal-rb/appraisal2
1958
+ [💎appraisal2-img]: https://img.shields.io/badge/appraised_by-appraisal2-34495e.svg?plastic&logo=ruby&logoColor=white
1959
+ [💎d-in-dvcs]: https://railsbling.com/posts/dvcs/put_the_d_in_dvcs/