tree_haver 5.0.5 → 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 (59) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/LICENSE.md +13 -0
  4. data/README.md +299 -660
  5. data/lib/tree_haver/backend_api.rb +78 -35
  6. data/lib/tree_haver/backend_context.rb +28 -0
  7. data/lib/tree_haver/backend_registry.rb +119 -382
  8. data/lib/tree_haver/backends/citrus.rb +55 -53
  9. data/lib/tree_haver/backends/ffi.rb +105 -101
  10. data/lib/tree_haver/backends/java.rb +92 -76
  11. data/lib/tree_haver/backends/mri.rb +44 -39
  12. data/lib/tree_haver/backends/parslet.rb +53 -48
  13. data/lib/tree_haver/backends/prism.rb +140 -43
  14. data/lib/tree_haver/backends/psych.rb +30 -26
  15. data/lib/tree_haver/backends/rust.rb +21 -17
  16. data/lib/tree_haver/backends/tslp.rb +274 -0
  17. data/lib/tree_haver/base/comment.rb +320 -0
  18. data/lib/tree_haver/base/language.rb +2 -2
  19. data/lib/tree_haver/base/node.rb +39 -31
  20. data/lib/tree_haver/base/parser.rb +4 -0
  21. data/lib/tree_haver/base/point.rb +3 -3
  22. data/lib/tree_haver/base/tree.rb +1 -1
  23. data/lib/tree_haver/citrus_grammar_finder.rb +21 -26
  24. data/lib/tree_haver/contracts.rb +1025 -0
  25. data/lib/tree_haver/grammar_finder.rb +125 -70
  26. data/lib/tree_haver/kaitai_backend.rb +30 -0
  27. data/lib/tree_haver/language.rb +36 -37
  28. data/lib/tree_haver/language_pack.rb +41 -0
  29. data/lib/tree_haver/language_registry.rb +34 -3
  30. data/lib/tree_haver/library_path_utils.rb +7 -7
  31. data/lib/tree_haver/node.rb +40 -31
  32. data/lib/tree_haver/parser.rb +44 -37
  33. data/lib/tree_haver/parslet_grammar_finder.rb +19 -26
  34. data/lib/tree_haver/path_validator.rb +39 -36
  35. data/lib/tree_haver/peg_backends.rb +76 -0
  36. data/lib/tree_haver/rspec/dependency_tags.rb +23 -1363
  37. data/lib/tree_haver/rspec.rb +1 -31
  38. data/lib/tree_haver/tree.rb +16 -7
  39. data/lib/tree_haver/version.rb +5 -14
  40. data/lib/tree_haver.rb +536 -1240
  41. data/sig/tree_haver.rbs +1 -229
  42. data.tar.gz.sig +0 -0
  43. metadata +154 -70
  44. metadata.gz.sig +0 -0
  45. data/CHANGELOG.md +0 -1393
  46. data/CITATION.cff +0 -20
  47. data/CODE_OF_CONDUCT.md +0 -134
  48. data/CONTRIBUTING.md +0 -359
  49. data/FUNDING.md +0 -74
  50. data/LICENSE.txt +0 -21
  51. data/REEK +0 -0
  52. data/RUBOCOP.md +0 -71
  53. data/SECURITY.md +0 -21
  54. data/lib/tree_haver/base.rb +0 -12
  55. data/lib/tree_haver/compat.rb +0 -43
  56. data/lib/tree_haver/rspec/testable_node.rb +0 -217
  57. data/sig/tree_haver/backends.rbs +0 -352
  58. data/sig/tree_haver/grammar_finder.rbs +0 -29
  59. data/sig/tree_haver/path_validator.rbs +0 -32
@@ -28,19 +28,20 @@ module TreeHaver
28
28
  # @return [Boolean] true if psych is available
29
29
  class << self
30
30
  def available?
31
- return @loaded if @load_attempted # rubocop:disable ThreadSafety/ClassInstanceVariable
32
- @load_attempted = true # rubocop:disable ThreadSafety/ClassInstanceVariable
31
+ return @loaded if @load_attempted
32
+
33
+ @load_attempted = true
33
34
  begin
34
- require "psych"
35
- @loaded = true # rubocop:disable ThreadSafety/ClassInstanceVariable
35
+ require 'psych'
36
+ @loaded = true
36
37
  rescue LoadError
37
- @loaded = false # rubocop:disable ThreadSafety/ClassInstanceVariable
38
+ @loaded = false
38
39
  rescue StandardError
