rubydex 0.2.9 → 0.4.0

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 (96) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +110 -6
  3. data/THIRD_PARTY_LICENSES.html +271 -2
  4. data/exe/rdx +2 -73
  5. data/ext/rubydex/config.c +140 -0
  6. data/ext/rubydex/config.h +16 -0
  7. data/ext/rubydex/declaration.c +1 -1
  8. data/ext/rubydex/definition.c +32 -4
  9. data/ext/rubydex/diagnostic.c +75 -1
  10. data/ext/rubydex/diagnostic.h +2 -0
  11. data/ext/rubydex/graph.c +27 -48
  12. data/ext/rubydex/graph.h +6 -0
  13. data/ext/rubydex/query.c +487 -0
  14. data/ext/rubydex/query.h +8 -0
  15. data/ext/rubydex/reference.c +60 -0
  16. data/ext/rubydex/rubydex.c +4 -0
  17. data/ext/rubydex/utils.c +23 -4
  18. data/ext/rubydex/utils.h +5 -0
  19. data/lib/ruby_lsp/rubydex/addon.rb +211 -0
  20. data/lib/rubydex/cli/command/console.rb +55 -0
  21. data/lib/rubydex/cli/command/lint/explain.rb +74 -0
  22. data/lib/rubydex/cli/command/lint.rb +202 -0
  23. data/lib/rubydex/cli/command/mcp.rb +30 -0
  24. data/lib/rubydex/cli/command/query.rb +70 -0
  25. data/lib/rubydex/cli/command/skill.rb +69 -0
  26. data/lib/rubydex/cli/command.rb +168 -0
  27. data/lib/rubydex/cli.rb +93 -0
  28. data/lib/rubydex/config.rb +59 -0
  29. data/lib/rubydex/diagnostic.rb +12 -3
  30. data/lib/rubydex/errors.rb +42 -1
  31. data/lib/rubydex/graph.rb +10 -3
  32. data/lib/rubydex/linter/custom_rule.rb +97 -0
  33. data/lib/rubydex/linter/helpers/path_helpers.rb +78 -0
  34. data/lib/rubydex/linter/helpers/source_access_helpers.rb +31 -0
  35. data/lib/rubydex/linter/rule_loader.rb +36 -0
  36. data/lib/rubydex/linter/rule_test_case.rb +343 -0
  37. data/lib/rubydex/linter/runner.rb +56 -0
  38. data/lib/rubydex/linter.rb +19 -0
  39. data/lib/rubydex/location.rb +3 -0
  40. data/lib/rubydex/mcp_server.rb +1 -2
  41. data/lib/rubydex/related_information.rb +17 -0
  42. data/lib/rubydex/rule.rb +33 -0
  43. data/lib/rubydex/severity.rb +70 -0
  44. data/lib/rubydex/skill.rb +88 -0
  45. data/lib/rubydex/skill_registry.rb +62 -0
  46. data/lib/rubydex/version.rb +1 -1
  47. data/lib/rubydex.rb +6 -0
  48. data/lib/rubydex_linter/rules/rule_structure.rb +125 -0
  49. data/rbi/rubydex.rbi +578 -15
  50. data/rust/Cargo.lock +7 -0
  51. data/rust/rubydex/Cargo.toml +1 -0
  52. data/rust/rubydex/benches/graph_memory.rs +20 -4
  53. data/rust/rubydex/src/compile_assertions.rs +15 -0
  54. data/rust/rubydex/src/config.rs +538 -157
  55. data/rust/rubydex/src/diagnostic.rs +66 -40
  56. data/rust/rubydex/src/errors.rs +0 -1
  57. data/rust/rubydex/src/indexing/local_graph.rs +6 -5
  58. data/rust/rubydex/src/indexing/rbs_indexer.rs +284 -8
  59. data/rust/rubydex/src/indexing/ruby_indexer.rs +59 -70
  60. data/rust/rubydex/src/indexing/ruby_indexer_tests.rs +195 -86
  61. data/rust/rubydex/src/lib.rs +1 -0
  62. data/rust/rubydex/src/listing.rs +26 -1
  63. data/rust/rubydex/src/main.rs +9 -128
  64. data/rust/rubydex/src/model/declaration.rs +301 -229
  65. data/rust/rubydex/src/model/definitions.rs +27 -26
  66. data/rust/rubydex/src/model/document.rs +43 -7
  67. data/rust/rubydex/src/model/graph.rs +67 -68
  68. data/rust/rubydex/src/model/id.rs +55 -0
  69. data/rust/rubydex/src/model/ids.rs +21 -9
  70. data/rust/rubydex/src/model/name.rs +88 -19
  71. data/rust/rubydex/src/model/references.rs +16 -13
  72. data/rust/rubydex/src/operation/ruby_builder.rs +78 -104
  73. data/rust/rubydex/src/path_helpers.rs +77 -0
  74. data/rust/rubydex/src/query/cypher/schema.rs +853 -0
  75. data/rust/rubydex/src/query/cypher/schema_info.rs +161 -0
  76. data/rust/rubydex/src/query/cypher/tests.rs +253 -0
  77. data/rust/rubydex/src/query/cypher.rs +54 -0
  78. data/rust/rubydex/src/query.rs +125 -43
  79. data/rust/rubydex/src/resolution.rs +368 -395
  80. data/rust/rubydex/src/resolution_tests.rs +504 -78
  81. data/rust/rubydex/src/test_utils/context.rs +2 -1
  82. data/rust/rubydex/src/test_utils/graph_test.rs +26 -12
  83. data/rust/rubydex/src/test_utils/local_graph_test.rs +19 -0
  84. data/rust/rubydex/tests/cli.rs +4 -4
  85. data/rust/rubydex-sys/src/config_api.rs +205 -0
  86. data/rust/rubydex-sys/src/cypher_api.rs +791 -0
  87. data/rust/rubydex-sys/src/declaration_api.rs +6 -3
  88. data/rust/rubydex-sys/src/definition_api.rs +27 -7
  89. data/rust/rubydex-sys/src/diagnostic_api.rs +77 -8
  90. data/rust/rubydex-sys/src/graph_api.rs +31 -68
  91. data/rust/rubydex-sys/src/lib.rs +2 -0
  92. data/rust/rubydex-sys/src/name_api.rs +2 -6
  93. data/rust/rubydex-sys/src/reference_api.rs +58 -12
  94. data/rust/rubydex-sys/src/utils.rs +37 -0
  95. data/skills/send-private-method/SKILL.md +133 -0
  96. metadata +37 -2
