ruby_tree_sitter 1.2.0-x86_64-linux → 1.4.0-x86_64-linux

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cfbfdfc0c297585fe24e04c4b9822ff404baa795182b7a7b144f56a3941db672
4
- data.tar.gz: 010faa21be194e40cdf7f5f00986c16ee5d51d64f377097bfb307a4245ba61b6
3
+ metadata.gz: 701e16d45de25378b1f6e0b3e21b8c589cadb9f2f4e8af420a130d55637cd984
4
+ data.tar.gz: b1899ead2950b9ab9806076a0708c084221d3a8b59bbeb6a0e6848b57cbecf93
5
5
  SHA512:
6
- metadata.gz: b98898e1a8b801de3599e6daebd2adaa27b2ea729a46ebe730095d31e053dfb84c48a83712364a09fd58bac236f1b499f16ced6dcf4640902d0d4c12ac4ac7be
7
- data.tar.gz: 4dd45cf49030a553819febb05cc8a0755ed549c413d44ea1c88836d4e2fad3c55138db3a3079776f2e14a828c38fd7d7b11664366ea8e84083098910528950b3
6
+ metadata.gz: e07d00239ccab6b2ddabff6d90076bd6c42c48b9f1a5b98f8cc8fc07a4688e5d36c357c2373c2d5629676b46a3b34fc694f3ab1e97fdd5614c9e6350c2ade28c
7
+ data.tar.gz: 0dbbac2900d41056e4ab37b8d298351b819c9ca42296b33962f6f087a773859afc7e7de666d3d0734f883702872f90d59bf66f7546bc43510f003b47c91e11fe
data/README.md CHANGED
@@ -200,4 +200,8 @@ To See a full list of the ruby-specific APIs, see [here](lib/README.md).
200
200
 
201
201
  ## Sponsors
202
202
 
203
- <img src="img/faveod.jpg" width="75%">
203
+ - <a href="https://faveod.com">https://faveod.com</a>
204
+
205
+ <a href="https://faveod.com">
206
+ <img src="img/faveod.jpg" width="66%" />
207
+ </a>
@@ -140,6 +140,13 @@ static VALUE query_initialize(VALUE self, VALUE language, VALUE source) {
140
140
  SELF = res;
141
141
  }
142
142
 
143
+ rb_iv_set(self, "@text_predicates", rb_ary_new());
144
+ rb_iv_set(self, "@property_predicates", rb_ary_new());
145
+ rb_iv_set(self, "@property_settings", rb_ary_new());
146
+ rb_iv_set(self, "@general_predicates", rb_ary_new());
147
+
148
+ rb_funcall(self, rb_intern("process"), 1, source);
149
+
143
150
  return self;
144
151
  }
145
152
 
@@ -49,7 +49,7 @@ DATA_FROM_VALUE(TSQueryCursor *, query_cursor)
49
49
  *
50
50
  * @return [QueryCursor]
51
51
  */
