rubydex 0.2.4-aarch64-linux → 0.2.6-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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +17 -16
  3. data/THIRD_PARTY_LICENSES.html +6 -6
  4. data/exe/rubydex_mcp +17 -0
  5. data/ext/rubydex/definition.c +89 -2
  6. data/ext/rubydex/document.c +36 -0
  7. data/ext/rubydex/extconf.rb +8 -0
  8. data/ext/rubydex/graph.c +32 -18
  9. data/ext/rubydex/handle.h +21 -5
  10. data/lib/rubydex/3.2/rubydex.so +0 -0
  11. data/lib/rubydex/3.3/rubydex.so +0 -0
  12. data/lib/rubydex/3.4/rubydex.so +0 -0
  13. data/lib/rubydex/4.0/rubydex.so +0 -0
  14. data/lib/rubydex/bin/rubydex_mcp +0 -0
  15. data/lib/rubydex/declaration.rb +3 -3
  16. data/lib/rubydex/errors.rb +8 -0
  17. data/lib/rubydex/graph.rb +3 -1
  18. data/lib/rubydex/librubydex_sys.so +0 -0
  19. data/lib/rubydex/location.rb +24 -0
  20. data/lib/rubydex/version.rb +1 -1
  21. data/lib/rubydex.rb +1 -0
  22. data/rbi/rubydex.rbi +37 -14
  23. data/rust/Cargo.lock +3 -3
  24. data/rust/rubydex/Cargo.toml +7 -1
  25. data/rust/rubydex/src/dot.rs +609 -0
  26. data/rust/rubydex/src/indexing/local_graph.rs +38 -0
  27. data/rust/rubydex/src/indexing/rbs_indexer.rs +19 -1
  28. data/rust/rubydex/src/indexing/ruby_indexer.rs +10 -0
  29. data/rust/rubydex/src/indexing/ruby_indexer_tests.rs +5 -1
  30. data/rust/rubydex/src/indexing.rs +38 -12
  31. data/rust/rubydex/src/lib.rs +2 -1
  32. data/rust/rubydex/src/listing.rs +14 -3
  33. data/rust/rubydex/src/main.rs +35 -7
  34. data/rust/rubydex/src/model/built_in.rs +5 -2
  35. data/rust/rubydex/src/model/comment.rs +2 -0
  36. data/rust/rubydex/src/model/declaration.rs +1 -0
  37. data/rust/rubydex/src/model/definitions.rs +20 -19
  38. data/rust/rubydex/src/model/document.rs +2 -0
  39. data/rust/rubydex/src/model/encoding.rs +2 -0
  40. data/rust/rubydex/src/model/graph.rs +51 -13
  41. data/rust/rubydex/src/model/identity_maps.rs +3 -0
  42. data/rust/rubydex/src/model/ids.rs +27 -1
  43. data/rust/rubydex/src/model/keywords.rs +3 -0
  44. data/rust/rubydex/src/model/name.rs +2 -0
  45. data/rust/rubydex/src/model/string_ref.rs +2 -0
  46. data/rust/rubydex/src/model/visibility.rs +3 -0
  47. data/rust/rubydex/src/operation/applier.rs +520 -0
  48. data/rust/rubydex/src/operation/mod.rs +285 -0
  49. data/rust/rubydex/src/operation/printer.rs +260 -0
  50. data/rust/rubydex/src/operation/ruby_builder.rs +2919 -0
  51. data/rust/rubydex/src/query.rs +114 -33
  52. data/rust/rubydex/src/resolution.rs +22 -9
  53. data/rust/rubydex/src/resolution_tests.rs +349 -209
  54. data/rust/rubydex/src/test_utils/graph_test.rs +19 -4
  55. data/rust/rubydex/src/test_utils/local_graph_test.rs +7 -6
  56. data/rust/rubydex/tests/cli.rs +17 -61
  57. data/rust/rubydex-mcp/Cargo.toml +9 -3
  58. data/rust/rubydex-mcp/src/server.rs +5 -1
  59. data/rust/rubydex-sys/Cargo.toml +9 -2
  60. data/rust/rubydex-sys/src/definition_api.rs +96 -2
  61. data/rust/rubydex-sys/src/document_api.rs +28 -0
  62. data/rust/rubydex-sys/src/graph_api.rs +2 -4
  63. metadata +11 -4
  64. data/rust/rubydex/src/visualization/dot.rs +0 -192
  65. data/rust/rubydex/src/visualization.rs +0 -6
