tree_haver 7.0.0 → 7.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/LICENSE.md +13 -0
  4. data/README.md +1959 -0
  5. data/lib/tree_haver/backend_api.rb +392 -0
  6. data/lib/tree_haver/backend_registry.rb +153 -3
  7. data/lib/tree_haver/backends/citrus.rb +489 -0
  8. data/lib/tree_haver/backends/ffi.rb +1013 -0
  9. data/lib/tree_haver/backends/java.rb +909 -0
  10. data/lib/tree_haver/backends/mri.rb +367 -0
  11. data/lib/tree_haver/backends/parslet.rb +565 -0
  12. data/lib/tree_haver/backends/prism.rb +568 -0
  13. data/lib/tree_haver/backends/psych.rb +379 -0
  14. data/lib/tree_haver/backends/rust.rb +243 -0
  15. data/lib/tree_haver/backends/tslp.rb +274 -0
  16. data/lib/tree_haver/base/comment.rb +320 -0
  17. data/lib/tree_haver/base/language.rb +98 -0
  18. data/lib/tree_haver/base/node.rb +330 -0
  19. data/lib/tree_haver/base/parser.rb +28 -0
  20. data/lib/tree_haver/base/point.rb +48 -0
  21. data/lib/tree_haver/base/tree.rb +128 -0
  22. data/lib/tree_haver/citrus_grammar_finder.rb +213 -0
  23. data/lib/tree_haver/contracts.rb +661 -96
  24. data/lib/tree_haver/grammar_finder.rb +429 -0
  25. data/lib/tree_haver/kaitai_backend.rb +2 -2
  26. data/lib/tree_haver/language.rb +294 -0
  27. data/lib/tree_haver/language_pack.rb +17 -166
  28. data/lib/tree_haver/language_registry.rb +221 -0
  29. data/lib/tree_haver/library_path_utils.rb +80 -0
  30. data/lib/tree_haver/node.rb +588 -0
  31. data/lib/tree_haver/parser.rb +445 -0
  32. data/lib/tree_haver/parslet_grammar_finder.rb +217 -0
  33. data/lib/tree_haver/path_validator.rb +356 -0
  34. data/lib/tree_haver/peg_backends.rb +7 -7
  35. data/lib/tree_haver/point.rb +27 -0
  36. data/lib/tree_haver/rspec/dependency_tags.rb +52 -0
  37. data/lib/tree_haver/rspec.rb +3 -0
  38. data/lib/tree_haver/tree.rb +267 -0
  39. data/lib/tree_haver/version.rb +5 -3
  40. data/lib/tree_haver.rb +613 -8
  41. data/sig/tree_haver.rbs +6 -0
  42. data.tar.gz.sig +0 -0
  43. metadata +314 -13
  44. metadata.gz.sig +0 -0
