{"id":35179872,"url":"https://github.com/kettle-rb/tree_haver","last_synced_at":"2026-02-04T21:05:27.400Z","repository":{"id":328639283,"uuid":"1116215673","full_name":"kettle-rb/tree_haver","owner":"kettle-rb","description":"🌴 TreeHaver is a cross-Ruby adapter for the tree-sitter \u0026 citrus parsing libraries; supporting MRI Ruby, JRuby, \u0026 TruffleRuby. Provides unified parsing API \u0026 AST when using ruby_tree_sitter, citrus, ffi, tree_stump (Rust), JRuby JARs, etc. As Faraday is to HTTP clients, this is for ASTs: \"Learn once \u0026 write anywhere; write once \u0026 run anywhere\"","archived":false,"fork":false,"pushed_at":"2026-01-10T11:08:38.000Z","size":4241,"stargazers_count":15,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-01-10T21:54:22.816Z","etag":null,"topics":["ast","ffi","jruby","ruby","rubygem","rust","tree-sitter"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/kettle-rb.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":".github/FUNDING.yml","license":"LICENSE.txt","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":"CITATION.cff","codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"buy_me_a_coffee":"pboling","community_bridge":null,"github":["pboling"],"issuehunt":"pboling","ko_fi":"pboling","liberapay":"pboling","open_collective":"kettle-rb","patreon":"galtzo","polar":"pboling","thanks_dev":"u/gh/pboling","tidelift":"rubygems/tree_haver"}},"created_at":"2025-12-14T12:38:59.000Z","updated_at":"2026-01-10T11:08:42.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/kettle-rb/tree_haver","commit_stats":null,"previous_names":["kettle-rb/tree_haver"],"tags_count":20,"template":false,"template_full_name":null,"purl":"pkg:github/kettle-rb/tree_haver","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kettle-rb%2Ftree_haver","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kettle-rb%2Ftree_haver/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kettle-rb%2Ftree_haver/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kettle-rb%2Ftree_haver/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/kettle-rb","download_url":"https://codeload.github.com/kettle-rb/tree_haver/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kettle-rb%2Ftree_haver/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28337866,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-12T06:09:07.588Z","status":"ssl_error","status_checked_at":"2026-01-12T06:05:18.301Z","response_time":98,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ast","ffi","jruby","ruby","rubygem","rust","tree-sitter"],"created_at":"2025-12-29T00:55:23.560Z","updated_at":"2026-02-04T21:05:27.390Z","avatar_url":"https://github.com/kettle-rb.png","language":"Ruby","funding_links":["https://buymeacoffee.com/pboling","https://github.com/sponsors/pboling","https://issuehunt.io/r/pboling","https://ko-fi.com/pboling","https://liberapay.com/pboling","https://opencollective.com/kettle-rb","https://patreon.com/galtzo","https://polar.sh/pboling","https://thanks.dev/u/gh/pboling","https://tidelift.com/funding/github/rubygems/tree_haver","https://tidelift.com/badges/package/rubygems/tree_haver"],"categories":["Ruby"],"sub_categories":[],"readme":"| 📍 NOTE                                                                                                                                                                                                       |\n|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| RubyGems (the [GitHub org][rubygems-org], not the website) [suffered][draper-security] a [hostile takeover][ellen-takeover] in September 2025.                                                                |\n| Ultimately [4 maintainers][simi-removed] were [hard removed][martin-removed] and a reason has been given for only 1 of those, while 2 others resigned in protest.                                             |\n| It is a [complicated story][draper-takeover] which is difficult to [parse quickly][draper-lies].                                                                                                              |\n| Simply put - there was active policy for adding or removing maintainers/owners of [rubygems][rubygems-maint-policy] and [bundler][bundler-maint-policy], and those [policies were not followed][policy-fail]. |\n| I'm adding notes like this to gems because I [don't condone theft][draper-theft] of repositories or gems from their rightful owners.                                                                          |\n| If a similar theft happened with my repos/gems, I'd hope some would stand up for me.                                                                                                                          |\n| Disenfranchised former-maintainers have started [gem.coop][gem-coop].                                                                                                                                         |\n| Once available I will publish there exclusively; unless RubyCentral makes amends with the community.                                                                                                          |\n| The [\"Technology for Humans: Joel Draper\"][reinteractive-podcast] podcast episode by [reinteractive][reinteractive] is the most cogent summary I'm aware of.                                                  |\n| See [here][gem-naming], [here][gem-coop] and [here][martin-ann] for more info on what comes next.                                                                                                             |\n| What I'm doing: A (WIP) proposal for [bundler/gem scopes][gem-scopes], and a (WIP) proposal for a federated [gem server][gem-server].                                                                         |\n\n[rubygems-org]: https://github.com/rubygems/\n[draper-security]: https://joel.drapper.me/p/ruby-central-security-measures/\n[draper-takeover]: https://joel.drapper.me/p/ruby-central-takeover/\n[ellen-takeover]: https://pup-e.com/blog/goodbye-rubygems/\n[simi-removed]: https://www.reddit.com/r/ruby/s/gOk42POCaV\n[martin-removed]: https://bsky.app/profile/martinemde.com/post/3m3occezxxs2q\n[draper-lies]: https://joel.drapper.me/p/ruby-central-fact-check/\n[draper-theft]: https://joel.drapper.me/p/ruby-central/\n[reinteractive]: https://reinteractive.com/ruby-on-rails\n[gem-coop]: https://gem.coop\n[gem-naming]: https://github.com/gem-coop/gem.coop/issues/12\n[martin-ann]: https://martinemde.com/2025/10/05/announcing-gem-coop.html\n[gem-scopes]: https://github.com/galtzo-floss/bundle-namespace\n[gem-server]: https://github.com/galtzo-floss/gem-server\n[reinteractive-podcast]: https://youtu.be/_H4qbtC5qzU?si=BvuBU90R2wAqD2E6\n[bundler-maint-policy]: https://github.com/ruby/rubygems/blob/b1ab33a3d52310a84d16b193991af07f5a6a07c0/doc/bundler/playbooks/TEAM_CHANGES.md\n[rubygems-maint-policy]: https://github.com/ruby/rubygems/blob/b1ab33a3d52310a84d16b193991af07f5a6a07c0/doc/rubygems/POLICIES.md?plain=1#L187-L196\n[policy-fail]: https://www.reddit.com/r/ruby/comments/1ove9vp/rubycentral_hates_this_one_fact/\n\n[![Galtzo FLOSS Logo by Aboling0, CC BY-SA 4.0][🖼️galtzo-i]][🖼️galtzo-discord] [![ruby-lang Logo, Yukihiro Matsumoto, Ruby Visual Identity Team, CC BY-SA 2.5][🖼️ruby-lang-i]][🖼️ruby-lang] [![kettle-rb Logo by Aboling0, CC BY-SA 4.0][🖼️kettle-rb-i]][🖼️kettle-rb]\n\n[🖼️galtzo-i]: https://logos.galtzo.com/assets/images/galtzo-floss/avatar-192px.svg\n[🖼️galtzo-discord]: https://discord.gg/3qme4XHNKN\n[🖼️ruby-lang-i]: https://logos.galtzo.com/assets/images/ruby-lang/avatar-192px.svg\n[🖼️ruby-lang]: https://www.ruby-lang.org/\n[🖼️kettle-rb-i]: https://logos.galtzo.com/assets/images/kettle-rb/avatar-192px.svg\n[🖼️kettle-rb]: https://github.com/kettle-rb\n\n# 🌴 TreeHaver\n\n[![Version][👽versioni]][👽dl-rank] [![GitHub tag (latest SemVer)][⛳️tag-img]][⛳️tag] [![License: MIT][📄license-img]][📄license-ref] [![Downloads Rank][👽dl-ranki]][👽dl-rank] [![Open Source Helpers][👽oss-helpi]][👽oss-help] [![CodeCov Test Coverage][🏀codecovi]][🏀codecov] [![Coveralls Test Coverage][🏀coveralls-img]][🏀coveralls] [![QLTY Test Coverage][🏀qlty-covi]][🏀qlty-cov] [![QLTY Maintainability][🏀qlty-mnti]][🏀qlty-mnt] [![CI Heads][🚎3-hd-wfi]][🚎3-hd-wf] [![CI Runtime Dependencies @ HEAD][🚎12-crh-wfi]][🚎12-crh-wf] [![CI Current][🚎11-c-wfi]][🚎11-c-wf] [![CI Truffle Ruby][🚎9-t-wfi]][🚎9-t-wf] [![Deps Locked][🚎13-🔒️-wfi]][🚎13-🔒️-wf] [![Deps Unlocked][🚎14-🔓️-wfi]][🚎14-🔓️-wf] [![CI Supported][🚎6-s-wfi]][🚎6-s-wf] [![CI Test Coverage][🚎2-cov-wfi]][🚎2-cov-wf] [![CI Style][🚎5-st-wfi]][🚎5-st-wf] [![CodeQL][🖐codeQL-img]][🖐codeQL] [![Apache SkyWalking Eyes License Compatibility Check][🚎15-🪪-wfi]][🚎15-🪪-wf]\n\n`if ci_badges.map(\u0026:color).detect { it != \"green\"}` ☝️ [let me know][🖼️galtzo-discord], as I may have missed the [discord notification][🖼️galtzo-discord].\n\n-----\n\n`if ci_badges.map(\u0026:color).all? { it == \"green\"}` 👇️ send money so I can do more of this. FLOSS maintenance is now my full-time job.\n\n[![OpenCollective Backers][🖇osc-backers-i]][🖇osc-backers] [![OpenCollective Sponsors][🖇osc-sponsors-i]][🖇osc-sponsors] [![Sponsor Me on Github][🖇sponsor-img]][🖇sponsor] [![Liberapay Goal Progress][⛳liberapay-img]][⛳liberapay] [![Donate on PayPal][🖇paypal-img]][🖇paypal] [![Buy me a coffee][🖇buyme-small-img]][🖇buyme] [![Donate on Polar][🖇polar-img]][🖇polar] [![Donate at ko-fi.com][🖇kofi-img]][🖇kofi]\n\n## 🌻 Synopsis\n\nTreeHaver 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.\n\n### The Adapter Pattern: Like Faraday, but for Parsing\n\nIf 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:\n\n| Gem             | Unified API for | Backend Examples                                                          |\n|-----------------|-----------------|---------------------------------------------------------------------------|\n| **Faraday**     | HTTP requests   | Net::HTTP, Typhoeus, Patron, Excon                                        |\n| **multi\\_json** | JSON parsing    | Oj, Yajl, JSON gem                                                        |\n| **multi\\_xml**  | XML parsing     | Nokogiri, LibXML, Ox                                                      |\n| **TreeHaver**   | Code parsing    | MRI, Rust, FFI, Java, Prism, Psych, Commonmarker, Markly, Citrus, Parslet |\n\n**Learn once, write anywhere.**\n\n**Write once, run anywhere.**\n\nJust as Faraday lets you swap HTTP adapters without changing your code, TreeHaver lets you swap tree-sitter backends. Your parsing code remains the same whether you're running on MRI with native C extensions, JRuby with FFI, or TruffleRuby.\n\n```ruby\n# Your code stays the same regardless of backend\nparser = TreeHaver::Parser.new\nparser.language = TreeHaver::Language.from_library(\"/path/to/grammar.so\")\ntree = parser.parse(source_code)\n\n# TreeHaver automatically picks the best available backend:\n# - MRI: ruby_tree_sitter, tree_stump, ffi, prism, psych, commonmarker, markly, citrus, parslet\n# - JRuby: ffi, java-tree-sitter (not a gem, but the jtreesitter maven package), prism, psych, commonmarker, markly, citrus, parslet\n# - TruffleRuby: prism, psych, commonmarker, markly, citrus, parslet\n#   (tree-sitter backends don't work on Truffleruby with ffi gem due to FFI STRUCT_BY_VALUE limitation)\n```\n\n### Key Features\n\n- **Universal Ruby Support**: Works on MRI Ruby, JRuby, and TruffleRuby\n- **10 Parsing Backends** - Choose the right backend for your needs:\n    - **Tree-sitter Backends** (high-performance, incremental parsing):\n        - **MRI Backend**: Leverages [`ruby_tree_sitter`][ruby_tree_sitter] gem (C extension, fastest on MRI)\n            - **Note**: `ruby_tree_sitter` currently requires unreleased fixes in the `pboling` fork, `tree_haver` branch.\n        - **Rust Backend**: Uses [`tree_stump`][tree_stump] gem (Rust with precompiled binaries)\n            - **Note**: `tree_stump` currently requires unreleased fixes in the `pboling` fork, `tree_haver` branch.\n        - **FFI Backend**: Pure Ruby FFI bindings to `libtree-sitter` (JRuby only; TruffleRuby's FFI doesn't support tree-sitter's struct-by-value returns)\n        - **Java Backend**: Native Java integration for JRuby with [`java-tree-sitter`](https://github.com/tree-sitter/java-tree-sitter) / [`jtreesitter`][jtreesitter] grammar JARs\n    - **Language-Specific Backends** (native parser integration):\n        - **Prism Backend**: Ruby's official parser ([Prism][prism], stdlib in Ruby 3.4+)\n        - **Psych Backend**: Ruby's YAML parser ([Psych][psych], stdlib)\n        - **Commonmarker Backend**: Fast Markdown parser ([Commonmarker][commonmarker], comrak Rust)\n        - **Markly Backend**: GitHub Flavored Markdown ([Markly][markly], cmark-gfm C)\n    - **Pure Ruby Fallback**:\n        - **Citrus Backend**: Pure Ruby PEG parsing via [`citrus`][citrus] (no native dependencies)\n        - **Parslet Backend**: Pure Ruby PEG parsing via [`parslet`][parslet] (no native dependencies)\n- **Automatic Backend Selection**: Intelligently selects the best backend for your Ruby implementation\n- **Language Agnostic**: Parse any language - Ruby, Markdown, YAML, JSON, Bash, TOML, JavaScript, etc.\n- **Grammar Discovery**: Built-in `GrammarFinder` utility for platform-aware grammar library discovery\n- **Unified Position API**: Consistent `start_line`, `end_line`, `source_position` across all backends\n- **Thread-Safe**: Built-in language registry with thread-safe caching\n- **Minimal API Surface**: Simple, focused API that covers the most common use cases\n\n### Backend Requirements\n\nTreeHaver has minimal dependencies and automatically selects the best backend for your Ruby implementation. Each backend has specific version requirements:\n\n#### MRI Backend (ruby\\_tree\\_sitter, C extensions)\n\n**Requires `ruby_tree_sitter` v2.0+**\n\nIn ruby\\_tree\\_sitter v2.0, all TreeSitter exceptions were changed to inherit from `Exception` (not `StandardError`). This was an intentional breaking change made for thread-safety and signal handling reasons.\n\n**Exception Mapping**: TreeHaver catches `TreeSitter::TreeSitterError` and its subclasses, converting them to `TreeHaver::NotAvailable` while preserving the original error message. This provides a consistent exception API across all backends:\n\n| ruby\\_tree\\_sitter Exception      | TreeHaver Exception       | When It Occurs                               |\n|-----------------------------------|---------------------------|----------------------------------------------|\n| `TreeSitter::ParserNotFoundError` | `TreeHaver::NotAvailable` | Parser library file cannot be loaded         |\n| `TreeSitter::LanguageLoadError`   | `TreeHaver::NotAvailable` | Language symbol loads but returns nothing    |\n| `TreeSitter::SymbolNotFoundError` | `TreeHaver::NotAvailable` | Symbol not found in library                  |\n| `TreeSitter::ParserVersionError`  | `TreeHaver::NotAvailable` | Parser version incompatible with tree-sitter |\n| `TreeSitter::QueryCreationError`  | `TreeHaver::NotAvailable` | Query creation fails                         |\n\n```ruby\n# MRI tree-sitter Backend\ngem \"ruby_tree_sitter\",\n  github: \"pboling/ruby-tree-sitter\",\n  branch: \"tree_haver\",\n  require: false # DO NOT LOAD, because conflicts with FFI\n```\n\n#### Rust Backend (tree\\_stump)\n\n**MRI Ruby only** - Does not work on JRuby or TruffleRuby.\n\nThe 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.\n\n- **JRuby**: Cannot load native `.so` extensions (runs on JVM)\n- **TruffleRuby**: magnus/rb-sys are incompatible with TruffleRuby's C API emulation\n  NOTE: `tree_stump` currently requires unreleased fixes in the `main` branch.\n\n```ruby\n# Rust tree-sitter backend (MRI only)\ngem \"tree_stump\",\n    # path: \"../../vendor/tree_stump\"\n  github: \"pboling/tree_stump\",\n  branch: \"tree_haver\"\n```\n\n#### FFI Backend\n\n**MRI and JRuby only** - Does not work on TruffleRuby.\n\nRequires the `ffi` gem and a system installation of `libtree-sitter`.\n\n- **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`.\n\n```ruby\n# Add to your Gemfile for FFI backend (MRI and JRuby)\ngem \"ffi\", \"\u003e= 1.15\", \"\u003c 2.0\"\n```\n\n```bash\n# Install libtree-sitter on your system:\n# macOS\nbrew install tree-sitter\n\n# Ubuntu/Debian\napt-get install libtree-sitter0 libtree-sitter-dev\n\n# Fedora\ndnf install tree-sitter tree-sitter-devel\n```\n\n#### Citrus Backend\n\nPure Ruby PEG parser with no native dependencies:\n\n```ruby\n# Add to your Gemfile for Citrus backend\ngem \"citrus\", \"~\u003e 3.0\"\n```\n\n#### Parslet Backend\n\nPure Ruby PEG parser with no native dependencies:\n\n```ruby\n# Add to your Gemfile for Parslet backend\ngem \"parslet\", \"~\u003e 2.0\"\n```\n\n#### Java Backend (JRuby only)\n\n**Requires jtreesitter \\\u003e= 0.26.0** from Maven Central. Older versions are not supported due to breaking API changes.\n\n```ruby\n# No gem dependency - uses JRuby's built-in Java integration\n# Download the JAR:\n# curl -L -o jtreesitter-0.26.0.jar \\\n#   \"https://repo1.maven.org/maven2/io/github/tree-sitter/jtreesitter/0.26.0/jtreesitter-0.26.0.jar\"\n\n# Set environment variable:\n# export TREE_SITTER_JAVA_JARS_DIR=/path/to/jars\n```\n\n**Also requires**:\n\n- Tree-sitter runtime library (`libtree-sitter.so`) version 0.26+ (must match jtreesitter version)\n- Grammar `.so` files built against tree-sitter 0.26+ (or rebuilt with `tree-sitter generate`)\n\n### Version Requirements for Tree-Sitter Backends\n\n#### tree-sitter Runtime Library\n\nAll 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.\n\n```bash\n# Check your tree-sitter version\ntree-sitter --version  # Should be 0.26.0 or newer for Java backend\n\n# macOS\nbrew install tree-sitter\n\n# Ubuntu/Debian\napt-get install libtree-sitter0 libtree-sitter-dev\n\n# Fedora\ndnf install tree-sitter tree-sitter-devel\n```\n\n#### jtreesitter (Java Backend)\n\n**The Java backend requires jtreesitter \\\u003e= 0.26.0.** This version introduced breaking API changes:\n\n- `Parser.parse()` returns `Optional\u003cTree\u003e` instead of `Tree`\n- `Tree.getRootNode()` returns `Node` directly (not `Optional\u003cNode\u003e`)\n- `Node.getChild()`, `getParent()`, `getNextSibling()`, `getPrevSibling()` return `Optional\u003cNode\u003e`\n- `Language.load(name)` was removed; use `SymbolLookup` API instead\n  Older versions of jtreesitter are **NOT supported**.\n\n```bash\n# Download jtreesitter 0.26.0 from Maven Central\ncurl -L -o jtreesitter-0.26.0.jar \\\n  \"https://repo1.maven.org/maven2/io/github/tree-sitter/jtreesitter/0.26.0/jtreesitter-0.26.0.jar\"\n\n# Or use the provided setup script\nbin/setup-jtreesitter\n```\n\nSet the environment variable to point to your JAR directory:\n\n```bash\nexport TREE_SITTER_JAVA_JARS_DIR=/path/to/jars\n```\n\n#### Grammar ABI Compatibility\n\n**CRITICAL**: Grammars must be built against a compatible tree-sitter version.\n\nTree-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:\n\n    Failed to load tree_sitter_toml\n    Version mismatch detected: The grammar was built against tree-sitter \u003c 0.26\n\nYou need to rebuild the grammar from source:\n\n```bash\n# Use the provided build script\nbin/build-grammar toml\n\n# Or manually:\ngit clone https://github.com/tree-sitter-grammars/tree-sitter-toml\ncd tree-sitter-toml\ntree-sitter generate  # Regenerates parser.c for your tree-sitter version\ncc -shared -fPIC -o libtree-sitter-toml.so src/parser.c src/scanner.c -I src\n```\n\n**Grammar sources for common languages:**\n\n| Language | Repository                                       |\n|----------|--------------------------------------------------|\n| TOML     | [tree-sitter-grammars/tree-sitter-toml][ts-toml] |\n| JSON     | [tree-sitter/tree-sitter-json][ts-json]          |\n| JSONC    | [WhyNotHugo/tree-sitter-jsonc][ts-jsonc]         |\n| Bash     | [tree-sitter/tree-sitter-bash][ts-bash]          |\n\n#### TruffleRuby Limitations\n\nTruffleRuby has **no working tree-sitter backend**:\n\n- **FFI**: TruffleRuby's FFI doesn't support `STRUCT_BY_VALUE` return types (used by `ts_tree_root_node`, `ts_node_child`, etc.)\n- **MRI/Rust**: C and Rust extensions require MRI's C API internals (`RBasic.flags`, `rb_gc_writebarrier`, etc.) that TruffleRuby doesn't expose\n  TruffleRuby users should use: **Prism** (Ruby), **Psych** (YAML), **Citrus/Parslet** (e.g., TOML via toml-rb/toml), or potentially **Commonmarker/Markly** (Markdown).\n\n#### JRuby Limitations\n\nJRuby runs on the JVM and **cannot load native `.so` extensions via Ruby's C API**:\n\n- **MRI/Rust**: C and Rust extensions simply cannot be loaded\n- **FFI**: Works\\! JRuby has excellent FFI support\n- **Java**: Works\\! The Java backend uses jtreesitter (requires \\\u003e= 0.26.0)\n  JRuby users should use: **Java backend** (best performance, full API) or **FFI backend** for tree-sitter, plus **Prism**, **Psych**, **Citrus/Parslet** for other formats.\n\n### Why TreeHaver?\n\ntree-sitter is a powerful parser generator that creates incremental parsers for many programming languages. However, integrating it into Ruby applications can be challenging:\n\n- MRI-based C extensions don't work on JRuby\n- FFI-based solutions may not be optimal for MRI\n- Managing different backends for different Ruby implementations is cumbersome\n  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.\n\n### The `*-merge` Gem Family\n\nThe `*-merge` gem family provides intelligent, AST-based merging for various file formats. At the foundation is [tree_haver][tree_haver], which provides a unified cross-Ruby parsing API that works seamlessly across MRI, JRuby, and TruffleRuby.\n\n| Gem                                      |                                                         Version / CI                                                         | Language\u003cbr\u003e/ Format | Parser Backend(s)                                                                                     | Description                                                                      |\n|------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------:|----------------------|-------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------|\n| [tree_haver][tree_haver]                 |                 [![Version][tree_haver-gem-i]][tree_haver-gem] \u003cbr/\u003e [![CI][tree_haver-ci-i]][tree_haver-ci]                 | Multi                | Supported Backends: MRI C, Rust, FFI, Java, Prism, Psych, Commonmarker, Markly, Citrus, Parslet       | **Foundation**: Cross-Ruby adapter for parsing libraries (like Faraday for HTTP) |\n| [ast-merge][ast-merge]                   |                   [![Version][ast-merge-gem-i]][ast-merge-gem] \u003cbr/\u003e [![CI][ast-merge-ci-i]][ast-merge-ci]                   | Text                 | internal                                                                                              | **Infrastructure**: Shared base classes and merge logic for all `*-merge` gems   |\n| [bash-merge][bash-merge]                 |                 [![Version][bash-merge-gem-i]][bash-merge-gem] \u003cbr/\u003e [![CI][bash-merge-ci-i]][bash-merge-ci]                 | Bash                 | [tree-sitter-bash][ts-bash] (via tree_haver)                                                          | Smart merge for Bash scripts                                                     |\n| [commonmarker-merge][commonmarker-merge] | [![Version][commonmarker-merge-gem-i]][commonmarker-merge-gem] \u003cbr/\u003e [![CI][commonmarker-merge-ci-i]][commonmarker-merge-ci] | Markdown             | [Commonmarker][commonmarker] (via tree_haver)                                                         | Smart merge for Markdown (CommonMark via comrak Rust)                            |\n| [dotenv-merge][dotenv-merge]             |             [![Version][dotenv-merge-gem-i]][dotenv-merge-gem] \u003cbr/\u003e [![CI][dotenv-merge-ci-i]][dotenv-merge-ci]             | Dotenv               | internal                                                                                              | Smart merge for `.env` files                                                     |\n| [json-merge][json-merge]                 |                 [![Version][json-merge-gem-i]][json-merge-gem] \u003cbr/\u003e [![CI][json-merge-ci-i]][json-merge-ci]                 | JSON                 | [tree-sitter-json][ts-json] (via tree_haver)                                                          | Smart merge for JSON files                                                       |\n| [jsonc-merge][jsonc-merge]               |               [![Version][jsonc-merge-gem-i]][jsonc-merge-gem] \u003cbr/\u003e [![CI][jsonc-merge-ci-i]][jsonc-merge-ci]               | JSONC                | [tree-sitter-jsonc][ts-jsonc] (via tree_haver)                                                        | ⚠️ Proof of concept; Smart merge for JSON with Comments                          |\n| [markdown-merge][markdown-merge]         |         [![Version][markdown-merge-gem-i]][markdown-merge-gem] \u003cbr/\u003e [![CI][markdown-merge-ci-i]][markdown-merge-ci]         | Markdown             | [Commonmarker][commonmarker] / [Markly][markly] (via tree_haver), [Parslet][parslet]                  | **Foundation**: Shared base for Markdown mergers with inner code block merging   |\n| [markly-merge][markly-merge]             |             [![Version][markly-merge-gem-i]][markly-merge-gem] \u003cbr/\u003e [![CI][markly-merge-ci-i]][markly-merge-ci]             | Markdown             | [Markly][markly] (via tree_haver)                                                                     | Smart merge for Markdown (CommonMark via cmark-gfm C)                            |\n| [prism-merge][prism-merge]               |               [![Version][prism-merge-gem-i]][prism-merge-gem] \u003cbr/\u003e [![CI][prism-merge-ci-i]][prism-merge-ci]               | Ruby                 | [Prism][prism] (`prism` std lib gem)                                                                  | Smart merge for Ruby source files                                                |\n| [psych-merge][psych-merge]               |               [![Version][psych-merge-gem-i]][psych-merge-gem] \u003cbr/\u003e [![CI][psych-merge-ci-i]][psych-merge-ci]               | YAML                 | [Psych][psych] (`psych` std lib gem)                                                                  | Smart merge for YAML files                                                       |\n| [rbs-merge][rbs-merge]                   |                   [![Version][rbs-merge-gem-i]][rbs-merge-gem] \u003cbr/\u003e [![CI][rbs-merge-ci-i]][rbs-merge-ci]                   | RBS                  | [tree-sitter-bash][ts-rbs] (via tree_haver), [RBS][rbs] (`rbs` std lib gem)                           | Smart merge for Ruby type signatures                                             |\n| [toml-merge][toml-merge]                 |                 [![Version][toml-merge-gem-i]][toml-merge-gem] \u003cbr/\u003e [![CI][toml-merge-ci-i]][toml-merge-ci]                 | TOML                 | [Parslet + toml][toml], [Citrus + toml-rb][toml-rb], [tree-sitter-toml][ts-toml] (all via tree_haver) | Smart merge for TOML files                                                       |\n\n#### Backend Platform Compatibility\n\ntree_haver supports multiple parsing backends, but not all backends work on all Ruby platforms:\n\n| Platform 👉️\u003cbr\u003e TreeHaver Backend 👇️          | MRI | JRuby | TruffleRuby | Notes                                                                      |\n|-------------------------------------------------|:---:|:-----:|:-----------:|----------------------------------------------------------------------------|\n| **MRI** ([ruby_tree_sitter][ruby_tree_sitter])  |  ✅  |   ❌   |      ❌      | C extension, MRI only                                                      |\n| **Rust** ([tree_stump][tree_stump])             |  ✅  |   ❌   |      ❌      | Rust extension via magnus/rb-sys, MRI only                                 |\n| **FFI** ([ffi][ffi])                            |  ✅  |   ✅   |      ❌      | TruffleRuby's FFI doesn't support `STRUCT_BY_VALUE`                        |\n| **Java** ([jtreesitter][jtreesitter])           |  ❌  |   ✅   |      ❌      | JRuby only, requires grammar JARs                                          |\n| **Prism** ([prism][prism])                      |  ✅  |   ✅   |      ✅      | Ruby parsing, stdlib in Ruby 3.4+                                          |\n| **Psych** ([psych][psych])                      |  ✅  |   ✅   |      ✅      | YAML parsing, stdlib                                                       |\n| **Citrus** ([citrus][citrus])                   |  ✅  |   ✅   |      ✅      | Pure Ruby PEG parser, no native dependencies                               |\n| **Parslet** ([parslet][parslet])                |  ✅  |   ✅   |      ✅      | Pure Ruby PEG parser, no native dependencies                               |\n| **Commonmarker** ([commonmarker][commonmarker]) |  ✅  |   ❌   |      ❓      | Rust extension for Markdown (via [commonmarker-merge][commonmarker-merge]) |\n| **Markly** ([markly][markly])                   |  ✅  |   ❌   |      ❓      | C extension for Markdown  (via [markly-merge][markly-merge])               |\n\n**Legend**: ✅ = Works, ❌ = Does not work, ❓ = Untested\n\n**Why some backends don't work on certain platforms**:\n\n- **JRuby**: Runs on the JVM; cannot load native C/Rust extensions (`.so` files)\n- **TruffleRuby**: Has C API emulation via Sulong/LLVM, but it doesn't expose all MRI internals that native extensions require (e.g., `RBasic.flags`, `rb_gc_writebarrier`)\n- **FFI on TruffleRuby**: TruffleRuby's FFI implementation doesn't support returning structs by value, which tree-sitter's C API requires\n\n**Example implementations** for the gem templating use case:\n\n| Gem                      | Purpose         | Description                                   |\n|--------------------------|-----------------|-----------------------------------------------|\n| [kettle-dev][kettle-dev] | Gem Development | Gem templating tool using `*-merge` gems      |\n| [kettle-jem][kettle-jem] | Gem Templating  | Gem template library with smart merge support |\n\n[tree_haver]: https://github.com/kettle-rb/tree_haver\n[ast-merge]: https://github.com/kettle-rb/ast-merge\n[prism-merge]: https://github.com/kettle-rb/prism-merge\n[psych-merge]: https://github.com/kettle-rb/psych-merge\n[json-merge]: https://github.com/kettle-rb/json-merge\n[jsonc-merge]: https://github.com/kettle-rb/jsonc-merge\n[bash-merge]: https://github.com/kettle-rb/bash-merge\n[rbs-merge]: https://github.com/kettle-rb/rbs-merge\n[dotenv-merge]: https://github.com/kettle-rb/dotenv-merge\n[toml-merge]: https://github.com/kettle-rb/toml-merge\n[markdown-merge]: https://github.com/kettle-rb/markdown-merge\n[markly-merge]: https://github.com/kettle-rb/markly-merge\n[commonmarker-merge]: https://github.com/kettle-rb/commonmarker-merge\n[kettle-dev]: https://github.com/kettle-rb/kettle-dev\n[kettle-jem]: https://github.com/kettle-rb/kettle-jem\n[tree_haver-gem]: https://bestgems.org/gems/tree_haver\n[ast-merge-gem]: https://bestgems.org/gems/ast-merge\n[prism-merge-gem]: https://bestgems.org/gems/prism-merge\n[psych-merge-gem]: https://bestgems.org/gems/psych-merge\n[json-merge-gem]: https://bestgems.org/gems/json-merge\n[jsonc-merge-gem]: https://bestgems.org/gems/jsonc-merge\n[bash-merge-gem]: https://bestgems.org/gems/bash-merge\n[rbs-merge-gem]: https://bestgems.org/gems/rbs-merge\n[dotenv-merge-gem]: https://bestgems.org/gems/dotenv-merge\n[toml-merge-gem]: https://bestgems.org/gems/toml-merge\n[markdown-merge-gem]: https://bestgems.org/gems/markdown-merge\n[markly-merge-gem]: https://bestgems.org/gems/markly-merge\n[commonmarker-merge-gem]: https://bestgems.org/gems/commonmarker-merge\n[kettle-dev-gem]: https://bestgems.org/gems/kettle-dev\n[kettle-jem-gem]: https://bestgems.org/gems/kettle-jem\n[tree_haver-gem-i]: https://img.shields.io/gem/v/tree_haver.svg\n[ast-merge-gem-i]: https://img.shields.io/gem/v/ast-merge.svg\n[prism-merge-gem-i]: https://img.shields.io/gem/v/prism-merge.svg\n[psych-merge-gem-i]: https://img.shields.io/gem/v/psych-merge.svg\n[json-merge-gem-i]: https://img.shields.io/gem/v/json-merge.svg\n[jsonc-merge-gem-i]: https://img.shields.io/gem/v/jsonc-merge.svg\n[bash-merge-gem-i]: https://img.shields.io/gem/v/bash-merge.svg\n[rbs-merge-gem-i]: https://img.shields.io/gem/v/rbs-merge.svg\n[dotenv-merge-gem-i]: https://img.shields.io/gem/v/dotenv-merge.svg\n[toml-merge-gem-i]: https://img.shields.io/gem/v/toml-merge.svg\n[markdown-merge-gem-i]: https://img.shields.io/gem/v/markdown-merge.svg\n[markly-merge-gem-i]: https://img.shields.io/gem/v/markly-merge.svg\n[commonmarker-merge-gem-i]: https://img.shields.io/gem/v/commonmarker-merge.svg\n[kettle-dev-gem-i]: https://img.shields.io/gem/v/kettle-dev.svg\n[kettle-jem-gem-i]: https://img.shields.io/gem/v/kettle-jem.svg\n[tree_haver-ci-i]: https://github.com/kettle-rb/tree_haver/actions/workflows/current.yml/badge.svg\n[ast-merge-ci-i]: https://github.com/kettle-rb/ast-merge/actions/workflows/current.yml/badge.svg\n[prism-merge-ci-i]: https://github.com/kettle-rb/prism-merge/actions/workflows/current.yml/badge.svg\n[psych-merge-ci-i]: https://github.com/kettle-rb/psych-merge/actions/workflows/current.yml/badge.svg\n[json-merge-ci-i]: https://github.com/kettle-rb/json-merge/actions/workflows/current.yml/badge.svg\n[jsonc-merge-ci-i]: https://github.com/kettle-rb/jsonc-merge/actions/workflows/current.yml/badge.svg\n[bash-merge-ci-i]: https://github.com/kettle-rb/bash-merge/actions/workflows/current.yml/badge.svg\n[rbs-merge-ci-i]: https://github.com/kettle-rb/rbs-merge/actions/workflows/current.yml/badge.svg\n[dotenv-merge-ci-i]: https://github.com/kettle-rb/dotenv-merge/actions/workflows/current.yml/badge.svg\n[toml-merge-ci-i]: https://github.com/kettle-rb/toml-merge/actions/workflows/current.yml/badge.svg\n[markdown-merge-ci-i]: https://github.com/kettle-rb/markdown-merge/actions/workflows/current.yml/badge.svg\n[markly-merge-ci-i]: https://github.com/kettle-rb/markly-merge/actions/workflows/current.yml/badge.svg\n[commonmarker-merge-ci-i]: https://github.com/kettle-rb/commonmarker-merge/actions/workflows/current.yml/badge.svg\n[kettle-dev-ci-i]: https://github.com/kettle-rb/kettle-dev/actions/workflows/current.yml/badge.svg\n[kettle-jem-ci-i]: https://github.com/kettle-rb/kettle-jem/actions/workflows/current.yml/badge.svg\n[tree_haver-ci]: https://github.com/kettle-rb/tree_haver/actions/workflows/current.yml\n[ast-merge-ci]: https://github.com/kettle-rb/ast-merge/actions/workflows/current.yml\n[prism-merge-ci]: https://github.com/kettle-rb/prism-merge/actions/workflows/current.yml\n[psych-merge-ci]: https://github.com/kettle-rb/psych-merge/actions/workflows/current.yml\n[json-merge-ci]: https://github.com/kettle-rb/json-merge/actions/workflows/current.yml\n[jsonc-merge-ci]: https://github.com/kettle-rb/jsonc-merge/actions/workflows/current.yml\n[bash-merge-ci]: https://github.com/kettle-rb/bash-merge/actions/workflows/current.yml\n[rbs-merge-ci]: https://github.com/kettle-rb/rbs-merge/actions/workflows/current.yml\n[dotenv-merge-ci]: https://github.com/kettle-rb/dotenv-merge/actions/workflows/current.yml\n[toml-merge-ci]: https://github.com/kettle-rb/toml-merge/actions/workflows/current.yml\n[markdown-merge-ci]: https://github.com/kettle-rb/markdown-merge/actions/workflows/current.yml\n[markly-merge-ci]: https://github.com/kettle-rb/markly-merge/actions/workflows/current.yml\n[commonmarker-merge-ci]: https://github.com/kettle-rb/commonmarker-merge/actions/workflows/current.yml\n[kettle-dev-ci]: https://github.com/kettle-rb/kettle-dev/actions/workflows/current.yml\n[kettle-jem-ci]: https://github.com/kettle-rb/kettle-jem/actions/workflows/current.yml\n[prism]: https://github.com/ruby/prism\n[psych]: https://github.com/ruby/psych\n[ffi]: https://github.com/ffi/ffi\n[ts-json]: https://github.com/tree-sitter/tree-sitter-json\n[ts-jsonc]: https://gitlab.com/WhyNotHugo/tree-sitter-jsonc\n[ts-bash]: https://github.com/tree-sitter/tree-sitter-bash\n[ts-rbs]: https://github.com/joker1007/tree-sitter-rbs\n[ts-toml]: https://github.com/tree-sitter-grammars/tree-sitter-toml\n[dotenv]: https://github.com/bkeepers/dotenv\n[rbs]: https://github.com/ruby/rbs\n[toml-rb]: https://github.com/emancu/toml-rb\n[toml]: https://github.com/jm/toml\n[markly]: https://github.com/ioquatix/markly\n[commonmarker]: https://github.com/gjtorikian/commonmarker\n[ruby_tree_sitter]: https://github.com/Faveod/ruby-tree-sitter\n[tree_stump]: https://github.com/joker1007/tree_stump\n[jtreesitter]: https://central.sonatype.com/artifact/io.github.tree-sitter/jtreesitter\n[citrus]: https://github.com/mjackson/citrus\n[parslet]: https://github.com/kschiess/parslet\n\n### Comparison with Other Ruby AST / Parser Bindings\n\n| Feature                   | [tree\\_haver][📜src-gh] (this gem)              | [ruby\\_tree\\_sitter][ruby_tree_sitter] | [tree\\_stump][tree_stump] | [citrus][citrus] | [parslet][parslet] |\n|---------------------------|-------------------------------------------------|----------------------------------------|---------------------------|------------------|--------------------|\n| **MRI Ruby**              | ✅ Yes                                           | ✅ Yes                                  | ✅ Yes                     | ✅ Yes            | ✅ Yes              |\n| **JRuby**                 | ✅ Yes (FFI, Java, Citrus, or Parslet backend)   | ❌ No                                   | ❌ No                      | ✅ Yes            | ✅ Yes              |\n| **TruffleRuby**           | ✅ Yes (FFI, Citrus, or Parslet)                 | ❌ No                                   | ❓ Unknown                 | ✅ Yes            | ✅ Yes              |\n| **Backend**               | Multi (MRI C, Rust, FFI, Java, Citrus, Parslet) | C extension only                       | Rust extension            | Pure Ruby        | Pure Ruby          |\n| **Incremental Parsing**   | ✅ Via MRI C/Rust/Java backend                   | ✅ Yes                                  | ✅ Yes                     | ❌ No             | ❌ No               |\n| **Query API**             | ⚡ Via MRI/Rust/Java backend                     | ✅ Yes                                  | ✅ Yes                     | ❌ No             | ❌ No               |\n| **Grammar Discovery**     | ✅ Built-in `GrammarFinder`                      | ❌ Manual                               | ❌ Manual                  | ❌ Manual         | ❌ Manual           |\n| **Security Validations**  | ✅ `PathValidator`                               | ❌ No                                   | ❌ No                      | ❌ No             | ❌ No               |\n| **Language Registration** | ✅ Thread-safe registry                          | ❌ No                                   | ❌ No                      | ❌ No             | ❌ No               |\n| **Native Performance**    | ⚡ Backend-dependent                             | ✅ Native C                             | ✅ Native Rust             | ❌ Pure Ruby      | ❌ Pure Ruby        |\n| **Precompiled Binaries**  | ⚡ Via Rust backend                              | ✅ Yes                                  | ✅ Yes                     | ✅ Pure Ruby      | ✅ Pure Ruby        |\n| **Zero Native Deps**      | ⚡ Via Citrus/Parslet backend                    | ❌ No                                   | ❌ No                      | ✅ Yes            | ✅ Yes              |\n| **Minimum Ruby**          | 3.2+                                            | 3.0+                                   | 3.1+                      | 0+               | 0+                 |\n\n**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.\n\n**Note:** TreeHaver can use `ruby_tree_sitter` (MRI) or `tree_stump` (MRI) as backends, or `java-tree-sitter` / `jtreesitter` \\\u003e= 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.\n\n**Note:** `tree_stump` currently requires unreleased fixes in the `main` branch.\n\n#### When to Use Each\n\n**Choose TreeHaver when:**\n\n- You need JRuby or TruffleRuby support\n- You're building a library that should work across Ruby implementations\n- You want automatic grammar discovery and security validations\n- You want flexibility to switch backends without code changes\n- You need incremental parsing with a unified API\n\n**Choose ruby\\_tree\\_sitter directly when:**\n\n- You only target MRI Ruby\n- You need the full Query API without abstraction\n- You want the most battle-tested C bindings\n- You don't need TreeHaver's grammar discovery\n\n**Choose tree\\_stump directly when:**\n\n- You only target MRI Ruby\n- You prefer Rust-based native extensions\n- You want precompiled binaries without system dependencies\n- You don't need TreeHaver's grammar discovery\n- **Note:** `tree_stump` currently requires unreleased fixes in the `main` branch.\n\n**Choose citrus or parslet directly when:**\n\n- You need zero native dependencies (pure Ruby)\n- You're using a Citrus or Parslet grammar (not tree-sitter grammars)\n- Performance is less critical than portability\n- You don't need TreeHaver's unified API\n\n## 💡 Info you can shake a stick at\n\n| Tokens to Remember      | [![Gem name][⛳️name-img]][👽dl-rank] [![Gem namespace][⛳️namespace-img]][📜src-gh]                                                                                                                                                                                                                                                                                                                                                                                        |\n|-------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| Works with JRuby        | [![JRuby 10.0 Compat][💎jruby-c-i]][🚎11-c-wf] [![JRuby HEAD Compat][💎jruby-headi]][🚎3-hd-wf]                                                                                                                                                                                                                                                                                                               |\n| Works with Truffle Ruby | [![Truffle Ruby 23.1 Compat][💎truby-23.1i]][🚎9-t-wf] [![Truffle Ruby 24.1 Compat][💎truby-c-i]][🚎11-c-wf]                                                                                                                                                                                                                                                                                               |\n| Works with MRI Ruby 3   | [![Ruby 3.2 Compat][💎ruby-3.2i]][🚎6-s-wf] [![Ruby 3.3 Compat][💎ruby-3.3i]][🚎6-s-wf] [![Ruby 3.4 Compat][💎ruby-c-i]][🚎11-c-wf] [![Ruby HEAD Compat][💎ruby-headi]][🚎3-hd-wf]                                                                                              |\n| Support \u0026 Community     | [![Join Me on Daily.dev's RubyFriends][✉️ruby-friends-img]][✉️ruby-friends] [![Live Chat on Discord][✉️discord-invite-img-ftb]][🖼️galtzo-discord] [![Get help from me on Upwork][👨🏼‍🏫expsup-upwork-img]][👨🏼‍🏫expsup-upwork] [![Get help from me on Codementor][👨🏼‍🏫expsup-codementor-img]][👨🏼‍🏫expsup-codementor]                    |\n| Source                  | [![Source on GitLab.com][📜src-gl-img]][📜src-gl] [![Source on CodeBerg.org][📜src-cb-img]][📜src-cb] [![Source on Github.com][📜src-gh-img]][📜src-gh] [![The best SHA: dQw4w9WgXcQ\\!](https://img.shields.io/badge/KLOC-2.484-FFDD67.svg?style=for-the-badge\u0026logo=YouTube\u0026logoColor=blue)][🧮kloc]                                                                                           |\n| 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]                                                                                                        |\n| Compliance              | [![License: MIT][📄license-img]][📄license-ref] [![Compatible with Apache Software Projects: Verified by SkyWalking Eyes][📄license-compat-img]][📄license-compat] [![📄ilo-declaration-img][📄ilo-declaration-img]][📄ilo-declaration] [![Security Policy][🔐security-img]][🔐security] [![Contributor Covenant 2.1][🪇conduct-img]][🪇conduct] [![SemVer 2.0.0][📌semver-img]][📌semver] |\n| 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]                                                                                                                                                                                  |\n| 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]                                                                                                                             |\n| `...` 💖                | [![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]                                                                                                                                                    |\n\n### Compatibility\n\nCompatible with MRI Ruby 3.2.0+, and concordant releases of JRuby, and TruffleRuby.\n\n| 🚚 *Amazing* test matrix was brought to you by | 🔎 appraisal2 🔎 and the color 💚 green 💚             |\n|------------------------------------------------|--------------------------------------------------------|\n| 👟 Check it out\\!                              | ✨ [github.com/appraisal-rb/appraisal2][💎appraisal2] ✨ |\n\n### Federated DVCS\n\n\u003cdetails markdown=\"1\"\u003e\n  \u003csummary\u003eFind this repo on federated forges (Coming soon!)\u003c/summary\u003e\n\n| Federated [DVCS][💎d-in-dvcs] Repository         | Status                                                                             | Issues                     | PRs                       | Wiki                       | CI                        | Discussions                    |\n|--------------------------------------------------|------------------------------------------------------------------------------------|----------------------------|---------------------------|----------------------------|---------------------------|--------------------------------|\n| 🧪 [kettle-rb/tree\\_haver on GitLab][📜src-gl]   | The Truth                                                                          | [💚][🤝gl-issues]          | [💚][🤝gl-pulls]          | [💚][📜gl-wiki]            | 🐭 Tiny Matrix            | ➖                              |\n| 🧊 [kettle-rb/tree\\_haver on CodeBerg][📜src-cb] | An Ethical Mirror ([Donate][🤝cb-donate])                                          | [💚][🤝cb-issues]          | [💚][🤝cb-pulls]          | ➖                          | ⭕️ No Matrix              | ➖                              |\n| 🐙 [kettle-rb/tree\\_haver on GitHub][📜src-gh]   | Another Mirror                                                                     | [💚][🤝gh-issues]          | [💚][🤝gh-pulls]          | [💚][📜gh-wiki]            | 💯 Full Matrix            | [💚][gh-discussions]           |\n| 🎮️ [Discord Server][🖼️galtzo-discord]          | [![Live Chat on Discord][✉️discord-invite-img-ftb]][🖼️galtzo-discord] | [Let's][🖼️galtzo-discord] | [talk][🖼️galtzo-discord] | [about][🖼️galtzo-discord] | [this][🖼️galtzo-discord] | [library\\!][🖼️galtzo-discord] |\n\n\u003c/details\u003e\n\n[gh-discussions]: https://github.com/kettle-rb/tree_haver/discussions\n\n### Enterprise Support [![Tidelift](https://tidelift.com/badges/package/rubygems/tree_haver)][🏙️entsup-tidelift]\n\nAvailable as part of the Tidelift Subscription.\n\n\u003cdetails markdown=\"1\"\u003e\n  \u003csummary\u003eNeed enterprise-level guarantees?\u003c/summary\u003e\n\nThe maintainers of this and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source packages you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact packages you use.\n\n[![Get help from me on Tidelift][🏙️entsup-tidelift-img]][🏙️entsup-tidelift]\n\n- 💡Subscribe for support guarantees covering *all* your FLOSS dependencies\n\n- 💡Tidelift is part of [Sonar][🏙️entsup-tidelift-sonar]\n\n- 💡Tidelift pays maintainers to maintain the software you depend on\\!\u003cbr/\u003e📊`@`Pointy Haired Boss: An [enterprise support][🏙️entsup-tidelift] subscription is \"[never gonna let you down][🧮kloc]\", and *supports* open source maintainers\n  Alternatively:\n\n- [![Live Chat on Discord][✉️discord-invite-img-ftb]][🖼️galtzo-discord]\n\n- [![Get help from me on Upwork][👨🏼‍🏫expsup-upwork-img]][👨🏼‍🏫expsup-upwork]\n\n- [![Get help from me on Codementor][👨🏼‍🏫expsup-codementor-img]][👨🏼‍🏫expsup-codementor]\n\n\u003c/details\u003e\n\n## ✨ Installation\n\nInstall the gem and add to the application's Gemfile by executing:\n\n```console\nbundle add tree_haver\n```\n\nIf bundler is not being used to manage dependencies, install the gem by executing:\n\n```console\ngem install tree_haver\n```\n\n### 🔒 Secure Installation\n\n\u003cdetails markdown=\"1\"\u003e\n  \u003csummary\u003eFor Medium or High Security Installations\u003c/summary\u003e\n\nThis gem is cryptographically signed, and has verifiable [SHA-256 and SHA-512][💎SHA_checksums] checksums by\n[stone\\_checksums][💎stone_checksums]. Be sure the gem you install hasn’t been tampered with\nby following the instructions below.\n\nAdd my public key (if you haven’t already, expires 2045-04-29) as a trusted certificate:\n\n```console\ngem cert --add \u003c(curl -Ls https://raw.github.com/galtzo-floss/certs/main/pboling.pem)\n```\n\nYou only need to do that once. Then proceed to install with:\n\n```console\ngem install tree_haver -P HighSecurity\n```\n\nThe `HighSecurity` trust profile will verify signed gems, and not allow the installation of unsigned dependencies.\n\nIf you want to up your security game full-time:\n\n```console\nbundle config set --global trust-policy MediumSecurity\n```\n\n`MediumSecurity` instead of `HighSecurity` is necessary if not all the gems you use are signed.\n\nNOTE: Be prepared to track down certs for signed gems and add them the same way you added mine.\n\n\u003c/details\u003e\n\n## ⚙️ Configuration\n\n### Available Backends\n\nTreeHaver supports 10 parsing backends, each with different trade-offs. The `auto` backend automatically selects the best available option.\n\n#### Tree-sitter Backends (Universal Parsing)\n\n| Backend  | Description                           | Performance | Portability | Examples                                                                                                                        |\n|----------|---------------------------------------|-------------|-------------|---------------------------------------------------------------------------------------------------------------------------------|\n| **Auto** | Auto-selects best backend             | Varies      | ✅ Universal | [JSON](examples/auto_json.rb) · [JSONC](examples/auto_jsonc.rb) · [Bash](examples/auto_bash.rb) · [TOML](examples/auto_toml.rb) |\n| **MRI**  | C extension via ruby\\_tree\\_sitter    | ⚡ Fastest   | MRI only    | [JSON](examples/mri_json.rb) · [JSONC](examples/mri_jsonc.rb) · \\~\\~Bash\\~\\~\\* · [TOML](examples/mri_toml.rb)                   |\n| **Rust** | Precompiled via tree\\_stump           | ⚡ Very Fast | ✅ Good      | [JSON](examples/rust_json.rb) · [JSONC](examples/rust_jsonc.rb) · \\~\\~Bash\\~\\~\\* · [TOML](examples/rust_toml.rb)                |\n| **FFI**  | Dynamic linking via FFI               | 🔵 Fast     | ✅ Universal | [JSON](examples/ffi_json.rb) · [JSONC](examples/ffi_jsonc.rb) · [Bash](examples/ffi_bash.rb) · [TOML](examples/ffi_toml.rb)     |\n| **Java** | JNI bindings (jtreesitter \\\u003e= 0.26.0) | ⚡ Very Fast | JRuby only  | [JSON](examples/java_json.rb) · [JSONC](examples/java_jsonc.rb) · [Bash](examples/java_bash.rb) · [TOML](examples/java_toml.rb) |\n\n#### Language-Specific Backends (Native Parser Integration)\n\n| Backend          | Description                 | Performance | Portability | Examples                                                                                                     |\n|------------------|-----------------------------|-------------|-------------|--------------------------------------------------------------------------------------------------------------|\n| **Prism**        | Ruby's official parser      | ⚡ Very Fast | ✅ Universal | [Ruby](examples/prism_ruby.rb)                                                                               |\n| **Psych**        | Ruby's YAML parser (stdlib) | ⚡ Very Fast | ✅ Universal | [YAML](examples/psych_yaml.rb)                                                                               |\n| **Commonmarker** | Markdown via comrak (Rust)  | ⚡ Very Fast | ✅ Good      | [Markdown](examples/commonmarker_markdown.rb) · [commonmarker-merge](examples/commonmarker_merge_example.rb) |\n| **Markly**       | GFM via cmark-gfm (C)       | ⚡ Very Fast | ✅ Good      | [Markdown](examples/markly_markdown.rb) · [Merge](examples/markly_merge_example.rb)                          |\n| **Citrus**       | Pure Ruby parsing           | 🟡 Slower   | ✅ Universal | [TOML](examples/citrus_toml.rb) · [Finitio](examples/citrus_finitio.rb) · [Dhall](examples/citrus_dhall.rb)  |\n| **Parslet**      | Pure Ruby parsing           | 🟡 Slower   | ✅ Universal | [TOML](examples/parslet_toml.rb)                                                                             |\n\n**Selection Priority (Auto mode):** MRI → Rust → FFI → Java → Prism → Psych → Commonmarker → Markly → Citrus → Parslet\n\n**Known Issues:**\n\n- \\*MRI + Bash: ABI incompatibility (use FFI instead)\n- \\*Rust + Bash: Version mismatch (use FFI instead)\n  **Backend Requirements:**\n\n```ruby\n# Tree-sitter backends\ngem \"ruby_tree_sitter\", \"~\u003e 2.0\"  # MRI backend\ngem \"tree_stump\"                   # Rust backend\ngem \"ffi\", \"\u003e= 1.15\", \"\u003c 2.0\"     # FFI backend\n# Java backend: no gem required (uses JRuby's built-in JNI)\n\n# Language-specific backends\ngem \"prism\", \"~\u003e 1.0\"              # Ruby parsing (stdlib in Ruby 3.4+)\n# Psych: no gem required (Ruby stdlib)\ngem \"commonmarker\", \"\u003e= 0.23\"      # Markdown parsing (comrak)\ngem \"markly\", \"~\u003e 0.11\"            # GFM parsing (cmark-gfm)\n\n# Pure Ruby fallbacks\ngem \"citrus\", \"~\u003e 3.0\"             # Citrus backend\ngem \"parslet\", \"~\u003e 2.0\"            # Parslet backend\n# Plus grammar gems: toml-rb (citrus), toml (parslet), dhall, finitio, etc.\n```\n\n**Force Specific Backend:**\n\n```ruby\n# Tree-sitter backends\nTreeHaver.backend = :mri    # Force MRI backend (ruby_tree_sitter)\nTreeHaver.backend = :rust   # Force Rust backend (tree_stump)\nTreeHaver.backend = :ffi    # Force FFI backend\nTreeHaver.backend = :java   # Force Java backend (JRuby only)\n\n# Language-specific backends\nTreeHaver.backend = :prism        # Force Prism (Ruby parsing)\nTreeHaver.backend = :psych        # Force Psych (YAML parsing)\nTreeHaver.backend = :commonmarker # Force Commonmarker (Markdown)\nTreeHaver.backend = :markly       # Force Markly (GFM Markdown)\nTreeHaver.backend = :citrus       # Force Citrus (Pure Ruby PEG)\nTreeHaver.backend = :parslet      # Force Parslet (Pure Ruby PEG)\n\n# Auto-selection (default)\nTreeHaver.backend = :auto   # Let TreeHaver choose\n```\n\n**Block-based Backend Switching:**\n\nUse `with_backend` to temporarily switch backends for a specific block of code.\nThis is thread-safe and supports nesting—the previous backend is automatically\nrestored when the block exits (even if an exception is raised).\n\n```ruby\n# Temporarily use a specific backend\nTreeHaver.with_backend(:mri) do\n  parser = TreeHaver::Parser.new\n  tree = parser.parse(source)\n  # All operations in this block use the MRI backend\nend\n# Backend is restored to its previous value here\n\n# Nested blocks work correctly\nTreeHaver.with_backend(:rust) do\n  # Uses :rust\n  TreeHaver.with_backend(:citrus) do\n    # Uses :citrus\n    parser = TreeHaver::Parser.new\n  end\n  # Back to :rust\n  TreeHaver.with_backend(:parslet) do\n    # Uses :parslet\n    parser = TreeHaver::Parser.new\n  end\n  # Back to :rust\nend\n# Back to original backend\n```\n\nThis is particularly useful for:\n\n- **Testing**: Test the same code with different backends\n- **Performance comparison**: Benchmark different backends\n- **Fallback scenarios**: Try one backend, fall back to another\n- **Thread isolation**: Each thread can use a different backend safely\n\n```ruby\n# Example: Testing with multiple backends\n[:mri, :rust, :citrus, :parslet].each do |backend_name|\n  TreeHaver.with_backend(backend_name) do\n    parser = TreeHaver::Parser.new\n    result = parser.parse(source)\n    puts \"#{backend_name}: #{result.root_node.type}\"\n  end\nend\n```\n\n**Check Backend Capabilities:**\n\n```ruby\nTreeHaver.backend              # =\u003e :ffi\nTreeHaver.backend_module       # =\u003e TreeHaver::Backends::FFI\nTreeHaver.capabilities         # =\u003e { backend: :ffi, parse: true, query: false, ... }\n```\n\nSee [examples/](examples/) directory for **26 complete working examples** demonstrating all 10 backends with multiple languages (JSON, JSONC, Bash, TOML, Ruby, YAML, Markdown) plus markdown-merge integration examples.\n\n### Security Considerations\n\n**⚠️ Loading shared libraries (.so/.dylib/.dll) executes arbitrary native code.**\n\nTreeHaver provides defense-in-depth validations, but you should understand the risks:\n\n#### Attack Vectors Mitigated\n\nTreeHaver's `PathValidator` module protects against:\n\n- **Path traversal**: Paths containing `/../` or `/./` are rejected\n- **Null byte injection**: Paths containing null bytes are rejected\n- **Non-absolute paths**: Relative paths are rejected to prevent CWD-based attacks\n- **Invalid extensions**: Only `.so`, `.dylib`, and `.dll` files are accepted\n- **Malicious filenames**: Filenames must match a safe pattern (alphanumeric, hyphens, underscores)\n- **Invalid language names**: Language names must be lowercase alphanumeric with underscores\n- **Invalid symbol names**: Symbol names must be valid C identifiers\n\n#### Secure Usage\n\n```ruby\n# Standard usage - paths from ENV are validated\nfinder = TreeHaver::GrammarFinder.new(:toml)\npath = finder.find_library_path  # Validates ENV path before returning\n\n# Maximum security - only trusted system directories\npath = finder.find_library_path_safe  # Ignores ENV, only /usr/lib etc.\n\n# Manual validation\nif TreeHaver::PathValidator.safe_library_path?(user_provided_path)\n  language = TreeHaver::Language.from_library(user_provided_path)\nend\n\n# Get validation errors for debugging\nerrors = TreeHaver::PathValidator.validation_errors(path)\n# =\u003e [\"Path is not absolute\", \"Path contains traversal sequence\"]\n```\n\n#### Trusted Directories\n\nThe `find_library_path_safe` method only returns paths in trusted directories.\n\n**Default trusted directories:**\n\n- `/usr/lib`, `/usr/lib64`\n- `/usr/lib/x86_64-linux-gnu`, `/usr/lib/aarch64-linux-gnu`\n- `/usr/local/lib`\n- `/opt/homebrew/lib`, `/opt/local/lib`\n  **Adding custom trusted directories:**\n  For non-standard installations (Homebrew on Linux, luarocks, mise, asdf, etc.), register additional trusted directories:\n\n```ruby\n# Programmatically at application startup\nTreeHaver::PathValidator.add_trusted_directory(\"/home/linuxbrew/.linuxbrew/Cellar\")\nTreeHaver::PathValidator.add_trusted_directory(\"~/.local/share/mise/installs/lua\")\n\n# Or via environment variable (comma-separated, in your shell profile)\nexport TREE_HAVER_TRUSTED_DIRS = \"/home/linuxbrew/.linuxbrew/Cellar,~/.local/share/mise/installs/lua\"\n```\n\n**Example: Fedora Silverblue with Homebrew and luarocks**\n\n```bash\n# In ~/.bashrc or ~/.zshrc\nexport TREE_HAVER_TRUSTED_DIRS=\"/home/linuxbrew/.linuxbrew/Cellar,~/.local/share/mise/installs/lua\"\n\n# tree-sitter runtime library\nexport TREE_SITTER_RUNTIME_LIB=/home/linuxbrew/.linuxbrew/Cellar/tree-sitter/0.26.3/lib/libtree-sitter.so\n\n# Language grammar (luarocks-installed)\nexport 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\n```\n\n#### Recommendations\n\n1.  **Production**: Consider using `find_library_path_safe` to ignore ENV overrides\n2.  **Development**: Standard `find_library_path` is convenient for testing\n3.  **User Input**: Always validate paths before passing to `Language.from_library`\n4.  **CI/CD**: Be cautious of ENV vars that could be set by untrusted sources\n5.  **Custom installs**: Register trusted directories via `TREE_HAVER_TRUSTED_DIRS` or `add_trusted_directory`\n\n### Backend Selection\n\nTreeHaver automatically selects the best backend for your Ruby implementation, but you can override this behavior:\n\n```ruby\n# Automatic backend selection (default)\nTreeHaver.backend = :auto\n\n# Force a specific backend\nTreeHaver.backend = :mri     # Use ruby_tree_sitter (MRI only, C extension)\nTreeHaver.backend = :rust    # Use tree_stump (MRI, Rust extension with precompiled binaries)\n                             # Note: `tree_stump` currently requires unreleased fixes in the `main` branch.\n                             # See: https://github.com/joker1007/tree_stump\nTreeHaver.backend = :ffi     # Use FFI bindings (works on MRI and JRuby)\nTreeHaver.backend = :java    # Use Java bindings (JRuby only, coming soon)\nTreeHaver.backend = :citrus  # Use Citrus pure Ruby parser\n                             # NOTE: Portable, all Ruby implementations\n                             # CAVEAT: few major language grammars, but many esoteric grammars\nTreeHaver.backend = :parslet # Use Parslet pure Ruby parser\n                             # NOTE: Portable, all Ruby implementations\n                             # CAVEAT: few major language grammars, but many esoteric grammars\n```\n\n**Auto-selection priority on MRI:** MRI → Rust → FFI → Citrus → Parslet\n\nYou can also set the backend via environment variable:\n\n```bash\nexport TREE_HAVER_BACKEND=rust\n```\n\n### Backend Registry\n\nTreeHaver provides a `BackendRegistry` module that allows external gems to register their backend availability checkers. This enables dynamic backend detection without hardcoding dependencies.\n\n#### Registering a Backend Availability Checker\n\nExternal gems (like `commonmarker-merge`, `markly-merge`, `rbs-merge`) can register their availability checker when loaded:\n\n```ruby\n# In your gem's backend module\nTreeHaver::BackendRegistry.register_availability_checker(:my_backend) do\n  # Return true if backend is available\n  require \"my_backend_gem\"\n  true\nrescue LoadError\n  false\nend\n```\n\n#### Checking Backend Availability\n\n```ruby\n# Check if a backend is available\nTreeHaver::BackendRegistry.available?(:commonmarker)  # =\u003e true/false\nTreeHaver::BackendRegistry.available?(:markly)        # =\u003e true/false\nTreeHaver::BackendRegistry.available?(:rbs)           # =\u003e true/false\n\n# Check if a checker is registered\nTreeHaver::BackendRegistry.registered?(:my_backend)   # =\u003e true/false\n\n# Get all registered backend names\nTreeHaver::BackendRegistry.registered_backends        # =\u003e [:mri, :rust, :ffi, ...]\n```\n\n#### How It Works\n\n1. Built-in backends (MRI, Rust, FFI, Java, Prism, Psych, Citrus, Parslet) automatically register their checkers when loaded\n2. External gems register their checkers when their backend module is loaded\n3. `TreeHaver::RSpec::DependencyTags` uses the registry to dynamically detect available backends\n4. Results are cached for performance (use `clear_cache!` to reset)\n\n#### RSpec Integration\n\nThe `BackendRegistry` is used by `TreeHaver::RSpec::DependencyTags` to configure RSpec exclusion filters:\n\n```ruby\n# In your spec_helper.rb\nrequire \"tree_haver/rspec/dependency_tags\"\n\n# Then in specs, use tags to skip tests when backends aren't available\nit \"requires commonmarker\", :commonmarker_backend do\n  # This test only runs when commonmarker is available\nend\n\nit \"requires markly\", :markly_backend do\n  # This test only runs when markly is available\nend\n```\n\n### Environment Variables\n\nTreeHaver recognizes several environment variables for configuration:\n\n**Note**: All path-based environment variables are validated before use. Invalid paths are ignored.\n\n#### Security Configuration\n\n- **`TREE_HAVER_TRUSTED_DIRS`**: Comma-separated list of additional trusted directories for grammar libraries\n\n  ```bash\n  # For Homebrew on Linux and luarocks\n  export TREE_HAVER_TRUSTED_DIRS=\"/home/linuxbrew/.linuxbrew/Cellar,~/.local/share/mise/installs/lua\"\n  ```\n\n  Tilde (`~`) is expanded to the user's home directory. Directories listed here are considered safe for `find_library_path_safe`.\n\n#### Core Runtime Library\n\n- **`TREE_SITTER_RUNTIME_LIB`**: Absolute path to the core `libtree-sitter` shared library\n  ```bash\n  export TREE_SITTER_RUNTIME_LIB=/usr/local/lib/libtree-sitter.so\n  ```\n\nIf not set, TreeHaver tries these names in order:\n\n- `tree-sitter`\n- `libtree-sitter.so.0`\n- `libtree-sitter.so`\n- `libtree-sitter.dylib`\n- `libtree-sitter.dll`\n\n#### Language Symbol Resolution\n\nWhen loading a language grammar, if you don't specify the `symbol:` parameter, TreeHaver resolves it in this precedence:\n\n1.  **`TREE_SITTER_LANG_SYMBOL`**: Explicit symbol override\n2.  Guessed from filename (e.g., `libtree-sitter-toml.so` → `tree_sitter_toml`)\n3.  Default fallback (`tree_sitter_toml`)\n\n```bash\nexport TREE_SITTER_LANG_SYMBOL=tree_sitter_toml\n```\n\n#### Language Library Paths\n\nFor specific languages, you can set environment variables to point to grammar libraries:\n\n```bash\nexport TREE_SITTER_TOML_PATH=/usr/local/lib/libtree-sitter-toml.so\nexport TREE_SITTER_JSON_PATH=/usr/local/lib/libtree-sitter-json.so\n```\n\n#### JRuby-Specific: Java Backend Configuration\n\nFor the Java backend on JRuby, you need:\n\n1.  **jtreesitter \\\u003e= 0.26.0** JAR from Maven Central\n2.  **Tree-sitter runtime library** (`libtree-sitter.so`) version 0.26+\n3.  **Grammar `.so` files** built against tree-sitter 0.26+\n\n```bash\n# Download jtreesitter JAR (or use bin/setup-jtreesitter)\nexport TREE_SITTER_JAVA_JARS_DIR=/path/to/java-tree-sitter/jars\n\n# Point to tree-sitter runtime (must be 0.26+)\nexport TREE_SITTER_RUNTIME_LIB=/usr/local/lib/libtree-sitter.so\n\n# Point to grammar libraries (must be built for tree-sitter 0.26+)\nexport TREE_SITTER_TOML_PATH=/path/to/libtree-sitter-toml.so\n```\n\n**Building grammars for Java backend:**\n\nIf you get \"version mismatch\" errors, rebuild the grammar:\n\n```bash\n# Use the provided build script\nbin/build-grammar toml\n\n# This regenerates parser.c for your tree-sitter version and compiles it\n```\n\nFor more see [docs](https://tree-sitter.github.io/java-tree-sitter/), [maven][jtreesitter], and [source](https://github.com/tree-sitter/java-tree-sitter).\n\n### Language Registration\n\nRegister languages once at application startup for convenient access:\n\n```ruby\n# Register a TOML grammar\nTreeHaver.register_language(\n  :toml,\n  path: \"/usr/local/lib/libtree-sitter-toml.so\",\n  symbol: \"tree_sitter_toml\",  # optional, will be inferred if omitted\n)\n\n# Now you can use the convenient helper\nlanguage = TreeHaver::Language.toml\n\n# Or still override path/symbol per-call\nlanguage = TreeHaver::Language.toml(\n  path: \"/custom/path/libtree-sitter-toml.so\",\n)\n```\n\n### Grammar Discovery with GrammarFinder\n\nFor libraries that need to automatically locate tree-sitter grammars (like the `*-merge` family of gems), TreeHaver provides the `GrammarFinder` utility class. It handles platform-aware grammar discovery without requiring language-specific code in TreeHaver itself.\n\n```ruby\n# Create a finder for any language\nfinder = TreeHaver::GrammarFinder.new(:toml)\n\n# Check if the grammar is available\nif finder.available?\n  puts \"TOML grammar found at: #{finder.find_library_path}\"\nelse\n  puts finder.not_found_message\n  # =\u003e \"tree-sitter toml grammar not found. Searched: /usr/lib/libtree-sitter-toml.so, ...\"\nend\n\n# Register the language if available\nfinder.register! if finder.available?\n\n# Now use the registered language\nlanguage = TreeHaver::Language.toml\n```\n\n#### GrammarFinder Automatic Derivation\n\nGiven just the language name, `GrammarFinder` automatically derives:\n\n| Property         | Derived Value (for `:toml`)                          |\n|------------------|------------------------------------------------------|\n| ENV var          | `TREE_SITTER_TOML_PATH`                              |\n| Library filename | `libtree-sitter-toml.so` (Linux) or `.dylib` (macOS) |\n| Symbol name      | `tree_sitter_toml`                                   |\n\n#### Search Order\n\n`GrammarFinder` searches for grammars in this order:\n\n1.  **Environment variable**: `TREE_SITTER_\u003cLANG\u003e_PATH` (highest priority)\n2.  **Extra paths**: Custom paths provided at initialization\n3.  **System paths**: Common installation directories (`/usr/lib`, `/usr/local/lib`, `/opt/homebrew/lib`, etc.)\n\n#### Usage in \\*-merge Gems\n\nThe `GrammarFinder` pattern enables clean integration in language-specific merge gems:\n\n```ruby\n# In toml-merge\nfinder = TreeHaver::GrammarFinder.new(:toml)\nfinder.register! if finder.available?\n\n# In json-merge\nfinder = TreeHaver::GrammarFinder.new(:json)\nfinder.register! if finder.available?\n\n# In bash-merge\nfinder = TreeHaver::GrammarFinder.new(:bash)\nfinder.register! if finder.available?\n```\n\nEach gem uses the same API—only the language name changes.\n\n#### Adding Custom Search Paths\n\nFor non-standard installations, provide extra search paths:\n\n```ruby\nfinder = TreeHaver::GrammarFinder.new(:toml, extra_paths: [\n  \"/opt/custom/lib\",\n  \"/home/user/.local/lib\",\n])\n```\n\n#### Debug Information\n\nGet detailed information about the grammar search:\n\n```ruby\nfinder = TreeHaver::GrammarFinder.new(:toml)\nputs finder.search_info\n# =\u003e {\n#      language: :toml,\n#      env_var: \"TREE_SITTER_TOML_PATH\",\n#      env_value: nil,\n#      symbol: \"tree_sitter_toml\",\n#      library_filename: \"libtree-sitter-toml.so\",\n#      search_paths: [\"/usr/lib/libtree-sitter-toml.so\", ...],\n#      found_path: \"/usr/lib/libtree-sitter-toml.so\",\n#      available: true\n#    }\n```\n\n### Checking Capabilities\n\nDifferent backends may support different features:\n\n```ruby\nTreeHaver.capabilities\n# =\u003e { backend: :mri, query: true, bytes_field: true }\n# or\n# =\u003e { backend: :ffi, parse: true, query: false, bytes_field: true }\n# or\n# =\u003e { backend: :citrus, parse: true, query: false, bytes_field: false }\n# or\n# =\u003e { backend: :parslet, parse: true, query: false, bytes_field: false }\n```\n\n### Compatibility Mode\n\nFor codebases migrating from `ruby_tree_sitter`, TreeHaver provides a compatibility shim:\n\n```ruby\nrequire \"tree_haver/compat\"\n\n# Now TreeSitter constants map to TreeHaver\nparser = TreeSitter::Parser.new  # Actually creates TreeHaver::Parser\n```\n\nThis is safe and idempotent—if the real `TreeSitter` module is already loaded, the shim does nothing.\n\n#### ⚠️ Important: Exception Hierarchy\n\n**Both ruby\\_tree\\_sitter v2+ and TreeHaver exceptions inherit from `Exception` (not `StandardError`).**\n\nThis design decision follows ruby\\_tree\\_sitter's lead for thread-safety and signal handling reasons. See [ruby\\_tree\\_sitter PR \\#83](https://github.com/Faveod/ruby-tree-sitter/pull/83) for the rationale.\n\n**What this means for exception handling:**\n\n```ruby\n# ⚠️ This will NOT catch TreeHaver errors\nbegin\n  TreeHaver::Language.from_library(\"/nonexistent.so\")\nrescue =\u003e e\n  puts \"Caught!\"  # Never reached - TreeHaver::Error inherits Exception\nend\n\n# ✅ Explicit rescue is required\nbegin\n  TreeHaver::Language.from_library(\"/nonexistent.so\")\nrescue TreeHaver::Error =\u003e e\n  puts \"Caught!\"  # This works\nend\n\n# ✅ Or rescue specific exceptions\nbegin\n  TreeHaver::Language.from_library(\"/nonexistent.so\")\nrescue TreeHaver::NotAvailable =\u003e e\n  puts \"Grammar not available: #{e.message}\"\nend\n```\n\n**TreeHaver Exception Hierarchy:**\n\n    Exception\n    └── TreeHaver::Error              # Base error class\n        ├── TreeHaver::NotAvailable   # Backend/grammar not available\n        └── TreeHaver::BackendConflict # Backend incompatibility detected\n\n**Compatibility Mode Behavior:**\n\nThe compat mode (`require \"tree_haver/compat\"`) creates aliases but **does not change the exception hierarchy**:\n\n```ruby\nrequire \"tree_haver/compat\"\n\n# TreeSitter constants are now aliases to TreeHaver\nTreeSitter::Error       # =\u003e TreeHaver::Error (still inherits Exception)\nTreeSitter::Parser      # =\u003e TreeHaver::Parser\nTreeSitter::Language    # =\u003e TreeHaver::Language\n\n# Exception handling remains the same\nbegin\n  TreeSitter::Language.load(\"missing\", \"/nonexistent.so\")\nrescue TreeSitter::Error =\u003e e  # Still requires explicit rescue\n  puts \"Error: #{e.message}\"\nend\n```\n\n**Best Practices:**\n\n1.  **Always use explicit rescue** for TreeHaver errors:\n\n    ```ruby\n    begin\n      finder = TreeHaver::GrammarFinder.new(:toml)\n      finder.register! if finder.available?\n      language = TreeHaver::Language.toml\n    rescue TreeHaver::NotAvailable =\u003e e\n      warn(\"TOML grammar not available: #{e.message}\")\n      # Fallback to another backend or fail gracefully\n    end\n    ```\n\n2.  **Never rely on `rescue =\u003e e`** to catch TreeHaver errors (it won't work)\n    **Why inherit from Exception?**\n    Following ruby\\_tree\\_sitter's reasoning:\n\n- **Thread safety**: Prevents accidental catching in thread cleanup code\n- **Signal handling**: Ensures parsing errors don't interfere with SIGTERM/SIGINT\n- **Intentional handling**: Forces developers to explicitly handle parsing errors\n  See `lib/tree_haver/compat.rb` for compatibility layer documentation.\n\n## 🔧 Basic Usage\n\n### Quick Start\n\nThe simplest way to parse code is with `TreeHaver.parser_for`, which handles all the complexity of language loading, grammar discovery, and backend selection:\n\n```ruby\nrequire \"tree_haver\"\n\n# Parse TOML - auto-discovers grammar and falls back to Citrus if needed\nparser = TreeHaver.parser_for(:toml)\ntree = parser.parse(\"[package]\\nname = \\\"my-app\\\"\")\n\n# Parse JSON\nparser = TreeHaver.parser_for(:json)\ntree = parser.parse('{\"key\": \"value\"}')\n\n# Parse Bash\nparser = TreeHaver.parser_for(:bash)\ntree = parser.parse(\"#!/bin/bash\\necho hello\")\n\n# With explicit library path\nparser = TreeHaver.parser_for(:toml, library_path: \"/custom/path/libtree-sitter-toml.so\")\n\n# With Citrus fallback configuration\nparser = TreeHaver.parser_for(\n  :toml,\n  citrus_config: {gem_name: \"toml-rb\", grammar_const: \"TomlRB::Document\"},\n)\n```\n\n`TreeHaver.parser_for` handles:\n\n1.  Checking if the language is already registered\n2.  Auto-discovering tree-sitter grammar via `GrammarFinder`\n3.  Falling back to Citrus grammar if tree-sitter is unavailable\n4.  Creating and configuring the parser\n5.  Raising `NotAvailable` with a helpful message if nothing works\n\n### Manual Parser Setup\n\nFor more control, you can create parsers manually:\n\nTreeHaver works with any language through its 10 backends. Here are examples for different parsing needs:\n\n#### Parsing with Tree-sitter (Universal Languages)\n\n```ruby\nrequire \"tree_haver\"\n\n# Load a tree-sitter grammar (works with MRI, Rust, FFI, or Java backend)\nlanguage = TreeHaver::Language.from_library(\n  \"/usr/local/lib/libtree-sitter-toml.so\",\n  symbol: \"tree_sitter_toml\",\n)\n\n# Create a parser\nparser = TreeHaver::Parser.new\nparser.language = language\n\n# Parse source code\nsource = \u003c\u003c~TOML\n  [package]\n  name = \"my-app\"\n  version = \"1.0.0\"\nTOML\n\ntree = parser.parse(source)\n\n# Access the unified Position API (works across all backends)\nroot = tree.root_node\nputs \"Root type: #{root.type}\"              # =\u003e \"document\"\nputs \"Start line: #{root.start_line}\"       # =\u003e 1 (1-based)\nputs \"End line: #{root.end_line}\"           # =\u003e 3\nputs \"Position: #{root.source_position}\"    # =\u003e {start_line: 1, end_line: 3, ...}\n\n# Traverse the tree\nroot.each do |child|\n  puts \"Child: #{child.type} at line #{child.start_line}\"\nend\n```\n\n#### Parsing Ruby with Prism\n\n```ruby\nrequire \"tree_haver\"\n\nTreeHaver.backend = :prism\nparser = TreeHaver::Parser.new\nparser.language = TreeHaver::Backends::Prism::Language.ruby\n\nsource = \u003c\u003c~RUBY\n  class Example\n    def hello\n      puts \"Hello, world!\"\n    end\n  end\nRUBY\n\ntree = parser.parse(source)\nroot = tree.root_node\n\n# Find all method definitions\ndef find_methods(node, results = [])\n  results \u003c\u003c node if node.type == \"def_node\"\n  node.children.each { |child| find_methods(child, results) }\n  results\nend\n\nmethods = find_methods(root)\nmethods.each do |method_node|\n  pos = method_node.source_position\n  puts \"Method at lines #{pos[:start_line]}-#{pos[:end_line]}\"\nend\n```\n\n#### Parsing YAML with Psych\n\n```ruby\nrequire \"tree_haver\"\n\nTreeHaver.backend = :psych\nparser = TreeHaver::Parser.new\nparser.language = TreeHaver::Backends::Psych::Language.yaml\n\nsource = \u003c\u003c~YAML\n  database:\n    host: localhost\n    port: 5432\nYAML\n\ntree = parser.parse(source)\nroot = tree.root_node\n\n# Navigate YAML structure\ndef show_structure(node, indent = 0)\n  prefix = \"  \" * indent\n  puts \"#{prefix}#{node.type} (line #{node.start_line})\"\n  node.children.each { |child| show_structure(child, indent + 1) }\nend\n\nshow_structure(root)\n```\n\n#### Parsing Markdown with Commonmarker or Markly\n\n```ruby\nrequire \"tree_haver\"\n\n# Choose your backend\nTreeHaver.backend = :commonmarker  # or :markly for GFM\n\nparser = TreeHaver::Parser.new\nparser.language = TreeHaver::Backends::Commonmarker::Language.markdown\n\nsource = \u003c\u003c~MARKDOWN\n  # My Document\n\n  ## Section\n\n  - Item 1\n  - Item 2\nMARKDOWN\n\ntree = parser.parse(source)\nroot = tree.root_node\n\n# Find all headings\ndef find_headings(node, results = [])\n  results \u003c\u003c node if node.type == \"heading\"\n  node.children.each { |child| find_headings(child, results) }\n  results\nend\n\nheadings = find_headings(root)\nheadings.each do |heading|\n  level = heading.header_level\n  text = heading.children.map(\u0026:text).join\n  puts \"H#{level}: #{text} (line #{heading.start_line})\"\nend\n```\n\n### Using Language Registration\n\nFor cleaner code, register languages at startup:\n\n```ruby\n# At application initialization\nTreeHaver.register_language(\n  :toml,\n  path: \"/usr/local/lib/libtree-sitter-toml.so\",\n)\n\nTreeHaver.register_language(\n  :json,\n  path: \"/usr/local/lib/libtree-sitter-json.so\",\n)\n\n# Later in your code\ntoml_language = TreeHaver::Language.toml\njson_language = TreeHaver::Language.json\n\nparser = TreeHaver::Parser.new\nparser.language = toml_language\ntree = parser.parse(toml_source)\n```\n\n#### Flexible Language Names\n\nThe `name` parameter in `register_language` is an arbitrary identifier you choose—it doesn't\nneed to match the actual language name. The actual grammar identity comes from the `path`\nand `symbol` parameters (for tree-sitter) or `grammar_module` (for Citrus/Parslet).\n\nThis flexibility is useful for:\n\n- **Aliasing**: Register the same grammar under multiple names\n- **Versioning**: Register different grammar versions (e.g., `:ruby_2`, `:ruby_3`)\n- **Testing**: Use unique names to avoid collisions between tests\n- **Context-specific naming**: Use names that make sense for your application\n\n```ruby\n# Register the same TOML grammar under different names for different purposes\nTreeHaver.register_language(\n  :config_parser,  # Custom name for your app\n  path: \"/usr/local/lib/libtree-sitter-toml.so\",\n  symbol: \"tree_sitter_toml\",\n)\n\nTreeHaver.register_language(\n  :toml_v1,  # Version-specific name\n  path: \"/usr/local/lib/libtree-sitter-toml.so\",\n  symbol: \"tree_sitter_toml\",\n)\n\n# Use your custom names\nconfig_lang = TreeHaver::Language.config_parser\nversioned_lang = TreeHaver::Language.toml_v1\n```\n\n### Parsing Different Languages\n\nTreeHaver works with any tree-sitter grammar:\n\n```ruby\n# Parse Ruby code\nruby_lang = TreeHaver::Language.from_library(\n  \"/path/to/libtree-sitter-ruby.so\",\n)\nparser = TreeHaver::Parser.new\nparser.language = ruby_lang\ntree = parser.parse(\"class Foo; end\")\n\n# Parse JavaScript\njs_lang = TreeHaver::Language.from_library(\n  \"/path/to/libtree-sitter-javascript.so\",\n)\nparser.language = js_lang  # Reuse the same parser\ntree = parser.parse(\"const x = 42;\")\n```\n\n### Walking the AST\n\nTreeHaver provides simple node traversal:\n\n```ruby\ntree = parser.parse(source)\nroot = tree.root_node\n\n# Recursive tree walk\ndef walk_tree(node, depth = 0)\n  puts \"#{\"  \" * depth}#{node.type}\"\n  node.each { |child| walk_tree(child, depth + 1) }\nend\n\nwalk_tree(root)\n```\n\n### Incremental Parsing\n\nTreeHaver 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.\n\n```ruby\n# Check if current backend supports incremental parsing\nif TreeHaver.capabilities[:incremental]\n  puts \"Incremental parsing is available!\"\nend\n\n# Initial parse\nparser = TreeHaver::Parser.new\nparser.language = language\ntree = parser.parse_string(nil, \"x = 1\")\n\n# User edits the source: \"x = 1\" -\u003e \"x = 42\"\n# Mark the tree as edited (tell tree-sitter what changed)\ntree.edit(\n  start_byte: 4,           # edit starts at byte 4\n  old_end_byte: 5,         # old text \"1\" ended at byte 5\n  new_end_byte: 6,         # new text \"42\" ends at byte 6\n  start_point: {row: 0, column: 4},\n  old_end_point: {row: 0, column: 5},\n  new_end_point: {row: 0, column: 6},\n)\n\n# Re-parse incrementally - tree-sitter reuses unchanged nodes\nnew_tree = parser.parse_string(tree, \"x = 42\")\n```\n\n**Note:** Incremental parsing requires the MRI (`ruby_tree_sitter`), Rust (`tree_stump`), or Java (`java-tree-sitter` / `jtreesitter`) backend. The FFI, Citrus, and Parslet backends do not currently support incremental parsing. You can check support with:\n\n**Note:** `tree_stump` currently requires unreleased fixes in the `main` branch.\n\n```ruby\ntree.supports_editing?  # =\u003e true if edit() is available\n```\n\n### Error Handling\n\n```ruby\nbegin\n  language = TreeHaver::Language.from_library(\"/path/to/grammar.so\")\nrescue TreeHaver::NotAvailable =\u003e e\n  puts \"Failed to load grammar: #{e.message}\"\nend\n\n# Check if a backend is available\nif TreeHaver.backend_module.nil?\n  puts \"No TreeHaver backend is available!\"\n  puts \"Install ruby_tree_sitter (MRI), ffi gem with libtree-sitter, citrus gem, or parslet gem\"\nend\n```\n\n### Platform-Specific Examples\n\n#### MRI Ruby\n\nOn MRI, TreeHaver uses `ruby_tree_sitter` by default:\n\n```ruby\n# Gemfile\ngem \"tree_haver\"\ngem \"ruby_tree_sitter\"  # MRI backend\n\n# Code - no changes needed, TreeHaver auto-selects MRI backend\nparser = TreeHaver::Parser.new\n```\n\n#### JRuby\n\nOn JRuby, TreeHaver can use the FFI backend, Java backend, Citrus backend, or Parslet backend:\n\n##### Option 1: FFI Backend (recommended for tree-sitter grammars)\n\n```ruby\n# Gemfile\ngem \"tree_haver\"\ngem \"ffi\"  # Required for FFI backend\n\n# Ensure libtree-sitter is installed on your system\n# On macOS with Homebrew:\n#   brew install tree-sitter\n\n# On Ubuntu/Debian:\n#   sudo apt-get install libtree-sitter0 libtree-sitter-dev\n\n# Code - TreeHaver auto-selects FFI backend on JRuby\nparser = TreeHaver::Parser.new\n```\n\n##### Option 2: Java Backend (native JVM performance)\n\n```bash\n# 1. Download java-tree-sitter JAR from Maven Central\nmkdir -p vendor/jars\ncurl -fSL -o vendor/jars/jtreesitter-0.23.2.jar \\\n  \"https://repo1.maven.org/maven2/io/github/tree-sitter/jtreesitter/0.23.2/jtreesitter-0.23.2.jar\"\n\n# 2. Set environment variables\nexport CLASSPATH=\"$(pwd)/vendor/jars:$CLASSPATH\"\nexport LD_LIBRARY_PATH=\"/path/to/libtree-sitter/lib:$LD_LIBRARY_PATH\"\n\n# 3. Run with JRuby (requires Java 22+ for Foreign Function API)\nJAVA_OPTS=\"--enable-native-access=ALL-UNNAMED\" jruby your_script.rb\n```\n\n```ruby\n# Force Java backend\nTreeHaver.backend = :java\n\n# Check if Java backend is available\nif TreeHaver::Backends::Java.available?\n  puts \"Java backend is ready!\"\n  puts TreeHaver.capabilities\n  # =\u003e { backend: :java, parse: true, query: true, bytes_field: true, incremental: true }\nend\n```\n\n**⚠️ Java Backend Limitation: Symbol Resolution**\n\nThe Java backend uses Java's Foreign Function \u0026 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.\n\nThis 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.\n\n**Recommended approach for JRuby:** Use the **FFI backend**:\n\n```ruby\n# On JRuby, use FFI backend (recommended)\nTreeHaver.backend = :ffi\n```\n\nThe 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.\n\nThe Java backend will work with:\n\n- 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))\n- Grammar `.so` files that statically link tree-sitter\n\n##### Option 3: Citrus Backend (pure Ruby, portable)\n\n```ruby\n# Gemfile\ngem \"tree_haver\"\ngem \"citrus\"  # Pure Ruby parser, zero native dependencies\n\n# Code - Force Citrus backend for maximum portability\nTreeHaver.backend = :citrus\n\n# Check if Citrus backend is available\nif TreeHaver::Backends::Citrus.available?\n  puts \"Citrus backend is ready!\"\n  puts TreeHaver.capabilities\n  # =\u003e { backend: :citrus, parse: true, query: false, bytes_field: false }\nend\n```\n\n**⚠️ Citrus Backend Limitations:**\n\n- Uses Citrus grammars (not tree-sitter grammars)\n- No incremental parsing support\n- No query API\n- Pure Ruby performance (slower than native backends)\n- Best for: prototyping, environments without native extension support, teaching\n\n##### Option 4: Parslet Backend (pure Ruby, portable)\n\n```ruby\n# Gemfile\ngem \"tree_haver\"\ngem \"parslet\"  # Pure Ruby parser, zero native dependencies\n\n# Code - Force Parslet backend for maximum portability\nTreeHaver.backend = :parslet\n\n# Check if Parslet backend is available\nif TreeHaver::Backends::Parslet.available?\n  puts \"Parslet backend is ready!\"\n  puts TreeHaver.capabilities\n  # =\u003e { backend: :parslet, parse: true, query: false, bytes_field: false }\nend\n```\n\n**⚠️ Parslet Backend Limitations:**\n\n- Uses Parslet grammars (not tree-sitter grammars)\n- No incremental parsing support\n- No query API\n- Pure Ruby performance (slower than native backends)\n- Best for: prototyping, environments without native extension support, teaching\n\n#### TruffleRuby\n\nTruffleRuby can use the MRI, FFI, Citrus, or Parslet backend:\n\n```ruby\n# Use FFI backend (recommended for tree-sitter grammars)\nTreeHaver.backend = :ffi\n\n# Or try MRI backend if ruby_tree_sitter compiles on your TruffleRuby version\nTreeHaver.backend = :mri\n\n# Or use Citrus backend for zero native dependencies\nTreeHaver.backend = :citrus\n\n# Or use Parslet backend for zero native dependencies\nTreeHaver.backend = :parslet\n```\n\n### Advanced: Thread-Safe Backend Switching\n\nTreeHaver provides `with_backend` for thread-safe, temporary backend switching. This is\nessential for testing, benchmarking, and applications that need different backends in\ndifferent contexts.\n\n#### Testing with Multiple Backends\n\nTest the same code path with different backends using `with_backend`:\n\n```ruby\n# In your test setup\nRSpec.describe(\"MyParser\") do\n  # Test with each available backend\n  [:mri, :rust, :citrus, :parslet].each do |backend_name|\n    context \"with #{backend_name} backend\" do\n      it \"parses correctly\" do\n        TreeHaver.with_backend(backend_name) do\n          parser = TreeHaver::Parser.new\n          result = parser.parse(\"x = 42\")\n          expect(result.root_node.type).to(eq(\"document\"))\n        end\n        # Backend automatically restored after block\n      end\n    end\n  end\nend\n```\n\n#### Thread Isolation\n\nEach thread can use a different backend safely—`with_backend` uses thread-local storage:\n\n```ruby\nthreads = []\n\nthreads \u003c\u003c Thread.new do\n  TreeHaver.with_backend(:mri) do\n    # This thread uses MRI backend\n    parser = TreeHaver::Parser.new\n    100.times { parser.parse(\"x = 1\") }\n  end\nend\n\nthreads \u003c\u003c Thread.new do\n  TreeHaver.with_backend(:citrus) do\n    # This thread uses Citrus backend simultaneously\n    parser = TreeHaver::Parser.new\n    100.times { parser.parse(\"x = 1\") }\n  end\nend\n\nthreads \u003c\u003c Thread.new do\n  TreeHaver.with_backend(:parslet) do\n    # This thread uses Parslet backend simultaneously\n    parser = TreeHaver::Parser.new\n    100.times { parser.parse(\"x = 1\") }\n  end\nend\n\nthreads.each(\u0026:join)\n```\n\n#### Nested Blocks\n\n`with_backend` supports nesting—inner blocks override outer blocks:\n\n```ruby\nTreeHaver.with_backend(:rust) do\n  puts TreeHaver.effective_backend  # =\u003e :rust\n\n  TreeHaver.with_backend(:citrus) do\n    puts TreeHaver.effective_backend  # =\u003e :citrus\n  end\n\n  TreeHaver.with_backend(:parslet) do\n    puts TreeHaver.effective_backend  # =\u003e :parslet\n  end\n\n  puts TreeHaver.effective_backend  # =\u003e :rust (restored)\nend\n```\n\n#### Fallback Pattern\n\nTry one backend, fall back to another on failure:\n\n```ruby\ndef parse_with_fallback(source)\n  TreeHaver.with_backend(:mri) do\n    TreeHaver::Parser.new.tap { |p| p.language = load_language }.parse(source)\n  end\nrescue TreeHaver::NotAvailable\n  # Fall back to Citrus if MRI backend unavailable\n  TreeHaver.with_backend(:citrus) do\n    TreeHaver::Parser.new.tap { |p| p.language = load_language }.parse(source)\n  end\nrescue TreeHaver::NotAvailable\n  # Fall back to Parslet if Citrus backend unavailable\n  TreeHaver.with_backend(:parslet) do\n    TreeHaver::Parser.new.tap { |p| p.language = load_language }.parse(source)\n  end\nend\n```\n\n### Complete Real-World Example\n\nHere's a practical example that extracts package names from a TOML file:\n\n```ruby\nrequire \"tree_haver\"\n\n# Setup\nTreeHaver.register_language(\n  :toml,\n  path: \"/usr/local/lib/libtree-sitter-toml.so\",\n)\n\ndef extract_package_name(toml_content)\n  # Create parser\n  parser = TreeHaver::Parser.new\n  parser.language = TreeHaver::Language.toml\n\n  # Parse\n  tree = parser.parse(toml_content)\n  root = tree.root_node\n\n  # Find [package] table\n  root.each do |child|\n    next unless child.type == \"table\"\n\n    child.each do |table_elem|\n      if table_elem.type == \"pair\"\n        # Look for name = \"...\" pair\n        key = table_elem.each.first\u0026.type\n        # In a real implementation, you'd extract the text value\n        # This is simplified for demonstration\n      end\n    end\n  end\nend\n\n# Usage\ntoml = \u003c\u003c~TOML\n  [package]\n  name = \"awesome-app\"\n  version = \"2.0.0\"\nTOML\n\npackage_name = extract_package_name(toml)\n```\n\n### 🧪 RSpec Integration\n\nTreeHaver provides shared RSpec helpers for conditional test execution based on dependency availability. This is useful for testing code that uses optional backends.\n\n```ruby\n# In your spec_helper.rb\nrequire \"tree_haver/rspec\"\n```\n\nThis automatically configures RSpec with exclusion filters for all TreeHaver dependencies. Use tags to conditionally run tests:\n\n```ruby\n# Runs only when FFI backend is available\nit \"parses with FFI\", :ffi do\n  # ...\nend\n\n# Runs only when ruby_tree_sitter gem is available\nit \"uses MRI backend\", :mri_backend do\n  # ...\nend\n\n# Runs only when tree-sitter-toml grammar works\nit \"parses TOML\", :tree_sitter_toml do\n  # ...\nend\n\n# Runs only when any markdown backend is available\nit \"parses markdown\", :markdown_backend do\n  # ...\nend\n```\n\n**Available Tags:**\n\nTags follow a naming convention:\n\n- `*_backend` = TreeHaver backends (mri, rust, ffi, java, prism, psych, commonmarker, markly, citrus, parslet, rbs)\n- `*_engine` = Ruby engines (mri, jruby, truffleruby)\n- `*_grammar` = tree-sitter grammar files (.so)\n- `*_parsing` = any parsing capability for a language (combines multiple backends/grammars)\n- `*_gem` = specific library gems\n\n| Tag                     | Description                                                               |\n|-------------------------|---------------------------------------------------------------------------|\n| **Backend Tags**        |                                                                           |\n| `:ffi_backend`          | FFI backend available (dynamic check, legacy alias: `:ffi`)               |\n| `:ffi_backend_only`     | FFI backend in isolation (won't trigger MRI check)                        |\n| `:mri_backend`          | ruby\\_tree\\_sitter gem available                                          |\n| `:mri_backend_only`     | MRI backend in isolation (won't trigger FFI check)                        |\n| `:rust_backend`         | tree\\_stump gem available                                                 |\n| `:java_backend`         | Java backend available (JRuby + jtreesitter)                              |\n| `:prism_backend`        | Prism gem available                                                       |\n| `:psych_backend`        | Psych available (stdlib)                                                  |\n| `:commonmarker_backend` | commonmarker gem available                                                |\n| `:markly_backend`       | markly gem available                                                      |\n| `:citrus_backend`       | Citrus gem available                                                      |\n| `:parslet_backend`      | Parslet gem available                                                     |\n| `:rbs_backend`          | RBS gem available (official RBS parser, MRI only)                         |\n| **Engine Tags**         |                                                                           |\n| `:mri_engine`           | Running on MRI (CRuby)                                                    |\n| `:jruby_engine`         | Running on JRuby                                                          |\n| `:truffleruby_engine`   | Running on TruffleRuby                                                    |\n| **Grammar Tags**        |                                                                           |\n| `:libtree_sitter`       | libtree-sitter.so is loadable via FFI                                     |\n| `:bash_grammar`         | tree-sitter-bash grammar available and parsing works                      |\n| `:toml_grammar`         | tree-sitter-toml grammar available and parsing works                      |\n| `:json_grammar`         | tree-sitter-json grammar available and parsing works                      |\n| `:jsonc_grammar`        | tree-sitter-jsonc grammar available and parsing works                     |\n| `:rbs_grammar`          | tree-sitter-rbs grammar available and parsing works                       |\n| **Parsing Tags**        |                                                                           |\n| `:toml_parsing`         | Any TOML parser available (tree-sitter OR toml-rb/Citrus OR toml/Parslet) |\n| `:markdown_parsing`     | Any markdown parser available (commonmarker OR markly)                    |\n| `:rbs_parsing`          | Any RBS parser available (rbs gem OR tree-sitter-rbs)                     |\n| `:native_parsing`       | Native tree-sitter backend and grammar available                          |\n| **Library Tags**        |                                                                           |\n| `:toml_rb_gem`          | toml-rb gem available (Citrus backend for TOML)                           |\n| `:toml_gem`             | toml gem available (Parslet backend for TOML)                             |\n| `:rbs_gem`              | rbs gem available (official RBS parser)                                   |\n\nAll tags have negated versions (e.g., `:not_mri_backend`, `:not_jruby_engine`, `:not_toml_parsing`) for testing fallback behavior.\n\n**Debug Output:**\n\nSet `TREE_HAVER_DEBUG=1` to print a dependency summary at the start of your test suite:\n\n```bash\nTREE_HAVER_DEBUG=1 bundle exec rspec\n```\n\n## 🦷 FLOSS Funding\n\nWhile kettle-rb tools are free software and will always be, the project would benefit immensely from some funding.\nRaising a monthly budget of... \"dollars\" would make the project more sustainable.\n\nWe welcome both individual and corporate sponsors\\! We also offer a\nwide array of funding channels to account for your preferences\n(although currently [Open Collective][🖇osc] is our preferred funding platform).\n\n**If you're working in a company that's making significant use of kettle-rb tools we'd\nappreciate it if you suggest to your company to become a kettle-rb sponsor.**\n\nYou can support the development of kettle-rb tools via\n[GitHub Sponsors][🖇sponsor],\n[Liberapay][⛳liberapay],\n[PayPal][🖇paypal],\n[Open Collective][🖇osc]\nand [Tidelift][🏙️entsup-tidelift].\n\n| 📍 NOTE                                                                                                                                                                                                              |\n|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| If doing a sponsorship in the form of donation is problematic for your company \u003cbr/\u003e from an accounting standpoint, we'd recommend the use of Tidelift, \u003cbr/\u003e where you can get a support-like subscription instead. |\n\n### Open Collective for Individuals\n\nSupport us with a monthly donation and help us continue our activities. \\[[Become a backer][🖇osc-backers]\\]\n\nNOTE: [kettle-readme-backers][kettle-readme-backers] updates this list every day, automatically.\n\n\u003c!-- OPENCOLLECTIVE-INDIVIDUALS:START --\u003e\nNo backers yet. Be the first!\n\u003c!-- OPENCOLLECTIVE-INDIVIDUALS:END --\u003e\n\n### Open Collective for Organizations\n\nBecome a sponsor and get your logo on our README on GitHub with a link to your site. \\[[Become a sponsor][🖇osc-sponsors]\\]\n\nNOTE: [kettle-readme-backers][kettle-readme-backers] updates this list every day, automatically.\n\n\u003c!-- OPENCOLLECTIVE-ORGANIZATIONS:START --\u003e\nNo sponsors yet. Be the first!\n\u003c!-- OPENCOLLECTIVE-ORGANIZATIONS:END --\u003e\n\n[kettle-readme-backers]: https://github.com/kettle-rb/tree_haver/blob/main/exe/kettle-readme-backers\n\n### Another way to support open-source\n\nI’m driven by a passion to foster a thriving open-source community – a space where people can tackle complex problems, no matter how small. Revitalizing libraries that have fallen into disrepair, and building new libraries focused on solving real-world challenges, are my passions. I was recently affected by layoffs, and the tech jobs market is unwelcoming. I’m reaching out here because your support would significantly aid my efforts to provide for my family, and my farm (11 🐔 chickens, 2 🐶 dogs, 3 🐰 rabbits, 8 🐈‍ cats).\n\nIf you work at a company that uses my work, please encourage them to support me as a corporate sponsor. My work on gems you use might show up in `bundle fund`.\n\nI’m developing a new library, [floss\\_funding][🖇floss-funding-gem], designed to empower open-source developers like myself to get paid for the work we do, in a sustainable way. Please give it a look.\n\n**[Floss-Funding.dev][🖇floss-funding.dev]: 👉️ No network calls. 👉️ No tracking. 👉️ No oversight. 👉️ Minimal crypto hashing. 💡 Easily disabled nags**\n\n[![OpenCollective Backers][🖇osc-backers-i]][🖇osc-backers] [![OpenCollective Sponsors][🖇osc-sponsors-i]][🖇osc-sponsors] [![Sponsor Me on Github][🖇sponsor-img]][🖇sponsor] [![Liberapay Goal Progress][⛳liberapay-img]][⛳liberapay] [![Donate on PayPal][🖇paypal-img]][🖇paypal] [![Buy me a coffee][🖇buyme-small-img]][🖇buyme] [![Donate on Polar][🖇polar-img]][🖇polar] [![Donate to my FLOSS efforts at ko-fi.com][🖇kofi-img]][🖇kofi] [![Donate to my FLOSS efforts using Patreon][🖇patreon-img]][🖇patreo","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkettle-rb%2Ftree_haver","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkettle-rb%2Ftree_haver","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkettle-rb%2Ftree_haver/lists"}