tree_haver 3.2.2 → 3.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: db807cd904d2fd3878705c02e63288b1c650853c3509431abf23d3ac4f9e83e2
4
- data.tar.gz: d6e9e8299e6dc85a8d856f8a93ae592fe823d5da94cbc624cf1c0bcdafa9adcc
3
+ metadata.gz: 02ffefe8f7a777c04ca0682c3bae06f92d383eacdf2e852755dd6e517cff68bf
4
+ data.tar.gz: a44f91a685d4736f7eac5a81cc04f43e274e18f8af8904eaa4e1247bf1779764
5
5
  SHA512:
6
- metadata.gz: f8b457ef8362cf164d91da763776bd273b9d04f96fca557f597a54b42b102b2b1b8ee5b0a7e98335a12bbe072945b0fd1205e351a4afa4e9e106542263450c70
7
- data.tar.gz: afca005da881caf5cbfdc7e8317e0accf9fca803a2d1582a510daaf1da5f80e46c9987a4f71913366503eee5f30e1dba4dd7ad7d7eaec6821ef9c2f2edbccf11
6
+ metadata.gz: e0d54671ac5cecea3e8496da4e5a9cf9f4ef4c3abd7bcb926f0d8830e230b2d850845e4a49e824e0144c79c35e7477abf6ee9f4a7284637c1fc0c0ff6ff15033
7
+ data.tar.gz: 8f7487bd3fba0f8d03128a32a485db360a1db5d97161f2885918992dd516140c37006d7bf2bc7d8f86867ad597fe5920683f126bd1d30c2257764aab19b37c88
checksums.yaml.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -30,6 +30,47 @@ Please file a bug if you notice a violation of semantic versioning.
30
30
 
31
31
  ### Security
32
32
 
33
+ ## [3.2.3] - 2026-01-02
34
+
35
+ - TAG: [v3.2.3][3.2.3t]
36
+ - COVERAGE: 94.91% -- 2088/2200 lines in 22 files
37
+ - BRANCH COVERAGE: 81.37% -- 738/907 branches in 22 files
38
+ - 90.14% documented
39
+
40
+ ### Fixed
41
+
42
+ - **`parser_for` now respects explicitly requested non-native backends** - Previously,
43
+ `parser_for` would always try tree-sitter backends first and only fall back to alternative
44
+ backends if tree-sitter was unavailable. Now it checks `effective_backend` and skips
45
+ tree-sitter attempts entirely when a non-native backend is explicitly requested via:
46
+ - `TREE_HAVER_BACKEND=citrus` (or `prism`, `psych`, `commonmarker`, `markly`)
47
+ - `TreeHaver.backend = :citrus`
48
+ - `TreeHaver.with_backend(:citrus) { ... }`
49
+
50
+ Native backends (`:mri`, `:rust`, `:ffi`, `:java`) still use tree-sitter grammar discovery.
51
+
52
+ - **`load_tree_sitter_language` now correctly ignores Citrus registrations** - Previously,
53
+ if a language was registered with Citrus first, `load_tree_sitter_language` would
54
+ incorrectly try to use it even when a native backend was explicitly requested. Now it
55
+ only uses registrations that have a `:tree_sitter` key, allowing proper backend switching
56
+ between Citrus and native tree-sitter backends.
57
+
58
+ - **`load_tree_sitter_language` now validates registered paths exist** - Previously,
59
+ if a language had a stale/invalid tree-sitter registration with a non-existent path
60
+ (e.g., from a test), the code would try to use it and fail. Now it checks
61
+ `File.exist?(path)` before using a registered path, falling back to auto-discovery
62
+ via `GrammarFinder` if the registered path doesn't exist.
63
+
64
+ - **`Language.method_missing` no longer falls back to Citrus when native backend explicitly requested** -
65
+ Previously, when tree-sitter loading failed (e.g., .so file missing), the code would
66
+ silently fall back to Citrus even if the user explicitly requested `:mri`, `:rust`,
67
+ `:ffi`, or `:java`. Now fallback to Citrus only happens when `effective_backend` is `:auto`.
68
+ This is a **breaking change** for users who relied on silent fallback behavior.
69
+
70
+ - **Simplified `parser_for` implementation** - Refactored from complex nested conditionals to
71
+ cleaner helper methods (`load_tree_sitter_language`, `load_citrus_language`). The logic is
72
+ now easier to follow and maintain.
73
+
33
74
  ## [3.2.2] - 2026-01-01