@@ -0,0 +1,213 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TreeHaver
4
+ # Utility for finding and registering Citrus grammar gems.
5
+ #
6
+ # CitrusGrammarFinder provides language-agnostic discovery of Citrus grammar
7
+ # gems. Given a language name and gem information, it attempts to load the
8
+ # grammar and register it with tree_haver.
9
+ #
10
+ # Unlike tree-sitter grammars (which are .so files), Citrus grammars are
11
+ # Ruby modules that respond to .parse(source). This class handles the
12
+ # discovery and registration of these grammars.
13
+ #
14
+ # @example Basic usage with toml-rb
15
+ # finder = TreeHaver::CitrusGrammarFinder.new(
16
+ # language: :toml,
17
+ # gem_name: "toml-rb",
18
+ # grammar_const: "TomlRB::Document"
19
+ # )
20
+ # finder.register! if finder.available?
21
+ #
22
+ # @example With custom require path
23
+ # finder = TreeHaver::CitrusGrammarFinder.new(
24
+ # language: :json,
25
+ # gem_name: "json-rb",
26
+ # grammar_const: "JsonRB::Grammar",
27
+ # require_path: "json/rb"
28
+ # )
29
+ #
30
+ # @see GrammarFinder For tree-sitter grammar discovery
31
+ class CitrusGrammarFinder
32
+ # @return [Symbol] the language identifier
33
+ attr_reader :language_name
34
+
35
+ # @return [String] the gem name to require
36
+ attr_reader :gem_name
37
+
38
+ # @return [String] the constant path to the grammar (e.g., "TomlRB::Document")
39
+ attr_reader :grammar_const
40
+
41
+ # @return [String, nil] custom require path (defaults to gem_name with dashes to slashes)
42
+ attr_reader :require_path
43
+
44
+ # Initialize a Citrus grammar finder
45
+ #
46
+ # @param language [Symbol, String] the language name (e.g., :toml, :json)
47
+ # @param gem_name [String] the gem name (e.g., "toml-rb")
48
+ # @param grammar_const [String] constant path to grammar (e.g., "TomlRB::Document")
49
+ # @param require_path [String, nil] custom require path (defaults to gem_name as-is)
50
+ def initialize(language:, gem_name:, grammar_const:, require_path: nil)
51
+ @language_name = language.to_sym
52
+ @gem_name = gem_name
53
+ @grammar_const = grammar_const
54
+ @require_path = require_path || gem_name
55
+ @load_attempted = false
56
+ @available = false
57
+ @grammar_module = nil
58
+ end
59
+
60
+ # Check if the Citrus grammar is available
61
+ #
62
+ # Attempts to require the gem and resolve the grammar constant.
63
+ # Result is cached after first call.
64
+ #
65
+ # @return [Boolean] true if grammar is available
66
+ def available?
67
+ return @available if @load_attempted
68
+
69
+ @load_attempted = true
70
+ debug = ENV['TREE_HAVER_DEBUG']
71
+
72
+ # Guard against nil require_path (can happen if gem_name was nil)
73
+ if @require_path.nil? || @require_path.empty?
74
+ warn("CitrusGrammarFinder: require_path is nil or empty for #{@language_name}") if debug
75
+ @available = false
76
+ return false
77
+ end
78
+
79
+ begin
80
+ # Try to require the gem
81
+ require @require_path
82
+
83
+ # Try to resolve the constant
84
+ @grammar_module = resolve_constant(@grammar_const)
85
+
86
+ # Verify it responds to parse
87
+ unless @grammar_module.respond_to?(:parse)
88
+ # simplecov:disable defensive - requires a gem with malformed grammar module
89
+ # Show what methods ARE available to help diagnose the issue
90
+ if debug
91
+ available_methods = @grammar_module.methods(false).sort.first(20)
92
+ warn("CitrusGrammarFinder: #{@grammar_const} doesn't respond to :parse")
93
+ warn("CitrusGrammarFinder: #{@grammar_const}.class = #{@grammar_module.class}")
94
+ warn("CitrusGrammarFinder: #{@grammar_const} is a #{@grammar_module.is_a?(Module) ? 'Module' : 'non-Module'}")
95
+ warn("CitrusGrammarFinder: Available singleton methods (first 20): #{available_methods.inspect}")
96
+ if @grammar_module.respond_to?(:instance_methods)
97
+ instance_methods = @grammar_module.instance_methods(false).sort.first(20)
98
+ warn("CitrusGrammarFinder: Available instance methods (first 20): #{instance_methods.inspect}")
99
+ end
100
+ end
101
+ @available = false
102
+ return false
103
+ # simplecov:enable
104
+ end
105
+
106
+ @available = true
107
+ rescue LoadError => e
108
+ # simplecov:disable defensive - requires gem to not be installed
109
+ # Only show LoadError details when debugging
110
+ if debug
111
+ warn("CitrusGrammarFinder: Failed to load '#{@require_path}': #{e.class}: #{e.message}")
112
+ warn("CitrusGrammarFinder: LoadError backtrace:\n #{e.backtrace&.first(10)&.join("\n ")}")
113
+ end
114
+ @available = false
115
+ # simplecov:enable
116
+ rescue NameError => e
117
+ # simplecov:disable defensive - requires gem with missing constant
118
+ # Only show NameError details when debugging
119
+ if debug
120
+ warn("CitrusGrammarFinder: Failed to resolve '#{@grammar_const}': #{e.class}: #{e.message}")
121
+ warn("CitrusGrammarFinder: NameError backtrace:\n #{e.backtrace&.first(10)&.join("\n ")}")
122
+ end
123
+ @available = false
124
+ # simplecov:enable
125
+ rescue TypeError => e
126
+ # simplecov:disable defensive - TruffleRuby-specific edge case
127
+ # TruffleRuby's bundled_gems.rb can raise TypeError when File.path is called on nil
128
+ # This happens in bundled_gems.rb:124 warning? method when caller locations return nil
129
+ # Always warn about TypeError as it indicates a platform-specific issue
130
+ warn("CitrusGrammarFinder: TypeError during load of '#{@require_path}': #{e.class}: #{e.message}")
131
+ warn('CitrusGrammarFinder: This may be a TruffleRuby bundled_gems.rb issue')
132
+ warn("CitrusGrammarFinder: TypeError backtrace:\n #{e.backtrace&.first(10)&.join("\n ")}") if debug
133
+ @available = false
134
+ # simplecov:enable
135
+ rescue StandardError => e
136
+ # simplecov:disable defensive - catch-all for unexpected errors
137
+ # Always warn about unexpected errors
138
+ warn("CitrusGrammarFinder: Unexpected error: #{e.class}: #{e.message}")
139
+ warn("CitrusGrammarFinder: backtrace:\n #{e.backtrace&.first(10)&.join("\n ")}") if debug
140
+ @available = false
141
+ # simplecov:enable
142
+ end
143
+
144
+ @available
145
+ end
146
+
147
+ # Get the resolved grammar module
148
+ #
149
+ # @return [Module, nil] the grammar module if available
150
+ def grammar_module
151
+ available? # Ensure we've tried to load
152
+ @grammar_module
153
+ end
154
+
155
+ # Register this Citrus grammar with TreeHaver
156
+ #
157
+ # After registration, the language can be used via:
158
+ # TreeHaver::Language.{language_name}
159
+ #
160
+ # @param raise_on_missing [Boolean] if true, raises when grammar not available
161
+ # @return [Boolean] true if registration succeeded
162
+ # @raise [NotAvailable] if grammar not available and raise_on_missing is true
163
+ def register!(raise_on_missing: false)
164
+ unless available?
165
+ raise NotAvailable, not_found_message if raise_on_missing
166
+
167
+ return false
168
+ end
169
+
170
+ TreeHaver.register_language(
171
+ @language_name,
172
+ grammar_module: @grammar_module,
173
+ gem_name: @gem_name
174
+ )
175
+ true
176
+ end
177
+
178
+ # Get debug information about the search
179
+ #
180
+ # @return [Hash] diagnostic information
181
+ def search_info
182
+ {
183
+ language: @language_name,
184
+ gem_name: @gem_name,
185
+ grammar_const: @grammar_const,
186
+ require_path: @require_path,
187
+ available: available?,
188
+ grammar_module: @grammar_module&.name
189
+ }
190
+ end
191
+
192
+ # Get a human-readable error message when grammar is not found
193
+ #
194
+ # @return [String] error message with installation hints
195
+ def not_found_message
196
+ "Citrus grammar for #{@language_name} not found. " \
197
+ "Install #{@gem_name} gem: gem install #{@gem_name}"
198
+ end
199
+
200
+ private
201
+
202
+ # Resolve a constant path like "TomlRB::Document"
203
+ #
204
+ # @param const_path [String] constant path
205
+ # @return [Object] the constant
206
+ # @raise [NameError] if constant not found
207
+ def resolve_constant(const_path)
208
+ const_path.split('::').reduce(Object) do |mod, const_name|
209
+ mod.const_get(const_name)
210
+ end
211
+ end
212
+ end
213
+ end