rubydex 0.1.0.beta12-aarch64-linux

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.
Files changed (109) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +23 -0
  3. data/README.md +125 -0
  4. data/THIRD_PARTY_LICENSES.html +4562 -0
  5. data/exe/rdx +47 -0
  6. data/ext/rubydex/declaration.c +453 -0
  7. data/ext/rubydex/declaration.h +23 -0
  8. data/ext/rubydex/definition.c +284 -0
  9. data/ext/rubydex/definition.h +28 -0
  10. data/ext/rubydex/diagnostic.c +6 -0
  11. data/ext/rubydex/diagnostic.h +11 -0
  12. data/ext/rubydex/document.c +97 -0
  13. data/ext/rubydex/document.h +10 -0
  14. data/ext/rubydex/extconf.rb +138 -0
  15. data/ext/rubydex/graph.c +681 -0
  16. data/ext/rubydex/graph.h +10 -0
  17. data/ext/rubydex/handle.h +44 -0
  18. data/ext/rubydex/location.c +22 -0
  19. data/ext/rubydex/location.h +15 -0
  20. data/ext/rubydex/reference.c +123 -0
  21. data/ext/rubydex/reference.h +15 -0
  22. data/ext/rubydex/rubydex.c +22 -0
  23. data/ext/rubydex/utils.c +108 -0
  24. data/ext/rubydex/utils.h +34 -0
  25. data/lib/rubydex/3.2/rubydex.so +0 -0
  26. data/lib/rubydex/3.3/rubydex.so +0 -0
  27. data/lib/rubydex/3.4/rubydex.so +0 -0
  28. data/lib/rubydex/4.0/rubydex.so +0 -0
  29. data/lib/rubydex/comment.rb +17 -0
  30. data/lib/rubydex/diagnostic.rb +21 -0
  31. data/lib/rubydex/failures.rb +15 -0
  32. data/lib/rubydex/graph.rb +98 -0
  33. data/lib/rubydex/keyword.rb +17 -0
  34. data/lib/rubydex/keyword_parameter.rb +13 -0
  35. data/lib/rubydex/librubydex_sys.so +0 -0
  36. data/lib/rubydex/location.rb +90 -0
  37. data/lib/rubydex/mixin.rb +22 -0
  38. data/lib/rubydex/version.rb +5 -0
  39. data/lib/rubydex.rb +23 -0
  40. data/rbi/rubydex.rbi +422 -0
  41. data/rust/Cargo.lock +1851 -0
  42. data/rust/Cargo.toml +29 -0
  43. data/rust/about.hbs +78 -0
  44. data/rust/about.toml +10 -0
  45. data/rust/rubydex/Cargo.toml +42 -0
  46. data/rust/rubydex/src/compile_assertions.rs +13 -0
  47. data/rust/rubydex/src/diagnostic.rs +110 -0
  48. data/rust/rubydex/src/errors.rs +28 -0
  49. data/rust/rubydex/src/indexing/local_graph.rs +224 -0
  50. data/rust/rubydex/src/indexing/rbs_indexer.rs +1551 -0
  51. data/rust/rubydex/src/indexing/ruby_indexer.rs +2329 -0
  52. data/rust/rubydex/src/indexing/ruby_indexer_tests.rs +4962 -0
  53. data/rust/rubydex/src/indexing.rs +210 -0
  54. data/rust/rubydex/src/integrity.rs +279 -0
  55. data/rust/rubydex/src/job_queue.rs +205 -0
  56. data/rust/rubydex/src/lib.rs +17 -0
  57. data/rust/rubydex/src/listing.rs +371 -0
  58. data/rust/rubydex/src/main.rs +160 -0
  59. data/rust/rubydex/src/model/built_in.rs +83 -0
  60. data/rust/rubydex/src/model/comment.rs +24 -0
  61. data/rust/rubydex/src/model/declaration.rs +671 -0
  62. data/rust/rubydex/src/model/definitions.rs +1682 -0
  63. data/rust/rubydex/src/model/document.rs +222 -0
  64. data/rust/rubydex/src/model/encoding.rs +22 -0
  65. data/rust/rubydex/src/model/graph.rs +3754 -0
  66. data/rust/rubydex/src/model/id.rs +110 -0
  67. data/rust/rubydex/src/model/identity_maps.rs +58 -0
  68. data/rust/rubydex/src/model/ids.rs +60 -0
  69. data/rust/rubydex/src/model/keywords.rs +256 -0
  70. data/rust/rubydex/src/model/name.rs +298 -0
  71. data/rust/rubydex/src/model/references.rs +111 -0
  72. data/rust/rubydex/src/model/string_ref.rs +50 -0
  73. data/rust/rubydex/src/model/visibility.rs +41 -0
  74. data/rust/rubydex/src/model.rs +15 -0
  75. data/rust/rubydex/src/offset.rs +147 -0
  76. data/rust/rubydex/src/position.rs +6 -0
  77. data/rust/rubydex/src/query.rs +1841 -0
  78. data/rust/rubydex/src/resolution.rs +6517 -0
  79. data/rust/rubydex/src/stats/memory.rs +71 -0
  80. data/rust/rubydex/src/stats/orphan_report.rs +264 -0
  81. data/rust/rubydex/src/stats/timer.rs +127 -0
  82. data/rust/rubydex/src/stats.rs +11 -0
  83. data/rust/rubydex/src/test_utils/context.rs +226 -0
  84. data/rust/rubydex/src/test_utils/graph_test.rs +730 -0
  85. data/rust/rubydex/src/test_utils/local_graph_test.rs +602 -0
  86. data/rust/rubydex/src/test_utils.rs +52 -0
  87. data/rust/rubydex/src/visualization/dot.rs +192 -0
  88. data/rust/rubydex/src/visualization.rs +6 -0
  89. data/rust/rubydex/tests/cli.rs +185 -0
  90. data/rust/rubydex-mcp/Cargo.toml +28 -0
  91. data/rust/rubydex-mcp/src/main.rs +48 -0
  92. data/rust/rubydex-mcp/src/server.rs +1145 -0
  93. data/rust/rubydex-mcp/src/tools.rs +49 -0
  94. data/rust/rubydex-mcp/tests/mcp.rs +302 -0
  95. data/rust/rubydex-sys/Cargo.toml +20 -0
  96. data/rust/rubydex-sys/build.rs +14 -0
  97. data/rust/rubydex-sys/cbindgen.toml +12 -0
  98. data/rust/rubydex-sys/src/declaration_api.rs +485 -0
  99. data/rust/rubydex-sys/src/definition_api.rs +443 -0
  100. data/rust/rubydex-sys/src/diagnostic_api.rs +99 -0
  101. data/rust/rubydex-sys/src/document_api.rs +85 -0
  102. data/rust/rubydex-sys/src/graph_api.rs +948 -0
  103. data/rust/rubydex-sys/src/lib.rs +79 -0
  104. data/rust/rubydex-sys/src/location_api.rs +79 -0
  105. data/rust/rubydex-sys/src/name_api.rs +135 -0
  106. data/rust/rubydex-sys/src/reference_api.rs +267 -0
  107. data/rust/rubydex-sys/src/utils.rs +70 -0
  108. data/rust/rustfmt.toml +2 -0
  109. metadata +159 -0
