tree_haver 7.1.9 → 7.1.10
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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/lib/tree_haver/backends/ffi.rb +1 -1
- data/lib/tree_haver/backends/rust.rb +1 -1
- data/lib/tree_haver/backends/rust_tslp.rb +301 -0
- data/lib/tree_haver/backends/tslp.rb +127 -17
- data/lib/tree_haver/grammar_finder.rb +22 -8
- data/lib/tree_haver/language_pack.rb +5 -0
- data/lib/tree_haver/version.rb +1 -1
- data/lib/tree_haver.rb +40 -4
- data.tar.gz.sig +0 -0
- metadata +22 -21
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c442758cd9e4d17f3088f4b501db45cae5a6b041db3fa3a31797bd2021b1a486
|
|
4
|
+
data.tar.gz: 33741a50778080021bdd02e11e87fe03f54e5631a2032d6dd51ab2a8c1434a1b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fcf407fba26bdba32f80ecebf92b6d946a6c176f187da0a6ee928e877723cd97a5bcca732811bf5695c8bb112a5b75614768f9e3d57cd9bcf4b50807e14fa390
|
|
7
|
+
data.tar.gz: f210d2e7d3673870097c56a22a2380a51a98620c2c39a6bf00ad2b66f54e4e96837ee26bc57caf8151eb0ae134d6e519090d9d1c15429c1a160e2e2d646e6468
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
|
@@ -613,7 +613,7 @@ module TreeHaver
|
|
|
613
613
|
ok = Native.ts_parser_set_language(@parser, ptr)
|
|
614
614
|
raise TreeHaver::NotAvailable, 'Failed to set language on parser' unless ok
|
|
615
615
|
|
|
616
|
-
lang #
|
|
616
|
+
lang # (intentional return value)
|
|
617
617
|
end
|
|
618
618
|
|
|
619
619
|
# Parse source code into a syntax tree
|
|
@@ -207,7 +207,7 @@ module TreeHaver
|
|
|
207
207
|
lang_name = lang.respond_to?(:name) ? lang.name : lang.to_s
|
|
208
208
|
# tree_stump uses set_language with a string name
|
|
209
209
|
@parser.set_language(lang_name)
|
|
210
|
-
lang #
|
|
210
|
+
lang # (intentional return value)
|
|
211
211
|
end
|
|
212
212
|
|
|
213
213
|
# Parse source code
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'digest'
|
|
5
|
+
|
|
6
|
+
module TreeHaver
|
|
7
|
+
module Backends
|
|
8
|
+
# Explicit Magnus bridge to the canonical Rust TreeHaver/TSLP substrate.
|
|
9
|
+
#
|
|
10
|
+
# This backend deliberately exposes only the normalized tree contract. It
|
|
11
|
+
# does not replace the direct Ruby language-pack or tree_stump backends.
|
|
12
|
+
module RustTslp
|
|
13
|
+
class << self
|
|
14
|
+
attr_reader :unavailable_reason
|
|
15
|
+
|
|
16
|
+
def available?
|
|
17
|
+
return @loaded if @load_attempted
|
|
18
|
+
|
|
19
|
+
@load_attempted = true
|
|
20
|
+
@loaded = load_host
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def reset!
|
|
24
|
+
@load_attempted = false
|
|
25
|
+
@loaded = false
|
|
26
|
+
@unavailable_reason = nil
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def capabilities
|
|
30
|
+
return {} unless available?
|
|
31
|
+
|
|
32
|
+
{
|
|
33
|
+
backend: :rust_tslp,
|
|
34
|
+
query: false,
|
|
35
|
+
bytes_field: true,
|
|
36
|
+
incremental: false,
|
|
37
|
+
comment_support: :nodes_only,
|
|
38
|
+
language_pack: true,
|
|
39
|
+
provenance: :rust_tree_haver
|
|
40
|
+
}
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def register_language_parser(language)
|
|
44
|
+
return unless available?
|
|
45
|
+
|
|
46
|
+
# Cache only registrations made by this adapter, never adopt a foreign
|
|
47
|
+
# duplicate ID. An external removal therefore fails closed on parse.
|
|
48
|
+
REGISTRATION_MUTEX.synchronize do
|
|
49
|
+
@registrations ||= {}
|
|
50
|
+
key = [::StructuredmergeCore, language.to_s]
|
|
51
|
+
@registrations[key] ||= ::StructuredmergeCore.register_language_pack_parser(
|
|
52
|
+
"tree_haver.rust_tslp.#{language}", language.to_s
|
|
53
|
+
).id
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
# rubocop:disable Metrics/MethodLength
|
|
60
|
+
def load_host
|
|
61
|
+
unless RUBY_ENGINE == 'ruby'
|
|
62
|
+
@unavailable_reason = 'the Rust TreeHaver Magnus bridge requires MRI Ruby'
|
|
63
|
+
return false
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
require 'structuredmerge_core' unless defined?(::StructuredmergeCore)
|
|
67
|
+
unless ::StructuredmergeCore.respond_to?(:parse_sources) && ::StructuredmergeCore.respond_to?(:register_language_pack_parser)
|
|
68
|
+
@unavailable_reason = 'structuredmerge-core does not expose typed Rust parser registration'
|
|
69
|
+
return false
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
true
|
|
73
|
+
rescue LoadError => e
|
|
74
|
+
@unavailable_reason = e.message
|
|
75
|
+
false
|
|
76
|
+
rescue StandardError => e
|
|
77
|
+
@unavailable_reason = e.message
|
|
78
|
+
false
|
|
79
|
+
end
|
|
80
|
+
# rubocop:enable Metrics/MethodLength
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
REGISTRATION_MUTEX = Mutex.new
|
|
84
|
+
|
|
85
|
+
# Identifies a language parsed by the canonical Rust substrate.
|
|
86
|
+
class Language < TreeHaver::Base::Language
|
|
87
|
+
def initialize(name)
|
|
88
|
+
super(name, backend: :rust_tslp, options: {})
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
class << self
|
|
92
|
+
def from_library(_path = nil, symbol: nil, name: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
93
|
+
new(name || :unknown)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Parses source into the Rust normalized tree representation.
|
|
99
|
+
class Parser < TreeHaver::Base::Parser
|
|
100
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
101
|
+
def parse(source)
|
|
102
|
+
raise TreeHaver::NotAvailable, unavailable_message unless RustTslp.available?
|
|
103
|
+
raise TreeHaver::NotAvailable, 'Rust TSLP language is not set' unless language
|
|
104
|
+
|
|
105
|
+
normalized_source = normalize_source_encoding(source)
|
|
106
|
+
core = ::StructuredmergeCore
|
|
107
|
+
provider_id = RustTslp.register_language_parser(language.name)
|
|
108
|
+
crlf = normalized_source.scan("\r\n").length
|
|
109
|
+
descriptor = core::SourceDescriptor.new(
|
|
110
|
+
source_id: 'tree_haver.source', role: 'source', byte_length: normalized_source.bytesize,
|
|
111
|
+
sha256: Digest::SHA256.hexdigest(normalized_source), encoding: 'utf8',
|
|
112
|
+
bom: normalized_source.start_with?("\uFEFF"), final_newline: normalized_source.end_with?("\n", "\r"),
|
|
113
|
+
line_endings: core::LineEndings.new(lf: normalized_source.count("\n") - crlf,
|
|
114
|
+
crlf: crlf, bare_cr: normalized_source.count("\r") - crlf)
|
|
115
|
+
)
|
|
116
|
+
request = core::ParseRequest.new(
|
|
117
|
+
schema: 'structuredmerge.parse-request/v1', request_id: 'tree_haver.parse',
|
|
118
|
+
source: core::SourceInput.new(descriptor: descriptor, bytes: normalized_source.bytes),
|
|
119
|
+
language: language.name.to_s, dialect: nil,
|
|
120
|
+
selection: core::ParserSelection.new(backend_id: provider_id, preference: [], required_capabilities: []),
|
|
121
|
+
options: core::ParseOptions.new(comments: true, diagnostics: true, tokens: false, native_extensions: true),
|
|
122
|
+
metadata: {}, extra: {}
|
|
123
|
+
)
|
|
124
|
+
limits = core::ParseLimits.new(max_batch_items: 1, max_input_bytes: 64 * 1024 * 1024,
|
|
125
|
+
max_nodes: 1_000_000, max_diagnostics: 1000)
|
|
126
|
+
result = core.parse_sources([request], limits).fetch(0)
|
|
127
|
+
Tree.new(result, source: normalized_source, language: language.name)
|
|
128
|
+
end
|
|
129
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
130
|
+
|
|
131
|
+
private
|
|
132
|
+
|
|
133
|
+
def normalize_source_encoding(source)
|
|
134
|
+
if [Encoding::UTF_8, Encoding::BINARY, Encoding::US_ASCII].include?(source.encoding)
|
|
135
|
+
utf8 = source.dup.force_encoding(Encoding::UTF_8)
|
|
136
|
+
return utf8.freeze if utf8.valid_encoding?
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
raise TreeHaver::NotAvailable, 'Rust TreeHaver normalized parsing requires valid UTF-8 source bytes'
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def unavailable_message
|
|
143
|
+
detail = RustTslp.unavailable_reason.to_s
|
|
144
|
+
detail = 'unknown reason' if detail.empty?
|
|
145
|
+
"Rust TreeHaver normalized parser is unavailable: #{detail}"
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Wraps one validated Rust normalized parse result.
|
|
150
|
+
class Tree < TreeHaver::Base::Tree
|
|
151
|
+
attr_reader :diagnostics, :language, :provenance
|
|
152
|
+
|
|
153
|
+
def initialize(result, source:, language:)
|
|
154
|
+
validate_result!(result)
|
|
155
|
+
super(result, source: source)
|
|
156
|
+
@language = language.to_s
|
|
157
|
+
@nodes_by_id = result.parsed.nodes.to_h { |node| [node.id, node] }
|
|
158
|
+
@root_id = result.parsed.root_id
|
|
159
|
+
@diagnostics = result.parsed.diagnostics.map(&:message)
|
|
160
|
+
@provenance = {
|
|
161
|
+
'backend_ref' => { 'id' => result.backend.id },
|
|
162
|
+
'language' => @language, 'runtime' => result.backend.runtime,
|
|
163
|
+
'parser' => result.backend.parser, 'parser_version' => result.backend.parser_version
|
|
164
|
+
}
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def root_node
|
|
168
|
+
Node.new(
|
|
169
|
+
@nodes_by_id.fetch(@root_id),
|
|
170
|
+
nodes_by_id: @nodes_by_id,
|
|
171
|
+
source: source,
|
|
172
|
+
lines: lines,
|
|
173
|
+
language: language
|
|
174
|
+
)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def errors
|
|
178
|
+
diagnostics
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
private
|
|
182
|
+
|
|
183
|
+
def validate_result!(result)
|
|
184
|
+
nodes = result.parsed.nodes
|
|
185
|
+
root_id = result.parsed.root_id
|
|
186
|
+
valid_root = root_id.is_a?(String) && nodes.any? { |node| node.id == root_id }
|
|
187
|
+
return if valid_root
|
|
188
|
+
|
|
189
|
+
raise TreeHaver::Error, 'Rust TreeHaver returned an invalid normalized tree'
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Adapts normalized node records to TreeHaver's node contract.
|
|
194
|
+
class Node < TreeHaver::Base::Node
|
|
195
|
+
# Preserve the established TreeHaver surface when the two language-pack
|
|
196
|
+
# implementations expose equivalent JSON5 grammar nodes under different
|
|
197
|
+
# native names.
|
|
198
|
+
NODE_TYPE_ALIASES = {
|
|
199
|
+
'json5' => {
|
|
200
|
+
'file' => 'document',
|
|
201
|
+
'member' => 'pair'
|
|
202
|
+
}
|
|
203
|
+
}.freeze
|
|
204
|
+
|
|
205
|
+
attr_reader :language
|
|
206
|
+
|
|
207
|
+
def initialize(node, nodes_by_id:, source: nil, lines: nil, language: nil)
|
|
208
|
+
super(node, source: source, lines: lines)
|
|
209
|
+
@nodes_by_id = nodes_by_id
|
|
210
|
+
@language = language.to_s
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def type
|
|
214
|
+
NODE_TYPE_ALIASES.fetch(language, {}).fetch(native_type, native_type)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def native_type = inner_node.native_type
|
|
218
|
+
|
|
219
|
+
def start_byte = span.range.start_byte
|
|
220
|
+
def end_byte = span.range.end_byte
|
|
221
|
+
def start_point = symbolize_point(span.start_point)
|
|
222
|
+
def end_point = symbolize_point(span.end_point)
|
|
223
|
+
def child_count = inner_node.children.length
|
|
224
|
+
def named? = inner_node.named
|
|
225
|
+
|
|
226
|
+
def has_error? # rubocop:disable Naming/PredicatePrefix
|
|
227
|
+
inner_node.has_error
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def error? = has_error?
|
|
231
|
+
def missing? = inner_node.missing
|
|
232
|
+
|
|
233
|
+
def extra?
|
|
234
|
+
extension = inner_node.extensions.find do |item|
|
|
235
|
+
item.schema == 'tree-haver.tree-sitter.node/v1' && item.namespace == 'tree-sitter'
|
|
236
|
+
end
|
|
237
|
+
raise TreeHaver::Error, 'Rust TreeHaver omitted native node flags' unless extension
|
|
238
|
+
|
|
239
|
+
# Alef transports only the open extension payload as JSON, not the tree.
|
|
240
|
+
JSON.parse(extension.payload).fetch('extra')
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def text
|
|
244
|
+
source.byteslice(start_byte...end_byte)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def child(index)
|
|
248
|
+
edge = inner_node.children[index]
|
|
249
|
+
edge && wrap(@nodes_by_id.fetch(edge.node_id))
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def children
|
|
253
|
+
inner_node.children.map { |edge| wrap(@nodes_by_id.fetch(edge.node_id)) }
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def child_by_field_name(name)
|
|
257
|
+
edge = inner_node.children.find { |item| item.field_name == name.to_s }
|
|
258
|
+
edge && wrap(@nodes_by_id.fetch(edge.node_id))
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def parent
|
|
262
|
+
parent_id = inner_node.parent_id
|
|
263
|
+
parent_id && wrap(@nodes_by_id.fetch(parent_id))
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def next_sibling
|
|
267
|
+
sibling_at(1)
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def prev_sibling
|
|
271
|
+
sibling_at(-1)
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
private
|
|
275
|
+
|
|
276
|
+
def span = inner_node.span
|
|
277
|
+
|
|
278
|
+
def symbolize_point(point)
|
|
279
|
+
{ row: point.row, column: point.column }
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def wrap(node)
|
|
283
|
+
self.class.new(node, nodes_by_id: @nodes_by_id, source: source, lines: lines, language: language)
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def sibling_at(offset)
|
|
287
|
+
parent_node = parent
|
|
288
|
+
return unless parent_node
|
|
289
|
+
|
|
290
|
+
siblings = parent_node.inner_node.children.map(&:node_id)
|
|
291
|
+
index = siblings.index(inner_node.id)
|
|
292
|
+
sibling_index = index&.+(offset)
|
|
293
|
+
return unless sibling_index&.between?(0, siblings.length - 1)
|
|
294
|
+
|
|
295
|
+
sibling_id = siblings[sibling_index]
|
|
296
|
+
sibling_id && wrap(@nodes_by_id.fetch(sibling_id))
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
end
|
|
@@ -8,11 +8,20 @@ module TreeHaver
|
|
|
8
8
|
# process API is not a TreeHaver parser backend and must not be used as a
|
|
9
9
|
# merge-gem integration surface.
|
|
10
10
|
module Tslp
|
|
11
|
-
|
|
11
|
+
# Transient grammar hot-load failures (for example a failed on-demand
|
|
12
|
+
# download of the tree-sitter-language-pack bundle) are retried on later
|
|
13
|
+
# availability probes instead of latching for the whole process. After
|
|
14
|
+
# this many consecutive transient failures the backend latches as
|
|
15
|
+
# unavailable so a dead network cannot slow every later parse attempt.
|
|
16
|
+
TRANSIENT_FAILURE_RETRY_LIMIT = 3
|
|
17
|
+
|
|
12
18
|
@loaded = false
|
|
19
|
+
@permanently_unavailable = false
|
|
20
|
+
@transient_failures = 0
|
|
13
21
|
@unavailable_reason = nil
|
|
14
22
|
@language_availability = {}
|
|
15
23
|
@language_unavailable_reasons = {}
|
|
24
|
+
@language_attempts = {}
|
|
16
25
|
PARSER_SMOKE_SOURCES = {
|
|
17
26
|
'json' => '{}',
|
|
18
27
|
'json5' => '{}',
|
|
@@ -28,35 +37,91 @@ module TreeHaver
|
|
|
28
37
|
}.freeze
|
|
29
38
|
DEFAULT_PARSER_SMOKE_SOURCE = ''
|
|
30
39
|
|
|
40
|
+
# Grammar hot-load failures caused by the on-demand bundle download
|
|
41
|
+
# (network flake, transient GitHub release fetch failure, cache lock
|
|
42
|
+
# contention) must not latch the backend for the whole process.
|
|
43
|
+
TRANSIENT_FAILURE_PATTERN = /
|
|
44
|
+
Download\ error
|
|
45
|
+
| Download\ cache\ lock\ error
|
|
46
|
+
| Failed\ to\ fetch\ manifest
|
|
47
|
+
| Failed\ to\ download
|
|
48
|
+
| Failed\ to\ read\ (cached|download)
|
|
49
|
+
| connection\ (refused|reset)
|
|
50
|
+
| timed?\ out
|
|
51
|
+
| timeout
|
|
52
|
+
| temporarily
|
|
53
|
+
/xi
|
|
54
|
+
|
|
31
55
|
class << self
|
|
32
56
|
attr_reader :unavailable_reason
|
|
33
57
|
|
|
34
58
|
def available?
|
|
35
|
-
return
|
|
59
|
+
return true if @loaded
|
|
60
|
+
return false if @permanently_unavailable
|
|
61
|
+
|
|
62
|
+
@loaded = probe_availability
|
|
63
|
+
return true if @loaded
|
|
64
|
+
|
|
65
|
+
if transient_reason?(@unavailable_reason) && @transient_failures < TRANSIENT_FAILURE_RETRY_LIMIT
|
|
66
|
+
# Leave the backend probeable so a later availability check retries
|
|
67
|
+
# the on-demand grammar download instead of failing every parse for
|
|
68
|
+
# the rest of the process.
|
|
69
|
+
@transient_failures += 1
|
|
70
|
+
return false
|
|
71
|
+
end
|
|
36
72
|
|
|
37
|
-
@
|
|
73
|
+
@permanently_unavailable = true
|
|
74
|
+
false
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Reason the last per-language probe failed, when known.
|
|
78
|
+
def language_unavailable_reason(language_name)
|
|
79
|
+
@language_unavailable_reasons[language_name.to_s]
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Pre-download and pre-load grammars through the language pack's
|
|
83
|
+
# hot-load API before any parse/smoke probing happens. This turns the
|
|
84
|
+
# on-demand download (which otherwise fires inside the first
|
|
85
|
+
# availability probe and can transiently fail) into an explicit,
|
|
86
|
+
# attributable warm-up step.
|
|
87
|
+
#
|
|
88
|
+
# @param languages [Array<String, Symbol>] grammar names to prefetch
|
|
89
|
+
# @return [Hash] {attempted:, prefetched:, failures: {name => reason}}
|
|
90
|
+
def prefetch(languages)
|
|
91
|
+
names = languages.map(&:to_s).uniq
|
|
38
92
|
begin
|
|
39
93
|
require 'tree_sitter_language_pack' unless defined?(::TreeSitterLanguagePack)
|
|
40
|
-
@loaded = parser_api_available?
|
|
41
|
-
if !@loaded && @unavailable_reason.to_s.empty?
|
|
42
|
-
@unavailable_reason = 'tree_sitter_language_pack parser API is not exposed'
|
|
43
|
-
end
|
|
44
94
|
rescue LoadError => e
|
|
45
|
-
|
|
46
|
-
|
|
95
|
+
return prefetch_skip_report('tree_sitter_language_pack is not installed', e.message, names)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
unless ::TreeSitterLanguagePack.respond_to?(:prefetch)
|
|
99
|
+
return prefetch_skip_report('tree_sitter_language_pack does not expose prefetch', nil, names)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
failures = {}
|
|
103
|
+
prefetched = names.select do |name|
|
|
104
|
+
::TreeSitterLanguagePack.prefetch([name])
|
|
105
|
+
true
|
|
47
106
|
rescue StandardError => e
|
|
48
|
-
|
|
49
|
-
|
|
107
|
+
failures[name] = e.message
|
|
108
|
+
false
|
|
50
109
|
end
|
|
51
|
-
|
|
110
|
+
{ attempted: true, prefetched: prefetched, failures: failures }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def transient_reason?(reason)
|
|
114
|
+
!reason.to_s.empty? && TRANSIENT_FAILURE_PATTERN.match?(reason.to_s)
|
|
52
115
|
end
|
|
53
116
|
|
|
54
117
|
def reset!
|
|
55
|
-
@load_attempted = false
|
|
56
118
|
@loaded = false
|
|
119
|
+
@permanently_unavailable = false
|
|
120
|
+
@transient_failures = 0
|
|
57
121
|
@unavailable_reason = nil
|
|
58
122
|
@language_availability = {}
|
|
59
123
|
@language_unavailable_reasons = {}
|
|
124
|
+
@language_attempts = {}
|
|
60
125
|
end
|
|
61
126
|
|
|
62
127
|
def capabilities
|
|
@@ -78,17 +143,62 @@ module TreeHaver
|
|
|
78
143
|
name = language_name.to_s
|
|
79
144
|
return @language_availability.fetch(name) if @language_availability.key?(name)
|
|
80
145
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
146
|
+
available = smoke_parse_language(name)
|
|
147
|
+
if available
|
|
148
|
+
@language_availability[name] = true
|
|
149
|
+
@language_unavailable_reasons.delete(name)
|
|
150
|
+
return true
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
reason = @language_unavailable_reasons[name] = @unavailable_reason
|
|
154
|
+
attempts = (@language_attempts[name] = @language_attempts.fetch(name, 0) + 1)
|
|
155
|
+
if transient_reason?(reason) && attempts < TRANSIENT_FAILURE_RETRY_LIMIT
|
|
156
|
+
# Do not memoize: a later probe retries the on-demand hot load.
|
|
157
|
+
return false
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
@language_availability[name] = false
|
|
161
|
+
false
|
|
84
162
|
rescue StandardError => e
|
|
163
|
+
name = language_name.to_s
|
|
85
164
|
@unavailable_reason = e.message
|
|
86
|
-
@language_unavailable_reasons[
|
|
165
|
+
@language_unavailable_reasons[name] = e.message
|
|
166
|
+
attempts = (@language_attempts[name] = @language_attempts.fetch(name, 0) + 1)
|
|
167
|
+
# Memoize only after exhausting the transient retry budget; raising
|
|
168
|
+
# here is how the on-demand grammar download reports failures.
|
|
169
|
+
@language_availability[name] = false if attempts >= TRANSIENT_FAILURE_RETRY_LIMIT
|
|
87
170
|
false
|
|
88
171
|
end
|
|
89
172
|
|
|
90
173
|
private
|
|
91
174
|
|
|
175
|
+
def prefetch_skip_report(reason, detail, names)
|
|
176
|
+
@unavailable_reason = detail if detail
|
|
177
|
+
{
|
|
178
|
+
attempted: false,
|
|
179
|
+
reason: reason,
|
|
180
|
+
prefetched: [],
|
|
181
|
+
failures: names.to_h { |name| [name, reason] }
|
|
182
|
+
}
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def probe_availability
|
|
186
|
+
require 'tree_sitter_language_pack' unless defined?(::TreeSitterLanguagePack)
|
|
187
|
+
loaded = parser_api_available?
|
|
188
|
+
if !loaded && @unavailable_reason.to_s.empty?
|
|
189
|
+
@unavailable_reason = 'tree_sitter_language_pack parser API is not exposed'
|
|
190
|
+
end
|
|
191
|
+
loaded
|
|
192
|
+
rescue LoadError => e
|
|
193
|
+
# A missing gem is permanent for the process lifetime.
|
|
194
|
+
@unavailable_reason = e.message
|
|
195
|
+
@permanently_unavailable = true
|
|
196
|
+
false
|
|
197
|
+
rescue StandardError => e
|
|
198
|
+
@unavailable_reason = e.message
|
|
199
|
+
false
|
|
200
|
+
end
|
|
201
|
+
|
|
92
202
|
def parser_api_available?
|
|
93
203
|
return false unless ::TreeSitterLanguagePack.respond_to?(:get_parser)
|
|
94
204
|
return false unless defined?(::TreeSitterLanguagePack::Parser)
|
|
@@ -314,12 +314,14 @@ module TreeHaver
|
|
|
314
314
|
# @raise [NotAvailable] if library not found and raise_on_missing is true
|
|
315
315
|
def register!(raise_on_missing: false)
|
|
316
316
|
if tree_sitter_language_pack_parser_available?
|
|
317
|
+
backend_module, backend_type, gem_name = language_pack_registration
|
|
317
318
|
TreeHaver.register_language(
|
|
318
319
|
@language_name,
|
|
319
|
-
backend_module:
|
|
320
|
-
backend_type:
|
|
321
|
-
gem_name:
|
|
320
|
+
backend_module: backend_module,
|
|
321
|
+
backend_type: backend_type,
|
|
322
|
+
gem_name: gem_name
|
|
322
323
|
)
|
|
324
|
+
backend_module.register_language_parser(@language_name) if backend_type == :rust_tslp
|
|
323
325
|
return true
|
|
324
326
|
end
|
|
325
327
|
|
|
@@ -377,6 +379,14 @@ module TreeHaver
|
|
|
377
379
|
|
|
378
380
|
private
|
|
379
381
|
|
|
382
|
+
def language_pack_registration
|
|
383
|
+
if TreeHaver.effective_backend == :rust_tslp
|
|
384
|
+
[TreeHaver::Backends::RustTslp, :rust_tslp, 'structuredmerge-core']
|
|
385
|
+
else
|
|
386
|
+
[TreeHaver::Backends::Tslp, :tslp, 'tree_sitter_language_pack']
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
|
|
380
390
|
def registered_tree_sitter_registration
|
|
381
391
|
TreeHaver::LanguageRegistry.registered(@language_name, :tree_sitter)
|
|
382
392
|
end
|
|
@@ -399,11 +409,15 @@ module TreeHaver
|
|
|
399
409
|
return @tree_sitter_language_pack_parser_available if defined?(@tree_sitter_language_pack_parser_available)
|
|
400
410
|
|
|
401
411
|
@tree_sitter_language_pack_parser_available = begin
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
::TreeSitterLanguagePack
|
|
406
|
-
TreeHaver::Backends::Tslp.
|
|
412
|
+
if TreeHaver.effective_backend == :rust_tslp
|
|
413
|
+
TreeHaver::Backends::RustTslp.available?
|
|
414
|
+
else
|
|
415
|
+
require 'tree_sitter_language_pack' unless defined?(::TreeSitterLanguagePack)
|
|
416
|
+
TreeHaver::Backends::Tslp.available? &&
|
|
417
|
+
::TreeSitterLanguagePack.respond_to?(:has_language) &&
|
|
418
|
+
::TreeSitterLanguagePack.has_language(@language_name.to_s) &&
|
|
419
|
+
TreeHaver::Backends::Tslp.parser_available_for?(@language_name)
|
|
420
|
+
end
|
|
407
421
|
rescue LoadError
|
|
408
422
|
false
|
|
409
423
|
rescue StandardError => e
|
|
@@ -9,9 +9,14 @@ module TreeHaver
|
|
|
9
9
|
id: 'kreuzberg-language-pack',
|
|
10
10
|
family: 'tree-sitter'
|
|
11
11
|
).freeze
|
|
12
|
+
RUST_TSLP_BACKEND = BackendReference.new(
|
|
13
|
+
id: 'rust_tslp',
|
|
14
|
+
family: 'tree-sitter'
|
|
15
|
+
).freeze
|
|
12
16
|
|
|
13
17
|
BackendRegistry.register(TSLP_BACKEND)
|
|
14
18
|
BackendRegistry.register(KREUZBERG_LANGUAGE_PACK_BACKEND)
|
|
19
|
+
BackendRegistry.register(RUST_TSLP_BACKEND)
|
|
15
20
|
BackendRegistry.register_availability_checker(:tslp) do
|
|
16
21
|
Backends::Tslp.available?
|
|
17
22
|
end
|
data/lib/tree_haver/version.rb
CHANGED
|
@@ -4,7 +4,7 @@ module TreeHaver
|
|
|
4
4
|
# Version namespace for this gem.
|
|
5
5
|
module Version
|
|
6
6
|
# Current gem version.
|
|
7
|
-
VERSION = '7.1.
|
|
7
|
+
VERSION = '7.1.10'
|
|
8
8
|
end
|
|
9
9
|
# Current gem version exposed at the traditional constant location.
|
|
10
10
|
VERSION = Version::VERSION # Traditional Constant Location
|
data/lib/tree_haver.rb
CHANGED
|
@@ -32,6 +32,7 @@ require_relative 'tree_haver/backends/ffi'
|
|
|
32
32
|
require_relative 'tree_haver/backends/rust'
|
|
33
33
|
require_relative 'tree_haver/backends/java'
|
|
34
34
|
require_relative 'tree_haver/backends/tslp'
|
|
35
|
+
require_relative 'tree_haver/backends/rust_tslp'
|
|
35
36
|
require_relative 'tree_haver/grammar_finder'
|
|
36
37
|
require_relative 'tree_haver/citrus_grammar_finder'
|
|
37
38
|
require_relative 'tree_haver/parslet_grammar_finder'
|
|
@@ -50,7 +51,8 @@ module TreeHaver
|
|
|
50
51
|
RUBY_BACKENDS = %i[citrus parslet prism psych commonmarker markly rbs].freeze
|
|
51
52
|
VALID_NATIVE_BACKENDS = NATIVE_BACKENDS.map(&:to_s).freeze
|
|
52
53
|
VALID_RUBY_BACKENDS = RUBY_BACKENDS.map(&:to_s).freeze
|
|
53
|
-
VALID_BACKENDS = (VALID_NATIVE_BACKENDS + VALID_RUBY_BACKENDS + %w[auto none tslp
|
|
54
|
+
VALID_BACKENDS = (VALID_NATIVE_BACKENDS + VALID_RUBY_BACKENDS + %w[auto none tslp rust_tslp
|
|
55
|
+
kreuzberg-language-pack]).freeze
|
|
54
56
|
DEFAULT_BACKEND_ID = 'tslp'
|
|
55
57
|
NATIVE_BACKEND_REFERENCES = NATIVE_BACKENDS.to_h do |backend_name|
|
|
56
58
|
[
|
|
@@ -76,6 +78,9 @@ module TreeHaver
|
|
|
76
78
|
BackendRegistry.register_tag(:java_backend, category: :backend, backend_name: :java) do
|
|
77
79
|
Backends::Java.available?
|
|
78
80
|
end
|
|
81
|
+
BackendRegistry.register_tag(:rust_tslp_backend, category: :backend, backend_name: :rust_tslp) do
|
|
82
|
+
Backends::RustTslp.available?
|
|
83
|
+
end
|
|
79
84
|
end
|
|
80
85
|
|
|
81
86
|
def default_backend_id
|
|
@@ -253,6 +258,19 @@ module TreeHaver
|
|
|
253
258
|
LanguageRegistry.registered(name)
|
|
254
259
|
end
|
|
255
260
|
|
|
261
|
+
# Warm up language-pack grammars through their hot-load/prefetch API so the
|
|
262
|
+
# on-demand grammar download happens explicitly, up front, instead of firing
|
|
263
|
+
# inside the first availability smoke probe during a merge.
|
|
264
|
+
#
|
|
265
|
+
# Safe to call with the TSLP backend unavailable or absent: reports
|
|
266
|
+
# attempted: false with a reason instead of raising.
|
|
267
|
+
#
|
|
268
|
+
# @param languages [Array<String, Symbol>] grammar names to prefetch
|
|
269
|
+
# @return [Hash] prefetch report (see Backends::Tslp.prefetch)
|
|
270
|
+
def prefetch_languages(languages)
|
|
271
|
+
Backends::Tslp.prefetch(languages)
|
|
272
|
+
end
|
|
273
|
+
|
|
256
274
|
def register_language(name, path: nil, symbol: nil, grammar_module: nil, grammar_class: nil, backend_module: nil,
|
|
257
275
|
backend_type: nil, gem_name: nil, contract: nil)
|
|
258
276
|
LanguageRegistry.register(name, :tree_sitter, path: path, symbol: symbol, contract: contract) if path
|
|
@@ -340,7 +358,21 @@ module TreeHaver
|
|
|
340
358
|
return parser_for_registered_backend(name, backend_type, registrations)
|
|
341
359
|
end
|
|
342
360
|
|
|
343
|
-
raise NotAvailable,
|
|
361
|
+
raise NotAvailable, no_parser_registered_message(name)
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
# Build the "No parser registered" error message, surfacing why the
|
|
365
|
+
# tree-sitter-language-pack backend declined the language (for example a
|
|
366
|
+
# failed on-demand grammar download) so CI logs are attributable instead of
|
|
367
|
+
# misleading.
|
|
368
|
+
def no_parser_registered_message(name)
|
|
369
|
+
message = "No parser registered for #{name}"
|
|
370
|
+
language_reason = Backends::Tslp.language_unavailable_reason(name)
|
|
371
|
+
backend_reason = Backends::Tslp.unavailable_reason
|
|
372
|
+
reasons = [language_reason, backend_reason].map(&:to_s).reject(&:empty?).uniq
|
|
373
|
+
return message if reasons.empty?
|
|
374
|
+
|
|
375
|
+
"#{message} (tree_sitter_language_pack: #{reasons.join('; ')})"
|
|
344
376
|
end
|
|
345
377
|
|
|
346
378
|
def ruby_reference_parser_backend_contract_report
|
|
@@ -407,7 +439,9 @@ module TreeHaver
|
|
|
407
439
|
elsif (registered_type = registered_backend_type_for_requested_backend(backend_id, registrations))
|
|
408
440
|
registered_type
|
|
409
441
|
elsif backend_ref&.family == 'tree-sitter'
|
|
410
|
-
|
|
442
|
+
registrations.find do |registered_type, config|
|
|
443
|
+
registration_contract(registered_type, config) == :tree_sitter
|
|
444
|
+
end&.first || :tree_sitter
|
|
411
445
|
else
|
|
412
446
|
backend_id.to_s.to_sym
|
|
413
447
|
end
|
|
@@ -452,7 +486,7 @@ module TreeHaver
|
|
|
452
486
|
def parser_for_registered_backend(name, backend_type, registrations)
|
|
453
487
|
config = registrations.fetch(backend_type)
|
|
454
488
|
case backend_type
|
|
455
|
-
when :psych, :prism, :commonmarker, :markly, :rbs
|
|
489
|
+
when :psych, :prism, :commonmarker, :markly, :rbs, :rust_tslp
|
|
456
490
|
parser_for_backend_module(config.fetch(:backend_module), name)
|
|
457
491
|
when :citrus
|
|
458
492
|
parser_for_citrus(config.fetch(:grammar_module))
|
|
@@ -562,6 +596,8 @@ module TreeHaver
|
|
|
562
596
|
Backends::Java
|
|
563
597
|
when :tslp, :"kreuzberg-language-pack"
|
|
564
598
|
Backends::Tslp
|
|
599
|
+
when :rust_tslp
|
|
600
|
+
Backends::RustTslp
|
|
565
601
|
when :citrus
|
|
566
602
|
Backends::Citrus
|
|
567
603
|
when :parslet
|
data.tar.gz.sig
CHANGED
|
Binary file
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: tree_haver
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 7.1.
|
|
4
|
+
version: 7.1.10
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Peter H. Boling
|
|
@@ -71,7 +71,7 @@ dependencies:
|
|
|
71
71
|
requirements:
|
|
72
72
|
- - ">="
|
|
73
73
|
- !ruby/object:Gem::Version
|
|
74
|
-
version: 1.
|
|
74
|
+
version: 1.17.0
|
|
75
75
|
- - "<"
|
|
76
76
|
- !ruby/object:Gem::Version
|
|
77
77
|
version: '2.0'
|
|
@@ -81,7 +81,7 @@ dependencies:
|
|
|
81
81
|
requirements:
|
|
82
82
|
- - ">="
|
|
83
83
|
- !ruby/object:Gem::Version
|
|
84
|
-
version: 1.
|
|
84
|
+
version: 1.17.0
|
|
85
85
|
- - "<"
|
|
86
86
|
- !ruby/object:Gem::Version
|
|
87
87
|
version: '2.0'
|
|
@@ -111,20 +111,20 @@ dependencies:
|
|
|
111
111
|
requirements:
|
|
112
112
|
- - "~>"
|
|
113
113
|
- !ruby/object:Gem::Version
|
|
114
|
-
version: '3.
|
|
114
|
+
version: '3.1'
|
|
115
115
|
- - ">="
|
|
116
116
|
- !ruby/object:Gem::Version
|
|
117
|
-
version: 3.
|
|
117
|
+
version: 3.1.8
|
|
118
118
|
type: :development
|
|
119
119
|
prerelease: false
|
|
120
120
|
version_requirements: !ruby/object:Gem::Requirement
|
|
121
121
|
requirements:
|
|
122
122
|
- - "~>"
|
|
123
123
|
- !ruby/object:Gem::Version
|
|
124
|
-
version: '3.
|
|
124
|
+
version: '3.1'
|
|
125
125
|
- - ">="
|
|
126
126
|
- !ruby/object:Gem::Version
|
|
127
|
-
version: 3.
|
|
127
|
+
version: 3.1.8
|
|
128
128
|
- !ruby/object:Gem::Dependency
|
|
129
129
|
name: bundler-audit
|
|
130
130
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -202,7 +202,7 @@ dependencies:
|
|
|
202
202
|
version: '3.2'
|
|
203
203
|
- - ">="
|
|
204
204
|
- !ruby/object:Gem::Version
|
|
205
|
-
version: 3.2.
|
|
205
|
+
version: 3.2.5
|
|
206
206
|
type: :development
|
|
207
207
|
prerelease: false
|
|
208
208
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -212,7 +212,7 @@ dependencies:
|
|
|
212
212
|
version: '3.2'
|
|
213
213
|
- - ">="
|
|
214
214
|
- !ruby/object:Gem::Version
|
|
215
|
-
version: 3.2.
|
|
215
|
+
version: 3.2.5
|
|
216
216
|
- !ruby/object:Gem::Dependency
|
|
217
217
|
name: kettle-test
|
|
218
218
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -222,7 +222,7 @@ dependencies:
|
|
|
222
222
|
version: '2.0'
|
|
223
223
|
- - ">="
|
|
224
224
|
- !ruby/object:Gem::Version
|
|
225
|
-
version: 2.0.
|
|
225
|
+
version: 2.0.23
|
|
226
226
|
type: :development
|
|
227
227
|
prerelease: false
|
|
228
228
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -232,7 +232,7 @@ dependencies:
|
|
|
232
232
|
version: '2.0'
|
|
233
233
|
- - ">="
|
|
234
234
|
- !ruby/object:Gem::Version
|
|
235
|
-
version: 2.0.
|
|
235
|
+
version: 2.0.23
|
|
236
236
|
- !ruby/object:Gem::Dependency
|
|
237
237
|
name: turbo_tests2
|
|
238
238
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -242,7 +242,7 @@ dependencies:
|
|
|
242
242
|
version: '3.2'
|
|
243
243
|
- - ">="
|
|
244
244
|
- !ruby/object:Gem::Version
|
|
245
|
-
version: 3.2.
|
|
245
|
+
version: 3.2.14
|
|
246
246
|
type: :development
|
|
247
247
|
prerelease: false
|
|
248
248
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -252,7 +252,7 @@ dependencies:
|
|
|
252
252
|
version: '3.2'
|
|
253
253
|
- - ">="
|
|
254
254
|
- !ruby/object:Gem::Version
|
|
255
|
-
version: 3.2.
|
|
255
|
+
version: 3.2.14
|
|
256
256
|
- !ruby/object:Gem::Dependency
|
|
257
257
|
name: ruby-progressbar
|
|
258
258
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -276,7 +276,7 @@ dependencies:
|
|
|
276
276
|
version: '1.0'
|
|
277
277
|
- - ">="
|
|
278
278
|
- !ruby/object:Gem::Version
|
|
279
|
-
version: 1.0.
|
|
279
|
+
version: 1.0.9
|
|
280
280
|
type: :development
|
|
281
281
|
prerelease: false
|
|
282
282
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -286,7 +286,7 @@ dependencies:
|
|
|
286
286
|
version: '1.0'
|
|
287
287
|
- - ">="
|
|
288
288
|
- !ruby/object:Gem::Version
|
|
289
|
-
version: 1.0.
|
|
289
|
+
version: 1.0.9
|
|
290
290
|
- !ruby/object:Gem::Dependency
|
|
291
291
|
name: gitmoji-regex
|
|
292
292
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -296,7 +296,7 @@ dependencies:
|
|
|
296
296
|
version: '2.0'
|
|
297
297
|
- - ">="
|
|
298
298
|
- !ruby/object:Gem::Version
|
|
299
|
-
version: 2.0.
|
|
299
|
+
version: 2.0.15
|
|
300
300
|
type: :development
|
|
301
301
|
prerelease: false
|
|
302
302
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -306,7 +306,7 @@ dependencies:
|
|
|
306
306
|
version: '2.0'
|
|
307
307
|
- - ">="
|
|
308
308
|
- !ruby/object:Gem::Version
|
|
309
|
-
version: 2.0.
|
|
309
|
+
version: 2.0.15
|
|
310
310
|
- !ruby/object:Gem::Dependency
|
|
311
311
|
name: toml
|
|
312
312
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -357,6 +357,7 @@ files:
|
|
|
357
357
|
- lib/tree_haver/backends/prism.rb
|
|
358
358
|
- lib/tree_haver/backends/psych.rb
|
|
359
359
|
- lib/tree_haver/backends/rust.rb
|
|
360
|
+
- lib/tree_haver/backends/rust_tslp.rb
|
|
360
361
|
- lib/tree_haver/backends/tslp.rb
|
|
361
362
|
- lib/tree_haver/base/comment.rb
|
|
362
363
|
- lib/tree_haver/base/language.rb
|
|
@@ -389,10 +390,10 @@ licenses:
|
|
|
389
390
|
- PolyForm-Small-Business-1.0.0
|
|
390
391
|
metadata:
|
|
391
392
|
homepage_uri: https://structuredmerge.org
|
|
392
|
-
source_code_uri: https://github.com/structuredmerge/structuredmerge-ruby/tree/v7.1.
|
|
393
|
-
changelog_uri: https://github.com/structuredmerge/structuredmerge-ruby/blob/v7.1.
|
|
393
|
+
source_code_uri: https://github.com/structuredmerge/structuredmerge-ruby/tree/v7.1.10
|
|
394
|
+
changelog_uri: https://github.com/structuredmerge/structuredmerge-ruby/blob/v7.1.10/CHANGELOG.md
|
|
394
395
|
bug_tracker_uri: https://github.com/structuredmerge/structuredmerge-ruby/issues
|
|
395
|
-
documentation_uri: https://www.rubydoc.info/gems/tree_haver/7.1.
|
|
396
|
+
documentation_uri: https://www.rubydoc.info/gems/tree_haver/7.1.10
|
|
396
397
|
funding_uri: https://github.com/sponsors/pboling
|
|
397
398
|
wiki_uri: https://github.com/structuredmerge/structuredmerge-ruby/wiki
|
|
398
399
|
news_uri: https://www.railsbling.com/tags/tree_haver
|
|
@@ -423,7 +424,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
423
424
|
- !ruby/object:Gem::Version
|
|
424
425
|
version: '0'
|
|
425
426
|
requirements: []
|
|
426
|
-
rubygems_version: 4.0.
|
|
427
|
+
rubygems_version: 4.0.20
|
|
427
428
|
specification_version: 4
|
|
428
429
|
summary: "\U0001F334 Structured Merge parser abstraction and process analysis for
|
|
429
430
|
Ruby"
|
metadata.gz.sig
CHANGED
|
Binary file
|