ruby_tree_sitter 1.1.0-arm64-darwin-22 → 1.3.0-arm64-darwin-22

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: 771611c72bcd18fedf2bdbc8bf89a966e20a2dfdef1318dcf9bd660197028356
4
- data.tar.gz: '08ddfca6d434e570e910eae6f3d57ae372aaaf44005d85a6c3b5aff8c85bc2bb'
3
+ metadata.gz: 26e92e5c6a53b9a4dc06ae353e86092e9b015b5be1cd2467382735d1bae6e6aa
4
+ data.tar.gz: 3fbad0cc58f93f82485c3f646f87f9fdf9de259c52564cc8c73e38af750ecd19
5
5
  SHA512:
6
- metadata.gz: be8d857a1bc1491a639d9978c64aeb1a6e1b5e602ac1339e7a28f3c20db2638450842e5ded9f61fb5b6fcb609809f05f94e843809a3ae6bb1b317be524507871
7
- data.tar.gz: dc3eadf9136739d502738552fb5bb17f5fbd712d7f802faf30f1b0ff3b9fa7e84569a100caa995e810899055522eacd616fe949f86581246896379bc772a9fac
6
+ metadata.gz: 5524cad1a10138083390a2a545c67f2c4054bbaa943d9c1edd47b5c6550778f23c4528381d9aff68760b0a6db02998f65cca06b2f65e0408da773af85849bce2
7
+ data.tar.gz: 5c583197c977779dd94c2fbb611eac93abd7b63134ac8d1ae9fdd423588aa90a0ffa677ed1cb360931bb81d106c26f64a9ad20dfa4b1def5d92df0299e277a50
data/README.md CHANGED
@@ -1,6 +1,12 @@
1
1
  # Ruby tree-sitter bindings
2
2
 
