tree_haver 4.0.5 → 5.0.1

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.
@@ -7,6 +7,9 @@ module TreeHaver
7
7
  # - Hash access: point[:row], point[:column]
8
8
  # - Method access: point.row, point.column
9
9
  #
10
+ # TreeHaver::Point is an alias for TreeHaver::Base::Point, which is a Struct
11
+ # providing all the necessary functionality.
12
+ #
10
13
  # @example Method access
11
14
  # point = TreeHaver::Point.new(5, 10)
12
15
  # point.row # => 5
@@ -18,48 +21,7 @@ module TreeHaver
18
21
  #
19
22
  # @example Converting to hash
20
23
  # point.to_h # => {row: 5, column: 10}
21
- class Point
22
- attr_reader :row, :column
23
-
24
- # Create a new Point
25
- #
26
- # @param row [Integer] the row (line) number, 0-indexed
27
- # @param column [Integer] the column number, 0-indexed
28
- def initialize(row, column)
29
- @row = row
30
- @column = column
31
- end
32
-
33
- # Hash-like access for compatibility
34
- #
35
- # @param key [Symbol, String] :row or :column
36
- # @return [Integer, nil] the value or nil if key not recognized
37
- def [](key)
38
- case key
39
- when :row, "row" then @row
40
- when :column, "column" then @column
41
- end
42
- end
43
-
44
- # Convert to a hash
45
- #
46
- # @return [Hash{Symbol => Integer}]
47
- def to_h
48
- {row: @row, column: @column}
49
- end
50
-
51
- # String representation
52
- #
53
- # @return [String]
54
- def to_s
55
- "(#{@row}, #{@column})"
56
- end
57
-
58
- # Inspect representation
59
- #
60
- # @return [String]
61
- def inspect
62
- "#<TreeHaver::Point row=#{@row} column=#{@column}>"
63
- end
64
- end
24
+ #
25
+ # @see Base::Point The underlying Struct implementation
26
+ Point = Base::Point
65
27
  end
@@ -162,7 +162,7 @@ require "set"
162
162
  # ==== Language Parsing Capability Tags (*_parsing)
163
163
  #
164
164
  # [:toml_parsing]
165
- # At least one TOML parser (tree-sitter-toml OR toml-rb/Citrus) is available.
165
+ # At least one TOML parser (tree-sitter-toml OR toml-rb/Citrus OR toml/Parslet) is available.
166
166
  #
167
167
  # [:markdown_parsing]
168
168
  # At least one markdown parser (commonmarker OR markly) is available.
@@ -531,25 +531,26 @@ module TreeHaver
531
531
  nil
532
532
  end
533
533
 
534
- # Check if commonmarker gem is available
534
+ # ============================================================
535
+ # Dynamic Backend Availability (via BackendRegistry)
536
+ # ============================================================
535
537
  #
536
- # Uses BackendRegistry which allows commonmarker-merge to register its checker.
538
+ # External gems register tags with BackendRegistry.register_tag which
539
+ # dynamically defines *_available? methods on this module.
537
540
  #
538
- # @return [Boolean] true if commonmarker gem is available
539
- def commonmarker_available?
540
- return @commonmarker_available if defined?(@commonmarker_available)
541
- @commonmarker_available = TreeHaver::BackendRegistry.available?(:commonmarker)
542
- end
543
-
544
- # Check if markly gem is available
541
+ # @example External gem registers a tag
542
+ # TreeHaver::BackendRegistry.register_tag(
543
+ # :my_backend_backend,
544
+ # category: :backend,
545
+ # require_path: "my_backend/merge"
546
+ # ) { MyBackend::Merge::Backend.available? }
545
547
  #
546
- # Uses BackendRegistry which allows markly-merge to register its checker.
548
+ # # The registration automatically defines:
549
+ # TreeHaver::RSpec::DependencyTags.my_backend_available? # => true/false
547
550
  #
