ruby_tree_sitter 1.6.0-x86_64-darwin
Sign up to get free protection for your applications and to get access to all the features.
- 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.bundle +0 -0
- data/lib/tree_sitter/3.1/tree_sitter.bundle +0 -0
- data/lib/tree_sitter/3.2/tree_sitter.bundle +0 -0
- data/lib/tree_sitter/3.3/tree_sitter.bundle +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
|