rbtr-lang-ruby 2026.9.0.dev0__py3-none-any.whl

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.
@@ -0,0 +1 @@
1
+ """Ruby language plugin package."""
@@ -0,0 +1,97 @@
1
+ """Ruby language plugin.
2
+
3
+ Provides symbol extraction (methods, classes, modules, constants,
4
+ and the RSpec `describe`/`context`/`it` DSL) and structured import
5
+ metadata from `require` / `require_relative`. Constants scope to
6
+ their enclosing class/module. RSpec groups (`describe`/`context`/
7
+ `feature`) are classes and examples (`it`/`specify`/`example`) are
8
+ functions, named by their description string.
9
+
10
+ Extracted chunks::
11
+
12
+ def greet ... end → function "greet", scope ""
13
+ class Shape ... end → class "Shape", scope ""
14
+ module Utils ... end → class "Utils", scope ""
15
+ class Foo
16
+ def bar ... end → method "bar", scope "Foo"
17
+ end
18
+
19
+ require "json"
20
+ → import, metadata {module: "json"}
21
+ require_relative "helpers"
22
+ → import, metadata {module: "helpers", dots: "1"}
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ from typing import TYPE_CHECKING
28
+
29
+ from rbtr.domain.models import ImportMeta
30
+ from rbtr.languages.registration import (
31
+ ImportResolver,
32
+ LanguageRegistration,
33
+ QueryExtraction,
34
+ load_query,
35
+ parse_path_relative,
36
+ )
37
+
38
+ if TYPE_CHECKING:
39
+ from tree_sitter import Node
40
+
41
+ # ── Query ────────────────────────────────────────────────────────────
42
+
43
+
44
+ # ── Import extractor ─────────────────────────────────────────────────
45
+
46
+
47
+ def extract_import_meta(
48
+ resolver: ImportResolver, node: Node, captures: dict[str, list[Node]]
49
+ ) -> ImportMeta:
50
+ """Extract import data from a Ruby `require` / `require_relative` node.
51
+
52
+ Reads `@_import_module` from captures (the query captures
53
+ the string argument), then sets `dots` for
54
+ `require_relative` (always relative to the current file).
55
+
56
+ Examples:
57
+
58
+ `require "json"`:
59
+ module="json"
60
+
61
+ `require_relative "helpers"`:
62
+ module="helpers", dots="1"
63
+
64
+ `require_relative "./config"`:
65
+ module="config", dots="1"
66
+
67
+ `require_relative "../lib/utils"`:
68
+ module="lib/utils", dots="2"
69
+ """
70
+ meta = resolver(node, captures)
71
+ method = node.child_by_field_name("method")
72
+ if method and method.text == b"require_relative":
73
+ # `require_relative` is always relative to the current file. Strip any
74
+ # `./`/`../` prefix into `dots` (a bare path is the current dir, dots=1)
75
+ # so the resolver doesn't see a leftover `./` it can't match.
76
+ dots, cleaned = parse_path_relative(meta.module)
77
+ meta.module = cleaned
78
+ meta.dots = str(dots or 1)
79
+ return meta
80
+
81
+
82
+ # ── Plugin ───────────────────────────────────────────────────────────
83
+
84
+
85
+ ruby = LanguageRegistration(
86
+ id="ruby",
87
+ extensions=frozenset({".rb"}),
88
+ grammar_module="tree_sitter_ruby",
89
+ extraction=QueryExtraction(
90
+ query=load_query(__package__, "ruby"),
91
+ scope_types=frozenset({"class", "module"}),
92
+ ),
93
+ source_roots=("", "lib"),
94
+ extraction_serial=5,
95
+ )
96
+
97
+ ruby.import_extractor(extract_import_meta)
File without changes
@@ -0,0 +1,51 @@
1
+
2
+ ; Comments (Ruby: `#` line and `=begin`/`=end` block, both `comment`).
3
+ (comment) @comment
4
+
5
+ (method
6
+ name: (identifier) @_fn_name) @function
7
+
8
+ (singleton_method
9
+ name: (identifier) @_fn_name) @function
10
+
11
+ (class
12
+ name: (constant) @_cls_name) @class
13
+
14
+ (module
15
+ name: (constant) @_cls_name) @class
16
+
17
+ (call
18
+ method: (identifier) @_call_name
19
+ arguments: (argument_list
20
+ (string) @_import_module)
21
+ (#eq? @_call_name "require")) @import
22
+
23
+ (call
24
+ method: (identifier) @_call_name
25
+ arguments: (argument_list
26
+ (string) @_import_module)
27
+ (#eq? @_call_name "require_relative")) @import
28
+
29
+ (assignment
30
+ left: (constant) @_var_name) @variable
31
+
32
+ (assignment
33
+ left: (left_assignment_list (constant) @_var_name)) @variable
34
+
35
+ (assignment
36
+ left: (left_assignment_list (rest_assignment (constant) @_var_name))) @variable
37
+
38
+ (call
39
+ method: (identifier) @_call_name
40
+ arguments: (argument_list . (string (string_content) @_cls_name))
41
+ (#any-of? @_call_name "describe" "context" "feature" "shared_examples" "shared_context")) @class
42
+
43
+ (call
44
+ method: (identifier) @_call_name
45
+ arguments: (argument_list . (constant) @_cls_name)
46
+ (#any-of? @_call_name "describe" "context" "feature")) @class
47
+
48
+ (call
49
+ method: (identifier) @_call_name
50
+ arguments: (argument_list . (string (string_content) @_fn_name))
51
+ (#any-of? @_call_name "it" "specify" "example" "scenario")) @function
File without changes
@@ -0,0 +1,3 @@
1
+ [
2
+ "ruby.rb::require_relative \"./config\" -> config.rb::DEFAULT_LOCALE [imports]"
3
+ ]
@@ -0,0 +1,382 @@
1
+ [
2
+ {
3
+ "blob_sha": "sha1",
4
+ "file_path": "config.rb",
5
+ "kind": "comment",
6
+ "name": "",
7
+ "scope": "",
8
+ "language": "ruby",
9
+ "file_language": "ruby",
10
+ "content": "# frozen_string_literal: true",
11
+ "line_start": 1,
12
+ "line_end": 1,
13
+ "metadata": {
14
+ "module": "",
15
+ "names": "",
16
+ "dots": "",
17
+ "language_hint": ""
18
+ },
19
+ "id": "16e7f0488dd01998"
20
+ },
21
+ {
22
+ "blob_sha": "sha1",
23
+ "file_path": "config.rb",
24
+ "kind": "variable",
25
+ "name": "DEFAULT_LOCALE",
26
+ "scope": "",
27
+ "language": "ruby",
28
+ "file_language": "ruby",
29
+ "content": "# Greeter configuration.\nDEFAULT_LOCALE = \"en\"",
30
+ "line_start": 3,
31
+ "line_end": 4,
32
+ "metadata": {
33
+ "module": "",
34
+ "names": "",
35
+ "dots": "",
36
+ "language_hint": ""
37
+ },
38
+ "id": "32b6464c28d5298b"
39
+ },
40
+ {
41
+ "blob_sha": "sha1",
42
+ "file_path": "ruby.rb",
43
+ "kind": "comment",
44
+ "name": "",
45
+ "scope": "",
46
+ "language": "ruby",
47
+ "file_language": "ruby",
48
+ "content": "# frozen_string_literal: true",
49
+ "line_start": 1,
50
+ "line_end": 1,
51
+ "metadata": {
52
+ "module": "",
53
+ "names": "",
54
+ "dots": "",
55
+ "language_hint": ""
56
+ },
57
+ "id": "16e7f0488dd01998"
58
+ },
59
+ {
60
+ "blob_sha": "sha1",
61
+ "file_path": "ruby.rb",
62
+ "kind": "comment",
63
+ "name": "",
64
+ "scope": "",
65
+ "language": "ruby",
66
+ "file_language": "ruby",
67
+ "content": "# Greeter — format greetings for named recipients.\n#\n# The Ruby plugin extracts top-level defs (as functions), classes and\n# modules, defs inside them (as methods), constant assignments (as\n# variables, scoped to their class/module), the RSpec describe/it DSL\n# (groups as classes, examples as functions), and require /\n# require_relative imports.",
68
+ "line_start": 3,
69
+ "line_end": 9,
70
+ "metadata": {
71
+ "module": "",
72
+ "names": "",
73
+ "dots": "",
74
+ "language_hint": ""
75
+ },
76
+ "id": "451436cb9495c8e3"
77
+ },
78
+ {
79
+ "blob_sha": "sha1",
80
+ "file_path": "ruby.rb",
81
+ "kind": "import",
82
+ "name": "require \"json\"",
83
+ "scope": "",
84
+ "language": "ruby",
85
+ "file_language": "ruby",
86
+ "content": "require \"json\"",
87
+ "line_start": 11,
88
+ "line_end": 11,
89
+ "metadata": {
90
+ "module": "json",
91
+ "names": "",
92
+ "dots": "",
93
+ "language_hint": ""
94
+ },
95
+ "id": "55888f95be8c407e"
96
+ },
97
+ {
98
+ "blob_sha": "sha1",
99
+ "file_path": "ruby.rb",
100
+ "kind": "import",
101
+ "name": "require_relative \"./config\"",
102
+ "scope": "",
103
+ "language": "ruby",
104
+ "file_language": "ruby",
105
+ "content": "require_relative \"./config\"",
106
+ "line_start": 12,
107
+ "line_end": 12,
108
+ "metadata": {
109
+ "module": "config",
110
+ "names": "",
111
+ "dots": "1",
112
+ "language_hint": ""
113
+ },
114
+ "id": "6f6e733c7294dbdc"
115
+ },
116
+ {
117
+ "blob_sha": "sha1",
118
+ "file_path": "ruby.rb",
119
+ "kind": "variable",
120
+ "name": "DEFAULT_GREETING",
121
+ "scope": "",
122
+ "language": "ruby",
123
+ "file_language": "ruby",
124
+ "content": "DEFAULT_GREETING = \"Hello\"",
125
+ "line_start": 14,
126
+ "line_end": 14,
127
+ "metadata": {
128
+ "module": "",
129
+ "names": "",
130
+ "dots": "",
131
+ "language_hint": ""
132
+ },
133
+ "id": "0c28350e9069a2c7"
134
+ },
135
+ {
136
+ "blob_sha": "sha1",
137
+ "file_path": "ruby.rb",
138
+ "kind": "comment",
139
+ "name": "",
140
+ "scope": "",
141
+ "language": "ruby",
142
+ "file_language": "ruby",
143
+ "content": "# trailing comment: its own chunk",
144
+ "line_start": 14,
145
+ "line_end": 14,
146
+ "metadata": {
147
+ "module": "",
148
+ "names": "",
149
+ "dots": "",
150
+ "language_hint": ""
151
+ },
152
+ "id": "ed4ca5590a0f51ed"
153
+ },
154
+ {
155
+ "blob_sha": "sha1",
156
+ "file_path": "ruby.rb",
157
+ "kind": "comment",
158
+ "name": "",
159
+ "scope": "",
160
+ "language": "ruby",
161
+ "file_language": "ruby",
162
+ "content": "# Standalone note, separated by blank lines from any definition.\n# Second line of the same block.",
163
+ "line_start": 16,
164
+ "line_end": 17,
165
+ "metadata": {
166
+ "module": "",
167
+ "names": "",
168
+ "dots": "",
169
+ "language_hint": ""
170
+ },
171
+ "id": "b9eb0ec1fcda7627"
172
+ },
173
+ {
174
+ "blob_sha": "sha1",
175
+ "file_path": "ruby.rb",
176
+ "kind": "class",
177
+ "name": "Shoutable",
178
+ "scope": "",
179
+ "language": "ruby",
180
+ "file_language": "ruby",
181
+ "content": "# Mixin providing a shout helper.\nmodule Shoutable\n def shout(message)\n message.upcase\n end\nend",
182
+ "line_start": 19,
183
+ "line_end": 24,
184
+ "metadata": {
185
+ "module": "",
186
+ "names": "",
187
+ "dots": "",
188
+ "language_hint": ""
189
+ },
190
+ "id": "e30c23f14bbed540"
191
+ },
192
+ {
193
+ "blob_sha": "sha1",
194
+ "file_path": "ruby.rb",
195
+ "kind": "method",
196
+ "name": "shout",
197
+ "scope": "Shoutable",
198
+ "language": "ruby",
199
+ "file_language": "ruby",
200
+ "content": "def shout(message)\n message.upcase\n end",
201
+ "line_start": 21,
202
+ "line_end": 23,
203
+ "metadata": {
204
+ "module": "",
205
+ "names": "",
206
+ "dots": "",
207
+ "language_hint": ""
208
+ },
209
+ "id": "a8d8716691e53686"
210
+ },
211
+ {
212
+ "blob_sha": "sha1",
213
+ "file_path": "ruby.rb",
214
+ "kind": "class",
215
+ "name": "Greeter",
216
+ "scope": "",
217
+ "language": "ruby",
218
+ "file_language": "ruby",
219
+ "content": "# Formats greetings with a prefix.\nclass Greeter\n include Shoutable\n\n MAX_NAME_LENGTH = 64\n\n def initialize(prefix = DEFAULT_GREETING)\n @prefix = prefix\n end\n\n # Greet a single recipient.\n def greet(name)\n \"#{@prefix}, #{name}\"\n end\n\n def self.default\n new(DEFAULT_GREETING)\n end\nend",
220
+ "line_start": 26,
221
+ "line_end": 44,
222
+ "metadata": {
223
+ "module": "",
224
+ "names": "",
225
+ "dots": "",
226
+ "language_hint": ""
227
+ },
228
+ "id": "1e1f11916c4f30a1"
229
+ },
230
+ {
231
+ "blob_sha": "sha1",
232
+ "file_path": "ruby.rb",
233
+ "kind": "variable",
234
+ "name": "MAX_NAME_LENGTH",
235
+ "scope": "Greeter",
236
+ "language": "ruby",
237
+ "file_language": "ruby",
238
+ "content": "MAX_NAME_LENGTH = 64",
239
+ "line_start": 30,
240
+ "line_end": 30,
241
+ "metadata": {
242
+ "module": "",
243
+ "names": "",
244
+ "dots": "",
245
+ "language_hint": ""
246
+ },
247
+ "id": "023bd4450cd467e2"
248
+ },
249
+ {
250
+ "blob_sha": "sha1",
251
+ "file_path": "ruby.rb",
252
+ "kind": "method",
253
+ "name": "initialize",
254
+ "scope": "Greeter",
255
+ "language": "ruby",
256
+ "file_language": "ruby",
257
+ "content": "def initialize(prefix = DEFAULT_GREETING)\n @prefix = prefix\n end",
258
+ "line_start": 32,
259
+ "line_end": 34,
260
+ "metadata": {
261
+ "module": "",
262
+ "names": "",
263
+ "dots": "",
264
+ "language_hint": ""
265
+ },
266
+ "id": "97a4b17df8cf5cb8"
267
+ },
268
+ {
269
+ "blob_sha": "sha1",
270
+ "file_path": "ruby.rb",
271
+ "kind": "method",
272
+ "name": "greet",
273
+ "scope": "Greeter",
274
+ "language": "ruby",
275
+ "file_language": "ruby",
276
+ "content": "# Greet a single recipient.\n def greet(name)\n \"#{@prefix}, #{name}\"\n end",
277
+ "line_start": 36,
278
+ "line_end": 39,
279
+ "metadata": {
280
+ "module": "",
281
+ "names": "",
282
+ "dots": "",
283
+ "language_hint": ""
284
+ },
285
+ "id": "50c202645f9e2fc6"
286
+ },
287
+ {
288
+ "blob_sha": "sha1",
289
+ "file_path": "ruby.rb",
290
+ "kind": "method",
291
+ "name": "default",
292
+ "scope": "Greeter",
293
+ "language": "ruby",
294
+ "file_language": "ruby",
295
+ "content": "def self.default\n new(DEFAULT_GREETING)\n end",
296
+ "line_start": 41,
297
+ "line_end": 43,
298
+ "metadata": {
299
+ "module": "",
300
+ "names": "",
301
+ "dots": "",
302
+ "language_hint": ""
303
+ },
304
+ "id": "e7912cb64a05b9fe"
305
+ },
306
+ {
307
+ "blob_sha": "sha1",
308
+ "file_path": "ruby.rb",
309
+ "kind": "function",
310
+ "name": "format_greeting",
311
+ "scope": "",
312
+ "language": "ruby",
313
+ "file_language": "ruby",
314
+ "content": "# Top-level helper that builds a greeter and greets.\ndef format_greeting(name)\n Greeter.new.greet(name)\nend",
315
+ "line_start": 46,
316
+ "line_end": 49,
317
+ "metadata": {
318
+ "module": "",
319
+ "names": "",
320
+ "dots": "",
321
+ "language_hint": ""
322
+ },
323
+ "id": "3f0252c2b63ec688"
324
+ },
325
+ {
326
+ "blob_sha": "sha1",
327
+ "file_path": "ruby.rb",
328
+ "kind": "class",
329
+ "name": "Greeter",
330
+ "scope": "",
331
+ "language": "ruby",
332
+ "file_language": "ruby",
333
+ "content": "# RSpec-style specification exercising the describe/it DSL.\nRSpec.describe Greeter do\n describe \"#greet\" do\n it \"includes the configured prefix\" do\n expect(Greeter.new.greet(\"Sam\")).to include(\"Hello\")\n end\n end\nend",
334
+ "line_start": 51,
335
+ "line_end": 58,
336
+ "metadata": {
337
+ "module": "",
338
+ "names": "",
339
+ "dots": "",
340
+ "language_hint": ""
341
+ },
342
+ "id": "40e1158328623a05"
343
+ },
344
+ {
345
+ "blob_sha": "sha1",
346
+ "file_path": "ruby.rb",
347
+ "kind": "class",
348
+ "name": "#greet",
349
+ "scope": "",
350
+ "language": "ruby",
351
+ "file_language": "ruby",
352
+ "content": "describe \"#greet\" do\n it \"includes the configured prefix\" do\n expect(Greeter.new.greet(\"Sam\")).to include(\"Hello\")\n end\n end",
353
+ "line_start": 53,
354
+ "line_end": 57,
355
+ "metadata": {
356
+ "module": "",
357
+ "names": "",
358
+ "dots": "",
359
+ "language_hint": ""
360
+ },
361
+ "id": "6557c0dbc5a0d319"
362
+ },
363
+ {
364
+ "blob_sha": "sha1",
365
+ "file_path": "ruby.rb",
366
+ "kind": "function",
367
+ "name": "includes the configured prefix",
368
+ "scope": "",
369
+ "language": "ruby",
370
+ "file_language": "ruby",
371
+ "content": "it \"includes the configured prefix\" do\n expect(Greeter.new.greet(\"Sam\")).to include(\"Hello\")\n end",
372
+ "line_start": 54,
373
+ "line_end": 56,
374
+ "metadata": {
375
+ "module": "",
376
+ "names": "",
377
+ "dots": "",
378
+ "language_hint": ""
379
+ },
380
+ "id": "62cc3ae125d0a288"
381
+ }
382
+ ]
@@ -0,0 +1,126 @@
1
+ """Ruby docstring-extraction test cases.
2
+
3
+ Each `@case` returns `(lang, source, symbol_name, snippet)` consumed by `test_docstrings.py`.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from pytest_cases import case
9
+
10
+ type DocstringCase = tuple[str, str, str, str]
11
+
12
+
13
+ @case(tags=["documented", "canonical", "exterior_doc"])
14
+ def case_ruby_hash_doc_on_def() -> DocstringCase:
15
+ """Canonical `#` comment above a top-level def."""
16
+ src = """\
17
+ # Greet the user.
18
+ def greet
19
+ 'hi'
20
+ end
21
+ """
22
+ return "ruby", src, "greet", "Greet the user"
23
+
24
+
25
+ @case(tags=["documented", "canonical", "exterior_doc"])
26
+ def case_ruby_hash_doc_on_class() -> DocstringCase:
27
+ """`#` comment above a class declaration."""
28
+ src = """\
29
+ # Service facade.
30
+ class Svc
31
+ end
32
+ """
33
+ return "ruby", src, "Svc", "Service facade"
34
+
35
+
36
+ @case(tags=["documented", "canonical", "exterior_doc"])
37
+ def case_ruby_hash_doc_on_module() -> DocstringCase:
38
+ """`#` comment above a module declaration."""
39
+ src = """\
40
+ # Utilities.
41
+ module Utils
42
+ end
43
+ """
44
+ return "ruby", src, "Utils", "Utilities"
45
+
46
+
47
+ @case(tags=["documented", "canonical", "exterior_doc"])
48
+ def case_ruby_multi_line_hash_doc() -> DocstringCase:
49
+ """Multi-line `#` doc comment."""
50
+ src = """\
51
+ # Compute a checksum.
52
+ #
53
+ # Returns a hex digest string.
54
+ def checksum
55
+ ''
56
+ end
57
+ """
58
+ return "ruby", src, "checksum", "Returns a hex digest"
59
+
60
+
61
+ @case(tags=["documented", "edge_case", "exterior_doc"])
62
+ def case_ruby_block_comment_begin_end() -> DocstringCase:
63
+ """`=begin` / `=end` block comment. The grammar treats it
64
+ as a single `comment` node; attachment works when it
65
+ immediately precedes the `def`.
66
+ """
67
+ src = """\
68
+ =begin
69
+ Block-style doc.
70
+ =end
71
+ def foo
72
+ end
73
+ """
74
+ return "ruby", src, "foo", "Block-style doc"
75
+
76
+
77
+ @case(tags=["documented", "unconventional", "exterior_doc"])
78
+ def case_ruby_shebang_like_comment_above_def() -> DocstringCase:
79
+ """Unconventional but valid: a `#` run whose first line
80
+ starts with `#!`-style emphasis still attaches.
81
+ """
82
+ src = """\
83
+ #! IMPORTANT: use Foo instead of Bar.
84
+ # Prefer modern API.
85
+ def legacy
86
+ end
87
+ """
88
+ return "ruby", src, "legacy", "IMPORTANT: use Foo"
89
+
90
+
91
+ @case(tags=["undocumented", "no_docs"])
92
+ def case_ruby_def_without_doc() -> DocstringCase:
93
+ """Undocumented top-level def."""
94
+ src = """\
95
+ def bare
96
+ end
97
+ """
98
+ return "ruby", src, "bare", "PHANTOM_DOC_TEXT_SHOULD_NEVER_APPEAR"
99
+
100
+
101
+ @case(tags=["undocumented", "boundary_not_attached"])
102
+ def case_ruby_doc_detached_by_blank_line() -> DocstringCase:
103
+ """Blank line between comment and def breaks attachment."""
104
+ src = """\
105
+ # Orphan.
106
+
107
+ def later
108
+ end
109
+ """
110
+ return "ruby", src, "later", "Orphan"
111
+
112
+
113
+ @case(tags=["undocumented", "invalid"])
114
+ def case_ruby_doc_does_not_steal_from_next() -> DocstringCase:
115
+ """A `#` comment between two top-level defs belongs to the
116
+ later def, not the earlier one.
117
+ """
118
+ src = """\
119
+ def first
120
+ end
121
+
122
+ # Doc for second.
123
+ def second
124
+ end
125
+ """
126
+ return "ruby", src, "first", "Doc for second"
@@ -0,0 +1,320 @@
1
+ """Ruby extraction test cases."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import pytest
6
+ from pytest_cases import case
7
+
8
+ type SymbolCase = tuple[str, str, list[tuple[str, str, str]]]
9
+ type ImportCase = tuple[str, str, dict[str, str]]
10
+ type MultiImportCase = tuple[str, str, int, list[dict[str, str]]]
11
+ type MixedCase = tuple[str, str, set[str], list[tuple[str, str]]]
12
+
13
+ _xfail_nested = pytest.mark.xfail(
14
+ reason="nested/chained destructuring unsupported — no query-only recursion",
15
+ strict=True,
16
+ )
17
+
18
+
19
+ @case(tags=["symbol"])
20
+ def case_ruby_method_no_args() -> SymbolCase:
21
+ """def greet — top-level function."""
22
+ src = """\
23
+ def greet
24
+ puts "hello"
25
+ end
26
+ """
27
+ return "ruby", src, [("function", "greet", "")]
28
+
29
+
30
+ @case(tags=["symbol"])
31
+ def case_ruby_method_with_args() -> SymbolCase:
32
+ """def add(a, b) — top-level function."""
33
+ src = """\
34
+ def add(a, b)
35
+ a + b
36
+ end
37
+ """
38
+ return "ruby", src, [("function", "add", "")]
39
+
40
+
41
+ @case(tags=["symbol"])
42
+ def case_ruby_multiple_functions() -> SymbolCase:
43
+ """Multiple top-level functions."""
44
+ src = """\
45
+ def foo
46
+ 1
47
+ end
48
+
49
+ def bar
50
+ 2
51
+ end
52
+ """
53
+ return "ruby", src, [("function", "foo", ""), ("function", "bar", "")]
54
+
55
+
56
+ @case(tags=["symbol"])
57
+ def case_ruby_class() -> SymbolCase:
58
+ """class Shape."""
59
+ src = """\
60
+ class Shape
61
+ end
62
+ """
63
+ return "ruby", src, [("class", "Shape", "")]
64
+
65
+
66
+ @case(tags=["symbol"])
67
+ def case_ruby_module() -> SymbolCase:
68
+ """module Utils."""
69
+ src = """\
70
+ module Utils
71
+ end
72
+ """
73
+ return "ruby", src, [("class", "Utils", "")]
74
+
75
+
76
+ @case(tags=["symbol"])
77
+ def case_ruby_class_superclass() -> SymbolCase:
78
+ """class Circle < Shape."""
79
+ src = """\
80
+ class Circle < Shape
81
+ end
82
+ """
83
+ return "ruby", src, [("class", "Circle", "")]
84
+
85
+
86
+ @case(tags=["symbol"])
87
+ def case_ruby_method_scoped_to_class() -> SymbolCase:
88
+ """Method scoped to class."""
89
+ src = """\
90
+ class Foo
91
+ def bar
92
+ 1
93
+ end
94
+ end
95
+ """
96
+ return "ruby", src, [("method", "bar", "Foo")]
97
+
98
+
99
+ @case(tags=["symbol"])
100
+ def case_ruby_singleton_method() -> SymbolCase:
101
+ """def self.build — singleton method."""
102
+ src = """\
103
+ class Factory
104
+ def self.build
105
+ new
106
+ end
107
+ end
108
+ """
109
+ return "ruby", src, [("method", "build", "Factory")]
110
+
111
+
112
+ @case(tags=["symbol"])
113
+ def case_ruby_method_scoped_to_module() -> SymbolCase:
114
+ """Method scoped to module."""
115
+ src = """\
116
+ module Helpers
117
+ def format(s)
118
+ s.strip
119
+ end
120
+ end
121
+ """
122
+ return "ruby", src, [("method", "format", "Helpers")]
123
+
124
+
125
+ @case(tags=["symbol"])
126
+ def case_ruby_top_level_not_scoped() -> SymbolCase:
127
+ """Function after class is not scoped."""
128
+ src = """\
129
+ class C
130
+ end
131
+
132
+ def standalone
133
+ 1
134
+ end
135
+ """
136
+ return "ruby", src, [("function", "standalone", "")]
137
+
138
+
139
+ @case(tags=["symbol"])
140
+ def case_ruby_nested_class_in_module() -> SymbolCase:
141
+ """Class nested in module, method scoped to inner class."""
142
+ src = """\
143
+ module Utils
144
+ class Parser
145
+ def parse(input)
146
+ input
147
+ end
148
+ end
149
+ end
150
+ """
151
+ return (
152
+ "ruby",
153
+ src,
154
+ [
155
+ ("class", "Utils", ""),
156
+ ("class", "Parser", "Utils"),
157
+ ("method", "parse", "Utils::Parser"),
158
+ ],
159
+ )
160
+
161
+
162
+ @case(tags=["symbol"])
163
+ def case_ruby_module_in_module() -> SymbolCase:
164
+ """A method in a module nested in a module carries the full path."""
165
+ src = """\
166
+ module A
167
+ module B
168
+ def f
169
+ 1
170
+ end
171
+ end
172
+ end
173
+ """
174
+ return "ruby", src, [("method", "f", "A::B")]
175
+
176
+
177
+ @case(tags=["symbol"])
178
+ def case_ruby_module_module_class_method() -> SymbolCase:
179
+ """A method nested module::module::class carries the full path."""
180
+ src = """\
181
+ module A
182
+ module B
183
+ class C
184
+ def go
185
+ 1
186
+ end
187
+ end
188
+ end
189
+ end
190
+ """
191
+ return "ruby", src, [("method", "go", "A::B::C")]
192
+
193
+
194
+ @case(tags=["import"])
195
+ def case_ruby_require_simple() -> ImportCase:
196
+ """require "json"."""
197
+ return "ruby", 'require "json"\n', {"module": "json"}
198
+
199
+
200
+ @case(tags=["import"])
201
+ def case_ruby_require_nested() -> ImportCase:
202
+ """require "net/http"."""
203
+ return "ruby", 'require "net/http"\n', {"module": "net/http"}
204
+
205
+
206
+ @case(tags=["import"])
207
+ def case_ruby_require_relative() -> ImportCase:
208
+ """require_relative "helpers"."""
209
+ return "ruby", 'require_relative "helpers"\n', {"module": "helpers", "dots": "1"}
210
+
211
+
212
+ @case(tags=["import"])
213
+ def case_ruby_require_relative_nested() -> ImportCase:
214
+ """require_relative "lib/utils"."""
215
+ return "ruby", 'require_relative "lib/utils"\n', {"module": "lib/utils", "dots": "1"}
216
+
217
+
218
+ @case(tags=["import"])
219
+ def case_ruby_require_relative_dot_prefix() -> ImportCase:
220
+ """require_relative "./config" — the `./` is stripped into dots."""
221
+ return "ruby", 'require_relative "./config"\n', {"module": "config", "dots": "1"}
222
+
223
+
224
+ @case(tags=["import"])
225
+ def case_ruby_require_relative_parent() -> ImportCase:
226
+ """require_relative "../lib/utils" — `../` becomes dots=2."""
227
+ return "ruby", 'require_relative "../lib/utils"\n', {"module": "lib/utils", "dots": "2"}
228
+
229
+
230
+ @case(tags=["import"])
231
+ def case_ruby_require_empty_string() -> ImportCase:
232
+ """require "" — empty string returns empty metadata.
233
+
234
+ Covers `ruby.py` lines 53 and 70.
235
+ """
236
+ return "ruby", 'require ""\n', {}
237
+
238
+
239
+ @case(tags=["multi_import"])
240
+ def case_ruby_multiple_requires() -> MultiImportCase:
241
+ """require + require_relative."""
242
+ src = """\
243
+ require "json"
244
+ require_relative "helpers"
245
+ """
246
+ return "ruby", src, 2, [{"module": "json"}, {"module": "helpers", "dots": "1"}]
247
+
248
+
249
+ @case(tags=["mixed"])
250
+ def case_ruby_full_file() -> MixedCase:
251
+ """Realistic Ruby file with doc comments on top-level
252
+ declarations. Comments inside the class body are not
253
+ attached to their methods by the current Ruby grammar (see
254
+ note in `case_docstrings.py`), so only the top-level
255
+ module, class, and `main` carry docs here. Methods carry the
256
+ full module::class path now that addressing composes the
257
+ enclosing-scope chain.
258
+ """
259
+ src = """\
260
+ require "json"
261
+ require_relative "config"
262
+
263
+ # Application namespace for the service.
264
+ module App
265
+ # Server runs the request loop.
266
+ class Server
267
+ def start
268
+ puts "starting"
269
+ end
270
+
271
+ def stop
272
+ puts "stopping"
273
+ end
274
+
275
+ def self.default
276
+ new
277
+ end
278
+ end
279
+ end
280
+
281
+ # Entry point used by bin/app.
282
+ def main
283
+ server = App::Server.new
284
+ server.start
285
+ end
286
+ """
287
+ return (
288
+ "ruby",
289
+ src,
290
+ {"import", "class", "method", "function"},
291
+ [("start", "App::Server"), ("stop", "App::Server"), ("default", "App::Server")],
292
+ )
293
+
294
+
295
+ @case(tags=["symbol"])
296
+ def case_ruby_constant() -> SymbolCase:
297
+ """Top-level constant."""
298
+ return "ruby", "MAX_SIZE = 100\n", [("variable", "MAX_SIZE", "")]
299
+
300
+
301
+ @case(tags=["symbol"])
302
+ def case_ruby_multiple_assignment() -> SymbolCase:
303
+ """Ruby multiple assignment of constants."""
304
+ return "ruby", "A, B = 1, 2\n", [("variable", "A", ""), ("variable", "B", "")]
305
+
306
+
307
+ @case(tags=["symbol"])
308
+ def case_ruby_splat_assignment() -> SymbolCase:
309
+ """Ruby splat target."""
310
+ return "ruby", "A, *B = list\n", [("variable", "A", ""), ("variable", "B", "")]
311
+
312
+
313
+ @case(tags=["symbol"], marks=_xfail_nested)
314
+ def case_ruby_nested_unpack_xfail() -> SymbolCase:
315
+ """Ruby nested destructuring — only the outer level captured today."""
316
+ return (
317
+ "ruby",
318
+ "(A, B), C = x\n",
319
+ [("variable", "A", ""), ("variable", "B", ""), ("variable", "C", "")],
320
+ )
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Greeter configuration.
4
+ DEFAULT_LOCALE = "en"
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Greeter — format greetings for named recipients.
4
+ #
5
+ # The Ruby plugin extracts top-level defs (as functions), classes and
6
+ # modules, defs inside them (as methods), constant assignments (as
7
+ # variables, scoped to their class/module), the RSpec describe/it DSL
8
+ # (groups as classes, examples as functions), and require /
9
+ # require_relative imports.
10
+
11
+ require "json"
12
+ require_relative "./config"
13
+
14
+ DEFAULT_GREETING = "Hello" # trailing comment: its own chunk
15
+
16
+ # Standalone note, separated by blank lines from any definition.
17
+ # Second line of the same block.
18
+
19
+ # Mixin providing a shout helper.
20
+ module Shoutable
21
+ def shout(message)
22
+ message.upcase
23
+ end
24
+ end
25
+
26
+ # Formats greetings with a prefix.
27
+ class Greeter
28
+ include Shoutable
29
+
30
+ MAX_NAME_LENGTH = 64
31
+
32
+ def initialize(prefix = DEFAULT_GREETING)
33
+ @prefix = prefix
34
+ end
35
+
36
+ # Greet a single recipient.
37
+ def greet(name)
38
+ "#{@prefix}, #{name}"
39
+ end
40
+
41
+ def self.default
42
+ new(DEFAULT_GREETING)
43
+ end
44
+ end
45
+
46
+ # Top-level helper that builds a greeter and greets.
47
+ def format_greeting(name)
48
+ Greeter.new.greet(name)
49
+ end
50
+
51
+ # RSpec-style specification exercising the describe/it DSL.
52
+ RSpec.describe Greeter do
53
+ describe "#greet" do
54
+ it "includes the configured prefix" do
55
+ expect(Greeter.new.greet("Sam")).to include("Hello")
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,43 @@
1
+ """Ruby doc-comment extraction (Ruby is exterior-doc: leading comments attach via the sibling walk)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pytest_cases import parametrize_with_cases
6
+
7
+ from rbtr.git import FileEntry
8
+ from rbtr.languages.extract import extract_file
9
+
10
+
11
+ @parametrize_with_cases(
12
+ "lang, source, name, snippet", cases=".cases_docstrings", has_tag="documented"
13
+ )
14
+ def test_documented_chunk_includes_doc_text(
15
+ lang: str, source: str, name: str, snippet: str
16
+ ) -> None:
17
+ """By default the chunk content carries the symbol's docs."""
18
+ chunks = extract_file(FileEntry("input", "sha1", source.encode()), lang)
19
+ chunk = next(c for c in chunks if c.name == name)
20
+ assert snippet in chunk.content, f"expected {snippet!r} in {lang}.{name}: {chunk.content!r}"
21
+
22
+
23
+ @parametrize_with_cases(
24
+ "lang, source, name, snippet", cases=".cases_docstrings", has_tag="undocumented"
25
+ )
26
+ def test_no_phantom_documentation(lang: str, source: str, name: str, snippet: str) -> None:
27
+ """Symbols without documentation do not gain any in content."""
28
+ chunks = extract_file(FileEntry("input", "sha1", source.encode()), lang)
29
+ chunk = next(c for c in chunks if c.name == name)
30
+ assert snippet not in chunk.content, (
31
+ f"unexpected {snippet!r} in {lang}.{name}: {chunk.content!r}"
32
+ )
33
+
34
+
35
+ @parametrize_with_cases(
36
+ "lang, source, name, snippet", cases=".cases_docstrings", has_tag="exterior_doc"
37
+ )
38
+ def test_leading_doc_folds_into_symbol(lang: str, source: str, name: str, snippet: str) -> None:
39
+ """A leading comment block folds into its symbol's chunk content."""
40
+ chunk = next(
41
+ c for c in extract_file(FileEntry("input", "sha1", source.encode()), lang) if c.name == name
42
+ )
43
+ assert snippet in chunk.content
@@ -0,0 +1,57 @@
1
+ """Ruby extraction tests (cases in `cases_extraction.py`)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pytest_cases import parametrize_with_cases
6
+
7
+ from rbtr.domain.models import ChunkKind, ImportMeta
8
+ from rbtr.git import FileEntry
9
+ from rbtr.languages.extract import extract_file
10
+
11
+
12
+ @parametrize_with_cases("lang, source, expected", cases=".cases_extraction", has_tag="symbol")
13
+ def test_extracts_expected_symbols(lang: str, source: str, expected: list) -> None:
14
+ """Each expected (kind, name, scope) tuple appears in the output."""
15
+ chunks = extract_file(FileEntry("input", "sha1", source.encode()), lang)
16
+ symbols = [(c.kind, c.name, c.scope) for c in chunks]
17
+ for exp in expected:
18
+ assert exp in symbols, f"expected {exp} not found in {symbols}"
19
+
20
+
21
+ @parametrize_with_cases(
22
+ "lang, source, expected_kinds, expected_methods", cases=".cases_extraction", has_tag="mixed"
23
+ )
24
+ def test_extracts_all_expected_kinds(
25
+ lang: str, source: str, expected_kinds: set[str], expected_methods: list[tuple[str, str]]
26
+ ) -> None:
27
+ """Realistic source produces all expected chunk kinds and method scoping."""
28
+ chunks = extract_file(FileEntry("input", "sha1", source.encode()), lang)
29
+ kinds = {c.kind for c in chunks}
30
+ for kind in expected_kinds:
31
+ assert kind in kinds, f"expected kind {kind!r} not in {kinds}"
32
+ methods = [(c.name, c.scope) for c in chunks if c.kind == ChunkKind.METHOD]
33
+ for name, scope in expected_methods:
34
+ assert (name, scope) in methods, f"expected method ({name}, {scope}) not in {methods}"
35
+
36
+
37
+ @parametrize_with_cases("lang, source, expected", cases=".cases_extraction", has_tag="import")
38
+ def test_extracts_import_metadata(lang: str, source: str, expected: dict) -> None:
39
+ """First import chunk has the expected metadata."""
40
+ chunks = extract_file(FileEntry("input", "sha1", source.encode()), lang)
41
+ imports = [c for c in chunks if c.kind == ChunkKind.IMPORT]
42
+ assert len(imports) >= 1, f"no import chunks extracted from {source!r}"
43
+ assert imports[0].metadata == ImportMeta(**expected)
44
+
45
+
46
+ @parametrize_with_cases(
47
+ "lang, source, count, metadata_list", cases=".cases_extraction", has_tag="multi_import"
48
+ )
49
+ def test_extracts_multi_import(
50
+ lang: str, source: str, count: int, metadata_list: list[dict]
51
+ ) -> None:
52
+ """Multiple imports have correct count and per-import metadata."""
53
+ chunks = extract_file(FileEntry("input", "sha1", source.encode()), lang)
54
+ imports = [c for c in chunks if c.kind == ChunkKind.IMPORT]
55
+ assert len(imports) == count
56
+ for imp, expected in zip(imports, metadata_list, strict=True):
57
+ assert imp.metadata == ImportMeta(**expected)
@@ -0,0 +1,75 @@
1
+ """Ruby sample extraction: the `samples/ruby/` project through the real pipeline."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import TYPE_CHECKING
7
+
8
+ import pytest
9
+ from tree_sitter import Parser
10
+
11
+ from rbtr.domain.models import Chunk, ChunkKind, Edge
12
+ from rbtr.git import FileEntry
13
+ from rbtr.languages.edges import build_resolution_map, infer_import_edges
14
+ from rbtr.languages.extract import extract_file
15
+ from rbtr.languages.manager import get_manager
16
+ from rbtr.testing import render_edges
17
+
18
+ if TYPE_CHECKING:
19
+ from syrupy.assertion import SnapshotAssertion
20
+
21
+
22
+ @pytest.fixture
23
+ def project() -> list[tuple[str, str]]:
24
+ root = Path(__file__).parent / "samples" / "ruby"
25
+ return [
26
+ (str(p.relative_to(root)), p.read_text()) for p in sorted(root.rglob("*")) if p.is_file()
27
+ ]
28
+
29
+
30
+ @pytest.fixture
31
+ def chunks(project: list[tuple[str, str]]) -> list[Chunk]:
32
+ manager = get_manager()
33
+ out: list[Chunk] = []
34
+ for path, text in project:
35
+ lang = manager.detect_language(path) or "ruby"
36
+ out.extend(extract_file(FileEntry(path, "sha1", text.encode()), lang))
37
+ return out
38
+
39
+
40
+ @pytest.fixture
41
+ def edges(project: list[tuple[str, str]], chunks: list[Chunk]) -> list[Edge]:
42
+ manager = get_manager()
43
+ repo_files = {path for path, _ in project}
44
+ return infer_import_edges(chunks, repo_files, build_resolution_map(manager))
45
+
46
+
47
+ def test_emits_expected_kinds(chunks: list[Chunk]) -> None:
48
+ """The sample exercises Ruby's function, class, method, variable, and import chunks."""
49
+ kinds = {c.kind for c in chunks}
50
+ assert {
51
+ ChunkKind.FUNCTION,
52
+ ChunkKind.CLASS,
53
+ ChunkKind.METHOD,
54
+ ChunkKind.VARIABLE,
55
+ ChunkKind.IMPORT,
56
+ ChunkKind.COMMENT,
57
+ } <= kinds
58
+
59
+
60
+ def test_parses_cleanly(project: list[tuple[str, str]]) -> None:
61
+ manager = get_manager()
62
+ for path, text in project:
63
+ grammar = manager.grammar(manager.detect_language(path) or "ruby")
64
+ assert grammar is not None
65
+ assert not Parser(grammar).parse(text.encode()).root_node.has_error, path
66
+
67
+
68
+ def test_extraction_matches_snapshot(chunks: list[Chunk], snapshot_json: SnapshotAssertion) -> None:
69
+ assert chunks == snapshot_json
70
+
71
+
72
+ def test_edges_match_snapshot(
73
+ chunks: list[Chunk], edges: list[Edge], snapshot_json: SnapshotAssertion
74
+ ) -> None:
75
+ assert render_edges(edges, chunks) == snapshot_json
@@ -0,0 +1,63 @@
1
+ Metadata-Version: 2.4
2
+ Name: rbtr-lang-ruby
3
+ Version: 2026.9.0.dev0
4
+ Summary: rbtr — Ruby language plugin
5
+ Keywords: code-search,code-index,tree-sitter,static-analysis,semantic-search,developer-tools,ruby
6
+ Author: Alejandro Giacometti
7
+ Author-email: Alejandro Giacometti <alejandro.giacometti@gmail.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3 :: Only
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Ruby
15
+ Classifier: Topic :: Software Development :: Libraries
16
+ Classifier: Topic :: Text Processing :: Indexing
17
+ Classifier: Typing :: Typed
18
+ Requires-Dist: rbtr==2026.9.0.dev0
19
+ Requires-Dist: tree-sitter-ruby
20
+ Requires-Python: >=3.13
21
+ Project-URL: Homepage, https://github.com/janrito/rbtr
22
+ Project-URL: Repository, https://github.com/janrito/rbtr
23
+ Project-URL: Documentation, https://github.com/janrito/rbtr/tree/main/packages/rbtr-lang-ruby#readme
24
+ Project-URL: Issues, https://github.com/janrito/rbtr/issues
25
+ Project-URL: Changelog, https://github.com/janrito/rbtr/releases
26
+ Description-Content-Type: text/markdown
27
+
28
+ # rbtr-lang-ruby
29
+
30
+ Ruby support for [rbtr]. Optional plugin — install with
31
+ `pip install rbtr[ruby]`.
32
+
33
+ [rbtr]: https://github.com/janrito/rbtr/tree/main/packages/rbtr#readme
34
+
35
+ ## What it ingests
36
+
37
+ - **Functions & methods** — `def` (a method inside a class/module is scoped
38
+ to it; top-level `def` is a function).
39
+ - **Classes** — `class` and `module` definitions (a module also scopes its
40
+ members).
41
+ - **Variables** — constants and module-level assignments.
42
+ - **Imports** — `require` and `require_relative`, for cross-file edges.
43
+
44
+ Leading `#` doc comments fold into the symbol's content.
45
+
46
+ ## Chunks produced
47
+
48
+ ```ruby
49
+ def greet(name); …; end # function "greet"
50
+ class User # class "User"
51
+ def save; …; end # method "save", scope "User"
52
+ end
53
+ MAX = 100 # variable "MAX"
54
+ require_relative "config" # import, metadata {module: config}
55
+ ```
56
+
57
+ ## Embedded / injected chunks
58
+
59
+ None. Ruby does not embed other languages.
60
+
61
+ ## Grammar & dependencies
62
+
63
+ Uses the `tree-sitter-ruby` grammar. No dependency on other language plugins.
@@ -0,0 +1,19 @@
1
+ rbtr_lang_ruby/__init__.py,sha256=8Q8etdrHu12hNWd6hq_K70CwbyF-EeBDZKRvf7ViD4o,36
2
+ rbtr_lang_ruby/plugin.py,sha256=o5rco7zB2RTYzZ8km-sMf7ZFj1p3lULc1Htlb3av3ek,3304
3
+ rbtr_lang_ruby/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ rbtr_lang_ruby/ruby.scm,sha256=W3iazNFUbWa86o2iIslgsDR_SvPAU3MOddE_h42d7PU,1362
5
+ rbtr_lang_ruby/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ rbtr_lang_ruby/tests/__snapshots__/test_samples/test_edges_match_snapshot.json,sha256=zTYX7J0wjD1YBx2mdYu8sNxr0W46-s2r4amwLzCyg-U,86
7
+ rbtr_lang_ruby/tests/__snapshots__/test_samples/test_extraction_matches_snapshot.json,sha256=LfSrymZZbYF4QJDDZ8ej0Ts7vzPGuUaXnqp5hJ_0LlU,9563
8
+ rbtr_lang_ruby/tests/cases_docstrings.py,sha256=eHnXQmbet2qBXsM7eujtkhd1MOg4mZQNyInrZ31PUNU,2958
9
+ rbtr_lang_ruby/tests/cases_extraction.py,sha256=N8Inv2njzoRb68b1TkfUnOSBn5XcS_WCTksQF-zU32U,7288
10
+ rbtr_lang_ruby/tests/samples/ruby/config.rb,sha256=dp2cfJf-qJYWg4gx1wewcQklVHY28Gjc_hVo43OujTo,78
11
+ rbtr_lang_ruby/tests/samples/ruby/ruby.rb,sha256=MdEK71xnjroo8XXSt3Q8wXIH0tlJhQsYWp6ao0H6gTs,1331
12
+ rbtr_lang_ruby/tests/test_docstrings.py,sha256=pTYLeKr4Gmb3_E4kiwnnCJBV_plOxXy7dQhO-3DYHyU,1752
13
+ rbtr_lang_ruby/tests/test_extraction.py,sha256=-eQZOo4BWtfOV3MUHWVocQkP1Vkgcs36D_YpfPuTDMw,2655
14
+ rbtr_lang_ruby/tests/test_samples.py,sha256=QJ0qb-Q0GwCEgRUcvGlV49DxtTR0cvpVpK5B6mKOxII,2393
15
+ rbtr_lang_ruby-2026.9.0.dev0.dist-info/licenses/LICENSE,sha256=3LvNTMhogXUXkHsDvTWaXdtGd9C-uuoIVD1ey8w9ITs,1077
16
+ rbtr_lang_ruby-2026.9.0.dev0.dist-info/WHEEL,sha256=-i9oRNYVXXZJUIYl5zclLIg6onEb0NLibTX34uln84w,81
17
+ rbtr_lang_ruby-2026.9.0.dev0.dist-info/entry_points.txt,sha256=bOb0oogqZZYs1lO5T3A-ftmYBoF65WXloUNxx4rX_Tg,52
18
+ rbtr_lang_ruby-2026.9.0.dev0.dist-info/METADATA,sha256=LXK24nhttttqhUt2fxbTAhNI8XY39ajwu9pbYCEO9iY,2284
19
+ rbtr_lang_ruby-2026.9.0.dev0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.12.13
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [rbtr.languages]
2
+ ruby = rbtr_lang_ruby.plugin:ruby
3
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alejandro Giacometti
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.