rubydex 0.2.5 → 0.2.7

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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +17 -16
  3. data/THIRD_PARTY_LICENSES.html +45 -12
  4. data/exe/rdx +2 -0
  5. data/ext/rubydex/declaration.c +115 -106
  6. data/ext/rubydex/definition.c +123 -72
  7. data/ext/rubydex/diagnostic.c +5 -0
  8. data/ext/rubydex/document.c +51 -23
  9. data/ext/rubydex/extconf.rb +1 -1
  10. data/ext/rubydex/graph.c +253 -66
  11. data/ext/rubydex/handle.h +34 -5
  12. data/ext/rubydex/location.c +5 -0
  13. data/ext/rubydex/reference.c +51 -46
  14. data/ext/rubydex/rubydex.c +5 -0
  15. data/ext/rubydex/signature.c +5 -0
  16. data/ext/rubydex/utils.c +13 -0
  17. data/ext/rubydex/utils.h +4 -0
  18. data/lib/rubydex/bin/rubydex_mcp.exe +0 -0
  19. data/lib/rubydex/errors.rb +11 -0
  20. data/lib/rubydex/graph.rb +9 -28
  21. data/lib/rubydex/location.rb +24 -0
  22. data/lib/rubydex/version.rb +1 -1
  23. data/lib/rubydex.rb +1 -0
  24. data/rbi/rubydex.rbi +40 -15
  25. data/rust/Cargo.lock +122 -17
  26. data/rust/rubydex/Cargo.toml +26 -2
  27. data/rust/rubydex/benches/graph_memory.rs +46 -0
  28. data/rust/rubydex/src/config.rs +275 -0
  29. data/rust/rubydex/src/dot.rs +609 -0
  30. data/rust/rubydex/src/errors.rs +2 -0
  31. data/rust/rubydex/src/indexing/rbs_indexer.rs +19 -1
  32. data/rust/rubydex/src/indexing/ruby_indexer.rs +4 -0
  33. data/rust/rubydex/src/lib.rs +8 -1
  34. data/rust/rubydex/src/main.rs +8 -5
  35. data/rust/rubydex/src/model/built_in.rs +5 -2
  36. data/rust/rubydex/src/model/comment.rs +2 -0
  37. data/rust/rubydex/src/model/declaration.rs +13 -51
  38. data/rust/rubydex/src/model/definitions.rs +13 -1
  39. data/rust/rubydex/src/model/document.rs +2 -0
  40. data/rust/rubydex/src/model/encoding.rs +2 -0
  41. data/rust/rubydex/src/model/graph.rs +88 -27
  42. data/rust/rubydex/src/model/identity_maps.rs +3 -0
  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 +1 -0
  48. data/rust/rubydex/src/operation/mod.rs +1 -0
  49. data/rust/rubydex/src/operation/ruby_builder.rs +4 -0
  50. data/rust/rubydex/src/query.rs +114 -33
  51. data/rust/rubydex/src/resolution.rs +18 -20
  52. data/rust/rubydex/src/resolution_tests.rs +132 -0
  53. data/rust/rubydex/tests/cli.rs +17 -61
  54. data/rust/rubydex-mcp/Cargo.toml +9 -3
  55. data/rust/rubydex-sys/Cargo.toml +9 -2
  56. data/rust/rubydex-sys/src/definition_api.rs +72 -2
  57. data/rust/rubydex-sys/src/document_api.rs +28 -0
  58. data/rust/rubydex-sys/src/graph_api.rs +74 -6
  59. metadata +6 -4
  60. data/rust/rubydex/src/visualization/dot.rs +0 -192
  61. data/rust/rubydex/src/visualization.rs +0 -6
@@ -1,8 +1,11 @@
1
1
  use std::collections::HashSet;
2
2
  use std::collections::hash_map::Entry;
3
- use std::path::PathBuf;
3
+ use std::path::{Path, PathBuf};
4
4
 
5
+ use crate::assert_mem_size;
6
+ use crate::config::Config;
5
7
  use crate::diagnostic::Diagnostic;
