rubydex 0.3.0 → 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 (81) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +45 -7
  3. data/THIRD_PARTY_LICENSES.html +238 -2
  4. data/exe/rdx +2 -149
  5. data/ext/rubydex/config.c +140 -0
  6. data/ext/rubydex/config.h +16 -0
  7. data/ext/rubydex/diagnostic.c +75 -1
  8. data/ext/rubydex/diagnostic.h +2 -0
  9. data/ext/rubydex/graph.c +13 -45
  10. data/ext/rubydex/graph.h +6 -0
  11. data/ext/rubydex/query.c +398 -16
  12. data/ext/rubydex/rubydex.c +2 -0
  13. data/ext/rubydex/utils.c +11 -4
  14. data/lib/ruby_lsp/rubydex/addon.rb +211 -0
  15. data/lib/rubydex/cli/command/console.rb +55 -0
  16. data/lib/rubydex/cli/command/lint/explain.rb +74 -0
  17. data/lib/rubydex/cli/command/lint.rb +202 -0
  18. data/lib/rubydex/cli/command/mcp.rb +30 -0
  19. data/lib/rubydex/cli/command/query.rb +70 -0
  20. data/lib/rubydex/cli/command/skill.rb +69 -0
  21. data/lib/rubydex/cli/command.rb +168 -0
  22. data/lib/rubydex/cli.rb +93 -0
  23. data/lib/rubydex/config.rb +59 -0
  24. data/lib/rubydex/diagnostic.rb +12 -3
  25. data/lib/rubydex/errors.rb +42 -1
  26. data/lib/rubydex/graph.rb +10 -3
  27. data/lib/rubydex/linter/custom_rule.rb +97 -0
  28. data/lib/rubydex/linter/helpers/path_helpers.rb +78 -0
  29. data/lib/rubydex/linter/helpers/source_access_helpers.rb +31 -0
  30. data/lib/rubydex/linter/rule_loader.rb +36 -0
  31. data/lib/rubydex/linter/rule_test_case.rb +343 -0
  32. data/lib/rubydex/linter/runner.rb +56 -0
  33. data/lib/rubydex/linter.rb +19 -0
  34. data/lib/rubydex/location.rb +3 -0
  35. data/lib/rubydex/mcp_server.rb +1 -2
  36. data/lib/rubydex/related_information.rb +17 -0
  37. data/lib/rubydex/rule.rb +33 -0
  38. data/lib/rubydex/severity.rb +70 -0
  39. data/lib/rubydex/skill.rb +88 -0
  40. data/lib/rubydex/skill_registry.rb +62 -0
  41. data/lib/rubydex/version.rb +1 -1
  42. data/lib/rubydex.rb +6 -0
  43. data/lib/rubydex_linter/rules/rule_structure.rb +125 -0
  44. data/rbi/rubydex.rbi +558 -17
  45. data/rust/Cargo.lock +2 -2
  46. data/rust/rubydex/Cargo.toml +1 -1
  47. data/rust/rubydex/benches/graph_memory.rs +3 -5
  48. data/rust/rubydex/src/config.rs +538 -157
  49. data/rust/rubydex/src/diagnostic.rs +66 -40
  50. data/rust/rubydex/src/errors.rs +0 -1
  51. data/rust/rubydex/src/indexing/local_graph.rs +6 -5
  52. data/rust/rubydex/src/indexing/rbs_indexer.rs +270 -6
  53. data/rust/rubydex/src/indexing/ruby_indexer.rs +10 -12
  54. data/rust/rubydex/src/indexing/ruby_indexer_tests.rs +134 -81
  55. data/rust/rubydex/src/lib.rs +1 -0
  56. data/rust/rubydex/src/listing.rs +26 -1
  57. data/rust/rubydex/src/main.rs +7 -4
  58. data/rust/rubydex/src/model/declaration.rs +302 -219
  59. data/rust/rubydex/src/model/graph.rs +27 -40
  60. data/rust/rubydex/src/model/name.rs +58 -17
  61. data/rust/rubydex/src/operation/ruby_builder.rs +30 -45
  62. data/rust/rubydex/src/path_helpers.rs +77 -0
  63. data/rust/rubydex/src/query/cypher/schema.rs +63 -0
  64. data/rust/rubydex/src/query/cypher/tests.rs +26 -1
  65. data/rust/rubydex/src/query/cypher.rs +8 -11
  66. data/rust/rubydex/src/query.rs +123 -43
  67. data/rust/rubydex/src/resolution.rs +139 -187
  68. data/rust/rubydex/src/resolution_tests.rs +247 -19
  69. data/rust/rubydex/src/test_utils/context.rs +2 -1
  70. data/rust/rubydex/src/test_utils/graph_test.rs +26 -12
  71. data/rust/rubydex/src/test_utils/local_graph_test.rs +19 -0
  72. data/rust/rubydex/tests/cli.rs +4 -4
  73. data/rust/rubydex-sys/src/config_api.rs +205 -0
  74. data/rust/rubydex-sys/src/cypher_api.rs +791 -0
  75. data/rust/rubydex-sys/src/diagnostic_api.rs +77 -8
  76. data/rust/rubydex-sys/src/graph_api.rs +13 -194
  77. data/rust/rubydex-sys/src/lib.rs +2 -0
  78. data/rust/rubydex-sys/src/name_api.rs +2 -6
  79. data/rust/rubydex-sys/src/utils.rs +37 -0
  80. data/skills/send-private-method/SKILL.md +133 -0
  81. metadata +31 -2