39
- # :nocov: defensive code - StandardError during require is extremely rare
40
- @loaded = false # rubocop:disable ThreadSafety/ClassInstanceVariable
41
- # :nocov:
40
+ # simplecov:disable defensive code - StandardError during require is extremely rare
41
+ @loaded = false
42
+ # simplecov:enable
42
43
  end
43
- @loaded # rubocop:disable ThreadSafety/ClassInstanceVariable
44
+ @loaded
44
45
  end
45
46
 
46
47
  # Reset the load state (primarily for testing)
@@ -48,8 +49,8 @@ module TreeHaver
48
49
  # @return [void]
49
50
  # @api private
50
51
  def reset!
51
- @load_attempted = false # rubocop:disable ThreadSafety/ClassInstanceVariable
52
- @loaded = false # rubocop:disable ThreadSafety/ClassInstanceVariable
52
+ @load_attempted = false
53
+ @loaded = false
53
54
  end
54
55
 
55
56
  # Get capabilities supported by this backend
@@ -57,6 +58,7 @@ module TreeHaver
57
58
  # @return [Hash{Symbol => Object}] capability map
58
59
  def capabilities
59
60
  return {} unless available?
61
+
60
62
  {
61
63
  backend: :psych,
62
64
  query: false, # Psych doesn't have tree-sitter-style queries
@@ -65,6 +67,7 @@ module TreeHaver
65
67
  pure_ruby: false, # Psych has native libyaml C extension
66
68
  yaml_only: true, # Psych only parses YAML
67
69
  error_tolerant: false, # Psych raises on syntax errors
70
+ comment_support: :none
68
71
  }
69
72
  end
70
73
  end
@@ -108,8 +111,8 @@ module TreeHaver
108
111
 
109
112
  unless lang_name == :yaml
110
113
  raise TreeHaver::NotAvailable,
111
- "Psych backend only supports YAML, not #{lang_name}. " \
112
- "Use a tree-sitter backend for #{lang_name} support."
114
+ "Psych backend only supports YAML, not #{lang_name}. " \
115
+ "Use a tree-sitter backend for #{lang_name} support."
113
116
  end
114
117
 
115
118
  yaml
@@ -132,8 +135,9 @@ module TreeHaver
132
135
  # @return [Tree] Parsed tree
133
136
  # @raise [::Psych::SyntaxError] on syntax errors
134
137
  def parse(source)
135
- raise "Language not set" unless language
136
- Psych.available? or raise "Psych not available"
138
+ raise 'Language not set' unless language
139
+
140
+ Psych.available? or raise 'Psych not available'
137
141
 
138
142
  ast = ::Psych.parse_stream(source)
139
143
  Tree.new(ast, source)
@@ -193,7 +197,7 @@ module TreeHaver
193
197
  #
194
198
  # @return [String] Node type
195
199
  def type
196
- inner_node.class.name.split("::").last.downcase
200
+ inner_node.class.name.split('::').last.downcase
197
201
  end
198
202
 
199
203
  # Alias for type (API compatibility)
@@ -336,7 +340,7 @@ module TreeHaver
336
340
  #
337
341
  # @return [String] Extracted text
338
342
  def extract_text_from_location
339
- return "" unless inner_node.respond_to?(:start_line) && inner_node.respond_to?(:end_line)
343
+ return '' unless inner_node.respond_to?(:start_line) && inner_node.respond_to?(:end_line)
340
344
 
341
345
  start_ln = inner_node.start_line || 0
342
346
  end_ln = inner_node.end_line || start_ln
@@ -344,19 +348,19 @@ module TreeHaver
344
348
  end_col = inner_node.end_column || 0
345
349
 
346
350
  if start_ln == end_ln
347
- line = lines[start_ln] || ""
348
- line[start_col...end_col] || ""
351
+ line = lines[start_ln] || ''
352
+ line[start_col...end_col] || ''
349
353
  else
350
354
  result = []
351
355
  (start_ln..end_ln).each do |ln|
352
- line = lines[ln] || ""
356
+ line = lines[ln] || ''
353
357
  result << if ln == start_ln
354
- line[start_col..]
355
- elsif ln == end_ln
356
- line[0...end_col]
357
- else
358
- line
359
- end
358
+ line[start_col..]
359
+ elsif ln == end_ln
360
+ line[0...end_col]
361
+ else
362
+ line
363
+ end
360
364
  end
361
365
  result.compact.join
362
366
  end
@@ -50,23 +50,24 @@ module TreeHaver
50
50
  # end
51
51
  class << self
52
52
  def available?
53
- return @loaded if @load_attempted # rubocop:disable ThreadSafety/ClassInstanceVariable
54
- @load_attempted = true # rubocop:disable ThreadSafety/ClassInstanceVariable
53
+ return @loaded if @load_attempted
54
+
55
+ @load_attempted = true
55
56
  begin
