tree_haver 5.0.4 → 7.0.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 (57) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/lib/tree_haver/backend_context.rb +28 -0
  4. data/lib/tree_haver/backend_registry.rb +19 -432
  5. data/lib/tree_haver/contracts.rb +460 -0
  6. data/lib/tree_haver/kaitai_backend.rb +30 -0
  7. data/lib/tree_haver/language_pack.rb +190 -0
  8. data/lib/tree_haver/peg_backends.rb +76 -0
  9. data/lib/tree_haver/version.rb +1 -12
  10. data/lib/tree_haver.rb +7 -1316
  11. data.tar.gz.sig +0 -0
  12. metadata +34 -245
  13. metadata.gz.sig +0 -0
  14. data/CHANGELOG.md +0 -1366
  15. data/CITATION.cff +0 -20
  16. data/CODE_OF_CONDUCT.md +0 -134
  17. data/CONTRIBUTING.md +0 -359
  18. data/FUNDING.md +0 -74
  19. data/LICENSE.txt +0 -21
  20. data/README.md +0 -2347
  21. data/REEK +0 -0
  22. data/RUBOCOP.md +0 -71
  23. data/SECURITY.md +0 -21
  24. data/lib/tree_haver/backend_api.rb +0 -349
  25. data/lib/tree_haver/backends/citrus.rb +0 -487
  26. data/lib/tree_haver/backends/ffi.rb +0 -1009
  27. data/lib/tree_haver/backends/java.rb +0 -893
  28. data/lib/tree_haver/backends/mri.rb +0 -362
  29. data/lib/tree_haver/backends/parslet.rb +0 -560
  30. data/lib/tree_haver/backends/prism.rb +0 -471
  31. data/lib/tree_haver/backends/psych.rb +0 -375
  32. data/lib/tree_haver/backends/rust.rb +0 -239
  33. data/lib/tree_haver/base/language.rb +0 -98
  34. data/lib/tree_haver/base/node.rb +0 -322
  35. data/lib/tree_haver/base/parser.rb +0 -24
  36. data/lib/tree_haver/base/point.rb +0 -48
  37. data/lib/tree_haver/base/tree.rb +0 -128
  38. data/lib/tree_haver/base.rb +0 -12
  39. data/lib/tree_haver/citrus_grammar_finder.rb +0 -218
  40. data/lib/tree_haver/compat.rb +0 -43
  41. data/lib/tree_haver/grammar_finder.rb +0 -374
  42. data/lib/tree_haver/language.rb +0 -295
  43. data/lib/tree_haver/language_registry.rb +0 -190
  44. data/lib/tree_haver/library_path_utils.rb +0 -80
  45. data/lib/tree_haver/node.rb +0 -579
  46. data/lib/tree_haver/parser.rb +0 -438
  47. data/lib/tree_haver/parslet_grammar_finder.rb +0 -224
  48. data/lib/tree_haver/path_validator.rb +0 -353
  49. data/lib/tree_haver/point.rb +0 -27
  50. data/lib/tree_haver/rspec/dependency_tags.rb +0 -1392
  51. data/lib/tree_haver/rspec/testable_node.rb +0 -217
  52. data/lib/tree_haver/rspec.rb +0 -33
  53. data/lib/tree_haver/tree.rb +0 -258
  54. data/sig/tree_haver/backends.rbs +0 -352
  55. data/sig/tree_haver/grammar_finder.rbs +0 -29
  56. data/sig/tree_haver/path_validator.rbs +0 -32
  57. data/sig/tree_haver.rbs +0 -234