548
- # @return [Boolean] true if markly gem is available
549
- def markly_available?
550
- return @markly_available if defined?(@markly_available)
551
- @markly_available = TreeHaver::BackendRegistry.available?(:markly)
552
- end
551
+ # Built-in backends (prism, psych, citrus, parslet) have explicit methods
552
+ # defined below. External backends get methods defined dynamically when
553
+ # their gem calls register_tag.
553
554
 
554
555
  # Check if prism gem is available
555
556
  #
@@ -577,6 +578,16 @@ module TreeHaver
577
578
  @citrus_available = TreeHaver::BackendRegistry.available?(:citrus)
578
579
  end
579
580
 
581
+ # Check if Parslet backend is available
582
+ #
583
+ # This checks if the parslet gem is installed and the backend works.
584
+ #
585
+ # @return [Boolean] true if Parslet backend is available
586
+ def parslet_available?
587
+ return @parslet_available if defined?(@parslet_available)
588
+ @parslet_available = TreeHaver::BackendRegistry.available?(:parslet)
589
+ end
590
+
580
591
  # ============================================================
581
592
  # Ruby Engine Detection
582
593
  # ============================================================
@@ -697,18 +708,44 @@ module TreeHaver
697
708
  end
698
709
  end
699
710
 
711
+ # Check if toml gem is available and functional (Parslet backend for TOML)
712
+ #
713
+ # @return [Boolean] true if toml gem is available and can parse TOML
714
+ def toml_gem_available?
715
+ return @toml_gem_available if defined?(@toml_gem_available)
716
+ @toml_gem_available = begin
717
+ require "toml"
718
+ # Verify it can actually parse - just requiring isn't enough
719
+ source_toml = <<~TOML
720
+ # My Information
721
+ [machine]
722
+ host = "localhost"
723
+ TOML
724
+ TOML.load(source_toml)
725
+ true
726
+ rescue LoadError
727
+ false
728
+ rescue StandardError
729
+ false
730
+ end
731
+ end
732
+
700
733
  # Check if at least one TOML backend is available
701
734
  #
702
735
  # @return [Boolean] true if any TOML backend works
703
736
  def any_toml_backend_available?
704
- tree_sitter_toml_available? || toml_rb_gem_available?
737
+ tree_sitter_toml_available? || toml_rb_gem_available? || toml_gem_available?
705
738
  end
706
739
 
707
740
  # Check if at least one markdown backend is available
708
741
  #
742
+ # Uses BackendRegistry.tag_available? to check external backends that may
743
+ # not have their methods defined yet (registered by external gems).
744
+ #
709
745
  # @return [Boolean] true if any markdown backend works
710
746
  def any_markdown_backend_available?
711
- markly_available? || commonmarker_available?
747
+ TreeHaver::BackendRegistry.tag_available?(:markly_backend) ||
748
+ TreeHaver::BackendRegistry.tag_available?(:commonmarker_backend)
712
749
  end
713
750
 
714
751
  def any_native_grammar_available?
@@ -780,44 +817,69 @@ module TreeHaver
780
817
  # Use stored blocked_backends if available, otherwise compute dynamically
781
818
  blocked = @blocked_backends || compute_blocked_backends
782
819
 
