rubydex 0.2.3-aarch64-linux → 0.2.5-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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/THIRD_PARTY_LICENSES.html +2 -2
  3. data/exe/rubydex_mcp +17 -0
  4. data/ext/rubydex/definition.c +56 -0
  5. data/ext/rubydex/extconf.rb +8 -0
  6. data/lib/rubydex/3.2/rubydex.so +0 -0
  7. data/lib/rubydex/3.3/rubydex.so +0 -0
  8. data/lib/rubydex/3.4/rubydex.so +0 -0
  9. data/lib/rubydex/4.0/rubydex.so +0 -0
  10. data/lib/rubydex/bin/rubydex_mcp +0 -0
  11. data/lib/rubydex/declaration.rb +12 -0
  12. data/lib/rubydex/graph.rb +3 -1
  13. data/lib/rubydex/librubydex_sys.so +0 -0
  14. data/lib/rubydex/version.rb +1 -1
  15. data/rbi/rubydex.rbi +14 -1
  16. data/rust/Cargo.lock +4 -4
  17. data/rust/rubydex/src/indexing/local_graph.rs +38 -0
  18. data/rust/rubydex/src/indexing/ruby_indexer.rs +6 -0
  19. data/rust/rubydex/src/indexing/ruby_indexer_tests.rs +5 -1
  20. data/rust/rubydex/src/indexing.rs +38 -12
  21. data/rust/rubydex/src/lib.rs +1 -0
  22. data/rust/rubydex/src/listing.rs +14 -3
  23. data/rust/rubydex/src/main.rs +27 -2
  24. data/rust/rubydex/src/model/definitions.rs +7 -18
  25. data/rust/rubydex/src/model/graph.rs +21 -4
  26. data/rust/rubydex/src/model/ids.rs +27 -1
  27. data/rust/rubydex/src/operation/applier.rs +519 -0
  28. data/rust/rubydex/src/operation/mod.rs +284 -0
  29. data/rust/rubydex/src/operation/printer.rs +260 -0
  30. data/rust/rubydex/src/operation/ruby_builder.rs +2915 -0
  31. data/rust/rubydex/src/resolution.rs +6 -1
  32. data/rust/rubydex/src/resolution_tests.rs +217 -209
  33. data/rust/rubydex/src/test_utils/graph_test.rs +19 -4
  34. data/rust/rubydex/src/test_utils/local_graph_test.rs +7 -6
  35. data/rust/rubydex-mcp/Cargo.toml +1 -1
  36. data/rust/rubydex-mcp/src/server.rs +10 -7
  37. data/rust/rubydex-sys/src/definition_api.rs +24 -0
  38. data/rust/rubydex-sys/src/graph_api.rs +1 -1
  39. metadata +9 -2