3
- [![ci](https://github.com/Faveod/ruby-tree-sitter/actions/workflows/ci.yml/badge.svg)](https://github.com/Faveod/ruby-tree-sitter/actions/workflows/ci.yml) [![valgrind](https://github.com/Faveod/ruby-tree-sitter/actions/workflows/valgrind.yml/badge.svg)](https://github.com/Faveod/ruby-tree-sitter/actions/workflows/valgrind.yml) [![asan/ubsan](https://github.com/Faveod/ruby-tree-sitter/actions/workflows/asan.yml/badge.svg)](https://github.com/Faveod/ruby-tree-sitter/actions/workflows/asan.yml)
3
+ [![docs](https://github.com/Faveod/ruby-tree-sitter/actions/workflows/publish-docs.yml/badge.svg)](https://faveod.github.io/ruby-tree-sitter)
4
+ [![rubygems.org](https://github.com/Faveod/ruby-tree-sitter/actions/workflows/publish.yml/badge.svg)](https://rubygems.org/gems/ruby_tree_sitter)
5
+ [![ci](https://github.com/Faveod/ruby-tree-sitter/actions/workflows/ci.yml/badge.svg)](https://github.com/Faveod/ruby-tree-sitter/actions/workflows/ci.yml)
6
+ <!--
7
+ [![valgrind](https://github.com/Faveod/ruby-tree-sitter/actions/workflows/valgrind.yml/badge.svg)](https://github.com/Faveod/ruby-tree-sitter/actions/workflows/valgrind.yml)
8
+ [![asan/ubsan](https://github.com/Faveod/ruby-tree-sitter/actions/workflows/asan.yml/badge.svg)](https://github.com/Faveod/ruby-tree-sitter/actions/workflows/asan.yml)
9
+ -->
4
10
 
5
11
  Ruby bindings for [tree-sitter](https://github.com/tree-sitter/tree-sitter).
6
12
 
@@ -63,8 +69,7 @@ This gem is a binding for `tree-sitter`. It doesn't have a version of
63
69
 
64
70
  You must install `tree-sitter` and make sure that their dynamic library is
65
71
  accessible from `$PATH`, or build the gem with `--disable-sys-libs`, which will
66
- download the latest tagged `tree-sitter` and build against it (see [Build from
67
- source](docs/Contributing.md#build-from-source) .)
72
+ download the latest tagged `tree-sitter` and build against it (see [Build from source](docs/Contributing.md#build-from-source) .)
68
73
 
69
74
  You can either install `tree-sitter` from source or through your go-to package manager.
70
75
 
@@ -133,8 +138,7 @@ bundle config set build.ruby_tree_sitter --disable-sys-libs
133
138
  If you don't want to install from `rubygems`, `git`, or if you don't want to
134
139
  compile on install, then download a native gem from this repository's
135
140
  [releases](https://github.com/Faveod/ruby-tree-sitter/releases), or you can
136
- compile it yourself (see [Build from
137
- source](docs/Contributing.md#build-from-source) .)
141
+ compile it yourself (see [Build from source](docs/Contributing.md#build-from-source) .)
138
142
 
139
143
  In that case, you'd have to point your `Gemfile` to the `gem` as such:
140
144
 
@@ -171,7 +175,7 @@ See `examples` directory.
171
175
 
172
176
  ## Development
173
177
 
174
- See [`docs/README.md`](docs/Contributing.md).
178
+ See [`docs/Contributing.md`](docs/Contributing.md).
175
179
 
176
180
  ## 🚧 👷‍♀️ Notes 👷 🚧
177
181
 
@@ -186,7 +190,7 @@ don't copy them left and right, and then expect them to work without
186
190
  `SEGFAULT`ing and creating a black-hole in your living-room. Assume that you
187
191
  have to work locally with them. If you get a `SEGFAULT`, you can debug the
188
192
  native `C` code using `gdb`. You can read more on `SEGFAULT`s
189
- [here](docs/SIGSEGV.md), and debugging [here](docs/Contributing.md#Debugging).
193
+ [here](docs/SIGSEGV.md), and debugging [here](docs/Contributing#Debugging.md).
190
194
 
191
195
  That said, we do aim at providing an idiomatic `Ruby` interface. It should also
192
196
  provide a _safer_ interface, where you don't have to worry about when and how
@@ -32,7 +32,7 @@ VALUE new_language(const TSLanguage *language) {
32
32
  * with this gem.
33
33
  *
34
34
  * @param name [String] the parser's name.
35
- * @param path [String] the parser's shared library (so, dylib) path on disk.
35
+ * @param path [String, Pathname] the parser's shared library (so, dylib) path on disk.
36
36
  *
37
37
  * @return [Language]
38
38
  */
@@ -154,9 +154,9 @@ static void logger_initialize_stderr(logger_t *logger) {
154
154
  *
155
155
  * You can provide your proper backend. You have to make sure that it
156
156
  * exposes a +printf+, +puts+, or +write+ (lookup is done in that specific
157
- * order). {StringIO} is a perfect candidate.
157
+ * order). {::StringIO} is a perfect candidate.
158
158
  *
159
- * You can also provide a format ({String}) if your backend supports a +printf+.
159
+ * You can also provide a format ({::String}) if your backend supports a +printf+.
160
160
  *
161
161
  * @example
162
162
  * backend = StringIO.new
@@ -166,14 +166,19 @@ static VALUE node_child_by_field_id(VALUE self, VALUE field_id) {
166
166
  /**
167
167
  * Get the node's child with the given field name.
168
168
  *
169
- * @param field_name [String]
169
+ * @param field_name [String, Symbol]
170
170
  *
171
171
  * @return [Node]
172
172
  */
173
173
  static VALUE node_child_by_field_name(VALUE self, VALUE field_name) {
174
- const char *name = StringValuePtr(field_name);
175
- uint32_t length = (uint32_t)RSTRING_LEN(field_name);
176
- return new_node_by_val(ts_node_child_by_field_name(SELF, name, length));
174
+ if (Qtrue == rb_funcall(self, rb_intern("field?"), 1, field_name)) {
175
+ VALUE field_str = rb_funcall(field_name, rb_intern("to_s"), 0);
176
+ const char *name = StringValuePtr(field_str);
177
+ uint32_t length = (uint32_t)RSTRING_LEN(field_str);
178
+ return new_node_by_val(ts_node_child_by_field_name(SELF, name, length));
179
+ } else {
180
+ return Qnil;
181
+ }
177
182
  }
178
183
 
179
184
  /**
@@ -208,7 +208,7 @@ static VALUE parser_set_logger(VALUE self, VALUE logger) {
208
208
  * same arguments. Or you can start parsing from scratch by first calling
209
209
  * {Parser#reset}.
210
210
  * 3. Parsing was cancelled using a cancellation flag that was set by an
211
- * earlier call to {Parsert#cancellation_flag=}. You can resume parsing
211
+ * earlier call to {Parser#cancellation_flag=}. You can resume parsing
212
212
  * from where the parser left out by calling {Parser#parse} again with
213
213
  * the same arguments.
214
214
  *
@@ -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
@@ -13,28 +13,17 @@ module TreeSitter
13
13
  @fields << name.to_sym if name
14
14
  end
15
15
 
16
- @fields
16
+ @fields.to_a
17
17
  end
18
18
 
19
+ # @param field [String, Symbol]
19
20
  def field?(field)
20
- fields.include?(field)
21
+ fields.include?(field.to_sym)
21
22
  end
22
23
 
23
- # FIXME: These APIs (`[]` and `fetch`) need absolute fixing.
24
- # 1. The documentation with the table doesn't work.
25
- # 1. The APIs are very confusing! Make them act similarly to Hash's
26
- # `fetch` and `[]`.
27
- # 1. `[]` should take a single input and return nil if nothing found
28
- # (no exceptions).
29
- # 1. `fetch` should should accept a single argument, potentially a
30
- # default, and raise exception if no default was provided.
31
- # Also allow for the `all:` kwarg.
32
- # 1. `values_at` takes many arguments.
33
- # And I don't think we can move to 1.0 without adressing them.
34
- #
35
24
  # Access node's named children.
36
25
  #
37
- # It's similar to {#fetch}, but differes in input type, return values, and
26
+ # It's similar to {#fetch}, but differs in input type, return values, and
38
27
  # the internal implementation.
39
28
  #
40
29
  # Both of these methods exist for separate use cases, but also because
@@ -60,15 +49,15 @@ module TreeSitter
60
49
  # @return [Node | Array<Node>]
61
50
  def [](*keys)
62
51
  case keys.length
63
- when 0 then raise "#{self.class.name}##{__method__} requires a key."
52
+ when 0 then raise ArgumentError, "#{self.class.name}##{__method__} requires a key."
64
53
  when 1
65
54
  case k = keys.first
66
- when Numeric then named_child(k)
55
+ when Integer then named_child(k)
67
56
  when String, Symbol
68
- raise "Cannot find field #{k}" unless fields.include?(k.to_sym)
57
+ raise IndexError, "Cannot find field #{k}. Available: #{fields}" unless fields.include?(k.to_sym)
69
58
 
70
59
  child_by_field_name(k.to_s)
71
- else raise <<~ERR
60
+ else raise ArgumentError, <<~ERR
72
61
  #{self.class.name}##{__method__} accepts Integer and returns named child at given index,
73
62
  or a (String | Symbol) and returns the child by given field name.
74
63
  ERR
@@ -139,7 +128,7 @@ module TreeSitter
139
128
 
140
129
  # Access node's named children.
141
130
  #
142
- # It's similar to {#fetch}, but differes in input type, return values, and
131
+ # It's similar to {#[]}, but differs in input type, return values, and
143
132
  # the internal implementation.
144
133
  #
145
134
  # Both of these methods exist for separate use cases, but also because
@@ -159,39 +148,18 @@ module TreeSitter
159
148
  # uses named_child | field_name_for_child
160
149
  # child_by_field_name | via each_node
161
150
  # ------------------------------+----------------------
162
- # @param all [Boolean] If `true`, return an array of nodes for all the
163
- # demanded keys, putting `nil` for missing ones. If `false`, return the
164
- # same array after calling `compact`. Defaults to `false`.
165
- #
166
- # See {#fetch_all}.
167
- def fetch(*keys, all: false, **_kwargs)
168
- dict = {}
169
- keys.each.with_index do |k, i|
170
- dict[k.to_s] = i
171
- end
151
+ #
152
+ # See {#[]}.
153
+ def fetch(*keys)
154
+ keys = keys.map(&:to_s)
155
+ key_set = keys.to_set
156
+ fields = {}
157
+ each_field do |f, _c|
158
+ fields[f] = self[f] if key_set.delete(f)
172
159
 
173
- res = {}
174
- each_field do |f, c|
175
- if dict.key?(f)
176
- res[f] = c
177
- dict.delete(f)
178
- end
179
- break if dict.empty?
160
+ break if key_set.empty?
180
161
  end
181
-
182
- res = keys.uniq.map { |k| res[k.to_s] }
183
- res = res.compact if !all
184
- res
185
- end
186
-
187
- # Access all named children of a node, returning `nil` for missing ones.
188
- #
189
- # Equivalent to `fetch(…, all: true)`.
190
- #
191
- # See {#fetch}.
192
- def fetch_all(*keys, **kwargs)
193
- kwargs[:all] = true
194
- fetch(*keys, **kwargs)
162
+ fields.values_at(*keys)
195
163
  end
196
164
  end
197
165
  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,28 @@
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
+ def initialize(cursor, query, src)
7
+ @cursor = cursor
8
+ @query = query
9
+ @src = src
10
+ end
11
+
12
+ # Iterator over captures.
13
+ #
14
+ # @yieldparam match [TreeSitter::QueryMatch]
15
+ # @yieldparam capture_index [Integer]
16
+ def each
17
+ return enum_for __method__ if !block_given?
18
+
19
+ while (capture_index, match = @cursor.next_capture)
20
+ next if !match.is_a?(TreeSitter::QueryMatch)
21
+
22
+ if match.satisfies_text_predicate?(@query, @src)
23
+ yield [match, capture_index]
24
+ end
25
+ end
26
+ end
27
+ end
28
+ 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,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TreeSitter
4
+ # A sequence of {QueryMatch} associated with a given {QueryCursor}.
5
+ class QueryMatches
6
+ def initialize(cursor, query, src)
7
+ @cursor = cursor
8
+ @query = query
9
+ @src = src
10
+ end
11
+
12
+ # Iterator over matches.
13
+ #
14
+ # @yieldparam match [TreeSitter::QueryMatch]
15
+ def each
16
+ return enum_for __method__ if !block_given?
17
+
18
+ while match = @cursor.next_match
19
+ if match.satisfies_text_predicate?(@query, @src)
20
+ yield match
21
+ end
22
+ end
23
+ end
24
+ end
25
+ 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.1.0'
7
+ VERSION = '1.3.0'
8
8
  end
data/lib/tree_sitter.rb CHANGED
@@ -6,9 +6,16 @@ end
6
6
 
7
7
  require 'set'
8
8
 
9
+ require 'tree_sitter/tree_sitter'
9
10
  require 'tree_sitter/version'
10
11
 
11
- require 'tree_sitter/tree_sitter'
12
12
  require 'tree_sitter/node'
13
+ require 'tree_sitter/query'
14
+ require 'tree_sitter/query_captures'
15
+ require 'tree_sitter/query_cursor'
16
+ require 'tree_sitter/query_match'
17
+ require 'tree_sitter/query_matches'
18
+ require 'tree_sitter/query_predicate'
19
+ require 'tree_sitter/text_predicate_capture'
13
20
 
14
21
  ObjectSpace.define_finalizer(TreeSitter::Tree.class, proc { TreeSitter::Tree.finalizer })
@@ -1,13 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
  # typed: true
3
3
 
4
+ require 'pathname'
5
+
4
6
  module TreeStand
5
7
  # Global configuration for the gem.
6
8
  # @api private
7
9
  class Config
8
10
  extend T::Sig
9
11
 
10
- sig { returns(String) }
11
- attr_accessor :parser_path
12
+ sig { returns(T.nilable(Pathname)) }
13
+ attr_reader :parser_path
14
+
15
+ def parser_path=(path)
16
+ @parser_path = Pathname(path)
17
+ end
12
18
  end
13
19
  end
@@ -11,16 +11,60 @@ module TreeStand
11
11
  extend Forwardable
12
12
  include Enumerable
13
13
 
14
+ # @!method changed?
15
+ # @return [Boolean] true if a syntax node has been edited.
16
+ # @!method child_count
17
+ # @return [Integer] the number of child nodes.
18
+ # @!method extra?
19
+ # @return [Boolean] true if the node is *extra* (e.g. comments).
20
+ # @!method has_error?
21
+ # @return [Boolean] true if the node is a syntax error or contains any syntax errors.
22
+ # @!method missing?
23
+ # @return [Boolean] true if the parser inserted that node to recover from error.
24
+ # @!method named?
25
+ # @return [Boolean] true if the node is not a literal in the grammar.
26
+ # @!method named_child_count
27
+ # @return [Integer] the number of *named* children.
14
28
  # @!method type
15
29
  # @return [Symbol] the type of the node in the tree-sitter grammar.
16
30
  # @!method error?
17
31
  # @return [bool] true if the node is an error node.
18
32
  def_delegators(
19
33
  :@ts_node,
20
- :type,
34
+ :changed?,
35
+ :child_count,
21
36
  :error?,
37
+ :extra?,
38
+ :has_error?,
39
+ :missing?,
40
+ :named?,
41
+ :named_child_count,
42
+ :type,
22
43
  )
23
44
 
45
+ # These are methods defined in {TreeStand::Node} but map to something
46
+ # in {TreeSitter::Node}, because we want a more idiomatic API.
47
+ THINLY_REMAPPED_METHODS = {
48
+ '[]': :[],
49
+ fetch: :fetch,
50
+ field: :child_by_field_name,
51
+ next: :next_sibling,
52
+ prev: :prev_sibling,
53
+ next_named: :next_named_sibling,
54
+ prev_named: :prev_named_sibling,
55
+ field_names: :fields,
56
+ }.freeze
57
+
58
+ # These are methods from {TreeSitter} that are thinly wrapped to create
59
+ # {TreeStand::Node} instead.
60
+ THINLY_WRAPPED_METHODS = (
61
+ %i[
62
+ child
63
+ named_child
64
+ parent
65
+ ] + THINLY_REMAPPED_METHODS.keys
66
+ ).freeze
67
+
24
68
  sig { returns(TreeStand::Tree) }
25
69
  attr_reader :tree
26
70
 
@@ -32,7 +76,6 @@ module TreeStand
32
76
  def initialize(tree, ts_node)
33
77
  @tree = tree
34
78
  @ts_node = ts_node
35
- @fields = @ts_node.each_field.to_a.map(&:first)
36
79
  end
37
80
 
38
81
  # TreeSitter uses a `TreeSitter::Cursor` to iterate over matches by calling
@@ -131,6 +174,74 @@ module TreeStand
131
174
  enumerator
132
175
  end
133
176
 
177
+ # Enumerate named children.
178
+ # @example
179
+ # node.text # => "3 * 4"
180
+ #
181
+ # @example Iterate over the child nodes
182
+ # node.each_named do |child|
183
+ # print child.text
184
+ # end
185
+ # # prints: 34
186
+ #
187
+ # @example Enumerable methods
188
+ # node.each_named.map(&:text) # => ["3", "4"]
189
+ #
190
+ # @yieldparam child [TreeStand::Node]
191
+ sig do
192
+ params(block: T.nilable(T.proc.params(node: TreeStand::Node).returns(BasicObject)))
193
+ .returns(T::Enumerator[TreeStand::Node])
194
+ end
195
+ def each_named(&block)
196
+ enumerator = Enumerator.new do |yielder|
197
+ @ts_node.each_named do |child|
198
+ yielder << TreeStand::Node.new(@tree, child)
199
+ end
200
+ end
201
+ enumerator.each(&block) if block_given?
202
+ enumerator
203
+ end
204
+
205
+ # Iterate of (field, child).
206
+ #
207
+ # @example
208
+ # node.text # => "3 * 4"
209
+ #
210
+ # @example Iterate over the child nodes
211
+ # node.each_field do |field, child|
212
+ # puts "#{field}: #{child.text}"
213
+ # end
214
+ # # prints:
215
+ # # left: 3
216
+ # # right: 4
217
+ #
218
+ # @example Enumerable methods
219
+ # node.each_field.map { |f, c| "#{f}: #{c}" } # => ["left: 3", "right: 4"]
220
+ #
221
+ # @yieldparam field [Symbol]
222
+ # @yieldparam child [TreeStand::Node]
223
+ sig do
224
+ params(block: T.nilable(T.proc.params(node: TreeStand::Node).returns(BasicObject)))
225
+ .returns(T::Enumerator[[Symbol, TreeStand::Node]])
226
+ end
227
+ def each_field(&block)
228
+ enumerator = Enumerator.new do |yielder|
229
+ @ts_node.each_field do |field, child|
230
+ yielder << [field.to_sym, TreeStand::Node.new(@tree, child)]
231
+ end
232
+ end
233
+ enumerator.each(&block) if block_given?
234
+ enumerator
235
+ end
236
+
237
+ # @example Enumerable methods
238
+ # node.named.map(&:text) # => ["3", "4"]
239
+ alias_method :named, :each_named
240
+
241
+ # @example Enumerable methods
242
+ # node.fields.map { |f, c| "#{f}: #{c}" } # => ["left: 3", "right: 4"]
243
+ alias_method :fields, :each_field
244
+
134
245
  # (see TreeStand::Visitors::TreeWalker)
135
246
  # Backed by {TreeStand::Visitors::TreeWalker}.
136
247
  #
@@ -154,15 +265,6 @@ module TreeStand
154
265
  enumerator
155
266
  end
156
267
 
157
- # @example
158
- # node.text # => "4"
159
- # node.parent.text # => "3 * 4"
160
- # node.parent.parent.text # => "1 + 3 * 4"
161
- sig { returns(TreeStand::Node) }
162
- def parent
163
- TreeStand::Node.new(@tree, @ts_node.parent)
164
- end
165
-
166
268
  # @example
167
269
  # node.text # => "3 * 4"
168
270
  # node.to_a.map(&:text) # => ["3", "*", "4"]
@@ -174,7 +276,7 @@ module TreeStand
174
276
  # wraps the parent {TreeStand::Tree #tree} and has access to the source document.
175
277
  sig { returns(String) }
176
278
  def text
177
- T.must(@tree.document[@ts_node.start_byte...@ts_node.end_byte])
279
+ T.must(@tree.document.byteslice(@ts_node.start_byte...@ts_node.end_byte))
178
280
  end
179
281
 
180
282
  # This class overrides the `method_missing` method to delegate to the
@@ -193,10 +295,20 @@ module TreeStand
193
295
  #
194
296
  # @overload method_missing(method_name, *args, &block)
195
297
  # @raise [NoMethodError]
196
- def method_missing(method, *args, &block)
197
- return super unless @fields.include?(method.to_s)
198
-
199
- TreeStand::Node.new(@tree, T.unsafe(@ts_node).public_send(method, *args, &block))
298
+ def method_missing(method, *args, **kwargs, &block)
299
+ if thinly_wrapped?(method)
300
+ from(
301
+ T.unsafe(@ts_node)
302
+ .public_send(
303
+ THINLY_REMAPPED_METHODS[method] || method,
304
+ *args,
305
+ **kwargs,
306
+ &block
307
+ ),
308
+ )
309
+ else
310
+ super
311
+ end
200
312
  end
201
313
 
202
314
  sig { params(other: Object).returns(T::Boolean) }
@@ -217,8 +329,31 @@ module TreeStand
217
329
 
218
330
  private
219
331
 
220
- def respond_to_missing?(method, *)
221
- @fields.include?(method.to_s) || super
332
+ def respond_to_missing?(method, *_args, **_kwargs)
333
+ thinly_wrapped?(method) || super
334
+ end
335
+
336
+ def thinly_wrapped?(method)
337
+ @ts_node.fields.include?(method) || THINLY_WRAPPED_METHODS.include?(method)
338
+ end
339
+
340
+ # FIXME: Make more generic if needed in other classes.
341
+
342
+ # 1 instance of {TreeStand} from a {TreeSitter} equivalent.
343
+ def from_a(node)
344
+ node.is_a?(TreeSitter::Node) ? TreeStand::Node.new(@tree, node) : node
345
+ end
346
+
347
+ # {TreeSitter} instance, or a collection ({Array, Hash})
348
+ def from(obj)
349
+ case obj
350
+ when Array
351
+ obj.map { |n| from(n) }
352
+ when Hash
353
+ obj.to_h { |k, v| [from(k), from(v)] }
354
+ else
355
+ from_a(obj)
356
+ end
222
357
  end
223
358
  end
224
359
  end
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
  # typed: true
3
3
 
4
+ require 'pathname'
5
+
4
6
  module TreeStand
5
7
  # Wrapper around the TreeSitter parser. It looks up the parser by filename in
6
8
  # the configured parsers directory.
@@ -14,6 +16,14 @@ module TreeStand
14
16
  #
15
17
  # # Looks for a parser in `path/to/parser/folder/ruby.{so,dylib}`
16
18
  # ruby_parser = TreeStand::Parser.new("ruby")
19
+ #
20
+ # If no {TreeStand::Config#parser_path} is setup, {TreeStand} will lookup in a
21
+ # set of default paths. You can always override any configuration by passing
22
+ # the environment variable `TREE_SITTER_PARSERS` (colon-separated).
23
+ #
24
+ # @see language
25
+ # @see search_for_lib
26
+ # @see LIBDIRS
17
27
  class Parser
18
28
  extend T::Sig
19
29
 
@@ -23,14 +33,160 @@ module TreeStand
23
33
  sig { returns(TreeSitter::Parser) }
24
34
  attr_reader :ts_parser
25
35
 
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.tr('-', '_'), T.unsafe(lib))
184
+ end
185
+
26
186
  # @param language [String]
27
187
  sig { params(language: String).void }
28
188
  def initialize(language)
29
- @language_string = language
30
- @ts_language = TreeSitter::Language.load(
31
- language,
32
- "#{TreeStand.config.parser_path}/#{language}.so",
33
- )
189
+ @ts_language = Parser.language(language)
34
190
  @ts_parser = TreeSitter::Parser.new.tap do |parser|
35
191
  parser.language = @ts_language
36
192
  end
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.1.0
4
+ version: 1.3.0
5
5
  platform: arm64-darwin-22
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-10 00:00:00.000000000 Z
12
+ date: 2024-06-26 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: sorbet-runtime
@@ -74,7 +74,15 @@ 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
77
78
  - lib/tree_sitter/node.rb
79
+ - lib/tree_sitter/query.rb
80
+ - lib/tree_sitter/query_captures.rb
81
+ - lib/tree_sitter/query_cursor.rb
82
+ - lib/tree_sitter/query_match.rb
83
+ - lib/tree_sitter/query_matches.rb
84
+ - lib/tree_sitter/query_predicate.rb
85
+ - lib/tree_sitter/text_predicate_capture.rb
78
86
  - lib/tree_sitter/tree_sitter.bundle
79
87
  - lib/tree_sitter/version.rb
80
88
  - lib/tree_stand.rb