783
- {
820
+ result = {
784
821
  # Backend selection from environment variables
785
822
  selected_backend: selected_backend,
786
823
  allowed_native_backends: allowed_native_backends,
787
824
  allowed_ruby_backends: allowed_ruby_backends,
788
- # TreeHaver backends (*_backend) - skip blocked backends to avoid loading them
789
- ffi_backend: blocked.include?(:ffi) ? :blocked : ffi_available?,
790
- mri_backend: blocked.include?(:mri) ? :blocked : mri_backend_available?,
791
- rust_backend: blocked.include?(:rust) ? :blocked : rust_backend_available?,
792
- java_backend: blocked.include?(:java) ? :blocked : java_backend_available?,
793
- prism_backend: blocked.include?(:prism) ? :blocked : prism_available?,
794
- psych_backend: blocked.include?(:psych) ? :blocked : psych_available?,
795
- commonmarker_backend: blocked.include?(:commonmarker) ? :blocked : commonmarker_available?,
796
- markly_backend: blocked.include?(:markly) ? :blocked : markly_available?,
797
- citrus_backend: blocked.include?(:citrus) ? :blocked : citrus_available?,
798
- rbs_backend: blocked.include?(:rbs) ? :blocked : rbs_backend_available?,
799
- # Ruby engines (*_engine)
800
- ruby_engine: RUBY_ENGINE,
801
- mri_engine: mri?,
802
- jruby_engine: jruby?,
803
- truffleruby_engine: truffleruby?,
804
- # Tree-sitter grammars (*_grammar) - also respect blocked backends
805
- # since grammar checks may load backends
806
- libtree_sitter: libtree_sitter_available?,
807
- bash_grammar: blocked.include?(:mri) ? :blocked : tree_sitter_bash_available?,
808
- toml_grammar: blocked.include?(:mri) ? :blocked : tree_sitter_toml_available?,
809
- json_grammar: blocked.include?(:mri) ? :blocked : tree_sitter_json_available?,
810
- jsonc_grammar: blocked.include?(:mri) ? :blocked : tree_sitter_jsonc_available?,
811
- rbs_grammar: blocked.include?(:mri) ? :blocked : tree_sitter_rbs_available?,
812
- any_native_grammar: blocked.include?(:mri) ? :blocked : any_native_grammar_available?,
813
- # Language parsing capabilities (*_parsing)
814
- toml_parsing: any_toml_backend_available?,
815
- markdown_parsing: any_markdown_backend_available?,
816
- rbs_parsing: any_rbs_backend_available?,
817
- # Specific libraries (*_gem)
818
- toml_rb_gem: toml_rb_gem_available?,
819
- rbs_gem: rbs_gem_available?,
820
825
  }
826
+
827
+ # Built-in TreeHaver backends (*_backend) - skip blocked backends to avoid loading them
828
+ builtin_backends = {
829
+ ffi: :ffi_available?,
830
+ mri: :mri_backend_available?,
831
+ rust: :rust_backend_available?,
832
+ java: :java_backend_available?,
833
+ prism: :prism_available?,
834
+ psych: :psych_available?,
835
+ citrus: :citrus_available?,
836
+ parslet: :parslet_available?,
837
+ rbs: :rbs_backend_available?,
838
+ }
839
+
840
+ builtin_backends.each do |backend, method|
841
+ tag = :"#{backend}_backend"
842
+ result[tag] = blocked.include?(backend) ? :blocked : public_send(method)
843
+ end
844
+
845
+ # Dynamically registered backends from BackendRegistry
846
+ TreeHaver::BackendRegistry.registered_tags.each do |tag_name|
847
+ next if result.key?(tag_name) # Don't override built-ins
848
+
849
+ meta = TreeHaver::BackendRegistry.tag_metadata(tag_name)
850
+ next unless meta && meta[:category] == :backend
851
+
852
+ backend = meta[:backend_name]
853
+ result[tag_name] = blocked.include?(backend) ? :blocked : TreeHaver::BackendRegistry.tag_available?(tag_name)
854
+ end
855
+
856
+ # Ruby engines (*_engine)
857
+ result[:ruby_engine] = RUBY_ENGINE
858
+ result[:mri_engine] = mri?
859
+ result[:jruby_engine] = jruby?
860
+ result[:truffleruby_engine] = truffleruby?
861
+
862
+ # Tree-sitter grammars (*_grammar) - also respect blocked backends
863
+ # since grammar checks may load backends
864
+ result[:libtree_sitter] = libtree_sitter_available?
865
+ result[:bash_grammar] = blocked.include?(:mri) ? :blocked : tree_sitter_bash_available?
866
+ result[:toml_grammar] = blocked.include?(:mri) ? :blocked : tree_sitter_toml_available?
867
+ result[:json_grammar] = blocked.include?(:mri) ? :blocked : tree_sitter_json_available?
868
+ result[:jsonc_grammar] = blocked.include?(:mri) ? :blocked : tree_sitter_jsonc_available?
869
+ result[:rbs_grammar] = blocked.include?(:mri) ? :blocked : tree_sitter_rbs_available?
870
+ result[:any_native_grammar] = blocked.include?(:mri) ? :blocked : any_native_grammar_available?
871
+
872
+ # Language parsing capabilities (*_parsing)
873
+ result[:toml_parsing] = any_toml_backend_available?
874
+ result[:markdown_parsing] = any_markdown_backend_available?
875
+ result[:rbs_parsing] = any_rbs_backend_available?
876
+
877
+ # Specific libraries (*_gem)
878
+ result[:toml_rb_gem] = toml_rb_gem_available?
879
+ result[:toml_gem] = toml_gem_available?
880
+ result[:rbs_gem] = rbs_gem_available?
881
+
882
+ result
821
883
  end
822
884
 
823
885
  # Get environment variable summary for debugging
@@ -873,7 +935,7 @@ module TreeHaver
873
935
  # @param test_source [String] sample source code to parse
874
936
  # @return [Boolean] true if parsing works without errors
875
937
  def grammar_works?(language, test_source)
876
- debug = ENV["TREE_HAVER_DEBUG"]
938
+ debug = !ENV.fetch("TREE_HAVER_DEBUG", "false").casecmp?("false")
877
939
  env_var = "TREE_SITTER_#{language.to_s.upcase}_PATH"
878
940
  env_value = ENV[env_var]
879
941
 
@@ -938,7 +1000,7 @@ RSpec.configure do |config|
938
1000
 
939
1001
  config.before(:suite) do
940
1002
  # Print dependency summary if TREE_HAVER_DEBUG is set
941
- if ENV["TREE_HAVER_DEBUG"]
1003
+ unless ENV.fetch("TREE_HAVER_DEBUG", "false").casecmp?("false")
942
1004
  puts "\n=== TreeHaver Environment Variables ==="
943
1005
  deps.env_summary.each do |var, value|
944
1006
  puts " #{var}: #{value.inspect}"
@@ -1029,33 +1091,35 @@ RSpec.configure do |config|
1029
1091
  #
1030
1092
  # This is dynamic based on TreeHaver::Backends::BLOCKED_BY configuration.
1031
1093
 
1032
- # Map of backend symbols to their availability check methods
1033
- backend_availability_methods = {
1034
- mri: :mri_backend_available?,
1035
- rust: :rust_backend_available?,
1036
- ffi: :ffi_available?,
1037
- java: :java_backend_available?,
1038
- prism: :prism_available?,
1039
- psych: :psych_available?,
1040
- commonmarker: :commonmarker_available?,
1041
- markly: :markly_available?,
1042
- citrus: :citrus_available?,
1043
- rbs: :rbs_backend_available?,
1044
- }
1045
-
1046
- # Map of backend symbols to their RSpec tag names
1047
- backend_tags = {
1048
- mri: :mri_backend,
1049
- rust: :rust_backend,
1050
- ffi: :ffi_backend,
1051
- java: :java_backend,
1052
- prism: :prism_backend,
1053
- psych: :psych_backend,
1054
- commonmarker: :commonmarker_backend,
1055
- markly: :markly_backend,
1056
- citrus: :citrus_backend,
1057
- rbs: :rbs_backend,
1058
- }
1094
+ # Build backend maps dynamically from BackendRegistry and built-in backends
1095
+ # This allows external gems to register and automatically get tag support
1096
+ backend_availability_methods = {}
1097
+ backend_tags = {}
1098
+
1099
+ # Built-in backends (always present in tree_haver)
1100
+ builtin_backends = %i[mri rust ffi java prism psych citrus parslet rbs]
1101
+ builtin_backends.each do |backend|
1102
+ # Special case for ffi which uses ffi_available? not ffi_backend_available?
1103
+ availability_method = (backend == :ffi) ? :ffi_available? : :"#{backend}_available?"
1104
+ # Special case for backends that use *_backend_available? naming
1105
+ availability_method = :"#{backend}_backend_available?" if %i[mri rust java rbs].include?(backend)
1106
+
1107
+ backend_availability_methods[backend] = availability_method
1108
+ backend_tags[backend] = :"#{backend}_backend"
1109
+ end
1110
+
1111
+ # Add dynamically registered backends from BackendRegistry
1112
+ # This picks up external gems like commonmarker-merge, markly-merge, etc.
1113
+ TreeHaver::BackendRegistry.registered_tags.each do |tag_name|
1114
+ meta = TreeHaver::BackendRegistry.tag_metadata(tag_name)
1115
+ next unless meta && meta[:category] == :backend
1116
+
1117
+ backend_name = meta[:backend_name]
1118
+ next if backend_availability_methods.key?(backend_name) # Don't override built-ins
1119
+
1120
+ backend_availability_methods[backend_name] = :"#{backend_name}_available?"
1121
+ backend_tags[backend_name] = tag_name
1122
+ end
1059
1123
 
1060
1124
  # Determine which backends should NOT have availability checked
1061
1125
  # based on which *_backend_only tag is being run OR which backend is
@@ -1183,7 +1247,7 @@ RSpec.configure do |config|
1183
1247
  # Language Parsing Capability Tags
1184
1248
  # ============================================================
1185
1249
  # Tags: *_parsing - require ANY parser for a language (any backend that can parse it)
1186
- # :toml_parsing - any TOML parser (tree-sitter-toml OR toml-rb/Citrus)
1250
+ # :toml_parsing - any TOML parser (tree-sitter-toml OR toml-rb/Citrus OR toml/Parslet)
1187
1251
  # :markdown_parsing - any Markdown parser (commonmarker OR markly)
1188
1252
  # :rbs_parsing - any RBS parser (rbs gem OR tree-sitter-rbs)
1189
1253
  # :native_parsing - any native tree-sitter backend + grammar
@@ -1202,10 +1266,12 @@ RSpec.configure do |config|
1202
1266
  # Specific Library Tags
1203
1267
  # ============================================================
1204
1268
  # Tags for specific gems/libraries (*_gem suffix)
1269
+ # :toml_gem - the toml gem (Parslet-based TOML parser)
1205
1270
  # :toml_rb_gem - the toml-rb gem (Citrus-based TOML parser)
1206
1271
  # :rbs_gem - the rbs gem (official RBS parser, MRI only)
1207
1272
  # Note: :rbs_backend is also available as an alias for :rbs_gem
1208
1273
 
1274
+ config.filter_run_excluding(toml_gem: true) unless deps.toml_gem_available?
1209
1275
  config.filter_run_excluding(toml_rb_gem: true) unless deps.toml_rb_gem_available?
1210
1276
  config.filter_run_excluding(rbs_gem: true) unless deps.rbs_gem_available?
1211
1277
 
@@ -1249,6 +1315,7 @@ RSpec.configure do |config|
1249
1315
  end
1250
1316
 
1251
1317
  # Specific libraries
1318
+ config.filter_run_excluding(not_toml_gem: true) if deps.toml_gem_available?
1252
1319
  config.filter_run_excluding(not_toml_rb_gem: true) if deps.toml_rb_gem_available?
1253
1320
  config.filter_run_excluding(not_rbs_gem: true) if deps.rbs_gem_available?
1254
1321
  end
@@ -0,0 +1,217 @@
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
@@ -9,7 +9,7 @@
9
9
  #
10
10
  # This will load:
11
11
  # - Dependency tags for conditional test execution
12
- # - (Future) Additional test helpers as needed
12
+ # - TestableNode for creating mock nodes in tests
13
13
  #
14
14
  # @example spec_helper.rb
15
15
  # require "tree_haver/rspec"
@@ -18,6 +18,16 @@
18
18
  # # Your additional configuration...
19
19
  # end
20
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
+ #
21
29
  # @see TreeHaver::RSpec::DependencyTags
30
+ # @see TreeHaver::RSpec::TestableNode
22
31
 
23
32
  require_relative "rspec/dependency_tags"
33
+ require_relative "rspec/testable_node"
@@ -10,7 +10,7 @@ module TreeHaver
10
10
  # Current version of the tree_haver gem
11
11
  #
12
12
  # @return [String] the version string
13
- VERSION = "4.0.5"
13
+ VERSION = "5.0.1"
14
14
  end
15
15
 
16
16
  # Traditional location for VERSION constant