@@ -0,0 +1,2915 @@
1
+ //! Visit the Ruby AST and produce an ordered list of operations.
2
+ //!
3
+ //! Walks the parsed AST and produces `Vec<Operation>` that can later be applied
4
+ //! by the applier to create definitions and declarations in a `LocalGraph`.
5
+
6
+ use std::collections::hash_map::Entry;
7
+
8
+ use crate::diagnostic::{Diagnostic, Rule};
9
+ use crate::model::comment::Comment;
10
+ use crate::model::definitions::{DefinitionFlags, Parameter, ParameterStruct, Signatures};
11
+ use crate::model::document::Document;
12
+ use crate::model::identity_maps::IdentityHashMap;
13
+ use crate::model::ids::{NameId, StringId, UriId};
14
+ use crate::model::name::{Name, NameRef, ParentScope};
15
+ use crate::model::string_ref::StringRef;
16
+ use crate::model::visibility::Visibility;
17
+ use crate::offset::Offset;
18
+ use crate::operation::{self as op, AttrKind, MixinKind, Operation, Target};
19
+
20
+ use ruby_prism::{ParseResult, Visit};
21
+
22
+ /// The result of running the operation builder on a Ruby source file.
23
+ ///
24
+ /// Contains the ordered operations and all interning data (strings, names, references)
25
+ /// needed to later apply the operations to a graph.
26
+ #[derive(Debug)]
27
+ pub struct OperationBuilderResult {
28
+ pub uri_id: UriId,
29
+ pub document: Document,
30
+ pub operations: Vec<Operation>,
31
+ pub strings: IdentityHashMap<StringId, StringRef>,
32
+ pub names: IdentityHashMap<NameId, NameRef>,
33
+ }
34
+
35
+ #[derive(Clone, Copy)]
36
+ enum Nesting {
37
+ /// A lexical scope (class/module keyword) that produces a new constant resolution scope.
38
+ LexicalScope { name_id: NameId, is_module: bool },
39
+ /// An owner that doesn't produce a lexical scope (Class.new/Module.new).
40
+ Owner { name_id: NameId, is_module: bool },
41
+ /// A method entry, used for instance variable ownership.
42
+ Method { receiver: Option<NestingReceiver> },
43
+ }
44
+
45
+ /// Tracks receiver info for methods on the nesting stack, so `method_receiver` can work
46
+ /// without looking up definitions. Distinct from `operation::Target` which represents
47
+ /// the source-level receiver without resolved names.
48
+ #[derive(Clone, Copy)]
49
+ enum NestingReceiver {
50
+ SelfReceiver(NameId),
51
+ ConstantReceiver(NameId),
52
+ }
53
+
54
+ struct VisibilityModifier {
55
+ visibility: Visibility,
56
+ is_inline: bool,
57
+ offset: Offset,
58
+ }
59
+
60
+ impl VisibilityModifier {
61
+ #[must_use]
62
+ pub fn new(visibility: Visibility, is_inline: bool, offset: Offset) -> Self {
63
+ Self {
64
+ visibility,
65
+ is_inline,
66
+ offset,
67
+ }
68
+ }
69
+ }
70
+
71
+ /// Visits the Ruby AST and produces an ordered list of operations.
72
+ pub struct RubyOperationBuilder<'a> {
73
+ uri_id: UriId,
74
+ source: &'a str,
75
+ // Interning
76
+ strings: IdentityHashMap<StringId, StringRef>,
77
+ names: IdentityHashMap<NameId, NameRef>,
78
+ document: Document,
79
+ // State
80
+ comments: Vec<CommentGroup>,
81
+ nesting_stack: Vec<Nesting>,
82
+ visibility_stack: Vec<VisibilityModifier>,
83
+ pending_decorator_offset: Option<Offset>,
84
+ // Output
85
+ operations: Vec<Operation>,
86
+ }
87
+
88
+ impl<'a> RubyOperationBuilder<'a> {
89
+ #[must_use]
90
+ pub fn new(uri: String, source: &'a str) -> Self {
91
+ let uri_id = UriId::from(&uri);
92
+
93
+ Self {
94
+ uri_id,
95
+ source,
96
+ strings: IdentityHashMap::default(),
97
+ names: IdentityHashMap::default(),
98
+ document: Document::new(uri, source),
99
+ comments: Vec::new(),
100
+ nesting_stack: Vec::new(),
101
+ visibility_stack: vec![VisibilityModifier::new(Visibility::Private, false, Offset::new(0, 0))],
102
+ pending_decorator_offset: None,
103
+ operations: Vec::new(),
104
+ }
105
+ }
106
+
107
+ #[must_use]
108
+ pub fn build(mut self) -> OperationBuilderResult {
109
+ let result = ruby_prism::parse(self.source.as_bytes());
110
+
111
+ for error in result.errors() {
112
+ self.add_diagnostic(
113
+ Rule::ParseError,
114
+ Offset::from_prism_location(&error.location()),
115
+ error.message().to_string(),
116
+ );
117
+ }
118
+
119
+ for warning in result.warnings() {
120
+ self.add_diagnostic(
121
+ Rule::ParseWarning,
122
+ Offset::from_prism_location(&warning.location()),
123
+ warning.message().to_string(),
124
+ );
125
+ }
126
+
127
+ self.comments = self.parse_comments_into_groups(&result);
128
+ self.visit(&result.node());
129
+
130
+ OperationBuilderResult {
131
+ uri_id: self.uri_id,
132
+ document: self.document,
133
+ operations: self.operations,
134
+ strings: self.strings,
135
+ names: self.names,
136
+ }
137
+ }
138
+
139
+ // -- Interning --
140
+
141
+ fn intern_string(&mut self, string: String) -> StringId {
142
+ let string_id = StringId::from(&string);
143
+
144
+ match self.strings.entry(string_id) {
145
+ Entry::Occupied(mut entry) => {
146
+ debug_assert!(string == **entry.get(), "StringId collision");
147
+ entry.get_mut().increment_ref_count(1);
148
+ }
149
+ Entry::Vacant(entry) => {
150
+ entry.insert(StringRef::new(string));
151
+ }
152
+ }
153
+
154
+ string_id
155
+ }
156
+
157
+ fn add_name(&mut self, name: Name) -> NameId {
158
+ let name_id = name.id();
159
+
160
+ match self.names.entry(name_id) {
161
+ Entry::Occupied(mut entry) => {
162
+ debug_assert!(*entry.get() == name, "NameId collision");
163
+ entry.get_mut().increment_ref_count(1);
164
+ }
165
+ Entry::Vacant(entry) => {
166
+ entry.insert(NameRef::Unresolved(Box::new(name)));
167
+ }
168
+ }
169
+
170
+ name_id
171
+ }
172
+
173
+ fn add_diagnostic(&mut self, rule: Rule, offset: Offset, message: String) {
174
+ let diagnostic = Diagnostic::new(rule, self.uri_id, offset, message);
175
+ self.document.add_diagnostic(diagnostic);
176
+ }
177
+
178
+ // -- Nesting helpers --
179
+
180
+ fn current_nesting_is_module(&self) -> bool {
181
+ self.nesting_stack
182
+ .iter()
183
+ .rev()
184
+ .find_map(|nesting| match nesting {
185
+ Nesting::LexicalScope { is_module, .. } | Nesting::Owner { is_module, .. } => Some(*is_module),
186
+ Nesting::Method { .. } => None,
187
+ })
188
+ .unwrap_or(false)
189
+ }
190
+
191
+ fn current_lexical_scope_name_id(&self) -> Option<NameId> {
192
+ self.nesting_stack.iter().rev().find_map(|nesting| match nesting {
193
+ Nesting::LexicalScope { name_id, .. } => Some(*name_id),
194
+ Nesting::Owner { .. } | Nesting::Method { .. } => None,
195
+ })
196
+ }
197
+
198
+ fn current_owner_name_id(&self) -> Option<NameId> {
199
+ self.nesting_stack.iter().rev().find_map(|nesting| match nesting {
200
+ Nesting::LexicalScope { name_id, .. } | Nesting::Owner { name_id, .. } => Some(*name_id),
201
+ Nesting::Method { .. } => None,
202
+ })
203
+ }
204
+
205
+ fn current_visibility(&self) -> &VisibilityModifier {
206
+ self.visibility_stack
207
+ .last()
208
+ .expect("visibility stack should not be empty")
209
+ }
210
+
211
+ fn parse_comments_into_groups(&mut self, result: &ParseResult<'_>) -> Vec<CommentGroup> {
212
+ let mut iter = result.comments().peekable();
213
+ let mut groups = Vec::new();
214
+
215
+ while let Some(comment) = iter.next() {
216
+ let mut group = CommentGroup::new();
217
+ group.add_comment(&comment);
218
+ while let Some(next_comment) = iter.peek() {
219
+ if group.accepts(next_comment, self.source) {
220
+ let next = iter.next().unwrap();
221
+ group.add_comment(&next);
222
+ } else {
223
+ break;
224
+ }
225
+ }
226
+ groups.push(group);
227
+ }
228
+ groups
229
+ }
230
+
231
+ fn location_to_string(location: &ruby_prism::Location) -> String {
232
+ String::from_utf8_lossy(location.as_slice()).to_string()
233
+ }
234
+
235
+ fn find_comments_for(&self, offset: u32) -> (Box<[Comment]>, DefinitionFlags) {
236
+ let offset_usize = offset as usize;
237
+ if self.comments.is_empty() {
238
+ return (Box::default(), DefinitionFlags::empty());
239
+ }
240
+
241
+ let idx = match self.comments.binary_search_by_key(&offset_usize, |g| g.end_offset) {
242
+ Ok(_) => {
243
+ debug_assert!(false, "Comment ends exactly at definition start");
244
+ return (Box::default(), DefinitionFlags::empty());
245
+ }
246
+ Err(i) if i > 0 => i - 1,
247
+ Err(_) => return (Box::default(), DefinitionFlags::empty()),
248
+ };
249
+
250
+ let group = &self.comments[idx];
251
+ let between = &self.source.as_bytes()[group.end_offset..offset_usize];
252
+ if !between.iter().all(|&b| b.is_ascii_whitespace()) {
253
+ return (Box::default(), DefinitionFlags::empty());
254
+ }
255
+
256
+ if bytecount::count(between, b'\n') > 2 {
257
+ return (Box::default(), DefinitionFlags::empty());
258
+ }
259
+
260
+ (group.comments(), group.flags())
261
+ }
262
+
263
+ fn take_decorator_offset(&mut self, definition_start: u32) -> Option<u32> {
264
+ let decorator_offset = self.pending_decorator_offset.take()?;
265
+ if decorator_offset.end() > definition_start {
266
+ return None;
267
+ }
268
+
269
+ let between = &self.source.as_bytes()[decorator_offset.end() as usize..definition_start as usize];
270
+ if between.iter().all(|&b| b.is_ascii_whitespace()) && bytecount::count(between, b'\n') <= 1 {
271
+ Some(decorator_offset.start())
272
+ } else {
273
+ None
274
+ }
275
+ }
276
+
277
+ fn index_constant_reference(&mut self, node: &ruby_prism::Node, push_final_reference: bool) -> Option<NameId> {
278
+ let mut parent_scope_id = ParentScope::None;
279
+
280
+ let location = match node {
281
+ ruby_prism::Node::ConstantPathNode { .. } => {
282
+ let constant = node.as_constant_path_node().unwrap();
283
+
284
+ if let Some(parent) = constant.parent() {
285
+ match parent {
286
+ ruby_prism::Node::ConstantPathNode { .. } | ruby_prism::Node::ConstantReadNode { .. } => {}
287
+ _ => {
288
+ self.add_diagnostic(
289
+ Rule::DynamicConstantReference,
290
+ Offset::from_prism_location(&parent.location()),
291
+ "Dynamic constant reference".to_string(),
292
+ );
293
+ return None;
294
+ }
295
+ }
296
+
297
+ parent_scope_id = self
298
+ .index_constant_reference(&parent, true)
299
+ .map_or(ParentScope::None, ParentScope::Some);
300
+ } else {
301
+ parent_scope_id = ParentScope::TopLevel;
302
+ }
303
+
304
+ constant.name_loc()
305
+ }
306
+ ruby_prism::Node::ConstantPathWriteNode { .. } => {
307
+ let constant = node.as_constant_path_write_node().unwrap();
308
+ let target = constant.target();
309
+
310
+ if let Some(parent) = target.parent() {
311
+ match parent {
312
+ ruby_prism::Node::ConstantPathNode { .. } | ruby_prism::Node::ConstantReadNode { .. } => {}
313
+ _ => {
314
+ return None;
315
+ }
316
+ }
317
+
318
+ parent_scope_id = self
319
+ .index_constant_reference(&parent, true)
320
+ .map_or(ParentScope::None, ParentScope::Some);
321
+ } else {
322
+ parent_scope_id = ParentScope::TopLevel;
323
+ }
324
+
325
+ target.name_loc()
326
+ }
327
+ ruby_prism::Node::ConstantReadNode { .. } => node.location(),
328
+ ruby_prism::Node::ConstantAndWriteNode { .. } => node.as_constant_and_write_node().unwrap().name_loc(),
329
+ ruby_prism::Node::ConstantOperatorWriteNode { .. } => {
330
+ node.as_constant_operator_write_node().unwrap().name_loc()
331
+ }
332
+ ruby_prism::Node::ConstantOrWriteNode { .. } => node.as_constant_or_write_node().unwrap().name_loc(),
333
+ ruby_prism::Node::ConstantTargetNode { .. } => node.as_constant_target_node().unwrap().location(),
334
+ ruby_prism::Node::ConstantWriteNode { .. } => node.as_constant_write_node().unwrap().name_loc(),
335
+ ruby_prism::Node::ConstantPathTargetNode { .. } => {
336
+ let target = node.as_constant_path_target_node().unwrap();
337
+
338
+ if let Some(parent) = target.parent() {
339
+ match parent {
340
+ ruby_prism::Node::ConstantPathNode { .. } | ruby_prism::Node::ConstantReadNode { .. } => {}
341
+ _ => {
342
+ return None;
343
+ }
344
+ }
345
+
346
+ parent_scope_id = self
347
+ .index_constant_reference(&parent, true)
348
+ .map_or(ParentScope::None, ParentScope::Some);
349
+ } else {
350
+ parent_scope_id = ParentScope::TopLevel;
351
+ }
352
+
353
+ target.name_loc()
354
+ }
355
+ _ => {
356
+ return None;
357
+ }
358
+ };
359
+
360
+ let offset = Offset::from_prism_location(&location);
361
+ let name = Self::location_to_string(&location);
362
+ let string_id = self.intern_string(name);
363
+ let name_id = self.add_name(Name::new(
364
+ string_id,
365
+ parent_scope_id,
366
+ self.current_lexical_scope_name_id(),
367
+ ));
368
+
369
+ if push_final_reference {
370
+ self.operations
371
+ .push(Operation::ReferenceConstant(op::ReferenceConstant {
372
+ name_id,
373
+ uri_id: self.uri_id,
374
+ offset,
375
+ }));
376
+ }
377
+
378
+ Some(name_id)
379
+ }
380
+
381
+ fn index_method_reference(&mut self, name: String, location: &ruby_prism::Location, receiver: Option<NameId>) {
382
+ let offset = Offset::from_prism_location(location);
383
+ let str_id = self.intern_string(name);
384
+ self.operations.push(Operation::ReferenceMethod(op::ReferenceMethod {
385
+ str_id,
386
+ uri_id: self.uri_id,
387
+ offset,
388
+ receiver: receiver.map(Target::Constant),
389
+ }));
390
+ }
391
+
392
+ fn index_method_reference_for_call(&mut self, node: &ruby_prism::CallNode) {
393
+ let method_receiver = self.method_receiver(node.receiver().as_ref(), node.location());
394
+
395
+ if method_receiver.is_none()
396
+ && let Some(receiver) = node.receiver()
397
+ {
398
+ self.visit(&receiver);
399
+ }
400
+
401
+ let message = String::from_utf8_lossy(node.name().as_slice()).to_string();
402
+ self.index_method_reference(message, &node.message_loc().unwrap(), method_receiver);
403
+ }
404
+
405
+ fn visit_call_node_parts(&mut self, node: &ruby_prism::CallNode) {
406
+ if let Some(receiver) = node.receiver() {
407
+ self.visit(&receiver);
408
+ }
409
+ if let Some(arguments) = node.arguments() {
410
+ self.visit_arguments_node(&arguments);
411
+ }
412
+ if let Some(block) = node.block() {
413
+ self.visit(&block);
414
+ }
415
+ }
416
+
417
+ // -- Method receiver resolution --
418
+
419
+ fn method_receiver(
420
+ &mut self,
421
+ receiver: Option<&ruby_prism::Node>,
422
+ fallback_location: ruby_prism::Location,
423
+ ) -> Option<NameId> {
424
+ let mut is_singleton_name = false;
425
+
426
+ let name_id = match receiver {
427
+ Some(ruby_prism::Node::SelfNode { .. }) | None => match self.nesting_stack.last() {
428
+ Some(Nesting::LexicalScope { name_id, .. } | Nesting::Owner { name_id, .. }) => {
429
+ is_singleton_name = true;
430
+ Some(*name_id)
431
+ }
432
+ Some(Nesting::Method { receiver, .. }) => {
433
+ if let Some(recv) = receiver {
434
+ is_singleton_name = true;
435
+ match recv {
436
+ NestingReceiver::SelfReceiver(name_id) | NestingReceiver::ConstantReceiver(name_id) => {
437
+ Some(*name_id)
438
+ }
439
+ }
440
+ } else {
441
+ self.current_owner_name_id()
442
+ }
443
+ }
444
+ None => {
445
+ let str_id = self.intern_string("Object".into());
446
+ Some(self.add_name(Name::new(str_id, ParentScope::None, None)))
447
+ }
448
+ },
449
+ Some(ruby_prism::Node::CallNode { .. }) => {
450
+ let call_node = receiver.unwrap().as_call_node().unwrap();
451
+ if call_node.name().as_slice() == b"singleton_class" {
452
+ is_singleton_name = true;
453
+ self.method_receiver(call_node.receiver().as_ref(), call_node.location())
454
+ } else {
455
+ None
456
+ }
457
+ }
458
+ Some(node) => {
459
+ is_singleton_name = true;
460
+ self.index_constant_reference(node, true)
461
+ }
462
+ }?;
463
+
464
+ if !is_singleton_name {
465
+ return Some(name_id);
466
+ }
467
+
468
+ let singleton_class_name = {
469
+ let name = self.names.get(&name_id).expect("Indexed constant name should exist");
470
+
471
+ let target_str = self
472
+ .strings
473
+ .get(name.str())
474
+ .expect("Indexed constant string should exist");
475
+
476
+ format!("<{}>", target_str.as_str())
477
+ };
478
+
479
+ let string_id = self.intern_string(singleton_class_name);
480
+ let new_name_id = self.add_name(Name::new(string_id, ParentScope::Attached(name_id), None));
481
+
482
+ let location = receiver.map_or(fallback_location, ruby_prism::Node::location);
483
+ let offset = Offset::from_prism_location(&location);
484
+ self.operations
485
+ .push(Operation::ReferenceConstant(op::ReferenceConstant {
486
+ name_id: new_name_id,
487
+ uri_id: self.uri_id,
488
+ offset,
489
+ }));
490
+ Some(new_name_id)
491
+ }
492
+
493
+ // -- Parameters --
494
+
495
+ fn collect_parameters(&mut self, node: &ruby_prism::DefNode) -> Vec<Parameter> {
496
+ let mut parameters: Vec<Parameter> = Vec::new();
497
+
498
+ let Some(parameters_list) = node.parameters() else {
499
+ return parameters;
500
+ };
501
+
502
+ for parameter in &parameters_list.requireds() {
503
+ let location = parameter.location();
504
+ let str_id = self.intern_string(Self::location_to_string(&location));
505
+ parameters.push(Parameter::RequiredPositional(ParameterStruct::new(
506
+ Offset::from_prism_location(&location),
507
+ str_id,
508
+ )));
509
+ }
510
+
511
+ for parameter in &parameters_list.optionals() {
512
+ let opt_param = parameter.as_optional_parameter_node().unwrap();
513
+ let name_loc = opt_param.name_loc();
514
+ let str_id = self.intern_string(Self::location_to_string(&name_loc));
515
+ parameters.push(Parameter::OptionalPositional(ParameterStruct::new(
516
+ Offset::from_prism_location(&name_loc),
517
+ str_id,
518
+ )));
519
+ self.visit(&opt_param.value());
520
+ }
521
+
522
+ if let Some(rest) = parameters_list.rest() {
523
+ let rest_param = rest.as_rest_parameter_node().unwrap();
524
+ let location = rest_param.name_loc().unwrap_or_else(|| rest.location());
525
+ let str_id = self.intern_string(Self::location_to_string(&location));
526
+ parameters.push(Parameter::RestPositional(ParameterStruct::new(
527
+ Offset::from_prism_location(&location),
528
+ str_id,
529
+ )));
530
+ }
531
+
532
+ for post in &parameters_list.posts() {
533
+ let location = post.location();
534
+ let str_id = self.intern_string(Self::location_to_string(&location));
535
+ parameters.push(Parameter::Post(ParameterStruct::new(
536
+ Offset::from_prism_location(&location),
537
+ str_id,
538
+ )));
539
+ }
540
+
541
+ for keyword in &parameters_list.keywords() {
542
+ match keyword {
543
+ ruby_prism::Node::RequiredKeywordParameterNode { .. } => {
544
+ let required = keyword.as_required_keyword_parameter_node().unwrap();
545
+ let name_loc = required.name_loc();
546
+ let str_id =
547
+ self.intern_string(Self::location_to_string(&name_loc).trim_end_matches(':').to_string());
548
+ parameters.push(Parameter::RequiredKeyword(ParameterStruct::new(
549
+ Offset::from_prism_location(&name_loc),
550
+ str_id,
551
+ )));
552
+ }
553
+ ruby_prism::Node::OptionalKeywordParameterNode { .. } => {
554
+ let optional = keyword.as_optional_keyword_parameter_node().unwrap();
555
+ let name_loc = optional.name_loc();
556
+ let str_id =
557
+ self.intern_string(Self::location_to_string(&name_loc).trim_end_matches(':').to_string());
558
+ parameters.push(Parameter::OptionalKeyword(ParameterStruct::new(
559
+ Offset::from_prism_location(&name_loc),
560
+ str_id,
561
+ )));
562
+ self.visit(&optional.value());
563
+ }
564
+ _ => {}
565
+ }
566
+ }
567
+
568
+ if let Some(rest) = parameters_list.keyword_rest() {
569
+ match rest {
570
+ ruby_prism::Node::KeywordRestParameterNode { .. } => {
571
+ let location = rest
572
+ .as_keyword_rest_parameter_node()
573
+ .unwrap()
574
+ .name_loc()
575
+ .unwrap_or_else(|| rest.location());
576
+ let str_id = self.intern_string(Self::location_to_string(&location));
577
+ parameters.push(Parameter::RestKeyword(ParameterStruct::new(
578
+ Offset::from_prism_location(&location),
579
+ str_id,
580
+ )));
581
+ }
582
+ ruby_prism::Node::ForwardingParameterNode { .. } => {
583
+ let location = rest.location();
584
+ let str_id = self.intern_string(Self::location_to_string(&location));
585
+ parameters.push(Parameter::Forward(ParameterStruct::new(
586
+ Offset::from_prism_location(&location),
587
+ str_id,
588
+ )));
589
+ }
590
+ _ => {}
591
+ }
592
+ }
593
+
594
+ if let Some(block) = parameters_list.block() {
595
+ let location = block.name_loc().unwrap_or_else(|| block.location());
596
+ let str_id = self.intern_string(Self::location_to_string(&location));
597
+ parameters.push(Parameter::Block(ParameterStruct::new(
598
+ Offset::from_prism_location(&location),
599
+ str_id,
600
+ )));
601
+ }
602
+
603
+ parameters
604
+ }
605
+
606
+ // -- Helpers --
607
+
608
+ fn each_string_or_symbol_arg<F>(node: &ruby_prism::CallNode, mut f: F)
609
+ where
610
+ F: FnMut(String, ruby_prism::Location),
611
+ {
612
+ if let Some(arguments) = node.arguments() {
613
+ for argument in &arguments.arguments() {
614
+ match argument {
615
+ ruby_prism::Node::SymbolNode { .. } => {
616
+ let symbol = argument.as_symbol_node().unwrap();
617
+ if let Some(value_loc) = symbol.value_loc() {
618
+ let name = Self::location_to_string(&value_loc);
619
+ f(name, value_loc);
620
+ }
621
+ }
622
+ ruby_prism::Node::StringNode { .. } => {
623
+ let string = argument.as_string_node().unwrap();
624
+ let name = String::from_utf8_lossy(string.unescaped()).to_string();
625
+ f(name, argument.location());
626
+ }
627
+ _ => {}
628
+ }
629
+ }
630
+ }
631
+ }
632
+
633
+ fn is_promotable_value(value: &ruby_prism::Node) -> bool {
634
+ value
635
+ .as_call_node()
636
+ .is_some_and(|call| call.receiver().is_none() || call.call_operator_loc().is_some())
637
+ }
638
+
639
+ // -- Definition handlers --
640
+
641
+ fn handle_class_definition(
642
+ &mut self,
643
+ location: &ruby_prism::Location,
644
+ name_node: Option<&ruby_prism::Node>,
645
+ body_node: Option<ruby_prism::Node>,
646
+ superclass_node: Option<ruby_prism::Node>,
647
+ is_lexical_scope: bool,
648
+ ) {
649
+ let offset = Offset::from_prism_location(location);
650
+ let (comments, flags) = self.find_comments_for(offset.start());
651
+ let superclass_name = superclass_node.as_ref().and_then(|n| {
652
+ if let Some(id) = self.index_constant_reference(n, false) {
653
+ self.operations
654
+ .push(Operation::ReferenceConstant(op::ReferenceConstant {
655
+ name_id: id,
656
+ uri_id: self.uri_id,
657
+ offset: Offset::from_prism_location(&n.location()),
658
+ }));
659
+ return Some(id);
660
+ }
661
+
662
+ if let ruby_prism::Node::CallNode { .. } = n {
663
+ let call = n.as_call_node().unwrap();
664
+ if let Some(receiver) = call.receiver()
665
+ && let Some(id) = self.index_constant_reference(&receiver, false)
666
+ {
667
+ self.operations
668
+ .push(Operation::ReferenceConstant(op::ReferenceConstant {
669
+ name_id: id,
670
+ uri_id: self.uri_id,
671
+ offset: Offset::from_prism_location(&receiver.location()),
672
+ }));
673
+ return Some(id);
674
+ }
675
+ }
676
+
677
+ None
678
+ });
679
+
680
+ if let Some(superclass_node) = superclass_node
681
+ && superclass_name.is_none()
682
+ {
683
+ self.add_diagnostic(
684
+ Rule::DynamicAncestor,
685
+ Offset::from_prism_location(&superclass_node.location()),
686
+ "Dynamic superclass".to_string(),
687
+ );
688
+ }
689
+
690
+ let (name_id, name_offset) = if let Some(name_node) = name_node {
691
+ let name_loc = match name_node {
692
+ ruby_prism::Node::ConstantPathNode { .. } => name_node.as_constant_path_node().unwrap().name_loc(),
693
+ ruby_prism::Node::ConstantPathWriteNode { .. } => {
694
+ name_node.as_constant_path_write_node().unwrap().target().name_loc()
695
+ }
696
+ _ => name_node.location(),
697
+ };
698
+ (
699
+ self.index_constant_reference(name_node, false),
700
+ Offset::from_prism_location(&name_loc),
701
+ )
702
+ } else {
703
+ let string_id = self.intern_string(format!("{}:{}<anonymous>", self.uri_id, offset.start()));
704
+ (
705
+ Some(self.add_name(Name::new(string_id, ParentScope::None, None))),
706
+ offset.clone(),
707
+ )
708
+ };
709
+
710
+ if let Some(name_id) = name_id {
711
+ self.operations.push(Operation::EnterClass(op::EnterClass {
712
+ name_id,
713
+ uri_id: self.uri_id,
714
+ offset: offset.clone(),
715
+ name_offset,
716
+ comments,
717
+ flags,
718
+ superclass_name,
719
+ is_lexical_scope,
720
+ }));
721
+
722
+ let nesting = if is_lexical_scope {
723
+ Nesting::LexicalScope {
724
+ name_id,
725
+ is_module: false,
726
+ }
727
+ } else {
728
+ Nesting::Owner {
729
+ name_id,
730
+ is_module: false,
731
+ }
732
+ };
733
+ self.nesting_stack.push(nesting);
734
+ self.visibility_stack
735
+ .push(VisibilityModifier::new(Visibility::Public, false, offset));
736
+ if let Some(body) = body_node {
737
+ self.visit(&body);
738
+ }
739
+ self.visibility_stack.pop();
740
+ self.nesting_stack.pop();
741
+ self.operations.push(Operation::ExitScope);
742
+ }
743
+ }
744
+
745
+ fn handle_module_definition(
746
+ &mut self,
747
+ location: &ruby_prism::Location,
748
+ name_node: Option<&ruby_prism::Node>,
749
+ body_node: Option<ruby_prism::Node>,
750
+ is_lexical_scope: bool,
751
+ ) {
752
+ let offset = Offset::from_prism_location(location);
753
+ let (comments, flags) = self.find_comments_for(offset.start());
754
+
755
+ let (name_id, name_offset) = if let Some(name_node) = name_node {
756
+ let name_loc = match name_node {
757
+ ruby_prism::Node::ConstantPathNode { .. } => name_node.as_constant_path_node().unwrap().name_loc(),
758
+ ruby_prism::Node::ConstantPathWriteNode { .. } => {
759
+ name_node.as_constant_path_write_node().unwrap().target().name_loc()
760
+ }
761
+ _ => name_node.location(),
762
+ };
763
+ (
764
+ self.index_constant_reference(name_node, false),
765
+ Offset::from_prism_location(&name_loc),
766
+ )
767
+ } else {
768
+ let string_id = self.intern_string(format!("{}:{}<anonymous>", self.uri_id, offset.start()));
769
+ (
770
+ Some(self.add_name(Name::new(string_id, ParentScope::None, None))),
771
+ offset.clone(),
772
+ )
773
+ };
774
+
775
+ if let Some(name_id) = name_id {
776
+ self.operations.push(Operation::EnterModule(op::EnterModule {
777
+ name_id,
778
+ uri_id: self.uri_id,
779
+ offset: offset.clone(),
780
+ name_offset,
781
+ comments,
782
+ flags,
783
+ is_lexical_scope,
784
+ }));
785
+
786
+ let nesting = if is_lexical_scope {
787
+ Nesting::LexicalScope {
788
+ name_id,
789
+ is_module: true,
790
+ }
791
+ } else {
792
+ Nesting::Owner {
793
+ name_id,
794
+ is_module: true,
795
+ }
796
+ };
797
+ self.nesting_stack.push(nesting);
798
+ self.visibility_stack
799
+ .push(VisibilityModifier::new(Visibility::Public, false, offset));
800
+ if let Some(body) = body_node {
801
+ self.visit(&body);
802
+ }
803
+ self.visibility_stack.pop();
804
+ self.nesting_stack.pop();
805
+ self.operations.push(Operation::ExitScope);
806
+ }
807
+ }
808
+
809
+ fn handle_dynamic_class_or_module(&mut self, node: &ruby_prism::Node, value: &ruby_prism::Node) -> bool {
810
+ let Some(call_node) = value.as_call_node() else {
811
+ return false;
812
+ };
813
+
814
+ if call_node.name().as_slice() != b"new" {
815
+ return false;
816
+ }
817
+
818
+ let Some(receiver) = call_node.receiver() else {
819
+ return false;
820
+ };
821
+
822
+ let receiver_name = receiver.location().as_slice();
823
+
824
+ if matches!(receiver_name, b"Module" | b"::Module") {
825
+ self.handle_module_definition(&node.location(), Some(node), call_node.block(), false);
826
+ } else if matches!(receiver_name, b"Class" | b"::Class") {
827
+ self.handle_class_definition(
828
+ &node.location(),
829
+ Some(node),
830
+ call_node.block(),
831
+ call_node.arguments().and_then(|args| args.arguments().iter().next()),
832
+ false,
833
+ );
834
+ } else {
835
+ return false;
836
+ }
837
+
838
+ self.index_method_reference_for_call(&call_node);
839
+ true
840
+ }
841
+
842
+ fn handle_mixin(&mut self, node: &ruby_prism::CallNode, kind: MixinKind) {
843
+ let Some(arguments) = node.arguments() else {
844
+ return;
845
+ };
846
+
847
+ let has_owner = self.current_owner_name_id().is_some();
848
+
849
+ let mixin_arguments = arguments
850
+ .arguments()
851
+ .iter()
852
+ .filter_map(|arg| {
853
+ if arg.as_self_node().is_some() {
854
+ if !has_owner {
855
+ self.add_diagnostic(
856
+ Rule::TopLevelMixinSelf,
857
+ Offset::from_prism_location(&arg.location()),
858
+ "Top level mixin self".to_string(),
859
+ );
860
+ return None;
861
+ }
862
+
863
+ Some((
864
+ self.current_lexical_scope_name_id().unwrap(),
865
+ Offset::from_prism_location(&arg.location()),
866
+ ))
867
+ } else if let Some(name_id) = self.index_constant_reference(&arg, false) {
868
+ Some((name_id, Offset::from_prism_location(&arg.location())))
869
+ } else {
870
+ self.add_diagnostic(
871
+ Rule::DynamicAncestor,
872
+ Offset::from_prism_location(&arg.location()),
873
+ "Dynamic mixin argument".to_string(),
874
+ );
875
+ None
876
+ }
877
+ })
878
+ .collect::<Vec<(NameId, Offset)>>();
879
+
880
+ if mixin_arguments.is_empty() || !has_owner {
881
+ return;
882
+ }
883
+
884
+ // Mixin operations with multiple arguments are inserted in reverse
885
+ for (name_id, offset) in mixin_arguments.into_iter().rev() {
886
+ self.operations
887
+ .push(Operation::ReferenceConstant(op::ReferenceConstant {
888
+ name_id,
889
+ uri_id: self.uri_id,
890
+ offset,
891
+ }));
892
+
893
+ self.operations.push(Operation::Mixin(op::Mixin {
894
+ kind,
895
+ target: Target::Constant(name_id),
896
+ }));
897
+ }
898
+ }
899
+
900
+ fn handle_constant_visibility(&mut self, node: &ruby_prism::CallNode, visibility: Visibility) {
901
+ let receiver = node.receiver();
902
+
903
+ let receiver_name_id = match receiver {
904
+ Some(ruby_prism::Node::ConstantPathNode { .. } | ruby_prism::Node::ConstantReadNode { .. }) => {
905
+ self.index_constant_reference(&receiver.unwrap(), true)
906
+ }
907
+ Some(ruby_prism::Node::SelfNode { .. }) | None => match self.nesting_stack.last() {
908
+ Some(Nesting::Method { .. }) => return,
909
+ None => {
910
+ self.add_diagnostic(
911
+ Rule::InvalidPrivateConstant,
912
+ Offset::from_prism_location(&node.location()),
913
+ "Private constant called at top level".to_string(),
914
+ );
915
+ return;
916
+ }
917
+ _ => None,
918
+ },
919
+ _ => {
920
+ self.add_diagnostic(
921
+ Rule::InvalidPrivateConstant,
922
+ Offset::from_prism_location(&node.location()),
923
+ "Dynamic receiver for private constant".to_string(),
924
+ );
925
+ return;
926
+ }
927
+ };
928
+
929
+ let Some(arguments) = node.arguments() else {
930
+ return;
931
+ };
932
+
933
+ for argument in &arguments.arguments() {
934
+ let (name, location) = match argument {
935
+ ruby_prism::Node::SymbolNode { .. } => {
936
+ let symbol = argument.as_symbol_node().unwrap();
937
+ if let Some(value_loc) = symbol.value_loc() {
938
+ (Self::location_to_string(&value_loc), value_loc)
939
+ } else {
940
+ continue;
941
+ }
942
+ }
943
+ ruby_prism::Node::StringNode { .. } => {
944
+ let string = argument.as_string_node().unwrap();
945
+ let name = String::from_utf8_lossy(string.unescaped()).to_string();
946
+ (name, argument.location())
947
+ }
948
+ _ => {
949
+ self.add_diagnostic(
950
+ Rule::InvalidPrivateConstant,
951
+ Offset::from_prism_location(&argument.location()),
952
+ "Private constant called with non-symbol argument".to_string(),
953
+ );
954
+ continue;
955
+ }
956
+ };
957
+
958
+ let str_id = self.intern_string(name);
959
+ let offset = Offset::from_prism_location(&location);
960
+
961
+ self.operations
962
+ .push(Operation::SetConstantVisibility(op::SetConstantVisibility {
963
+ receiver: receiver_name_id.map(Target::Constant),
964
+ target: str_id,
965
+ visibility,
966
+ uri_id: self.uri_id,
967
+ offset,
968
+ comments: Box::default(),
969
+ flags: DefinitionFlags::empty(),
970
+ }));
971
+ }
972
+ }
973
+
974
+ // -- Constant definition helpers --
975
+
976
+ fn add_constant_definition(
977
+ &mut self,
978
+ node: &ruby_prism::Node,
979
+ also_add_reference: bool,
980
+ promotable: bool,
981
+ ) -> Option<()> {
982
+ let name_id = self.index_constant_reference(node, also_add_reference)?;
983
+
984
+ let location = match node {
985
+ ruby_prism::Node::ConstantWriteNode { .. } => node.as_constant_write_node().unwrap().name_loc(),
986
+ ruby_prism::Node::ConstantOrWriteNode { .. } => node.as_constant_or_write_node().unwrap().name_loc(),
987
+ ruby_prism::Node::ConstantPathNode { .. } => node.as_constant_path_node().unwrap().name_loc(),
988
+ _ => node.location(),
989
+ };
990
+
991
+ let offset = Offset::from_prism_location(&location);
992
+ let (comments, mut flags) = self.find_comments_for(offset.start());
993
+ if promotable {
994
+ flags |= DefinitionFlags::PROMOTABLE;
995
+ }
996
+ self.operations.push(Operation::DefineConstant(op::DefineConstant {
997
+ name_id,
998
+ uri_id: self.uri_id,
999
+ offset,
1000
+ comments,
1001
+ flags,
1002
+ }));
1003
+
1004
+ Some(())
1005
+ }
1006
+
1007
+ fn index_constant_alias_target(&mut self, value: &ruby_prism::Node) -> Option<NameId> {
1008
+ match value {
1009
+ ruby_prism::Node::ConstantReadNode { .. } | ruby_prism::Node::ConstantPathNode { .. } => {
1010
+ self.index_constant_reference(value, true)
1011
+ }
1012
+ ruby_prism::Node::ConstantWriteNode { .. } => {
1013
+ let node = value.as_constant_write_node().unwrap();
1014
+ let target_name_id = self.index_constant_alias_target(&node.value())?;
1015
+ self.add_constant_alias_definition(value, target_name_id, false);
1016
+ Some(target_name_id)
1017
+ }
1018
+ ruby_prism::Node::ConstantOrWriteNode { .. } => {
1019
+ let node = value.as_constant_or_write_node().unwrap();
1020
+ let target_name_id = self.index_constant_alias_target(&node.value())?;
1021
+ self.add_constant_alias_definition(value, target_name_id, false);
1022
+ Some(target_name_id)
1023
+ }
1024
+ ruby_prism::Node::ConstantPathWriteNode { .. } => {
1025
+ let node = value.as_constant_path_write_node().unwrap();
1026
+ let target_name_id = self.index_constant_alias_target(&node.value())?;
1027
+ self.add_constant_alias_definition(&node.target().as_node(), target_name_id, false);
1028
+ Some(target_name_id)
1029
+ }
1030
+ ruby_prism::Node::ConstantPathOrWriteNode { .. } => {
1031
+ let node = value.as_constant_path_or_write_node().unwrap();
1032
+ let target_name_id = self.index_constant_alias_target(&node.value())?;
1033
+ self.add_constant_alias_definition(&node.target().as_node(), target_name_id, true);
1034
+ Some(target_name_id)
1035
+ }
1036
+ _ => None,
1037
+ }
1038
+ }
1039
+
1040
+ fn add_constant_alias_definition(
1041
+ &mut self,
1042
+ name_node: &ruby_prism::Node,
1043
+ target_name_id: NameId,
1044
+ also_add_reference: bool,
1045
+ ) -> Option<()> {
1046
+ let name_id = self.index_constant_reference(name_node, also_add_reference)?;
1047
+
1048
+ let location = match name_node {
1049
+ ruby_prism::Node::ConstantWriteNode { .. } => name_node.as_constant_write_node().unwrap().name_loc(),
1050
+ ruby_prism::Node::ConstantOrWriteNode { .. } => name_node.as_constant_or_write_node().unwrap().name_loc(),
1051
+ ruby_prism::Node::ConstantPathNode { .. } => name_node.as_constant_path_node().unwrap().name_loc(),
1052
+ _ => name_node.location(),
1053
+ };
1054
+
1055
+ let offset = Offset::from_prism_location(&location);
1056
+ let (comments, flags) = self.find_comments_for(offset.start());
1057
+
1058
+ self.operations.push(Operation::AliasConstant(op::AliasConstant {
1059
+ name_id,
1060
+ target_name_id,
1061
+ uri_id: self.uri_id,
1062
+ offset,
1063
+ comments,
1064
+ flags,
1065
+ }));
1066
+
1067
+ Some(())
1068
+ }
1069
+
1070
+ fn is_attr_call(arg: &ruby_prism::Node) -> bool {
1071
+ arg.as_call_node().is_some_and(|call| {
1072
+ let receiver = call.receiver();
1073
+ let bare_or_self = receiver.is_none() || receiver.as_ref().is_some_and(|r| r.as_self_node().is_some());
1074
+ bare_or_self
1075
+ && matches!(
1076
+ call.name().as_slice(),
1077
+ b"attr" | b"attr_reader" | b"attr_writer" | b"attr_accessor"
1078
+ )
1079
+ })
1080
+ }
1081
+
1082
+ fn handle_visibility_arguments(
1083
+ &mut self,
1084
+ arguments: &ruby_prism::ArgumentsNode,
1085
+ visibility: Visibility,
1086
+ call_offset: &Offset,
1087
+ call_name: &str,
1088
+ ) {
1089
+ let args = arguments.arguments();
1090
+ let arg_count = args.len();
1091
+
1092
+ for arg in &args {
1093
+ if matches!(arg, ruby_prism::Node::DefNode { .. }) || (arg_count == 1 && Self::is_attr_call(&arg)) {
1094
+ let previous_visibility = self.current_visibility().visibility;
1095
+
1096
+ self.operations
1097
+ .push(Operation::SetDefaultVisibility(op::SetDefaultVisibility {
1098
+ visibility,
1099
+ uri_id: self.uri_id,
1100
+ offset: call_offset.clone(),
1101
+ }));
1102
+
1103
+ self.visibility_stack
1104
+ .push(VisibilityModifier::new(visibility, true, call_offset.clone()));
1105
+ self.visit(&arg);
1106
+ self.visibility_stack.pop();
1107
+
1108
+ self.operations
1109
+ .push(Operation::SetDefaultVisibility(op::SetDefaultVisibility {
1110
+ visibility: previous_visibility,
1111
+ uri_id: self.uri_id,
1112
+ offset: call_offset.clone(),
1113
+ }));
1114
+ } else if matches!(
1115
+ arg,
1116
+ ruby_prism::Node::SymbolNode { .. } | ruby_prism::Node::StringNode { .. }
1117
+ ) {
1118
+ self.create_method_visibility_operation(&arg, visibility, DefinitionFlags::empty());
1119
+ } else {
1120
+ let arg_offset = Offset::from_prism_location(&arg.location());
1121
+ let message = if Self::is_attr_call(&arg) {
1122
+ format!("`{call_name}` with `attr_*` is only supported as a single argument")
1123
+ } else {
1124
+ format!("`{call_name}` called with a non-literal argument")
1125
+ };
1126
+ self.add_diagnostic(Rule::InvalidMethodVisibility, arg_offset, message);
1127
+ self.visit(&arg);
1128
+ }
1129
+ }
1130
+ }
1131
+
1132
+ fn create_method_visibility_operation(
1133
+ &mut self,
1134
+ arg: &ruby_prism::Node,
1135
+ visibility: Visibility,
1136
+ flags: DefinitionFlags,
1137
+ ) {
1138
+ let (name, location) = match arg {
1139
+ ruby_prism::Node::SymbolNode { .. } => {
1140
+ let symbol = arg.as_symbol_node().unwrap();
1141
+ if let Some(value_loc) = symbol.value_loc() {
1142
+ (Self::location_to_string(&value_loc), value_loc)
1143
+ } else {
1144
+ return;
1145
+ }
1146
+ }
1147
+ ruby_prism::Node::StringNode { .. } => {
1148
+ let string = arg.as_string_node().unwrap();
1149
+ let name = String::from_utf8_lossy(string.unescaped()).to_string();
1150
+ (name, arg.location())
1151
+ }
1152
+ _ => return,
1153
+ };
1154
+
1155
+ let str_id = self.intern_string(format!("{name}()"));
1156
+ let offset = Offset::from_prism_location(&location);
1157
+
1158
+ self.operations
1159
+ .push(Operation::SetMethodVisibility(op::SetMethodVisibility {
1160
+ str_id,
1161
+ visibility,
1162
+ uri_id: self.uri_id,
1163
+ offset,
1164
+ flags,
1165
+ }));
1166
+ }
1167
+
1168
+ fn create_method_visibility_operation_from_name(
1169
+ &mut self,
1170
+ name: &str,
1171
+ location: &ruby_prism::Location,
1172
+ visibility: Visibility,
1173
+ flags: DefinitionFlags,
1174
+ ) {
1175
+ let str_id = self.intern_string(format!("{name}()"));
1176
+ let offset = Offset::from_prism_location(location);
1177
+
1178
+ self.operations
1179
+ .push(Operation::SetMethodVisibility(op::SetMethodVisibility {
1180
+ str_id,
1181
+ visibility,
1182
+ uri_id: self.uri_id,
1183
+ offset,
1184
+ flags,
1185
+ }));
1186
+ }
1187
+
1188
+ #[allow(clippy::too_many_lines)]
1189
+ fn handle_singleton_method_visibility(
1190
+ &mut self,
1191
+ node: &ruby_prism::CallNode,
1192
+ visibility: Visibility,
1193
+ call_name: &str,
1194
+ ) {
1195
+ match node.receiver() {
1196
+ Some(ruby_prism::Node::SelfNode { .. }) | None => match self.nesting_stack.last() {
1197
+ Some(Nesting::Method { .. }) => {
1198
+ self.visit_call_node_parts(node);
1199
+ return;
1200
+ }
1201
+ None => {
1202
+ self.add_diagnostic(
1203
+ Rule::InvalidMethodVisibility,
1204
+ Offset::from_prism_location(&node.location()),
1205
+ format!("`{call_name}` called at top level"),
1206
+ );
1207
+ self.visit_call_node_parts(node);
1208
+ return;
1209
+ }
1210
+ _ => {}
1211
+ },
1212
+ _ => {
1213
+ self.visit_call_node_parts(node);
1214
+ return;
1215
+ }
1216
+ }
1217
+
1218
+ let Some(arguments) = node.arguments() else {
1219
+ return;
1220
+ };
1221
+
1222
+ let args = arguments.arguments();
1223
+ let arg_count = args.len();
1224
+
1225
+ for argument in &args {
1226
+ match argument {
1227
+ ruby_prism::Node::SymbolNode { .. } | ruby_prism::Node::StringNode { .. } => {
1228
+ self.create_method_visibility_operation(
1229
+ &argument,
1230
+ visibility,
1231
+ DefinitionFlags::SINGLETON_METHOD_VISIBILITY,
1232
+ );
1233
+ }
1234
+ ruby_prism::Node::ArrayNode { .. } if arg_count == 1 => {
1235
+ let array = argument.as_array_node().unwrap();
1236
+ for element in &array.elements() {
1237
+ match element {
1238
+ ruby_prism::Node::SymbolNode { .. } | ruby_prism::Node::StringNode { .. } => {
1239
+ self.create_method_visibility_operation(
1240
+ &element,
1241
+ visibility,
1242
+ DefinitionFlags::SINGLETON_METHOD_VISIBILITY,
1243
+ );
1244
+ }
1245
+ ruby_prism::Node::DefNode { .. } => {
1246
+ let def_node = element.as_def_node().unwrap();
1247
+ if def_node.receiver().is_none() {
1248
+ self.add_diagnostic(
1249
+ Rule::InvalidMethodVisibility,
1250
+ Offset::from_prism_location(&element.location()),
1251
+ format!("`{call_name}` requires a singleton method definition"),
1252
+ );
1253
+ self.visit(&element);
1254
+ continue;
1255
+ }
1256
+ let name_loc = def_node.name_loc();
1257
+ let name = Self::location_to_string(&name_loc);
1258
+ self.create_method_visibility_operation_from_name(
1259
+ &name,
1260
+ &name_loc,
1261
+ visibility,
1262
+ DefinitionFlags::SINGLETON_METHOD_VISIBILITY,
1263
+ );
1264
+ self.visit(&element);
1265
+ }
1266
+ _ => {
1267
+ self.add_diagnostic(
1268
+ Rule::InvalidMethodVisibility,
1269
+ Offset::from_prism_location(&element.location()),
1270
+ format!(
1271
+ "`{call_name}` array element must be a Symbol, String, or method definition"
1272
+ ),
1273
+ );
1274
+ self.visit(&element);
1275
+ }
1276
+ }
1277
+ }
1278
+ }
1279
+ ruby_prism::Node::DefNode { .. } => {
1280
+ let def_node = argument.as_def_node().unwrap();
1281
+ if def_node.receiver().is_none() {
1282
+ self.add_diagnostic(
1283
+ Rule::InvalidMethodVisibility,
1284
+ Offset::from_prism_location(&argument.location()),
1285
+ format!("`{call_name}` requires a singleton method definition"),
1286
+ );
1287
+ self.visit(&argument);
1288
+ continue;
1289
+ }
1290
+ let name_loc = def_node.name_loc();
1291
+ let name = Self::location_to_string(&name_loc);
1292
+ self.create_method_visibility_operation_from_name(
1293
+ &name,
1294
+ &name_loc,
1295
+ visibility,
1296
+ DefinitionFlags::SINGLETON_METHOD_VISIBILITY,
1297
+ );
1298
+ self.visit(&argument);
1299
+ }
1300
+ arg if Self::is_attr_call(&arg) => {
1301
+ self.add_diagnostic(
1302
+ Rule::InvalidMethodVisibility,
1303
+ Offset::from_prism_location(&arg.location()),
1304
+ format!("`{call_name}` does not accept `attr_*` arguments"),
1305
+ );
1306
+ self.visit(&arg);
1307
+ }
1308
+ ruby_prism::Node::ArrayNode { .. } => {
1309
+ self.add_diagnostic(
1310
+ Rule::InvalidMethodVisibility,
1311
+ Offset::from_prism_location(&argument.location()),
1312
+ format!("`{call_name}` array argument must be the only argument"),
1313
+ );
1314
+ self.visit(&argument);
1315
+ }
1316
+ _ => {
1317
+ self.add_diagnostic(
1318
+ Rule::InvalidMethodVisibility,
1319
+ Offset::from_prism_location(&argument.location()),
1320
+ format!("`{call_name}` called with a non-literal argument"),
1321
+ );
1322
+ self.visit(&argument);
1323
+ }
1324
+ }
1325
+ }
1326
+ }
1327
+
1328
+ fn add_global_variable_definition(&mut self, location: &ruby_prism::Location) {
1329
+ let name = Self::location_to_string(location);
1330
+ let str_id = self.intern_string(name);
1331
+ let offset = Offset::from_prism_location(location);
1332
+ let (comments, flags) = self.find_comments_for(offset.start());
1333
+
1334
+ self.operations
1335
+ .push(Operation::DefineGlobalVariable(op::DefineGlobalVariable {
1336
+ str_id,
1337
+ uri_id: self.uri_id,
1338
+ offset,
1339
+ comments,
1340
+ flags,
1341
+ }));
1342
+ }
1343
+
1344
+ fn add_instance_variable_definition(&mut self, location: &ruby_prism::Location) {
1345
+ let name = Self::location_to_string(location);
1346
+ let str_id = self.intern_string(name);
1347
+ let offset = Offset::from_prism_location(location);
1348
+ let (comments, flags) = self.find_comments_for(offset.start());
1349
+
1350
+ self.operations
1351
+ .push(Operation::DefineInstanceVariable(op::DefineInstanceVariable {
1352
+ str_id,
1353
+ uri_id: self.uri_id,
1354
+ offset,
1355
+ comments,
1356
+ flags,
1357
+ }));
1358
+ }
1359
+
1360
+ fn add_class_variable_definition(&mut self, location: &ruby_prism::Location) {
1361
+ let name = Self::location_to_string(location);
1362
+ let str_id = self.intern_string(name);
1363
+ let offset = Offset::from_prism_location(location);
1364
+ let (comments, flags) = self.find_comments_for(offset.start());
1365
+
1366
+ self.operations
1367
+ .push(Operation::DefineClassVariable(op::DefineClassVariable {
1368
+ str_id,
1369
+ uri_id: self.uri_id,
1370
+ offset,
1371
+ comments,
1372
+ flags,
1373
+ }));
1374
+ }
1375
+ }
1376
+
1377
+ struct CommentGroup {
1378
+ end_offset: usize,
1379
+ comments: Vec<Comment>,
1380
+ deprecated: bool,
1381
+ }
1382
+
1383
+ impl CommentGroup {
1384
+ #[must_use]
1385
+ pub fn new() -> Self {
1386
+ Self {
1387
+ end_offset: 0,
1388
+ comments: Vec::new(),
1389
+ deprecated: false,
1390
+ }
1391
+ }
1392
+
1393
+ fn accepts(&self, next: &ruby_prism::Comment, source: &str) -> bool {
1394
+ let current_end_offset = self.end_offset;
1395
+ let next_line_start_offset = next.location().start_offset();
1396
+ let between = &source.as_bytes()[current_end_offset..next_line_start_offset];
1397
+ if !between.iter().all(|&b| b.is_ascii_whitespace()) {
1398
+ return false;
1399
+ }
1400
+ bytecount::count(between, b'\n') <= 1
1401
+ }
1402
+
1403
+ fn add_comment(&mut self, comment: &ruby_prism::Comment) {
1404
+ self.end_offset = comment.location().end_offset();
1405
+ let text = String::from_utf8_lossy(comment.location().as_slice()).to_string();
1406
+ if text.lines().any(|line| line.starts_with("# @deprecated")) {
1407
+ self.deprecated = true;
1408
+ }
1409
+ self.comments.push(Comment::new(
1410
+ Offset::from_prism_location(&comment.location()),
1411
+ text.trim().to_string(),
1412
+ ));
1413
+ }
1414
+
1415
+ fn comments(&self) -> Box<[Comment]> {
1416
+ self.comments.clone().into_boxed_slice()
1417
+ }
1418
+
1419
+ fn flags(&self) -> DefinitionFlags {
1420
+ if self.deprecated {
1421
+ DefinitionFlags::DEPRECATED
1422
+ } else {
1423
+ DefinitionFlags::empty()
1424
+ }
1425
+ }
1426
+ }
1427
+
1428
+ // -- Visit implementation --
1429
+
1430
+ impl Visit<'_> for RubyOperationBuilder<'_> {
1431
+ fn visit_class_node(&mut self, node: &ruby_prism::ClassNode<'_>) {
1432
+ self.handle_class_definition(
1433
+ &node.location(),
1434
+ Some(&node.constant_path()),
1435
+ node.body(),
1436
+ node.superclass(),
1437
+ true,
1438
+ );
1439
+ }
1440
+
1441
+ fn visit_module_node(&mut self, node: &ruby_prism::ModuleNode) {
1442
+ self.handle_module_definition(&node.location(), Some(&node.constant_path()), node.body(), true);
1443
+ }
1444
+
1445
+ fn visit_singleton_class_node(&mut self, node: &ruby_prism::SingletonClassNode) {
1446
+ let expression = node.expression();
1447
+
1448
+ let (attached_target, name_offset) = if expression.as_self_node().is_some() {
1449
+ (
1450
+ self.current_lexical_scope_name_id(),
1451
+ Offset::from_prism_location(&expression.location()),
1452
+ )
1453
+ } else if matches!(
1454
+ expression,
1455
+ ruby_prism::Node::ConstantPathNode { .. } | ruby_prism::Node::ConstantReadNode { .. }
1456
+ ) {
1457
+ (
1458
+ self.index_constant_reference(&expression, true),
1459
+ Offset::from_prism_location(&expression.location()),
1460
+ )
1461
+ } else {
1462
+ self.visit(&expression);
1463
+ self.add_diagnostic(
1464
+ Rule::DynamicSingletonDefinition,
1465
+ Offset::from_prism_location(&node.location()),
1466
+ "Dynamic singleton class definition".to_string(),
1467
+ );
1468
+ return;
1469
+ };
1470
+
1471
+ let Some(attached_target) = attached_target else {
1472
+ self.add_diagnostic(
1473
+ Rule::DynamicSingletonDefinition,
1474
+ Offset::from_prism_location(&node.location()),
1475
+ "Dynamic singleton class definition".to_string(),
1476
+ );
1477
+ return;
1478
+ };
1479
+
1480
+ let offset = Offset::from_prism_location(&node.location());
1481
+ let (comments, flags) = self.find_comments_for(offset.start());
1482
+
1483
+ let singleton_class_name = {
1484
+ let name = self
1485
+ .names
1486
+ .get(&attached_target)
1487
+ .expect("Attached target name should exist");
1488
+ let target_str = self
1489
+ .strings
1490
+ .get(name.str())
1491
+ .expect("Attached target string should exist");
1492
+ format!("<{}>", target_str.as_str())
1493
+ };
1494
+
1495
+ let string_id = self.intern_string(singleton_class_name);
1496
+ let nesting = self.current_lexical_scope_name_id();
1497
+ let name_id = self.add_name(Name::new(string_id, ParentScope::Attached(attached_target), nesting));
1498
+ self.operations
1499
+ .push(Operation::EnterSingletonClass(op::EnterSingletonClass {
1500
+ name_id,
1501
+ uri_id: self.uri_id,
1502
+ offset: offset.clone(),
1503
+ name_offset,
1504
+ comments,
1505
+ flags,
1506
+ }));
1507
+
1508
+ self.nesting_stack.push(Nesting::LexicalScope {
1509
+ name_id,
1510
+ is_module: false,
1511
+ });
1512
+ self.visibility_stack
1513
+ .push(VisibilityModifier::new(Visibility::Public, false, offset));
1514
+ if let Some(body) = node.body() {
1515
+ self.visit(&body);
1516
+ }
1517
+ self.visibility_stack.pop();
1518
+ self.nesting_stack.pop();
1519
+ self.operations.push(Operation::ExitScope);
1520
+ }
1521
+
1522
+ #[allow(clippy::too_many_lines)]
1523
+ fn visit_def_node(&mut self, node: &ruby_prism::DefNode) {
1524
+ let name = Self::location_to_string(&node.name_loc());
1525
+ let str_id = self.intern_string(format!("{name}()"));
1526
+ let offset = Offset::from_prism_location(&node.location());
1527
+ let parameters = self.collect_parameters(node);
1528
+ let is_singleton = node.receiver().is_some();
1529
+
1530
+ let current_visibility = self.current_visibility();
1531
+ let visibility = if is_singleton {
1532
+ Visibility::Public
1533
+ } else {
1534
+ current_visibility.visibility
1535
+ };
1536
+ let offset_for_comments = if is_singleton {
1537
+ offset.clone()
1538
+ } else if current_visibility.is_inline {
1539
+ current_visibility.offset.clone()
1540
+ } else {
1541
+ offset.clone()
1542
+ };
1543
+
1544
+ let comment_offset = self
1545
+ .take_decorator_offset(offset_for_comments.start())
1546
+ .unwrap_or_else(|| offset_for_comments.start());
1547
+ let (comments, flags) = self.find_comments_for(comment_offset);
1548
+
1549
+ let (receiver, method_nesting_receiver) = if let Some(recv_node) = node.receiver() {
1550
+ match recv_node {
1551
+ ruby_prism::Node::SelfNode { .. } => {
1552
+ let nesting_name = self.current_owner_name_id();
1553
+ (
1554
+ Some(Target::ExplicitSelf),
1555
+ nesting_name.map(NestingReceiver::SelfReceiver),
1556
+ )
1557
+ }
1558
+ ruby_prism::Node::ConstantPathNode { .. } | ruby_prism::Node::ConstantReadNode { .. } => {
1559
+ let name_id = self.index_constant_reference(&recv_node, true);
1560
+ (
1561
+ name_id.map(Target::Constant),
1562
+ name_id.map(NestingReceiver::ConstantReceiver),
1563
+ )
1564
+ }
1565
+ _ => {
1566
+ self.add_diagnostic(
1567
+ Rule::DynamicSingletonDefinition,
1568
+ Offset::from_prism_location(&node.location()),
1569
+ "Dynamic receiver for singleton method definition".to_string(),
1570
+ );
1571
+ self.visit(&recv_node);
1572
+ return;
1573
+ }
1574
+ }
1575
+ } else {
1576
+ (None, None)
1577
+ };
1578
+
1579
+ if receiver.is_none() && visibility == Visibility::ModuleFunction {
1580
+ // module_function: emit two EnterMethod/ExitScope pairs (singleton + instance),
1581
+ // each visiting the body so ivars are associated with both methods.
1582
+ let singleton_receiver = Some(Target::ExplicitSelf);
1583
+ let body = node.body();
1584
+
1585
+ self.operations.push(Operation::EnterMethod(op::EnterMethod {
1586
+ str_id,
1587
+ uri_id: self.uri_id,
1588
+ offset: offset.clone(),
1589
+ comments: comments.clone(),
1590
+ flags: flags.clone(),
1591
+ signatures: Signatures::Simple(parameters.clone().into_boxed_slice()),
1592
+ receiver: singleton_receiver,
1593
+ }));
1594
+ self.nesting_stack.push(Nesting::Method {
1595
+ receiver: method_nesting_receiver,
1596
+ });
1597
+ if let Some(ref body) = body {
1598
+ self.visit(body);
1599
+ }
1600
+ self.nesting_stack.pop();
1601
+ self.operations.push(Operation::ExitScope);
1602
+
1603
+ self.operations.push(Operation::EnterMethod(op::EnterMethod {
1604
+ str_id,
1605
+ uri_id: self.uri_id,
1606
+ offset: offset.clone(),
1607
+ comments,
1608
+ flags,
1609
+ signatures: Signatures::Simple(parameters.into_boxed_slice()),
1610
+ receiver,
1611
+ }));
1612
+ self.nesting_stack.push(Nesting::Method {
1613
+ receiver: method_nesting_receiver,
1614
+ });
1615
+ if let Some(ref body) = body {
1616
+ self.visit(body);
1617
+ }
1618
+ self.nesting_stack.pop();
1619
+ self.operations.push(Operation::ExitScope);
1620
+ } else {
1621
+ // Singleton methods at top level have receiver=None (no class to point self to).
1622
+ // Bracket with SetDefaultVisibility(Public) so the applier assigns the correct visibility.
1623
+ let needs_singleton_visibility_bracket = is_singleton && receiver.is_none();
1624
+ let previous_visibility = if needs_singleton_visibility_bracket {
1625
+ let prev = self.current_visibility().visibility;
1626
+ self.operations
1627
+ .push(Operation::SetDefaultVisibility(op::SetDefaultVisibility {
1628
+ visibility: Visibility::Public,
1629
+ uri_id: self.uri_id,
1630
+ offset: offset.clone(),
1631
+ }));
1632
+ Some(prev)
1633
+ } else {
1634
+ None
1635
+ };
1636
+
1637
+ self.operations.push(Operation::EnterMethod(op::EnterMethod {
1638
+ str_id,
1639
+ uri_id: self.uri_id,
1640
+ offset: offset.clone(),
1641
+ comments,
1642
+ flags,
1643
+ signatures: Signatures::Simple(parameters.into_boxed_slice()),
1644
+ receiver,
1645
+ }));
1646
+ self.nesting_stack.push(Nesting::Method {
1647
+ receiver: method_nesting_receiver,
1648
+ });
1649
+ if let Some(body) = node.body() {
1650
+ self.visit(&body);
1651
+ }
1652
+ self.nesting_stack.pop();
1653
+ self.operations.push(Operation::ExitScope);
1654
+
1655
+ if let Some(prev) = previous_visibility {
1656
+ self.operations
1657
+ .push(Operation::SetDefaultVisibility(op::SetDefaultVisibility {
1658
+ visibility: prev,
1659
+ uri_id: self.uri_id,
1660
+ offset: offset.clone(),
1661
+ }));
1662
+ }
1663
+ }
1664
+ }
1665
+
1666
+ fn visit_constant_and_write_node(&mut self, node: &ruby_prism::ConstantAndWriteNode) {
1667
+ self.index_constant_reference(&node.as_node(), true);
1668
+ self.visit(&node.value());
1669
+ }
1670
+
1671
+ fn visit_constant_operator_write_node(&mut self, node: &ruby_prism::ConstantOperatorWriteNode) {
1672
+ self.index_constant_reference(&node.as_node(), true);
1673
+ self.visit(&node.value());
1674
+ }
1675
+
1676
+ fn visit_constant_or_write_node(&mut self, node: &ruby_prism::ConstantOrWriteNode) {
1677
+ if let Some(target_name_id) = self.index_constant_alias_target(&node.value()) {
1678
+ self.add_constant_alias_definition(&node.as_node(), target_name_id, true);
1679
+ } else {
1680
+ self.add_constant_definition(&node.as_node(), true, Self::is_promotable_value(&node.value()));
1681
+ self.visit(&node.value());
1682
+ }
1683
+ }
1684
+
1685
+ fn visit_constant_write_node(&mut self, node: &ruby_prism::ConstantWriteNode) {
1686
+ let value = node.value();
1687
+ if self.handle_dynamic_class_or_module(&node.as_node(), &value) {
1688
+ return;
1689
+ }
1690
+
1691
+ if let Some(target_name_id) = self.index_constant_alias_target(&value) {
1692
+ self.add_constant_alias_definition(&node.as_node(), target_name_id, false);
1693
+ } else {
1694
+ self.add_constant_definition(&node.as_node(), false, Self::is_promotable_value(&value));
1695
+ self.visit(&value);
1696
+ }
1697
+ }
1698
+
1699
+ fn visit_constant_path_and_write_node(&mut self, node: &ruby_prism::ConstantPathAndWriteNode) {
1700
+ self.visit_constant_path_node(&node.target());
1701
+ self.visit(&node.value());
1702
+ }
1703
+
1704
+ fn visit_constant_path_operator_write_node(&mut self, node: &ruby_prism::ConstantPathOperatorWriteNode) {
1705
+ self.visit_constant_path_node(&node.target());
1706
+ self.visit(&node.value());
1707
+ }
1708
+
1709
+ fn visit_constant_path_or_write_node(&mut self, node: &ruby_prism::ConstantPathOrWriteNode) {
1710
+ if let Some(target_name_id) = self.index_constant_alias_target(&node.value()) {
1711
+ self.add_constant_alias_definition(&node.target().as_node(), target_name_id, true);
1712
+ } else {
1713
+ self.add_constant_definition(&node.target().as_node(), true, Self::is_promotable_value(&node.value()));
1714
+ self.visit(&node.value());
1715
+ }
1716
+ }
1717
+
1718
+ fn visit_constant_path_write_node(&mut self, node: &ruby_prism::ConstantPathWriteNode) {
1719
+ let value = node.value();
1720
+ if self.handle_dynamic_class_or_module(&node.as_node(), &value) {
1721
+ return;
1722
+ }
1723
+
1724
+ if let Some(target_name_id) = self.index_constant_alias_target(&value) {
1725
+ self.add_constant_alias_definition(&node.target().as_node(), target_name_id, false);
1726
+ } else {
1727
+ self.add_constant_definition(&node.target().as_node(), false, Self::is_promotable_value(&value));
1728
+ self.visit(&value);
1729
+ }
1730
+ }
1731
+
1732
+ fn visit_constant_read_node(&mut self, node: &ruby_prism::ConstantReadNode<'_>) {
1733
+ self.index_constant_reference(&node.as_node(), true);
1734
+ }
1735
+
1736
+ fn visit_constant_path_node(&mut self, node: &ruby_prism::ConstantPathNode<'_>) {
1737
+ self.index_constant_reference(&node.as_node(), true);
1738
+ }
1739
+
1740
+ fn visit_multi_write_node(&mut self, node: &ruby_prism::MultiWriteNode) {
1741
+ for left in &node.lefts() {
1742
+ match left {
1743
+ ruby_prism::Node::ConstantTargetNode { .. } | ruby_prism::Node::ConstantPathTargetNode { .. } => {
1744
+ self.add_constant_definition(&left, false, true);
1745
+ }
1746
+ ruby_prism::Node::GlobalVariableTargetNode { .. } => {
1747
+ self.add_global_variable_definition(&left.location());
1748
+ }
1749
+ ruby_prism::Node::InstanceVariableTargetNode { .. } => {
1750
+ self.add_instance_variable_definition(&left.location());
1751
+ }
1752
+ ruby_prism::Node::ClassVariableTargetNode { .. } => {
1753
+ self.add_class_variable_definition(&left.location());
1754
+ }
1755
+ ruby_prism::Node::CallTargetNode { .. } => {
1756
+ let call_target_node = left.as_call_target_node().unwrap();
1757
+ let method_receiver = self.method_receiver(Some(&call_target_node.receiver()), left.location());
1758
+
1759
+ if method_receiver.is_none() {
1760
+ self.visit(&call_target_node.receiver());
1761
+ }
1762
+
1763
+ let name = String::from_utf8_lossy(call_target_node.name().as_slice()).to_string();
1764
+ self.index_method_reference(name, &call_target_node.location(), method_receiver);
1765
+ }
1766
+ _ => {}
1767
+ }
1768
+ }
1769
+
1770
+ self.visit(&node.value());
1771
+ }
1772
+
1773
+ #[allow(clippy::too_many_lines)]
1774
+ fn visit_call_node(&mut self, node: &ruby_prism::CallNode) {
1775
+ let index_attr = |kind: AttrKind, call: &ruby_prism::CallNode, builder: &mut Self| {
1776
+ let receiver = call.receiver();
1777
+ if receiver.is_some() && receiver.unwrap().as_self_node().is_none() {
1778
+ return;
1779
+ }
1780
+
1781
+ let call_offset = Offset::from_prism_location(&call.location());
1782
+
1783
+ let current_visibility = builder.current_visibility();
1784
+ let offset_for_comments = if current_visibility.is_inline {
1785
+ current_visibility.offset.clone()
1786
+ } else {
1787
+ call_offset
1788
+ };
1789
+
1790
+ let comment_offset = builder
1791
+ .take_decorator_offset(offset_for_comments.start())
1792
+ .unwrap_or_else(|| offset_for_comments.start());
1793
+
1794
+ Self::each_string_or_symbol_arg(call, |name, location| {
1795
+ let str_id = builder.intern_string(format!("{name}()"));
1796
+ let offset = Offset::from_prism_location(&location);
1797
+ let (comments, flags) = builder.find_comments_for(comment_offset);
1798
+
1799
+ builder.operations.push(Operation::DefineAttribute(op::DefineAttribute {
1800
+ kind,
1801
+ str_id,
1802
+ uri_id: builder.uri_id,
1803
+ offset,
1804
+ comments,
1805
+ flags,
1806
+ }));
1807
+ });
1808
+ };
1809
+
1810
+ let message_loc = node.message_loc();
1811
+ if message_loc.is_none() {
1812
+ return;
1813
+ }
1814
+
1815
+ let message = String::from_utf8_lossy(node.name().as_slice()).to_string();
1816
+
1817
+ match message.as_str() {
1818
+ "attr_accessor" => {
1819
+ index_attr(AttrKind::Accessor, node, self);
1820
+ }
1821
+ "attr_reader" => {
1822
+ index_attr(AttrKind::Reader, node, self);
1823
+ }
1824
+ "attr_writer" => {
1825
+ index_attr(AttrKind::Writer, node, self);
1826
+ }
1827
+ "attr" => {
1828
+ let create_writer = if let Some(arguments) = node.arguments() {
1829
+ let args_vec: Vec<_> = arguments.arguments().iter().collect();
1830
+ matches!(args_vec.as_slice(), [_, ruby_prism::Node::TrueNode { .. }])
1831
+ } else {
1832
+ false
1833
+ };
1834
+
1835
+ if create_writer {
1836
+ index_attr(AttrKind::Accessor, node, self);
1837
+ } else {
1838
+ index_attr(AttrKind::Reader, node, self);
1839
+ }
1840
+ }
1841
+ "alias_method" => {
1842
+ let recv_node = node.receiver();
1843
+ let recv_ref = recv_node.as_ref();
1844
+ if recv_ref.is_some_and(|recv| {
1845
+ !matches!(
1846
+ recv,
1847
+ ruby_prism::Node::SelfNode { .. }
1848
+ | ruby_prism::Node::ConstantReadNode { .. }
1849
+ | ruby_prism::Node::ConstantPathNode { .. }
1850
+ )
1851
+ }) {
1852
+ self.visit_call_node_parts(node);
1853
+ return;
1854
+ }
1855
+
1856
+ let mut names: Vec<(String, Offset)> = Vec::new();
1857
+ Self::each_string_or_symbol_arg(node, |name, location| {
1858
+ names.push((name, Offset::from_prism_location(&location)));
1859
+ });
1860
+
1861
+ if names.len() != 2 {
1862
+ return;
1863
+ }
1864
+
1865
+ let (new_name, _new_offset) = &names[0];
1866
+ let (old_name, old_offset) = &names[1];
1867
+
1868
+ let new_name_str_id = self.intern_string(format!("{new_name}()"));
1869
+ let old_name_str_id = self.intern_string(format!("{old_name}()"));
1870
+
1871
+ let (receiver, method_receiver) = match recv_ref {
1872
+ Some(
1873
+ recv @ (ruby_prism::Node::ConstantPathNode { .. } | ruby_prism::Node::ConstantReadNode { .. }),
1874
+ ) => {
1875
+ let name_id = self.index_constant_reference(recv, true);
1876
+ (name_id.map(Target::Constant), name_id)
1877
+ }
1878
+ _ => (None, self.method_receiver(recv_ref, node.location())),
1879
+ };
1880
+
1881
+ let ref_str_id = self.intern_string(format!("{old_name}()"));
1882
+ self.operations.push(Operation::ReferenceMethod(op::ReferenceMethod {
1883
+ str_id: ref_str_id,
1884
+ uri_id: self.uri_id,
1885
+ offset: old_offset.clone(),
1886
+ receiver: method_receiver.map(Target::Constant),
1887
+ }));
1888
+
1889
+ let offset = Offset::from_prism_location(&node.location());
1890
+ let (comments, flags) = self.find_comments_for(offset.start());
1891
+
1892
+ self.operations.push(Operation::AliasMethod(op::AliasMethod {
1893
+ new_name_str_id,
1894
+ old_name_str_id,
1895
+ uri_id: self.uri_id,
1896
+ offset,
1897
+ comments,
1898
+ flags,
1899
+ receiver,
1900
+ }));
1901
+ }
1902
+ "include" => {
1903
+ let receiver = node.receiver();
1904
+ if receiver.is_none() || receiver.as_ref().is_some_and(|r| r.as_self_node().is_some()) {
1905
+ self.handle_mixin(node, MixinKind::Include);
1906
+ } else {
1907
+ self.visit_call_node_parts(node);
1908
+ }
1909
+ }
1910
+ "prepend" => {
1911
+ let receiver = node.receiver();
1912
+ if receiver.is_none() || receiver.as_ref().is_some_and(|r| r.as_self_node().is_some()) {
1913
+ self.handle_mixin(node, MixinKind::Prepend);
1914
+ } else {
1915
+ self.visit_call_node_parts(node);
1916
+ }
1917
+ }
1918
+ "extend" => {
1919
+ let receiver = node.receiver();
1920
+ if receiver.is_none() || receiver.as_ref().is_some_and(|r| r.as_self_node().is_some()) {
1921
+ self.handle_mixin(node, MixinKind::Extend);
1922
+ } else {
1923
+ self.visit_call_node_parts(node);
1924
+ }
1925
+ }
1926
+ "private" | "protected" | "public" | "module_function" => {
1927
+ if node.receiver().is_some() {
1928
+ let offset = Offset::from_prism_location(&node.location());
1929
+ self.add_diagnostic(
1930
+ Rule::InvalidMethodVisibility,
1931
+ offset,
1932
+ format!("`{message}` cannot be called with an explicit receiver"),
1933
+ );
1934
+ self.visit_call_node_parts(node);
1935
+ return;
1936
+ }
1937
+
1938
+ let visibility = Visibility::from_string(message.as_str());
1939
+ let offset = Offset::from_prism_location(&node.location());
1940
+
1941
+ if let Some(arguments) = node.arguments() {
1942
+ if visibility == Visibility::ModuleFunction && !self.current_nesting_is_module() {
1943
+ self.add_diagnostic(
1944
+ Rule::InvalidMethodVisibility,
1945
+ offset,
1946
+ "`module_function` can only be used in modules".to_string(),
1947
+ );
1948
+ self.visit_arguments_node(&arguments);
1949
+ } else {
1950
+ self.handle_visibility_arguments(&arguments, visibility, &offset, &message);
1951
+ }
1952
+ } else {
1953
+ let last_visibility = self.visibility_stack.last_mut().unwrap();
1954
+ *last_visibility = VisibilityModifier::new(visibility, false, offset);
1955
+ self.operations
1956
+ .push(Operation::SetDefaultVisibility(op::SetDefaultVisibility {
1957
+ visibility,
1958
+ uri_id: self.uri_id,
1959
+ offset: Offset::from_prism_location(&node.location()),
1960
+ }));
1961
+ }
1962
+ }
1963
+ "new" => {
1964
+ let receiver_name = node.receiver().map(|r| r.location().as_slice());
1965
+
1966
+ if matches!(receiver_name, Some(b"Class" | b"::Class")) {
1967
+ self.handle_class_definition(
1968
+ &node.location(),
1969
+ None,
1970
+ node.block(),
1971
+ node.arguments().and_then(|args| args.arguments().iter().next()),
1972
+ false,
1973
+ );
1974
+ } else if matches!(receiver_name, Some(b"Module" | b"::Module")) {
1975
+ self.handle_module_definition(&node.location(), None, node.block(), false);
1976
+ } else {
1977
+ if let Some(arguments) = node.arguments() {
1978
+ self.visit_arguments_node(&arguments);
1979
+ }
1980
+ if let Some(block) = node.block() {
1981
+ self.visit(&block);
1982
+ }
1983
+ }
1984
+
1985
+ self.index_method_reference_for_call(node);
1986
+ }
1987
+ "sig"
1988
+ if node.receiver().is_none()
1989
+ || matches!(
1990
+ node.receiver(),
1991
+ Some(ruby_prism::Node::ConstantPathNode { .. } | ruby_prism::Node::ConstantReadNode { .. })
1992
+ ) =>
1993
+ {
1994
+ self.pending_decorator_offset = Some(Offset::from_prism_location(&node.location()));
1995
+
1996
+ if let Some(arguments) = node.arguments() {
1997
+ self.visit_arguments_node(&arguments);
1998
+ }
1999
+ if let Some(block) = node.block() {
2000
+ self.visit(&block);
2001
+ }
2002
+ self.index_method_reference_for_call(node);
2003
+ }
2004
+ "private_constant" => {
2005
+ self.handle_constant_visibility(node, Visibility::Private);
2006
+ }
2007
+ "public_constant" => {
2008
+ self.handle_constant_visibility(node, Visibility::Public);
2009
+ }
2010
+ "private_class_method" => {
2011
+ self.handle_singleton_method_visibility(node, Visibility::Private, "private_class_method");
2012
+ }
2013
+ "public_class_method" => {
2014
+ self.handle_singleton_method_visibility(node, Visibility::Public, "public_class_method");
2015
+ }
2016
+ _ => {
2017
+ if let Some(arguments) = node.arguments() {
2018
+ self.visit_arguments_node(&arguments);
2019
+ }
2020
+ if let Some(block) = node.block() {
2021
+ self.visit(&block);
2022
+ }
2023
+
2024
+ let method_receiver = self.method_receiver(node.receiver().as_ref(), node.location());
2025
+
2026
+ if method_receiver.is_none()
2027
+ && let Some(receiver) = node.receiver()
2028
+ {
2029
+ self.visit(&receiver);
2030
+ }
2031
+
2032
+ self.index_method_reference(message.clone(), &node.message_loc().unwrap(), method_receiver);
2033
+
2034
+ match message.as_str() {
2035
+ ">" | "<" | ">=" | "<=" => {
2036
+ self.index_method_reference("<=>".to_string(), &node.message_loc().unwrap(), method_receiver);
2037
+ }
2038
+ _ => {}
2039
+ }
2040
+ }
2041
+ }
2042
+ }
2043
+
2044
+ fn visit_call_and_write_node(&mut self, node: &ruby_prism::CallAndWriteNode) {
2045
+ let method_receiver = self.method_receiver(node.receiver().as_ref(), node.location());
2046
+ if method_receiver.is_none()
2047
+ && let Some(receiver) = node.receiver()
2048
+ {
2049
+ self.visit(&receiver);
2050
+ }
2051
+
2052
+ let read_name = String::from_utf8_lossy(node.read_name().as_slice()).to_string();
2053
+ self.index_method_reference(read_name, &node.operator_loc(), method_receiver);
2054
+
2055
+ let write_name = String::from_utf8_lossy(node.write_name().as_slice()).to_string();
2056
+ self.index_method_reference(write_name, &node.operator_loc(), method_receiver);
2057
+
2058
+ self.visit(&node.value());
2059
+ }
2060
+
2061
+ fn visit_call_operator_write_node(&mut self, node: &ruby_prism::CallOperatorWriteNode) {
2062
+ let method_receiver = self.method_receiver(node.receiver().as_ref(), node.location());
2063
+ if method_receiver.is_none()
2064
+ && let Some(receiver) = node.receiver()
2065
+ {
2066
+ self.visit(&receiver);
2067
+ }
2068
+
2069
+ let read_name = String::from_utf8_lossy(node.read_name().as_slice()).to_string();
2070
+ self.index_method_reference(read_name, &node.call_operator_loc().unwrap(), method_receiver);
2071
+
2072
+ let write_name = String::from_utf8_lossy(node.write_name().as_slice()).to_string();
2073
+ self.index_method_reference(write_name, &node.call_operator_loc().unwrap(), method_receiver);
2074
+
2075
+ self.visit(&node.value());
2076
+ }
2077
+
2078
+ fn visit_call_or_write_node(&mut self, node: &ruby_prism::CallOrWriteNode) {
2079
+ let method_receiver = self.method_receiver(node.receiver().as_ref(), node.location());
2080
+ if method_receiver.is_none()
2081
+ && let Some(receiver) = node.receiver()
2082
+ {
2083
+ self.visit(&receiver);
2084
+ }
2085
+
2086
+ let read_name = String::from_utf8_lossy(node.read_name().as_slice()).to_string();
2087
+ self.index_method_reference(read_name, &node.operator_loc(), method_receiver);
2088
+
2089
+ let write_name = String::from_utf8_lossy(node.write_name().as_slice()).to_string();
2090
+ self.index_method_reference(write_name, &node.operator_loc(), method_receiver);
2091
+
2092
+ self.visit(&node.value());
2093
+ }
2094
+
2095
+ fn visit_global_variable_write_node(&mut self, node: &ruby_prism::GlobalVariableWriteNode) {
2096
+ self.add_global_variable_definition(&node.name_loc());
2097
+ self.visit(&node.value());
2098
+ }
2099
+
2100
+ fn visit_global_variable_and_write_node(&mut self, node: &ruby_prism::GlobalVariableAndWriteNode<'_>) {
2101
+ self.add_global_variable_definition(&node.name_loc());
2102
+ self.visit(&node.value());
2103
+ }
2104
+
2105
+ fn visit_global_variable_or_write_node(&mut self, node: &ruby_prism::GlobalVariableOrWriteNode<'_>) {
2106
+ self.add_global_variable_definition(&node.name_loc());
2107
+ self.visit(&node.value());
2108
+ }
2109
+
2110
+ fn visit_global_variable_operator_write_node(&mut self, node: &ruby_prism::GlobalVariableOperatorWriteNode<'_>) {
2111
+ self.add_global_variable_definition(&node.name_loc());
2112
+ self.visit(&node.value());
2113
+ }
2114
+
2115
+ fn visit_instance_variable_and_write_node(&mut self, node: &ruby_prism::InstanceVariableAndWriteNode) {
2116
+ self.add_instance_variable_definition(&node.name_loc());
2117
+ self.visit(&node.value());
2118
+ }
2119
+
2120
+ fn visit_instance_variable_operator_write_node(&mut self, node: &ruby_prism::InstanceVariableOperatorWriteNode) {
2121
+ self.add_instance_variable_definition(&node.name_loc());
2122
+ self.visit(&node.value());
2123
+ }
2124
+
2125
+ fn visit_instance_variable_or_write_node(&mut self, node: &ruby_prism::InstanceVariableOrWriteNode) {
2126
+ self.add_instance_variable_definition(&node.name_loc());
2127
+ self.visit(&node.value());
2128
+ }
2129
+
2130
+ fn visit_instance_variable_write_node(&mut self, node: &ruby_prism::InstanceVariableWriteNode) {
2131
+ self.add_instance_variable_definition(&node.name_loc());
2132
+ self.visit(&node.value());
2133
+ }
2134
+
2135
+ fn visit_class_variable_and_write_node(&mut self, node: &ruby_prism::ClassVariableAndWriteNode) {
2136
+ self.add_class_variable_definition(&node.name_loc());
2137
+ self.visit(&node.value());
2138
+ }
2139
+
2140
+ fn visit_class_variable_operator_write_node(&mut self, node: &ruby_prism::ClassVariableOperatorWriteNode) {
2141
+ self.add_class_variable_definition(&node.name_loc());
2142
+ self.visit(&node.value());
2143
+ }
2144
+
2145
+ fn visit_class_variable_or_write_node(&mut self, node: &ruby_prism::ClassVariableOrWriteNode) {
2146
+ self.add_class_variable_definition(&node.name_loc());
2147
+ self.visit(&node.value());
2148
+ }
2149
+
2150
+ fn visit_class_variable_write_node(&mut self, node: &ruby_prism::ClassVariableWriteNode) {
2151
+ self.add_class_variable_definition(&node.name_loc());
2152
+ self.visit(&node.value());
2153
+ }
2154
+
2155
+ fn visit_block_argument_node(&mut self, node: &ruby_prism::BlockArgumentNode<'_>) {
2156
+ let expression = node.expression();
2157
+ if let Some(expression) = expression {
2158
+ match expression {
2159
+ ruby_prism::Node::SymbolNode { .. } => {
2160
+ let symbol = expression.as_symbol_node().unwrap();
2161
+ let name = Self::location_to_string(&symbol.value_loc().unwrap());
2162
+ self.index_method_reference(name, &node.location(), None);
2163
+ }
2164
+ _ => {
2165
+ self.visit(&expression);
2166
+ }
2167
+ }
2168
+ }
2169
+ }
2170
+
2171
+ fn visit_alias_method_node(&mut self, node: &ruby_prism::AliasMethodNode<'_>) {
2172
+ let mut new_name = if let Some(symbol_node) = node.new_name().as_symbol_node() {
2173
+ Self::location_to_string(&symbol_node.value_loc().unwrap())
2174
+ } else {
2175
+ Self::location_to_string(&node.new_name().location())
2176
+ };
2177
+
2178
+ let mut old_name = if let Some(symbol_node) = node.old_name().as_symbol_node() {
2179
+ Self::location_to_string(&symbol_node.value_loc().unwrap())
2180
+ } else {
2181
+ Self::location_to_string(&node.old_name().location())
2182
+ };
2183
+
2184
+ new_name.push_str("()");
2185
+ old_name.push_str("()");
2186
+
2187
+ let offset = Offset::from_prism_location(&node.location());
2188
+ let (comments, flags) = self.find_comments_for(offset.start());
2189
+ let new_name_str_id = self.intern_string(new_name);
2190
+ let old_name_str_id = self.intern_string(old_name.clone());
2191
+
2192
+ self.operations.push(Operation::AliasMethod(op::AliasMethod {
2193
+ new_name_str_id,
2194
+ old_name_str_id,
2195
+ uri_id: self.uri_id,
2196
+ offset,
2197
+ comments,
2198
+ flags,
2199
+ receiver: None,
2200
+ }));
2201
+
2202
+ self.index_method_reference(old_name, &node.old_name().location(), None);
2203
+ }
2204
+
2205
+ fn visit_alias_global_variable_node(&mut self, node: &ruby_prism::AliasGlobalVariableNode<'_>) {
2206
+ let new_name = Self::location_to_string(&node.new_name().location());
2207
+ let old_name = Self::location_to_string(&node.old_name().location());
2208
+ let new_name_str_id = self.intern_string(new_name);
2209
+ let old_name_str_id = self.intern_string(old_name);
2210
+ let offset = Offset::from_prism_location(&node.location());
2211
+ let (comments, flags) = self.find_comments_for(offset.start());
2212
+
2213
+ self.operations
2214
+ .push(Operation::AliasGlobalVariable(op::AliasGlobalVariable {
2215
+ new_name_str_id,
2216
+ old_name_str_id,
2217
+ uri_id: self.uri_id,
2218
+ offset,
2219
+ comments,
2220
+ flags,
2221
+ }));
2222
+ }
2223
+
2224
+ fn visit_and_node(&mut self, node: &ruby_prism::AndNode) {
2225
+ let left = node.left();
2226
+ let method_receiver = self.method_receiver(Some(&left), left.location());
2227
+
2228
+ if method_receiver.is_none() {
2229
+ self.visit(&left);
2230
+ }
2231
+
2232
+ self.index_method_reference("&&".to_string(), &node.location(), method_receiver);
2233
+ self.visit(&node.right());
2234
+ }
2235
+
2236
+ fn visit_or_node(&mut self, node: &ruby_prism::OrNode) {
2237
+ let left = node.left();
2238
+ let method_receiver = self.method_receiver(Some(&left), left.location());
2239
+
2240
+ if method_receiver.is_none() {
2241
+ self.visit(&left);
2242
+ }
2243
+
2244
+ self.index_method_reference("||".to_string(), &node.location(), method_receiver);
2245
+ self.visit(&node.right());
2246
+ }
2247
+ }
2248
+
2249
+ #[cfg(test)]
2250
+ mod tests {
2251
+ use super::*;
2252
+ use crate::operation::printer;
2253
+
2254
+ fn build_operations(source: &str) -> OperationBuilderResult {
2255
+ let source = crate::test_utils::normalize_indentation(source);
2256
+ let builder = RubyOperationBuilder::new("file:///test.rb".to_string(), &source);
2257
+ builder.build()
2258
+ }
2259
+
2260
+ fn normalize_expected(expected: &str) -> String {
2261
+ crate::test_utils::normalize_indentation(expected).trim().to_string()
2262
+ }
2263
+
2264
+ fn assert_operations(source: &str, expected: &str) {
2265
+ let result = build_operations(source);
2266
+ let actual = printer::print_operations(&result.operations, &result.strings, &result.names, false);
2267
+ let expected = normalize_expected(expected);
2268
+ assert_eq!(actual, expected, "\n\nActual:\n{actual}\n\nExpected:\n{expected}\n");
2269
+ }
2270
+
2271
+ fn assert_operations_with_references(source: &str, expected: &str) {
2272
+ let result = build_operations(source);
2273
+ let actual = printer::print_operations(&result.operations, &result.strings, &result.names, true);
2274
+ let expected = normalize_expected(expected);
2275
+ assert_eq!(actual, expected, "\n\nActual:\n{actual}\n\nExpected:\n{expected}\n");
2276
+ }
2277
+
2278
+ // -- Namespace tests --
2279
+
2280
+ #[test]
2281
+ fn build_class_node() {
2282
+ assert_operations(
2283
+ "
2284
+ class Foo
2285
+ class Bar; end
2286
+ end
2287
+ ",
2288
+ "
2289
+ EnterClass(Foo)
2290
+ EnterClass(Bar)
2291
+ ExitScope
2292
+ ExitScope
2293
+ ",
2294
+ );
2295
+ }
2296
+
2297
+ #[test]
2298
+ fn build_class_with_qualified_name() {
2299
+ assert_operations(
2300
+ "
2301
+ class Foo::Bar; end
2302
+ ",
2303
+ "
2304
+ EnterClass(Foo::Bar)
2305
+ ExitScope
2306
+ ",
2307
+ );
2308
+ }
2309
+
2310
+ #[test]
2311
+ fn build_class_with_superclass() {
2312
+ assert_operations(
2313
+ "
2314
+ class Foo < Bar; end
2315
+ ",
2316
+ "
2317
+ EnterClass(Foo, superclass: Bar)
2318
+ ExitScope
2319
+ ",
2320
+ );
2321
+ }
2322
+
2323
+ #[test]
2324
+ fn build_module_node() {
2325
+ assert_operations(
2326
+ "
2327
+ module Foo
2328
+ module Bar; end
2329
+ end
2330
+ ",
2331
+ "
2332
+ EnterModule(Foo)
2333
+ EnterModule(Bar)
2334
+ ExitScope
2335
+ ExitScope
2336
+ ",
2337
+ );
2338
+ }
2339
+
2340
+ #[test]
2341
+ fn build_singleton_class() {
2342
+ assert_operations(
2343
+ "
2344
+ class Foo
2345
+ class << self
2346
+ def bar; end
2347
+ end
2348
+ end
2349
+ ",
2350
+ "
2351
+ EnterClass(Foo)
2352
+ EnterSingletonClass(Foo::<Foo>)
2353
+ EnterMethod(bar())
2354
+ ExitScope
2355
+ ExitScope
2356
+ ExitScope
2357
+ ",
2358
+ );
2359
+ }
2360
+
2361
+ // -- Method tests --
2362
+
2363
+ #[test]
2364
+ fn build_def_node() {
2365
+ assert_operations(
2366
+ "
2367
+ def foo; end
2368
+
2369
+ class Foo
2370
+ def bar; end
2371
+ def self.baz; end
2372
+ end
2373
+ ",
2374
+ "
2375
+ EnterMethod(foo())
2376
+ ExitScope
2377
+ EnterClass(Foo)
2378
+ EnterMethod(bar())
2379
+ ExitScope
2380
+ EnterMethod(self.baz())
2381
+ ExitScope
2382
+ ExitScope
2383
+ ",
2384
+ );
2385
+ }
2386
+
2387
+ #[test]
2388
+ fn build_def_node_with_constant_receiver() {
2389
+ assert_operations(
2390
+ "
2391
+ class Bar
2392
+ def Foo.quz; end
2393
+ end
2394
+ ",
2395
+ "
2396
+ EnterClass(Bar)
2397
+ EnterMethod(Foo.quz())
2398
+ ExitScope
2399
+ ExitScope
2400
+ ",
2401
+ );
2402
+ }
2403
+
2404
+ // -- Visibility tests --
2405
+
2406
+ #[test]
2407
+ fn build_default_visibility() {
2408
+ assert_operations(
2409
+ "
2410
+ class Foo
2411
+ private
2412
+
2413
+ def m1; end
2414
+
2415
+ public
2416
+
2417
+ def m2; end
2418
+ end
2419
+ ",
2420
+ "
2421
+ EnterClass(Foo)
2422
+ SetDefaultVisibility(private)
2423
+ EnterMethod(m1())
2424
+ ExitScope
2425
+ SetDefaultVisibility(public)
2426
+ EnterMethod(m2())
2427
+ ExitScope
2428
+ ExitScope
2429
+ ",
2430
+ );
2431
+ }
2432
+
2433
+ #[test]
2434
+ fn build_inline_visibility() {
2435
+ assert_operations(
2436
+ "
2437
+ protected def m1; end
2438
+ ",
2439
+ "
2440
+ SetDefaultVisibility(protected)
2441
+ EnterMethod(m1())
2442
+ ExitScope
2443
+ SetDefaultVisibility(private)
2444
+ ",
2445
+ );
2446
+ }
2447
+
2448
+ // TODO: `private :bar` with symbol args should produce SetMethodVisibility operations.
2449
+ // This is one of the key motivations for the operation-based approach.
2450
+
2451
+ #[test]
2452
+ fn build_module_function() {
2453
+ assert_operations(
2454
+ "
2455
+ module Foo
2456
+ module_function
2457
+
2458
+ def bar; end
2459
+ end
2460
+ ",
2461
+ "
2462
+ EnterModule(Foo)
2463
+ SetDefaultVisibility(module_function)
2464
+ EnterMethod(self.bar())
2465
+ ExitScope
2466
+ EnterMethod(bar())
2467
+ ExitScope
2468
+ ExitScope
2469
+ ",
2470
+ );
2471
+ }
2472
+
2473
+ #[test]
2474
+ fn build_module_function_with_ivar() {
2475
+ assert_operations(
2476
+ "
2477
+ module Foo
2478
+ module_function
2479
+
2480
+ def bar
2481
+ @x = 1
2482
+ end
2483
+ end
2484
+ ",
2485
+ "
2486
+ EnterModule(Foo)
2487
+ SetDefaultVisibility(module_function)
2488
+ EnterMethod(self.bar())
2489
+ DefineInstanceVariable(@x)
2490
+ ExitScope
2491
+ EnterMethod(bar())
2492
+ DefineInstanceVariable(@x)
2493
+ ExitScope
2494
+ ExitScope
2495
+ ",
2496
+ );
2497
+ }
2498
+
2499
+ // -- Constant tests --
2500
+
2501
+ #[test]
2502
+ fn build_constant_write() {
2503
+ assert_operations(
2504
+ "
2505
+ FOO = 1
2506
+
2507
+ class Bar
2508
+ BAZ = 2
2509
+ end
2510
+ ",
2511
+ "
2512
+ DefineConstant(FOO)
2513
+ EnterClass(Bar)
2514
+ DefineConstant(BAZ)
2515
+ ExitScope
2516
+ ",
2517
+ );
2518
+ }
2519
+
2520
+ #[test]
2521
+ fn build_constant_path_write() {
2522
+ assert_operations(
2523
+ "
2524
+ FOO::BAR = 1
2525
+ ",
2526
+ "
2527
+ DefineConstant(FOO::BAR)
2528
+ ",
2529
+ );
2530
+ }
2531
+
2532
+ #[test]
2533
+ fn build_constant_alias() {
2534
+ assert_operations(
2535
+ "
2536
+ ALIAS = OtherConstant
2537
+ ",
2538
+ "
2539
+ AliasConstant(ALIAS -> OtherConstant)
2540
+ ",
2541
+ );
2542
+ }
2543
+
2544
+ #[test]
2545
+ fn build_set_constant_visibility() {
2546
+ assert_operations(
2547
+ "
2548
+ module Foo
2549
+ BAR = 42
2550
+ private_constant :BAR
2551
+ end
2552
+ ",
2553
+ "
2554
+ EnterModule(Foo)
2555
+ DefineConstant(BAR)
2556
+ SetConstantVisibility(BAR, vis: private)
2557
+ ExitScope
2558
+ ",
2559
+ );
2560
+ }
2561
+
2562
+ #[test]
2563
+ fn build_public_constant() {
2564
+ assert_operations(
2565
+ "
2566
+ module Foo
2567
+ BAR = 42
2568
+ public_constant :BAR
2569
+ end
2570
+ ",
2571
+ "
2572
+ EnterModule(Foo)
2573
+ DefineConstant(BAR)
2574
+ SetConstantVisibility(BAR, vis: public)
2575
+ ExitScope
2576
+ ",
2577
+ );
2578
+ }
2579
+
2580
+ #[test]
2581
+ fn build_private_constant_multiple() {
2582
+ assert_operations(
2583
+ "
2584
+ module Foo
2585
+ BAR = 42
2586
+ BAZ = 43
2587
+ private_constant :BAR, :BAZ
2588
+ end
2589
+ ",
2590
+ "
2591
+ EnterModule(Foo)
2592
+ DefineConstant(BAR)
2593
+ DefineConstant(BAZ)
2594
+ SetConstantVisibility(BAR, vis: private)
2595
+ SetConstantVisibility(BAZ, vis: private)
2596
+ ExitScope
2597
+ ",
2598
+ );
2599
+ }
2600
+
2601
+ // -- Attribute tests --
2602
+
2603
+ #[test]
2604
+ fn build_attr_accessor() {
2605
+ assert_operations(
2606
+ "
2607
+ class Foo
2608
+ attr_accessor :bar
2609
+ attr_reader :baz
2610
+ attr_writer :qux
2611
+ end
2612
+ ",
2613
+ "
2614
+ EnterClass(Foo)
2615
+ DefineAttribute(accessor bar())
2616
+ DefineAttribute(reader baz())
2617
+ DefineAttribute(writer qux())
2618
+ ExitScope
2619
+ ",
2620
+ );
2621
+ }
2622
+
2623
+ #[test]
2624
+ fn build_multiple_attr_accessors() {
2625
+ assert_operations(
2626
+ "
2627
+ class Foo
2628
+ attr_accessor :bar, :baz
2629
+ end
2630
+ ",
2631
+ "
2632
+ EnterClass(Foo)
2633
+ DefineAttribute(accessor bar())
2634
+ DefineAttribute(accessor baz())
2635
+ ExitScope
2636
+ ",
2637
+ );
2638
+ }
2639
+
2640
+ #[test]
2641
+ fn build_attr_with_visibility() {
2642
+ assert_operations(
2643
+ "
2644
+ class Foo
2645
+ private
2646
+
2647
+ attr_reader :bar
2648
+ end
2649
+ ",
2650
+ "
2651
+ EnterClass(Foo)
2652
+ SetDefaultVisibility(private)
2653
+ DefineAttribute(reader bar())
2654
+ ExitScope
2655
+ ",
2656
+ );
2657
+ }
2658
+
2659
+ // -- Mixin tests --
2660
+
2661
+ #[test]
2662
+ fn build_mixins() {
2663
+ assert_operations(
2664
+ "
2665
+ class Foo
2666
+ include Bar
2667
+ prepend Baz
2668
+ extend Qux
2669
+ end
2670
+ ",
2671
+ "
2672
+ EnterClass(Foo)
2673
+ Mixin(include, Bar)
2674
+ Mixin(prepend, Baz)
2675
+ Mixin(extend, Qux)
2676
+ ExitScope
2677
+ ",
2678
+ );
2679
+ }
2680
+
2681
+ // -- Alias tests --
2682
+
2683
+ #[test]
2684
+ fn build_alias_method() {
2685
+ assert_operations(
2686
+ "
2687
+ class Foo
2688
+ alias foo bar
2689
+ end
2690
+ ",
2691
+ "
2692
+ EnterClass(Foo)
2693
+ AliasMethod(foo() -> bar())
2694
+ ExitScope
2695
+ ",
2696
+ );
2697
+ }
2698
+
2699
+ #[test]
2700
+ fn build_alias_method_call() {
2701
+ assert_operations(
2702
+ "
2703
+ class Foo
2704
+ alias_method :new_name, :old_name
2705
+ end
2706
+ ",
2707
+ "
2708
+ EnterClass(Foo)
2709
+ AliasMethod(new_name() -> old_name())
2710
+ ExitScope
2711
+ ",
2712
+ );
2713
+ }
2714
+
2715
+ #[test]
2716
+ fn build_alias_global_variable() {
2717
+ assert_operations(
2718
+ "
2719
+ alias $new $old
2720
+ ",
2721
+ "
2722
+ AliasGlobalVariable($new -> $old)
2723
+ ",
2724
+ );
2725
+ }
2726
+
2727
+ // -- Variable tests --
2728
+
2729
+ #[test]
2730
+ fn build_instance_variable() {
2731
+ assert_operations(
2732
+ "
2733
+ class Foo
2734
+ def initialize
2735
+ @bar = 1
2736
+ end
2737
+ end
2738
+ ",
2739
+ "
2740
+ EnterClass(Foo)
2741
+ EnterMethod(initialize())
2742
+ DefineInstanceVariable(@bar)
2743
+ ExitScope
2744
+ ExitScope
2745
+ ",
2746
+ );
2747
+ }
2748
+
2749
+ #[test]
2750
+ fn build_class_variable() {
2751
+ assert_operations(
2752
+ "
2753
+ class Foo
2754
+ @@bar = 1
2755
+ end
2756
+ ",
2757
+ "
2758
+ EnterClass(Foo)
2759
+ DefineClassVariable(@@bar)
2760
+ ExitScope
2761
+ ",
2762
+ );
2763
+ }
2764
+
2765
+ #[test]
2766
+ fn build_global_variable() {
2767
+ assert_operations(
2768
+ "
2769
+ $foo = 1
2770
+ ",
2771
+ "
2772
+ DefineGlobalVariable($foo)
2773
+ ",
2774
+ );
2775
+ }
2776
+
2777
+ // -- Reference tests --
2778
+
2779
+ #[test]
2780
+ fn build_constant_references() {
2781
+ assert_operations_with_references(
2782
+ "
2783
+ Foo
2784
+ ",
2785
+ "
2786
+ ReferenceConstant(Foo)
2787
+ ",
2788
+ );
2789
+ }
2790
+
2791
+ #[test]
2792
+ fn build_method_references() {
2793
+ assert_operations_with_references(
2794
+ "
2795
+ foo
2796
+ ",
2797
+ "
2798
+ ReferenceMethod(foo)
2799
+ ",
2800
+ );
2801
+ }
2802
+
2803
+ // -- Ordering tests --
2804
+
2805
+ #[test]
2806
+ fn build_operations_ordering_with_visibility() {
2807
+ assert_operations(
2808
+ "
2809
+ class Foo
2810
+ def m1; end
2811
+ private
2812
+ def m2; end
2813
+ end
2814
+ ",
2815
+ "
2816
+ EnterClass(Foo)
2817
+ EnterMethod(m1())
2818
+ ExitScope
2819
+ SetDefaultVisibility(private)
2820
+ EnterMethod(m2())
2821
+ ExitScope
2822
+ ExitScope
2823
+ ",
2824
+ );
2825
+ }
2826
+
2827
+ #[test]
2828
+ fn build_visibility_resets_in_nested_class() {
2829
+ assert_operations(
2830
+ "
2831
+ class Foo
2832
+ private
2833
+
2834
+ class Bar
2835
+ def m1; end
2836
+ end
2837
+
2838
+ def m2; end
2839
+ end
2840
+ ",
2841
+ "
2842
+ EnterClass(Foo)
2843
+ SetDefaultVisibility(private)
2844
+ EnterClass(Bar)
2845
+ EnterMethod(m1())
2846
+ ExitScope
2847
+ ExitScope
2848
+ EnterMethod(m2())
2849
+ ExitScope
2850
+ ExitScope
2851
+ ",
2852
+ );
2853
+ }
2854
+
2855
+ #[test]
2856
+ fn build_visibility_in_singleton_class() {
2857
+ assert_operations(
2858
+ "
2859
+ class Foo
2860
+ protected
2861
+
2862
+ class << self
2863
+ def m1; end
2864
+
2865
+ private
2866
+
2867
+ def m2; end
2868
+ end
2869
+
2870
+ def m3; end
2871
+ end
2872
+ ",
2873
+ "
2874
+ EnterClass(Foo)
2875
+ SetDefaultVisibility(protected)
2876
+ EnterSingletonClass(Foo::<Foo>)
2877
+ EnterMethod(m1())
2878
+ ExitScope
2879
+ SetDefaultVisibility(private)
2880
+ EnterMethod(m2())
2881
+ ExitScope
2882
+ ExitScope
2883
+ EnterMethod(m3())
2884
+ ExitScope
2885
+ ExitScope
2886
+ ",
2887
+ );
2888
+ }
2889
+
2890
+ #[test]
2891
+ fn build_top_level_method_visibility() {
2892
+ assert_operations(
2893
+ "
2894
+ def m1; end
2895
+
2896
+ protected def m2; end
2897
+
2898
+ public
2899
+
2900
+ def m3; end
2901
+ ",
2902
+ "
2903
+ EnterMethod(m1())
2904
+ ExitScope
2905
+ SetDefaultVisibility(protected)
2906
+ EnterMethod(m2())
2907
+ ExitScope
2908
+ SetDefaultVisibility(private)
2909
+ SetDefaultVisibility(public)
2910
+ EnterMethod(m3())
2911
+ ExitScope
2912
+ ",
2913
+ );
2914
+ }
2915
+ }