ruby_tree_sitter 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7447395b7231af234e93a7cdaa245beb48e72b41121350365d71123ae170baa5
4
- data.tar.gz: 16bdb9be8c40402fcd135a177802108d70eb6c504541e045917af2c2045a109d
3
+ metadata.gz: 76f6accef62b96e6e67a815317815eee260d79c439e2efad49d2f235329d4b8e
4
+ data.tar.gz: 4affeaa090844fd467042247444f6cfdf5f98e63bc79e11cd8ff6b23ffac0795
5
5
  SHA512:
6
- metadata.gz: 80f1ba5a83de868997ebe8cb3308f5c5d1acc1ae6e4d18645446f2ebfff10884d27651749b013cff2e2f2aeefedaf26b51012fa490fdb26f6ebd4533b85f293c
7
- data.tar.gz: 7387367fc5cc306f80be04607ae08be2fd0ca5913bb9463129547682230620449d9449fe67d898f208975c4c14ea0d72f0b7ae2c84c84ea1d78e8b84091e4db6
6
+ metadata.gz: f0e629b66d07a20f5a4d9b274f7a16ec1ca3b748d66548878b58e153129b158844674eb7d8d512781ba7ab2a56cf9b97213587aadf6e66e00a55e595eb263390
7
+ data.tar.gz: b648d4546b019f336b7e84a945d6741bfb6ae320525abf9300d288f8e6b84059f5f2b4ecfa199502e5eae638f41b816ccaeb50693e509ae9cbb81d4f04f7c0e5
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
  *
@@ -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
@@ -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.2.0'
8
8
  end
@@ -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.gsub(/-/, '_'), 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.2.0
5
5
  platform: ruby
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-05-27 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: sorbet-runtime