@@ -0,0 +1,2919 @@
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 name_offset = Offset::from_prism_location(&node.name_loc());
1528
+ let parameters = self.collect_parameters(node);
1529
+ let is_singleton = node.receiver().is_some();
1530
+
1531
+ let current_visibility = self.current_visibility();
1532
+ let visibility = if is_singleton {
1533
+ Visibility::Public
1534
+ } else {
1535
+ current_visibility.visibility
1536
+ };
1537
+ let offset_for_comments = if is_singleton {
1538
+ offset.clone()
1539
+ } else if current_visibility.is_inline {
1540
+ current_visibility.offset.clone()
1541
+ } else {
1542
+ offset.clone()
1543
+ };
1544
+
1545
+ let comment_offset = self
1546
+ .take_decorator_offset(offset_for_comments.start())
1547
+ .unwrap_or_else(|| offset_for_comments.start());
1548
+ let (comments, flags) = self.find_comments_for(comment_offset);
1549
+
1550
+ let (receiver, method_nesting_receiver) = if let Some(recv_node) = node.receiver() {
1551
+ match recv_node {
1552
+ ruby_prism::Node::SelfNode { .. } => {
1553
+ let nesting_name = self.current_owner_name_id();
1554
+ (
1555
+ Some(Target::ExplicitSelf),
1556
+ nesting_name.map(NestingReceiver::SelfReceiver),
1557
+ )
1558
+ }
1559
+ ruby_prism::Node::ConstantPathNode { .. } | ruby_prism::Node::ConstantReadNode { .. } => {
1560
+ let name_id = self.index_constant_reference(&recv_node, true);
1561
+ (
1562
+ name_id.map(Target::Constant),
1563
+ name_id.map(NestingReceiver::ConstantReceiver),
1564
+ )
1565
+ }
1566
+ _ => {
1567
+ self.add_diagnostic(
1568
+ Rule::DynamicSingletonDefinition,
1569
+ Offset::from_prism_location(&node.location()),
1570
+ "Dynamic receiver for singleton method definition".to_string(),
1571
+ );
1572
+ self.visit(&recv_node);
1573
+ return;
1574
+ }
1575
+ }
1576
+ } else {
1577
+ (None, None)
1578
+ };
1579
+
1580
+ if receiver.is_none() && visibility == Visibility::ModuleFunction {
1581
+ // module_function: emit two EnterMethod/ExitScope pairs (singleton + instance),
1582
+ // each visiting the body so ivars are associated with both methods.
1583
+ let singleton_receiver = Some(Target::ExplicitSelf);
1584
+ let body = node.body();
1585
+
1586
+ self.operations.push(Operation::EnterMethod(op::EnterMethod {
1587
+ str_id,
1588
+ uri_id: self.uri_id,
1589
+ offset: offset.clone(),
1590
+ name_offset: name_offset.clone(),
1591
+ comments: comments.clone(),
1592
+ flags: flags.clone(),
1593
+ signatures: Signatures::Simple(parameters.clone().into_boxed_slice()),
1594
+ receiver: singleton_receiver,
1595
+ }));
1596
+ self.nesting_stack.push(Nesting::Method {
1597
+ receiver: method_nesting_receiver,
1598
+ });
1599
+ if let Some(ref body) = body {
1600
+ self.visit(body);
1601
+ }
1602
+ self.nesting_stack.pop();
1603
+ self.operations.push(Operation::ExitScope);
1604
+
1605
+ self.operations.push(Operation::EnterMethod(op::EnterMethod {
1606
+ str_id,
1607
+ uri_id: self.uri_id,
1608
+ offset: offset.clone(),
1609
+ name_offset: name_offset.clone(),
1610
+ comments,
1611
+ flags,
1612
+ signatures: Signatures::Simple(parameters.into_boxed_slice()),
1613
+ receiver,
1614
+ }));
1615
+ self.nesting_stack.push(Nesting::Method {
1616
+ receiver: method_nesting_receiver,
1617
+ });
1618
+ if let Some(ref body) = body {
1619
+ self.visit(body);
1620
+ }
1621
+ self.nesting_stack.pop();
1622
+ self.operations.push(Operation::ExitScope);
1623
+ } else {
1624
+ // Singleton methods at top level have receiver=None (no class to point self to).
1625
+ // Bracket with SetDefaultVisibility(Public) so the applier assigns the correct visibility.
1626
+ let needs_singleton_visibility_bracket = is_singleton && receiver.is_none();
1627
+ let previous_visibility = if needs_singleton_visibility_bracket {
1628
+ let prev = self.current_visibility().visibility;
1629
+ self.operations
1630
+ .push(Operation::SetDefaultVisibility(op::SetDefaultVisibility {
1631
+ visibility: Visibility::Public,
1632
+ uri_id: self.uri_id,
1633
+ offset: offset.clone(),
1634
+ }));
1635
+ Some(prev)
1636
+ } else {
1637
+ None
1638
+ };
1639
+
1640
+ self.operations.push(Operation::EnterMethod(op::EnterMethod {
1641
+ str_id,
1642
+ uri_id: self.uri_id,
1643
+ offset: offset.clone(),
1644
+ name_offset,
1645
+ comments,
1646
+ flags,
1647
+ signatures: Signatures::Simple(parameters.into_boxed_slice()),
1648
+ receiver,
1649
+ }));
1650
+ self.nesting_stack.push(Nesting::Method {
1651
+ receiver: method_nesting_receiver,
1652
+ });
1653
+ if let Some(body) = node.body() {
1654
+ self.visit(&body);
1655
+ }
1656
+ self.nesting_stack.pop();
1657
+ self.operations.push(Operation::ExitScope);
1658
+
1659
+ if let Some(prev) = previous_visibility {
1660
+ self.operations
1661
+ .push(Operation::SetDefaultVisibility(op::SetDefaultVisibility {
1662
+ visibility: prev,
1663
+ uri_id: self.uri_id,
1664
+ offset: offset.clone(),
1665
+ }));
1666
+ }
1667
+ }
1668
+ }
1669
+
1670
+ fn visit_constant_and_write_node(&mut self, node: &ruby_prism::ConstantAndWriteNode) {
1671
+ self.index_constant_reference(&node.as_node(), true);
1672
+ self.visit(&node.value());
1673
+ }
1674
+
1675
+ fn visit_constant_operator_write_node(&mut self, node: &ruby_prism::ConstantOperatorWriteNode) {
1676
+ self.index_constant_reference(&node.as_node(), true);
1677
+ self.visit(&node.value());
1678
+ }
1679
+
1680
+ fn visit_constant_or_write_node(&mut self, node: &ruby_prism::ConstantOrWriteNode) {
1681
+ if let Some(target_name_id) = self.index_constant_alias_target(&node.value()) {
1682
+ self.add_constant_alias_definition(&node.as_node(), target_name_id, true);
1683
+ } else {
1684
+ self.add_constant_definition(&node.as_node(), true, Self::is_promotable_value(&node.value()));
1685
+ self.visit(&node.value());
1686
+ }
1687
+ }
1688
+
1689
+ fn visit_constant_write_node(&mut self, node: &ruby_prism::ConstantWriteNode) {
1690
+ let value = node.value();
1691
+ if self.handle_dynamic_class_or_module(&node.as_node(), &value) {
1692
+ return;
1693
+ }
1694
+
1695
+ if let Some(target_name_id) = self.index_constant_alias_target(&value) {
1696
+ self.add_constant_alias_definition(&node.as_node(), target_name_id, false);
1697
+ } else {
1698
+ self.add_constant_definition(&node.as_node(), false, Self::is_promotable_value(&value));
1699
+ self.visit(&value);
1700
+ }
1701
+ }
1702
+
1703
+ fn visit_constant_path_and_write_node(&mut self, node: &ruby_prism::ConstantPathAndWriteNode) {
1704
+ self.visit_constant_path_node(&node.target());
1705
+ self.visit(&node.value());
1706
+ }
1707
+
1708
+ fn visit_constant_path_operator_write_node(&mut self, node: &ruby_prism::ConstantPathOperatorWriteNode) {
1709
+ self.visit_constant_path_node(&node.target());
1710
+ self.visit(&node.value());
1711
+ }
1712
+
1713
+ fn visit_constant_path_or_write_node(&mut self, node: &ruby_prism::ConstantPathOrWriteNode) {
1714
+ if let Some(target_name_id) = self.index_constant_alias_target(&node.value()) {
1715
+ self.add_constant_alias_definition(&node.target().as_node(), target_name_id, true);
1716
+ } else {
1717
+ self.add_constant_definition(&node.target().as_node(), true, Self::is_promotable_value(&node.value()));
1718
+ self.visit(&node.value());
1719
+ }
1720
+ }
1721
+
1722
+ fn visit_constant_path_write_node(&mut self, node: &ruby_prism::ConstantPathWriteNode) {
1723
+ let value = node.value();
1724
+ if self.handle_dynamic_class_or_module(&node.as_node(), &value) {
1725
+ return;
1726
+ }
1727
+
1728
+ if let Some(target_name_id) = self.index_constant_alias_target(&value) {
1729
+ self.add_constant_alias_definition(&node.target().as_node(), target_name_id, false);
1730
+ } else {
1731
+ self.add_constant_definition(&node.target().as_node(), false, Self::is_promotable_value(&value));
1732
+ self.visit(&value);
1733
+ }
1734
+ }
1735
+
1736
+ fn visit_constant_read_node(&mut self, node: &ruby_prism::ConstantReadNode<'_>) {
1737
+ self.index_constant_reference(&node.as_node(), true);
1738
+ }
1739
+
1740
+ fn visit_constant_path_node(&mut self, node: &ruby_prism::ConstantPathNode<'_>) {
1741
+ self.index_constant_reference(&node.as_node(), true);
1742
+ }
1743
+
1744
+ fn visit_multi_write_node(&mut self, node: &ruby_prism::MultiWriteNode) {
1745
+ for left in &node.lefts() {
1746
+ match left {
1747
+ ruby_prism::Node::ConstantTargetNode { .. } | ruby_prism::Node::ConstantPathTargetNode { .. } => {
1748
+ self.add_constant_definition(&left, false, true);
1749
+ }
1750
+ ruby_prism::Node::GlobalVariableTargetNode { .. } => {
1751
+ self.add_global_variable_definition(&left.location());
1752
+ }
1753
+ ruby_prism::Node::InstanceVariableTargetNode { .. } => {
1754
+ self.add_instance_variable_definition(&left.location());
1755
+ }
1756
+ ruby_prism::Node::ClassVariableTargetNode { .. } => {
1757
+ self.add_class_variable_definition(&left.location());
1758
+ }
1759
+ ruby_prism::Node::CallTargetNode { .. } => {
1760
+ let call_target_node = left.as_call_target_node().unwrap();
1761
+ let method_receiver = self.method_receiver(Some(&call_target_node.receiver()), left.location());
1762
+
1763
+ if method_receiver.is_none() {
1764
+ self.visit(&call_target_node.receiver());
1765
+ }
1766
+
1767
+ let name = String::from_utf8_lossy(call_target_node.name().as_slice()).to_string();
1768
+ self.index_method_reference(name, &call_target_node.location(), method_receiver);
1769
+ }
1770
+ _ => {}
1771
+ }
1772
+ }
1773
+
1774
+ self.visit(&node.value());
1775
+ }
1776
+
1777
+ #[allow(clippy::too_many_lines)]
1778
+ fn visit_call_node(&mut self, node: &ruby_prism::CallNode) {
1779
+ let index_attr = |kind: AttrKind, call: &ruby_prism::CallNode, builder: &mut Self| {
1780
+ let receiver = call.receiver();
1781
+ if receiver.is_some() && receiver.unwrap().as_self_node().is_none() {
1782
+ return;
1783
+ }
1784
+
1785
+ let call_offset = Offset::from_prism_location(&call.location());
1786
+
1787
+ let current_visibility = builder.current_visibility();
1788
+ let offset_for_comments = if current_visibility.is_inline {
1789
+ current_visibility.offset.clone()
1790
+ } else {
1791
+ call_offset
1792
+ };
1793
+
1794
+ let comment_offset = builder
1795
+ .take_decorator_offset(offset_for_comments.start())
1796
+ .unwrap_or_else(|| offset_for_comments.start());
1797
+
1798
+ Self::each_string_or_symbol_arg(call, |name, location| {
1799
+ let str_id = builder.intern_string(format!("{name}()"));
1800
+ let offset = Offset::from_prism_location(&location);
1801
+ let (comments, flags) = builder.find_comments_for(comment_offset);
1802
+
1803
+ builder.operations.push(Operation::DefineAttribute(op::DefineAttribute {
1804
+ kind,
1805
+ str_id,
1806
+ uri_id: builder.uri_id,
1807
+ offset,
1808
+ comments,
1809
+ flags,
1810
+ }));
1811
+ });
1812
+ };
1813
+
1814
+ let message_loc = node.message_loc();
1815
+ if message_loc.is_none() {
1816
+ return;
1817
+ }
1818
+
1819
+ let message = String::from_utf8_lossy(node.name().as_slice()).to_string();
1820
+
1821
+ match message.as_str() {
1822
+ "attr_accessor" => {
1823
+ index_attr(AttrKind::Accessor, node, self);
1824
+ }
1825
+ "attr_reader" => {
1826
+ index_attr(AttrKind::Reader, node, self);
1827
+ }
1828
+ "attr_writer" => {
1829
+ index_attr(AttrKind::Writer, node, self);
1830
+ }
1831
+ "attr" => {
1832
+ let create_writer = if let Some(arguments) = node.arguments() {
1833
+ let args_vec: Vec<_> = arguments.arguments().iter().collect();
1834
+ matches!(args_vec.as_slice(), [_, ruby_prism::Node::TrueNode { .. }])
1835
+ } else {
1836
+ false
1837
+ };
1838
+
1839
+ if create_writer {
1840
+ index_attr(AttrKind::Accessor, node, self);
1841
+ } else {
1842
+ index_attr(AttrKind::Reader, node, self);
1843
+ }
1844
+ }
1845
+ "alias_method" => {
1846
+ let recv_node = node.receiver();
1847
+ let recv_ref = recv_node.as_ref();
1848
+ if recv_ref.is_some_and(|recv| {
1849
+ !matches!(
1850
+ recv,
1851
+ ruby_prism::Node::SelfNode { .. }
1852
+ | ruby_prism::Node::ConstantReadNode { .. }
1853
+ | ruby_prism::Node::ConstantPathNode { .. }
1854
+ )
1855
+ }) {
1856
+ self.visit_call_node_parts(node);
1857
+ return;
1858
+ }
1859
+
1860
+ let mut names: Vec<(String, Offset)> = Vec::new();
1861
+ Self::each_string_or_symbol_arg(node, |name, location| {
1862
+ names.push((name, Offset::from_prism_location(&location)));
1863
+ });
1864
+
1865
+ if names.len() != 2 {
1866
+ return;
1867
+ }
1868
+
1869
+ let (new_name, _new_offset) = &names[0];
1870
+ let (old_name, old_offset) = &names[1];
1871
+
1872
+ let new_name_str_id = self.intern_string(format!("{new_name}()"));
1873
+ let old_name_str_id = self.intern_string(format!("{old_name}()"));
1874
+
1875
+ let (receiver, method_receiver) = match recv_ref {
1876
+ Some(
1877
+ recv @ (ruby_prism::Node::ConstantPathNode { .. } | ruby_prism::Node::ConstantReadNode { .. }),
1878
+ ) => {
1879
+ let name_id = self.index_constant_reference(recv, true);
1880
+ (name_id.map(Target::Constant), name_id)
1881
+ }
1882
+ _ => (None, self.method_receiver(recv_ref, node.location())),
1883
+ };
1884
+
1885
+ let ref_str_id = self.intern_string(format!("{old_name}()"));
1886
+ self.operations.push(Operation::ReferenceMethod(op::ReferenceMethod {
1887
+ str_id: ref_str_id,
1888
+ uri_id: self.uri_id,
1889
+ offset: old_offset.clone(),
1890
+ receiver: method_receiver.map(Target::Constant),
1891
+ }));
1892
+
1893
+ let offset = Offset::from_prism_location(&node.location());
1894
+ let (comments, flags) = self.find_comments_for(offset.start());
1895
+
1896
+ self.operations.push(Operation::AliasMethod(op::AliasMethod {
1897
+ new_name_str_id,
1898
+ old_name_str_id,
1899
+ uri_id: self.uri_id,
1900
+ offset,
1901
+ comments,
1902
+ flags,
1903
+ receiver,
1904
+ }));
1905
+ }
1906
+ "include" => {
1907
+ let receiver = node.receiver();
1908
+ if receiver.is_none() || receiver.as_ref().is_some_and(|r| r.as_self_node().is_some()) {
1909
+ self.handle_mixin(node, MixinKind::Include);
1910
+ } else {
1911
+ self.visit_call_node_parts(node);
1912
+ }
1913
+ }
1914
+ "prepend" => {
1915
+ let receiver = node.receiver();
1916
+ if receiver.is_none() || receiver.as_ref().is_some_and(|r| r.as_self_node().is_some()) {
1917
+ self.handle_mixin(node, MixinKind::Prepend);
1918
+ } else {
1919
+ self.visit_call_node_parts(node);
1920
+ }
1921
+ }
1922
+ "extend" => {
1923
+ let receiver = node.receiver();
1924
+ if receiver.is_none() || receiver.as_ref().is_some_and(|r| r.as_self_node().is_some()) {
1925
+ self.handle_mixin(node, MixinKind::Extend);
1926
+ } else {
1927
+ self.visit_call_node_parts(node);
1928
+ }
1929
+ }
1930
+ "private" | "protected" | "public" | "module_function" => {
1931
+ if node.receiver().is_some() {
1932
+ let offset = Offset::from_prism_location(&node.location());
1933
+ self.add_diagnostic(
1934
+ Rule::InvalidMethodVisibility,
1935
+ offset,
1936
+ format!("`{message}` cannot be called with an explicit receiver"),
1937
+ );
1938
+ self.visit_call_node_parts(node);
1939
+ return;
1940
+ }
1941
+
1942
+ let visibility = Visibility::from_string(message.as_str());
1943
+ let offset = Offset::from_prism_location(&node.location());
1944
+
1945
+ if let Some(arguments) = node.arguments() {
1946
+ if visibility == Visibility::ModuleFunction && !self.current_nesting_is_module() {
1947
+ self.add_diagnostic(
1948
+ Rule::InvalidMethodVisibility,
1949
+ offset,
1950
+ "`module_function` can only be used in modules".to_string(),
1951
+ );
1952
+ self.visit_arguments_node(&arguments);
1953
+ } else {
1954
+ self.handle_visibility_arguments(&arguments, visibility, &offset, &message);
1955
+ }
1956
+ } else {
1957
+ let last_visibility = self.visibility_stack.last_mut().unwrap();
1958
+ *last_visibility = VisibilityModifier::new(visibility, false, offset);
1959
+ self.operations
1960
+ .push(Operation::SetDefaultVisibility(op::SetDefaultVisibility {
1961
+ visibility,
1962
+ uri_id: self.uri_id,
1963
+ offset: Offset::from_prism_location(&node.location()),
1964
+ }));
1965
+ }
1966
+ }
1967
+ "new" => {
1968
+ let receiver_name = node.receiver().map(|r| r.location().as_slice());
1969
+
1970
+ if matches!(receiver_name, Some(b"Class" | b"::Class")) {
1971
+ self.handle_class_definition(
1972
+ &node.location(),
1973
+ None,
1974
+ node.block(),
1975
+ node.arguments().and_then(|args| args.arguments().iter().next()),
1976
+ false,
1977
+ );
1978
+ } else if matches!(receiver_name, Some(b"Module" | b"::Module")) {
1979
+ self.handle_module_definition(&node.location(), None, node.block(), false);
1980
+ } else {
1981
+ if let Some(arguments) = node.arguments() {
1982
+ self.visit_arguments_node(&arguments);
1983
+ }
1984
+ if let Some(block) = node.block() {
1985
+ self.visit(&block);
1986
+ }
1987
+ }
1988
+
1989
+ self.index_method_reference_for_call(node);
1990
+ }
1991
+ "sig"
1992
+ if node.receiver().is_none()
1993
+ || matches!(
1994
+ node.receiver(),
1995
+ Some(ruby_prism::Node::ConstantPathNode { .. } | ruby_prism::Node::ConstantReadNode { .. })
1996
+ ) =>
1997
+ {
1998
+ self.pending_decorator_offset = Some(Offset::from_prism_location(&node.location()));
1999
+
2000
+ if let Some(arguments) = node.arguments() {
2001
+ self.visit_arguments_node(&arguments);
2002
+ }
2003
+ if let Some(block) = node.block() {
2004
+ self.visit(&block);
2005
+ }
2006
+ self.index_method_reference_for_call(node);
2007
+ }
2008
+ "private_constant" => {
2009
+ self.handle_constant_visibility(node, Visibility::Private);
2010
+ }
2011
+ "public_constant" => {
2012
+ self.handle_constant_visibility(node, Visibility::Public);
2013
+ }
2014
+ "private_class_method" => {
2015
+ self.handle_singleton_method_visibility(node, Visibility::Private, "private_class_method");
2016
+ }
2017
+ "public_class_method" => {
2018
+ self.handle_singleton_method_visibility(node, Visibility::Public, "public_class_method");
2019
+ }
2020
+ _ => {
2021
+ if let Some(arguments) = node.arguments() {
2022
+ self.visit_arguments_node(&arguments);
2023
+ }
2024
+ if let Some(block) = node.block() {
2025
+ self.visit(&block);
2026
+ }
2027
+
2028
+ let method_receiver = self.method_receiver(node.receiver().as_ref(), node.location());
2029
+
2030
+ if method_receiver.is_none()
2031
+ && let Some(receiver) = node.receiver()
2032
+ {
2033
+ self.visit(&receiver);
2034
+ }
2035
+
2036
+ self.index_method_reference(message.clone(), &node.message_loc().unwrap(), method_receiver);
2037
+
2038
+ match message.as_str() {
2039
+ ">" | "<" | ">=" | "<=" => {
2040
+ self.index_method_reference("<=>".to_string(), &node.message_loc().unwrap(), method_receiver);
2041
+ }
2042
+ _ => {}
2043
+ }
2044
+ }
2045
+ }
2046
+ }
2047
+
2048
+ fn visit_call_and_write_node(&mut self, node: &ruby_prism::CallAndWriteNode) {
2049
+ let method_receiver = self.method_receiver(node.receiver().as_ref(), node.location());
2050
+ if method_receiver.is_none()
2051
+ && let Some(receiver) = node.receiver()
2052
+ {
2053
+ self.visit(&receiver);
2054
+ }
2055
+
2056
+ let read_name = String::from_utf8_lossy(node.read_name().as_slice()).to_string();
2057
+ self.index_method_reference(read_name, &node.operator_loc(), method_receiver);
2058
+
2059
+ let write_name = String::from_utf8_lossy(node.write_name().as_slice()).to_string();
2060
+ self.index_method_reference(write_name, &node.operator_loc(), method_receiver);
2061
+
2062
+ self.visit(&node.value());
2063
+ }
2064
+
2065
+ fn visit_call_operator_write_node(&mut self, node: &ruby_prism::CallOperatorWriteNode) {
2066
+ let method_receiver = self.method_receiver(node.receiver().as_ref(), node.location());
2067
+ if method_receiver.is_none()
2068
+ && let Some(receiver) = node.receiver()
2069
+ {
2070
+ self.visit(&receiver);
2071
+ }
2072
+
2073
+ let read_name = String::from_utf8_lossy(node.read_name().as_slice()).to_string();
2074
+ self.index_method_reference(read_name, &node.call_operator_loc().unwrap(), method_receiver);
2075
+
2076
+ let write_name = String::from_utf8_lossy(node.write_name().as_slice()).to_string();
2077
+ self.index_method_reference(write_name, &node.call_operator_loc().unwrap(), method_receiver);
2078
+
2079
+ self.visit(&node.value());
2080
+ }
2081
+
2082
+ fn visit_call_or_write_node(&mut self, node: &ruby_prism::CallOrWriteNode) {
2083
+ let method_receiver = self.method_receiver(node.receiver().as_ref(), node.location());
2084
+ if method_receiver.is_none()
2085
+ && let Some(receiver) = node.receiver()
2086
+ {
2087
+ self.visit(&receiver);
2088
+ }
2089
+
2090
+ let read_name = String::from_utf8_lossy(node.read_name().as_slice()).to_string();
2091
+ self.index_method_reference(read_name, &node.operator_loc(), method_receiver);
2092
+
2093
+ let write_name = String::from_utf8_lossy(node.write_name().as_slice()).to_string();
2094
+ self.index_method_reference(write_name, &node.operator_loc(), method_receiver);
2095
+
2096
+ self.visit(&node.value());
2097
+ }
2098
+
2099
+ fn visit_global_variable_write_node(&mut self, node: &ruby_prism::GlobalVariableWriteNode) {
2100
+ self.add_global_variable_definition(&node.name_loc());
2101
+ self.visit(&node.value());
2102
+ }
2103
+
2104
+ fn visit_global_variable_and_write_node(&mut self, node: &ruby_prism::GlobalVariableAndWriteNode<'_>) {
2105
+ self.add_global_variable_definition(&node.name_loc());
2106
+ self.visit(&node.value());
2107
+ }
2108
+
2109
+ fn visit_global_variable_or_write_node(&mut self, node: &ruby_prism::GlobalVariableOrWriteNode<'_>) {
2110
+ self.add_global_variable_definition(&node.name_loc());
2111
+ self.visit(&node.value());
2112
+ }
2113
+
2114
+ fn visit_global_variable_operator_write_node(&mut self, node: &ruby_prism::GlobalVariableOperatorWriteNode<'_>) {
2115
+ self.add_global_variable_definition(&node.name_loc());
2116
+ self.visit(&node.value());
2117
+ }
2118
+
2119
+ fn visit_instance_variable_and_write_node(&mut self, node: &ruby_prism::InstanceVariableAndWriteNode) {
2120
+ self.add_instance_variable_definition(&node.name_loc());
2121
+ self.visit(&node.value());
2122
+ }
2123
+
2124
+ fn visit_instance_variable_operator_write_node(&mut self, node: &ruby_prism::InstanceVariableOperatorWriteNode) {
2125
+ self.add_instance_variable_definition(&node.name_loc());
2126
+ self.visit(&node.value());
2127
+ }
2128
+
2129
+ fn visit_instance_variable_or_write_node(&mut self, node: &ruby_prism::InstanceVariableOrWriteNode) {
2130
+ self.add_instance_variable_definition(&node.name_loc());
2131
+ self.visit(&node.value());
2132
+ }
2133
+
2134
+ fn visit_instance_variable_write_node(&mut self, node: &ruby_prism::InstanceVariableWriteNode) {
2135
+ self.add_instance_variable_definition(&node.name_loc());
2136
+ self.visit(&node.value());
2137
+ }
2138
+
2139
+ fn visit_class_variable_and_write_node(&mut self, node: &ruby_prism::ClassVariableAndWriteNode) {
2140
+ self.add_class_variable_definition(&node.name_loc());
2141
+ self.visit(&node.value());
2142
+ }
2143
+
2144
+ fn visit_class_variable_operator_write_node(&mut self, node: &ruby_prism::ClassVariableOperatorWriteNode) {
2145
+ self.add_class_variable_definition(&node.name_loc());
2146
+ self.visit(&node.value());
2147
+ }
2148
+
2149
+ fn visit_class_variable_or_write_node(&mut self, node: &ruby_prism::ClassVariableOrWriteNode) {
2150
+ self.add_class_variable_definition(&node.name_loc());
2151
+ self.visit(&node.value());
2152
+ }
2153
+
2154
+ fn visit_class_variable_write_node(&mut self, node: &ruby_prism::ClassVariableWriteNode) {
2155
+ self.add_class_variable_definition(&node.name_loc());
2156
+ self.visit(&node.value());
2157
+ }
2158
+
2159
+ fn visit_block_argument_node(&mut self, node: &ruby_prism::BlockArgumentNode<'_>) {
2160
+ let expression = node.expression();
2161
+ if let Some(expression) = expression {
2162
+ match expression {
2163
+ ruby_prism::Node::SymbolNode { .. } => {
2164
+ let symbol = expression.as_symbol_node().unwrap();
2165
+ let name = Self::location_to_string(&symbol.value_loc().unwrap());
2166
+ self.index_method_reference(name, &node.location(), None);
2167
+ }
2168
+ _ => {
2169
+ self.visit(&expression);
2170
+ }
2171
+ }
2172
+ }
2173
+ }
2174
+
2175
+ fn visit_alias_method_node(&mut self, node: &ruby_prism::AliasMethodNode<'_>) {
2176
+ let mut new_name = if let Some(symbol_node) = node.new_name().as_symbol_node() {
2177
+ Self::location_to_string(&symbol_node.value_loc().unwrap())
2178
+ } else {
2179
+ Self::location_to_string(&node.new_name().location())
2180
+ };
2181
+
2182
+ let mut old_name = if let Some(symbol_node) = node.old_name().as_symbol_node() {
2183
+ Self::location_to_string(&symbol_node.value_loc().unwrap())
2184
+ } else {
2185
+ Self::location_to_string(&node.old_name().location())
2186
+ };
2187
+
2188
+ new_name.push_str("()");
2189
+ old_name.push_str("()");
2190
+
2191
+ let offset = Offset::from_prism_location(&node.location());
2192
+ let (comments, flags) = self.find_comments_for(offset.start());
2193
+ let new_name_str_id = self.intern_string(new_name);
2194
+ let old_name_str_id = self.intern_string(old_name.clone());
2195
+
2196
+ self.operations.push(Operation::AliasMethod(op::AliasMethod {
2197
+ new_name_str_id,
2198
+ old_name_str_id,
2199
+ uri_id: self.uri_id,
2200
+ offset,
2201
+ comments,
2202
+ flags,
2203
+ receiver: None,
2204
+ }));
2205
+
2206
+ self.index_method_reference(old_name, &node.old_name().location(), None);
2207
+ }
2208
+
2209
+ fn visit_alias_global_variable_node(&mut self, node: &ruby_prism::AliasGlobalVariableNode<'_>) {
2210
+ let new_name = Self::location_to_string(&node.new_name().location());
2211
+ let old_name = Self::location_to_string(&node.old_name().location());
2212
+ let new_name_str_id = self.intern_string(new_name);
2213
+ let old_name_str_id = self.intern_string(old_name);
2214
+ let offset = Offset::from_prism_location(&node.location());
2215
+ let (comments, flags) = self.find_comments_for(offset.start());
2216
+
2217
+ self.operations
2218
+ .push(Operation::AliasGlobalVariable(op::AliasGlobalVariable {
2219
+ new_name_str_id,
2220
+ old_name_str_id,
2221
+ uri_id: self.uri_id,
2222
+ offset,
2223
+ comments,
2224
+ flags,
2225
+ }));
2226
+ }
2227
+
2228
+ fn visit_and_node(&mut self, node: &ruby_prism::AndNode) {
2229
+ let left = node.left();
2230
+ let method_receiver = self.method_receiver(Some(&left), left.location());
2231
+
2232
+ if method_receiver.is_none() {
2233
+ self.visit(&left);
2234
+ }
2235
+
2236
+ self.index_method_reference("&&".to_string(), &node.location(), method_receiver);
2237
+ self.visit(&node.right());
2238
+ }
2239
+
2240
+ fn visit_or_node(&mut self, node: &ruby_prism::OrNode) {
2241
+ let left = node.left();
2242
+ let method_receiver = self.method_receiver(Some(&left), left.location());
2243
+
2244
+ if method_receiver.is_none() {
2245
+ self.visit(&left);
2246
+ }
2247
+
2248
+ self.index_method_reference("||".to_string(), &node.location(), method_receiver);
2249
+ self.visit(&node.right());
2250
+ }
2251
+ }
2252
+
2253
+ #[cfg(test)]
2254
+ mod tests {
2255
+ use super::*;
2256
+ use crate::operation::printer;
2257
+
2258
+ fn build_operations(source: &str) -> OperationBuilderResult {
2259
+ let source = crate::test_utils::normalize_indentation(source);
2260
+ let builder = RubyOperationBuilder::new("file:///test.rb".to_string(), &source);
2261
+ builder.build()
2262
+ }
2263
+
2264
+ fn normalize_expected(expected: &str) -> String {
2265
+ crate::test_utils::normalize_indentation(expected).trim().to_string()
2266
+ }
2267
+
2268
+ fn assert_operations(source: &str, expected: &str) {
2269
+ let result = build_operations(source);
2270
+ let actual = printer::print_operations(&result.operations, &result.strings, &result.names, false);
2271
+ let expected = normalize_expected(expected);
2272
+ assert_eq!(actual, expected, "\n\nActual:\n{actual}\n\nExpected:\n{expected}\n");
2273
+ }
2274
+
2275
+ fn assert_operations_with_references(source: &str, expected: &str) {
2276
+ let result = build_operations(source);
2277
+ let actual = printer::print_operations(&result.operations, &result.strings, &result.names, true);
2278
+ let expected = normalize_expected(expected);
2279
+ assert_eq!(actual, expected, "\n\nActual:\n{actual}\n\nExpected:\n{expected}\n");
2280
+ }
2281
+
2282
+ // -- Namespace tests --
2283
+
2284
+ #[test]
2285
+ fn build_class_node() {
2286
+ assert_operations(
2287
+ "
2288
+ class Foo
2289
+ class Bar; end
2290
+ end
2291
+ ",
2292
+ "
2293
+ EnterClass(Foo)
2294
+ EnterClass(Bar)
2295
+ ExitScope
2296
+ ExitScope
2297
+ ",
2298
+ );
2299
+ }
2300
+
2301
+ #[test]
2302
+ fn build_class_with_qualified_name() {
2303
+ assert_operations(
2304
+ "
2305
+ class Foo::Bar; end
2306
+ ",
2307
+ "
2308
+ EnterClass(Foo::Bar)
2309
+ ExitScope
2310
+ ",
2311
+ );
2312
+ }
2313
+
2314
+ #[test]
2315
+ fn build_class_with_superclass() {
2316
+ assert_operations(
2317
+ "
2318
+ class Foo < Bar; end
2319
+ ",
2320
+ "
2321
+ EnterClass(Foo, superclass: Bar)
2322
+ ExitScope
2323
+ ",
2324
+ );
2325
+ }
2326
+
2327
+ #[test]
2328
+ fn build_module_node() {
2329
+ assert_operations(
2330
+ "
2331
+ module Foo
2332
+ module Bar; end
2333
+ end
2334
+ ",
2335
+ "
2336
+ EnterModule(Foo)
2337
+ EnterModule(Bar)
2338
+ ExitScope
2339
+ ExitScope
2340
+ ",
2341
+ );
2342
+ }
2343
+
2344
+ #[test]
2345
+ fn build_singleton_class() {
2346
+ assert_operations(
2347
+ "
2348
+ class Foo
2349
+ class << self
2350
+ def bar; end
2351
+ end
2352
+ end
2353
+ ",
2354
+ "
2355
+ EnterClass(Foo)
2356
+ EnterSingletonClass(Foo::<Foo>)
2357
+ EnterMethod(bar())
2358
+ ExitScope
2359
+ ExitScope
2360
+ ExitScope
2361
+ ",
2362
+ );
2363
+ }
2364
+
2365
+ // -- Method tests --
2366
+
2367
+ #[test]
2368
+ fn build_def_node() {
2369
+ assert_operations(
2370
+ "
2371
+ def foo; end
2372
+
2373
+ class Foo
2374
+ def bar; end
2375
+ def self.baz; end
2376
+ end
2377
+ ",
2378
+ "
2379
+ EnterMethod(foo())
2380
+ ExitScope
2381
+ EnterClass(Foo)
2382
+ EnterMethod(bar())
2383
+ ExitScope
2384
+ EnterMethod(self.baz())
2385
+ ExitScope
2386
+ ExitScope
2387
+ ",
2388
+ );
2389
+ }
2390
+
2391
+ #[test]
2392
+ fn build_def_node_with_constant_receiver() {
2393
+ assert_operations(
2394
+ "
2395
+ class Bar
2396
+ def Foo.quz; end
2397
+ end
2398
+ ",
2399
+ "
2400
+ EnterClass(Bar)
2401
+ EnterMethod(Foo.quz())
2402
+ ExitScope
2403
+ ExitScope
2404
+ ",
2405
+ );
2406
+ }
2407
+
2408
+ // -- Visibility tests --
2409
+
2410
+ #[test]
2411
+ fn build_default_visibility() {
2412
+ assert_operations(
2413
+ "
2414
+ class Foo
2415
+ private
2416
+
2417
+ def m1; end
2418
+
2419
+ public
2420
+
2421
+ def m2; end
2422
+ end
2423
+ ",
2424
+ "
2425
+ EnterClass(Foo)
2426
+ SetDefaultVisibility(private)
2427
+ EnterMethod(m1())
2428
+ ExitScope
2429
+ SetDefaultVisibility(public)
2430
+ EnterMethod(m2())
2431
+ ExitScope
2432
+ ExitScope
2433
+ ",
2434
+ );
2435
+ }
2436
+
2437
+ #[test]
2438
+ fn build_inline_visibility() {
2439
+ assert_operations(
2440
+ "
2441
+ protected def m1; end
2442
+ ",
2443
+ "
2444
+ SetDefaultVisibility(protected)
2445
+ EnterMethod(m1())
2446
+ ExitScope
2447
+ SetDefaultVisibility(private)
2448
+ ",
2449
+ );
2450
+ }
2451
+
2452
+ // TODO: `private :bar` with symbol args should produce SetMethodVisibility operations.
2453
+ // This is one of the key motivations for the operation-based approach.
2454
+
2455
+ #[test]
2456
+ fn build_module_function() {
2457
+ assert_operations(
2458
+ "
2459
+ module Foo
2460
+ module_function
2461
+
2462
+ def bar; end
2463
+ end
2464
+ ",
2465
+ "
2466
+ EnterModule(Foo)
2467
+ SetDefaultVisibility(module_function)
2468
+ EnterMethod(self.bar())
2469
+ ExitScope
2470
+ EnterMethod(bar())
2471
+ ExitScope
2472
+ ExitScope
2473
+ ",
2474
+ );
2475
+ }
2476
+
2477
+ #[test]
2478
+ fn build_module_function_with_ivar() {
2479
+ assert_operations(
2480
+ "
2481
+ module Foo
2482
+ module_function
2483
+
2484
+ def bar
2485
+ @x = 1
2486
+ end
2487
+ end
2488
+ ",
2489
+ "
2490
+ EnterModule(Foo)
2491
+ SetDefaultVisibility(module_function)
2492
+ EnterMethod(self.bar())
2493
+ DefineInstanceVariable(@x)
2494
+ ExitScope
2495
+ EnterMethod(bar())
2496
+ DefineInstanceVariable(@x)
2497
+ ExitScope
2498
+ ExitScope
2499
+ ",
2500
+ );
2501
+ }
2502
+
2503
+ // -- Constant tests --
2504
+
2505
+ #[test]
2506
+ fn build_constant_write() {
2507
+ assert_operations(
2508
+ "
2509
+ FOO = 1
2510
+
2511
+ class Bar
2512
+ BAZ = 2
2513
+ end
2514
+ ",
2515
+ "
2516
+ DefineConstant(FOO)
2517
+ EnterClass(Bar)
2518
+ DefineConstant(BAZ)
2519
+ ExitScope
2520
+ ",
2521
+ );
2522
+ }
2523
+
2524
+ #[test]
2525
+ fn build_constant_path_write() {
2526
+ assert_operations(
2527
+ "
2528
+ FOO::BAR = 1
2529
+ ",
2530
+ "
2531
+ DefineConstant(FOO::BAR)
2532
+ ",
2533
+ );
2534
+ }
2535
+
2536
+ #[test]
2537
+ fn build_constant_alias() {
2538
+ assert_operations(
2539
+ "
2540
+ ALIAS = OtherConstant
2541
+ ",
2542
+ "
2543
+ AliasConstant(ALIAS -> OtherConstant)
2544
+ ",
2545
+ );
2546
+ }
2547
+
2548
+ #[test]
2549
+ fn build_set_constant_visibility() {
2550
+ assert_operations(
2551
+ "
2552
+ module Foo
2553
+ BAR = 42
2554
+ private_constant :BAR
2555
+ end
2556
+ ",
2557
+ "
2558
+ EnterModule(Foo)
2559
+ DefineConstant(BAR)
2560
+ SetConstantVisibility(BAR, vis: private)
2561
+ ExitScope
2562
+ ",
2563
+ );
2564
+ }
2565
+
2566
+ #[test]
2567
+ fn build_public_constant() {
2568
+ assert_operations(
2569
+ "
2570
+ module Foo
2571
+ BAR = 42
2572
+ public_constant :BAR
2573
+ end
2574
+ ",
2575
+ "
2576
+ EnterModule(Foo)
2577
+ DefineConstant(BAR)
2578
+ SetConstantVisibility(BAR, vis: public)
2579
+ ExitScope
2580
+ ",
2581
+ );
2582
+ }
2583
+
2584
+ #[test]
2585
+ fn build_private_constant_multiple() {
2586
+ assert_operations(
2587
+ "
2588
+ module Foo
2589
+ BAR = 42
2590
+ BAZ = 43
2591
+ private_constant :BAR, :BAZ
2592
+ end
2593
+ ",
2594
+ "
2595
+ EnterModule(Foo)
2596
+ DefineConstant(BAR)
2597
+ DefineConstant(BAZ)
2598
+ SetConstantVisibility(BAR, vis: private)
2599
+ SetConstantVisibility(BAZ, vis: private)
2600
+ ExitScope
2601
+ ",
2602
+ );
2603
+ }
2604
+
2605
+ // -- Attribute tests --
2606
+
2607
+ #[test]
2608
+ fn build_attr_accessor() {
2609
+ assert_operations(
2610
+ "
2611
+ class Foo
2612
+ attr_accessor :bar
2613
+ attr_reader :baz
2614
+ attr_writer :qux
2615
+ end
2616
+ ",
2617
+ "
2618
+ EnterClass(Foo)
2619
+ DefineAttribute(accessor bar())
2620
+ DefineAttribute(reader baz())
2621
+ DefineAttribute(writer qux())
2622
+ ExitScope
2623
+ ",
2624
+ );
2625
+ }
2626
+
2627
+ #[test]
2628
+ fn build_multiple_attr_accessors() {
2629
+ assert_operations(
2630
+ "
2631
+ class Foo
2632
+ attr_accessor :bar, :baz
2633
+ end
2634
+ ",
2635
+ "
2636
+ EnterClass(Foo)
2637
+ DefineAttribute(accessor bar())
2638
+ DefineAttribute(accessor baz())
2639
+ ExitScope
2640
+ ",
2641
+ );
2642
+ }
2643
+
2644
+ #[test]
2645
+ fn build_attr_with_visibility() {
2646
+ assert_operations(
2647
+ "
2648
+ class Foo
2649
+ private
2650
+
2651
+ attr_reader :bar
2652
+ end
2653
+ ",
2654
+ "
2655
+ EnterClass(Foo)
2656
+ SetDefaultVisibility(private)
2657
+ DefineAttribute(reader bar())
2658
+ ExitScope
2659
+ ",
2660
+ );
2661
+ }
2662
+
2663
+ // -- Mixin tests --
2664
+
2665
+ #[test]
2666
+ fn build_mixins() {
2667
+ assert_operations(
2668
+ "
2669
+ class Foo
2670
+ include Bar
2671
+ prepend Baz
2672
+ extend Qux
2673
+ end
2674
+ ",
2675
+ "
2676
+ EnterClass(Foo)
2677
+ Mixin(include, Bar)
2678
+ Mixin(prepend, Baz)
2679
+ Mixin(extend, Qux)
2680
+ ExitScope
2681
+ ",
2682
+ );
2683
+ }
2684
+
2685
+ // -- Alias tests --
2686
+
2687
+ #[test]
2688
+ fn build_alias_method() {
2689
+ assert_operations(
2690
+ "
2691
+ class Foo
2692
+ alias foo bar
2693
+ end
2694
+ ",
2695
+ "
2696
+ EnterClass(Foo)
2697
+ AliasMethod(foo() -> bar())
2698
+ ExitScope
2699
+ ",
2700
+ );
2701
+ }
2702
+
2703
+ #[test]
2704
+ fn build_alias_method_call() {
2705
+ assert_operations(
2706
+ "
2707
+ class Foo
2708
+ alias_method :new_name, :old_name
2709
+ end
2710
+ ",
2711
+ "
2712
+ EnterClass(Foo)
2713
+ AliasMethod(new_name() -> old_name())
2714
+ ExitScope
2715
+ ",
2716
+ );
2717
+ }
2718
+
2719
+ #[test]
2720
+ fn build_alias_global_variable() {
2721
+ assert_operations(
2722
+ "
2723
+ alias $new $old
2724
+ ",
2725
+ "
2726
+ AliasGlobalVariable($new -> $old)
2727
+ ",
2728
+ );
2729
+ }
2730
+
2731
+ // -- Variable tests --
2732
+
2733
+ #[test]
2734
+ fn build_instance_variable() {
2735
+ assert_operations(
2736
+ "
2737
+ class Foo
2738
+ def initialize
2739
+ @bar = 1
2740
+ end
2741
+ end
2742
+ ",
2743
+ "
2744
+ EnterClass(Foo)
2745
+ EnterMethod(initialize())
2746
+ DefineInstanceVariable(@bar)
2747
+ ExitScope
2748
+ ExitScope
2749
+ ",
2750
+ );
2751
+ }
2752
+
2753
+ #[test]
2754
+ fn build_class_variable() {
2755
+ assert_operations(
2756
+ "
2757
+ class Foo
2758
+ @@bar = 1
2759
+ end
2760
+ ",
2761
+ "
2762
+ EnterClass(Foo)
2763
+ DefineClassVariable(@@bar)
2764
+ ExitScope
2765
+ ",
2766
+ );
2767
+ }
2768
+
2769
+ #[test]
2770
+ fn build_global_variable() {
2771
+ assert_operations(
2772
+ "
2773
+ $foo = 1
2774
+ ",
2775
+ "
2776
+ DefineGlobalVariable($foo)
2777
+ ",
2778
+ );
2779
+ }
2780
+
2781
+ // -- Reference tests --
2782
+
2783
+ #[test]
2784
+ fn build_constant_references() {
2785
+ assert_operations_with_references(
2786
+ "
2787
+ Foo
2788
+ ",
2789
+ "
2790
+ ReferenceConstant(Foo)
2791
+ ",
2792
+ );
2793
+ }
2794
+
2795
+ #[test]
2796
+ fn build_method_references() {
2797
+ assert_operations_with_references(
2798
+ "
2799
+ foo
2800
+ ",
2801
+ "
2802
+ ReferenceMethod(foo)
2803
+ ",
2804
+ );
2805
+ }
2806
+
2807
+ // -- Ordering tests --
2808
+
2809
+ #[test]
2810
+ fn build_operations_ordering_with_visibility() {
2811
+ assert_operations(
2812
+ "
2813
+ class Foo
2814
+ def m1; end
2815
+ private
2816
+ def m2; end
2817
+ end
2818
+ ",
2819
+ "
2820
+ EnterClass(Foo)
2821
+ EnterMethod(m1())
2822
+ ExitScope
2823
+ SetDefaultVisibility(private)
2824
+ EnterMethod(m2())
2825
+ ExitScope
2826
+ ExitScope
2827
+ ",
2828
+ );
2829
+ }
2830
+
2831
+ #[test]
2832
+ fn build_visibility_resets_in_nested_class() {
2833
+ assert_operations(
2834
+ "
2835
+ class Foo
2836
+ private
2837
+
2838
+ class Bar
2839
+ def m1; end
2840
+ end
2841
+
2842
+ def m2; end
2843
+ end
2844
+ ",
2845
+ "
2846
+ EnterClass(Foo)
2847
+ SetDefaultVisibility(private)
2848
+ EnterClass(Bar)
2849
+ EnterMethod(m1())
2850
+ ExitScope
2851
+ ExitScope
2852
+ EnterMethod(m2())
2853
+ ExitScope
2854
+ ExitScope
2855
+ ",
2856
+ );
2857
+ }
2858
+
2859
+ #[test]
2860
+ fn build_visibility_in_singleton_class() {
2861
+ assert_operations(
2862
+ "
2863
+ class Foo
2864
+ protected
2865
+
2866
+ class << self
2867
+ def m1; end
2868
+
2869
+ private
2870
+
2871
+ def m2; end
2872
+ end
2873
+
2874
+ def m3; end
2875
+ end
2876
+ ",
2877
+ "
2878
+ EnterClass(Foo)
2879
+ SetDefaultVisibility(protected)
2880
+ EnterSingletonClass(Foo::<Foo>)
2881
+ EnterMethod(m1())
2882
+ ExitScope
2883
+ SetDefaultVisibility(private)
2884
+ EnterMethod(m2())
2885
+ ExitScope
2886
+ ExitScope
2887
+ EnterMethod(m3())
2888
+ ExitScope
2889
+ ExitScope
2890
+ ",
2891
+ );
2892
+ }
2893
+
2894
+ #[test]
2895
+ fn build_top_level_method_visibility() {
2896
+ assert_operations(
2897
+ "
2898
+ def m1; end
2899
+
2900
+ protected def m2; end
2901
+
2902
+ public
2903
+
2904
+ def m3; end
2905
+ ",
2906
+ "
2907
+ EnterMethod(m1())
2908
+ ExitScope
2909
+ SetDefaultVisibility(protected)
2910
+ EnterMethod(m2())
2911
+ ExitScope
2912
+ SetDefaultVisibility(private)
2913
+ SetDefaultVisibility(public)
2914
+ EnterMethod(m3())
2915
+ ExitScope
2916
+ ",
2917
+ );
2918
+ }
2919
+ }