8
+ use crate::errors::Errors;
6
9
  use crate::indexing::local_graph::LocalGraph;
7
10
  use crate::model::built_in::{OBJECT_ID, add_built_in_data};
8
11
  use crate::model::declaration::{Ancestor, Declaration, Namespace};
@@ -28,6 +31,7 @@ pub enum NameDependent {
28
31
  /// This name's `nesting` is the key name — reference-only dependency.
29
32
  NestedName(NameId),
30
33
  }
34
+ assert_mem_size!(NameDependent, 16);
31
35
 
32
36
  /// Items processed by the unified invalidation worklist.
33
37
  enum InvalidationItem {
@@ -38,6 +42,7 @@ enum InvalidationItem {
38
42
  /// Ancestor context changed — unresolve references under this name but keep the name resolved.
39
43
  References(NameId),
40
44
  }
45
+ assert_mem_size!(InvalidationItem, 16);
41
46
 
42
47
  /// A work item produced by graph mutations (update/delete) that needs resolution.
43
48
  #[derive(Debug)]
@@ -49,6 +54,7 @@ pub enum Unit {
49
54
  /// A declaration whose ancestors need re-linearization
50
55
  Ancestors(DeclarationId),
51
56
  }
57
+ assert_mem_size!(Unit, 16);
52
58
 
53
59
  // The `Graph` is the global representation of the entire Ruby codebase. It contains all declarations and their
54
60
  // relationships
@@ -81,9 +87,10 @@ pub struct Graph {
81
87
  /// Drained by `take_pending_work()` before resolution.
82
88
  pending_work: Vec<Unit>,
83
89
 
84
- /// Paths to exclude from file discovery during indexing.
85
- excluded_paths: HashSet<PathBuf>,
90
+ /// Project configuration
91
+ config: Config,
86
92
  }
93
+ assert_mem_size!(Graph, 352);
87
94
 
88
95
  impl Graph {
89
96
  #[must_use]
@@ -99,7 +106,7 @@ impl Graph {
99
106
  position_encoding: Encoding::default(),
100
107
  name_dependents: IdentityHashMap::default(),
101
108
  pending_work: Vec::default(),
102
- excluded_paths: HashSet::new(),
109
+ config: Config::new(),
103
110
  };
104
111
 
105
112
  add_built_in_data(&mut graph);
@@ -121,13 +128,40 @@ impl Graph {
121
128
  /// Adds paths to exclude from file discovery during indexing. Excluded directories will be skipped entirely during
122
129
  /// directory traversal.
123
130
  pub fn exclude_paths(&mut self, paths: Vec<PathBuf>) {
124
- self.excluded_paths.extend(paths);
131
+ self.config.exclude_paths(paths);
125
132
  }
126
133
 
127
134
  /// Returns the set of paths excluded from file discovery.
128
135
  #[must_use]
129
- pub fn excluded_paths(&self) -> &HashSet<PathBuf> {
130
- &self.excluded_paths
136
+ pub fn excluded_paths(&self) -> HashSet<PathBuf> {
137
+ self.config.excluded_paths()
138
+ }
139
+
140
+ /// Returns the root directory of the workspace being indexed.
141
+ #[must_use]
142
+ pub fn workspace_path(&self) -> &Path {
143
+ self.config.workspace_path()
144
+ }
145
+
146
+ /// Sets the root directory of the workspace being indexed.
147
+ pub fn set_workspace_path(&mut self, workspace_path: PathBuf) {
148
+ self.config.set_workspace_path(workspace_path);
149
+ }
150
+
151
+ /// Loads a configuration file. Pass `None` to load the default `rubydex.toml` configuration file if it exists
152
+ ///
153
+ /// # Errors
154
+ ///
155
+ /// Returns an [`Errors::ConfigNotFound`] if an explicitly requested file does not exist or an
156
+ /// [`Errors::ConfigError`] if a file cannot otherwise be read or its contents are malformed.
157
+ pub fn load_config(&mut self, config_path: Option<&Path>) -> Result<(), Errors> {
158
+ match config_path {
159
+ Some(path) => {
160
+ let path = self.config.workspace_path().join(path);
161
+ self.config.load_file(&path)
162
+ }
163
+ None => self.config.load_default(),
164
+ }
131
165
  }
132
166
 
133
167
  /// # Panics
@@ -306,9 +340,6 @@ impl Graph {
306
340
  self.definition_to_declaration_id(self.definitions.get(&definition_id).unwrap())
307
341
  }
308
342
 
309
- /// # Panics
310
- ///
311
- /// Panics if the definition is not found
312
343
  #[must_use]
313
344
  pub fn definition_to_declaration_id(&self, definition: &Definition) -> Option<&DeclarationId> {
314
345
  let (nesting_name_id, member_str_id) = match definition {
@@ -442,20 +473,21 @@ impl Graph {
442
473
  }
443
474
 
444
475
  /// Looks up the declaration for a `SelfReceiver` method/alias through the singleton class.
476
+ ///
477
+ /// Returns `None` when the owner cannot be resolved to a namespace with a singleton class. This
478
+ /// can happen when the enclosing construct resolved to a non-namespace declaration (e.g. a
479
+ /// constant or constant alias that a same-named `class`/`module` reopened without promotion), in
480
+ /// which case the method has no owning declaration.
445
481
  fn find_self_receiver_declaration(&self, def_id: DefinitionId, member_str_id: StringId) -> Option<&DeclarationId> {
446
482
  let owner_decl_id = self.definition_id_to_declaration_id(def_id)?;
447
483
  let singleton_id = self
448
484
  .declarations
449
- .get(owner_decl_id)
450
- .unwrap()
451
- .as_namespace()
452
- .unwrap()
485
+ .get(owner_decl_id)?
486
+ .as_namespace()?
453
487
  .singleton_class()?;
454
488
  self.declarations
455
- .get(singleton_id)
456
- .unwrap()
457
- .as_namespace()
458
- .unwrap()
489
+ .get(singleton_id)?
490
+ .as_namespace()?
459
491
  .member(&member_str_id)
460
492
  }
461
493
 
@@ -483,10 +515,7 @@ impl Graph {
483
515
 
484
516
  #[must_use]
485
517
  pub fn all_diagnostics(&self) -> Vec<&Diagnostic> {
486
- let document_diagnostics = self.documents.values().flat_map(Document::diagnostics);
487
- let declaration_diagnostics = self.declarations.values().flat_map(Declaration::diagnostics);
488
-
489
- document_diagnostics.chain(declaration_diagnostics).collect()
518
+ self.documents.values().flat_map(Document::diagnostics).collect()
490
519
  }
491
520
 
492
521
  /// Interns a string in the graph unless already interned. This method is only used to back the
@@ -1198,9 +1227,6 @@ impl Graph {
1198
1227
  for def_id in detach_def_ids {
1199
1228
  decl.remove_definition(def_id);
1200
1229
  }
1201
- if !detach_def_ids.is_empty() {
1202
- decl.clear_diagnostics();
1203
- }
1204
1230
  }
1205
1231
 
1206
1232
  let Some(decl) = self.declarations.get(&decl_id) else {
@@ -1540,8 +1566,8 @@ mod tests {
1540
1566
  use crate::model::declaration::Ancestors;
1541
1567
  use crate::test_utils::GraphTest;
1542
1568
  use crate::{
1543
- assert_declaration_does_not_exist, assert_dependents, assert_descendants, assert_members_eq,
1544
- assert_no_diagnostics, assert_no_members,
1569
+ assert_declaration_does_not_exist, assert_declaration_kind_eq, assert_dependents, assert_descendants,
1570
+ assert_members_eq, assert_no_diagnostics, assert_no_members,
1545
1571
  };
1546
1572
 
1547
1573
  #[test]
@@ -1563,6 +1589,41 @@ mod tests {
1563
1589
  );
1564
1590
  }
1565
1591
 
1592
+ #[test]
1593
+ fn singleton_method_in_non_namespace_owner_does_not_panic() {
1594
+ // `Aliased = Bar` assigns a constant to another constant, producing a (non-promotable)
1595
+ // `ConstantAlias` declaration. Reopening it with `class Aliased` is valid Ruby (it reopens
1596
+ // `Bar`), but the class definition is attached to the existing `ConstantAlias` declaration
1597
+ // without promoting it to a namespace. A `def self.foo` inside then has a `SelfReceiver`
1598
+ // owner whose declaration is not a namespace. Resolving that definition to its declaration
1599
+ // must return `None` rather than panicking.
1600
+ let mut context = GraphTest::new();
1601
+
1602
+ context.index_uri(
1603
+ "file:///foo.rb",
1604
+ "
1605
+ class Bar
1606
+ end
1607
+
1608
+ Aliased = Bar
1609
+
1610
+ class Aliased
1611
+ def self.foo; end
1612
+ end
1613
+ ",
1614
+ );
1615
+ context.resolve();
1616
+
1617
+ // The declaration stays a non-namespace constant alias.
1618
+ assert_declaration_kind_eq!(context, "Aliased", "ConstantAlias");
1619
+
1620
+ // Mirrors what consumers like the DOT exporter do: resolve every definition to its
1621
+ // declaration. This previously panicked on the singleton method's non-namespace owner.
1622
+ for definition in context.graph().definitions().values() {
1623
+ let _ = context.graph().definition_to_declaration_id(definition);
1624
+ }
1625
+ }
1626
+
1566
1627
  #[test]
1567
1628
  fn deleting_file_triggers_name_dependent_cleanup() {
1568
1629
  let mut context = GraphTest::new();
@@ -6,10 +6,13 @@ use std::{
6
6
  hash::{BuildHasher, Hasher},
7
7
  };
8
8
 
9
+ use crate::assert_mem_size;
10
+
9
11
  #[derive(Default)]
10
12
  pub struct IdentityHasher {
11
13
  hash: u64,
12
14
  }
15
+ assert_mem_size!(IdentityHasher, 8);
13
16
 
14
17
  impl Hasher for IdentityHasher {
15
18
  fn write(&mut self, _bytes: &[u8]) {
@@ -1,3 +1,5 @@
1
+ use crate::assert_mem_size;
2
+
1
3
  /// A Ruby keyword with its documentation.
2
4
  #[derive(Debug)]
3
5
  pub struct Keyword {
@@ -6,6 +8,7 @@ pub struct Keyword {
6
8
  /// Documentation string for hover display
7
9
  documentation: &'static str,
8
10
  }
11
+ assert_mem_size!(Keyword, 32);
9
12
 
10
13
  impl Keyword {
11
14
  #[must_use]
@@ -141,6 +141,7 @@ pub struct ResolvedName {
141
141
  name: Name,
142
142
  declaration_id: DeclarationId,
143
143
  }
144
+ assert_mem_size!(ResolvedName, 48);
144
145
 
145
146
  impl ResolvedName {
146
147
  #[must_use]
@@ -173,6 +174,7 @@ pub enum NameRef {
173
174
  /// This name has been resolved to an existing declaration
174
175
  Resolved(Box<ResolvedName>),
175
176
  }
177
+ assert_mem_size!(NameRef, 16);
176
178
 
177
179
  impl NameRef {
178
180
  #[must_use]
@@ -1,3 +1,4 @@
1
+ use crate::assert_mem_size;
1
2
  use std::ops::Deref;
2
3
 
3
4
  /// A reference-counted string used in the graph.
@@ -11,6 +12,7 @@ pub struct StringRef {
11
12
  value: String,
12
13
  ref_count: u32,
13
14
  }
15
+ assert_mem_size!(StringRef, 32);
14
16
 
15
17
  impl StringRef {
16
18
  #[must_use]
@@ -1,6 +1,8 @@
1
1
  use core::fmt;
2
2
  use std::fmt::Display;
3
3
 
4
+ use crate::assert_mem_size;
5
+
4
6
  #[derive(Debug, Clone, Copy, PartialEq, Eq)]
5
7
  pub enum Visibility {
6
8
  Public,
@@ -8,6 +10,7 @@ pub enum Visibility {
8
10
  Private,
9
11
  ModuleFunction,
10
12
  }
13
+ assert_mem_size!(Visibility, 1);
11
14
 
12
15
  impl Visibility {
13
16
  /// Parse a visibility from a string.
@@ -219,6 +219,7 @@ impl OperationApplier {
219
219
  op.str_id,
220
220
  op.uri_id,
221
221
  op.offset,
222
+ op.name_offset,
222
223
  op.comments,
223
224
  op.flags,
224
225
  lexical_nesting_id,
@@ -152,6 +152,7 @@ pub struct EnterMethod {
152
152
  pub str_id: StringId,
153
153
  pub uri_id: UriId,
154
154
  pub offset: Offset,
155
+ pub name_offset: Offset,
155
156
  pub comments: Box<[Comment]>,
156
157
  pub flags: DefinitionFlags,
157
158
  pub signatures: Signatures,
@@ -1524,6 +1524,7 @@ impl Visit<'_> for RubyOperationBuilder<'_> {
1524
1524
  let name = Self::location_to_string(&node.name_loc());
1525
1525
  let str_id = self.intern_string(format!("{name}()"));
1526
1526
  let offset = Offset::from_prism_location(&node.location());
1527
+ let name_offset = Offset::from_prism_location(&node.name_loc());
1527
1528
  let parameters = self.collect_parameters(node);
1528
1529
  let is_singleton = node.receiver().is_some();
1529
1530
 
@@ -1586,6 +1587,7 @@ impl Visit<'_> for RubyOperationBuilder<'_> {
1586
1587
  str_id,
1587
1588
  uri_id: self.uri_id,
1588
1589
  offset: offset.clone(),
1590
+ name_offset: name_offset.clone(),
1589
1591
  comments: comments.clone(),
1590
1592
  flags: flags.clone(),
1591
1593
  signatures: Signatures::Simple(parameters.clone().into_boxed_slice()),
@@ -1604,6 +1606,7 @@ impl Visit<'_> for RubyOperationBuilder<'_> {
1604
1606
  str_id,
1605
1607
  uri_id: self.uri_id,
1606
1608
  offset: offset.clone(),
1609
+ name_offset: name_offset.clone(),
1607
1610
  comments,
1608
1611
  flags,
1609
1612
  signatures: Signatures::Simple(parameters.into_boxed_slice()),
@@ -1638,6 +1641,7 @@ impl Visit<'_> for RubyOperationBuilder<'_> {
1638
1641
  str_id,
1639
1642
  uri_id: self.uri_id,
1640
1643
  offset: offset.clone(),
1644
+ name_offset,
1641
1645
  comments,
1642
1646
  flags,
1643
1647
  signatures: Signatures::Simple(parameters.into_boxed_slice()),
@@ -454,20 +454,7 @@ fn expression_completion<'a>(
454
454
  let NameRef::Resolved(name_ref) = name_ref else {
455
455
  return Err(format!("Expected name {nesting_name_id} to be resolved").into());
456
456
  };
457
- // When no explicit self is given, self is the innermost lexical scope (the nesting's own declaration).
458
- // When explicit, follow constant aliases so callers can pass whatever the expression that set self
459
- // resolves to without having to unwrap aliases themselves. Missing or non-namespace decls are graph
460
- // inconsistencies and surfaced as errors.
461
- let resolved_self_decl_id = match self_decl_id {
462
- Some(id) => resolve_self_namespace(graph, id)?,
463
- None => *name_ref.declaration_id(),
464
- };
465
- let self_decl = graph
466
- .declarations()
467
- .get(&resolved_self_decl_id)
468
- .unwrap()
469
- .as_namespace()
470
- .ok_or("Expected associated declaration to be a namespace")?;
457
+
471
458
  let innermost_lexical_decl = graph
472
459
  .declarations()
473
460
  .get(name_ref.declaration_id())
@@ -496,7 +483,16 @@ fn expression_completion<'a>(
496
483
 
497
484
  // Collect methods and instance variables, which are based on the inheritance chain of the `self` type (which may
498
485
  // not match the immediate lexical scope)
499
- collect_methods_and_ivars_from_self(graph, self_decl, &mut context, &mut candidates);
486
+ if let Some(self_decl_id) = self_decl_id.map(|id| resolve_self_namespace(graph, id)).transpose()? {
487
+ let self_decl = graph
488
+ .declarations()
489
+ .get(&self_decl_id)
490
+ .unwrap()
491
+ .as_namespace()
492
+ .ok_or("Expected associated declaration to be a namespace")?;
493
+
494
+ collect_methods_and_ivars_from_self(graph, self_decl, &mut context, &mut candidates);
495
+ }
500
496
 
501
497
  // Keywords are always available in expression contexts
502
498
  candidates.extend(keywords::KEYWORDS.iter().map(CompletionCandidate::Keyword));
@@ -1120,7 +1116,7 @@ mod tests {
1120
1116
  assert_declaration_completion_eq!(
1121
1117
  context,
1122
1118
  CompletionReceiver::Expression {
1123
- self_decl_id: None,
1119
+ self_decl_id: Some(DeclarationId::from("Child")),
1124
1120
  nesting_name_id: name_id,
1125
1121
  },
1126
1122
  [
@@ -1169,7 +1165,7 @@ mod tests {
1169
1165
  assert_declaration_completion_eq!(
1170
1166
  context,
1171
1167
  CompletionReceiver::Expression {
1172
- self_decl_id: None,
1168
+ self_decl_id: Some(DeclarationId::from("Child")),
1173
1169
  nesting_name_id: name_id,
1174
1170
  },
1175
1171
  [
@@ -1218,7 +1214,7 @@ mod tests {
1218
1214
  assert_declaration_completion_eq!(
1219
1215
  context,
1220
1216
  CompletionReceiver::Expression {
1221
- self_decl_id: None,
1217
+ self_decl_id: Some(DeclarationId::from("Foo")),
1222
1218
  nesting_name_id: name_id,
1223
1219
  },
1224
1220
  [
@@ -1268,7 +1264,7 @@ mod tests {
1268
1264
  assert_declaration_completion_eq!(
1269
1265
  context,
1270
1266
  CompletionReceiver::Expression {
1271
- self_decl_id: None,
1267
+ self_decl_id: Some(DeclarationId::from("Foo::<Foo>")),
1272
1268
  nesting_name_id: name_id,
1273
1269
  },
1274
1270
  [
@@ -1288,7 +1284,7 @@ mod tests {
1288
1284
  assert_declaration_completion_eq!(
1289
1285
  context,
1290
1286
  CompletionReceiver::Expression {
1291
- self_decl_id: None,
1287
+ self_decl_id: Some(DeclarationId::from("Bar")),
1292
1288
  nesting_name_id: name_id,
1293
1289
  },
1294
1290
  [
@@ -1305,6 +1301,48 @@ mod tests {
1305
1301
  );
1306
1302
  }
1307
1303
 
1304
+ #[test]
1305
+ fn completion_candidates_for_instance_variables_inside_singleton_class_body() {
1306
+ let mut context = GraphTest::new();
1307
+ context.index_uri(
1308
+ "file:///foo.rb",
1309
+ "
1310
+ class Foo
1311
+ @class_level_ivar = 1
1312
+
1313
+ def initialize
1314
+ @instance_level_ivar = 1
1315
+ end
1316
+
1317
+ class << self
1318
+ @singleton_level_ivar = 1
1319
+ end
1320
+ end
1321
+ ",
1322
+ );
1323
+ context.resolve();
1324
+
1325
+ let foo_id = Name::new(StringId::from("Foo"), ParentScope::None, None).id();
1326
+ let name_id = Name::new(StringId::from("<Foo>"), ParentScope::Attached(foo_id), Some(foo_id)).id();
1327
+
1328
+ assert_declaration_completion_eq!(
1329
+ context,
1330
+ CompletionReceiver::Expression {
1331
+ self_decl_id: Some(DeclarationId::from("Foo::<Foo>::<<Foo>>")),
1332
+ nesting_name_id: name_id,
1333
+ },
1334
+ [
1335
+ "Module",
1336
+ "Class",
1337
+ "Object",
1338
+ "BasicObject",
1339
+ "Kernel",
1340
+ "Foo",
1341
+ "Foo::<Foo>::<<Foo>>#@singleton_level_ivar"
1342
+ ]
1343
+ );
1344
+ }
1345
+
1308
1346
  #[test]
1309
1347
  fn completion_candidates_includes_constants_accessible_within_lexical_scope() {
1310
1348
  let mut context = GraphTest::new();
@@ -1337,10 +1375,11 @@ mod tests {
1337
1375
  Some(Name::new(StringId::from("Foo"), ParentScope::None, None).id()),
1338
1376
  )
1339
1377
  .id();
1378
+
1340
1379
  assert_declaration_completion_eq!(
1341
1380
  context,
1342
1381
  CompletionReceiver::Expression {
1343
- self_decl_id: None,
1382
+ self_decl_id: Some(DeclarationId::from("Bar")),
1344
1383
  nesting_name_id: name_id,
1345
1384
  },
1346
1385
  [
@@ -1361,7 +1400,7 @@ mod tests {
1361
1400
  assert_declaration_completion_eq!(
1362
1401
  context,
1363
1402
  CompletionReceiver::Expression {
1364
- self_decl_id: None,
1403
+ self_decl_id: Some(DeclarationId::from("Bar")),
1365
1404
  nesting_name_id: name_id,
1366
1405
  },
1367
1406
  [
@@ -1405,7 +1444,7 @@ mod tests {
1405
1444
  assert_declaration_completion_eq!(
1406
1445
  context,
1407
1446
  CompletionReceiver::Expression {
1408
- self_decl_id: None,
1447
+ self_decl_id: Some(DeclarationId::from("Foo::Bar")),
1409
1448
  nesting_name_id: name_id,
1410
1449
  },
1411
1450
  [
@@ -1452,7 +1491,7 @@ mod tests {
1452
1491
  assert_declaration_completion_eq!(
1453
1492
  context,
1454
1493
  CompletionReceiver::Expression {
1455
- self_decl_id: None,
1494
+ self_decl_id: Some(DeclarationId::from("Foo::Bar")),
1456
1495
  nesting_name_id: name_id,
1457
1496
  },
1458
1497
  ["Foo::Bar", "$var2", "$var", "Foo::Bar#bar_m()"]
@@ -1982,7 +2021,7 @@ mod tests {
1982
2021
  assert_declaration_completion_eq!(
1983
2022
  context,
1984
2023
  CompletionReceiver::MethodArgument {
1985
- self_decl_id: None,
2024
+ self_decl_id: Some(DeclarationId::from("Foo")),
1986
2025
  nesting_name_id: name_id,
1987
2026
  method_decl_id: DeclarationId::from("Foo#greet()"),
1988
2027
  },
@@ -2000,6 +2039,48 @@ mod tests {
2000
2039
  );
2001
2040
  }
2002
2041
 
2042
+ #[test]
2043
+ fn method_argument_in_body_completion_uses_singleton_self() {
2044
+ let mut context = GraphTest::new();
2045
+ context.index_uri(
2046
+ "file:///foo.rb",
2047
+ "
2048
+ class Foo
2049
+ @class_level_ivar = 1
2050
+
2051
+ def instance_method; end
2052
+
2053
+ def self.configure(name:, label: 'default'); end
2054
+
2055
+ # `configure(...)` is invoked at class body level — cursor inside the args.
2056
+ end
2057
+ ",
2058
+ );
2059
+ context.resolve();
2060
+
2061
+ let name_id = Name::new(StringId::from("Foo"), ParentScope::None, None).id();
2062
+ assert_declaration_completion_eq!(
2063
+ context,
2064
+ CompletionReceiver::MethodArgument {
2065
+ self_decl_id: Some(DeclarationId::from("Foo::<Foo>")),
2066
+ nesting_name_id: name_id,
2067
+ method_decl_id: DeclarationId::from("Foo::<Foo>#configure()"),
2068
+ },
2069
+ [
2070
+ "Module",
2071
+ "Class",
2072
+ "Object",
2073
+ "BasicObject",
2074
+ "Kernel",
2075
+ "Foo",
2076
+ "Foo::<Foo>#configure()",
2077
+ "Foo::<Foo>#@class_level_ivar",
2078
+ "name:",
2079
+ "label:"
2080
+ ]
2081
+ );
2082
+ }
2083
+
2003
2084
  #[test]
2004
2085
  fn method_argument_completion_no_keyword_params() {
2005
2086
  let mut context = GraphTest::new();
@@ -2018,7 +2099,7 @@ mod tests {
2018
2099
  assert_declaration_completion_eq!(
2019
2100
  context,
2020
2101
  CompletionReceiver::MethodArgument {
2021
- self_decl_id: None,
2102
+ self_decl_id: Some(DeclarationId::from("Foo")),
2022
2103
  nesting_name_id: name_id,
2023
2104
  method_decl_id: DeclarationId::from("Foo#bar()"),
2024
2105
  },
@@ -2044,7 +2125,7 @@ mod tests {
2044
2125
  assert_declaration_completion_eq!(
2045
2126
  context,
2046
2127
  CompletionReceiver::MethodArgument {
2047
- self_decl_id: None,
2128
+ self_decl_id: Some(DeclarationId::from("Foo")),
2048
2129
  nesting_name_id: name_id,
2049
2130
  method_decl_id: DeclarationId::from("Foo#search()"),
2050
2131
  },
@@ -2088,7 +2169,7 @@ mod tests {
2088
2169
  assert_declaration_completion_eq!(
2089
2170
  context,
2090
2171
  CompletionReceiver::MethodArgument {
2091
- self_decl_id: None,
2172
+ self_decl_id: Some(DeclarationId::from("Foo")),
2092
2173
  nesting_name_id: name_id,
2093
2174
  method_decl_id: DeclarationId::from("Foo#bar()"),
2094
2175
  },
@@ -2181,7 +2262,7 @@ mod tests {
2181
2262
  assert_completion_eq!(
2182
2263
  context,
2183
2264
  CompletionReceiver::MethodArgument {
2184
- self_decl_id: None,
2265
+ self_decl_id: Some(DeclarationId::from("Foo")),
2185
2266
  nesting_name_id: name_id,
2186
2267
  method_decl_id: DeclarationId::from("Foo#bar()"),
2187
2268
  },
@@ -2612,7 +2693,7 @@ mod tests {
2612
2693
  assert_declaration_completion_eq!(
2613
2694
  context,
2614
2695
  CompletionReceiver::Expression {
2615
- self_decl_id: None,
2696
+ self_decl_id: Some(DeclarationId::from("Object")),
2616
2697
  nesting_name_id: name_id,
2617
2698
  },
2618
2699
  [
@@ -2950,7 +3031,7 @@ mod tests {
2950
3031
  assert_declaration_completion_eq!(
2951
3032
  context,
2952
3033
  CompletionReceiver::Expression {
2953
- self_decl_id: None,
3034
+ self_decl_id: Some(DeclarationId::from("Foo")),
2954
3035
  nesting_name_id: foo_name_id,
2955
3036
  },
2956
3037
  [
@@ -2970,7 +3051,7 @@ mod tests {
2970
3051
  assert_declaration_completion_eq!(
2971
3052
  context,
2972
3053
  CompletionReceiver::Expression {
2973
- self_decl_id: None,
3054
+ self_decl_id: Some(DeclarationId::from("Bar")),
2974
3055
  nesting_name_id: bar_name_id,
2975
3056
  },
2976
3057
  [
@@ -3286,7 +3367,7 @@ mod tests {
3286
3367
  assert_declaration_completion_eq!(
3287
3368
  context,
3288
3369
  CompletionReceiver::Expression {
3289
- self_decl_id: None,
3370
+ self_decl_id: Some(DeclarationId::from("Foo")),
3290
3371
  nesting_name_id: foo_name_id,
3291
3372
  },
3292
3373
  [