rubydex 0.4.0 → 0.4.2

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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +9 -14
  3. data/THIRD_PARTY_LICENSES.html +13 -24
  4. data/ext/rubydex/definition.c +34 -0
  5. data/ext/rubydex/definition.h +3 -0
  6. data/ext/rubydex/diagnostic.c +2 -49
  7. data/ext/rubydex/extconf.rb +4 -2
  8. data/ext/rubydex/graph.c +59 -11
  9. data/lib/rubydex/cli/command/dead_code.rb +59 -0
  10. data/lib/rubydex/cli/command/lint/explain.rb +8 -6
  11. data/lib/rubydex/cli/command/lint.rb +10 -4
  12. data/lib/rubydex/linter/rule_loader.rb +1 -1
  13. data/lib/rubydex/mcp_server/tools/codebase_stats_tool.rb +1 -1
  14. data/lib/rubydex/mcp_server/tools/find_constant_references_tool.rb +4 -4
  15. data/lib/rubydex/mcp_server/tools/find_dead_code_candidates_tool.rb +33 -0
  16. data/lib/rubydex/mcp_server/tools/get_declaration_tool.rb +2 -2
  17. data/lib/rubydex/mcp_server/tools/get_descendants_tool.rb +4 -4
  18. data/lib/rubydex/mcp_server/tools/get_file_declarations_tool.rb +2 -2
  19. data/lib/rubydex/mcp_server/tools/search_declarations_tool.rb +6 -6
  20. data/lib/rubydex/mcp_server.rb +24 -12
  21. data/lib/rubydex/rules/dynamic_ancestor.rb +29 -0
  22. data/lib/rubydex/rules/dynamic_constant_reference.rb +25 -0
  23. data/lib/rubydex/rules/dynamic_singleton_definition.rb +30 -0
  24. data/lib/rubydex/rules/invalid_constant_visibility.rb +28 -0
  25. data/lib/rubydex/rules/invalid_method_visibility.rb +28 -0
  26. data/lib/rubydex/rules/parse_error.rb +25 -0
  27. data/lib/rubydex/rules/parse_warning.rb +28 -0
  28. data/lib/rubydex/rules/top_level_mixin_self.rb +27 -0
  29. data/lib/rubydex/rules/undefined_constant_visibility_target.rb +27 -0
  30. data/lib/rubydex/rules/undefined_method_visibility_target.rb +27 -0
  31. data/lib/rubydex/skill.rb +1 -0
  32. data/lib/rubydex/version.rb +1 -1
  33. data/lib/rubydex.rb +2 -0
  34. data/lib/rubydex_linter/rules/rule_structure.rb +19 -4
  35. data/rbi/rubydex.rbi +32 -130
  36. data/rust/Cargo.lock +17 -11
  37. data/rust/Cargo.toml +3 -0
  38. data/rust/rubydex/Cargo.toml +6 -2
  39. data/rust/rubydex/src/bin/generate_ruby_rules.rs +94 -0
  40. data/rust/rubydex/src/compile_assertions.rs +2 -0
  41. data/rust/rubydex/src/config.rs +1 -1
  42. data/rust/rubydex/src/diagnostic.rs +142 -51
  43. data/rust/rubydex/src/indexing/rbs_indexer.rs +4 -4
  44. data/rust/rubydex/src/indexing/ruby_indexer.rs +26 -18
  45. data/rust/rubydex/src/indexing/ruby_indexer_tests.rs +42 -2
  46. data/rust/rubydex/src/indexing.rs +6 -6
  47. data/rust/rubydex/src/listing.rs +12 -12
  48. data/rust/rubydex/src/model/built_in.rs +1 -1
  49. data/rust/rubydex/src/model/comment.rs +4 -4
  50. data/rust/rubydex/src/model/declaration.rs +25 -17
  51. data/rust/rubydex/src/model/document.rs +11 -11
  52. data/rust/rubydex/src/model/graph.rs +3 -3
  53. data/rust/rubydex/src/offset.rs +1 -13
  54. data/rust/rubydex/src/operation/ruby_builder.rs +29 -19
  55. data/rust/rubydex/src/query.rs +193 -2
  56. data/rust/rubydex/src/test_utils/graph_test.rs +2 -2
  57. data/rust/rubydex/src/test_utils/local_graph_test.rs +13 -6
  58. data/rust/rubydex-sys/Cargo.toml +2 -2
  59. data/rust/rubydex-sys/src/definition_api.rs +69 -2
  60. data/rust/rubydex-sys/src/diagnostic_api.rs +0 -35
  61. data/rust/rubydex-sys/src/graph_api.rs +193 -45
  62. data/rust/rubydex-sys/src/name_api.rs +219 -48
  63. metadata +191 -180