34
75
 
35
76
  - TAG: [v3.2.2][3.2.2t]
@@ -674,7 +715,9 @@ Despite the major version bump to 3.0.0 (following semver due to the breaking `L
674
715
 
675
716
  - Initial release
676
717
 
677
- [Unreleased]: https://github.com/kettle-rb/tree_haver/compare/v3.2.2...HEAD
718
+ [Unreleased]: https://github.com/kettle-rb/tree_haver/compare/v3.2.3...HEAD
719
+ [3.2.3]: https://github.com/kettle-rb/tree_haver/compare/v3.2.2...v3.2.3
720
+ [3.2.3t]: https://github.com/kettle-rb/tree_haver/releases/tag/v3.2.3
678
721
  [3.2.2]: https://github.com/kettle-rb/tree_haver/compare/v3.2.1...v3.2.2
679
722
  [3.2.2t]: https://github.com/kettle-rb/tree_haver/releases/tag/v3.2.2
680
723
  [3.2.1]: https://github.com/kettle-rb/tree_haver/compare/v3.2.0...v3.2.1
data/LICENSE.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2025 Peter H. Boling
3
+ Copyright (c) 2025-2026 Peter H. Boling
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -1788,7 +1788,7 @@ See [LICENSE.txt](LICENSE.txt) for the official [Copyright Notice](https://opens
1788
1788
 
1789
1789
  <ul>
1790
1790
  <li>
1791
- Copyright (c) 2025 Peter H. Boling, of
1791
+ Copyright (c) 2025-2026 Peter H. Boling, of
1792
1792
  <a href="https://discord.gg/3qme4XHNKN">
1793
1793
  Galtzo.com
1794
1794
  <picture>
@@ -1977,7 +1977,7 @@ Thanks for RTFM. ☺️
1977
1977
  [📌gitmoji]: https://gitmoji.dev
1978
1978
  [📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
1979
1979
  [🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
1980
- [🧮kloc-img]: https://img.shields.io/badge/KLOC-2.190-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
1980
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-2.200-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
1981
1981
  [🔐security]: SECURITY.md
1982
1982
  [🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
1983
1983
  [📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
@@ -211,10 +211,13 @@ module TreeHaver
211
211
 
212
212
  # No tree-sitter path registered - check for Citrus fallback
213
213
  # This enables auto-fallback when tree-sitter grammar is not installed
214
- # but a Citrus grammar (pure Ruby) is available
215
- citrus_reg = all_backends[:citrus]
216
- if citrus_reg && citrus_reg[:grammar_module]
217
- return Backends::Citrus::Language.new(citrus_reg[:grammar_module])
214
+ # but a Citrus grammar (pure Ruby) is available.
215
+ # Only fall back when backend is :auto - explicit native backend requests should fail.
216
+ if TreeHaver.effective_backend == :auto
217
+ citrus_reg = all_backends[:citrus]
218
+ if citrus_reg && citrus_reg[:grammar_module]
219
+ return Backends::Citrus::Language.new(citrus_reg[:grammar_module])
220
+ end
218
221
  end
219
222
 
220
223
  # No appropriate registration found
@@ -237,17 +240,29 @@ module TreeHaver
237
240
  # - FFI can't find required symbols like ts_parser_new (FFI::NotFoundError)
238
241
  # - Invalid arguments were provided (ArgumentError)
239
242
  #
243
+ # Fallback to Citrus ONLY happens when:
244
+ # - The effective backend is :auto (user didn't explicitly request a native backend)
245
+ # - A Citrus grammar is registered for the language
246
+ #
247
+ # If the user explicitly requested a native backend (:mri, :rust, :ffi, :java),
248
+ # we should NOT silently fall back to Citrus - that would violate the user's intent.
249
+ #
240
250
  # @param error [Exception] the original error
241
251
  # @param all_backends [Hash] all registered backends for the language
242
- # @return [Backends::Citrus::Language] if Citrus fallback available
243
- # @raise [Exception] re-raises original error if no fallback
252
+ # @return [Backends::Citrus::Language] if Citrus fallback available and allowed
253
+ # @raise [Exception] re-raises original error if no fallback or fallback not allowed
244
254
  # @api private
245
255
  def handle_tree_sitter_load_failure(error, all_backends)
246
- citrus_reg = all_backends[:citrus]
247
- if citrus_reg && citrus_reg[:grammar_module]
248
- return Backends::Citrus::Language.new(citrus_reg[:grammar_module])
256
+ # Only fall back to Citrus when backend is :auto
257
+ # If user explicitly requested a native backend, respect that choice
258
+ effective = TreeHaver.effective_backend
259
+ if effective == :auto
260
+ citrus_reg = all_backends[:citrus]
261
+ if citrus_reg && citrus_reg[:grammar_module]
262
+ return Backends::Citrus::Language.new(citrus_reg[:grammar_module])
263
+ end
249
264
  end
250
- # No Citrus fallback available, re-raise the original error
265
+ # No Citrus fallback allowed or available, re-raise the original error
251
266
  raise error
252
267
  end
253
268
  end
@@ -10,7 +10,7 @@ module TreeHaver
10
10
  # Current version of the tree_haver gem
11
11
  #
12
12
  # @return [String] the version string (e.g., "3.0.0")
13
- VERSION = "3.2.2"
13
+ VERSION = "3.2.3"
14
14
  end
15
15
 
16
16
  # Traditional location for VERSION constant
data/lib/tree_haver.rb CHANGED
@@ -16,7 +16,7 @@ require_relative "tree_haver/version"
16
16
  #
17
17
  # == Backends
18
18
  #
19
- # Supports 10 backends:
19
+ # Supports 9 backends:
20
20
  # - Tree-sitter: MRI (C), Rust, FFI, Java
21
21
  # - Native parsers: Prism (Ruby), Psych (YAML), Commonmarker (Markdown), Markly (GFM)
22
22
  # - Pure Ruby: Citrus (portable fallback)
@@ -266,6 +266,11 @@ module TreeHaver
266
266
  # Parser class for parsing source code into syntax trees
267
267
  autoload :Parser, File.join(__dir__, "tree_haver", "parser")
268
268
 
269
+ # Native tree-sitter backends that support loading shared libraries (.so files)
270
+ # These backends wrap the tree-sitter C library via various bindings.
271
+ # Pure Ruby backends (Citrus, Prism, Psych, Commonmarker, Markly) are excluded.
272
+ NATIVE_BACKENDS = %i[mri rust ffi java].freeze
273
+
269
274
  # Get the current backend selection
270
275
  #
271
276
  # @return [Symbol] one of :auto, :mri, :rust, :ffi, :java, or :citrus
@@ -590,11 +595,6 @@ module TreeHaver
590
595
  mod
591
596
  end
592
597
 
593
- # Native tree-sitter backends that support loading shared libraries (.so files)
594
- # These backends wrap the tree-sitter C library via various bindings.
595
- # Pure Ruby backends (Citrus, Prism, Psych, Commonmarker, Markly) are excluded.
596
- NATIVE_BACKENDS = %i[mri rust ffi java].freeze
597
-
598
598
  # Resolve a native tree-sitter backend module (for from_library)
599
599
  #
600
600
  # This method is similar to resolve_backend_module but ONLY considers
@@ -836,114 +836,107 @@ module TreeHaver
836
836
 
837
837
  # Create a parser configured for a specific language
838
838
  #
839
- # This is the recommended high-level API for creating a parser. It handles:
840
- # 1. Checking if the language is already registered
841
- # 2. Auto-discovering tree-sitter grammar via GrammarFinder
842
- # 3. Falling back to Citrus grammar if tree-sitter is unavailable
843
- # 4. Creating and configuring the parser
839
+ # Respects the effective backend setting (via TREE_HAVER_BACKEND env var,
840
+ # TreeHaver.backend=, or with_backend block).
844
841
  #
845
842
  # @param language_name [Symbol, String] the language to parse (e.g., :toml, :json, :bash)
846
843
  # @param library_path [String, nil] optional explicit path to tree-sitter grammar library
847
844
  # @param symbol [String, nil] optional tree-sitter symbol name (defaults to "tree_sitter_<name>")
848
845
  # @param citrus_config [Hash, nil] optional Citrus fallback configuration
849
- # @option citrus_config [String] :gem_name gem name for the Citrus grammar
850
- # @option citrus_config [String] :grammar_const fully qualified constant name for grammar module
851
846
  # @return [TreeHaver::Parser] configured parser with language set
852
847
  # @raise [TreeHaver::NotAvailable] if no parser backend is available for the language
853
848
  #
854
849
  # @example Basic usage (auto-discovers grammar)
855
850
  # parser = TreeHaver.parser_for(:toml)
856
- # tree = parser.parse("[package]\nname = \"my-app\"")
857
851
  #
858
- # @example With explicit library path
859
- # parser = TreeHaver.parser_for(:toml, library_path: "/custom/path/libtree-sitter-toml.so")
860
- #
861
- # @example With Citrus fallback configuration
862
- # parser = TreeHaver.parser_for(:toml,
863
- # citrus_config: { gem_name: "toml-rb", grammar_const: "TomlRB::Document" }
864
- # )
852
+ # @example Force Citrus backend
853
+ # TreeHaver.with_backend(:citrus) { TreeHaver.parser_for(:toml) }
865
854
  def parser_for(language_name, library_path: nil, symbol: nil, citrus_config: nil)
866
855
  name = language_name.to_sym
867
856
  symbol ||= "tree_sitter_#{name}"
857
+ requested = effective_backend
868
858
 
869
- # Step 1: Try to get the language (may already be registered)
870
- language = begin
871
- # Check if already registered and loadable
872
- if registered_language(name)
873
- Language.public_send(name, path: library_path, symbol: symbol)
874
- end
875
- rescue NotAvailable, ArgumentError, LoadError
876
- nil
877
- end
859
+ # Determine which backends to try based on effective_backend
860
+ try_tree_sitter = (requested == :auto) || NATIVE_BACKENDS.include?(requested)
861
+ try_citrus = (requested == :auto) || (requested == :citrus)
878
862
 
879
- # Step 2: If not registered, try GrammarFinder for tree-sitter
880
- unless language
881
- # Principle of Least Surprise: If user provides an explicit path,
882
- # it MUST exist. Don't silently fall back to auto-discovery.
883
- if library_path && !library_path.empty?
884
- unless File.exist?(library_path)
885
- raise NotAvailable,
886
- "Specified parser path does not exist: #{library_path}"
887
- end
888
- begin
889
- register_language(name, path: library_path, symbol: symbol)
890
- language = Language.public_send(name)
891
- rescue NotAvailable, ArgumentError, LoadError => e
892
- # Re-raise with more context since user explicitly provided this path
893
- raise NotAvailable,
894
- "Failed to load parser from specified path #{library_path}: #{e.message}"
895
- end
896
- else
897
- # Auto-discover via GrammarFinder (no explicit path provided)
898
- begin
899
- finder = GrammarFinder.new(name)
900
- if finder.available?
901
- finder.register!
902
- language = Language.public_send(name)
903
- end
904
- rescue NotAvailable, ArgumentError, LoadError
905
- language = nil
906
- end
907
- end
908
- end
863
+ language = nil
909
864
 
910
- # Step 3: Try Citrus fallback if tree-sitter failed
911
- unless language
912
- # Use explicit config, or fall back to built-in defaults for known languages
913
- citrus_config ||= CITRUS_DEFAULTS[name] || {}
914
-
915
- # Only attempt if we have the required configuration
916
- if citrus_config[:gem_name] && citrus_config[:grammar_const]
917
- begin
918
- citrus_finder = CitrusGrammarFinder.new(
919
- language: name,
920
- gem_name: citrus_config[:gem_name],
921
- grammar_const: citrus_config[:grammar_const],
922
- require_path: citrus_config[:require_path],
923
- )
924
- if citrus_finder.available?
925
- citrus_finder.register!
926
- language = Language.public_send(name)
927
- end
928
- rescue NotAvailable, ArgumentError, LoadError, NameError, TypeError
929
- language = nil
930
- end
931
- end
865
+ # Try tree-sitter if applicable
866
+ if try_tree_sitter && !language
867
+ language = load_tree_sitter_language(name, library_path: library_path, symbol: symbol)
932
868
  end
933
869
 
934
- # Step 4: Raise if nothing worked
935
- unless language
936
- raise NotAvailable,
937
- "No parser available for #{name}. " \
938
- "Install tree-sitter-#{name} or the appropriate Ruby gem. " \
939
- "Set TREE_SITTER_#{name.to_s.upcase}_PATH for custom grammar location."
870
+ # Try Citrus if applicable
871
+ if try_citrus && !language
872
+ language = load_citrus_language(name, citrus_config: citrus_config)
940
873
  end
941
874
 
942
- # Step 5: Create and configure parser
875
+ # Raise if nothing worked
876
+ raise NotAvailable, "No parser available for #{name}. " \
877
+ "Install tree-sitter-#{name} or configure a Citrus grammar." unless language
878
+
879
+ # Create and configure parser
943
880
  parser = Parser.new
944
881
  parser.language = language
945
882
  parser
946
883
  end
884
+
885
+ private
886
+
887
+ # Load a tree-sitter language, either from registry or via auto-discovery
888
+ # @return [Language, nil]
889
+ # @raise [NotAvailable] if explicit library_path is provided but doesn't exist or can't load
890
+ def load_tree_sitter_language(name, library_path: nil, symbol: nil)
891
+ # If explicit path provided, it must work - don't swallow errors
892
+ if library_path && !library_path.empty?
893
+ raise NotAvailable, "Specified parser path does not exist: #{library_path}" unless File.exist?(library_path)
894
+ register_language(name, path: library_path, symbol: symbol)
895
+ return Language.public_send(name)
896
+ end
897
+
898
+ # Auto-discovery: errors are acceptable, just return nil
899
+ begin
900
+ # Try already-registered tree-sitter language (not Citrus)
901
+ # But only if the registered path actually exists - ignore stale/test registrations
902
+ registration = registered_language(name)
903
+ ts_reg = registration&.dig(:tree_sitter)
904
+ if ts_reg && ts_reg[:path] && File.exist?(ts_reg[:path])
905
+ return Language.public_send(name, symbol: symbol)
906
+ end
907
+
908
+ # Auto-discover via GrammarFinder
909
+ finder = GrammarFinder.new(name)
910
+ if finder.available?
911
+ finder.register!
912
+ return Language.public_send(name)
913
+ end
914
+ rescue NotAvailable, ArgumentError, LoadError
915
+ # Auto-discovery failed, that's okay
916
+ end
917
+
918
+ nil
919
+ end
920
+
921
+ # Load a Citrus language from configuration or defaults
922
+ # @return [Language, nil]
923
+ def load_citrus_language(name, citrus_config: nil)
924
+ config = citrus_config || CITRUS_DEFAULTS[name] || {}
925
+ return unless config[:gem_name] && config[:grammar_const]
926
+
927
+ finder = CitrusGrammarFinder.new(
928
+ language: name,
929
+ gem_name: config[:gem_name],
930
+ grammar_const: config[:grammar_const],
931
+ require_path: config[:require_path],
932
+ )
933
+ return unless finder.available?
934
+
935
+ finder.register!
936
+ Language.public_send(name)
937
+ rescue NotAvailable, ArgumentError, LoadError, NameError, TypeError
938
+ nil
939
+ end
947
940
  end
948
941
 
949
942
  # Language and Parser classes have been moved to separate files:
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tree_haver
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.2.2
4
+ version: 3.2.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter H. Boling
@@ -292,10 +292,10 @@ licenses:
292
292
  - MIT
293
293
  metadata:
294
294
  homepage_uri: https://tree-haver.galtzo.com/
295
- source_code_uri: https://github.com/kettle-rb/tree_haver/tree/v3.2.2
296
- changelog_uri: https://github.com/kettle-rb/tree_haver/blob/v3.2.2/CHANGELOG.md
295
+ source_code_uri: https://github.com/kettle-rb/tree_haver/tree/v3.2.3
296
+ changelog_uri: https://github.com/kettle-rb/tree_haver/blob/v3.2.3/CHANGELOG.md
297
297
  bug_tracker_uri: https://github.com/kettle-rb/tree_haver/issues
298
- documentation_uri: https://www.rubydoc.info/gems/tree_haver/3.2.2
298
+ documentation_uri: https://www.rubydoc.info/gems/tree_haver/3.2.3
299
299
  funding_uri: https://github.com/sponsors/pboling
300
300
  wiki_uri: https://github.com/kettle-rb/tree_haver/wiki
301
301
  news_uri: https://www.railsbling.com/tags/tree_haver
metadata.gz.sig CHANGED
Binary file