@@ -0,0 +1,2329 @@
1
+ //! Visit the Ruby AST and create the definitions.
2
+
3
+ use crate::diagnostic::Rule;
4
+ use crate::indexing::local_graph::LocalGraph;
5
+ use crate::model::comment::Comment;
6
+ use crate::model::definitions::{
7
+ AttrAccessorDefinition, AttrReaderDefinition, AttrWriterDefinition, ClassDefinition, ClassVariableDefinition,
8
+ ConstantAliasDefinition, ConstantDefinition, ConstantVisibilityDefinition, Definition, DefinitionFlags,
9
+ ExtendDefinition, GlobalVariableAliasDefinition, GlobalVariableDefinition, IncludeDefinition,
10
+ InstanceVariableDefinition, MethodAliasDefinition, MethodDefinition, MethodVisibilityDefinition, Mixin,
11
+ ModuleDefinition, Parameter, ParameterStruct, PrependDefinition, Receiver, Signatures, SingletonClassDefinition,
12
+ };
13
+ use crate::model::document::Document;
14
+ use crate::model::ids::{DefinitionId, NameId, StringId, UriId};
15
+ use crate::model::name::{Name, ParentScope};
16
+ use crate::model::references::{ConstantReference, MethodRef};
17
+ use crate::model::visibility::Visibility;
18
+ use crate::offset::Offset;
19
+
20
+ use ruby_prism::{ParseResult, Visit};
21
+
22
+ #[derive(Clone, Copy)]
23
+ enum MixinType {
24
+ Include,
25
+ Prepend,
26
+ Extend,
27
+ }
28
+
29
+ enum Nesting {
30
+ /// Nesting stack entries that produce a new lexical scope to which constant references must be attached to (i.e.:
31
+ /// the class and module keywords). All lexical scopes are also owner, but the opposite is not true
32
+ LexicalScope(DefinitionId),
33
+ /// An owner entry that will be associated with all members encountered, but will not produce a new lexical scope
34
+ /// (e.g.: Module.new or Class.new)
35
+ Owner(DefinitionId),
36
+ /// A method entry that is used to set the correct owner for instance variables, but cannot own anything itself
37
+ Method(DefinitionId),
38
+ }
39
+
40
+ impl Nesting {
41
+ fn id(&self) -> DefinitionId {
42
+ match self {
43
+ Nesting::LexicalScope(id) | Nesting::Owner(id) | Nesting::Method(id) => *id,
44
+ }
45
+ }
46
+ }
47
+
48
+ struct VisibilityModifier {
49
+ visibility: Visibility,
50
+ is_inline: bool,
51
+ offset: Offset,
52
+ }
53
+
54
+ impl VisibilityModifier {
55
+ #[must_use]
56
+ pub fn new(visibility: Visibility, is_inline: bool, offset: Offset) -> Self {
57
+ Self {
58
+ visibility,
59
+ is_inline,
60
+ offset,
61
+ }
62
+ }
63
+
64
+ #[must_use]
65
+ pub fn visibility(&self) -> &Visibility {
66
+ &self.visibility
67
+ }
68
+
69
+ #[must_use]
70
+ pub fn is_inline(&self) -> bool {
71
+ self.is_inline
72
+ }
73
+
74
+ #[must_use]
75
+ pub fn offset(&self) -> &Offset {
76
+ &self.offset
77
+ }
78
+ }
79
+
80
+ /// The indexer for the definitions found in the Ruby source code.
81
+ ///
82
+ /// It implements the `Visit` trait from `ruby_prism` to visit the AST and create a hash of definitions that must be
83
+ /// merged into the global state later.
84
+ pub struct RubyIndexer<'a> {
85
+ uri_id: UriId,
86
+ local_graph: LocalGraph,
87
+ source: &'a str,
88
+ comments: Vec<CommentGroup>,
89
+ nesting_stack: Vec<Nesting>,
90
+ visibility_stack: Vec<VisibilityModifier>,
91
+ pending_decorator_offset: Option<Offset>,
92
+ }
93
+
94
+ impl<'a> RubyIndexer<'a> {
95
+ #[must_use]
96
+ pub fn new(uri: String, source: &'a str) -> Self {
97
+ let uri_id = UriId::from(&uri);
98
+ let local_graph = LocalGraph::new(uri_id, Document::new(uri, source));
99
+
100
+ Self {
101
+ uri_id,
102
+ local_graph,
103
+ source,
104
+ comments: Vec::new(),
105
+ nesting_stack: Vec::new(),
106
+ visibility_stack: vec![VisibilityModifier::new(Visibility::Private, false, Offset::new(0, 0))],
107
+ pending_decorator_offset: None,
108
+ }
109
+ }
110
+
111
+ #[must_use]
112
+ pub fn local_graph(self) -> LocalGraph {
113
+ self.local_graph
114
+ }
115
+
116
+ pub fn index(&mut self) {
117
+ let result = ruby_prism::parse(self.source.as_bytes());
118
+
119
+ for error in result.errors() {
120
+ self.local_graph.add_diagnostic(
121
+ Rule::ParseError,
122
+ Offset::from_prism_location(&error.location()),
123
+ error.message().to_string(),
124
+ );
125
+ }
126
+
127
+ for warning in result.warnings() {
128
+ self.local_graph.add_diagnostic(
129
+ Rule::ParseWarning,
130
+ Offset::from_prism_location(&warning.location()),
131
+ warning.message().to_string(),
132
+ );
133
+ }
134
+
135
+ self.comments = self.parse_comments_into_groups(&result);
136
+ self.visit(&result.node());
137
+ }
138
+
139
+ fn parse_comments_into_groups(&mut self, result: &ParseResult<'_>) -> Vec<CommentGroup> {
140
+ let mut iter = result.comments().peekable();
141
+ let mut groups = Vec::new();
142
+
143
+ while let Some(comment) = iter.next() {
144
+ let mut group = CommentGroup::new();
145
+ group.add_comment(&comment);
146
+ while let Some(next_comment) = iter.peek() {
147
+ if group.accepts(next_comment, self.source) {
148
+ let next = iter.next().unwrap();
149
+ group.add_comment(&next);
150
+ } else {
151
+ break;
152
+ }
153
+ }
154
+ groups.push(group);
155
+ }
156
+ groups
157
+ }
158
+
159
+ fn location_to_string(location: &ruby_prism::Location) -> String {
160
+ String::from_utf8_lossy(location.as_slice()).to_string()
161
+ }
162
+
163
+ fn offset_to_string(&self, offset: &Offset) -> String {
164
+ self.source[offset.start() as usize..offset.end() as usize].to_string()
165
+ }
166
+
167
+ fn find_comments_for(&self, offset: u32) -> (Box<[Comment]>, DefinitionFlags) {
168
+ let offset_usize = offset as usize;
169
+ if self.comments.is_empty() {
170
+ return (Box::default(), DefinitionFlags::empty());
171
+ }
172
+
173
+ let idx = match self.comments.binary_search_by_key(&offset_usize, |g| g.end_offset) {
174
+ Ok(_) => {
175
+ // This should never happen in valid Ruby syntax - a comment cannot end exactly
176
+ // where a definition begins (there must be at least a newline between them)
177
+ debug_assert!(false, "Comment ends exactly at definition start - this indicates a bug");
178
+ return (Box::default(), DefinitionFlags::empty());
179
+ }
180
+ Err(i) if i > 0 => i - 1,
181
+ Err(_) => return (Box::default(), DefinitionFlags::empty()),
182
+ };
183
+
184
+ let group = &self.comments[idx];
185
+ let between = &self.source.as_bytes()[group.end_offset..offset_usize];
186
+ if !between.iter().all(|&b| b.is_ascii_whitespace()) {
187
+ return (Box::default(), DefinitionFlags::empty());
188
+ }
189
+
190
+ // We allow at most one blank line between the comment and the definition
191
+ if bytecount::count(between, b'\n') > 2 {
192
+ return (Box::default(), DefinitionFlags::empty());
193
+ }
194
+
195
+ (group.comments(), group.flags())
196
+ }
197
+
198
+ /// We consider comments above a method decorator like Sorbet's sig to be documentation for methods and attributes.
199
+ /// To find the correct comment offset, we remember the offsets for the sigs we find
200
+ fn take_decorator_offset(&mut self, definition_start: u32) -> Option<u32> {
201
+ let decorator_offset = self.pending_decorator_offset.take()?;
202
+ if decorator_offset.end() > definition_start {
203
+ return None;
204
+ }
205
+
206
+ let between = &self.source.as_bytes()[decorator_offset.end() as usize..definition_start as usize];
207
+ if between.iter().all(|&b| b.is_ascii_whitespace()) && bytecount::count(between, b'\n') <= 1 {
208
+ Some(decorator_offset.start())
209
+ } else {
210
+ None
211
+ }
212
+ }
213
+
214
+ fn collect_parameters(&mut self, node: &ruby_prism::DefNode) -> Vec<Parameter> {
215
+ let mut parameters: Vec<Parameter> = Vec::new();
216
+
217
+ if let Some(parameters_list) = node.parameters() {
218
+ for parameter in &parameters_list.requireds() {
219
+ let location = parameter.location();
220
+ let str_id = self.local_graph.intern_string(Self::location_to_string(&location));
221
+
222
+ parameters.push(Parameter::RequiredPositional(ParameterStruct::new(
223
+ Offset::from_prism_location(&location),
224
+ str_id,
225
+ )));
226
+ }
227
+
228
+ for parameter in &parameters_list.optionals() {
229
+ let opt_param = parameter.as_optional_parameter_node().unwrap();
230
+ let name_loc = opt_param.name_loc();
231
+ let str_id = self.local_graph.intern_string(Self::location_to_string(&name_loc));
232
+
233
+ parameters.push(Parameter::OptionalPositional(ParameterStruct::new(
234
+ Offset::from_prism_location(&name_loc),
235
+ str_id,
236
+ )));
237
+ }
238
+
239
+ if let Some(rest) = parameters_list.rest() {
240
+ let rest_param = rest.as_rest_parameter_node().unwrap();
241
+ let location = rest_param.name_loc().unwrap_or_else(|| rest.location());
242
+ let str_id = self.local_graph.intern_string(Self::location_to_string(&location));
243
+
244
+ parameters.push(Parameter::RestPositional(ParameterStruct::new(
245
+ Offset::from_prism_location(&location),
246
+ str_id,
247
+ )));
248
+ }
249
+
250
+ for post in &parameters_list.posts() {
251
+ let location = post.location();
252
+ let str_id = self.local_graph.intern_string(Self::location_to_string(&location));
253
+
254
+ parameters.push(Parameter::Post(ParameterStruct::new(
255
+ Offset::from_prism_location(&location),
256
+ str_id,
257
+ )));
258
+ }
259
+
260
+ for keyword in &parameters_list.keywords() {
261
+ match keyword {
262
+ ruby_prism::Node::RequiredKeywordParameterNode { .. } => {
263
+ let required = keyword.as_required_keyword_parameter_node().unwrap();
264
+ let loc = required.name_loc();
265
+ let full = Offset::from_prism_location(&loc);
266
+ let offset = Offset::new(full.start(), full.end() - 1); // Exclude trailing colon
267
+ let str_id = self.local_graph.intern_string(self.offset_to_string(&offset));
268
+
269
+ parameters.push(Parameter::RequiredKeyword(ParameterStruct::new(offset, str_id)));
270
+ }
271
+ ruby_prism::Node::OptionalKeywordParameterNode { .. } => {
272
+ let optional = keyword.as_optional_keyword_parameter_node().unwrap();
273
+ let loc = optional.name_loc();
274
+ let full = Offset::from_prism_location(&loc);
275
+ let offset = Offset::new(full.start(), full.end() - 1); // Exclude trailing colon
276
+ let str_id = self.local_graph.intern_string(self.offset_to_string(&offset));
277
+
278
+ parameters.push(Parameter::OptionalKeyword(ParameterStruct::new(offset, str_id)));
279
+ }
280
+ _ => {}
281
+ }
282
+ }
283
+
284
+ if let Some(rest) = parameters_list.keyword_rest() {
285
+ match rest {
286
+ ruby_prism::Node::KeywordRestParameterNode { .. } => {
287
+ let location = rest
288
+ .as_keyword_rest_parameter_node()
289
+ .unwrap()
290
+ .name_loc()
291
+ .unwrap_or_else(|| rest.location());
292
+ let str_id = self.local_graph.intern_string(Self::location_to_string(&location));
293
+
294
+ parameters.push(Parameter::RestKeyword(ParameterStruct::new(
295
+ Offset::from_prism_location(&location),
296
+ str_id,
297
+ )));
298
+ }
299
+ ruby_prism::Node::ForwardingParameterNode { .. } => {
300
+ let location = rest.location();
301
+ let str_id = self.local_graph.intern_string(Self::location_to_string(&location));
302
+
303
+ parameters.push(Parameter::Forward(ParameterStruct::new(
304
+ Offset::from_prism_location(&location),
305
+ str_id,
306
+ )));
307
+ }
308
+ _ => {
309
+ // Do nothing
310
+ }
311
+ }
312
+ }
313
+
314
+ if let Some(block) = parameters_list.block() {
315
+ let location = block.name_loc().unwrap_or_else(|| block.location());
316
+ let str_id = self.local_graph.intern_string(Self::location_to_string(&location));
317
+
318
+ parameters.push(Parameter::Block(ParameterStruct::new(
319
+ Offset::from_prism_location(&location),
320
+ str_id,
321
+ )));
322
+ }
323
+ }
324
+
325
+ parameters
326
+ }
327
+
328
+ /// Gets the `NameId` of the current lexical scope (class/module/singleton class).
329
+ /// Used to resolve `self` to a concrete `NameId` during indexing.
330
+ ///
331
+ /// Iterates through the definitions stack in reverse to find the first class/module/singleton class, skipping
332
+ /// methods. Ignores `Class.new` and other owners that do not produce lexical scopes
333
+ ///
334
+ /// # Panics
335
+ ///
336
+ /// Panics if the definition is not a class, module, or singleton class
337
+ fn current_lexical_scope_name_id(&self) -> Option<NameId> {
338
+ self.nesting_stack.iter().rev().find_map(|nesting| match nesting {
339
+ Nesting::LexicalScope(id) => {
340
+ if let Some(definition) = self.local_graph.definitions().get(id) {
341
+ match definition {
342
+ Definition::Class(class_def) => Some(*class_def.name_id()),
343
+ Definition::Module(module_def) => Some(*module_def.name_id()),
344
+ Definition::SingletonClass(singleton_class_def) => Some(*singleton_class_def.name_id()),
345
+ Definition::Method(_) => None,
346
+ _ => panic!("current nesting is not a class/module/singleton class: {definition:?}"),
347
+ }
348
+ } else {
349
+ None
350
+ }
351
+ }
352
+ Nesting::Method(_) | Nesting::Owner(_) => None,
353
+ })
354
+ }
355
+
356
+ /// Gets the `NameId` of the current owner (class/module/singleton class), including `Class.new`/`Module.new`.
357
+ /// Used to resolve `self` in singleton method definitions (e.g., `def self.bar`).
358
+ ///
359
+ /// Unlike `current_lexical_scope_name_id`, this method considers `Nesting::Owner` entries,
360
+ /// because `self` inside a `Class.new` block refers to the new class being created.
361
+ fn current_owner_name_id(&self) -> Option<NameId> {
362
+ self.nesting_stack.iter().rev().find_map(|nesting| match nesting {
363
+ Nesting::LexicalScope(id) | Nesting::Owner(id) => {
364
+ if let Some(definition) = self.local_graph.definitions().get(id) {
365
+ match definition {
366
+ Definition::Class(class_def) => Some(*class_def.name_id()),
367
+ Definition::Module(module_def) => Some(*module_def.name_id()),
368
+ Definition::SingletonClass(singleton_class_def) => Some(*singleton_class_def.name_id()),
369
+ Definition::Method(_) => None,
370
+ _ => panic!("current nesting is not a class/module/singleton class: {definition:?}"),
371
+ }
372
+ } else {
373
+ None
374
+ }
375
+ }
376
+ Nesting::Method(_) => None,
377
+ })
378
+ }
379
+
380
+ // Runs the given closure for each string or symbol argument of a call node.
381
+ fn each_string_or_symbol_arg<F>(node: &ruby_prism::CallNode, mut f: F)
382
+ where
383
+ F: FnMut(String, ruby_prism::Location),
384
+ {
385
+ if let Some(arguments) = node.arguments() {
386
+ for argument in &arguments.arguments() {
387
+ match argument {
388
+ ruby_prism::Node::SymbolNode { .. } => {
389
+ let symbol = argument.as_symbol_node().unwrap();
390
+
391
+ if let Some(value_loc) = symbol.value_loc() {
392
+ let name = Self::location_to_string(&value_loc);
393
+ f(name, value_loc);
394
+ }
395
+ }
396
+ ruby_prism::Node::StringNode { .. } => {
397
+ let string = argument.as_string_node().unwrap();
398
+ let name = String::from_utf8_lossy(string.unescaped()).to_string();
399
+ f(name, argument.location());
400
+ }
401
+ _ => {}
402
+ }
403
+ }
404
+ }
405
+ }
406
+
407
+ fn index_constant_reference(&mut self, node: &ruby_prism::Node, push_final_reference: bool) -> Option<NameId> {
408
+ let mut parent_scope_id = ParentScope::None;
409
+
410
+ let location = match node {
411
+ ruby_prism::Node::ConstantPathNode { .. } => {
412
+ let constant = node.as_constant_path_node().unwrap();
413
+
414
+ if let Some(parent) = constant.parent() {
415
+ // Ignore parent scopes that are not constants, like `foo::Bar`
416
+ match parent {
417
+ ruby_prism::Node::ConstantPathNode { .. } | ruby_prism::Node::ConstantReadNode { .. } => {}
418
+ _ => {
419
+ self.local_graph.add_diagnostic(
420
+ Rule::DynamicConstantReference,
421
+ Offset::from_prism_location(&parent.location()),
422
+ "Dynamic constant reference".to_string(),
423
+ );
424
+
425
+ return None;
426
+ }
427
+ }
428
+
429
+ parent_scope_id = self
430
+ .index_constant_reference(&parent, true)
431
+ .map_or(ParentScope::None, ParentScope::Some);
432
+ } else {
433
+ parent_scope_id = ParentScope::TopLevel;
434
+ }
435
+
436
+ constant.name_loc()
437
+ }
438
+ ruby_prism::Node::ConstantPathWriteNode { .. } => {
439
+ let constant = node.as_constant_path_write_node().unwrap();
440
+ let target = constant.target();
441
+
442
+ if let Some(parent) = target.parent() {
443
+ // Ignore parent scopes that are not constants, like `foo::Bar`
444
+ match parent {
445
+ ruby_prism::Node::ConstantPathNode { .. } | ruby_prism::Node::ConstantReadNode { .. } => {}
446
+ _ => {
447
+ return None;
448
+ }
449
+ }
450
+
451
+ parent_scope_id = self
452
+ .index_constant_reference(&parent, true)
453
+ .map_or(ParentScope::None, ParentScope::Some);
454
+ } else {
455
+ parent_scope_id = ParentScope::TopLevel;
456
+ }
457
+
458
+ target.name_loc()
459
+ }
460
+ ruby_prism::Node::ConstantReadNode { .. } => node.location(),
461
+ ruby_prism::Node::ConstantAndWriteNode { .. } => node.as_constant_and_write_node().unwrap().name_loc(),
462
+ ruby_prism::Node::ConstantOperatorWriteNode { .. } => {
463
+ node.as_constant_operator_write_node().unwrap().name_loc()
464
+ }
465
+ ruby_prism::Node::ConstantOrWriteNode { .. } => node.as_constant_or_write_node().unwrap().name_loc(),
466
+ ruby_prism::Node::ConstantTargetNode { .. } => node.as_constant_target_node().unwrap().location(),
467
+ ruby_prism::Node::ConstantWriteNode { .. } => node.as_constant_write_node().unwrap().name_loc(),
468
+ ruby_prism::Node::ConstantPathTargetNode { .. } => {
469
+ let target = node.as_constant_path_target_node().unwrap();
470
+
471
+ if let Some(parent) = target.parent() {
472
+ match parent {
473
+ ruby_prism::Node::ConstantPathNode { .. } | ruby_prism::Node::ConstantReadNode { .. } => {}
474
+ _ => {
475
+ return None;
476
+ }
477
+ }
478
+
479
+ parent_scope_id = self
480
+ .index_constant_reference(&parent, true)
481
+ .map_or(ParentScope::None, ParentScope::Some);
482
+ } else {
483
+ parent_scope_id = ParentScope::TopLevel;
484
+ }
485
+
486
+ target.name_loc()
487
+ }
488
+ _ => {
489
+ return None;
490
+ }
491
+ };
492
+
493
+ let offset = Offset::from_prism_location(&location);
494
+ let name = Self::location_to_string(&location);
495
+ let string_id = self.local_graph.intern_string(name);
496
+ let name_id = self.local_graph.add_name(Name::new(
497
+ string_id,
498
+ parent_scope_id,
499
+ self.current_lexical_scope_name_id(),
500
+ ));
501
+
502
+ if push_final_reference {
503
+ self.local_graph
504
+ .add_constant_reference(ConstantReference::new(name_id, self.uri_id, offset));
505
+ }
506
+
507
+ Some(name_id)
508
+ }
509
+
510
+ fn index_method_reference(&mut self, name: String, location: &ruby_prism::Location, receiver: Option<NameId>) {
511
+ let offset = Offset::from_prism_location(location);
512
+ let str_id = self.local_graph.intern_string(name);
513
+ let reference = MethodRef::new(str_id, self.uri_id, offset, receiver);
514
+ self.local_graph.add_method_reference(reference);
515
+ }
516
+
517
+ fn add_definition_from_location<F>(&mut self, location: &ruby_prism::Location, builder: F) -> DefinitionId
518
+ where
519
+ F: FnOnce(StringId, Offset, Box<[Comment]>, DefinitionFlags, Option<DefinitionId>, UriId) -> Definition,
520
+ {
521
+ let name = Self::location_to_string(location);
522
+ let str_id = self.local_graph.intern_string(name);
523
+ let offset = Offset::from_prism_location(location);
524
+ let (comments, flags) = self.find_comments_for(offset.start());
525
+ let parent_nesting_id = self.parent_nesting_id();
526
+ let uri_id = self.uri_id;
527
+
528
+ let definition = builder(str_id, offset, comments, flags, parent_nesting_id, uri_id);
529
+ let definition_id = self.local_graph.add_definition(definition);
530
+
531
+ self.add_member_to_current_owner(definition_id);
532
+
533
+ definition_id
534
+ }
535
+
536
+ fn add_instance_variable_definition(&mut self, location: &ruby_prism::Location) -> DefinitionId {
537
+ let name = Self::location_to_string(location);
538
+ let str_id = self.local_graph.intern_string(name);
539
+ let offset = Offset::from_prism_location(location);
540
+ let (comments, flags) = self.find_comments_for(offset.start());
541
+ let parent_nesting_id = self.parent_nesting_id();
542
+ let uri_id = self.uri_id;
543
+
544
+ let definition = Definition::InstanceVariable(Box::new(InstanceVariableDefinition::new(
545
+ str_id,
546
+ uri_id,
547
+ offset,
548
+ comments,
549
+ flags,
550
+ parent_nesting_id,
551
+ )));
552
+
553
+ let definition_id = self.local_graph.add_definition(definition);
554
+ self.add_member_to_current_owner(definition_id);
555
+ definition_id
556
+ }
557
+
558
+ /// Adds a class variable definition.
559
+ ///
560
+ /// Class variables use lexical scoping - they belong to the lexically enclosing class/module,
561
+ /// not the method receiver. This is different from instance variables which follow the receiver.
562
+ fn add_class_variable_definition(&mut self, location: &ruby_prism::Location) -> DefinitionId {
563
+ let name = Self::location_to_string(location);
564
+ let str_id = self.local_graph.intern_string(name);
565
+ let offset = Offset::from_prism_location(location);
566
+ let (comments, flags) = self.find_comments_for(offset.start());
567
+ // Class variables use the enclosing class/module (skipping methods) as lexical nesting
568
+ let lexical_nesting_id = self.parent_lexical_scope_id();
569
+ let uri_id = self.uri_id;
570
+
571
+ let definition = Definition::ClassVariable(Box::new(ClassVariableDefinition::new(
572
+ str_id,
573
+ uri_id,
574
+ offset,
575
+ comments,
576
+ flags,
577
+ lexical_nesting_id,
578
+ )));
579
+
580
+ let definition_id = self.local_graph.add_definition(definition);
581
+ self.add_member_to_current_owner(definition_id);
582
+ definition_id
583
+ }
584
+
585
+ /// Returns whether a value node represents a method call that could produce a class or module.
586
+ /// Only regular method calls (bare calls or dot/safe-nav calls) are considered promotable.
587
+ /// Operator calls like `1 + 2` are `CallNode`s in Prism but should not be promotable.
588
+ fn is_promotable_value(value: &ruby_prism::Node) -> bool {
589
+ value.as_call_node().is_some_and(|call| {
590
+ // Bare calls (no receiver): `some_factory_call`
591
+ // Dot/safe-nav/scope calls: `Struct.new(...)`, `foo&.bar`, `Struct::new`
592
+ // Excluded: operator calls like `1 + 2` which have a receiver but no call operator
593
+ call.receiver().is_none() || call.call_operator_loc().is_some()
594
+ })
595
+ }
596
+
597
+ fn add_constant_definition(
598
+ &mut self,
599
+ node: &ruby_prism::Node,
600
+ also_add_reference: bool,
601
+ promotable: bool,
602
+ ) -> Option<DefinitionId> {
603
+ let name_id = self.index_constant_reference(node, also_add_reference)?;
604
+
605
+ // Get the location for the constant name/path only (not including the value)
606
+ let location = match node {
607
+ ruby_prism::Node::ConstantWriteNode { .. } => node.as_constant_write_node().unwrap().name_loc(),
608
+ ruby_prism::Node::ConstantOrWriteNode { .. } => node.as_constant_or_write_node().unwrap().name_loc(),
609
+ ruby_prism::Node::ConstantPathNode { .. } => node.as_constant_path_node().unwrap().name_loc(),
610
+ _ => node.location(),
611
+ };
612
+
613
+ let offset = Offset::from_prism_location(&location);
614
+ let (comments, mut flags) = self.find_comments_for(offset.start());
615
+ if promotable {
616
+ flags |= DefinitionFlags::PROMOTABLE;
617
+ }
618
+ let lexical_nesting_id = self.parent_lexical_scope_id();
619
+
620
+ let definition = Definition::Constant(Box::new(ConstantDefinition::new(
621
+ name_id,
622
+ self.uri_id,
623
+ offset,
624
+ comments,
625
+ flags,
626
+ lexical_nesting_id,
627
+ )));
628
+ let definition_id = self.local_graph.add_definition(definition);
629
+
630
+ self.add_member_to_current_owner(definition_id);
631
+
632
+ Some(definition_id)
633
+ }
634
+
635
+ fn handle_class_definition(
636
+ &mut self,
637
+ location: &ruby_prism::Location,
638
+ name_node: Option<&ruby_prism::Node>,
639
+ body_node: Option<ruby_prism::Node>,
640
+ superclass_node: Option<ruby_prism::Node>,
641
+ nesting_type: fn(DefinitionId) -> Nesting,
642
+ ) {
643
+ let offset = Offset::from_prism_location(location);
644
+ let (comments, flags) = self.find_comments_for(offset.start());
645
+ let lexical_nesting_id = self.parent_lexical_scope_id();
646
+ let superclass = superclass_node.as_ref().and_then(|n| {
647
+ // Try direct constant reference first
648
+ if let Some(id) = self.index_constant_reference(n, false) {
649
+ return Some(self.local_graph.add_constant_reference(ConstantReference::new(
650
+ id,
651
+ self.uri_id,
652
+ Offset::from_prism_location(&n.location()),
653
+ )));
654
+ }
655
+
656
+ // For call nodes (e.g. `ActiveRecord::Migration[7.0]`), try the receiver constant
657
+ if let ruby_prism::Node::CallNode { .. } = n {
658
+ let call = n.as_call_node().unwrap();
659
+ if let Some(receiver) = call.receiver()
660
+ && let Some(id) = self.index_constant_reference(&receiver, false)
661
+ {
662
+ return Some(self.local_graph.add_constant_reference(ConstantReference::new(
663
+ id,
664
+ self.uri_id,
665
+ Offset::from_prism_location(&receiver.location()),
666
+ )));
667
+ }
668
+ }
669
+
670
+ None
671
+ });
672
+
673
+ if let Some(superclass_node) = superclass_node
674
+ && superclass.is_none()
675
+ {
676
+ self.local_graph.add_diagnostic(
677
+ Rule::DynamicAncestor,
678
+ Offset::from_prism_location(&superclass_node.location()),
679
+ "Dynamic superclass".to_string(),
680
+ );
681
+ }
682
+
683
+ let (name_id, name_offset) = if let Some(name_node) = name_node {
684
+ let name_loc = match name_node {
685
+ ruby_prism::Node::ConstantPathNode { .. } => name_node.as_constant_path_node().unwrap().name_loc(),
686
+ ruby_prism::Node::ConstantPathWriteNode { .. } => {
687
+ name_node.as_constant_path_write_node().unwrap().target().name_loc()
688
+ }
689
+ _ => name_node.location(),
690
+ };
691
+ (
692
+ self.index_constant_reference(name_node, false),
693
+ Offset::from_prism_location(&name_loc),
694
+ )
695
+ } else {
696
+ let string_id = self
697
+ .local_graph
698
+ .intern_string(format!("{}:{}<anonymous>", self.uri_id, offset.start()));
699
+
700
+ (
701
+ Some(self.local_graph.add_name(Name::new(string_id, ParentScope::None, None))),
702
+ offset.clone(),
703
+ )
704
+ };
705
+
706
+ if let Some(name_id) = name_id {
707
+ let definition = Definition::Class(Box::new(ClassDefinition::new(
708
+ name_id,
709
+ self.uri_id,
710
+ offset.clone(),
711
+ name_offset,
712
+ comments,
713
+ flags,
714
+ lexical_nesting_id,
715
+ superclass,
716
+ )));
717
+
718
+ let definition_id = self.local_graph.add_definition(definition);
719
+
720
+ self.add_member_to_current_lexical_scope(definition_id);
721
+
722
+ if let Some(body) = body_node {
723
+ self.nesting_stack.push(nesting_type(definition_id));
724
+ self.visibility_stack
725
+ .push(VisibilityModifier::new(Visibility::Public, false, offset));
726
+ self.visit(&body);
727
+ self.visibility_stack.pop();
728
+ self.nesting_stack.pop();
729
+ }
730
+ }
731
+ }
732
+
733
+ fn handle_module_definition(
734
+ &mut self,
735
+ location: &ruby_prism::Location,
736
+ name_node: Option<&ruby_prism::Node>,
737
+ body_node: Option<ruby_prism::Node>,
738
+ nesting_type: fn(DefinitionId) -> Nesting,
739
+ ) {
740
+ let offset = Offset::from_prism_location(location);
741
+ let (comments, flags) = self.find_comments_for(offset.start());
742
+ let lexical_nesting_id = self.parent_lexical_scope_id();
743
+
744
+ let (name_id, name_offset) = if let Some(name_node) = name_node {
745
+ let name_loc = match name_node {
746
+ ruby_prism::Node::ConstantPathNode { .. } => name_node.as_constant_path_node().unwrap().name_loc(),
747
+ ruby_prism::Node::ConstantPathWriteNode { .. } => {
748
+ name_node.as_constant_path_write_node().unwrap().target().name_loc()
749
+ }
750
+ _ => name_node.location(),
751
+ };
752
+ (
753
+ self.index_constant_reference(name_node, false),
754
+ Offset::from_prism_location(&name_loc),
755
+ )
756
+ } else {
757
+ let string_id = self
758
+ .local_graph
759
+ .intern_string(format!("{}:{}<anonymous>", self.uri_id, offset.start()));
760
+
761
+ (
762
+ Some(self.local_graph.add_name(Name::new(string_id, ParentScope::None, None))),
763
+ offset.clone(),
764
+ )
765
+ };
766
+
767
+ if let Some(name_id) = name_id {
768
+ let definition = Definition::Module(Box::new(ModuleDefinition::new(
769
+ name_id,
770
+ self.uri_id,
771
+ offset.clone(),
772
+ name_offset,
773
+ comments,
774
+ flags,
775
+ lexical_nesting_id,
776
+ )));
777
+
778
+ let definition_id = self.local_graph.add_definition(definition);
779
+
780
+ self.add_member_to_current_lexical_scope(definition_id);
781
+
782
+ if let Some(body) = body_node {
783
+ self.nesting_stack.push(nesting_type(definition_id));
784
+ self.visibility_stack
785
+ .push(VisibilityModifier::new(Visibility::Public, false, offset));
786
+ self.visit(&body);
787
+ self.visibility_stack.pop();
788
+ self.nesting_stack.pop();
789
+ }
790
+ }
791
+ }
792
+
793
+ /// Handle dynamic class or module definitions, like `Module.new`, `Class.new`, `Data.define` and so on
794
+ fn handle_dynamic_class_or_module(&mut self, node: &ruby_prism::Node, value: &ruby_prism::Node) -> bool {
795
+ let Some(call_node) = value.as_call_node() else {
796
+ return false;
797
+ };
798
+
799
+ if call_node.name().as_slice() != b"new" {
800
+ return false;
801
+ }
802
+
803
+ let Some(receiver) = call_node.receiver() else {
804
+ return false;
805
+ };
806
+
807
+ let receiver_name = receiver.location().as_slice();
808
+
809
+ if matches!(receiver_name, b"Module" | b"::Module") {
810
+ self.handle_module_definition(&node.location(), Some(node), call_node.block(), Nesting::Owner);
811
+ } else if matches!(receiver_name, b"Class" | b"::Class") {
812
+ self.handle_class_definition(
813
+ &node.location(),
814
+ Some(node),
815
+ call_node.block(),
816
+ call_node.arguments().and_then(|args| args.arguments().iter().next()),
817
+ Nesting::Owner,
818
+ );
819
+ } else {
820
+ return false;
821
+ }
822
+
823
+ self.index_method_reference_for_call(&call_node);
824
+ true
825
+ }
826
+
827
+ /// Returns the definition ID of the current nesting (class, module, or singleton class),
828
+ /// but skips methods in the definitions stack.
829
+ fn current_nesting_definition_id(&self) -> Option<DefinitionId> {
830
+ self.nesting_stack.iter().rev().find_map(|nesting| match nesting {
831
+ Nesting::LexicalScope(id) | Nesting::Owner(id) => Some(*id),
832
+ Nesting::Method(_) => None,
833
+ })
834
+ }
835
+
836
+ fn current_nesting_is_module(&self) -> bool {
837
+ self.current_nesting_definition_id().is_some_and(|id| {
838
+ self.local_graph
839
+ .definitions()
840
+ .get(&id)
841
+ .is_some_and(|def| matches!(def, Definition::Module(_)))
842
+ })
843
+ }
844
+
845
+ /// Indexes the final constant target from a value node, unwrapping chained assignments.
846
+ ///
847
+ /// For `A = B = C`, when processing `A`, the value is `ConstantWriteNode(B)`.
848
+ /// This function recursively unwraps to find the final `ConstantReadNode(C)` and indexes it.
849
+ ///
850
+ /// Returns `Some(NameId)` if the final value is a constant (`ConstantReadNode` or `ConstantPathNode`),
851
+ /// or `None` if the chain ends in a non-constant value.
852
+ fn index_constant_alias_target(&mut self, value: &ruby_prism::Node) -> Option<NameId> {
853
+ match value {
854
+ ruby_prism::Node::ConstantReadNode { .. } | ruby_prism::Node::ConstantPathNode { .. } => {
855
+ self.index_constant_reference(value, true)
856
+ }
857
+ ruby_prism::Node::ConstantWriteNode { .. } => {
858
+ let node = value.as_constant_write_node().unwrap();
859
+ let target_name_id = self.index_constant_alias_target(&node.value())?;
860
+ self.add_constant_alias_definition(value, target_name_id, false);
861
+ Some(target_name_id)
862
+ }
863
+ ruby_prism::Node::ConstantOrWriteNode { .. } => {
864
+ let node = value.as_constant_or_write_node().unwrap();
865
+ let target_name_id = self.index_constant_alias_target(&node.value())?;
866
+ self.add_constant_alias_definition(value, target_name_id, false);
867
+ Some(target_name_id)
868
+ }
869
+ ruby_prism::Node::ConstantPathWriteNode { .. } => {
870
+ let node = value.as_constant_path_write_node().unwrap();
871
+ let target_name_id = self.index_constant_alias_target(&node.value())?;
872
+ self.add_constant_alias_definition(&node.target().as_node(), target_name_id, false);
873
+ Some(target_name_id)
874
+ }
875
+ ruby_prism::Node::ConstantPathOrWriteNode { .. } => {
876
+ let node = value.as_constant_path_or_write_node().unwrap();
877
+ let target_name_id = self.index_constant_alias_target(&node.value())?;
878
+ self.add_constant_alias_definition(&node.target().as_node(), target_name_id, true);
879
+ Some(target_name_id)
880
+ }
881
+ _ => None,
882
+ }
883
+ }
884
+
885
+ fn add_constant_alias_definition(
886
+ &mut self,
887
+ name_node: &ruby_prism::Node,
888
+ target_name_id: NameId,
889
+ also_add_reference: bool,
890
+ ) -> Option<DefinitionId> {
891
+ let name_id = self.index_constant_reference(name_node, also_add_reference)?;
892
+
893
+ // Get the location for just the constant name (not including the namespace or value).
894
+ let location = match name_node {
895
+ ruby_prism::Node::ConstantWriteNode { .. } => name_node.as_constant_write_node().unwrap().name_loc(),
896
+ ruby_prism::Node::ConstantOrWriteNode { .. } => name_node.as_constant_or_write_node().unwrap().name_loc(),
897
+ ruby_prism::Node::ConstantPathNode { .. } => name_node.as_constant_path_node().unwrap().name_loc(),
898
+ _ => name_node.location(),
899
+ };
900
+
901
+ let offset = Offset::from_prism_location(&location);
902
+ let (comments, flags) = self.find_comments_for(offset.start());
903
+ let lexical_nesting_id = self.parent_lexical_scope_id();
904
+
905
+ let alias_constant = ConstantDefinition::new(name_id, self.uri_id, offset, comments, flags, lexical_nesting_id);
906
+ let definition =
907
+ Definition::ConstantAlias(Box::new(ConstantAliasDefinition::new(target_name_id, alias_constant)));
908
+ let definition_id = self.local_graph.add_definition(definition);
909
+
910
+ self.add_member_to_current_owner(definition_id);
911
+
912
+ Some(definition_id)
913
+ }
914
+
915
+ /// Adds a member to the current owner (class, module, or singleton class).
916
+ ///
917
+ /// Iterates through the definitions stack in reverse to find the first class/module/singleton
918
+ /// class, skipping methods, and adds the member to it.
919
+ fn add_member_to_current_owner(&mut self, member_id: DefinitionId) {
920
+ let Some(owner_id) = self.current_nesting_definition_id() else {
921
+ return;
922
+ };
923
+
924
+ let owner = self
925
+ .local_graph
926
+ .get_definition_mut(owner_id)
927
+ .expect("owner definition should exist");
928
+
929
+ match owner {
930
+ Definition::Class(class) => class.add_member(member_id),
931
+ Definition::SingletonClass(singleton_class) => singleton_class.add_member(member_id),
932
+ Definition::Module(module) => module.add_member(member_id),
933
+ _ => unreachable!("find above only matches anonymous/class/module/singleton"),
934
+ }
935
+ }
936
+
937
+ /// Adds a member to the current lexical scope
938
+ ///
939
+ /// Iterates through the definitions stack in reverse to find the first class/module/singleton class, skipping
940
+ /// methods, and adds the member to it. Ignores owner nestings such as Class.new
941
+ fn add_member_to_current_lexical_scope(&mut self, member_id: DefinitionId) {
942
+ let Some(owner_id) = self.parent_lexical_scope_id() else {
943
+ return;
944
+ };
945
+
946
+ let owner = self
947
+ .local_graph
948
+ .get_definition_mut(owner_id)
949
+ .expect("owner definition should exist");
950
+
951
+ match owner {
952
+ Definition::Class(class) => class.add_member(member_id),
953
+ Definition::SingletonClass(singleton_class) => singleton_class.add_member(member_id),
954
+ Definition::Module(module) => module.add_member(member_id),
955
+ _ => unreachable!("find above only matches class/module/singleton"),
956
+ }
957
+ }
958
+
959
+ fn handle_mixin(&mut self, node: &ruby_prism::CallNode, mixin_type: MixinType) {
960
+ let Some(arguments) = node.arguments() else {
961
+ return;
962
+ };
963
+
964
+ let parent_nesting_id = self.current_nesting_definition_id();
965
+
966
+ // Collect all arguments as constant references. Ignore anything that isn't a constant
967
+ let mixin_arguments = arguments
968
+ .arguments()
969
+ .iter()
970
+ .filter_map(|arg| {
971
+ if arg.as_self_node().is_some() {
972
+ if parent_nesting_id.is_none() {
973
+ self.local_graph.add_diagnostic(
974
+ Rule::TopLevelMixinSelf,
975
+ Offset::from_prism_location(&arg.location()),
976
+ "Top level mixin self".to_string(),
977
+ );
978
+
979
+ return None;
980
+ }
981
+
982
+ Some((
983
+ self.current_lexical_scope_name_id().unwrap(),
984
+ Offset::from_prism_location(&arg.location()),
985
+ ))
986
+ } else if let Some(name_id) = self.index_constant_reference(&arg, false) {
987
+ Some((name_id, Offset::from_prism_location(&arg.location())))
988
+ } else {
989
+ self.local_graph.add_diagnostic(
990
+ Rule::DynamicAncestor,
991
+ Offset::from_prism_location(&arg.location()),
992
+ "Dynamic mixin argument".to_string(),
993
+ );
994
+
995
+ None
996
+ }
997
+ })
998
+ .collect::<Vec<(NameId, Offset)>>();
999
+
1000
+ if mixin_arguments.is_empty() {
1001
+ return;
1002
+ }
1003
+
1004
+ let Some(lexical_nesting_id) = parent_nesting_id else {
1005
+ return;
1006
+ };
1007
+
1008
+ // Mixin operations with multiple arguments are inserted in reverse, so that they are processed in the expected
1009
+ // order by resolution
1010
+ for (id, offset) in mixin_arguments.into_iter().rev() {
1011
+ let constant_ref_id =
1012
+ self.local_graph
1013
+ .add_constant_reference(ConstantReference::new(id, self.uri_id, offset));
1014
+
1015
+ let mixin = match mixin_type {
1016
+ MixinType::Include => Mixin::Include(IncludeDefinition::new(constant_ref_id)),
1017
+ MixinType::Prepend => Mixin::Prepend(PrependDefinition::new(constant_ref_id)),
1018
+ MixinType::Extend => Mixin::Extend(ExtendDefinition::new(constant_ref_id)),
1019
+ };
1020
+
1021
+ match self.local_graph.get_definition_mut(lexical_nesting_id).unwrap() {
1022
+ Definition::Class(class_def) => class_def.add_mixin(mixin),
1023
+ Definition::Module(module_def) => module_def.add_mixin(mixin),
1024
+ Definition::SingletonClass(singleton_class_def) => singleton_class_def.add_mixin(mixin),
1025
+ _ => {}
1026
+ }
1027
+ }
1028
+ }
1029
+
1030
+ /// Indexes a method reference for a call node, creating constant references for the receiver when applicable.
1031
+ fn index_method_reference_for_call(&mut self, node: &ruby_prism::CallNode) {
1032
+ let method_receiver = self.method_receiver(node.receiver().as_ref(), node.location());
1033
+
1034
+ if method_receiver.is_none()
1035
+ && let Some(receiver) = node.receiver()
1036
+ {
1037
+ self.visit(&receiver);
1038
+ }
1039
+
1040
+ let message = String::from_utf8_lossy(node.name().as_slice()).to_string();
1041
+ self.index_method_reference(message, &node.message_loc().unwrap(), method_receiver);
1042
+ }
1043
+
1044
+ /// Visits every part of a call node, except for the message itself. Convenient for when we're only interested in
1045
+ /// continuing the traversal
1046
+ fn visit_call_node_parts(&mut self, node: &ruby_prism::CallNode) {
1047
+ if let Some(receiver) = node.receiver() {
1048
+ self.visit(&receiver);
1049
+ }
1050
+
1051
+ if let Some(arguments) = node.arguments() {
1052
+ self.visit_arguments_node(&arguments);
1053
+ }
1054
+
1055
+ if let Some(block) = node.block() {
1056
+ self.visit(&block);
1057
+ }
1058
+ }
1059
+
1060
+ #[must_use]
1061
+ fn parent_lexical_scope_id(&self) -> Option<DefinitionId> {
1062
+ self.nesting_stack.iter().rev().find_map(|nesting| match nesting {
1063
+ Nesting::LexicalScope(id) => Some(*id),
1064
+ Nesting::Owner(_) | Nesting::Method(_) => None,
1065
+ })
1066
+ }
1067
+
1068
+ #[must_use]
1069
+ fn parent_nesting_id(&self) -> Option<DefinitionId> {
1070
+ self.nesting_stack.last().map(Nesting::id)
1071
+ }
1072
+
1073
+ #[must_use]
1074
+ fn current_visibility(&self) -> &VisibilityModifier {
1075
+ self.visibility_stack
1076
+ .last()
1077
+ .expect("visibility stack should not be empty")
1078
+ }
1079
+
1080
+ fn method_receiver(
1081
+ &mut self,
1082
+ receiver: Option<&ruby_prism::Node>,
1083
+ fallback_location: ruby_prism::Location,
1084
+ ) -> Option<NameId> {
1085
+ let mut is_singleton_name = false;
1086
+
1087
+ let name_id = match receiver {
1088
+ Some(ruby_prism::Node::SelfNode { .. }) | None => {
1089
+ // Implicit or explicit self receiver
1090
+
1091
+ match self.nesting_stack.last() {
1092
+ Some(Nesting::LexicalScope(id) | Nesting::Owner(id)) => {
1093
+ let definition = self
1094
+ .local_graph
1095
+ .definitions()
1096
+ .get(id)
1097
+ .expect("Nesting definition should exist");
1098
+
1099
+ match definition {
1100
+ Definition::Class(class_def) => {
1101
+ is_singleton_name = true;
1102
+ Some(*class_def.name_id())
1103
+ }
1104
+ Definition::Module(module_def) => {
1105
+ is_singleton_name = true;
1106
+ Some(*module_def.name_id())
1107
+ }
1108
+ Definition::SingletonClass(singleton_class_def) => {
1109
+ is_singleton_name = true;
1110
+ Some(*singleton_class_def.name_id())
1111
+ }
1112
+ Definition::Method(_) => None,
1113
+ _ => panic!("current nesting is not a class/module/singleton class: {definition:?}"),
1114
+ }
1115
+ }
1116
+ Some(Nesting::Method(id)) => {
1117
+ // If we're inside a method definition, we need to check what its receiver is as that changes the type of `self`
1118
+ let Some(Definition::Method(definition)) = self.local_graph.definitions().get(id) else {
1119
+ unreachable!("method definition for nesting should exist")
1120
+ };
1121
+
1122
+ if let Some(receiver) = definition.receiver() {
1123
+ is_singleton_name = true;
1124
+ match receiver {
1125
+ Receiver::SelfReceiver(def_id) => self
1126
+ .local_graph
1127
+ .definitions()
1128
+ .get(def_id)
1129
+ .and_then(Definition::name_id)
1130
+ .copied(),
1131
+ Receiver::ConstantReceiver(name_id) => Some(*name_id),
1132
+ }
1133
+ } else {
1134
+ self.current_owner_name_id()
1135
+ }
1136
+ }
1137
+ None => {
1138
+ let str_id = self.local_graph.intern_string("Object".into());
1139
+ Some(self.local_graph.add_name(Name::new(str_id, ParentScope::None, None)))
1140
+ }
1141
+ }
1142
+ }
1143
+ Some(ruby_prism::Node::CallNode { .. }) => {
1144
+ // Check if the receiver is `singleton_class`
1145
+ let call_node = receiver.unwrap().as_call_node().unwrap();
1146
+
1147
+ if call_node.name().as_slice() == b"singleton_class" {
1148
+ is_singleton_name = true;
1149
+ self.method_receiver(call_node.receiver().as_ref(), call_node.location())
1150
+ } else {
1151
+ None
1152
+ }
1153
+ }
1154
+ Some(node) => {
1155
+ is_singleton_name = true;
1156
+ self.index_constant_reference(node, true)
1157
+ }
1158
+ }?;
1159
+
1160
+ if !is_singleton_name {
1161
+ return Some(name_id);
1162
+ }
1163
+
1164
+ let singleton_class_name = {
1165
+ let name = self
1166
+ .local_graph
1167
+ .names()
1168
+ .get(&name_id)
1169
+ .expect("Indexed constant name should exist");
1170
+
1171
+ let target_str = self
1172
+ .local_graph
1173
+ .strings()
1174
+ .get(name.str())
1175
+ .expect("Indexed constant string should exist");
1176
+
1177
+ format!("<{}>", target_str.as_str())
1178
+ };
1179
+
1180
+ let string_id = self.local_graph.intern_string(singleton_class_name);
1181
+ let new_name_id = self
1182
+ .local_graph
1183
+ .add_name(Name::new(string_id, ParentScope::Attached(name_id), None));
1184
+
1185
+ let location = receiver.map_or(fallback_location, ruby_prism::Node::location);
1186
+ let offset = Offset::from_prism_location(&location);
1187
+ self.local_graph
1188
+ .add_constant_reference(ConstantReference::new(new_name_id, self.uri_id, offset));
1189
+ Some(new_name_id)
1190
+ }
1191
+
1192
+ fn handle_constant_visibility(&mut self, node: &ruby_prism::CallNode, visibility: Visibility) {
1193
+ let receiver = node.receiver();
1194
+
1195
+ let receiver_name_id = match receiver {
1196
+ Some(ruby_prism::Node::ConstantPathNode { .. } | ruby_prism::Node::ConstantReadNode { .. }) => {
1197
+ self.index_constant_reference(&receiver.unwrap(), true)
1198
+ }
1199
+ Some(ruby_prism::Node::SelfNode { .. }) | None => match self.nesting_stack.last() {
1200
+ Some(Nesting::Method(_)) => {
1201
+ // Dynamic private constant (called from a method), we ignore it but don't report an error since it's valid Ruby
1202
+ // if being called from a singleton method.
1203
+
1204
+ return;
1205
+ }
1206
+ None => {
1207
+ self.local_graph.add_diagnostic(
1208
+ Rule::InvalidPrivateConstant,
1209
+ Offset::from_prism_location(&node.location()),
1210
+ "Private constant called at top level".to_string(),
1211
+ );
1212
+
1213
+ return;
1214
+ }
1215
+ _ => None,
1216
+ },
1217
+ _ => {
1218
+ self.local_graph.add_diagnostic(
1219
+ Rule::InvalidPrivateConstant,
1220
+ Offset::from_prism_location(&node.location()),
1221
+ "Dynamic receiver for private constant".to_string(),
1222
+ );
1223
+
1224
+ return;
1225
+ }
1226
+ };
1227
+
1228
+ let Some(arguments) = node.arguments() else {
1229
+ return;
1230
+ };
1231
+
1232
+ for argument in &arguments.arguments() {
1233
+ let (name, location) = match argument {
1234
+ ruby_prism::Node::SymbolNode { .. } => {
1235
+ let symbol = argument.as_symbol_node().unwrap();
1236
+ if let Some(value_loc) = symbol.value_loc() {
1237
+ (Self::location_to_string(&value_loc), value_loc)
1238
+ } else {
1239
+ continue;
1240
+ }
1241
+ }
1242
+ ruby_prism::Node::StringNode { .. } => {
1243
+ let string = argument.as_string_node().unwrap();
1244
+ let name = String::from_utf8_lossy(string.unescaped()).to_string();
1245
+ (name, argument.location())
1246
+ }
1247
+ _ => {
1248
+ self.local_graph.add_diagnostic(
1249
+ Rule::InvalidPrivateConstant,
1250
+ Offset::from_prism_location(&argument.location()),
1251
+ "Private constant called with non-symbol argument".to_string(),
1252
+ );
1253
+
1254
+ return;
1255
+ }
1256
+ };
1257
+
1258
+ let str_id = self.local_graph.intern_string(name);
1259
+ let offset = Offset::from_prism_location(&location);
1260
+ let definition = Definition::ConstantVisibility(Box::new(ConstantVisibilityDefinition::new(
1261
+ self.local_graph.add_name(Name::new(
1262
+ str_id,
1263
+ receiver_name_id.map_or(ParentScope::None, ParentScope::Some),
1264
+ self.current_lexical_scope_name_id(),
1265
+ )),
1266
+ visibility,
1267
+ self.uri_id,
1268
+ offset,
1269
+ Box::default(),
1270
+ DefinitionFlags::empty(),
1271
+ self.current_nesting_definition_id(),
1272
+ )));
1273
+
1274
+ let definition_id = self.local_graph.add_definition(definition);
1275
+
1276
+ self.add_member_to_current_owner(definition_id);
1277
+ }
1278
+ }
1279
+
1280
+ fn is_attr_call(arg: &ruby_prism::Node) -> bool {
1281
+ arg.as_call_node().is_some_and(|call| {
1282
+ let receiver = call.receiver();
1283
+ let bare_or_self = receiver.is_none() || receiver.as_ref().is_some_and(|r| r.as_self_node().is_some());
1284
+ bare_or_self
1285
+ && matches!(
1286
+ call.name().as_slice(),
1287
+ b"attr" | b"attr_reader" | b"attr_writer" | b"attr_accessor"
1288
+ )
1289
+ })
1290
+ }
1291
+
1292
+ /// Classifies each visibility argument and applies visibility left-to-right:
1293
+ /// - `DefNode`: inline visibility (always valid)
1294
+ /// - Sole attr_* call: inline visibility (multi-arg attr_* is unsupported — returns array)
1295
+ /// - `SymbolNode`/`StringNode`: retroactive `MethodVisibilityDefinition`
1296
+ /// - Anything else: per-arg diagnostic
1297
+ fn handle_visibility_arguments(
1298
+ &mut self,
1299
+ arguments: &ruby_prism::ArgumentsNode,
1300
+ visibility: Visibility,
1301
+ call_offset: &Offset,
1302
+ call_name: &str,
1303
+ ) {
1304
+ let args = arguments.arguments();
1305
+ let arg_count = args.len();
1306
+
1307
+ for arg in &args {
1308
+ if matches!(arg, ruby_prism::Node::DefNode { .. }) || (arg_count == 1 && Self::is_attr_call(&arg)) {
1309
+ self.visibility_stack
1310
+ .push(VisibilityModifier::new(visibility, true, call_offset.clone()));
1311
+ self.visit(&arg);
1312
+ self.visibility_stack.pop();
1313
+ } else if matches!(
1314
+ arg,
1315
+ ruby_prism::Node::SymbolNode { .. } | ruby_prism::Node::StringNode { .. }
1316
+ ) {
1317
+ self.create_method_visibility_definition(&arg, visibility);
1318
+ } else {
1319
+ // Unsupported arg — diagnostic + visit for side effects.
1320
+ let arg_offset = Offset::from_prism_location(&arg.location());
1321
+ let message = if Self::is_attr_call(&arg) {
1322
+ format!("`{call_name}` with `attr_*` is only supported as a single argument")
1323
+ } else {
1324
+ format!("`{call_name}` called with a non-literal argument")
1325
+ };
1326
+ self.local_graph
1327
+ .add_diagnostic(Rule::InvalidMethodVisibility, arg_offset, message);
1328
+ self.visit(&arg);
1329
+ }
1330
+ }
1331
+ }
1332
+
1333
+ fn create_method_visibility_definition(&mut self, arg: &ruby_prism::Node, visibility: Visibility) {
1334
+ let (name, location) = match arg {
1335
+ ruby_prism::Node::SymbolNode { .. } => {
1336
+ let symbol = arg.as_symbol_node().unwrap();
1337
+ if let Some(value_loc) = symbol.value_loc() {
1338
+ (Self::location_to_string(&value_loc), value_loc)
1339
+ } else {
1340
+ return;
1341
+ }
1342
+ }
1343
+ ruby_prism::Node::StringNode { .. } => {
1344
+ let string = arg.as_string_node().unwrap();
1345
+ let name = String::from_utf8_lossy(string.unescaped()).to_string();
1346
+ (name, arg.location())
1347
+ }
1348
+ _ => return,
1349
+ };
1350
+
1351
+ let str_id = self.local_graph.intern_string(format!("{name}()"));
1352
+ let arg_offset = Offset::from_prism_location(&location);
1353
+ let definition = Definition::MethodVisibility(Box::new(MethodVisibilityDefinition::new(
1354
+ str_id,
1355
+ visibility,
1356
+ self.uri_id,
1357
+ arg_offset,
1358
+ Box::default(),
1359
+ DefinitionFlags::empty(),
1360
+ self.current_nesting_definition_id(),
1361
+ )));
1362
+
1363
+ let definition_id = self.local_graph.add_definition(definition);
1364
+ self.add_member_to_current_owner(definition_id);
1365
+ }
1366
+ }
1367
+
1368
+ struct CommentGroup {
1369
+ end_offset: usize,
1370
+ comments: Vec<Comment>,
1371
+ deprecated: bool,
1372
+ }
1373
+
1374
+ impl CommentGroup {
1375
+ #[must_use]
1376
+ pub fn new() -> Self {
1377
+ Self {
1378
+ end_offset: 0,
1379
+ comments: Vec::new(),
1380
+ deprecated: false,
1381
+ }
1382
+ }
1383
+
1384
+ // Accepts the next line if it is continuous
1385
+ fn accepts(&self, next: &ruby_prism::Comment, source: &str) -> bool {
1386
+ let current_end_offset = self.end_offset;
1387
+ let next_line_start_offset = next.location().start_offset();
1388
+
1389
+ let between = &source.as_bytes()[current_end_offset..next_line_start_offset];
1390
+ if !between.iter().all(|&b| b.is_ascii_whitespace()) {
1391
+ return false;
1392
+ }
1393
+
1394
+ // If there is at most one newline between the two texts,
1395
+ // that means two texts are continuous
1396
+ bytecount::count(between, b'\n') <= 1
1397
+ }
1398
+
1399
+ // For the magic comments, what we want to do is the following:
1400
+ // 1. still move the group end offset to the end of the magic comment
1401
+ // 2. not add the comment to the comments array
1402
+ fn add_comment(&mut self, comment: &ruby_prism::Comment) {
1403
+ self.end_offset = comment.location().end_offset();
1404
+ let text = String::from_utf8_lossy(comment.location().as_slice()).to_string();
1405
+
1406
+ if text.lines().any(|line| line.starts_with("# @deprecated")) {
1407
+ self.deprecated = true;
1408
+ }
1409
+
1410
+ self.comments.push(Comment::new(
1411
+ Offset::from_prism_location(&comment.location()),
1412
+ text.trim().to_string(),
1413
+ ));
1414
+ }
1415
+
1416
+ fn comments(&self) -> Box<[Comment]> {
1417
+ self.comments.clone().into_boxed_slice()
1418
+ }
1419
+
1420
+ fn flags(&self) -> DefinitionFlags {
1421
+ if self.deprecated {
1422
+ DefinitionFlags::DEPRECATED
1423
+ } else {
1424
+ DefinitionFlags::empty()
1425
+ }
1426
+ }
1427
+ }
1428
+
1429
+ impl Visit<'_> for RubyIndexer<'_> {
1430
+ fn visit_class_node(&mut self, node: &ruby_prism::ClassNode<'_>) {
1431
+ self.handle_class_definition(
1432
+ &node.location(),
1433
+ Some(&node.constant_path()),
1434
+ node.body(),
1435
+ node.superclass(),
1436
+ Nesting::LexicalScope,
1437
+ );
1438
+ }
1439
+
1440
+ fn visit_module_node(&mut self, node: &ruby_prism::ModuleNode) {
1441
+ self.handle_module_definition(
1442
+ &node.location(),
1443
+ Some(&node.constant_path()),
1444
+ node.body(),
1445
+ Nesting::LexicalScope,
1446
+ );
1447
+ }
1448
+
1449
+ fn visit_singleton_class_node(&mut self, node: &ruby_prism::SingletonClassNode) {
1450
+ let expression = node.expression();
1451
+
1452
+ // Determine the attached_target for the singleton class and the name_offset
1453
+ let (attached_target, name_offset) = if expression.as_self_node().is_some() {
1454
+ // `class << self` - resolve self to current class/module's NameId
1455
+ // name_offset points to "self"
1456
+ (
1457
+ self.current_lexical_scope_name_id(),
1458
+ Offset::from_prism_location(&expression.location()),
1459
+ )
1460
+ } else if matches!(
1461
+ expression,
1462
+ ruby_prism::Node::ConstantPathNode { .. } | ruby_prism::Node::ConstantReadNode { .. }
1463
+ ) {
1464
+ // `class << Foo` or `class << Foo::Bar` - use the constant's NameId
1465
+ // name_offset points to the expression (the constant reference)
1466
+ (
1467
+ self.index_constant_reference(&expression, true),
1468
+ Offset::from_prism_location(&expression.location()),
1469
+ )
1470
+ } else {
1471
+ // Dynamic expression (e.g., `class << some_var`) - skip creating definition
1472
+ self.visit(&expression);
1473
+ self.local_graph.add_diagnostic(
1474
+ Rule::DynamicSingletonDefinition,
1475
+ Offset::from_prism_location(&node.location()),
1476
+ "Dynamic singleton class definition".to_string(),
1477
+ );
1478
+ return;
1479
+ };
1480
+
1481
+ let Some(attached_target) = attached_target else {
1482
+ self.local_graph.add_diagnostic(
1483
+ Rule::DynamicSingletonDefinition,
1484
+ Offset::from_prism_location(&node.location()),
1485
+ "Dynamic singleton class definition".to_string(),
1486
+ );
1487
+
1488
+ return;
1489
+ };
1490
+
1491
+ let offset = Offset::from_prism_location(&node.location());
1492
+ let (comments, flags) = self.find_comments_for(offset.start());
1493
+ let lexical_nesting_id = self.parent_lexical_scope_id();
1494
+
1495
+ let singleton_class_name = {
1496
+ let name = self
1497
+ .local_graph
1498
+ .names()
1499
+ .get(&attached_target)
1500
+ .expect("Attached target name should exist");
1501
+ let target_str = self
1502
+ .local_graph
1503
+ .strings()
1504
+ .get(name.str())
1505
+ .expect("Attached target string should exist");
1506
+ format!("<{}>", target_str.as_str())
1507
+ };
1508
+
1509
+ let string_id = self.local_graph.intern_string(singleton_class_name);
1510
+ let name_id = self
1511
+ .local_graph
1512
+ .add_name(Name::new(string_id, ParentScope::Attached(attached_target), None));
1513
+
1514
+ let definition = Definition::SingletonClass(Box::new(SingletonClassDefinition::new(
1515
+ name_id,
1516
+ self.uri_id,
1517
+ offset.clone(),
1518
+ name_offset,
1519
+ comments,
1520
+ flags,
1521
+ lexical_nesting_id,
1522
+ )));
1523
+
1524
+ let definition_id = self.local_graph.add_definition(definition);
1525
+
1526
+ self.add_member_to_current_owner(definition_id);
1527
+
1528
+ if let Some(body) = node.body() {
1529
+ self.nesting_stack.push(Nesting::LexicalScope(definition_id));
1530
+ self.visibility_stack
1531
+ .push(VisibilityModifier::new(Visibility::Public, false, offset));
1532
+ self.visit(&body);
1533
+ self.visibility_stack.pop();
1534
+ self.nesting_stack.pop();
1535
+ }
1536
+ }
1537
+
1538
+ fn visit_constant_and_write_node(&mut self, node: &ruby_prism::ConstantAndWriteNode) {
1539
+ self.index_constant_reference(&node.as_node(), true);
1540
+ self.visit(&node.value());
1541
+ }
1542
+
1543
+ fn visit_constant_operator_write_node(&mut self, node: &ruby_prism::ConstantOperatorWriteNode) {
1544
+ self.index_constant_reference(&node.as_node(), true);
1545
+ self.visit(&node.value());
1546
+ }
1547
+
1548
+ fn visit_constant_or_write_node(&mut self, node: &ruby_prism::ConstantOrWriteNode) {
1549
+ if let Some(target_name_id) = self.index_constant_alias_target(&node.value()) {
1550
+ self.add_constant_alias_definition(&node.as_node(), target_name_id, true);
1551
+ } else {
1552
+ self.add_constant_definition(&node.as_node(), true, Self::is_promotable_value(&node.value()));
1553
+ self.visit(&node.value());
1554
+ }
1555
+ }
1556
+
1557
+ fn visit_constant_write_node(&mut self, node: &ruby_prism::ConstantWriteNode) {
1558
+ let value = node.value();
1559
+ if self.handle_dynamic_class_or_module(&node.as_node(), &value) {
1560
+ return;
1561
+ }
1562
+
1563
+ if let Some(target_name_id) = self.index_constant_alias_target(&value) {
1564
+ self.add_constant_alias_definition(&node.as_node(), target_name_id, false);
1565
+ } else {
1566
+ self.add_constant_definition(&node.as_node(), false, Self::is_promotable_value(&value));
1567
+ self.visit(&value);
1568
+ }
1569
+ }
1570
+
1571
+ fn visit_constant_path_and_write_node(&mut self, node: &ruby_prism::ConstantPathAndWriteNode) {
1572
+ self.visit_constant_path_node(&node.target());
1573
+ self.visit(&node.value());
1574
+ }
1575
+
1576
+ fn visit_constant_path_operator_write_node(&mut self, node: &ruby_prism::ConstantPathOperatorWriteNode) {
1577
+ self.visit_constant_path_node(&node.target());
1578
+ self.visit(&node.value());
1579
+ }
1580
+
1581
+ fn visit_constant_path_or_write_node(&mut self, node: &ruby_prism::ConstantPathOrWriteNode) {
1582
+ if let Some(target_name_id) = self.index_constant_alias_target(&node.value()) {
1583
+ self.add_constant_alias_definition(&node.target().as_node(), target_name_id, true);
1584
+ } else {
1585
+ self.add_constant_definition(&node.target().as_node(), true, Self::is_promotable_value(&node.value()));
1586
+ self.visit(&node.value());
1587
+ }
1588
+ }
1589
+
1590
+ fn visit_constant_path_write_node(&mut self, node: &ruby_prism::ConstantPathWriteNode) {
1591
+ let value = node.value();
1592
+ if self.handle_dynamic_class_or_module(&node.as_node(), &value) {
1593
+ return;
1594
+ }
1595
+
1596
+ if let Some(target_name_id) = self.index_constant_alias_target(&value) {
1597
+ self.add_constant_alias_definition(&node.target().as_node(), target_name_id, false);
1598
+ } else {
1599
+ self.add_constant_definition(&node.target().as_node(), false, Self::is_promotable_value(&value));
1600
+ self.visit(&value);
1601
+ }
1602
+ }
1603
+
1604
+ fn visit_constant_read_node(&mut self, node: &ruby_prism::ConstantReadNode<'_>) {
1605
+ self.index_constant_reference(&node.as_node(), true);
1606
+ }
1607
+
1608
+ fn visit_constant_path_node(&mut self, node: &ruby_prism::ConstantPathNode<'_>) {
1609
+ self.index_constant_reference(&node.as_node(), true);
1610
+ }
1611
+
1612
+ fn visit_multi_write_node(&mut self, node: &ruby_prism::MultiWriteNode) {
1613
+ for left in &node.lefts() {
1614
+ match left {
1615
+ ruby_prism::Node::ConstantTargetNode { .. } | ruby_prism::Node::ConstantPathTargetNode { .. } => {
1616
+ // Individual values aren't available in multi-write, so we default to
1617
+ // promotable because multi-assignment often comes from meta-programming
1618
+ // (e.g., `A, B = create_classes`).
1619
+ self.add_constant_definition(&left, false, true);
1620
+ }
1621
+ ruby_prism::Node::GlobalVariableTargetNode { .. } => {
1622
+ self.add_definition_from_location(
1623
+ &left.location(),
1624
+ |str_id, offset, comments, flags, lexical_nesting_id, uri_id| {
1625
+ Definition::GlobalVariable(Box::new(GlobalVariableDefinition::new(
1626
+ str_id,
1627
+ uri_id,
1628
+ offset,
1629
+ comments,
1630
+ flags,
1631
+ lexical_nesting_id,
1632
+ )))
1633
+ },
1634
+ );
1635
+ }
1636
+ ruby_prism::Node::InstanceVariableTargetNode { .. } => {
1637
+ self.add_instance_variable_definition(&left.location());
1638
+ }
1639
+ ruby_prism::Node::ClassVariableTargetNode { .. } => {
1640
+ self.add_class_variable_definition(&left.location());
1641
+ }
1642
+ ruby_prism::Node::CallTargetNode { .. } => {
1643
+ let call_target_node = left.as_call_target_node().unwrap();
1644
+ let method_receiver = self.method_receiver(Some(&call_target_node.receiver()), left.location());
1645
+
1646
+ if method_receiver.is_none() {
1647
+ self.visit(&call_target_node.receiver());
1648
+ }
1649
+
1650
+ let name = String::from_utf8_lossy(call_target_node.name().as_slice()).to_string();
1651
+ self.index_method_reference(name, &call_target_node.location(), method_receiver);
1652
+ }
1653
+ _ => {}
1654
+ }
1655
+ }
1656
+
1657
+ self.visit(&node.value());
1658
+ }
1659
+
1660
+ fn visit_def_node(&mut self, node: &ruby_prism::DefNode) {
1661
+ let name = Self::location_to_string(&node.name_loc());
1662
+ let str_id = self.local_graph.intern_string(format!("{name}()"));
1663
+ let offset = Offset::from_prism_location(&node.location());
1664
+ let parent_nesting_id = self.current_nesting_definition_id();
1665
+ let parameters = self.collect_parameters(node);
1666
+ let is_singleton = node.receiver().is_some();
1667
+
1668
+ let current_visibility = self.current_visibility();
1669
+ let (visibility, offset_for_comments) = if is_singleton {
1670
+ (Visibility::Public, offset.clone())
1671
+ } else if current_visibility.is_inline() {
1672
+ // If the visibility is inline, we use its offset for the comments
1673
+ (*current_visibility.visibility(), current_visibility.offset().clone())
1674
+ } else {
1675
+ (*current_visibility.visibility(), offset.clone())
1676
+ };
1677
+
1678
+ let comment_offset = self
1679
+ .take_decorator_offset(offset_for_comments.start())
1680
+ .unwrap_or_else(|| offset_for_comments.start());
1681
+ let (comments, flags) = self.find_comments_for(comment_offset);
1682
+
1683
+ let receiver = if let Some(recv_node) = node.receiver() {
1684
+ match recv_node {
1685
+ // def self.foo - receiver is the enclosing definition's DefinitionId
1686
+ ruby_prism::Node::SelfNode { .. } => self.current_nesting_definition_id().map(Receiver::SelfReceiver),
1687
+ // def Foo.bar or def Foo::Bar.baz - receiver is the constant's NameId
1688
+ ruby_prism::Node::ConstantPathNode { .. } | ruby_prism::Node::ConstantReadNode { .. } => self
1689
+ .index_constant_reference(&recv_node, true)
1690
+ .map(Receiver::ConstantReceiver),
1691
+ // Dynamic receiver (def foo.bar) - visit and then skip
1692
+ // We still want to visit because it could be a variable reference
1693
+ _ => {
1694
+ self.local_graph.add_diagnostic(
1695
+ Rule::DynamicSingletonDefinition,
1696
+ Offset::from_prism_location(&node.location()),
1697
+ "Dynamic receiver for singleton method definition".to_string(),
1698
+ );
1699
+
1700
+ self.visit(&recv_node);
1701
+ return;
1702
+ }
1703
+ }
1704
+ } else {
1705
+ None
1706
+ };
1707
+
1708
+ let definition_id = if receiver.is_none() && visibility == Visibility::ModuleFunction {
1709
+ // module_function creates two method definitions:
1710
+ // 1. Public singleton method (class/module method)
1711
+ let method = Definition::Method(Box::new(MethodDefinition::new(
1712
+ str_id,
1713
+ self.uri_id,
1714
+ offset.clone(),
1715
+ comments.clone(),
1716
+ flags.clone(),
1717
+ parent_nesting_id,
1718
+ Signatures::Simple(parameters.clone().into_boxed_slice()),
1719
+ Visibility::Public,
1720
+ self.current_nesting_definition_id().map(Receiver::SelfReceiver),
1721
+ )));
1722
+ let definition_id = self.local_graph.add_definition(method);
1723
+
1724
+ self.add_member_to_current_owner(definition_id);
1725
+
1726
+ // 2. Private instance method
1727
+ let method = Definition::Method(Box::new(MethodDefinition::new(
1728
+ str_id,
1729
+ self.uri_id,
1730
+ offset,
1731
+ comments,
1732
+ flags,
1733
+ parent_nesting_id,
1734
+ Signatures::Simple(parameters.into_boxed_slice()),
1735
+ Visibility::Private,
1736
+ receiver,
1737
+ )));
1738
+ let definition_id = self.local_graph.add_definition(method);
1739
+
1740
+ self.add_member_to_current_owner(definition_id);
1741
+
1742
+ definition_id
1743
+ } else {
1744
+ let method = Definition::Method(Box::new(MethodDefinition::new(
1745
+ str_id,
1746
+ self.uri_id,
1747
+ offset,
1748
+ comments,
1749
+ flags,
1750
+ parent_nesting_id,
1751
+ Signatures::Simple(parameters.into_boxed_slice()),
1752
+ visibility,
1753
+ receiver,
1754
+ )));
1755
+ let definition_id = self.local_graph.add_definition(method);
1756
+
1757
+ self.add_member_to_current_owner(definition_id);
1758
+
1759
+ definition_id
1760
+ };
1761
+
1762
+ if let Some(body) = node.body() {
1763
+ self.nesting_stack.push(Nesting::Method(definition_id));
1764
+ self.visit(&body);
1765
+ self.nesting_stack.pop();
1766
+ }
1767
+ }
1768
+
1769
+ #[allow(clippy::too_many_lines)]
1770
+ fn visit_call_node(&mut self, node: &ruby_prism::CallNode) {
1771
+ enum AttrKind {
1772
+ Accessor,
1773
+ Reader,
1774
+ Writer,
1775
+ }
1776
+
1777
+ let mut index_attr = |kind: AttrKind, call: &ruby_prism::CallNode| {
1778
+ let receiver = call.receiver();
1779
+ if receiver.is_some() && receiver.unwrap().as_self_node().is_none() {
1780
+ return;
1781
+ }
1782
+
1783
+ let call_offset = Offset::from_prism_location(&call.location());
1784
+
1785
+ let current_visibility = self.current_visibility();
1786
+ let (visibility, offset_for_comments) = if current_visibility.is_inline() {
1787
+ (*current_visibility.visibility(), current_visibility.offset().clone())
1788
+ } else {
1789
+ (*current_visibility.visibility(), call_offset.clone())
1790
+ };
1791
+
1792
+ let comment_offset = self
1793
+ .take_decorator_offset(offset_for_comments.start())
1794
+ .unwrap_or_else(|| offset_for_comments.start());
1795
+
1796
+ Self::each_string_or_symbol_arg(call, |name, location| {
1797
+ let str_id = self.local_graph.intern_string(format!("{name}()"));
1798
+ let parent_nesting_id = self.parent_nesting_id();
1799
+ let offset = Offset::from_prism_location(&location);
1800
+
1801
+ let (comments, flags) = self.find_comments_for(comment_offset);
1802
+
1803
+ // module_function makes attr_* methods private (without creating singleton methods)
1804
+ let visibility = match visibility {
1805
+ Visibility::ModuleFunction => Visibility::Private,
1806
+ v => v,
1807
+ };
1808
+
1809
+ let definition = match kind {
1810
+ AttrKind::Accessor => Definition::AttrAccessor(Box::new(AttrAccessorDefinition::new(
1811
+ str_id,
1812
+ self.uri_id,
1813
+ offset,
1814
+ comments,
1815
+ flags,
1816
+ parent_nesting_id,
1817
+ visibility,
1818
+ ))),
1819
+ AttrKind::Reader => Definition::AttrReader(Box::new(AttrReaderDefinition::new(
1820
+ str_id,
1821
+ self.uri_id,
1822
+ offset,
1823
+ comments,
1824
+ flags,
1825
+ parent_nesting_id,
1826
+ visibility,
1827
+ ))),
1828
+ AttrKind::Writer => Definition::AttrWriter(Box::new(AttrWriterDefinition::new(
1829
+ str_id,
1830
+ self.uri_id,
1831
+ offset,
1832
+ comments,
1833
+ flags,
1834
+ parent_nesting_id,
1835
+ visibility,
1836
+ ))),
1837
+ };
1838
+
1839
+ let definition_id = self.local_graph.add_definition(definition);
1840
+ self.add_member_to_current_owner(definition_id);
1841
+ });
1842
+ };
1843
+
1844
+ let message_loc = node.message_loc();
1845
+
1846
+ if message_loc.is_none() {
1847
+ // No message, we can't index this node
1848
+ return;
1849
+ }
1850
+
1851
+ let message = String::from_utf8_lossy(node.name().as_slice()).to_string();
1852
+
1853
+ match message.as_str() {
1854
+ "attr_accessor" => {
1855
+ index_attr(AttrKind::Accessor, node);
1856
+ }
1857
+ "attr_reader" => {
1858
+ index_attr(AttrKind::Reader, node);
1859
+ }
1860
+ "attr_writer" => {
1861
+ index_attr(AttrKind::Writer, node);
1862
+ }
1863
+ "attr" => {
1864
+ // attr :foo, true => both reader and writer
1865
+ // attr :foo, false => only reader
1866
+ // attr :foo => only reader
1867
+ // attr :foo, "bar", :baz => only readers for foo, bar, and baz
1868
+ let create_writer = if let Some(arguments) = node.arguments() {
1869
+ let args_vec: Vec<_> = arguments.arguments().iter().collect();
1870
+ matches!(args_vec.as_slice(), [_, ruby_prism::Node::TrueNode { .. }])
1871
+ } else {
1872
+ false
1873
+ };
1874
+
1875
+ if create_writer {
1876
+ index_attr(AttrKind::Accessor, node);
1877
+ } else {
1878
+ index_attr(AttrKind::Reader, node);
1879
+ }
1880
+ }
1881
+ "alias_method" => {
1882
+ let recv_node = node.receiver();
1883
+ let recv_ref = recv_node.as_ref();
1884
+ if recv_ref.is_some_and(|recv| {
1885
+ !matches!(
1886
+ recv,
1887
+ ruby_prism::Node::SelfNode { .. }
1888
+ | ruby_prism::Node::ConstantReadNode { .. }
1889
+ | ruby_prism::Node::ConstantPathNode { .. }
1890
+ )
1891
+ }) {
1892
+ // TODO: Add a diagnostic for dynamic receivers
1893
+ self.visit_call_node_parts(node);
1894
+ return;
1895
+ }
1896
+
1897
+ let mut names: Vec<(String, Offset)> = Vec::new();
1898
+
1899
+ Self::each_string_or_symbol_arg(node, |name, location| {
1900
+ names.push((name, Offset::from_prism_location(&location)));
1901
+ });
1902
+
1903
+ if names.len() != 2 {
1904
+ // TODO: Add a diagnostic for this
1905
+ return;
1906
+ }
1907
+
1908
+ let (new_name, _new_offset) = &names[0];
1909
+ let (old_name, old_offset) = &names[1];
1910
+
1911
+ let new_name_str_id = self.local_graph.intern_string(format!("{new_name}()"));
1912
+ let old_name_str_id = self.local_graph.intern_string(format!("{old_name}()"));
1913
+
1914
+ let (receiver, method_receiver) = match recv_ref {
1915
+ Some(
1916
+ recv @ (ruby_prism::Node::ConstantPathNode { .. } | ruby_prism::Node::ConstantReadNode { .. }),
1917
+ ) => {
1918
+ let name_id = self.index_constant_reference(recv, true);
1919
+ (name_id.map(Receiver::ConstantReceiver), name_id)
1920
+ }
1921
+ _ => (None, self.method_receiver(recv_ref, node.location())),
1922
+ };
1923
+ let reference = MethodRef::new(old_name_str_id, self.uri_id, old_offset.clone(), method_receiver);
1924
+ self.local_graph.add_method_reference(reference);
1925
+
1926
+ let offset = Offset::from_prism_location(&node.location());
1927
+ let (comments, flags) = self.find_comments_for(offset.start());
1928
+
1929
+ let definition = Definition::MethodAlias(Box::new(MethodAliasDefinition::new(
1930
+ new_name_str_id,
1931
+ old_name_str_id,
1932
+ self.uri_id,
1933
+ offset,
1934
+ comments,
1935
+ flags,
1936
+ self.current_nesting_definition_id(),
1937
+ receiver,
1938
+ )));
1939
+
1940
+ let definition_id = self.local_graph.add_definition(definition);
1941
+
1942
+ self.add_member_to_current_owner(definition_id);
1943
+ }
1944
+ "include" => {
1945
+ let receiver = node.receiver();
1946
+ if receiver.is_none() || receiver.as_ref().is_some_and(|r| r.as_self_node().is_some()) {
1947
+ self.handle_mixin(node, MixinType::Include);
1948
+ } else {
1949
+ self.visit_call_node_parts(node);
1950
+ }
1951
+ }
1952
+ "prepend" => {
1953
+ let receiver = node.receiver();
1954
+ if receiver.is_none() || receiver.as_ref().is_some_and(|r| r.as_self_node().is_some()) {
1955
+ self.handle_mixin(node, MixinType::Prepend);
1956
+ } else {
1957
+ self.visit_call_node_parts(node);
1958
+ }
1959
+ }
1960
+ "extend" => {
1961
+ let receiver = node.receiver();
1962
+ if receiver.is_none() || receiver.as_ref().is_some_and(|r| r.as_self_node().is_some()) {
1963
+ self.handle_mixin(node, MixinType::Extend);
1964
+ } else {
1965
+ self.visit_call_node_parts(node);
1966
+ }
1967
+ }
1968
+ "private" | "protected" | "public" | "module_function" => {
1969
+ if node.receiver().is_some() {
1970
+ let offset = Offset::from_prism_location(&node.location());
1971
+ self.local_graph.add_diagnostic(
1972
+ Rule::InvalidMethodVisibility,
1973
+ offset,
1974
+ format!("`{message}` cannot be called with an explicit receiver"),
1975
+ );
1976
+ self.visit_call_node_parts(node);
1977
+ return;
1978
+ }
1979
+
1980
+ let visibility = Visibility::from_string(message.as_str());
1981
+ let offset = Offset::from_prism_location(&node.location());
1982
+
1983
+ if let Some(arguments) = node.arguments() {
1984
+ if visibility == Visibility::ModuleFunction && !self.current_nesting_is_module() {
1985
+ self.local_graph.add_diagnostic(
1986
+ Rule::InvalidMethodVisibility,
1987
+ offset,
1988
+ "`module_function` can only be used in modules".to_string(),
1989
+ );
1990
+ self.visit_arguments_node(&arguments);
1991
+ } else {
1992
+ self.handle_visibility_arguments(&arguments, visibility, &offset, message.as_str());
1993
+ }
1994
+ } else {
1995
+ // Flag mode: `private` with no arguments
1996
+ //
1997
+ // Replace the current visibility so it affects all subsequent method definitions.
1998
+ let last_visibility = self.visibility_stack.last_mut().unwrap();
1999
+ *last_visibility = VisibilityModifier::new(visibility, false, offset);
2000
+ }
2001
+ }
2002
+ "new" => {
2003
+ let receiver_name = node.receiver().map(|r| r.location().as_slice());
2004
+
2005
+ if matches!(receiver_name, Some(b"Class" | b"::Class")) {
2006
+ self.handle_class_definition(
2007
+ &node.location(),
2008
+ None,
2009
+ node.block(),
2010
+ node.arguments().and_then(|args| args.arguments().iter().next()),
2011
+ Nesting::Owner,
2012
+ );
2013
+ } else if matches!(receiver_name, Some(b"Module" | b"::Module")) {
2014
+ self.handle_module_definition(&node.location(), None, node.block(), Nesting::Owner);
2015
+ } else {
2016
+ if let Some(arguments) = node.arguments() {
2017
+ self.visit_arguments_node(&arguments);
2018
+ }
2019
+
2020
+ if let Some(block) = node.block() {
2021
+ self.visit(&block);
2022
+ }
2023
+ }
2024
+
2025
+ self.index_method_reference_for_call(node);
2026
+ }
2027
+ "sig"
2028
+ if node.receiver().is_none()
2029
+ || matches!(
2030
+ node.receiver(),
2031
+ Some(ruby_prism::Node::ConstantPathNode { .. } | ruby_prism::Node::ConstantReadNode { .. })
2032
+ ) =>
2033
+ {
2034
+ self.pending_decorator_offset = Some(Offset::from_prism_location(&node.location()));
2035
+
2036
+ if let Some(arguments) = node.arguments() {
2037
+ self.visit_arguments_node(&arguments);
2038
+ }
2039
+
2040
+ if let Some(block) = node.block() {
2041
+ self.visit(&block);
2042
+ }
2043
+
2044
+ self.index_method_reference_for_call(node);
2045
+ }
2046
+ "private_constant" => {
2047
+ self.handle_constant_visibility(node, Visibility::Private);
2048
+ }
2049
+ "public_constant" => {
2050
+ self.handle_constant_visibility(node, Visibility::Public);
2051
+ }
2052
+ _ => {
2053
+ // For method calls that we don't explicitly handle each part, we continue visiting their parts as we
2054
+ // may discover something inside
2055
+ if let Some(arguments) = node.arguments() {
2056
+ self.visit_arguments_node(&arguments);
2057
+ }
2058
+
2059
+ if let Some(block) = node.block() {
2060
+ self.visit(&block);
2061
+ }
2062
+
2063
+ let method_receiver = self.method_receiver(node.receiver().as_ref(), node.location());
2064
+
2065
+ if method_receiver.is_none()
2066
+ && let Some(receiver) = node.receiver()
2067
+ {
2068
+ self.visit(&receiver);
2069
+ }
2070
+
2071
+ self.index_method_reference(message.clone(), &node.message_loc().unwrap(), method_receiver);
2072
+
2073
+ match message.as_str() {
2074
+ ">" | "<" | ">=" | "<=" => {
2075
+ self.index_method_reference("<=>".to_string(), &node.message_loc().unwrap(), method_receiver);
2076
+ }
2077
+ _ => {}
2078
+ }
2079
+ }
2080
+ }
2081
+ }
2082
+
2083
+ fn visit_call_and_write_node(&mut self, node: &ruby_prism::CallAndWriteNode) {
2084
+ let method_receiver = self.method_receiver(node.receiver().as_ref(), node.location());
2085
+
2086
+ if method_receiver.is_none()
2087
+ && let Some(receiver) = node.receiver()
2088
+ {
2089
+ self.visit(&receiver);
2090
+ }
2091
+
2092
+ let read_name = String::from_utf8_lossy(node.read_name().as_slice()).to_string();
2093
+ self.index_method_reference(read_name, &node.operator_loc(), method_receiver);
2094
+
2095
+ let write_name = String::from_utf8_lossy(node.write_name().as_slice()).to_string();
2096
+ self.index_method_reference(write_name, &node.operator_loc(), method_receiver);
2097
+
2098
+ self.visit(&node.value());
2099
+ }
2100
+
2101
+ fn visit_call_operator_write_node(&mut self, node: &ruby_prism::CallOperatorWriteNode) {
2102
+ let method_receiver = self.method_receiver(node.receiver().as_ref(), node.location());
2103
+
2104
+ if method_receiver.is_none()
2105
+ && let Some(receiver) = node.receiver()
2106
+ {
2107
+ self.visit(&receiver);
2108
+ }
2109
+
2110
+ let read_name = String::from_utf8_lossy(node.read_name().as_slice()).to_string();
2111
+ self.index_method_reference(read_name, &node.call_operator_loc().unwrap(), method_receiver);
2112
+
2113
+ let write_name = String::from_utf8_lossy(node.write_name().as_slice()).to_string();
2114
+ self.index_method_reference(write_name, &node.call_operator_loc().unwrap(), method_receiver);
2115
+
2116
+ self.visit(&node.value());
2117
+ }
2118
+
2119
+ fn visit_call_or_write_node(&mut self, node: &ruby_prism::CallOrWriteNode) {
2120
+ let method_receiver = self.method_receiver(node.receiver().as_ref(), node.location());
2121
+
2122
+ if method_receiver.is_none()
2123
+ && let Some(receiver) = node.receiver()
2124
+ {
2125
+ self.visit(&receiver);
2126
+ }
2127
+
2128
+ let read_name = String::from_utf8_lossy(node.read_name().as_slice()).to_string();
2129
+ self.index_method_reference(read_name, &node.operator_loc(), method_receiver);
2130
+
2131
+ let write_name = String::from_utf8_lossy(node.write_name().as_slice()).to_string();
2132
+ self.index_method_reference(write_name, &node.operator_loc(), method_receiver);
2133
+
2134
+ self.visit(&node.value());
2135
+ }
2136
+
2137
+ fn visit_global_variable_write_node(&mut self, node: &ruby_prism::GlobalVariableWriteNode) {
2138
+ self.add_definition_from_location(
2139
+ &node.name_loc(),
2140
+ |str_id, offset, comments, flags, lexical_nesting_id, uri_id| {
2141
+ Definition::GlobalVariable(Box::new(GlobalVariableDefinition::new(
2142
+ str_id,
2143
+ uri_id,
2144
+ offset,
2145
+ comments,
2146
+ flags,
2147
+ lexical_nesting_id,
2148
+ )))
2149
+ },
2150
+ );
2151
+ self.visit(&node.value());
2152
+ }
2153
+
2154
+ fn visit_global_variable_and_write_node(&mut self, node: &ruby_prism::GlobalVariableAndWriteNode<'_>) {
2155
+ self.add_definition_from_location(
2156
+ &node.name_loc(),
2157
+ |str_id, offset, comments, flags, nesting_id, uri_id| {
2158
+ Definition::GlobalVariable(Box::new(GlobalVariableDefinition::new(
2159
+ str_id, uri_id, offset, comments, flags, nesting_id,
2160
+ )))
2161
+ },
2162
+ );
2163
+ self.visit(&node.value());
2164
+ }
2165
+
2166
+ fn visit_global_variable_or_write_node(&mut self, node: &ruby_prism::GlobalVariableOrWriteNode<'_>) {
2167
+ self.add_definition_from_location(
2168
+ &node.name_loc(),
2169
+ |str_id, offset, comments, flags, nesting_id, uri_id| {
2170
+ Definition::GlobalVariable(Box::new(GlobalVariableDefinition::new(
2171
+ str_id, uri_id, offset, comments, flags, nesting_id,
2172
+ )))
2173
+ },
2174
+ );
2175
+ self.visit(&node.value());
2176
+ }
2177
+
2178
+ fn visit_global_variable_operator_write_node(&mut self, node: &ruby_prism::GlobalVariableOperatorWriteNode<'_>) {
2179
+ self.add_definition_from_location(
2180
+ &node.name_loc(),
2181
+ |str_id, offset, comments, flags, nesting_id, uri_id| {
2182
+ Definition::GlobalVariable(Box::new(GlobalVariableDefinition::new(
2183
+ str_id, uri_id, offset, comments, flags, nesting_id,
2184
+ )))
2185
+ },
2186
+ );
2187
+ self.visit(&node.value());
2188
+ }
2189
+
2190
+ fn visit_instance_variable_and_write_node(&mut self, node: &ruby_prism::InstanceVariableAndWriteNode) {
2191
+ self.add_instance_variable_definition(&node.name_loc());
2192
+ self.visit(&node.value());
2193
+ }
2194
+
2195
+ fn visit_instance_variable_operator_write_node(&mut self, node: &ruby_prism::InstanceVariableOperatorWriteNode) {
2196
+ self.add_instance_variable_definition(&node.name_loc());
2197
+ self.visit(&node.value());
2198
+ }
2199
+
2200
+ fn visit_instance_variable_or_write_node(&mut self, node: &ruby_prism::InstanceVariableOrWriteNode) {
2201
+ self.add_instance_variable_definition(&node.name_loc());
2202
+ self.visit(&node.value());
2203
+ }
2204
+
2205
+ fn visit_instance_variable_write_node(&mut self, node: &ruby_prism::InstanceVariableWriteNode) {
2206
+ self.add_instance_variable_definition(&node.name_loc());
2207
+ self.visit(&node.value());
2208
+ }
2209
+
2210
+ fn visit_class_variable_and_write_node(&mut self, node: &ruby_prism::ClassVariableAndWriteNode) {
2211
+ self.add_class_variable_definition(&node.name_loc());
2212
+ self.visit(&node.value());
2213
+ }
2214
+
2215
+ fn visit_class_variable_operator_write_node(&mut self, node: &ruby_prism::ClassVariableOperatorWriteNode) {
2216
+ self.add_class_variable_definition(&node.name_loc());
2217
+ self.visit(&node.value());
2218
+ }
2219
+
2220
+ fn visit_class_variable_or_write_node(&mut self, node: &ruby_prism::ClassVariableOrWriteNode) {
2221
+ self.add_class_variable_definition(&node.name_loc());
2222
+ self.visit(&node.value());
2223
+ }
2224
+
2225
+ fn visit_class_variable_write_node(&mut self, node: &ruby_prism::ClassVariableWriteNode) {
2226
+ self.add_class_variable_definition(&node.name_loc());
2227
+ self.visit(&node.value());
2228
+ }
2229
+
2230
+ fn visit_block_argument_node(&mut self, node: &ruby_prism::BlockArgumentNode<'_>) {
2231
+ let expression = node.expression();
2232
+ if let Some(expression) = expression {
2233
+ match expression {
2234
+ ruby_prism::Node::SymbolNode { .. } => {
2235
+ let symbol = expression.as_symbol_node().unwrap();
2236
+ let name = Self::location_to_string(&symbol.value_loc().unwrap());
2237
+ self.index_method_reference(name, &node.location(), None);
2238
+ }
2239
+ _ => {
2240
+ self.visit(&expression);
2241
+ }
2242
+ }
2243
+ }
2244
+ }
2245
+
2246
+ fn visit_alias_method_node(&mut self, node: &ruby_prism::AliasMethodNode<'_>) {
2247
+ let mut new_name = if let Some(symbol_node) = node.new_name().as_symbol_node() {
2248
+ Self::location_to_string(&symbol_node.value_loc().unwrap())
2249
+ } else {
2250
+ Self::location_to_string(&node.new_name().location())
2251
+ };
2252
+
2253
+ let mut old_name = if let Some(symbol_node) = node.old_name().as_symbol_node() {
2254
+ Self::location_to_string(&symbol_node.value_loc().unwrap())
2255
+ } else {
2256
+ Self::location_to_string(&node.old_name().location())
2257
+ };
2258
+
2259
+ new_name.push_str("()");
2260
+ old_name.push_str("()");
2261
+
2262
+ let offset = Offset::from_prism_location(&node.location());
2263
+ let (comments, flags) = self.find_comments_for(offset.start());
2264
+ let definition = Definition::MethodAlias(Box::new(MethodAliasDefinition::new(
2265
+ self.local_graph.intern_string(new_name),
2266
+ self.local_graph.intern_string(old_name.clone()),
2267
+ self.uri_id,
2268
+ offset,
2269
+ comments,
2270
+ flags,
2271
+ self.current_nesting_definition_id(),
2272
+ None,
2273
+ )));
2274
+
2275
+ let definition_id = self.local_graph.add_definition(definition);
2276
+
2277
+ self.add_member_to_current_owner(definition_id);
2278
+ self.index_method_reference(old_name, &node.old_name().location(), None);
2279
+ }
2280
+
2281
+ fn visit_alias_global_variable_node(&mut self, node: &ruby_prism::AliasGlobalVariableNode<'_>) {
2282
+ let new_name = Self::location_to_string(&node.new_name().location());
2283
+ let old_name = Self::location_to_string(&node.old_name().location());
2284
+ let offset = Offset::from_prism_location(&node.location());
2285
+ let (comments, flags) = self.find_comments_for(offset.start());
2286
+
2287
+ let definition = Definition::GlobalVariableAlias(Box::new(GlobalVariableAliasDefinition::new(
2288
+ self.local_graph.intern_string(new_name),
2289
+ self.local_graph.intern_string(old_name),
2290
+ self.uri_id,
2291
+ offset,
2292
+ comments,
2293
+ flags,
2294
+ self.parent_nesting_id(),
2295
+ )));
2296
+
2297
+ let definition_id = self.local_graph.add_definition(definition);
2298
+
2299
+ self.add_member_to_current_owner(definition_id);
2300
+ }
2301
+
2302
+ fn visit_and_node(&mut self, node: &ruby_prism::AndNode) {
2303
+ let left = node.left();
2304
+ let method_receiver = self.method_receiver(Some(&left), left.location());
2305
+
2306
+ if method_receiver.is_none() {
2307
+ self.visit(&left);
2308
+ }
2309
+
2310
+ self.index_method_reference("&&".to_string(), &node.location(), method_receiver);
2311
+ self.visit(&node.right());
2312
+ }
2313
+
2314
+ fn visit_or_node(&mut self, node: &ruby_prism::OrNode) {
2315
+ let left = node.left();
2316
+ let method_receiver = self.method_receiver(Some(&left), left.location());
2317
+
2318
+ if method_receiver.is_none() {
2319
+ self.visit(&left);
2320
+ }
2321
+
2322
+ self.index_method_reference("||".to_string(), &node.location(), method_receiver);
2323
+ self.visit(&node.right());
2324
+ }
2325
+ }
2326
+
2327
+ #[cfg(test)]
2328
+ #[path = "ruby_indexer_tests.rs"]
2329
+ mod tests;