@@ -2,7 +2,11 @@ use std::fmt::Display;
2
2
 
3
3
  use crate::{
4
4
  assert_mem_size,
5
- model::ids::{DeclarationId, NameId, StringId},
5
+ model::{
6
+ id::id_from_parts,
7
+ identity_maps::IdentityHashMap,
8
+ ids::{DeclarationId, NameId, StringId},
9
+ },
6
10
  };
7
11
 
8
12
  #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -90,31 +94,58 @@ pub struct Name {
90
94
  /// list of names to represent the nesting
91
95
  nesting: Option<NameId>,
92
96
  ref_count: u32,
97
+ /// The depth of the name, which combines the depths of the parent scope and nesting
98
+ depth: u16,
93
99
  }
100
+ assert_mem_size!(Name, 40);
94
101
 
95
102
  impl PartialEq for Name {
96
103
  fn eq(&self, other: &Self) -> bool {
97
104
  self.str == other.str && self.parent_scope == other.parent_scope && self.nesting == other.nesting
98
105
  }
99
106
  }
100
- assert_mem_size!(Name, 40);
101
107
 
102
108
  impl Name {
103
109
  #[must_use]
104
- pub fn new(str: StringId, parent_scope: ParentScope, nesting: Option<NameId>) -> Self {
110
+ pub fn new(
111
+ names: &IdentityHashMap<NameId, NameRef>,
112
+ str: StringId,
113
+ parent_scope: ParentScope,
114
+ nesting: Option<NameId>,
115
+ ) -> Self {
105
116
  Self {
106
117
  str,
107
118
  parent_scope,
108
119
  nesting,
109
120
  ref_count: 1,
121
+ depth: Self::name_depth(names, parent_scope, nesting),
110
122
  }
111
123
  }
112
124
 
125
+ #[must_use]
126
+ fn name_depth(names: &IdentityHashMap<NameId, NameRef>, parent_scope: ParentScope, nesting: Option<NameId>) -> u16 {
127
+ if parent_scope.is_top_level() {
128
+ return 1;
129
+ }
130
+
131
+ let parent_depth = parent_scope
132
+ .as_ref()
133
+ .map_or(0, |id| names.get(id).map_or(0, NameRef::depth));
134
+ let nesting_depth = nesting.map_or(0, |id| names.get(&id).map_or(0, NameRef::depth));
135
+
136
+ parent_depth.saturating_add(nesting_depth).saturating_add(1)
137
+ }
138
+
113
139
  #[must_use]
114
140
  pub fn str(&self) -> &StringId {
115
141
  &self.str
116
142
  }
117
143
 
144
+ #[must_use]
145
+ pub fn depth(&self) -> u16 {
146
+ self.depth
147
+ }
148
+
118
149
  #[must_use]
119
150
  pub fn parent_scope(&self) -> &ParentScope {
120
151
  &self.parent_scope
@@ -127,12 +158,19 @@ impl Name {
127
158
 
128
159
  #[must_use]
129
160
  pub fn id(&self) -> NameId {
130
- NameId::from(&format!(
131
- "{}{}{}",
132
- self.str,
133
- self.parent_scope,
134
- self.nesting.map_or(String::from("None"), |id| id.to_string())
135
- ))
161
+ // We need to include both the variant and the bytes of the parent scope when present
162
+ let (parent_tag, parent_id): (u8, u64) = match self.parent_scope {
163
+ ParentScope::None => (0, 0),
164
+ ParentScope::TopLevel => (1, 0),
165
+ ParentScope::Some(id) => (2, id.get()),
166
+ ParentScope::Attached(id) => (3, id.get()),
167
+ };
168
+ let (nesting_tag, nesting_id): (u8, u64) = match self.nesting {
169
+ None => (0, 0),
170
+ Some(id) => (1, id.get()),
171
+ };
172
+
173
+ id_from_parts!(NameId; self.str.get(), parent_tag, parent_id, nesting_tag, nesting_id)
136
174
  }
137
175
  }
138
176
 
@@ -217,6 +255,14 @@ impl NameRef {
217
255
  }
218
256
  }
219
257
 
258
+ #[must_use]
259
+ pub fn depth(&self) -> u16 {
260
+ match self {
261
+ NameRef::Unresolved(name) => name.depth(),
262
+ NameRef::Resolved(resolved_name) => resolved_name.name.depth(),
263
+ }
264
+ }
265
+
220
266
  /// # Panics
221
267
  ///
222
268
  /// Panics if we exceed the maximum size of the reference count
@@ -271,30 +317,53 @@ mod tests {
271
317
 
272
318
  #[test]
273
319
  fn same_parent_scope_and_nesting() {
274
- let name_1 = Name::new(StringId::from("Foo"), ParentScope::None, None);
275
- let name_2 = Name::new(StringId::from("Foo"), ParentScope::None, None);
320
+ let names = IdentityHashMap::default();
321
+
322
+ let name_1 = Name::new(&names, StringId::from("Foo"), ParentScope::None, None);
323
+ let name_2 = Name::new(&names, StringId::from("Foo"), ParentScope::None, None);
276
324
  assert_eq!(name_1.id(), name_2.id());
277
325
 
278
- let name_3 = Name::new(StringId::from("Foo"), ParentScope::Some(name_1.id()), None);
279
- let name_4 = Name::new(StringId::from("Foo"), ParentScope::Some(name_2.id()), None);
326
+ let name_3 = Name::new(&names, StringId::from("Foo"), ParentScope::Some(name_1.id()), None);
327
+ let name_4 = Name::new(&names, StringId::from("Foo"), ParentScope::Some(name_2.id()), None);
280
328
  assert_eq!(name_3.id(), name_4.id());
281
329
 
282
- let name_5 = Name::new(StringId::from("Foo"), ParentScope::None, Some(name_1.id()));
283
- let name_6 = Name::new(StringId::from("Foo"), ParentScope::None, Some(name_2.id()));
330
+ let name_5 = Name::new(&names, StringId::from("Foo"), ParentScope::None, Some(name_1.id()));
331
+ let name_6 = Name::new(&names, StringId::from("Foo"), ParentScope::None, Some(name_2.id()));
284
332
  assert_eq!(name_5.id(), name_6.id());
285
333
  assert_ne!(name_3.id(), name_5.id());
286
334
  assert_ne!(name_4.id(), name_6.id());
287
335
 
288
336
  let name_7 = Name::new(
337
+ &names,
289
338
  StringId::from("Foo"),
290
- ParentScope::Some(Name::new(StringId::from("Foo"), ParentScope::None, None).id()),
291
- Some(Name::new(StringId::from("Foo"), ParentScope::None, None).id()),
339
+ ParentScope::Some(Name::new(&names, StringId::from("Foo"), ParentScope::None, None).id()),
340
+ Some(Name::new(&names, StringId::from("Foo"), ParentScope::None, None).id()),
292
341
  );
293
342
  let name_8 = Name::new(
343
+ &names,
294
344
  StringId::from("Foo"),
295
- ParentScope::Some(Name::new(StringId::from("Foo"), ParentScope::None, None).id()),
296
- Some(Name::new(StringId::from("Foo"), ParentScope::None, None).id()),
345
+ ParentScope::Some(Name::new(&names, StringId::from("Foo"), ParentScope::None, None).id()),
346
+ Some(Name::new(&names, StringId::from("Foo"), ParentScope::None, None).id()),
297
347
  );
298
348
  assert_eq!(name_7.id(), name_8.id());
299
349
  }
350
+
351
+ #[test]
352
+ fn parent_scope_variants_are_distinct() {
353
+ let names = IdentityHashMap::default();
354
+ let inner = Name::new(&names, StringId::from("Foo"), ParentScope::None, None).id();
355
+
356
+ let ids = [
357
+ Name::new(&names, StringId::from("Foo"), ParentScope::None, None).id(),
358
+ Name::new(&names, StringId::from("Foo"), ParentScope::TopLevel, None).id(),
359
+ Name::new(&names, StringId::from("Foo"), ParentScope::Some(inner), None).id(),
360
+ Name::new(&names, StringId::from("Foo"), ParentScope::Attached(inner), None).id(),
361
+ ];
362
+
363
+ for (i, a) in ids.iter().enumerate() {
364
+ for b in &ids[i + 1..] {
365
+ assert_ne!(a, b);
366
+ }
367
+ }
368
+ }
300
369
  }
@@ -1,6 +1,9 @@
1
1
  use crate::{
2
2
  assert_mem_size,
3
- model::ids::{ConstantReferenceId, MethodReferenceId, NameId, StringId, UriId},
3
+ model::{
4
+ id::id_from_parts,
5
+ ids::{ConstantReferenceId, MethodReferenceId, NameId, StringId, UriId},
6
+ },
4
7
  offset::Offset,
5
8
  };
6
9
 
@@ -43,13 +46,13 @@ impl ConstantReference {
43
46
 
44
47
  #[must_use]
45
48
  pub fn id(&self) -> ConstantReferenceId {
46
- ConstantReferenceId::from(&format!(
47
- "{}:{}:{}-{}",
48
- self.name_id,
49
- self.uri_id,
49
+ id_from_parts!(
50
+ ConstantReferenceId;
51
+ self.name_id.get(),
52
+ self.uri_id.get(),
50
53
  self.offset.start(),
51
- self.offset.end()
52
- ))
54
+ self.offset.end(),
55
+ )
53
56
  }
54
57
  }
55
58
 
@@ -100,12 +103,12 @@ impl MethodRef {
100
103
 
101
104
  #[must_use]
102
105
  pub fn id(&self) -> MethodReferenceId {
103
- MethodReferenceId::from(&format!(
104
- "{}:{}:{}-{}",
105
- self.str,
106
- self.uri_id,
106
+ id_from_parts!(
107
+ MethodReferenceId;
108
+ self.str.get(),
109
+ self.uri_id.get(),
107
110
  self.offset.start(),
108
- self.offset.end()
109
- ))
111
+ self.offset.end(),
112
+ )
110
113
  }
111
114
  }
@@ -154,7 +154,8 @@ impl<'a> RubyOperationBuilder<'a> {
154
154
  string_id
155
155
  }
156
156
 
157
- fn add_name(&mut self, name: Name) -> NameId {
157
+ fn add_name(&mut self, str: StringId, parent_scope: ParentScope, nesting: Option<NameId>) -> NameId {
158
+ let name = Name::new(&self.names, str, parent_scope, nesting);
158
159
  let name_id = name.id();
159
160
 
160
161
  match self.names.entry(name_id) {
@@ -360,11 +361,7 @@ impl<'a> RubyOperationBuilder<'a> {
360
361
  let offset = Offset::from_prism_location(&location);
361
362
  let name = Self::location_to_string(&location);
362
363
  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
- ));
364
+ let name_id = self.add_name(string_id, parent_scope_id, self.current_lexical_scope_name_id());
368
365
 
369
366
  if push_final_reference {
370
367
  self.operations
@@ -443,7 +440,7 @@ impl<'a> RubyOperationBuilder<'a> {
443
440
  }
444
441
  None => {
445
442
  let str_id = self.intern_string("Object".into());
446
- Some(self.add_name(Name::new(str_id, ParentScope::None, None)))
443
+ Some(self.add_name(str_id, ParentScope::None, None))
447
444
  }
448
445
  },
449
446
  Some(ruby_prism::Node::CallNode { .. }) => {
@@ -477,7 +474,7 @@ impl<'a> RubyOperationBuilder<'a> {
477
474
  };
478
475
 
479
476
  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));
477
+ let new_name_id = self.add_name(string_id, ParentScope::Attached(name_id), None);
481
478
 
482
479
  let location = receiver.map_or(fallback_location, ruby_prism::Node::location);
483
480
  let offset = Offset::from_prism_location(&location);
@@ -611,20 +608,8 @@ impl<'a> RubyOperationBuilder<'a> {
611
608
  {
612
609
  if let Some(arguments) = node.arguments() {
613
610
  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
- _ => {}
611
+ if let Some((name, location)) = Self::extract_literal_name(&argument) {
612
+ f(name, location);
628
613
  }
629
614
  }
630
615
  }
@@ -701,10 +686,7 @@ impl<'a> RubyOperationBuilder<'a> {
701
686
  )
702
687
  } else {
703
688
  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
- )
689
+ (Some(self.add_name(string_id, ParentScope::None, None)), offset.clone())
708
690
  };
709
691
 
710
692
  if let Some(name_id) = name_id {
@@ -766,10 +748,7 @@ impl<'a> RubyOperationBuilder<'a> {
766
748
  )
767
749
  } else {
768
750
  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
- )
751
+ (Some(self.add_name(string_id, ParentScope::None, None)), offset.clone())
773
752
  };
774
753
 
775
754
  if let Some(name_id) = name_id {
@@ -861,7 +840,7 @@ impl<'a> RubyOperationBuilder<'a> {
861
840
  }
862
841
 
863
842
  Some((
864
- self.current_lexical_scope_name_id().unwrap(),
843
+ self.current_owner_name_id().unwrap(),
865
844
  Offset::from_prism_location(&arg.location()),
866
845
  ))
867
846
  } else if let Some(name_id) = self.index_constant_reference(&arg, false) {
@@ -899,28 +878,29 @@ impl<'a> RubyOperationBuilder<'a> {
899
878
 
900
879
  fn handle_constant_visibility(&mut self, node: &ruby_prism::CallNode, visibility: Visibility) {
901
880
  let receiver = node.receiver();
881
+ let call_name = String::from_utf8_lossy(node.name().as_slice());
902
882
 
903
- let receiver_name_id = match receiver {
883
+ let receiver_name_id = match &receiver {
904
884
  Some(ruby_prism::Node::ConstantPathNode { .. } | ruby_prism::Node::ConstantReadNode { .. }) => {
905
- self.index_constant_reference(&receiver.unwrap(), true)
885
+ self.index_constant_reference(receiver.as_ref().unwrap(), true)
906
886
  }
907
887
  Some(ruby_prism::Node::SelfNode { .. }) | None => match self.nesting_stack.last() {
908
888
  Some(Nesting::Method { .. }) => return,
909
889
  None => {
910
890
  self.add_diagnostic(
911
- Rule::InvalidPrivateConstant,
891
+ Rule::InvalidConstantVisibility,
912
892
  Offset::from_prism_location(&node.location()),
913
- "Private constant called at top level".to_string(),
893
+ format!("`{call_name}` called at top level"),
914
894
  );
915
895
  return;
916
896
  }
917
897
  _ => None,
918
898
  },
919
- _ => {
899
+ Some(other) => {
920
900
  self.add_diagnostic(
921
- Rule::InvalidPrivateConstant,
922
- Offset::from_prism_location(&node.location()),
923
- "Dynamic receiver for private constant".to_string(),
901
+ Rule::InvalidConstantVisibility,
902
+ Offset::from_prism_location(&other.location()),
903
+ format!("Dynamic receiver for `{call_name}`"),
924
904
  );
925
905
  return;
926
906
  }
@@ -931,28 +911,13 @@ impl<'a> RubyOperationBuilder<'a> {
931
911
  };
932
912
 
933
913
  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
- }
914
+ let Some((name, location)) = Self::extract_literal_name(&argument) else {
915
+ self.add_diagnostic(
916
+ Rule::InvalidConstantVisibility,
917
+ Offset::from_prism_location(&argument.location()),
918
+ format!("`{call_name}` called with a non-literal argument"),
919
+ );
920
+ continue;
956
921
  };
957
922
 
958
923
  let str_id = self.intern_string(name);
@@ -1067,6 +1032,22 @@ impl<'a> RubyOperationBuilder<'a> {
1067
1032
  Some(())
1068
1033
  }
1069
1034
 
1035
+ fn extract_literal_name<'b>(arg: &ruby_prism::Node<'b>) -> Option<(String, ruby_prism::Location<'b>)> {
1036
+ match arg {
1037
+ ruby_prism::Node::SymbolNode { .. } => {
1038
+ let symbol = arg.as_symbol_node().unwrap();
1039
+ let value_loc = symbol.value_loc()?;
1040
+ Some((Self::location_to_string(&value_loc), value_loc))
1041
+ }
1042
+ ruby_prism::Node::StringNode { .. } => {
1043
+ let string = arg.as_string_node().unwrap();
1044
+ let name = String::from_utf8_lossy(string.unescaped()).to_string();
1045
+ Some((name, arg.location()))
1046
+ }
1047
+ _ => None,
1048
+ }
1049
+ }
1050
+
1070
1051
  fn is_attr_call(arg: &ruby_prism::Node) -> bool {
1071
1052
  arg.as_call_node().is_some_and(|call| {
1072
1053
  let receiver = call.receiver();
@@ -1116,6 +1097,18 @@ impl<'a> RubyOperationBuilder<'a> {
1116
1097
  ruby_prism::Node::SymbolNode { .. } | ruby_prism::Node::StringNode { .. }
1117
1098
  ) {
1118
1099
  self.create_method_visibility_operation(&arg, visibility, DefinitionFlags::empty());
1100
+ if visibility == Visibility::ModuleFunction {
1101
+ // `module_function` also creates a public singleton method, so we emit a
1102
+ // second def for the singleton side. The two defs stay separate (rather than
1103
+ // shared) so reverse-lookup invalidation can detach `Foo#bar` and
1104
+ // `Foo::<Foo>#bar` independently. The `SINGLETON_METHOD_VISIBILITY` flag
1105
+ // routes this one to the singleton class.
1106
+ self.create_method_visibility_operation(
1107
+ &arg,
1108
+ visibility,
1109
+ DefinitionFlags::SINGLETON_METHOD_VISIBILITY,
1110
+ );
1111
+ }
1119
1112
  } else {
1120
1113
  let arg_offset = Offset::from_prism_location(&arg.location());
1121
1114
  let message = if Self::is_attr_call(&arg) {
@@ -1135,21 +1128,8 @@ impl<'a> RubyOperationBuilder<'a> {
1135
1128
  visibility: Visibility,
1136
1129
  flags: DefinitionFlags,
1137
1130
  ) {
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,
1131
+ let Some((name, location)) = Self::extract_literal_name(arg) else {
1132
+ return;
1153
1133
  };
1154
1134
 
1155
1135
  let str_id = self.intern_string(format!("{name}()"));
@@ -1372,6 +1352,14 @@ impl<'a> RubyOperationBuilder<'a> {
1372
1352
  flags,
1373
1353
  }));
1374
1354
  }
1355
+
1356
+ fn visit_method_body(&mut self, receiver: Option<NestingReceiver>, body: Option<&ruby_prism::Node<'_>>) {
1357
+ self.nesting_stack.push(Nesting::Method { receiver });
1358
+ if let Some(body) = body {
1359
+ self.visit(body);
1360
+ }
1361
+ self.nesting_stack.pop();
1362
+ }
1375
1363
  }
1376
1364
 
1377
1365
  struct CommentGroup {
@@ -1494,7 +1482,7 @@ impl Visit<'_> for RubyOperationBuilder<'_> {
1494
1482
 
1495
1483
  let string_id = self.intern_string(singleton_class_name);
1496
1484
  let nesting = self.current_lexical_scope_name_id();
1497
- let name_id = self.add_name(Name::new(string_id, ParentScope::Attached(attached_target), nesting));
1485
+ let name_id = self.add_name(string_id, ParentScope::Attached(attached_target), nesting);
1498
1486
  self.operations
1499
1487
  .push(Operation::EnterSingletonClass(op::EnterSingletonClass {
1500
1488
  name_id,
@@ -1578,8 +1566,10 @@ impl Visit<'_> for RubyOperationBuilder<'_> {
1578
1566
  };
1579
1567
 
1580
1568
  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.
1569
+ // Each EnterMethod creates a method definition, so emit one for the public singleton
1570
+ // method and one for the private instance method. Emit body definitions and references
1571
+ // only under the instance method because emitting them in both scopes would duplicate
1572
+ // their source-based IDs.
1583
1573
  let singleton_receiver = Some(Target::ExplicitSelf);
1584
1574
  let body = node.body();
1585
1575
 
@@ -1593,13 +1583,6 @@ impl Visit<'_> for RubyOperationBuilder<'_> {
1593
1583
  signatures: Signatures::Simple(parameters.clone().into_boxed_slice()),
1594
1584
  receiver: singleton_receiver,
1595
1585
  }));
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
1586
  self.operations.push(Operation::ExitScope);
1604
1587
 
1605
1588
  self.operations.push(Operation::EnterMethod(op::EnterMethod {
@@ -1612,13 +1595,7 @@ impl Visit<'_> for RubyOperationBuilder<'_> {
1612
1595
  signatures: Signatures::Simple(parameters.into_boxed_slice()),
1613
1596
  receiver,
1614
1597
  }));
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();
1598
+ self.visit_method_body(method_nesting_receiver, body.as_ref());
1622
1599
  self.operations.push(Operation::ExitScope);
1623
1600
  } else {
1624
1601
  // Singleton methods at top level have receiver=None (no class to point self to).
@@ -1647,13 +1624,8 @@ impl Visit<'_> for RubyOperationBuilder<'_> {
1647
1624
  signatures: Signatures::Simple(parameters.into_boxed_slice()),
1648
1625
  receiver,
1649
1626
  }));
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();
1627
+ let body = node.body();
1628
+ self.visit_method_body(method_nesting_receiver, body.as_ref());
1657
1629
  self.operations.push(Operation::ExitScope);
1658
1630
 
1659
1631
  if let Some(prev) = previous_visibility {
@@ -2475,14 +2447,15 @@ mod tests {
2475
2447
  }
2476
2448
 
2477
2449
  #[test]
2478
- fn build_module_function_with_ivar() {
2479
- assert_operations(
2450
+ fn build_module_function_with_body() {
2451
+ assert_operations_with_references(
2480
2452
  "
2481
2453
  module Foo
2482
2454
  module_function
2483
2455
 
2484
2456
  def bar
2485
- @x = 1
2457
+ @x = CONST
2458
+ baz(1)
2486
2459
  end
2487
2460
  end
2488
2461
  ",
@@ -2490,10 +2463,11 @@ mod tests {
2490
2463
  EnterModule(Foo)
2491
2464
  SetDefaultVisibility(module_function)
2492
2465
  EnterMethod(self.bar())
2493
- DefineInstanceVariable(@x)
2494
2466
  ExitScope
2495
2467
  EnterMethod(bar())
2496
2468
  DefineInstanceVariable(@x)
2469
+ ReferenceConstant(CONST)
2470
+ ReferenceMethod(baz)
2497
2471
  ExitScope
2498
2472
  ExitScope
2499
2473
  ",
@@ -0,0 +1,77 @@
1
+ use std::fs;
2
+ use std::io;
3
+ use std::path::{Path, PathBuf};
4
+
5
+ /// Resolves `path` into the spelling rubydex knows a path by: absolute, with symlinks expanded and, on Windows, spelled
6
+ /// plainly instead of verbatim.
7
+ ///
8
+ /// # Errors
9
+ ///
10
+ /// Returns an error if the path cannot be canonicalized, as for one that does not exist.
11
+ pub(crate) fn resolved(path: &Path) -> io::Result<PathBuf> {
12
+ Ok(simplified(fs::canonicalize(path)?))
13
+ }
14
+
15
+ /// Only Windows spells a path verbatim, so everywhere else there is nothing to simplify.
16
+ #[cfg(not(windows))]
17
+ pub(crate) fn simplified(path: PathBuf) -> PathBuf {
18
+ path
19
+ }
20
+
21
+ /// Since we canonicalize paths, we must simplify Windows paths in order to use them correctly with glob patterns, like
22
+ /// stripping the `\\?\` prefix.
23
+ #[cfg(windows)]
24
+ pub(crate) fn simplified(path: PathBuf) -> PathBuf {
25
+ use std::path::Component;
26
+ use std::path::Prefix;
27
+
28
+ let mut components = path.components();
29
+ let Some(Component::Prefix(prefix)) = components.next() else {
30
+ return path;
31
+ };
32
+
33
+ // Only a verbatim drive or share has a plain equivalent: `\\?\D:\dir` is `D:\dir`, and `\\?\UNC\server\share` is
34
+ // the share `\\server\share`. Any other verbatim prefix names a device, such as `\\?\pipe\name`, which has none.
35
+ let root = match prefix.kind() {
36
+ Prefix::VerbatimDisk(drive) => format!("{}:\\", char::from(drive)),
37
+ Prefix::VerbatimUNC(server, share) => {
38
+ format!(r"\\{}\{}\", server.to_string_lossy(), share.to_string_lossy())
39
+ }
40
+ _ => return path,
41
+ };
42
+
43
+ // The root already carries the separator that follows it, so the one spelled as its own component is dropped rather
44
+ // than pushed, which would otherwise reset the path back to the root it was just given.
45
+ let mut simplified = PathBuf::from(root);
46
+ simplified.extend(components.filter(|component| !matches!(component, Component::RootDir)));
47
+ simplified
48
+ }
49
+
50
+ #[cfg(all(test, windows))]
51
+ mod tests {
52
+ use super::*;
53
+
54
+ #[test]
55
+ fn simplified_rewrites_a_verbatim_path_into_its_plain_spelling() {
56
+ assert_eq!(
57
+ simplified(PathBuf::from(r"\\?\D:\a\project")),
58
+ PathBuf::from(r"D:\a\project")
59
+ );
60
+ }
61
+
62
+ #[test]
63
+ fn simplified_keeps_a_verbatim_share_rooted_on_that_share() {
64
+ assert_eq!(
65
+ simplified(PathBuf::from(r"\\?\UNC\server\share\project")),
66
+ PathBuf::from(r"\\server\share\project")
67
+ );
68
+ }
69
+
70
+ #[test]
71
+ fn simplified_leaves_a_path_with_no_plain_equivalent_alone() {
72
+ // A device path is not addressable without the prefix, and a plain path has nothing to simplify.
73
+ for path in [r"\\?\pipe\rubydex", r"D:\a\project", r"\\server\share\project"] {
74
+ assert_eq!(simplified(PathBuf::from(path)), PathBuf::from(path));
75
+ }
76
+ }
77
+ }