rubydex 0.4.1-aarch64-linux → 0.4.2-aarch64-linux

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +11 -0
  3. data/THIRD_PARTY_LICENSES.html +13 -24
  4. data/lib/rubydex/cli/command/dead_code.rb +59 -0
  5. data/lib/rubydex/librubydex_sys.so +0 -0
  6. data/lib/rubydex/mcp_server/tools/codebase_stats_tool.rb +1 -1
  7. data/lib/rubydex/mcp_server/tools/find_constant_references_tool.rb +4 -4
  8. data/lib/rubydex/mcp_server/tools/find_dead_code_candidates_tool.rb +33 -0
  9. data/lib/rubydex/mcp_server/tools/get_declaration_tool.rb +2 -2
  10. data/lib/rubydex/mcp_server/tools/get_descendants_tool.rb +4 -4
  11. data/lib/rubydex/mcp_server/tools/get_file_declarations_tool.rb +2 -2
  12. data/lib/rubydex/mcp_server/tools/search_declarations_tool.rb +6 -6
  13. data/lib/rubydex/mcp_server.rb +24 -12
  14. data/lib/rubydex/version.rb +1 -1
  15. data/rust/Cargo.lock +17 -11
  16. data/rust/Cargo.toml +3 -0
  17. data/rust/rubydex/Cargo.toml +2 -2
  18. data/rust/rubydex/src/compile_assertions.rs +2 -0
  19. data/rust/rubydex/src/config.rs +1 -1
  20. data/rust/rubydex/src/indexing/rbs_indexer.rs +4 -4
  21. data/rust/rubydex/src/indexing/ruby_indexer.rs +26 -18
  22. data/rust/rubydex/src/indexing/ruby_indexer_tests.rs +42 -2
  23. data/rust/rubydex/src/indexing.rs +6 -6
  24. data/rust/rubydex/src/listing.rs +12 -12
  25. data/rust/rubydex/src/model/built_in.rs +1 -1
  26. data/rust/rubydex/src/model/comment.rs +4 -4
  27. data/rust/rubydex/src/model/declaration.rs +25 -17
  28. data/rust/rubydex/src/model/document.rs +11 -11
  29. data/rust/rubydex/src/model/graph.rs +3 -3
  30. data/rust/rubydex/src/offset.rs +1 -13
  31. data/rust/rubydex/src/operation/ruby_builder.rs +29 -19
  32. data/rust/rubydex/src/query.rs +3 -2
  33. data/rust/rubydex/src/test_utils/graph_test.rs +2 -2
  34. data/rust/rubydex/src/test_utils/local_graph_test.rs +13 -6
  35. data/rust/rubydex-sys/Cargo.toml +2 -2
  36. data/rust/rubydex-sys/src/definition_api.rs +1 -1
  37. data/rust/rubydex-sys/src/graph_api.rs +96 -49
  38. data/rust/rubydex-sys/src/name_api.rs +193 -58
  39. metadata +199 -198
@@ -87,8 +87,8 @@ pub struct RubyOperationBuilder<'a> {
87
87
 