52
- static VALUE query_cursor_exec(VALUE self, VALUE query, VALUE node) {
52
+ static VALUE query_cursor_exec_static(VALUE self, VALUE query, VALUE node) {
53
53
  VALUE res = query_cursor_allocate(cQueryCursor);
54
54
  query_cursor_t *query_cursor = unwrap(res);
55
55
  ts_query_cursor_exec(query_cursor->data, value_to_query(query),
@@ -57,6 +57,21 @@ static VALUE query_cursor_exec(VALUE self, VALUE query, VALUE node) {
57
57
  return res;
58
58
  }
59
59
 
60
+ /**
61
+ * Start running a given query on a given node.
62
+ *
63
+ * @param query [Query]
64
+ * @param node [Node]
65
+ *
66
+ * @return [QueryCursor]
67
+ */
68
+ static VALUE query_cursor_exec(VALUE self, VALUE query, VALUE node) {
69
+ query_cursor_t *query_cursor = unwrap(self);
70
+ ts_query_cursor_exec(query_cursor->data, value_to_query(query),
71
+ value_to_node(node));
72
+ return self;
73
+ }
74
+
60
75
  /**
61
76
  * Manage the maximum number of in-progress matches allowed by this query
62
77
  * cursor.
@@ -190,13 +205,14 @@ void init_query_cursor(void) {
190
205
  rb_define_alloc_func(cQueryCursor, query_cursor_allocate);
191
206
 
192
207
  /* Module methods */
193
- rb_define_module_function(cQueryCursor, "exec", query_cursor_exec, 2);
208
+ rb_define_module_function(cQueryCursor, "exec", query_cursor_exec_static, 2);
194
209
 
195
210
  /* Class methods */
196
211
  // Accessors
197
212
  DECLARE_ACCESSOR(cQueryCursor, query_cursor, match_limit)
198
213
 
199
214
  // Other
215
+ rb_define_method(cQueryCursor, "exec", query_cursor_exec, 2);
200
216
  rb_define_method(cQueryCursor, "exceed_match_limit?",
201
217
  query_cursor_did_exceed_match_limit, 0);
202
218
  rb_define_method(cQueryCursor, "match_limit", query_cursor_get_match_limit,
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ # splits an array like [rust](https://doc.rust-lang.org/std/primitive.slice.html#method.split)
4
+ def array_split_like_rust(array, &block)
5
+ return enum_for(__method__, array) if !block_given?
6
+
7
+ return [] if array.empty?
8
+
9
+ result = []
10
+ current_slice = []
11
+
12
+ array.each do |element|
13
+ if yield(element)
14
+ result << current_slice
15
+ current_slice = []
16
+ else
17
+ current_slice << element
18
+ end
19
+ end
20
+
21
+ result << current_slice
22
+ result
23
+ end
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TreeSitter
4
+ # A colon-separated list of paths pointing to directories that can contain parsers.
5
+ # Order matters.
6
+ # Takes precedence over default lookup paths.
7
+ ENV_PARSERS =
8
+ ENV['TREE_SITTER_PARSERS']
9
+ &.split(':')
10
+ &.map { |v| Pathname(v) }
11
+ .freeze
12
+
13
+ # The default paths we use to lookup parsers.
14
+ # Order matters.
15
+ LIBDIRS = [
16
+ '.vendor/parsers',
17
+ '.vendor/tree-sitter-parsers',
18
+ 'vendor/parsers',
19
+ 'vendor/tree-sitter-parsers',
20
+ 'parsers',
21
+ 'tree-sitter-parsers',
22
+ '/opt/local/lib',
23
+ '/opt/lib',
24
+ '/usr/local/lib',
25
+ '/usr/lib',
26
+ ].map { |p| Pathname(p) }.freeze
27
+
28
+ # Mixins.
29
+ module Mixins
30
+ # Language Mixin.
31
+ module Language
32
+ # Load a language from configuration or default lookup paths.
33
+ #
34
+ # @example Load java from default paths
35
+ # # This will look for:
36
+ # #
37
+ # # .vendor/tree-sitter-parsers/(java/)?(libtree-sitter-)?java.{ext}
38
+ # # .vendor/parsers/(java/)?(libtree-sitter-)?java.{ext}
39
+ # # vendor/tree-sitter-parsers/(java/)?(libtree-sitter-)?java.{ext}
40
+ # # vendor/parsers/(java/)?(libtree-sitter-)?java.{ext}
41
+ # # parsers/(java/)?(libtree-sitter-)?java.{ext}
42
+ # # tree-sitter-parsers/(java/)?(libtree-sitter-)?java.{ext}
43
+ # # /opt/local/lib/(java/)?(libtree-sitter-)?java.{ext}
44
+ # # /opt/lib/(java/)?(libtree-sitter-)?java.{ext}
45
+ # # /usr/local/lib/(java/)?(libtree-sitter-)?java.{ext}
46
+ # # /usr/lib/(java/)?(libtree-sitter-)?java.{ext}
47
+ # #
48
+ # java = TreeSitter.language('java')
49
+ #
50
+ # @example (TreeStand) Load java from a configured path
51
+ # # This will look for:
52
+ # #
53
+ # # /my/path/(java/)?(libtree-sitter-)?java.{ext}
54
+ # #
55
+ # TreeStand.config.parser_path = '/my/path'
56
+ # java = TreeStand::Parser.language('java')
57
+ #
58
+ # @example (TreeStand) Load java from environment variables
59
+ # # This will look for:
60
+ # #
61
+ # # /my/forced/env/path/(java/)?(libtree-sitter-)?java.{ext}
62
+ # # /my/path/(java/)?(libtree-sitter-)?java.{ext}
63
+ # #
64
+ # # … and the same works for the default paths if `TreeStand.config.parser_path`
65
+ # # was `nil`
66
+ # ENV['TREE_SITTER_PARSERS'] = '/my/forced/env/path'
67
+ # TreeStand.config.parser_path = '/my/path'
68
+ # java = TreeStand::Parser.language('java')
69
+ #
70
+ # @param name [String] the name of the parser.
71
+ # This name is used to load the symbol from the compiled parser, replacing `-` with `_`.
72
+ #
73
+ # @return [TreeSitter:language] a language object to use in your parsers.
74
+ #
75
+ # @raise [RuntimeError] if the parser was not found.
76
+ #
77
+ # @see search_for_lib
78
+ def language(name)
79
+ lib = search_for_lib(name)
80
+
81
+ if lib.nil?
82
+ raise <<~MSG.chomp
83
+ Failed to load a parser for #{name}.
84
+
85
+ #{search_lib_message}
86
+ MSG
87
+ end
88
+
89
+ # We know that the bindings will accept `lib`, but I don't know how to tell sorbet
90
+ # the types in ext/tree_sitter where `load` is defined.
91
+ TreeSitter::Language.load(name.tr('-', '_'), T.unsafe(lib))
92
+ end
93
+
94
+ # The platform-specific extension of the parser.
95
+ # @return [String] `dylib` or `so` for mac or linux.
96
+ def ext
97
+ case Gem::Platform.local.os
98
+ in /darwin/ then 'dylib'
99
+ else 'so'
100
+ end
101
+ end
102
+
103
+ private
104
+
105
+ # The library directories we need to look into.
106
+ #
107
+ # @return [Array<Pathname>] the list of candidate places to use when searching for parsers.
108
+ #
109
+ # @see ENV_PARSERS
110
+ # @see LIBDIRS
111
+ def lib_dirs = [*TreeSitter::ENV_PARSERS, *TreeSitter::LIBDIRS]
112
+
113
+ # Lookup a parser by name.
114
+ #
115
+ # Precedence:
116
+ # 1. `Env['TREE_SITTER_PARSERS]`.
117
+ # 2. {TreeStand::Config#parser_path} if using {TreeStand}.
118
+ # 3. {LIBDIRS}.
119
+ #
120
+ # If a {TreeStand::Config#parser_path} is `nil`, {LIBDIRS} is used.
121
+ # If a {TreeStand::Config#parser_path} is a {::Pathname}, {LIBDIRS} is ignored.
122
+ def search_for_lib(name)
123
+ files = [
124
+ name,
125
+ "tree-sitter-#{name}",
126
+ "libtree-sitter-#{name}",
127
+ ].map { |v| "#{v}.#{ext}" }
128
+
129
+ lib_dirs
130
+ .product(files)
131
+ .find do |dir, so|
132
+ path = dir / so
133
+ path = dir / name / so if !path.exist?
134
+ break path.expand_path if path.exist?
135
+ end
136
+ end
137
+
138
+ # Generates a string message on where parser lookup happens.
139
+ #
140
+ # @return [String] A pretty message.
141
+ def search_lib_message
142
+ indent = 2
143
+ pretty = ->(arr) {
144
+ if arr
145
+ arr
146
+ .compact
147
+ .map { |v| "#{' ' * indent}#{v.expand_path}" }
148
+ .join("\n")
149
+ end
150
+ }
151
+ <<~MSG.chomp
152
+ From ENV['TREE_SITTER_PARSERS']:
153
+ #{pretty.call(ENV_PARSERS)}
154
+
155
+ From Defaults:
156
+ #{pretty.call(lib_dirs)}
157
+ MSG
158
+ end
159
+ end
160
+ end
161
+ end
@@ -3,6 +3,8 @@
3
3
  module TreeSitter
4
4
  # Node is a wrapper around a tree-sitter node.
5
5
  class Node
6
+ include Enumerable
7
+
6
8
  # @return [Array<Symbol>] the node's named fields
7
9
  def fields
8
10
  return @fields if @fields
@@ -87,10 +89,10 @@ module TreeSitter
87
89
  # Iterate over a node's children.
88
90
  #
89
91
  # @yieldparam child [Node] the child
90
- def each
92
+ def each(&_block)
91
93
  return enum_for __method__ if !block_given?
92
94
 
93
- (0...(child_count)).each do |i|
95
+ (0...child_count).each do |i|
94
96
  yield child(i)
95
97
  end
96
98
  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
Binary file
@@ -4,5 +4,5 @@ module TreeSitter
4
4
  # The version of the tree-sitter library.
5
5
  TREESITTER_VERSION = '0.22.6'
6
6
  # The current version of the gem.
7
- VERSION = '1.2.0'
7
+ VERSION = '1.4.0'
8
8
  end
data/lib/tree_sitter.rb CHANGED
@@ -1,14 +1,28 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # TreeSitter is a Ruby interface to the tree-sitter parsing library.
4
- module TreeSitter
5
- end
6
-
7
3
  require 'set'
8
4
 
5
+ require 'tree_sitter/tree_sitter'
9
6
  require 'tree_sitter/version'
10
7
 
11
- require 'tree_sitter/tree_sitter'
8
+ require 'tree_sitter/mixins/language'
9
+
12
10
  require 'tree_sitter/node'
11
+ require 'tree_sitter/query'
12
+ require 'tree_sitter/query_captures'
13
+ require 'tree_sitter/query_cursor'
14
+ require 'tree_sitter/query_match'
15
+ require 'tree_sitter/query_matches'
16
+ require 'tree_sitter/query_predicate'
17
+ require 'tree_sitter/text_predicate_capture'
18
+
19
+ # TreeSitter is a Ruby interface to the tree-sitter parsing library.
20
+ module TreeSitter
21
+ extend Mixins::Language
22
+
23
+ class << self
24
+ alias_method :lang, :language
25
+ end
26
+ end
13
27
 
14
28
  ObjectSpace.define_finalizer(TreeSitter::Tree.class, proc { TreeSitter::Tree.finalizer })
@@ -97,19 +97,11 @@ module TreeStand
97
97
  sig { params(query_string: String).returns(T::Array[T::Hash[String, TreeStand::Node]]) }
98
98
  def query(query_string)
99
99
  ts_query = TreeSitter::Query.new(@tree.parser.ts_language, query_string)
100
- ts_cursor = TreeSitter::QueryCursor.exec(ts_query, ts_node)
101
- matches = []
102
- while ts_match = ts_cursor.next_match
103
- captures = {}
104
-
105
- ts_match.captures.each do |ts_capture|
106
- capture_name = ts_query.capture_name_for_id(ts_capture.index)
107
- captures[capture_name] = TreeStand::Node.new(@tree, ts_capture.node)
108
- end
109
-
110
- matches << captures
111
- end
112
- matches
100
+ TreeSitter::QueryCursor
101
+ .new
102
+ .matches(ts_query, @tree.ts_tree.root_node, @tree.document)
103
+ .each_capture_hash
104
+ .map { |h| h.transform_values! { |n| TreeStand::Node.new(@tree, n) } }
113
105
  end
114
106
 
115
107
  # Returns the first captured node that matches the query string or nil if
@@ -26,6 +26,7 @@ module TreeStand
26
26
  # @see LIBDIRS
27
27
  class Parser
28
28
  extend T::Sig
29
+ extend TreeSitter::Mixins::Language
29
30
 
30
31
  sig { returns(TreeSitter::Language) }
31
32
  attr_reader :ts_language
@@ -33,156 +34,6 @@ module TreeStand
33
34
  sig { returns(TreeSitter::Parser) }
34
35
  attr_reader :ts_parser
35
36
 
36
- # A colon-seprated list of paths pointing to directories that can contain parsers.
37
- # Order matters.
38
- # Takes precedence over default lookup paths.
39
- ENV_PARSERS =
40
- ENV['TREE_SITTER_PARSERS']
41
- &.split(':')
42
- &.map { |v| Pathname(v) }
43
- .freeze
44
-
45
- # The default paths we use to lookup parsers when no specific
46
- # {TreeStand::Config#parser_path} is {nil}.
47
- # Order matters.
48
- LIBDIRS = [
49
- 'tree-sitter-parsers',
50
- '/opt/local/lib',
51
- '/opt/lib',
52
- '/usr/local/lib',
53
- '/usr/lib',
54
- ].map { |p| Pathname(p) }.freeze
55
-
56
- # The library directories we need to look into.
57
- #
58
- # @return [Array<Pathname>] the list of candidate places to use when searching for parsers.
59
- #
60
- # @see ENV_PARSERS
61
- # @see LIBDIRS
62
- sig { returns(T::Array[Pathname]) }
63
- def self.lib_dirs = [
64
- *ENV_PARSERS,
65
- *(TreeStand.config.parser_path ? [TreeStand.config.parser_path] : LIBDIRS),
66
- ].compact
67
-
68
- # The platform-specific extension of the parser.
69
- # @return [String] `dylib` or `so` for mac or linux.
70
- sig { returns(String) }
71
- def self.ext
72
- case Gem::Platform.local.os
73
- in /darwin/ then 'dylib'
74
- else 'so'
75
- end
76
- end
77
-
78
- # Lookup a parser by name.
79
- #
80
- # Precedence:
81
- # 1. `Env['TREE_SITTER_PARSERS]`
82
- # 2. {TreeStand::Config#parser_path}
83
- # 3. {LIBDIRS}
84
- #
85
- # If a {TreeStand::Config#parser_path} is `nil`, {LIBDIRS} is used.
86
- # If a {TreeStand::Config#parser_path} is a {::Pathname}, {LIBDIRS} is ignored.
87
- sig { params(name: String).returns(T.nilable(Pathname)) }
88
- def self.search_for_lib(name)
89
- files = [
90
- name,
91
- "tree-sitter-#{name}",
92
- "libtree-sitter-#{name}",
93
- ].map { |v| "#{v}.#{ext}" }
94
-
95
- res = lib_dirs
96
- .product(files)
97
- .find do |dir, so|
98
- path = dir / so
99
- path = dir / name / so if !path.exist?
100
- break path.expand_path if path.exist?
101
- end
102
- res.is_a?(Array) ? nil : res
103
- end
104
-
105
- # Generates a string message on where parser lookup happens.
106
- #
107
- # @return [String] A pretty message.
108
- sig { returns(String) }
109
- def self.search_lib_message
110
- indent = 2
111
- pretty = ->(arr) {
112
- if arr
113
- arr
114
- .compact
115
- .map { |v| "#{' ' * indent}#{v.expand_path}" }
116
- .join("\n")
117
- end
118
- }
119
- <<~MSG
120
- From ENV['TREE_SITTER_PARSERS']:
121
- #{pretty.call(ENV_PARSERS)}
122
-
123
- From TreeStand.config.parser_path:
124
- #{pretty.call([TreeStand.config.parser_path])}
125
-
126
- From Defaults:
127
- #{pretty.call(LIBDIRS)}
128
- MSG
129
- end
130
-
131
- # Load a language from configuration or default lookup paths.
132
- #
133
- # @example Load java from default paths
134
- # # This will look for:
135
- # #
136
- # # tree-sitter-parsers/(java/)?(libtree-sitter-)?java.{ext}
137
- # # /opt/local/lib/(java/)?(libtree-sitter-)?java.{ext}
138
- # # /opt/lib/(java/)?(libtree-sitter-)?java.{ext}
139
- # # /usr/local/lib/(java/)?(libtree-sitter-)?java.{ext}
140
- # # /usr/lib/(java/)?(libtree-sitter-)?java.{ext}
141
- # #
142
- # java = TreeStand::Parser.language('java')
143
- #
144
- # @example Load java from a configured path
145
- # # This will look for:
146
- # #
147
- # # /my/path/(java/)?(libtree-sitter-)?java.{ext}
148
- # #
149
- # TreeStand.config.parser_path = '/my/path'
150
- # java = TreeStand::Parser.language('java')
151
- #
152
- # @example Load java from environment variables
153
- # # This will look for:
154
- # #
155
- # # /my/forced/env/path/(java/)?(libtree-sitter-)?java.{ext}
156
- # # /my/path/(java/)?(libtree-sitter-)?java.{ext}
157
- # #
158
- # # … and the same works for the default paths if `TreeStand.config.parser_path`
159
- # # was `nil`
160
- # ENV['TREE_SITTER_PARSERS'] = '/my/forced/env/path'
161
- # TreeStand.config.parser_path = '/my/path'
162
- # java = TreeStand::Parser.language('java')
163
- #
164
- # @param name [String] the name of the parser.
165
- # This name is used to load the symbol from the compiled parser, replacing `-` with `_`.
166
- # @return [TreeSitter:language] a language object to use in your parsers.
167
- # @raise [RuntimeError] if the parser was not found.
168
- # @see search_for_lib
169
- sig { params(name: String).returns(TreeSitter::Language) }
170
- def self.language(name)
171
- lib = search_for_lib(name)
172
-
173
- if lib.nil?
174
- raise <<~MSG
175
- Failed to load a parser for #{name}.
176
-
177
- #{search_lib_message}
178
- MSG
179
- end
180
-
181
- # We know that the bindings will accept `lib`, but I don't know how to tell sorbet
182
- # the types in ext/tree_sitter where `load` is defined.
183
- TreeSitter::Language.load(name.gsub(/-/, '_'), T.unsafe(lib))
184
- end
185
-
186
37
  # @param language [String]
187
38
  sig { params(language: String).void }
188
39
  def initialize(language)
@@ -192,6 +43,19 @@ module TreeStand
192
43
  end
193
44
  end
194
45
 
46
+ # The library directories we need to look into.
47
+ #
48
+ # @return [Array<Pathname>] the list of candidate places to use when searching for parsers.
49
+ #
50
+ # @see ENV_PARSERS
51
+ # @see LIBDIRS
52
+ def self.lib_dirs
53
+ [
54
+ *TreeSitter::ENV_PARSERS,
55
+ *(TreeStand.config.parser_path ? [TreeStand.config.parser_path] : TreeSitter::LIBDIRS),
56
+ ]
57
+ end
58
+
195
59
  # Parse the provided document with the TreeSitter parser.
196
60
  # @param tree [TreeStand::Tree, nil] providing the old tree will allow the
197
61
  # parser to take advantage of incremental parsing and improve performance
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_tree_sitter
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 1.4.0
5
5
  platform: x86_64-linux
6
6
  authors:
7
7
  - Firas al-Khalil
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2024-05-27 00:00:00.000000000 Z
12
+ date: 2024-07-03 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: sorbet-runtime
@@ -74,7 +74,16 @@ files:
74
74
  - ext/tree_sitter/tree_sitter.c
75
75
  - ext/tree_sitter/tree_sitter.h
76
76
  - lib/tree_sitter.rb
77
+ - lib/tree_sitter/helpers.rb
78
+ - lib/tree_sitter/mixins/language.rb
77
79
  - lib/tree_sitter/node.rb
80
+ - lib/tree_sitter/query.rb
81
+ - lib/tree_sitter/query_captures.rb
82
+ - lib/tree_sitter/query_cursor.rb
83
+ - lib/tree_sitter/query_match.rb
84
+ - lib/tree_sitter/query_matches.rb
85
+ - lib/tree_sitter/query_predicate.rb
86
+ - lib/tree_sitter/text_predicate_capture.rb
78
87
  - lib/tree_sitter/tree_sitter.so
79
88
  - lib/tree_sitter/version.rb
80
89
  - lib/tree_stand.rb