@@ -1,217 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Ensure TreeHaver::Node and TreeHaver::Point are loaded
4
- require "tree_haver"
5
-
6
- module TreeHaver
7
- module RSpec
8
- # A mock inner node that provides the minimal interface TreeHaver::Node expects.
9
- #
10
- # This is what TreeHaver::Node wraps - it simulates the backend-specific node
11
- # (like tree-sitter's Node, Markly::Node, etc.)
12
- #
13
- # @api private
14
- class MockInnerNode
15
- attr_reader :type, :start_byte, :end_byte, :children_data
16
-
17
- def initialize(
18
- type:,
19
- text: nil,
20
- start_byte: 0,
21
- end_byte: nil,
22
- start_row: 0,
23
- start_column: 0,
24
- end_row: nil,
25
- end_column: nil,
26
- children: []
27
- )
28
- @type = type.to_s
29
- @text_content = text
30
- @start_byte = start_byte
31
- @end_byte = end_byte || (text ? start_byte + text.length : start_byte)
32
- @start_row = start_row
33
- @start_column = start_column
34
- @end_row = end_row || start_row
35
- @end_column = end_column || (text ? start_column + text.length : start_column)
36
- @children_data = children
37
- end
38
-
39
- def start_point
40
- TreeHaver::Point.new(@start_row, @start_column)
41
- end
42
-
43
- def end_point
44
- TreeHaver::Point.new(@end_row, @end_column)
45
- end
46
-
47
- def child_count
48
- @children_data.length
49
- end
50
-
51
- def child(index)
52
- return if index.nil? || index < 0 || index >= @children_data.length
53
-
54
- @children_data[index]
55
- end
56
-
57
- # Return children array (for enumerable behavior)
58
- def children
59
- @children_data
60
- end
61
-
62
- def first_child
63
- @children_data.first
64
- end
65
-
66
- def last_child
67
- @children_data.last
68
- end
69
-
70
- # Iterate over children
71
- def each(&block)
72
- return enum_for(:each) unless block
73
-
74
- @children_data.each(&block)
75
- end
76
-
77
- def named?
78
- true
79
- end
80
-
81
- # Test nodes are always valid (no parse errors)
82
- def has_error?
83
- false
84
- end
85
-
86
- # Test nodes are never missing (not error recovery insertions)
87
- def missing?
88
- false
89
- end
90
-
91
- # Some backends provide text directly
92
- def text
93
- @text_content
94
- end
95
-
96
- # For backends that use string_content (like Markly/Commonmarker)
97
- def string_content
98
- @text_content
99
- end
100
- end
101
-
102
- # A real TreeHaver::Node that wraps a MockInnerNode.
103
- #
104
- # This gives us full TreeHaver::Node behavior (#text, #type, #source_position, etc.)
105
- # while allowing us to control the underlying data for testing.
106
- #
107
- # TestableNode is designed for testing code that works with TreeHaver nodes
108
- # without requiring an actual parser backend. It creates real TreeHaver::Node
109
- # instances with controlled, predictable data.
110
- #
111
- # @example Creating a testable node
112
- # node = TreeHaver::RSpec::TestableNode.create(
113
- # type: :heading,
114
- # text: "## My Heading",
115
- # start_line: 1
116
- # )
117
- # node.text # => "## My Heading"
118
- # node.type # => "heading"
119
- # node.start_line # => 1
120
- #
121
- # @example Creating with children
122
- # parent = TreeHaver::RSpec::TestableNode.create(
123
- # type: :document,
124
- # text: "# Title\n\nParagraph",
125
- # children: [
126
- # { type: :heading, text: "# Title", start_line: 1 },
127
- # { type: :paragraph, text: "Paragraph", start_line: 3 },
128
- # ]
129
- # )
130
- #
131
- # @example Using the convenience constant
132
- # # After requiring tree_haver/rspec/testable_node, you can use:
133
- # node = TestableNode.create(type: :paragraph, text: "Hello")
134
- #
135
- class TestableNode < TreeHaver::Node
136
- class << self
137
- # Create a TestableNode with the given attributes.
138
- #
139
- # @param type [Symbol, String] Node type (e.g., :heading, :paragraph)
140
- # @param text [String] The text content of the node
141
- # @param start_line [Integer] 1-based start line number (default: 1)
142
- # @param end_line [Integer, nil] 1-based end line number (default: calculated from text)
143
- # @param start_column [Integer] 0-based start column (default: 0)
144
- # @param end_column [Integer, nil] 0-based end column (default: calculated from text)
145
- # @param start_byte [Integer] Start byte offset (default: 0)
146
- # @param end_byte [Integer, nil] End byte offset (default: calculated from text)
147
- # @param children [Array<Hash>] Child node specifications
148
- # @param source [String, nil] Full source text (default: uses text param)
149
- # @return [TestableNode]
150
- def create(
151
- type:,
152
- text: "",
153
- start_line: 1,
154
- end_line: nil,
155
- start_column: 0,
156
- end_column: nil,
157
- start_byte: 0,
158
- end_byte: nil,
159
- children: [],
160
- source: nil
161
- )
162
- # Convert 1-based line to 0-based row
163
- start_row = start_line - 1
164
- end_row = end_line ? end_line - 1 : start_row + text.count("\n")
165
-
166
- # Calculate end_column if not provided
167
- if end_column.nil?
168
- lines = text.split("\n", -1)
169
- end_column = lines.last&.length || 0
170
- end
171
-
172
- # Build children as MockInnerNodes
173
- child_nodes = children.map do |child_spec|
174
- MockInnerNode.new(**child_spec)
175
- end
176
-
177
- inner = MockInnerNode.new(
178
- type: type,
179
- text: text,
180
- start_byte: start_byte,
181
- end_byte: end_byte,
182
- start_row: start_row,
183
- start_column: start_column,
184
- end_row: end_row,
185
- end_column: end_column,
186
- children: child_nodes,
187
- )
188
-
189
- # Create a real TreeHaver::Node wrapping our mock
190
- # Pass source so TreeHaver::Node can extract text if needed
191
- new(inner, source: source || text)
192
- end
193
-
194
- # Create multiple nodes from an array of specifications.
195
- #
196
- # @param specs [Array<Hash>] Array of node specifications
197
- # @return [Array<TestableNode>]
198
- def create_list(*specs)
199
- specs.flatten.map { |spec| create(**spec) }
200
- end
201
- end
202
-
203
- # Additional test helper methods
204
-
205
- # Check if this is a testable node (for test assertions)
206
- #
207
- # @return [Boolean] true
208
- def testable?
209
- true
210
- end
211
- end
212
- end
213
- end
214
-
215
- # Make TestableNode available at top level for convenience in specs.
216
- # This allows specs to use `TestableNode.create(...)` without the full namespace.
217
- TestableNode = TreeHaver::RSpec::TestableNode
@@ -1,33 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # TreeHaver RSpec Integration
4
- #
5
- # This file provides RSpec helpers and configuration for testing
6
- # code that uses TreeHaver. Require this in your spec_helper.rb:
7
- #
8
- # require "tree_haver/rspec"
9
- #
10
- # This will load:
11
- # - Dependency tags for conditional test execution
12
- # - TestableNode for creating mock nodes in tests
13
- #
14
- # @example spec_helper.rb
15
- # require "tree_haver/rspec"
16
- #
17
- # RSpec.configure do |config|
18
- # # Your additional configuration...
19
- # end
20
- #
21
- # @example Using TestableNode
22
- # node = TestableNode.create(
23
- # type: :heading,
24
- # text: "## My Heading",
25
- # start_line: 1
26
- # )
27
- # expect(node.type).to eq("heading")
28
- #
29
- # @see TreeHaver::RSpec::DependencyTags
30
- # @see TreeHaver::RSpec::TestableNode
31
-
32
- require_relative "rspec/dependency_tags"
33
- require_relative "rspec/testable_node"
@@ -1,258 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module TreeHaver
4
- # Unified Tree wrapper providing a consistent API across all backends
5
- #
6
- # This class wraps backend-specific tree objects and provides a unified interface.
7
- # It stores the source text to enable text extraction from nodes.
8
- #
9
- # == Wrapping/Unwrapping Contract
10
- #
11
- # TreeHaver follows a consistent pattern for object wrapping:
12
- #
13
- # 1. **TreeHaver::Parser** (top level) handles ALL wrapping/unwrapping
14
- # 2. **Backends** work exclusively with raw backend objects
15
- # 3. **User-facing API** uses only TreeHaver wrapper classes
16
- #
17
- # Specifically for trees:
18
- # - Backend Parser#parse returns raw backend tree (TreeSitter::Tree, TreeStump::Tree, etc.)
19
- # - TreeHaver::Parser#parse wraps it in TreeHaver::Tree
20
- # - TreeHaver::Parser#parse_string unwraps old_tree before passing to backend
21
- # - Backend Parser#parse_string receives raw backend tree, returns raw backend tree
22
- # - TreeHaver::Parser#parse_string wraps the returned tree
23
- #
24
- # This ensures:
25
- # - Backends are simple and consistent
26
- # - All complexity is in one place (TreeHaver top level)
27
- # - Users always work with TreeHaver wrapper classes
28
- #
29
- # @example Basic usage
30
- # parser = TreeHaver::Parser.new
31
- # parser.language = TreeHaver::Language.toml
32
- # tree = parser.parse(source)
33
- # root = tree.root_node
34
- # puts root.type
35
- #
36
- # @example Incremental parsing (if backend supports it)
37
- # tree = parser.parse("x = 1")
38
- # # Edit the source: "x = 1" → "x = 42"
39
- # tree.edit(
40
- # start_byte: 4,
41
- # old_end_byte: 5,
42
- # new_end_byte: 6,
43
- # start_point: { row: 0, column: 4 },
44
- # old_end_point: { row: 0, column: 5 },
45
- # new_end_point: { row: 0, column: 6 }
46
- # )
47
- # new_tree = parser.parse_string(tree, "x = 42")
48
- #
49
- # @example Accessing backend-specific features
50
- # # Via passthrough (method_missing delegates to inner_tree)
51
- # tree.some_backend_specific_method # Automatically delegated
52
- #
53
- # # Or explicitly via inner_tree
54
- # tree.inner_tree.some_backend_specific_method
55
- class Tree < Base::Tree
56
- # The wrapped backend-specific tree object
57
- #
58
- # This provides direct access to the underlying backend tree for advanced usage
59
- # when you need backend-specific features not exposed by the unified API.
60
- #
61
- # @return [Object] The underlying tree (TreeSitter::Tree, TreeStump::Tree, etc.)
62
- # @example Accessing backend-specific methods
63
- # # Print DOT graph (TreeStump-specific)
64
- # if tree.inner_tree.respond_to?(:print_dot_graph)
65
- # File.open("tree.dot", "w") do |f|
66
- # tree.inner_tree.print_dot_graph(f)
67
- # end
68
- # end
69
- # NOTE: inner_tree is inherited from Base::Tree
70
-
71
- # The source text
72
- #
73
- # Stored to enable text extraction from nodes via byte offsets.
74
- #
75
- # @return [String] The original source code
76
- # NOTE: source is inherited from Base::Tree
77
-
78
- # @param tree [Object] Backend-specific tree object
79
- # @param source [String] Source text for node text extraction
80
- def initialize(tree, source: nil)
81
- super(tree, source: source)
82
- end
83
-
84
- # Get the root node of the tree
85
- #
86
- # @return [Node] Wrapped root node
87
- def root_node
88
- root = @inner_tree.root_node
89
- return if root.nil?
90
- Node.new(root, source: @source)
91
- end
92
-
93
- # Mark the tree as edited for incremental re-parsing
94
- #
95
- # Call this method after the source code has been modified but before
96
- # re-parsing. This tells tree-sitter which parts of the tree are
97
- # invalidated so it can efficiently re-parse only the affected regions.
98
- #
99
- # Not all backends support incremental parsing. Use {#supports_editing?}
100
- # to check before calling this method.
101
- #
102
- # @param start_byte [Integer] byte offset where the edit starts
103
- # @param old_end_byte [Integer] byte offset where the old text ended
104
- # @param new_end_byte [Integer] byte offset where the new text ends
105
- # @param start_point [Hash] starting position as `{ row:, column: }`
106
- # @param old_end_point [Hash] old ending position as `{ row:, column: }`
107
- # @param new_end_point [Hash] new ending position as `{ row:, column: }`
108
- # @return [void]
109
- # @raise [TreeHaver::NotAvailable] if the backend doesn't support incremental parsing
110
- # @see https://tree-sitter.github.io/tree-sitter/using-parsers#editing
111
- #
112
- # @example Incremental parsing workflow
113
- # # Original source: "x = 1"
114
- # tree = parser.parse("x = 1")
115
- #
116
- # # Edit the source: replace "1" with "42" at byte offset 4
117
- # tree.edit(
118
- # start_byte: 4,
119
- # old_end_byte: 5, # "1" ends at byte 5
120
- # new_end_byte: 6, # "42" ends at byte 6
121
- # start_point: { row: 0, column: 4 },
122
- # old_end_point: { row: 0, column: 5 },
123
- # new_end_point: { row: 0, column: 6 }
124
- # )
125
- #
126
- # # Re-parse with the edited tree for incremental parsing
127
- # new_tree = parser.parse_string(tree, "x = 42")
128
- def edit(start_byte:, old_end_byte:, new_end_byte:, start_point:, old_end_point:, new_end_point:)
129
- # MRI backend (ruby_tree_sitter) requires an InputEdit object
130
- if defined?(::TreeSitter::InputEdit) && @inner_tree.is_a?(::TreeSitter::Tree)
131
- input_edit = ::TreeSitter::InputEdit.new
132
- input_edit.start_byte = start_byte
133
- input_edit.old_end_byte = old_end_byte
134
- input_edit.new_end_byte = new_end_byte
135
-
136
- # Convert hash points to Point objects if needed
137
- input_edit.start_point = make_point(start_point)
138
- input_edit.old_end_point = make_point(old_end_point)
139
- input_edit.new_end_point = make_point(new_end_point)
140
-
141
- @inner_tree.edit(input_edit)
142
- else
143
- # Other backends may accept keyword arguments directly
144
- @inner_tree.edit(
145
- start_byte: start_byte,
146
- old_end_byte: old_end_byte,
147
- new_end_byte: new_end_byte,
148
- start_point: start_point,
149
- old_end_point: old_end_point,
150
- new_end_point: new_end_point,
151
- )
152
- end
153
- rescue NoMethodError => e
154
- # Re-raise as NotAvailable if it's about the edit method
155
- raise unless e.name == :edit || e.message.include?("edit")
156
- raise TreeHaver::NotAvailable,
157
- "Incremental parsing not supported by current backend. " \
158
- "Use MRI (ruby_tree_sitter), Rust (tree_stump), or Java (java-tree-sitter / jtreesitter) backend."
159
- end
160
-
161
- private
162
-
163
- # Convert a point hash to a TreeSitter::Point if available
164
- # @api private
165
- def make_point(point_hash)
166
- if defined?(::TreeSitter::Point)
167
- pt = ::TreeSitter::Point.new
168
- pt.row = point_hash[:row]
169
- pt.column = point_hash[:column]
170
- pt
171
- else
172
- point_hash
173
- end
174
- end
175
-
176
- public
177
-
178
- # Check if the current backend supports incremental parsing
179
- #
180
- # Incremental parsing allows tree-sitter to reuse unchanged nodes when
181
- # re-parsing edited source code, improving performance for large files
182
- # with small edits.
183
- #
184
- # @return [Boolean] true if {#edit} can be called on this tree
185
- # @example
186
- # if tree.supports_editing?
187
- # tree.edit(...)
188
- # new_tree = parser.parse_string(tree, edited_source)
189
- # else
190
- # # Fall back to full re-parse
191
- # new_tree = parser.parse(edited_source)
192
- # end
193
- def supports_editing?
194
- # Try to get the edit method to verify it exists
195
- # This is more reliable than respond_to? with Delegator wrappers
196
- @inner_tree.method(:edit)
197
- true
198
- rescue NameError
199
- # NameError is the parent class of NoMethodError, so this catches both
200
- false
201
- end
202
-
203
- # String representation
204
- # @return [String]
205
- def inspect
206
- inner_class = @inner_tree ? @inner_tree.class.name : "nil"
207
- "#<#{self.class} source_length=#{@source&.bytesize || "unknown"} inner_tree=#{inner_class}>"
208
- end
209
-
210
- # Check if tree responds to a method (includes delegation to inner_tree)
211
- #
212
- # @param method_name [Symbol] method to check
213
- # @param include_private [Boolean] include private methods
214
- # @return [Boolean]
215
- def respond_to_missing?(method_name, include_private = false)
216
- @inner_tree.respond_to?(method_name, include_private) || super
217
- end
218
-
219
- # Delegate unknown methods to the underlying backend-specific tree
220
- #
221
- # This provides passthrough access for advanced usage when you need
222
- # backend-specific features not exposed by TreeHaver's unified API.
223
- #
224
- # The delegation is automatic and transparent - you can call backend-specific
225
- # methods directly on the TreeHaver::Tree and they'll be forwarded to the
226
- # underlying tree implementation.
227
- #
228
- # @param method_name [Symbol] method to call
229
- # @param args [Array] arguments to pass
230
- # @param block [Proc] block to pass
231
- # @return [Object] result from the underlying tree
232
- #
233
- # @example Using TreeStump-specific methods
234
- # # print_dot_graph is TreeStump-specific
235
- # File.open("tree.dot", "w") do |f|
236
- # tree.print_dot_graph(f) # Delegated to inner_tree
237
- # end
238
- #
239
- # @example Safe usage with respond_to? check
240
- # if tree.respond_to?(:print_dot_graph)
241
- # File.open("tree.dot", "w") { |f| tree.print_dot_graph(f) }
242
- # end
243
- #
244
- # @example Equivalent explicit access
245
- # tree.print_dot_graph(file) # Via passthrough (method_missing)
246
- # tree.inner_tree.print_dot_graph(file) # Explicit access (same result)
247
- #
248
- # @note This maintains backward compatibility with code written for
249
- # specific backends while providing the benefits of the unified API
250
- def method_missing(method_name, *args, **kwargs, &block)
251
- if @inner_tree.respond_to?(method_name)
252
- @inner_tree.public_send(method_name, *args, **kwargs, &block)
253
- else
254
- super
255
- end
256
- end
257
- end
258
- end