56
57
  # tree_stump uses magnus which requires MRI's C API
57
58
  # It doesn't work on JRuby or TruffleRuby
58
- if RUBY_ENGINE == "ruby"
59
- require "tree_stump"
60
- @loaded = true # rubocop:disable ThreadSafety/ClassInstanceVariable
59
+ if RUBY_ENGINE == 'ruby'
60
+ require 'tree_stump'
61
+ @loaded = true
61
62
  else
62
- @loaded = false # rubocop:disable ThreadSafety/ClassInstanceVariable
63
+ @loaded = false
63
64
  end
64
65
  rescue LoadError
65
- @loaded = false # rubocop:disable ThreadSafety/ClassInstanceVariable
66
+ @loaded = false
66
67
  rescue StandardError
67
- @loaded = false # rubocop:disable ThreadSafety/ClassInstanceVariable
68
+ @loaded = false
68
69
  end
69
- @loaded # rubocop:disable ThreadSafety/ClassInstanceVariable
70
+ @loaded
70
71
  end
71
72
 
72
73
  # Reset the load state (primarily for testing)
@@ -74,8 +75,8 @@ module TreeHaver
74
75
  # @return [void]
75
76
  # @api private
76
77
  def reset!
77
- @load_attempted = false # rubocop:disable ThreadSafety/ClassInstanceVariable
78
- @loaded = false # rubocop:disable ThreadSafety/ClassInstanceVariable
78
+ @load_attempted = false
79
+ @loaded = false
79
80
  end
80
81
 
81
82
  # Get capabilities supported by this backend
@@ -83,14 +84,16 @@ module TreeHaver
83
84
  # @return [Hash{Symbol => Object}] capability map
84
85
  # @example
85
86
  # TreeHaver::Backends::Rust.capabilities
86
- # # => { backend: :rust, query: true, bytes_field: true, incremental: false }
87
+ # # => { backend: :rust, query: true, bytes_field: true, incremental: false, comment_support: :nodes_only }
87
88
  def capabilities
88
89
  return {} unless available?
90
+
89
91
  {
90
92
  backend: :rust,
91
93
  query: true,
92
94
  bytes_field: true,
93
- incremental: false, # TreeStump doesn't currently expose incremental parsing to Ruby
95
+ incremental: false, # TreeStump doesn't currently expose incremental parsing to Ruby
96
+ comment_support: :nodes_only
94
97
  }
95
98
  end
96
99
  end
@@ -145,7 +148,7 @@ module TreeHaver
145
148
  end
146
149
 
147
150
  # Alias eql? to ==
148
- alias_method :eql?, :==
151
+ alias eql? ==
149
152
 
150
153
  # Load a language from a shared library path
151
154
  #
@@ -158,7 +161,7 @@ module TreeHaver
158
161
  # lang = TreeHaver::Backends::Rust::Language.from_library("/usr/local/lib/libtree-sitter-toml.so")
159
162
  class << self
160
163
  def from_library(path, symbol: nil, name: nil) # rubocop:disable Lint/UnusedMethodArgument
161
- raise TreeHaver::NotAvailable, "tree_stump not available" unless Rust.available?
164
+ raise TreeHaver::NotAvailable, 'tree_stump not available' unless Rust.available?
162
165
 
163
166
  # Validate the path exists before calling register_lang to provide a clear error
164
167
  raise TreeHaver::NotAvailable, "Language library not found: #{path}" unless File.exist?(path)
@@ -174,7 +177,7 @@ module TreeHaver
174
177
  end
175
178
 
176
179
  # Backward-compatible alias for from_library
177
- alias_method :from_path, :from_library
180
+ alias from_path from_library
178
181
  end
179
182
  end
180
183
 
@@ -186,7 +189,8 @@ module TreeHaver
186
189
  #
187
190
  # @raise [TreeHaver::NotAvailable] if tree_stump is not available
188
191
  def initialize
189
- raise TreeHaver::NotAvailable, "tree_stump not available" unless Rust.available?
192
+ raise TreeHaver::NotAvailable, 'tree_stump not available' unless Rust.available?
193
+
190
194
  @parser = ::TreeStump::Parser.new
191
195
  end
192
196
 
