ruby_tree_sitter 1.6.0-arm-linux-gnu
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 +7 -0
- data/LICENSE +22 -0
- data/README.md +213 -0
- data/ext/tree_sitter/encoding.c +29 -0
- data/ext/tree_sitter/extconf.rb +149 -0
- data/ext/tree_sitter/input.c +127 -0
- data/ext/tree_sitter/input_edit.c +42 -0
- data/ext/tree_sitter/language.c +219 -0
- data/ext/tree_sitter/logger.c +228 -0
- data/ext/tree_sitter/macros.h +163 -0
- data/ext/tree_sitter/node.c +623 -0
- data/ext/tree_sitter/parser.c +398 -0
- data/ext/tree_sitter/point.c +26 -0
- data/ext/tree_sitter/quantifier.c +43 -0
- data/ext/tree_sitter/query.c +289 -0
- data/ext/tree_sitter/query_capture.c +28 -0
- data/ext/tree_sitter/query_cursor.c +231 -0
- data/ext/tree_sitter/query_error.c +41 -0
- data/ext/tree_sitter/query_match.c +44 -0
- data/ext/tree_sitter/query_predicate_step.c +83 -0
- data/ext/tree_sitter/range.c +35 -0
- data/ext/tree_sitter/repo.rb +128 -0
- data/ext/tree_sitter/symbol_type.c +46 -0
- data/ext/tree_sitter/tree.c +234 -0
- data/ext/tree_sitter/tree_cursor.c +269 -0
- data/ext/tree_sitter/tree_sitter.c +44 -0
- data/ext/tree_sitter/tree_sitter.h +107 -0
- data/lib/tree_sitter/3.0/tree_sitter.so +0 -0
- data/lib/tree_sitter/3.1/tree_sitter.so +0 -0
- data/lib/tree_sitter/3.2/tree_sitter.so +0 -0
- data/lib/tree_sitter/3.3/tree_sitter.so +0 -0
- data/lib/tree_sitter/helpers.rb +23 -0
- data/lib/tree_sitter/mixins/language.rb +167 -0
- data/lib/tree_sitter/node.rb +167 -0
- data/lib/tree_sitter/query.rb +191 -0
- data/lib/tree_sitter/query_captures.rb +30 -0
- data/lib/tree_sitter/query_cursor.rb +27 -0
- data/lib/tree_sitter/query_match.rb +100 -0
- data/lib/tree_sitter/query_matches.rb +39 -0
- data/lib/tree_sitter/query_predicate.rb +14 -0
- data/lib/tree_sitter/text_predicate_capture.rb +37 -0
- data/lib/tree_sitter/version.rb +8 -0
- data/lib/tree_sitter.rb +34 -0
- data/lib/tree_stand/ast_modifier.rb +30 -0
- data/lib/tree_stand/breadth_first_visitor.rb +54 -0
- data/lib/tree_stand/config.rb +19 -0
- data/lib/tree_stand/node.rb +351 -0
- data/lib/tree_stand/parser.rb +87 -0
- data/lib/tree_stand/range.rb +55 -0
- data/lib/tree_stand/tree.rb +123 -0
- data/lib/tree_stand/utils/printer.rb +73 -0
- data/lib/tree_stand/version.rb +7 -0
- data/lib/tree_stand/visitor.rb +127 -0
- data/lib/tree_stand/visitors/tree_walker.rb +37 -0
- data/lib/tree_stand.rb +48 -0
- data/tree_sitter.gemspec +34 -0
- metadata +135 -0
@@ -0,0 +1,167 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TreeSitter
|
4
|
+
# Node is a wrapper around a tree-sitter node.
|
5
|
+
class Node
|
6
|
+
include Enumerable
|
7
|
+
|
8
|
+
# @return [Array<Symbol>] the node's named fields
|
9
|
+
def fields
|
10
|
+
return @fields if @fields
|
11
|
+
|
12
|
+
@fields = Set.new
|
13
|
+
child_count.times do |i|
|
14
|
+
name = field_name_for_child(i)
|
15
|
+
@fields << name.to_sym if name
|
16
|
+
end
|
17
|
+
|
18
|
+
@fields.to_a
|
19
|
+
end
|
20
|
+
|
21
|
+
# @param field [String, Symbol]
|
22
|
+
def field?(field)
|
23
|
+
fields.include?(field.to_sym)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Access node's named children.
|
27
|
+
#
|
28
|
+
# It's similar to {#fetch}, but differs in input type, return values, and
|
29
|
+
# the internal implementation.
|
30
|
+
#
|
31
|
+
# Both of these methods exist for separate use cases, but also because
|
32
|
+
# sometime tree-sitter does some monkey business and having both separate
|
33
|
+
# implementations can help.
|
34
|
+
#
|
35
|
+
# Comparison with {#fetch}:
|
36
|
+
#
|
37
|
+
# [] | fetch
|
38
|
+
# ------------------------------+----------------------
|
39
|
+
# input types Integer, String, Symbol | Array<String, Symbol>
|
40
|
+
# Array<Integer, String, Symbol>|
|
41
|
+
# ------------------------------+----------------------
|
42
|
+
# returns 1-to-1 correspondance with | unique nodes
|
43
|
+
# input |
|
44
|
+
# ------------------------------+----------------------
|
45
|
+
# uses named_child | field_name_for_child
|
46
|
+
# child_by_field_name | via each_node
|
47
|
+
# ------------------------------+----------------------
|
48
|
+
#
|
49
|
+
# @param keys [Integer | String | Symbol | Array<Integer, String, Symbol>, #read]
|
50
|
+
#
|
51
|
+
# @return [Node | Array<Node>]
|
52
|
+
def [](*keys)
|
53
|
+
case keys.length
|
54
|
+
when 0 then raise ArgumentError, "#{self.class.name}##{__method__} requires a key."
|
55
|
+
when 1
|
56
|
+
case k = keys.first
|
57
|
+
when Integer then named_child(k)
|
58
|
+
when String, Symbol
|
59
|
+
raise IndexError, "Cannot find field #{k}. Available: #{fields}" unless fields.include?(k.to_sym)
|
60
|
+
|
61
|
+
child_by_field_name(k.to_s)
|
62
|
+
else raise ArgumentError, <<~ERR
|
63
|
+
#{self.class.name}##{__method__} accepts Integer and returns named child at given index,
|
64
|
+
or a (String | Symbol) and returns the child by given field name.
|
65
|
+
ERR
|
66
|
+
end
|
67
|
+
else
|
68
|
+
keys.map { |key| self[key] }
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# @!visibility private
|
73
|
+
#
|
74
|
+
# Allows access to child_by_field_name without using [].
|
75
|
+
def method_missing(method_name, *_args, &_block)
|
76
|
+
if fields.include?(method_name)
|
77
|
+
child_by_field_name(method_name.to_s)
|
78
|
+
else
|
79
|
+
super
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# @!visibility private
|
84
|
+
#
|
85
|
+
def respond_to_missing?(*args)
|
86
|
+
args.length == 1 && fields.include?(args[0])
|
87
|
+
end
|
88
|
+
|
89
|
+
# Iterate over a node's children.
|
90
|
+
#
|
91
|
+
# @yieldparam child [Node] the child
|
92
|
+
def each(&_block)
|
93
|
+
return enum_for __method__ if !block_given?
|
94
|
+
|
95
|
+
(0...child_count).each do |i|
|
96
|
+
yield child(i)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Iterate over a node's children assigned to a field.
|
101
|
+
#
|
102
|
+
# @yieldparam name [NilClass | String] field name.
|
103
|
+
# @yieldparam child [Node] the child.
|
104
|
+
def each_field
|
105
|
+
return enum_for __method__ if !block_given?
|
106
|
+
|
107
|
+
each.with_index do |c, i|
|
108
|
+
f = field_name_for_child(i)
|
109
|
+
next if f.nil? || f.empty?
|
110
|
+
|
111
|
+
yield f, c
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
# Iterate over a node's named children
|
116
|
+
#
|
117
|
+
# @yieldparam child [Node] the child
|
118
|
+
def each_named
|
119
|
+
return enum_for __method__ if !block_given?
|
120
|
+
|
121
|
+
(0...(named_child_count)).each do |i|
|
122
|
+
yield named_child(i)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# @return [Array<TreeSitter::Node>] all the node's children
|
127
|
+
def to_a
|
128
|
+
each.to_a
|
129
|
+
end
|
130
|
+
|
131
|
+
# Access node's named children.
|
132
|
+
#
|
133
|
+
# It's similar to {#[]}, but differs in input type, return values, and
|
134
|
+
# the internal implementation.
|
135
|
+
#
|
136
|
+
# Both of these methods exist for separate use cases, but also because
|
137
|
+
# sometime tree-sitter does some monkey business and having both separate
|
138
|
+
# implementations can help.
|
139
|
+
#
|
140
|
+
# Comparison with {#fetch}:
|
141
|
+
#
|
142
|
+
# [] | fetch
|
143
|
+
# ------------------------------+----------------------
|
144
|
+
# input types Integer, String, Symbol | String, Symbol
|
145
|
+
# Array<Integer, String, Symbol>| Array<String, Symbol>
|
146
|
+
# ------------------------------+----------------------
|
147
|
+
# returns 1-to-1 correspondance with | unique nodes
|
148
|
+
# input |
|
149
|
+
# ------------------------------+----------------------
|
150
|
+
# uses named_child | field_name_for_child
|
151
|
+
# child_by_field_name | via each_node
|
152
|
+
# ------------------------------+----------------------
|
153
|
+
#
|
154
|
+
# See {#[]}.
|
155
|
+
def fetch(*keys)
|
156
|
+
keys = keys.map(&:to_s)
|
157
|
+
key_set = keys.to_set
|
158
|
+
fields = {}
|
159
|
+
each_field do |f, _c|
|
160
|
+
fields[f] = self[f] if key_set.delete(f)
|
161
|
+
|
162
|
+
break if key_set.empty?
|
163
|
+
end
|
164
|
+
fields.values_at(*keys)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
@@ -0,0 +1,191 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'helpers'
|
4
|
+
|
5
|
+
module TreeSitter
|
6
|
+
# Query is a wrapper around a tree-sitter query.
|
7
|
+
class Query
|
8
|
+
attr_reader :capture_names
|
9
|
+
attr_reader :capture_quantifiers
|
10
|
+
attr_reader :text_predicates
|
11
|
+
attr_reader :property_predicates
|
12
|
+
attr_reader :property_settings
|
13
|
+
attr_reader :general_predicates
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
# Called from query.c on initialize.
|
18
|
+
#
|
19
|
+
# Prepares all the predicates so we could process them in places like
|
20
|
+
# {QueryMatch#satisfies_text_predicate?}.
|
21
|
+
#
|
22
|
+
# This is translation from the [rust bindings](https://github.com/tree-sitter/tree-sitter/blob/e553578696fe86071846ed612ee476d0167369c1/lib/binding_rust/lib.rs#L1860)
|
23
|
+
# Because it's a direct translation, it's way too long and we need to shut up rubocop.
|
24
|
+
# TODO: refactor + simplify when stable.
|
25
|
+
def process(source) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
|
26
|
+
string_count = self.string_count
|
27
|
+
capture_count = self.capture_count
|
28
|
+
pattern_count = self.pattern_count
|
29
|
+
|
30
|
+
# Build a vector of strings to store the capture names.
|
31
|
+
capture_names = capture_count.times.map { |i| capture_name_for_id(i) }
|
32
|
+
|
33
|
+
# Build a vector to store capture qunatifiers.
|
34
|
+
capture_quantifiers =
|
35
|
+
pattern_count.times.map do |i|
|
36
|
+
capture_count.times.map do |j|
|
37
|
+
capture_quantifier_for_id(i, j)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Build a vector of strings to represent literal values used in predicates.
|
42
|
+
string_values = string_count.times.map { |i| string_value_for_id(i) }
|
43
|
+
|
44
|
+
# Build a vector of predicates for each pattern.
|
45
|
+
pattern_count.times do |i| # rubocop:disable Metrics/BlockLength
|
46
|
+
predicate_steps = predicates_for_pattern(i)
|
47
|
+
byte_offset = start_byte_for_pattern(i)
|
48
|
+
row =
|
49
|
+
source.chars.map.with_index
|
50
|
+
.take_while { |_, i| i < byte_offset } # rubocop:disable Lint/ShadowingOuterLocalVariable
|
51
|
+
.filter { |c, _| c == "\n" }
|
52
|
+
.size
|
53
|
+
text_predicates = []
|
54
|
+
property_predicates = []
|
55
|
+
property_settings = []
|
56
|
+
general_predicates = []
|
57
|
+
|
58
|
+
array_split_like_rust(predicate_steps) { |s| s.type == QueryPredicateStep::DONE } # rubocop:disable Metrics/BlockLength
|
59
|
+
.each do |p|
|
60
|
+
next if p.empty?
|
61
|
+
|
62
|
+
if p[0] == QueryPredicateStep::STRING
|
63
|
+
cap = capture_names[p[0].value_id]
|
64
|
+
raise ArgumentError, <<~MSG.chomp
|
65
|
+
L#{row}: Expected predicate to start with a function name. Got @#{cap}.
|
66
|
+
MSG
|
67
|
+
end
|
68
|
+
|
69
|
+
# Build a predicate for each of the known predicate function names.
|
70
|
+
operator_name = string_values[p[0].value_id]
|
71
|
+
|
72
|
+
case operator_name
|
73
|
+
in 'any-eq?' | 'any-not-eq?' | 'eq?' | 'not-eq?'
|
74
|
+
if p.size != 3
|
75
|
+
raise ArgumentError, <<~MSG.chomp
|
76
|
+
L#{row}: Wrong number of arguments to ##{operator_name} predicate. Expected 2, got #{p.size - 1}.
|
77
|
+
MSG
|
78
|
+
end
|
79
|
+
|
80
|
+
if p[1].type != QueryPredicateStep::CAPTURE
|
81
|
+
lit = string_values[p[1].value_id]
|
82
|
+
raise ArgumentError, <<~MSG.chomp
|
83
|
+
L#{row}: First argument to ##{operator_name} predicate must be a capture name. Got literal "#{lit}".
|
84
|
+
MSG
|
85
|
+
end
|
86
|
+
|
87
|
+
is_positive = %w[eq? any-eq?].include?(operator_name)
|
88
|
+
match_all = %w[eq? not-eq?].include?(operator_name)
|
89
|
+
# NOTE: in the rust impl, match_all can hit an unreachable! but I am simplifying
|
90
|
+
# for readability. Same applies for the other `in` branches.
|
91
|
+
text_predicates <<
|
92
|
+
if p[2].type == QueryPredicateStep::CAPTURE
|
93
|
+
TextPredicateCapture.eq_capture(p[1].value_id, p[2].value_id, is_positive, match_all)
|
94
|
+
else
|
95
|
+
TextPredicateCapture.eq_string(p[1].value_id, string_values[p[2].value_id], is_positive, match_all)
|
96
|
+
end
|
97
|
+
|
98
|
+
in 'match?' | 'not-match?' | 'any-match?' | 'any-not-match?'
|
99
|
+
if p.size != 3
|
100
|
+
raise ArgumentError, <<~MSG.chomp
|
101
|
+
L#{row}: Wrong number of arguments to ##{operator_name} predicate. Expected 2, got #{p.size - 1}.
|
102
|
+
MSG
|
103
|
+
end
|
104
|
+
|
105
|
+
if p[1].type != QueryPredicateStep::CAPTURE
|
106
|
+
lit = string_values[p[1].value_id]
|
107
|
+
raise ArgumentError, <<~MSG.chomp
|
108
|
+
L#{row}: First argument to ##{operator_name} predicate must be a capture name. Got literal "#{lit}".
|
109
|
+
MSG
|
110
|
+
end
|
111
|
+
|
112
|
+
if p[2].type == QueryPredicateStep::CAPTURE
|
113
|
+
cap = capture_names[p[2].value_id]
|
114
|
+
raise ArgumentError, <<~MSG.chomp
|
115
|
+
L#{row}: First argument to ##{operator_name} predicate must be a literal. Got capture @#{cap}".
|
116
|
+
MSG
|
117
|
+
end
|
118
|
+
|
119
|
+
is_positive = %w[match? any-match?].include?(operator_name)
|
120
|
+
match_all = %w[match? not-match?].include?(operator_name)
|
121
|
+
regex = /#{string_values[p[2].value_id]}/
|
122
|
+
|
123
|
+
text_predicates << TextPredicateCapture.match_string(p[1].value_id, regex, is_positive, match_all)
|
124
|
+
|
125
|
+
in 'set!'
|
126
|
+
property_settings << 'todo!'
|
127
|
+
|
128
|
+
in 'is?' | 'is-not?'
|
129
|
+
property_predicates << 'todo!'
|
130
|
+
|
131
|
+
in 'any-of?' | 'not-any-of?'
|
132
|
+
if p.size < 2
|
133
|
+
raise ArgumentError, <<~MSG.chomp
|
134
|
+
L#{row}: Wrong number of arguments to ##{operator_name} predicate. Expected at least 1, got #{p.size - 1}.
|
135
|
+
MSG
|
136
|
+
end
|
137
|
+
|
138
|
+
if p[1].type != QueryPredicateStep::CAPTURE
|
139
|
+
lit = string_values[p[1].value_id]
|
140
|
+
raise ArgumentError, <<~MSG.chomp
|
141
|
+
L#{row}: First argument to ##{operator_name} predicate must be a capture name. Got literal "#{lit}".
|
142
|
+
MSG
|
143
|
+
end
|
144
|
+
|
145
|
+
is_positive = operator_name == 'any_of'
|
146
|
+
values = []
|
147
|
+
|
148
|
+
p[2..].each do |arg|
|
149
|
+
if arg.type == QueryPredicateStep::CAPTURE
|
150
|
+
lit = string_values[arg.value_id]
|
151
|
+
raise ArgumentError, <<~MSG.chomp
|
152
|
+
L#{row}: First argument to ##{operator_name} predicate must be a capture name. Got literal "#{lit}".
|
153
|
+
MSG
|
154
|
+
end
|
155
|
+
values << string_values[arg.value_id]
|
156
|
+
end
|
157
|
+
|
158
|
+
# TODO: is the map to to_s necessary in ruby?
|
159
|
+
text_predicates <<
|
160
|
+
TextPredicateCapture.any_string(p[1].value_id, values.map(&:to_s), is_positive, match_all)
|
161
|
+
else
|
162
|
+
general_predicates <<
|
163
|
+
QueryPredicate.new(
|
164
|
+
operator_name,
|
165
|
+
p[1..].map do |a|
|
166
|
+
if a.type == QueryPredicateStep::CAPTURE
|
167
|
+
{ capture: a.value_id }
|
168
|
+
else
|
169
|
+
{ string: string_values[a.value_id] }
|
170
|
+
end
|
171
|
+
end,
|
172
|
+
)
|
173
|
+
end
|
174
|
+
|
175
|
+
@text_predicates << text_predicates
|
176
|
+
@property_predicates << property_predicates
|
177
|
+
@property_settings << property_settings
|
178
|
+
@general_predicates << general_predicates
|
179
|
+
end
|
180
|
+
|
181
|
+
@capture_names = capture_names
|
182
|
+
@capture_quantifiers = capture_quantifiers
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
# TODO
|
187
|
+
def parse_property
|
188
|
+
# todo
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TreeSitter
|
4
|
+
# A sequence of {TreeSitter::QueryCapture} associated with a given {TreeSitter::QueryCursor}.
|
5
|
+
class QueryCaptures
|
6
|
+
include Enumerable
|
7
|
+
|
8
|
+
def initialize(cursor, query, src)
|
9
|
+
@cursor = cursor
|
10
|
+
@query = query
|
11
|
+
@src = src
|
12
|
+
end
|
13
|
+
|
14
|
+
# Iterator over captures.
|
15
|
+
#
|
16
|
+
# @yieldparam match [TreeSitter::QueryMatch]
|
17
|
+
# @yieldparam capture_index [Integer]
|
18
|
+
def each(&_block)
|
19
|
+
return enum_for __method__ if !block_given?
|
20
|
+
|
21
|
+
while (capture_index, match = @cursor.next_capture)
|
22
|
+
next if !match.is_a?(TreeSitter::QueryMatch)
|
23
|
+
|
24
|
+
if match.satisfies_text_predicate?(@query, @src)
|
25
|
+
yield [match, capture_index]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TreeSitter
|
4
|
+
# A Cursor for {Query}.
|
5
|
+
class QueryCursor
|
6
|
+
# Iterate over all of the matches in the order that they were found.
|
7
|
+
#
|
8
|
+
# Each match contains the index of the pattern that matched, and a list of
|
9
|
+
# captures. Because multiple patterns can match the same set of nodes,
|
10
|
+
# one match may contain captures that appear *before* some of the
|
11
|
+
# captures from a previous match.
|
12
|
+
def matches(query, node, src)
|
13
|
+
self.exec(query, node)
|
14
|
+
QueryMatches.new(self, query, src)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Iterate over all of the individual captures in the order that they
|
18
|
+
# appear.
|
19
|
+
#
|
20
|
+
# This is useful if you don't care about which pattern matched, and just
|
21
|
+
# want a single, ordered sequence of captures.
|
22
|
+
def captures(query, node, src)
|
23
|
+
self.exec(query, node)
|
24
|
+
QueryCaptures.new(self, query, src)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'query_captures'
|
4
|
+
|
5
|
+
module TreeSitter
|
6
|
+
# A match for a {Query}.
|
7
|
+
class QueryMatch
|
8
|
+
# All nodes at a given capture index.
|
9
|
+
#
|
10
|
+
# @param index [Integer]
|
11
|
+
#
|
12
|
+
# @return [TreeSitter::Node]
|
13
|
+
def nodes_for_capture_index(index) = captures.filter_map { |capture| capture.node if capture.index == index }
|
14
|
+
|
15
|
+
# Whether the {QueryMatch} satisfies the text predicates in the query.
|
16
|
+
#
|
17
|
+
# This is a translation from the [rust bindings](https://github.com/tree-sitter/tree-sitter/blob/e553578696fe86071846ed612ee476d0167369c1/lib/binding_rust/lib.rs#L2502).
|
18
|
+
# Because it's a direct translation, it's way too long and we need to shut up rubocop.
|
19
|
+
# TODO: refactor + simplify when satable.
|
20
|
+
def satisfies_text_predicate?(query, src) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
|
21
|
+
return true if query.text_predicates[pattern_index].nil?
|
22
|
+
|
23
|
+
query # rubocop:disable Metrics/BlockLength
|
24
|
+
.text_predicates[pattern_index]
|
25
|
+
.all? do |predicate|
|
26
|
+
case predicate.type
|
27
|
+
in TextPredicateCapture::EQ_CAPTURE
|
28
|
+
fst_nodes = nodes_for_capture_index(predicate.fst)
|
29
|
+
snd_nodes = nodes_for_capture_index(predicate.snd)
|
30
|
+
res = nil
|
31
|
+
consumed = 0
|
32
|
+
fst_nodes.zip(snd_nodes).each do |node1, node2|
|
33
|
+
text1 = node_text(node1, src)
|
34
|
+
text2 = node_text(node2, src)
|
35
|
+
if (text1 == text2) != predicate.positive? && predicate.match_all?
|
36
|
+
res = false
|
37
|
+
break
|
38
|
+
end
|
39
|
+
if (text1 == text2) == predicate.positive? && !predicate.match_all?
|
40
|
+
res = true
|
41
|
+
break
|
42
|
+
end
|
43
|
+
consumed += 1
|
44
|
+
end
|
45
|
+
(res.nil? && consumed == fst_nodes.length && consumed == snd_nodes.length) \
|
46
|
+
|| res
|
47
|
+
|
48
|
+
in TextPredicateCapture::EQ_STRING
|
49
|
+
nodes = nodes_for_capture_index(predicate.fst)
|
50
|
+
res = true
|
51
|
+
nodes.each do |node|
|
52
|
+
text = node_text(node, src)
|
53
|
+
if (predicate.snd == text) != predicate.positive? && predicate.match_all?
|
54
|
+
res = false
|
55
|
+
break
|
56
|
+
end
|
57
|
+
if (predicate.snd == text) == predicate.positive? && !predicate.match_all?
|
58
|
+
res = true
|
59
|
+
break
|
60
|
+
end
|
61
|
+
end
|
62
|
+
res
|
63
|
+
|
64
|
+
in TextPredicateCapture::MATCH_STRING
|
65
|
+
nodes = nodes_for_capture_index(predicate.fst)
|
66
|
+
res = true
|
67
|
+
nodes.each do |node|
|
68
|
+
text = node_text(node, src)
|
69
|
+
if predicate.snd.match?(text) != predicate.positive? && predicate.match_all?
|
70
|
+
res = false
|
71
|
+
break
|
72
|
+
end
|
73
|
+
if predicate.snd.match?(text) == predicate.positive? && !predicate.match_all?
|
74
|
+
res = true
|
75
|
+
break
|
76
|
+
end
|
77
|
+
end
|
78
|
+
res
|
79
|
+
|
80
|
+
in TextPredicateCapture::ANY_STRING
|
81
|
+
nodes = nodes_for_capture_index(predicate.fst)
|
82
|
+
res = true
|
83
|
+
nodes.each do |node|
|
84
|
+
text = node_text(node, src)
|
85
|
+
if predicate.snd.any? { |v| v == text } != predicate.positive?
|
86
|
+
res = false
|
87
|
+
break
|
88
|
+
end
|
89
|
+
end
|
90
|
+
res
|
91
|
+
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
def node_text(node, text) = text.byteslice(node.start_byte...node.end_byte)
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TreeSitter
|
4
|
+
# A sequence of {QueryMatch} associated with a given {QueryCursor}.
|
5
|
+
class QueryMatches
|
6
|
+
include Enumerable
|
7
|
+
|
8
|
+
def initialize(cursor, query, src)
|
9
|
+
@cursor = cursor
|
10
|
+
@query = query
|
11
|
+
@src = src
|
12
|
+
end
|
13
|
+
|
14
|
+
# Iterator over matches.
|
15
|
+
#
|
16
|
+
# @yieldparam match [TreeSitter::QueryMatch]
|
17
|
+
def each(&_block)
|
18
|
+
return enum_for __method__ if !block_given?
|
19
|
+
|
20
|
+
while match = @cursor.next_match
|
21
|
+
if match.satisfies_text_predicate?(@query, @src)
|
22
|
+
yield match
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Iterate over all the results presented as hashes of `capture name => node`.
|
28
|
+
#
|
29
|
+
# @yieldparam match [Hash<String, TreeSitter::Node>]
|
30
|
+
def each_capture_hash(&_block)
|
31
|
+
# TODO: should we return [Array<Hash<Symbol, TreeSitter::Node]>>] instead?
|
32
|
+
return enum_for __method__ if !block_given?
|
33
|
+
|
34
|
+
each do |match|
|
35
|
+
yield match.captures.to_h { |cap| [@query.capture_name_for_id(cap.index), cap.node] }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TreeSitter
|
4
|
+
# A {Query} predicate generic representation.
|
5
|
+
class QueryPredicate
|
6
|
+
attr_accessor :operator
|
7
|
+
attr_accessor :args
|
8
|
+
|
9
|
+
def initialize(operator, args)
|
10
|
+
@operator = operator
|
11
|
+
@args = args
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TreeSitter
|
4
|
+
# A representation for text predicates.
|
5
|
+
class TextPredicateCapture
|
6
|
+
EQ_CAPTURE = 0 # Equality Capture
|
7
|
+
EQ_STRING = 1 # Equality String
|
8
|
+
MATCH_STRING = 2 # Match String
|
9
|
+
ANY_STRING = 3 # Any String
|
10
|
+
|
11
|
+
attr_reader :fst
|
12
|
+
attr_reader :snd
|
13
|
+
attr_reader :type
|
14
|
+
|
15
|
+
# Create a TextPredicateCapture for {EQ_CAPTURE}.
|
16
|
+
def self.eq_capture(...) = new(EQ_CAPTURE, ...)
|
17
|
+
# Create a TextPredicateCapture for {EQ_STRING}.
|
18
|
+
def self.eq_string(...) = new(EQ_STRING, ...)
|
19
|
+
# Create a TextPredicateCapture for {MATCH_STRING}.
|
20
|
+
def self.match_string(...) = new(MATCH_STRING, ...)
|
21
|
+
# Create a TextPredicateCapture for {ANY_STRING}.
|
22
|
+
def self.any_string(...) = new(ANY_STRING, ...)
|
23
|
+
|
24
|
+
def initialize(type, fst, snd, positive, match_all)
|
25
|
+
@type = type
|
26
|
+
@fst = fst
|
27
|
+
@snd = snd
|
28
|
+
@positive = positive
|
29
|
+
@match_all = match_all
|
30
|
+
end
|
31
|
+
|
32
|
+
# `#eq` is positive, `#not-eq` is not.
|
33
|
+
def positive? = @positive
|
34
|
+
# `#any-` means don't match all.
|
35
|
+
def match_all? = @match_all
|
36
|
+
end
|
37
|
+
end
|
data/lib/tree_sitter.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'set'
|
4
|
+
|
5
|
+
begin
|
6
|
+
RUBY_VERSION =~ /(\d+\.\d+)/
|
7
|
+
require "tree_sitter/#{Regexp.last_match(1)}/tree_sitter"
|
8
|
+
rescue LoadError
|
9
|
+
require 'tree_sitter/tree_sitter'
|
10
|
+
end
|
11
|
+
|
12
|
+
require 'tree_sitter/version'
|
13
|
+
|
14
|
+
require 'tree_sitter/mixins/language'
|
15
|
+
|
16
|
+
require 'tree_sitter/node'
|
17
|
+
require 'tree_sitter/query'
|
18
|
+
require 'tree_sitter/query_captures'
|
19
|
+
require 'tree_sitter/query_cursor'
|
20
|
+
require 'tree_sitter/query_match'
|
21
|
+
require 'tree_sitter/query_matches'
|
22
|
+
require 'tree_sitter/query_predicate'
|
23
|
+
require 'tree_sitter/text_predicate_capture'
|
24
|
+
|
25
|
+
# TreeSitter is a Ruby interface to the tree-sitter parsing library.
|
26
|
+
module TreeSitter
|
27
|
+
extend Mixins::Language
|
28
|
+
|
29
|
+
class << self
|
30
|
+
alias_method :lang, :language
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
ObjectSpace.define_finalizer(TreeSitter::Tree.class, proc { TreeSitter::Tree.finalizer })
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# typed: true
|
3
|
+
|
4
|
+
module TreeStand
|
5
|
+
# An experimental class to modify the AST. It re-runs the query on the
|
6
|
+
# modified document every loop to ensure that the match is still valid.
|
7
|
+
# @see TreeStand::Tree
|
8
|
+
# @api experimental
|
9
|
+
class AstModifier
|
10
|
+
extend T::Sig
|
11
|
+
|
12
|
+
sig { params(tree: TreeStand::Tree).void }
|
13
|
+
def initialize(tree)
|
14
|
+
@tree = tree
|
15
|
+
end
|
16
|
+
|
17
|
+
# @param query [String]
|
18
|
+
# @yieldparam self [self]
|
19
|
+
# @yieldparam match [TreeStand::Match]
|
20
|
+
# @return [void]
|
21
|
+
def on_match(query)
|
22
|
+
matches = @tree.query(query)
|
23
|
+
|
24
|
+
while !matches.empty?
|
25
|
+
yield self, matches.first
|
26
|
+
matches = @tree.query(query)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|