ruby_tree_sitter 1.1.0-x86_64-linux → 1.2.0-x86_64-linux
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +11 -7
- data/ext/tree_sitter/language.c +1 -1
- data/ext/tree_sitter/logger.c +2 -2
- data/ext/tree_sitter/node.c +9 -4
- data/ext/tree_sitter/parser.c +1 -1
- data/lib/tree_sitter/node.rb +19 -51
- data/lib/tree_sitter/tree_sitter.so +0 -0
- data/lib/tree_sitter/version.rb +1 -1
- data/lib/tree_stand/config.rb +8 -2
- data/lib/tree_stand/node.rb +153 -18
- data/lib/tree_stand/parser.rb +161 -5
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cfbfdfc0c297585fe24e04c4b9822ff404baa795182b7a7b144f56a3941db672
|
4
|
+
data.tar.gz: 010faa21be194e40cdf7f5f00986c16ee5d51d64f377097bfb307a4245ba61b6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b98898e1a8b801de3599e6daebd2adaa27b2ea729a46ebe730095d31e053dfb84c48a83712364a09fd58bac236f1b499f16ced6dcf4640902d0d4c12ac4ac7be
|
7
|
+
data.tar.gz: 4dd45cf49030a553819febb05cc8a0755ed549c413d44ea1c88836d4e2fad3c55138db3a3079776f2e14a828c38fd7d7b11664366ea8e84083098910528950b3
|
data/README.md
CHANGED
@@ -1,6 +1,12 @@
|
|
1
1
|
# Ruby tree-sitter bindings
|
2
2
|
|
3
|
-
[![
|
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/
|
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
|
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
|
data/ext/tree_sitter/language.c
CHANGED
@@ -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
|
*/
|
data/ext/tree_sitter/logger.c
CHANGED
@@ -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
|
data/ext/tree_sitter/node.c
CHANGED
@@ -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
|
-
|
175
|
-
|
176
|
-
|
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
|
/**
|
data/ext/tree_sitter/parser.c
CHANGED
@@ -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 {
|
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
|
*
|
data/lib/tree_sitter/node.rb
CHANGED
@@ -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
|
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
|
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 {#
|
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
|
-
#
|
163
|
-
#
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
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
|
-
|
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
|
Binary file
|
data/lib/tree_sitter/version.rb
CHANGED
data/lib/tree_stand/config.rb
CHANGED
@@ -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(
|
11
|
-
|
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
|
data/lib/tree_stand/node.rb
CHANGED
@@ -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
|
-
:
|
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
|
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
|
-
|
198
|
-
|
199
|
-
|
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
|
-
|
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
|
data/lib/tree_stand/parser.rb
CHANGED
@@ -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
|
-
@
|
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.
|
4
|
+
version: 1.2.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-
|
12
|
+
date: 2024-05-27 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: sorbet-runtime
|