@@ -0,0 +1,274 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TreeHaver
4
+ module Backends
5
+ # TSLP backend using tree_sitter_language_pack's on-demand parser API.
6
+ #
7
+ # This backend intentionally requires the real parser API. The language-pack
8
+ # process API is not a TreeHaver parser backend and must not be used as a
9
+ # merge-gem integration surface.
10
+ module Tslp
11
+ @load_attempted = false
12
+ @loaded = false
13
+ @unavailable_reason = nil
14
+ @language_availability = {}
15
+ @language_unavailable_reasons = {}
16
+ PARSER_SMOKE_SOURCES = {
17
+ 'json' => '{}',
18
+ 'json5' => '{}',
19
+ 'bash' => "echo tree_haver\n",
20
+ 'go' => "package main\nfunc main() {}\n",
21
+ 'html' => "<!doctype html>\n<title>TreeHaver</title>\n",
22
+ 'markdown' => "# TreeHaver\n",
23
+ 'ruby' => "class TreeHaverSmoke\nend\n",
24
+ 'rust' => "fn main() {}\n",
25
+ 'typescript' => "export const treeHaver = true;\n",
26
+ 'toml' => "title = \"tree_haver\"\n",
27
+ 'yaml' => "tree_haver: true\n"
28
+ }.freeze
29
+ DEFAULT_PARSER_SMOKE_SOURCE = ''
30
+
31
+ class << self
32
+ attr_reader :unavailable_reason
33
+
34
+ def available?
35
+ return @loaded if @load_attempted
36
+
37
+ @load_attempted = true
38
+ begin
39
+ require 'tree_sitter_language_pack' unless defined?(::TreeSitterLanguagePack)
40
+ @loaded = parser_api_available?
41
+ if !@loaded && @unavailable_reason.to_s.empty?
42
+ @unavailable_reason = 'tree_sitter_language_pack parser API is not exposed'
43
+ end
44
+ rescue LoadError => e
45
+ @loaded = false
46
+ @unavailable_reason = e.message
47
+ rescue StandardError => e
48
+ @loaded = false
49
+ @unavailable_reason = e.message
50
+ end
51
+ @loaded
52
+ end
53
+
54
+ def reset!
55
+ @load_attempted = false
56
+ @loaded = false
57
+ @unavailable_reason = nil
58
+ @language_availability = {}
59
+ @language_unavailable_reasons = {}
60
+ end
61
+
62
+ def capabilities
63
+ return {} unless available?
64
+
65
+ {
66
+ backend: :tslp,
67
+ query: false,
68
+ bytes_field: true,
69
+ incremental: false,
70
+ comment_support: :nodes_only,
71
+ language_pack: true
72
+ }
73
+ end
74
+
75
+ def parser_available_for?(language_name)
76
+ return false unless available?
77
+
78
+ name = language_name.to_s
79
+ return @language_availability.fetch(name) if @language_availability.key?(name)
80
+
81
+ @language_availability[name] = smoke_parse_language(name)
82
+ @language_unavailable_reasons[name] = @unavailable_reason unless @language_availability.fetch(name)
83
+ @language_availability.fetch(name)
84
+ rescue StandardError => e
85
+ @unavailable_reason = e.message
86
+ @language_unavailable_reasons[language_name.to_s] = e.message
87
+ false
88
+ end
89
+
90
+ private
91
+
92
+ def parser_api_available?
93
+ return false unless ::TreeSitterLanguagePack.respond_to?(:get_parser)
94
+ return false unless defined?(::TreeSitterLanguagePack::Parser)
95
+ return false unless ::TreeSitterLanguagePack::Parser.instance_methods.include?(:parse)
96
+
97
+ parser_api_smoke_test
98
+ rescue StandardError => e
99
+ @unavailable_reason = e.message
100
+ false
101
+ end
102
+
103
+ def parser_api_smoke_test
104
+ language_name, source = PARSER_SMOKE_SOURCES.find do |name, _smoke_source|
105
+ !::TreeSitterLanguagePack.respond_to?(:has_language) ||
106
+ ::TreeSitterLanguagePack.has_language(name)
107
+ end
108
+ return false unless language_name
109
+
110
+ smoke_parse_language(language_name, source: source)
111
+ end
112
+
113
+ def smoke_parse_language(language_name, source: smoke_source_for(language_name))
114
+ name = language_name.to_s
115
+ if ::TreeSitterLanguagePack.respond_to?(:has_language) &&
116
+ !::TreeSitterLanguagePack.has_language(name)
117
+ @unavailable_reason = "tree_sitter_language_pack does not publish #{name}"
118
+ return false
119
+ end
120
+
121
+ parser = ::TreeSitterLanguagePack.get_parser(name)
122
+ return false unless parser
123
+
124
+ tree = parser.parse(source)
125
+ return false unless tree&.respond_to?(:root_node)
126
+
127
+ root = tree.root_node
128
+ root && !node_has_error?(root)
129
+ end
130
+
131
+ def smoke_source_for(language_name)
132
+ PARSER_SMOKE_SOURCES.fetch(language_name.to_s, DEFAULT_PARSER_SMOKE_SOURCE)
133
+ end
134
+
135
+ def node_has_error?(node)
136
+ if node.respond_to?(:has_error)
137
+ node.has_error
138
+ elsif node.respond_to?(:has_error?)
139
+ node.has_error?
140
+ else
141
+ false
142
+ end
143
+ end
144
+ end
145
+
146
+ class Language < TreeHaver::Base::Language
147
+ def initialize(name)
148
+ super(name.to_sym, backend: :tslp, options: {})
149
+ end
150
+
151
+ class << self
152
+ def from_library(_path = nil, symbol: nil, name: nil) # rubocop:disable Lint/UnusedMethodArgument
153
+ new(name || :unknown)
154
+ end
155
+ end
156
+ end
157
+
158
+ class Parser < TreeHaver::Base::Parser
159
+ def parse(source)
160
+ raise TreeHaver::NotAvailable, unavailable_message unless Tslp.available?
161
+ raise TreeHaver::NotAvailable, 'TSLP language is not set' unless language
162
+
163
+ parser = ::TreeSitterLanguagePack.get_parser(language.name.to_s)
164
+ raise TreeHaver::NotAvailable, "TSLP did not return a parser for #{language.name}" unless parser
165
+
166
+ raw_tree = parser.parse(source)
167
+ raise TreeHaver::NotAvailable, "TSLP did not return a parse tree for #{language.name}" unless raw_tree
168
+
169
+ Tree.new(raw_tree, source: source, language: language.name)
170
+ end
171
+
172
+ private
173
+
174
+ def unavailable_message
175
+ reason = Tslp.unavailable_reason
176
+ detail = reason.to_s.empty? ? 'unknown reason' : reason
177
+ "tree_sitter_language_pack parser API is unavailable: #{detail}"
178
+ end
179
+ end
180
+
181
+ class Tree < TreeHaver::Base::Tree
182
+ attr_reader :language
183
+
184
+ def initialize(inner_tree = nil, source: nil, lines: nil, language: nil)
185
+ super(inner_tree, source: source, lines: lines)
186
+ @language = language.to_s
187
+ end
188
+
189
+ def root_node
190
+ Node.new(inner_tree.root_node, source: source, lines: lines, language: language)
191
+ end
192
+ end
193
+
194
+ class Node < TreeHaver::Base::Node
195
+ NODE_TYPE_ALIASES = {
196
+ 'json5' => {
197
+ 'file' => 'document',
198
+ 'member' => 'pair'
199
+ }
200
+ }.freeze
201
+
202
+ attr_reader :language
203
+
204
+ def initialize(node, source: nil, lines: nil, language: nil)
205
+ super(node, source: source, lines: lines)
206
+ @language = language.to_s
207
+ end
208
+
209
+ def type
210
+ NODE_TYPE_ALIASES.fetch(language, {}).fetch(native_type, native_type)
211
+ end
212
+
213
+ def native_type
214
+ inner_node.kind
215
+ end
216
+
217
+ def start_byte
218
+ inner_node.start_byte
219
+ end
220
+
221
+ def end_byte
222
+ inner_node.end_byte
223
+ end
224
+
225
+ def start_point
226
+ point = inner_node.start_position
227
+ { row: point.row, column: point.column }
228
+ end
229
+
230
+ def end_point
231
+ point = inner_node.end_position
232
+ { row: point.row, column: point.column }
233
+ end
234
+
235
+ def children
236
+ Array.new(inner_node.child_count) do |index|
237
+ child = inner_node.child(index)
238
+ child && self.class.new(child, source: source, lines: lines, language: language)
239
+ end.compact
240
+ end
241
+
242
+ def child_by_field_name(name)
243
+ child = inner_node.child_by_field_name(name.to_s) if inner_node.respond_to?(:child_by_field_name)
244
+ child && self.class.new(child, source: source, lines: lines, language: language)
245
+ end
246
+
247
+ def parent
248
+ parent = inner_node.parent if inner_node.respond_to?(:parent)
249
+ parent && self.class.new(parent, source: source, lines: lines, language: language)
250
+ end
251
+
252
+ def named?
253
+ inner_node.is_named
254
+ end
255
+
256
+ def has_error?
257
+ inner_node.has_error
258
+ end
259
+
260
+ def error?
261
+ inner_node.is_error
262
+ end
263
+
264
+ def missing?
265
+ inner_node.is_missing
266
+ end
267
+
268
+ def extra?
269
+ inner_node.is_extra
270
+ end
271
+ end
272
+ end
273
+ end
274
+ end