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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +195 -2
- data/README.md +498 -357
- data/lib/tree_haver/backend_registry.rb +230 -7
- data/lib/tree_haver/backends/citrus.rb +98 -114
- data/lib/tree_haver/backends/ffi.rb +76 -13
- data/lib/tree_haver/backends/java.rb +99 -14
- data/lib/tree_haver/backends/mri.rb +25 -1
- data/lib/tree_haver/backends/parslet.rb +560 -0
- data/lib/tree_haver/backends/prism.rb +1 -1
- data/lib/tree_haver/backends/psych.rb +1 -1
- data/lib/tree_haver/backends/rust.rb +1 -1
- data/lib/tree_haver/base/node.rb +8 -1
- data/lib/tree_haver/language.rb +44 -13
- data/lib/tree_haver/parser.rb +127 -35
- data/lib/tree_haver/parslet_grammar_finder.rb +224 -0
- data/lib/tree_haver/point.rb +6 -44
- data/lib/tree_haver/rspec/dependency_tags.rb +148 -81
- data/lib/tree_haver/rspec/testable_node.rb +217 -0
- data/lib/tree_haver/rspec.rb +11 -1
- data/lib/tree_haver/version.rb +1 -1
- data/lib/tree_haver.rb +100 -13
- data.tar.gz.sig +0 -0
- metadata +16 -14
- metadata.gz.sig +0 -0
data/lib/tree_haver/point.rb
CHANGED
|
@@ -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
|
-
|
|
22
|
-
|
|
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
|
-
#
|
|
534
|
+
# ============================================================
|
|
535
|
+
# Dynamic Backend Availability (via BackendRegistry)
|
|
536
|
+
# ============================================================
|
|
535
537
|
#
|
|
536
|
-
#
|
|
538
|
+
# External gems register tags with BackendRegistry.register_tag which
|
|
539
|
+
# dynamically defines *_available? methods on this module.
|
|
537
540
|
#
|
|
538
|
-
# @
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
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
|
-
#
|
|
548
|
+
# # The registration automatically defines:
|
|
549
|
+
# TreeHaver::RSpec::DependencyTags.my_backend_available? # => true/false
|
|
547
550
|
#
|
|
548
|
-
#
|
|
549
|
-
|
|
550
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
#
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
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
|
data/lib/tree_haver/rspec.rb
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
#
|
|
10
10
|
# This will load:
|
|
11
11
|
# - Dependency tags for conditional test execution
|
|
12
|
-
# -
|
|
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"
|