@@ -1,10 +1,9 @@
1
1
  use std::collections::HashSet;
2
2
  use std::collections::hash_map::Entry;
3
- use std::path::{Path, PathBuf};
3
+ use std::path::Path;
4
4
 
5
5
  use crate::config::Config;
6
6
  use crate::diagnostic::Diagnostic;
7
- use crate::errors::Errors;
8
7
  use crate::indexing::local_graph::LocalGraph;
9
8
  use crate::model::built_in::{OBJECT_ID, add_built_in_data};
10
9
  use crate::model::declaration::{Ancestor, Declaration, Namespace};
@@ -93,7 +92,7 @@ pub struct Graph {
93
92
  /// Project configuration
94
93
  config: Config,
95
94
  }
96
- assert_mem_size!(Graph, 352);
95
+ assert_mem_size!(Graph, 368);
97
96
  assert_send_sync!(Graph);
98
97
 
99
98
  impl Graph {
@@ -110,7 +109,7 @@ impl Graph {
110
109
  position_encoding: Encoding::default(),
111
110
  name_dependents: IdentityHashMap::default(),
112
111
  pending_work: Vec::default(),
113
- config: Config::new(),
112
+ config: Config::default(),
114
113
  };
115
114
 
116
115
  add_built_in_data(&mut graph);
@@ -147,25 +146,9 @@ impl Graph {
147
146
  self.config.workspace_path()
148
147
  }
149
148
 
150
- /// Sets the root directory of the workspace being indexed.
151
- pub fn set_workspace_path(&mut self, workspace_path: PathBuf) {
152
- self.config.set_workspace_path(workspace_path);
153
- }
154
-
155
- /// Loads a configuration file. Pass `None` to load the default `rubydex.toml` configuration file if it exists
156
- ///
157
- /// # Errors
158
- ///
159
- /// Returns an [`Errors::ConfigNotFound`] if an explicitly requested file does not exist or an
160
- /// [`Errors::ConfigError`] if a file cannot otherwise be read or its contents are malformed.
161
- pub fn load_config(&mut self, config_path: Option<&Path>) -> Result<(), Errors> {
162
- match config_path {
163
- Some(path) => {
164
- let path = self.config.workspace_path().join(path);
165
- self.config.load_file(&path)
166
- }
167
- None => self.config.load_default(),
168
- }
149
+ /// Loads a config for the graph
150
+ pub fn load_config(&mut self, config: &Config) {
151
+ self.config = config.clone();
169
152
  }
170
153
 
171
154
  /// # Panics
@@ -207,7 +190,15 @@ impl Graph {
207
190
  if should_promote {
208
191
  let mut new_declaration = constructor(fully_qualified_name);
209
192
  let removed_declaration = occupied_entry.remove();
210
- new_declaration.as_namespace_mut().unwrap().extend(removed_declaration);
193
+ let singleton_class_id = removed_declaration
194
+ .as_namespace()
195
+ .and_then(Namespace::singleton_class)
196
+ .copied();
197
+ let new_namespace = new_declaration.as_namespace_mut().unwrap();
198
+ new_namespace.extend(removed_declaration);
199
+ if let Some(singleton_class_id) = singleton_class_id {
200
+ new_namespace.set_singleton_class_id(singleton_class_id);
201
+ }
211
202
  new_declaration.add_definition(definition_id);
212
203
  self.declarations.insert(declaration_id, new_declaration);
213
204
  } else {
@@ -540,7 +531,8 @@ impl Graph {
540
531
  /// Registers a name in the graph unless already registered. In regular indexing, this only happens in the local
541
532
  /// graph. This method is only used to back the `Graph#resolve_constant` Ruby API because every name must be
542
533
  /// registered in the graph to properly resolve
543
- pub fn add_name(&mut self, name: Name) -> NameId {
534
+ pub fn add_name(&mut self, str: StringId, parent_scope: ParentScope, nesting: Option<NameId>) -> NameId {
535
+ let name = Name::new(&self.names, str, parent_scope, nesting);
544
536
  let name_id = name.id();
545
537
 
546
538
  match self.names.entry(name_id) {
@@ -893,12 +885,7 @@ impl Graph {
893
885
  ) {
894
886
  if let Some(declaration) = self.declarations.get_mut(owner_id) {
895
887
  match declaration {
896
- Declaration::Namespace(Namespace::Class(it)) => it.add_member(member_str_id, member_declaration_id),
897
- Declaration::Namespace(Namespace::Module(it)) => it.add_member(member_str_id, member_declaration_id),
898
- Declaration::Namespace(Namespace::SingletonClass(it)) => {
899
- it.add_member(member_str_id, member_declaration_id);
900
- }
901
- Declaration::Namespace(Namespace::Todo(it)) => it.add_member(member_str_id, member_declaration_id),
888
+ Declaration::Namespace(namespace) => namespace.add_member(member_str_id, member_declaration_id),
902
889
  Declaration::Constant(_) => {
903
890
  // TODO: temporary hack to avoid crashing on `Struct.new`, `Class.new` and `Module.new`
904
891
  }
@@ -1293,7 +1280,7 @@ impl Graph {
1293
1280
  && let Some(anc_decl) = self.declarations.get_mut(&ancestor_id)
1294
1281
  && let Some(ns) = anc_decl.as_namespace_mut()
1295
1282
  {
1296
- ns.remove_descendant(&decl_id);
1283
+ ns.remove_descendant(decl_id);
1297
1284
  }
1298
1285
  }
1299
1286
  }
@@ -1317,7 +1304,7 @@ impl Graph {
1317
1304
  && let Some(anc_decl) = self.declarations.get_mut(ancestor_id)
1318
1305
  && let Some(ns) = anc_decl.as_namespace_mut()
1319
1306
  {
1320
- ns.remove_descendant(&decl_id);
1307
+ ns.remove_descendant(decl_id);
1321
1308
  }
1322
1309
  }
1323
1310
 
@@ -1811,7 +1798,7 @@ mod tests {
1811
1798
  context.index_uri("file:///a.rb", "");
1812
1799
 
1813
1800
  {
1814
- let Declaration::Namespace(Namespace::Class(foo)) =
1801
+ let Declaration::Namespace(foo @ Namespace::Class(_)) =
1815
1802
  context.graph().declarations().get(&DeclarationId::from("Foo")).unwrap()
1816
1803
  else {
1817
1804
  panic!("Expected Foo to be a class");
@@ -1819,7 +1806,7 @@ mod tests {
1819
1806
  assert!(matches!(foo.ancestors(), Ancestors::Partial(a) if a.is_empty()));
1820
1807
  assert!(foo.descendants().is_empty());
1821
1808
 
1822
- let Declaration::Namespace(Namespace::Class(baz)) =
1809
+ let Declaration::Namespace(baz @ Namespace::Class(_)) =
1823
1810
  context.graph().declarations().get(&DeclarationId::from("Baz")).unwrap()
1824
1811
  else {
1825
1812
  panic!("Expected Baz to be a class");
@@ -1827,7 +1814,7 @@ mod tests {
1827
1814
  assert!(matches!(baz.ancestors(), Ancestors::Partial(a) if a.is_empty()));
1828
1815
  assert!(baz.descendants().is_empty());
1829
1816
 
1830
- let Declaration::Namespace(Namespace::Module(bar)) =
1817
+ let Declaration::Namespace(bar @ Namespace::Module(_)) =
1831
1818
  context.graph().declarations().get(&DeclarationId::from("Bar")).unwrap()
1832
1819
  else {
1833
1820
  panic!("Expected Bar to be a module");
@@ -2198,9 +2185,9 @@ mod tests {
2198
2185
 
2199
2186
  assert_eq!(
2200
2187
  vec![
2201
- "parse-error: expected an `end` to close the `class` statement (file:///foo1.rb)",
2202
- "parse-error: unexpected end-of-input, assuming it is closing the parent top level context (file:///foo1.rb)",
2203
- "parse-warning: assigned but unused variable - foo (file:///foo2.rb)",
2188
+ "ParseError: expected an `end` to close the `class` statement (file:///foo1.rb)",
2189
+ "ParseError: unexpected end-of-input, assuming it is closing the parent top level context (file:///foo1.rb)",
2190
+ "ParseWarning: assigned but unused variable - foo (file:///foo2.rb)",
2204
2191
  ],
2205
2192
  diagnostics,
2206
2193
  );
@@ -2522,7 +2509,7 @@ mod tests {
2522
2509
  );
2523
2510
 
2524
2511
  // Delete bar.rb — the Bar name should be fully removed
2525
- let bar_name_id = Name::new(StringId::from("Bar"), ParentScope::None, None).id();
2512
+ let bar_name_id = Name::new(context.graph().names(), StringId::from("Bar"), ParentScope::None, None).id();
2526
2513
  context.index_uri("file:///bar.rb", "");
2527
2514
  context.resolve();
2528
2515
 
@@ -4,6 +4,7 @@ use crate::{
4
4
  assert_mem_size,
5
5
  model::{
6
6
  id::id_from_parts,
7
+ identity_maps::IdentityHashMap,
7
8
  ids::{DeclarationId, NameId, StringId},
8
9
  },
9
10
  };
@@ -93,31 +94,58 @@ pub struct Name {
93
94
  /// list of names to represent the nesting
94
95
  nesting: Option<NameId>,
95
96
  ref_count: u32,
97
+ /// The depth of the name, which combines the depths of the parent scope and nesting
98
+ depth: u16,
96
99
  }
100
+ assert_mem_size!(Name, 40);
97
101
 
98
102
  impl PartialEq for Name {
99
103
  fn eq(&self, other: &Self) -> bool {
100
104
  self.str == other.str && self.parent_scope == other.parent_scope && self.nesting == other.nesting
101
105
  }
102
106
  }
103
- assert_mem_size!(Name, 40);
104
107
 
105
108
  impl Name {
106
109
  #[must_use]
107
- 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 {
108
116
  Self {
109
117
  str,
110
118
  parent_scope,
111
119
  nesting,
112
120
  ref_count: 1,
121
+ depth: Self::name_depth(names, parent_scope, nesting),
113
122
  }
114
123
  }
115
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
+
116
139
  #[must_use]
117
140
  pub fn str(&self) -> &StringId {
118
141
  &self.str
119
142
  }
120
143
 
144
+ #[must_use]
145
+ pub fn depth(&self) -> u16 {
146
+ self.depth
147
+ }
148
+
121
149
  #[must_use]
122
150
  pub fn parent_scope(&self) -> &ParentScope {
123
151
  &self.parent_scope
@@ -227,6 +255,14 @@ impl NameRef {
227
255
  }
228
256
  }
229
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
+
230
266
  /// # Panics
231
267
  ///
232
268
  /// Panics if we exceed the maximum size of the reference count
@@ -281,42 +317,47 @@ mod tests {
281
317
 
282
318
  #[test]
283
319
  fn same_parent_scope_and_nesting() {
284
- let name_1 = Name::new(StringId::from("Foo"), ParentScope::None, None);
285
- 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);
286
324
  assert_eq!(name_1.id(), name_2.id());
287
325
 
288
- let name_3 = Name::new(StringId::from("Foo"), ParentScope::Some(name_1.id()), None);
289
- 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);
290
328
  assert_eq!(name_3.id(), name_4.id());
291
329
 
292
- let name_5 = Name::new(StringId::from("Foo"), ParentScope::None, Some(name_1.id()));
293
- 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()));
294
332
  assert_eq!(name_5.id(), name_6.id());
295
333
  assert_ne!(name_3.id(), name_5.id());
296
334
  assert_ne!(name_4.id(), name_6.id());
297
335
 
298
336
  let name_7 = Name::new(
337
+ &names,
299
338
  StringId::from("Foo"),
300
- ParentScope::Some(Name::new(StringId::from("Foo"), ParentScope::None, None).id()),
301
- 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()),
302
341
  );
303
342
  let name_8 = Name::new(
343
+ &names,
304
344
  StringId::from("Foo"),
305
- ParentScope::Some(Name::new(StringId::from("Foo"), ParentScope::None, None).id()),
306
- 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()),
307
347
  );
308
348
  assert_eq!(name_7.id(), name_8.id());
309
349
  }
310
350
 
311
351
  #[test]
312
352
  fn parent_scope_variants_are_distinct() {
313
- let inner = Name::new(StringId::from("Foo"), ParentScope::None, None).id();
353
+ let names = IdentityHashMap::default();
354
+ let inner = Name::new(&names, StringId::from("Foo"), ParentScope::None, None).id();
314
355
 
315
356
  let ids = [
316
- Name::new(StringId::from("Foo"), ParentScope::None, None).id(),
317
- Name::new(StringId::from("Foo"), ParentScope::TopLevel, None).id(),
318
- Name::new(StringId::from("Foo"), ParentScope::Some(inner), None).id(),
319
- Name::new(StringId::from("Foo"), ParentScope::Attached(inner), None).id(),
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(),
320
361
  ];
321
362
 
322
363
  for (i, a) in ids.iter().enumerate() {
@@ -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);
@@ -689,10 +686,7 @@ impl<'a> RubyOperationBuilder<'a> {
689
686
  )
690
687
  } else {
691
688
  let string_id = self.intern_string(format!("{}:{}<anonymous>", self.uri_id, offset.start()));
692
- (
693
- Some(self.add_name(Name::new(string_id, ParentScope::None, None))),
694
- offset.clone(),
695
- )
689
+ (Some(self.add_name(string_id, ParentScope::None, None)), offset.clone())
696
690
  };
697
691
 
698
692
  if let Some(name_id) = name_id {
@@ -754,10 +748,7 @@ impl<'a> RubyOperationBuilder<'a> {
754
748
  )
755
749
  } else {
756
750
  let string_id = self.intern_string(format!("{}:{}<anonymous>", self.uri_id, offset.start()));
757
- (
758
- Some(self.add_name(Name::new(string_id, ParentScope::None, None))),
759
- offset.clone(),
760
- )
751
+ (Some(self.add_name(string_id, ParentScope::None, None)), offset.clone())
761
752
  };
762
753
 
763
754
  if let Some(name_id) = name_id {
@@ -849,7 +840,7 @@ impl<'a> RubyOperationBuilder<'a> {
849
840
  }
850
841
 
851
842
  Some((
852
- self.current_lexical_scope_name_id().unwrap(),
843
+ self.current_owner_name_id().unwrap(),
853
844
  Offset::from_prism_location(&arg.location()),
854
845
  ))
855
846
  } else if let Some(name_id) = self.index_constant_reference(&arg, false) {
@@ -1361,6 +1352,14 @@ impl<'a> RubyOperationBuilder<'a> {
1361
1352
  flags,
1362
1353
  }));
1363
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
+ }
1364
1363
  }
1365
1364
 
1366
1365
  struct CommentGroup {
@@ -1483,7 +1482,7 @@ impl Visit<'_> for RubyOperationBuilder<'_> {
1483
1482
 
1484
1483
  let string_id = self.intern_string(singleton_class_name);
1485
1484
  let nesting = self.current_lexical_scope_name_id();
1486
- 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);
1487
1486
  self.operations
1488
1487
  .push(Operation::EnterSingletonClass(op::EnterSingletonClass {
1489
1488
  name_id,
@@ -1567,8 +1566,10 @@ impl Visit<'_> for RubyOperationBuilder<'_> {
1567
1566
  };
1568
1567
 
1569
1568
  if receiver.is_none() && visibility == Visibility::ModuleFunction {
1570
- // module_function: emit two EnterMethod/ExitScope pairs (singleton + instance),
1571
- // 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.
1572
1573
  let singleton_receiver = Some(Target::ExplicitSelf);
1573
1574
  let body = node.body();
1574
1575
 
@@ -1582,13 +1583,6 @@ impl Visit<'_> for RubyOperationBuilder<'_> {
1582
1583
  signatures: Signatures::Simple(parameters.clone().into_boxed_slice()),
1583
1584
  receiver: singleton_receiver,
1584
1585
  }));
1585
- self.nesting_stack.push(Nesting::Method {
1586
- receiver: method_nesting_receiver,
1587
- });
1588
- if let Some(ref body) = body {
1589
- self.visit(body);
1590
- }
1591
- self.nesting_stack.pop();
1592
1586
  self.operations.push(Operation::ExitScope);
1593
1587
 
1594
1588
  self.operations.push(Operation::EnterMethod(op::EnterMethod {
@@ -1601,13 +1595,7 @@ impl Visit<'_> for RubyOperationBuilder<'_> {
1601
1595
  signatures: Signatures::Simple(parameters.into_boxed_slice()),
1602
1596
  receiver,
1603
1597
  }));
1604
- self.nesting_stack.push(Nesting::Method {
1605
- receiver: method_nesting_receiver,
1606
- });
1607
- if let Some(ref body) = body {
1608
- self.visit(body);
1609
- }
1610
- self.nesting_stack.pop();
1598
+ self.visit_method_body(method_nesting_receiver, body.as_ref());
1611
1599
  self.operations.push(Operation::ExitScope);
1612
1600
  } else {
1613
1601
  // Singleton methods at top level have receiver=None (no class to point self to).
@@ -1636,13 +1624,8 @@ impl Visit<'_> for RubyOperationBuilder<'_> {
1636
1624
  signatures: Signatures::Simple(parameters.into_boxed_slice()),
1637
1625
  receiver,
1638
1626
  }));
1639
- self.nesting_stack.push(Nesting::Method {
1640
- receiver: method_nesting_receiver,
1641
- });
1642
- if let Some(body) = node.body() {
1643
- self.visit(&body);
1644
- }
1645
- self.nesting_stack.pop();
1627
+ let body = node.body();
1628
+ self.visit_method_body(method_nesting_receiver, body.as_ref());
1646
1629
  self.operations.push(Operation::ExitScope);
1647
1630
 
1648
1631
  if let Some(prev) = previous_visibility {
@@ -2464,14 +2447,15 @@ mod tests {
2464
2447
  }
2465
2448
 
2466
2449
  #[test]
2467
- fn build_module_function_with_ivar() {
2468
- assert_operations(
2450
+ fn build_module_function_with_body() {
2451
+ assert_operations_with_references(
2469
2452
  "
2470
2453
  module Foo
2471
2454
  module_function
2472
2455
 
2473
2456
  def bar
2474
- @x = 1
2457
+ @x = CONST
2458
+ baz(1)
2475
2459
  end
2476
2460
  end
2477
2461
  ",
@@ -2479,10 +2463,11 @@ mod tests {
2479
2463
  EnterModule(Foo)
2480
2464
  SetDefaultVisibility(module_function)
2481
2465
  EnterMethod(self.bar())
2482
- DefineInstanceVariable(@x)
2483
2466
  ExitScope
2484
2467
  EnterMethod(bar())
2485
2468
  DefineInstanceVariable(@x)
2469
+ ReferenceConstant(CONST)
2470
+ ReferenceMethod(baz)
2486
2471
  ExitScope
2487
2472
  ExitScope
2488
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
+ }
@@ -39,6 +39,22 @@ pub enum NodeRef {
39
39
  Document(UriId),
40
40
  }
41
41
 
42
+ impl NodeRef {
43
+ /// Decodes the opaque id produced by [`GraphProvider::node_id`] back into a `NodeRef`. Returns
44
+ /// `None` if the string is not a recognized `tag:id` form.
45
+ #[must_use]
46
+ pub fn decode(encoded: &str) -> Option<NodeRef> {
47
+ let (tag, rest) = encoded.split_once(':')?;
48
+ let value: u64 = rest.parse().ok()?;
49
+ match tag {
50
+ "decl" => Some(NodeRef::Declaration(DeclarationId::new(value))),
51
+ "def" => Some(NodeRef::Definition(DefinitionId::new(value))),
52
+ "doc" => Some(NodeRef::Document(UriId::new(value))),
53
+ _ => None,
54
+ }
55
+ }
56
+ }
57
+
42
58
  /// A relationship type.
43
59
  #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
44
60
  pub enum RelType {
@@ -377,6 +393,10 @@ impl GraphProvider for Graph {
377
393
  RelType::parse(rel_type).map_or_else(Vec::new, |rel| rel_source_nodes(self, rel))
378
394
  }
379
395
 
396
+ fn expand_in(&self, node: NodeRef, rel_type: &str) -> Option<Vec<NodeRef>> {
397
+ RelType::parse(rel_type).and_then(|rel| expand_in(self, node, rel))
398
+ }
399
+
380
400
  fn property(&self, node: NodeRef, prop: &str) -> CypherValue {
381
401
  property(self, node, prop)
382
402
  }
@@ -388,6 +408,17 @@ impl GraphProvider for Graph {
388
408
  fn name(&self, node: NodeRef) -> String {
389
409
  node_name(self, node)
390
410
  }
411
+
412
+ fn node_id(&self, node: NodeRef) -> String {
413
+ // Encode the node category plus the underlying hashed id so a consumer can decode it back to
414
+ // the right handle. The category tag is required because labels alone are ambiguous
415
+ // (a class declaration and a class definition both have the label "Class").
416
+ match node {
417
+ NodeRef::Declaration(id) => format!("decl:{}", *id),
418
+ NodeRef::Definition(id) => format!("def:{}", *id),
419
+ NodeRef::Document(id) => format!("doc:{}", *id),
420
+ }
421
+ }
391
422
  }
392
423
 
393
424
  /// Returns all nodes matching the given labels. An empty slice matches every node; otherwise a node
@@ -582,6 +613,38 @@ pub fn rel_source_nodes(graph: &Graph, rel: RelType) -> Vec<NodeRef> {
582
613
  }
583
614
  }
584
615
 
616
+ /// Expands the *incoming* edges of `node` for the given relationship type — the targeted reverse of
617
+ /// [`expand_out`], returning `Some` only when the graph stores the reverse cheaply and exactly.
618
+ /// Returning `None` lets the executor fall back to `rel_sources` + `expand` (always correct, but an
619
+ /// O(all sources) whole-graph build).
620
+ ///
621
+ /// Only the Document → Definition → Declaration spine is answered directly:
622
+ /// - reverse `DEFINES`: a definition's document (each definition belongs to exactly one).
623
+ /// - reverse `DECLARES`: the per-file definitions composing a declaration.
624
+ ///
625
+ /// The remaining edges either have no stored reverse (e.g. direct subclasses of `HAS_PARENT`) or a
626
+ /// reverse that isn't an exact set match (the `HAS_DESCENDANT` set is self-inclusive, unlike the
627
+ /// `HAS_ANCESTOR` walk), so they fall through to the default.
628
+ #[must_use]
629
+ pub fn expand_in(graph: &Graph, node: NodeRef, rel: RelType) -> Option<Vec<NodeRef>> {
630
+ match (node, rel) {
631
+ (NodeRef::Definition(def_id), RelType::Defines) => {
632
+ let uri_id = *graph.definitions().get(&def_id)?.uri_id();
633
+ Some(vec![NodeRef::Document(uri_id)])
634
+ }
635
+ (NodeRef::Declaration(decl_id), RelType::Declares) => Some(
636
+ graph
637
+ .declarations()
638
+ .get(&decl_id)?
639
+ .definitions()
640
+ .iter()
641
+ .map(|id| NodeRef::Definition(*id))
642
+ .collect(),
643
+ ),
644
+ _ => None,
645
+ }
646
+ }
647
+
585
648
  /// Expands the outgoing edges of `node` for the given relationship type.
586
649
  #[must_use]
587
650
  pub fn expand_out(graph: &Graph, node: NodeRef, rel: RelType) -> Vec<NodeRef> {