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