tree_haver 5.0.5 → 7.0.0

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