88
88
  impl<'a> RubyOperationBuilder<'a> {
89
89
  #[must_use]
90
- pub fn new(uri: String, source: &'a str) -> Self {
91
- let uri_id = UriId::from(&uri);
90
+ pub fn new(uri: Box<str>, source: &'a str) -> Self {
91
+ let uri_id = UriId::from(&*uri);
92
92
 
93
93
  Self {
94
94
  uri_id,
@@ -112,7 +112,7 @@ impl<'a> RubyOperationBuilder<'a> {
112
112
  self.add_diagnostic(
113
113
  Rule::ParseError,
114
114
  Offset::from_prism_location(&error.location()),
115
- error.message().to_string(),
115
+ String::from_utf8_lossy(error.message()).to_string(),
116
116
  );
117
117
  }
118
118
 
@@ -120,7 +120,7 @@ impl<'a> RubyOperationBuilder<'a> {
120
120
  self.add_diagnostic(
121
121
  Rule::ParseWarning,
122
122
  Offset::from_prism_location(&warning.location()),
123
- warning.message().to_string(),
123
+ String::from_utf8_lossy(warning.message()).to_string(),
124
124
  );
125
125
  }
126
126
 
@@ -489,6 +489,7 @@ impl<'a> RubyOperationBuilder<'a> {
489
489
 
490
490
  // -- Parameters --
491
491
 
492
+ #[allow(clippy::too_many_lines)]
492
493
  fn collect_parameters(&mut self, node: &ruby_prism::DefNode) -> Vec<Parameter> {
493
494
  let mut parameters: Vec<Parameter> = Vec::new();
494
495
 
@@ -588,7 +589,9 @@ impl<'a> RubyOperationBuilder<'a> {
588
589
  }
589
590
  }
590
591
 
591
- if let Some(block) = parameters_list.block() {
592
+ if let Some(block) = parameters_list.block()
593
+ && let Some(block) = block.as_block_parameter_node()
594
+ {
592
595
  let location = block.name_loc().unwrap_or_else(|| block.location());
593
596
  let str_id = self.intern_string(Self::location_to_string(&location));
594
597
  parameters.push(Parameter::Block(ParameterStruct::new(
@@ -1036,8 +1039,8 @@ impl<'a> RubyOperationBuilder<'a> {
1036
1039
  match arg {
1037
1040
  ruby_prism::Node::SymbolNode { .. } => {
1038
1041
  let symbol = arg.as_symbol_node().unwrap();
1039
- let value_loc = symbol.value_loc()?;
1040
- Some((Self::location_to_string(&value_loc), value_loc))
1042
+ let content_loc = symbol.content_loc();
1043
+ Some((Self::location_to_string(&content_loc), content_loc))
1041
1044
  }
1042
1045
  ruby_prism::Node::StringNode { .. } => {
1043
1046
  let string = arg.as_string_node().unwrap();
@@ -1380,7 +1383,7 @@ impl CommentGroup {
1380
1383
 
1381
1384
  fn accepts(&self, next: &ruby_prism::Comment, source: &str) -> bool {
1382
1385
  let current_end_offset = self.end_offset;
1383
- let next_line_start_offset = next.location().start_offset();
1386
+ let next_line_start_offset = next.location().start() as usize;
1384
1387
  let between = &source.as_bytes()[current_end_offset..next_line_start_offset];
1385
1388
  if !between.iter().all(|&b| b.is_ascii_whitespace()) {
1386
1389
  return false;
@@ -1389,14 +1392,14 @@ impl CommentGroup {
1389
1392
  }
1390
1393
 
1391
1394
  fn add_comment(&mut self, comment: &ruby_prism::Comment) {
1392
- self.end_offset = comment.location().end_offset();
1395
+ self.end_offset = comment.location().end() as usize;
1393
1396
  let text = String::from_utf8_lossy(comment.location().as_slice()).to_string();
1394
1397
  if text.lines().any(|line| line.starts_with("# @deprecated")) {
1395
1398
  self.deprecated = true;
1396
1399
  }
1397
1400
  self.comments.push(Comment::new(
1398
1401
  Offset::from_prism_location(&comment.location()),
1399
- text.trim().to_string(),
1402
+ text.trim().into(),
1400
1403
  ));
1401
1404
  }
1402
1405
 
@@ -1783,10 +1786,17 @@ impl Visit<'_> for RubyOperationBuilder<'_> {
1783
1786
  });
1784
1787
  };
1785
1788
 
1786
- let message_loc = node.message_loc();
1787
- if message_loc.is_none() {
1789
+ let message_loc = if node.name().as_slice().is_empty() {
1790
+ // Incomplete calls like `Foo.` have no message. We still index them using the call operator location, so
1791
+ // that the receiver's singleton class gets created, which completion depends on
1792
+ node.call_operator_loc()
1793
+ } else {
1794
+ node.message_loc()
1795
+ };
1796
+
1797
+ let Some(message_loc) = message_loc else {
1788
1798
  return;
1789
- }
1799
+ };
1790
1800
 
1791
1801
  let message = String::from_utf8_lossy(node.name().as_slice()).to_string();
1792
1802
 
@@ -2005,11 +2015,11 @@ impl Visit<'_> for RubyOperationBuilder<'_> {
2005
2015
  self.visit(&receiver);
2006
2016
  }
2007
2017
 
2008
- self.index_method_reference(message.clone(), &node.message_loc().unwrap(), method_receiver);
2018
+ self.index_method_reference(message.clone(), &message_loc, method_receiver);
2009
2019
 
2010
2020
  match message.as_str() {
2011
2021
  ">" | "<" | ">=" | "<=" => {
2012
- self.index_method_reference("<=>".to_string(), &node.message_loc().unwrap(), method_receiver);
2022
+ self.index_method_reference("<=>".to_string(), &message_loc, method_receiver);
2013
2023
  }
2014
2024
  _ => {}
2015
2025
  }
@@ -2134,7 +2144,7 @@ impl Visit<'_> for RubyOperationBuilder<'_> {
2134
2144
  match expression {
2135
2145
  ruby_prism::Node::SymbolNode { .. } => {
2136
2146
  let symbol = expression.as_symbol_node().unwrap();
2137
- let name = Self::location_to_string(&symbol.value_loc().unwrap());
2147
+ let name = Self::location_to_string(&symbol.content_loc());
2138
2148
  self.index_method_reference(name, &node.location(), None);
2139
2149
  }
2140
2150
  _ => {
@@ -2146,13 +2156,13 @@ impl Visit<'_> for RubyOperationBuilder<'_> {
2146
2156
 
2147
2157
  fn visit_alias_method_node(&mut self, node: &ruby_prism::AliasMethodNode<'_>) {
2148
2158
  let mut new_name = if let Some(symbol_node) = node.new_name().as_symbol_node() {
2149
- Self::location_to_string(&symbol_node.value_loc().unwrap())
2159
+ Self::location_to_string(&symbol_node.content_loc())
2150
2160
  } else {
2151
2161
  Self::location_to_string(&node.new_name().location())
2152
2162
  };
2153
2163
 
2154
2164
  let mut old_name = if let Some(symbol_node) = node.old_name().as_symbol_node() {
2155
- Self::location_to_string(&symbol_node.value_loc().unwrap())
2165
+ Self::location_to_string(&symbol_node.content_loc())
2156
2166
  } else {
2157
2167
  Self::location_to_string(&node.old_name().location())
2158
2168
  };
@@ -2229,7 +2239,7 @@ mod tests {
2229
2239
 
2230
2240
  fn build_operations(source: &str) -> OperationBuilderResult {
2231
2241
  let source = crate::test_utils::normalize_indentation(source);
2232
- let builder = RubyOperationBuilder::new("file:///test.rb".to_string(), &source);
2242
+ let builder = RubyOperationBuilder::new("file:///test.rb".into(), &source);
2233
2243
  builder.build()
2234
2244
  }
2235
2245
 
@@ -10,7 +10,7 @@ use crate::model::declaration::{Ancestor, Declaration, Namespace};
10
10
  use crate::model::definitions::{Definition, Parameter};
11
11
  use crate::model::graph::Graph;
12
12
  use crate::model::identity_maps::IdentityHashSet;
13
- use crate::model::ids::{DeclarationId, DefinitionId, NameId, StringId, UriId};
13
+ use crate::model::ids::{ConstantReferenceId, DeclarationId, DefinitionId, NameId, StringId, UriId};
14
14
  use crate::model::keywords::{self, Keyword};
15
15
  use crate::model::name::NameRef;
16
16
  use crate::model::visibility::Visibility;
@@ -875,7 +875,8 @@ pub fn dead_code_candidates(graph: &Graph) -> Vec<DeclarationId> {
875
875
  .iter()
876
876
  .filter(|id| {
877
877
  let decl = declarations.get(id).unwrap();
878
- decl.constant_references().is_some_and(HashSet::is_empty)
878
+ decl.constant_references()
879
+ .is_some_and(<[ConstantReferenceId]>::is_empty)
879
880
  && !matches!(decl, Declaration::Namespace(Namespace::SingletonClass(_)))
880
881
  && !decl.has_no_definitions()
881
882
  && !is_built_in(graph, decl)
@@ -44,14 +44,14 @@ impl GraphTest {
44
44
  /// Indexes a Ruby source
45
45
  pub fn index_uri(&mut self, uri: &str, source: &str) {
46
46
  let source = normalize_indentation(source);
47
- let local_graph = indexing::build_local_graph(uri.to_string(), &source, &LanguageId::Ruby, self.backend);
47
+ let local_graph = indexing::build_local_graph(uri.into(), &source, &LanguageId::Ruby, self.backend);
48
48
  self.graph.consume_document_changes(local_graph);
49
49
  }
50
50
 
51
51
  /// Indexes an RBS source
52
52
  pub fn index_rbs_uri(&mut self, uri: &str, source: &str) {
53
53
  let source = normalize_indentation(source);
54
- indexing::index_source(&mut self.graph, uri, &source, &LanguageId::Rbs);
54
+ indexing::index_source(&mut self.graph, uri.into(), &source, &LanguageId::Rbs);
55
55
  }
56
56
 
57
57
  pub fn delete_uri(&mut self, uri: &str) {
@@ -24,22 +24,29 @@ impl LocalGraphTest {
24
24
 
25
25
  #[must_use]
26
26
  pub fn new_with_backend(uri: &str, source: &str, backend: IndexerBackend) -> Self {
27
- let uri = uri.to_string();
28
27
  let source = normalize_indentation(source);
29
- let graph = build_local_graph(uri.clone(), &source, &LanguageId::Ruby, backend);
30
- Self { uri, source, graph }
28
+ let graph = build_local_graph(uri.into(), &source, &LanguageId::Ruby, backend);
29
+
30
+ Self {
31
+ uri: uri.to_string(),
32
+ source,
33
+ graph,
34
+ }
31
35
  }
32
36
 
33
37
  #[must_use]
34
38
  pub fn new_rbs(uri: &str, source: &str) -> Self {
35
- let uri = uri.to_string();
36
39
  let source = normalize_indentation(source);
37
40
 
38
- let mut indexer = RBSIndexer::new(uri.clone(), &source);
41
+ let mut indexer = RBSIndexer::new(uri.into(), &source);
39
42
  indexer.index();
40
43
  let graph = indexer.local_graph();
41
44
 
42
- Self { uri, source, graph }
45
+ Self {
46
+ uri: uri.to_string(),
47
+ source,
48
+ graph,
49
+ }
43
50
  }
44
51
 
45
52
  #[must_use]
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "rubydex-sys"
3
- version = "0.2.6"
3
+ version.workspace = true
4
4
  edition = "2024"
5
5
  rust-version = "1.89.0"
6
6
  license = "MIT"
@@ -15,7 +15,7 @@ categories = ["development-tools", "external-ffi-bindings"]
15
15
  crate-type = ["cdylib", "staticlib"]
16
16
 
17
17
  [dependencies]
18
- rubydex = { version = "0.2.6", path = "../rubydex" }
18
+ rubydex = { version = "=0.4.2", path = "../rubydex" }
19
19
  libc = "0.2.174"
20
20
  url = "2.5.4"
21
21
  line-index = "0.1.2"
@@ -248,7 +248,7 @@ pub unsafe extern "C" fn rdx_definition_comments(pointer: GraphPointer, definiti
248
248
  .comments()
249
249
  .iter()
250
250
  .map(|c| CommentEntry {
251
- string: CString::new(c.string().as_str()).unwrap().into_raw().cast_const(),
251
+ string: CString::new(c.string()).unwrap().into_raw().cast_const(),
252
252
  location: create_location_for_uri_and_offset(graph, document, c.offset()),
253
253
  })
254
254
  .collect::<Vec<CommentEntry>>()
@@ -173,11 +173,11 @@ pub unsafe extern "C" fn rdx_graph_resolve_constant(
173
173
  let nesting: Vec<String> = unsafe { utils::convert_double_pointer_to_vec(nesting, count).unwrap() };
174
174
  let const_name: String = unsafe { utils::convert_char_ptr_to_string(const_name).unwrap() };
175
175
 
176
- let Some((name_id, names_to_untrack)) = name_api::nesting_stack_to_name_id(graph, &const_name, nesting) else {
176
+ let Some(tracked_name) = name_api::name_from_nesting_names(graph, &const_name, nesting) else {
177
177
  return ptr::null();
178
178
  };
179
179
 
180
- resolve_constant_name(graph, name_id, names_to_untrack)
180
+ resolve_constant_name(tracked_name)
181
181
  })
182
182
  }
183
183
 
@@ -225,15 +225,14 @@ pub unsafe extern "C" fn rdx_graph_resolve_constant_from_definition(
225
225
  }
226
226
  };
227
227
 
228
- let Some((name_id, names_to_untrack)) = name_api::name_in_nesting_to_name_id(graph, &const_name, Some(nesting))
229
- else {
228
+ let Some(tracked_name) = name_api::name_from_lexical_nesting(graph, &const_name, Some(nesting)) else {
230
229
  return CDefinitionConstantResolutionResult {
231
230
  status: CDefinitionConstantResolution::NotFound,
232
231
  declaration: ptr::null(),
233
232
  };
234
233
  };
235
234
 
236
- let declaration = resolve_constant_name(graph, name_id, names_to_untrack);
235
+ let declaration = resolve_constant_name(tracked_name);
237
236
  let status = if declaration.is_null() {
238
237
  CDefinitionConstantResolution::NotFound
239
238
  } else {
@@ -260,18 +259,13 @@ fn class_or_module_definition_name_id(
260
259
  }
261
260
  }
262
261
 
263
- fn resolve_constant_name(graph: &mut Graph, name_id: NameId, names_to_untrack: Vec<NameId>) -> *const CDeclaration {
264
- let declaration_id = Resolver::new(graph).resolve_constant(name_id);
265
- let declaration = declaration_id.map_or(ptr::null(), |id| {
266
- let declaration = graph.declarations().get(&id).unwrap();
262
+ fn resolve_constant_name(mut tracked_name: name_api::TrackedName<'_>) -> *const CDeclaration {
263
+ let name_id = tracked_name.name_id();
264
+ let declaration_id = Resolver::new(tracked_name.graph_mut()).resolve_constant(name_id);
265
+ declaration_id.map_or(ptr::null(), |id| {
266
+ let declaration = tracked_name.graph().declarations().get(&id).unwrap();
267
267
  Box::into_raw(Box::new(CDeclaration::from_declaration(id, declaration))).cast_const()
268
- });
269
-
270
- for name_id in names_to_untrack {
271
- graph.untrack_name(name_id);
272
- }
273
-
274
- declaration
268
+ })
275
269
  }
276
270
 
277
271
  /// Adds glob patterns to exclude from file discovery during indexing.
@@ -745,7 +739,7 @@ pub unsafe extern "C" fn rdx_index_source(
745
739
  };
746
740
 
747
741
  with_mut_graph(pointer, |graph| {
748
- indexing::index_source(graph, &uri_str, source_str, &language);
742
+ indexing::index_source(graph, uri_str.into(), source_str, &language);
749
743
  IndexSourceResult::Success
750
744
  })
751
745
  }
@@ -815,19 +809,19 @@ pub unsafe extern "C" fn rdx_completion_candidates_free(ptr: *mut CompletionCand
815
809
  }
816
810
  }
817
811
 
818
- /// Converts the nesting stack into a `NameId`.
819
- /// The last element of the nesting stack is treated as the self type; if the stack is empty, `"Object"` is used.
812
+ /// Converts the nesting names into a tracked name.
813
+ /// The last nesting name is treated as the self type; if the array is empty, `"Object"` is used.
820
814
  ///
821
- /// Returns `Err` if the nesting array contains invalid UTF-8.
815
+ /// Returns `None` if the nesting array contains invalid UTF-8.
822
816
  ///
823
817
  /// # Safety
824
818
  ///
825
819
  /// `nesting` must point to `nesting_count` valid, null-terminated UTF-8 strings.
826
- unsafe fn completion_nesting_name_id(
820
+ unsafe fn completion_nesting_name(
827
821
  graph: &mut Graph,
828
822
  nesting: *const *const c_char,
829
823
  nesting_count: usize,
830
- ) -> Option<(NameId, Vec<NameId>)> {
824
+ ) -> Option<name_api::TrackedName<'_>> {
831
825
  let mut nesting: Vec<String> = unsafe { utils::convert_double_pointer_to_vec(nesting, nesting_count).ok()? };
832
826
 
833
827
  // When serving completion in a bare script, the self (top level) context is Object
@@ -837,7 +831,7 @@ unsafe fn completion_nesting_name_id(
837
831
  nesting.pop().unwrap()
838
832
  };
839
833
 
840
- name_api::nesting_stack_to_name_id(graph, &self_name, nesting)
834
+ name_api::name_from_nesting_names(graph, &self_name, nesting)
841
835
  }
842
836
 
843
837
  /// The result of a completion operation, carrying either a candidate array or an error message.
@@ -866,19 +860,10 @@ impl CompletionResult {
866
860
  }
867
861
 
868
862
  /// Runs completion for the given receiver and returns a structured result with candidates or an error message
869
- fn run_and_finalize_completion(
870
- graph: &mut Graph,
871
- receiver: CompletionReceiver,
872
- names_to_untrack: Vec<NameId>,
873
- ) -> CompletionResult {
863
+ fn run_and_finalize_completion(graph: &Graph, receiver: CompletionReceiver) -> CompletionResult {
874
864
  let candidates = match query::completion_candidates(graph, CompletionContext::new(receiver)) {
875
865
  Ok(candidates) => candidates,
876
- Err(e) => {
877
- for name_id in names_to_untrack {
878
- graph.untrack_name(name_id);
879
- }
880
- return CompletionResult::error(&e.to_string());
881
- }
866
+ Err(e) => return CompletionResult::error(&e.to_string()),
882
867
  };
883
868
 
884
869
  let entries: Vec<CCompletionCandidate> = candidates
@@ -926,10 +911,6 @@ fn run_and_finalize_completion(
926
911
  })
927
912
  .collect();
928
913
 
929
- for name_id in names_to_untrack {
930
- graph.untrack_name(name_id);
931
- }
932
-
933
914
  CompletionResult::success(CompletionCandidateArray::from_vec(entries))
934
915
  }
935
916
 
@@ -950,20 +931,19 @@ pub unsafe extern "C" fn rdx_graph_complete_expression(
950
931
  self_receiver: *const c_char,
951
932
  ) -> CompletionResult {
952
933
  with_mut_graph(pointer, |graph| {
953
- let Some((name_id, names_to_untrack)) = (unsafe { completion_nesting_name_id(graph, nesting, nesting_count) })
954
- else {
934
+ let Some(tracked_name) = (unsafe { completion_nesting_name(graph, nesting, nesting_count) }) else {
955
935
  return CompletionResult::success(ptr::null_mut());
956
936
  };
937
+ let name_id = tracked_name.name_id();
957
938
 
958
939
  let self_decl_id = unsafe { decl_id_from_char_ptr(self_receiver) };
959
940
 
960
941
  run_and_finalize_completion(
961
- graph,
942
+ tracked_name.graph(),
962
943
  CompletionReceiver::Expression {
963
944
  self_decl_id,
964
945
  nesting_name_id: name_id,
965
946
  },
966
- names_to_untrack,
967
947
  )
968
948
  })
969
949
  }
@@ -997,7 +977,6 @@ pub unsafe extern "C" fn rdx_graph_complete_namespace_access(
997
977
  self_decl_id,
998
978
  namespace_decl_id: declaration_id_from_lookup_name(&name_str),
999
979
  },
1000
- Vec::new(),
1001
980
  )
1002
981
  })
1003
982
  }
@@ -1031,7 +1010,6 @@ pub unsafe extern "C" fn rdx_graph_complete_method_call(
1031
1010
  self_decl_id,
1032
1011
  receiver_decl_id: declaration_id_from_lookup_name(&name_str),
1033
1012
  },
1034
- Vec::new(),
1035
1013
  )
1036
1014
  })
1037
1015
  }
@@ -1060,22 +1038,20 @@ pub unsafe extern "C" fn rdx_graph_complete_method_argument(
1060
1038
  };
1061
1039
 
1062
1040
  with_mut_graph(pointer, |graph| {
1063
- let Some((nesting_name_id, names_to_untrack)) =
1064
- (unsafe { completion_nesting_name_id(graph, nesting, nesting_count) })
1065
- else {
1041
+ let Some(tracked_name) = (unsafe { completion_nesting_name(graph, nesting, nesting_count) }) else {
1066
1042
  return CompletionResult::success(ptr::null_mut());
1067
1043
  };
1044
+ let nesting_name_id = tracked_name.name_id();
1068
1045
 
1069
1046
  let self_decl_id = unsafe { decl_id_from_char_ptr(self_receiver) };
1070
1047
 
1071
1048
  run_and_finalize_completion(
1072
- graph,
1049
+ tracked_name.graph(),
1073
1050
  CompletionReceiver::MethodArgument {
1074
1051
  self_decl_id,
1075
1052
  nesting_name_id,
1076
1053
  method_decl_id: declaration_id_from_lookup_name(&name_str),
1077
1054
  },
1078
- names_to_untrack,
1079
1055
  )
1080
1056
  })
1081
1057
  }
@@ -1196,6 +1172,7 @@ pub unsafe extern "C" fn rdx_keyword_free(ptr: *const CKeyword) {
1196
1172
 
1197
1173
  #[cfg(test)]
1198
1174
  mod tests {
1175
+ use crate::declaration_api;
1199
1176
  use rubydex::indexing::ruby_indexer::RubyIndexer;
1200
1177
 
1201
1178
  use super::*;
@@ -1246,7 +1223,9 @@ mod tests {
1246
1223
  nesting_ptrs.as_ptr(),
1247
1224
  nesting_ptrs.len(),
1248
1225
  );
1226
+ assert!(!decl.is_null());
1249
1227
  assert_eq!((*decl).id(), *DeclarationId::from("Foo::BAR"));
1228
+ declaration_api::free_c_declaration(decl);
1250
1229
  };
1251
1230
 
1252
1231
  let graph = unsafe { Box::from_raw(graph_ptr.cast::<RwLock<Graph>>()) };
@@ -1268,4 +1247,72 @@ mod tests {
1268
1247
  .ref_count()
1269
1248
  );
1270
1249
  }
1250
+
1251
+ #[test]
1252
+ fn names_are_untracked_after_completion_error() {
1253
+ let mut indexer = RubyIndexer::new(
1254
+ "file:///foo.rb".into(),
1255
+ "
1256
+ class Foo
1257
+ end
1258
+ ",
1259
+ );
1260
+ indexer.index();
1261
+
1262
+ let mut graph = Graph::new();
1263
+ graph.consume_document_changes(indexer.local_graph());
1264
+ let mut resolver = Resolver::new(&mut graph);
1265
+ resolver.resolve();
1266
+
1267
+ let (foo_name_id, foo_string_id, name_ref_count, string_ref_count) = graph
1268
+ .names()
1269
+ .iter()
1270
+ .find_map(|(name_id, name)| {
1271
+ let string_id = *name.str();
1272
+ if graph.strings().get(&string_id).unwrap().as_str() == "Foo"
1273
+ && name.parent_scope().is_none()
1274
+ && name.nesting().is_none()
1275
+ {
1276
+ Some((
1277
+ *name_id,
1278
+ string_id,
1279
+ name.ref_count(),
1280
+ graph.strings().get(&string_id).unwrap().ref_count(),
1281
+ ))
1282
+ } else {
1283
+ None
1284
+ }
1285
+ })
1286
+ .unwrap();
1287
+ let name_count = graph.names().len();
1288
+ let string_count = graph.strings().len();
1289
+
1290
+ let graph_ptr = Box::into_raw(Box::new(RwLock::new(graph))) as GraphPointer;
1291
+ let nesting_strings = [CString::new("Foo").unwrap()];
1292
+ let nesting_ptrs: Vec<*const c_char> = nesting_strings.iter().map(|s| s.as_ptr()).collect();
1293
+ let self_receiver = CString::new("Nonexistent").unwrap();
1294
+
1295
+ let result = unsafe {
1296
+ rdx_graph_complete_expression(
1297
+ graph_ptr,
1298
+ nesting_ptrs.as_ptr(),
1299
+ nesting_ptrs.len(),
1300
+ self_receiver.as_ptr(),
1301
+ )
1302
+ };
1303
+ assert!(result.candidates.is_null());
1304
+ assert!(!result.error.is_null());
1305
+ utils::free_c_string(result.error);
1306
+
1307
+ let graph = unsafe { Box::from_raw(graph_ptr.cast::<RwLock<Graph>>()) };
1308
+ let graph = graph.read().unwrap();
1309
+
1310
+ assert_eq!(name_count, graph.names().len());
1311
+ assert_eq!(string_count, graph.strings().len());
1312
+ assert_eq!(name_ref_count, graph.names().get(&foo_name_id).unwrap().ref_count());
1313
+ assert_eq!(
1314
+ string_ref_count,
1315
+ graph.strings().get(&foo_string_id).unwrap().ref_count()
1316
+ );
1317
+ }
1271
1318
  }