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.
- rbtr_lang_ruby/__init__.py +1 -0
- rbtr_lang_ruby/plugin.py +97 -0
- rbtr_lang_ruby/py.typed +0 -0
- rbtr_lang_ruby/ruby.scm +51 -0
- rbtr_lang_ruby/tests/__init__.py +0 -0
- rbtr_lang_ruby/tests/__snapshots__/test_samples/test_edges_match_snapshot.json +3 -0
- rbtr_lang_ruby/tests/__snapshots__/test_samples/test_extraction_matches_snapshot.json +382 -0
- rbtr_lang_ruby/tests/cases_docstrings.py +126 -0
- rbtr_lang_ruby/tests/cases_extraction.py +320 -0
- rbtr_lang_ruby/tests/samples/ruby/config.rb +4 -0
- rbtr_lang_ruby/tests/samples/ruby/ruby.rb +58 -0
- rbtr_lang_ruby/tests/test_docstrings.py +43 -0
- rbtr_lang_ruby/tests/test_extraction.py +57 -0
- rbtr_lang_ruby/tests/test_samples.py +75 -0
- rbtr_lang_ruby-2026.9.0.dev0.dist-info/METADATA +63 -0
- rbtr_lang_ruby-2026.9.0.dev0.dist-info/RECORD +19 -0
- rbtr_lang_ruby-2026.9.0.dev0.dist-info/WHEEL +4 -0
- rbtr_lang_ruby-2026.9.0.dev0.dist-info/entry_points.txt +3 -0
- rbtr_lang_ruby-2026.9.0.dev0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Ruby language plugin package."""
|
rbtr_lang_ruby/plugin.py
ADDED
|
@@ -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)
|
rbtr_lang_ruby/py.typed
ADDED
|
File without changes
|
rbtr_lang_ruby/ruby.scm
ADDED
|
@@ -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,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,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,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.
|