@@ -900,7 +900,7 @@ mod class_and_module_tests {
900
900
  assert_def_name_eq!(&context, def, "Baz");
901
901
  assert_def_name_offset_eq!(&context, def, "3:11-3:14");
902
902
  assert!(def.superclass_ref().is_none());
903
- assert!(def.members().is_empty());
903
+ assert_eq!(def.members(), []);
904
904
 
905
905
  assert_definition_at!(&context, "2:3-4:6", Class, |parent_nesting| {
906
906
  assert_eq!(parent_nesting.id(), def.lexical_nesting_id().unwrap());
@@ -948,7 +948,7 @@ mod class_and_module_tests {
948
948
  assert_def_name_eq!(&context, def, "Quuux");
949
949
  assert_def_name_offset_eq!(&context, def, "3:13-3:18");
950
950
  assert!(def.superclass_ref().is_none());
951
- assert!(def.members().is_empty());
951
+ assert_eq!(def.members(), []);
952
952
 
953
953
  assert_definition_at!(&context, "2:3-4:6", Class, |parent_nesting| {
954
954
  assert_eq!(parent_nesting.id(), def.lexical_nesting_id().unwrap());
@@ -1250,6 +1250,46 @@ mod method_tests {
1250
1250
  });
1251
1251
  }
1252
1252
 
1253
+ #[test]
1254
+ fn index_def_node_with_no_block_parameter() {
1255
+ let context = index_source({
1256
+ "
1257
+ def foo(a, &nil); end
1258
+ "
1259
+ });
1260
+
1261
+ assert_no_local_diagnostics!(&context);
1262
+
1263
+ assert_definition_at!(&context, "1:1-1:22", Method, |def| {
1264
+ assert_simple_signature!(def, |params| {
1265
+ assert_eq!(params.len(), 1);
1266
+ assert_parameter!(&params[0], RequiredPositional, |param| {
1267
+ assert_string_eq!(context, param.str(), "a");
1268
+ });
1269
+ });
1270
+ });
1271
+ }
1272
+
1273
+ #[test]
1274
+ fn index_def_node_with_trailing_comma_in_parameters() {
1275
+ let context = index_source({
1276
+ "
1277
+ def foo(a,); end
1278
+ "
1279
+ });
1280
+
1281
+ assert_no_local_diagnostics!(&context);
1282
+
1283
+ assert_definition_at!(&context, "1:1-1:17", Method, |def| {
1284
+ assert_simple_signature!(def, |params| {
1285
+ assert_eq!(params.len(), 1);
1286
+ assert_parameter!(&params[0], RequiredPositional, |param| {
1287
+ assert_string_eq!(context, param.str(), "a");
1288
+ });
1289
+ });
1290
+ });
1291
+ }
1292
+
1253
1293
  #[test]
1254
1294
  fn index_nested_method_definitions() {
1255
1295
  let context = index_source({
@@ -101,7 +101,7 @@ impl Job for IndexingJob {
101
101
  };
102
102
 
103
103
  let language = self.path.extension().map_or(LanguageId::Ruby, LanguageId::from);
104
- let local_graph = build_local_graph(url.to_string(), &source, &language, self.backend);
104
+ let local_graph = build_local_graph(url.to_string().into(), &source, &language, self.backend);
105
105
 
106
106
  self.local_graph_tx
107
107
  .send(local_graph)
@@ -110,8 +110,8 @@ impl Job for IndexingJob {
110
110
  }
111
111
 
112
112
  /// Indexes a single source string in memory, dispatching to the appropriate indexer based on `language_id`.
113
- pub fn index_source(graph: &mut Graph, uri: &str, source: &str, language_id: &LanguageId) {
114
- let local_graph = build_local_graph(uri.to_string(), source, language_id, IndexerBackend::RubyIndexer);
113
+ pub fn index_source(graph: &mut Graph, uri: Box<str>, source: &str, language_id: &LanguageId) {
114
+ let local_graph = build_local_graph(uri, source, language_id, IndexerBackend::RubyIndexer);
115
115
  graph.consume_document_changes(local_graph);
116
116
  }
117
117
 
@@ -153,7 +153,7 @@ pub fn index_files(graph: &mut Graph, paths: Vec<PathBuf>, backend: IndexerBacke
153
153
 
154
154
  /// Indexes a source string using the appropriate indexer for the given language.
155
155
  #[must_use]
156
- pub fn build_local_graph(uri: String, source: &str, language: &LanguageId, backend: IndexerBackend) -> LocalGraph {
156
+ pub fn build_local_graph(uri: Box<str>, source: &str, language: &LanguageId, backend: IndexerBackend) -> LocalGraph {
157
157
  match language {
158
158
  LanguageId::Ruby => match backend {
159
159
  IndexerBackend::RubyIndexer => {
@@ -203,7 +203,7 @@ mod tests {
203
203
  let mut graph = Graph::new();
204
204
  let errors = index_files(&mut graph, vec![relative_to_pwd.clone()], IndexerBackend::RubyIndexer);
205
205
 
206
- assert!(errors.is_empty());
206
+ assert_eq!(errors.as_slice(), []);
207
207
  assert_eq!(graph.documents().len(), 2);
208
208
  }
209
209
 
@@ -228,7 +228,7 @@ mod tests {
228
228
  assert_eq!(6, graph.definitions().len());
229
229
  assert_eq!(2, graph.documents().len());
230
230
 
231
- index_source(&mut graph, &uri, "", &LanguageId::Ruby);
231
+ index_source(&mut graph, uri.into(), "", &LanguageId::Ruby);
232
232
 
233
233
  assert_eq!(5, graph.definitions().len());
234
234
  assert_eq!(2, graph.documents().len());
@@ -211,7 +211,7 @@ mod tests {
211
211
 
212
212
  let (files, errors) = collect_document_paths(&context, &["foo", "bar"]);
213
213
 
214
- assert!(errors.is_empty());
214
+ assert_eq!(errors.as_slice(), []);
215
215
 
216
216
  assert_eq!(
217
217
  files,
@@ -236,7 +236,7 @@ mod tests {
236
236
 
237
237
  let (files, errors) = collect_document_paths(&context, &["bar"]);
238
238
 
239
- assert!(errors.is_empty());
239
+ assert_eq!(errors.as_slice(), []);
240
240
 
241
241
  assert_eq!(
242
242
  files,
@@ -260,7 +260,7 @@ mod tests {
260
260
 
261
261
  let (files, errors) = collect_document_paths(&context, &["lib", "sig", "config.ru"]);
262
262
 
263
- assert!(errors.is_empty());
263
+ assert_eq!(errors.as_slice(), []);
264
264
 
265
265
  assert_eq!(
266
266
  [
@@ -287,7 +287,7 @@ mod tests {
287
287
  &HashSet::new(),
288
288
  );
289
289
 
290
- assert!(files.is_empty());
290
+ assert_eq!(files, [] as [std::path::PathBuf; 0]);
291
291
 
292
292
  assert_eq!(
293
293
  errors,
@@ -315,8 +315,8 @@ mod tests {
315
315
 
316
316
  let (files, errors) = collect_file_paths(vec![relative_root.to_string_lossy().into_owned()], &HashSet::new());
317
317
 
318
- assert!(errors.is_empty());
319
- assert!(!files.is_empty());
318
+ assert_eq!(errors.as_slice(), []);
319
+ assert_ne!(files, [] as [std::path::PathBuf; 0]);
320
320
  assert!(
321
321
  files.iter().all(|path| path.is_absolute()),
322
322
  "expected only absolute paths, got {files:?}"
@@ -336,7 +336,7 @@ mod tests {
336
336
 
337
337
  let (files, errors) = collect_document_paths_with_exclusions(&context, &["included", "excluded"], &excluded);
338
338
 
339
- assert!(errors.is_empty());
339
+ assert_eq!(errors.as_slice(), []);
340
340
  assert_eq!(files, [included.to_str().unwrap().to_string()]);
341
341
  }
342
342
 
@@ -353,7 +353,7 @@ mod tests {
353
353
 
354
354
  let (files, errors) = collect_document_paths_with_exclusions(&context, &["root"], &excluded);
355
355
 
356
- assert!(errors.is_empty());
356
+ assert_eq!(errors.as_slice(), []);
357
357
  assert_eq!(files, [kept.to_str().unwrap().to_string()]);
358
358
  }
359
359
 
@@ -374,7 +374,7 @@ mod tests {
374
374
 
375
375
  let (files, errors) = collect_document_paths(&context, &["project"]);
376
376
 
377
- assert!(errors.is_empty());
377
+ assert_eq!(errors.as_slice(), []);
378
378
  // The symlink is indexed at its own path, not resolved to the target.
379
379
  let alias = PathBuf::from("project").join("alias.rb");
380
380
  assert_eq!(files, [alias.to_str().unwrap().to_string()]);
@@ -398,7 +398,7 @@ mod tests {
398
398
 
399
399
  let (files, errors) = collect_document_paths(&context, &["project"]);
400
400
 
401
- assert!(errors.is_empty());
401
+ assert_eq!(errors.as_slice(), []);
402
402
  // The symlinked directory is not followed, so `outside/bar.rb` is never reached.
403
403
  assert_eq!(files, [kept.to_str().unwrap().to_string()]);
404
404
  }
@@ -415,7 +415,7 @@ mod tests {
415
415
 
416
416
  let (files, errors) = collect_document_paths(&context, &["link"]);
417
417
 
418
- assert!(errors.is_empty());
418
+ assert_eq!(errors.as_slice(), []);
419
419
  // The requested root is traversed; files are indexed under the requested (symlink) path.
420
420
  let foo = PathBuf::from("link").join("foo.rb");
421
421
  assert_eq!(files, [foo.to_str().unwrap().to_string()]);
@@ -458,7 +458,7 @@ mod tests {
458
458
 
459
459
  let (files, errors) = collect_document_paths_with_exclusions(&context, &["lib"], &excluded);
460
460
 
461
- assert!(errors.is_empty());
461
+ assert_eq!(errors.as_slice(), []);
462
462
  assert_eq!(files, [kept.to_str().unwrap().to_string()]);
463
463
  }
464
464
  }
@@ -48,7 +48,7 @@ pub fn add_built_in_data(graph: &mut Graph) {
48
48
  class Class < Module
49
49
  end
50
50
  ";
51
- indexing::index_source(graph, uri.as_ref(), source, &LanguageId::Rbs);
51
+ indexing::index_source(graph, uri.to_string().into(), source, &LanguageId::Rbs);
52
52
 
53
53
  // Creating declarations eagerly is still necessary because we need to associate correct ownership data no matter in
54
54
  // what order we discover classes and modules
@@ -4,13 +4,13 @@ use crate::offset::Offset;
4
4
  #[derive(Debug, Clone)]
5
5
  pub struct Comment {
6
6
  offset: Offset,
7
- string: String,
7
+ string: Box<str>,
8
8
  }
9
- assert_mem_size!(Comment, 32);
9
+ assert_mem_size!(Comment, 24);
10
10
 
11
11
  impl Comment {
12
12
  #[must_use]
13
- pub fn new(offset: Offset, string: String) -> Self {
13
+ pub fn new(offset: Offset, string: Box<str>) -> Self {
14
14
  Self { offset, string }
15
15
  }
16
16
 
@@ -20,7 +20,7 @@ impl Comment {
20
20
  }
21
21
 
22
22
  #[must_use]
23
- pub fn string(&self) -> &String {
23
+ pub fn string(&self) -> &str {
24
24
  &self.string
25
25
  }
26
26
  }
@@ -119,7 +119,7 @@ pub struct DeclarationCore<T> {
119
119
  /// The list of definition IDs that compose this declaration
120
120
  definition_ids: Vec<DefinitionId>,
121
121
  /// The set of references that are made to this declaration
122
- references: IdentityHashSet<T>,
122
+ references: Vec<T>,
123
123
  /// The ID of the owner of this declaration
124
124
  owner_id: DeclarationId,
125
125
  }
@@ -130,7 +130,7 @@ impl<T: Eq + Hash> DeclarationCore<T> {
130
130
  Self {
131
131
  name: name.into_boxed_str(),
132
132
  definition_ids: Vec::new(),
133
- references: IdentityHashSet::default(),
133
+ references: Vec::new(),
134
134
  owner_id,
135
135
  }
136
136
  }
@@ -152,16 +152,24 @@ impl<T: Eq + Hash> DeclarationCore<T> {
152
152
  }
153
153
 
154
154
  #[must_use]
155
- pub fn references(&self) -> &IdentityHashSet<T> {
155
+ pub fn references(&self) -> &[T] {
156
156
  &self.references
157
157
  }
158
158
 
159
159
  pub fn add_reference(&mut self, reference_id: T) {
160
- self.references.insert(reference_id);
160
+ debug_assert!(
161
+ !self.references.contains(&reference_id),
162
+ "Cannot add the same exact reference to a declaration twice. Duplicate reference IDs"
163
+ );
164
+
165
+ self.references.push(reference_id);
161
166
  }
162
167
 
163
168
  pub fn remove_reference(&mut self, reference_id: &T) {
164
- self.references.remove(reference_id);
169
+ if let Some(pos) = self.references.iter().position(|id| id == reference_id) {
170
+ self.references.swap_remove(pos);
171
+ self.references.shrink_to_fit();
172
+ }
165
173
  }
166
174
 
167
175
  #[must_use]
@@ -493,7 +501,7 @@ impl Declaration {
493
501
  /// Returns the constant reference IDs for declarations that track constant references (`Namespace`, `Constant`,
494
502
  /// `ConstantAlias`). Returns `None` for other declaration types.
495
503
  #[must_use]
496
- pub fn constant_references(&self) -> Option<&IdentityHashSet<ConstantReferenceId>> {
504
+ pub fn constant_references(&self) -> Option<&[ConstantReferenceId]> {
497
505
  match self {
498
506
  Declaration::Namespace(it) => Some(it.references()),
499
507
  Declaration::Constant(it) | Declaration::ConstantAlias(it) => Some(it.references()),
@@ -559,7 +567,7 @@ impl Namespace {
559
567
  }
560
568
 
561
569
  #[must_use]
562
- pub fn references(&self) -> &IdentityHashSet<ConstantReferenceId> {
570
+ pub fn references(&self) -> &[ConstantReferenceId] {
563
571
  all_namespaces!(self, it => &it.core.references)
564
572
  }
565
573
 
@@ -674,31 +682,31 @@ impl Namespace {
674
682
  }
675
683
 
676
684
  namespace_declaration!(Class, ClassDeclaration);
677
- assert_mem_size!(ClassDeclaration, 184);
685
+ assert_mem_size!(ClassDeclaration, 176);
678
686
  namespace_declaration!(Module, ModuleDeclaration);
679
- assert_mem_size!(ModuleDeclaration, 184);
687
+ assert_mem_size!(ModuleDeclaration, 176);
680
688
  namespace_declaration!(SingletonClass, SingletonClassDeclaration);
681
- assert_mem_size!(SingletonClassDeclaration, 184);
689
+ assert_mem_size!(SingletonClassDeclaration, 176);
682
690
  namespace_declaration!(Todo, TodoDeclaration);
683
- assert_mem_size!(TodoDeclaration, 184);
691
+ assert_mem_size!(TodoDeclaration, 176);
684
692
 
685
693
  pub type ConstantDeclaration = DeclarationCore<ConstantReferenceId>;
686
- assert_mem_size!(ConstantDeclaration, 80);
694
+ assert_mem_size!(ConstantDeclaration, 72);
687
695
 
688
696
  pub type MethodDeclaration = DeclarationCore<MethodReferenceId>;
689
- assert_mem_size!(MethodDeclaration, 80);
697
+ assert_mem_size!(MethodDeclaration, 72);
690
698
 
691
699
  pub type GlobalVariableDeclaration = DeclarationCore<GlobalVariableReferenceId>;
692
- assert_mem_size!(GlobalVariableDeclaration, 80);
700
+ assert_mem_size!(GlobalVariableDeclaration, 72);
693
701
 
694
702
  pub type InstanceVariableDeclaration = DeclarationCore<InstanceVariableReferenceId>;
695
- assert_mem_size!(InstanceVariableDeclaration, 80);
703
+ assert_mem_size!(InstanceVariableDeclaration, 72);
696
704
 
697
705
  pub type ClassVariableDeclaration = DeclarationCore<ClassVariableReferenceId>;
698
- assert_mem_size!(ClassVariableDeclaration, 80);
706
+ assert_mem_size!(ClassVariableDeclaration, 72);
699
707
 
700
708
  pub type ConstantAliasDeclaration = DeclarationCore<ConstantReferenceId>;
701
- assert_mem_size!(ConstantAliasDeclaration, 80);
709
+ assert_mem_size!(ConstantAliasDeclaration, 72);
702
710
 
703
711
  #[cfg(test)]
704
712
  mod tests {
@@ -12,7 +12,7 @@ use crate::model::ids::{ConstantReferenceId, DefinitionId, MethodReferenceId};
12
12
  // definitions and references discovered in it
13
13
  #[derive(Debug)]
14
14
  pub struct Document {
15
- uri: String,
15
+ uri: Box<str>,
16
16
  line_index: LineIndex,
17
17
  definition_ids: Vec<DefinitionId>,
18
18
  method_reference_ids: Vec<MethodReferenceId>,
@@ -20,11 +20,11 @@ pub struct Document {
20
20
  diagnostics: Vec<Diagnostic>,
21
21
  content_hash: u64,
22
22
  }
23
- assert_mem_size!(Document, 184);
23
+ assert_mem_size!(Document, 176);
24
24
 
25
25
  impl Document {
26
26
  #[must_use]
27
- pub fn new(uri: String, source: &str) -> Self {
27
+ pub fn new(uri: Box<str>, source: &str) -> Self {
28
28
  Self {
29
29
  uri,
30
30
  line_index: LineIndex::new(source),
@@ -176,7 +176,7 @@ mod tests {
176
176
  #[test]
177
177
  #[should_panic(expected = "Cannot add the same exact definition to a document twice. Duplicate definition IDs")]
178
178
  fn inserting_duplicate_definitions() {
179
- let mut document = Document::new("file:///foo.rb".to_string(), "class Foo; end");
179
+ let mut document = Document::new("file:///foo.rb".into(), "class Foo; end");
180
180
  let def_id = DefinitionId::new(123);
181
181
 
182
182
  document.add_definition(def_id);
@@ -187,7 +187,7 @@ mod tests {
187
187
 
188
188
  #[test]
189
189
  fn tracking_references() {
190
- let mut document = Document::new("file:///foo.rb".to_string(), "class Foo; end");
190
+ let mut document = Document::new("file:///foo.rb".into(), "class Foo; end");
191
191
  let method_ref = MethodReferenceId::new(1);
192
192
  let constant_ref = ConstantReferenceId::new(2);
193
193
 
@@ -202,7 +202,7 @@ mod tests {
202
202
  fn require_path() {
203
203
  let root = test_root();
204
204
  let path = root.join("lib").join("foo").join("bar.rb");
205
- let uri = Url::from_file_path(path).unwrap().to_string();
205
+ let uri = Url::from_file_path(path).unwrap().to_string().into();
206
206
  let load_paths = [root.join("lib")];
207
207
 
208
208
  let document = Document::new(uri, "");
@@ -213,7 +213,7 @@ mod tests {
213
213
  fn require_path_first_load_path_wins() {
214
214
  let root = test_root();
215
215
  let path = root.join("a").join("b").join("foo.rb");
216
- let uri = Url::from_file_path(path).unwrap().to_string();
216
+ let uri = Url::from_file_path(path).unwrap().to_string().into();
217
217
  let load_path_a = root.join("a");
218
218
  let load_path_ab = root.join("a").join("b");
219
219
  let load_paths = [load_path_a, load_path_ab];
@@ -228,18 +228,18 @@ mod tests {
228
228
  let load_paths = [root.join("lib")];
229
229
 
230
230
  // non-file URI
231
- let document = Document::new("untitled:Untitled-1".to_string(), "");
231
+ let document = Document::new("untitled:Untitled-1".into(), "");
232
232
  assert!(document.require_path(&load_paths).is_none());
233
233
 
234
234
  // non-.rb extension
235
235
  let path = root.join("lib").join("foo.so");
236
- let uri = Url::from_file_path(path).unwrap().to_string();
236
+ let uri = Url::from_file_path(path).unwrap().to_string().into();
237
237
  let document = Document::new(uri, "");
238
238
  assert!(document.require_path(&load_paths).is_none());
239
239
 
240
240
  // /libfoo is not under /lib - should not match due to partial directory name
241
241
  let path = root.join("libfoo").join("bar.rb");
242
- let uri = Url::from_file_path(path).unwrap().to_string();
242
+ let uri = Url::from_file_path(path).unwrap().to_string().into();
243
243
  let document = Document::new(uri, "");
244
244
  assert!(document.require_path(&load_paths).is_none());
245
245
  }
@@ -248,7 +248,7 @@ mod tests {
248
248
  fn require_path_with_spaces() {
249
249
  let root = test_root();
250
250
  let path = root.join("lib").join("space foo").join("bar.rb");
251
- let uri = Url::from_file_path(path).unwrap().to_string();
251
+ let uri = Url::from_file_path(path).unwrap().to_string().into();
252
252
  let load_paths = [root.join("lib")];
253
253
 
254
254
  let document = Document::new(uri, "");
@@ -1759,7 +1759,7 @@ mod tests {
1759
1759
  assert_eq!(context.graph().constant_references.len(), 4);
1760
1760
  {
1761
1761
  let declaration = context.graph().declarations().get(&DeclarationId::from("Foo")).unwrap();
1762
- assert!(declaration.as_namespace().unwrap().references().is_empty());
1762
+ assert_eq!(declaration.as_namespace().unwrap().references(), []);
1763
1763
  }
1764
1764
  }
1765
1765
 
@@ -2090,14 +2090,14 @@ mod tests {
2090
2090
  let definitions = context.graph().get("CommentedClass").unwrap();
2091
2091
  let def = definitions.first().unwrap();
2092
2092
  assert_eq!(
2093
- def.comments().iter().map(Comment::string).collect::<Vec<&String>>(),
2093
+ def.comments().iter().map(Comment::string).collect::<Vec<&str>>(),
2094
2094
  ["# This is a class comment", "# Multi-line comment"]
2095
2095
  );
2096
2096
 
2097
2097
  let definitions = context.graph().get("CommentedModule").unwrap();
2098
2098
  let def = definitions.first().unwrap();
2099
2099
  assert_eq!(
2100
- def.comments().iter().map(Comment::string).collect::<Vec<&String>>(),
2100
+ def.comments().iter().map(Comment::string).collect::<Vec<&str>>(),
2101
2101
  ["# Module comment"]
2102
2102
  );
2103
2103
 
@@ -30,21 +30,9 @@ impl Offset {
30
30
  Self { start, end }
31
31
  }
32
32
 
33
- /// # Panics
34
- ///
35
- /// This function can panic if the Prism location offsets do not fit into a u32
36
33
  #[must_use]
37
34
  pub fn from_prism_location(location: &ruby_prism::Location) -> Self {
38
- Self::new(
39
- location
40
- .start_offset()
41
- .try_into()
42
- .expect("Expected usize `start` to fit in `u32`"),
43
- location
44
- .end_offset()
45
- .try_into()
46
- .expect("Expected usize `end` to fit in `u32`"),
47
- )
35
+ Self::new(location.start(), location.end())
48
36
  }
49
37
 
50
38
  /// # Panics
@@ -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