rubydex 0.2.7-aarch64-linux → 0.2.9-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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -3
  3. data/THIRD_PARTY_LICENSES.html +132 -1378
  4. data/exe/rdx +28 -2
  5. data/ext/rubydex/extconf.rb +0 -8
  6. data/ext/rubydex/graph.c +43 -29
  7. data/lib/rubydex/3.2/rubydex.so +0 -0
  8. data/lib/rubydex/3.3/rubydex.so +0 -0
  9. data/lib/rubydex/3.4/rubydex.so +0 -0
  10. data/lib/rubydex/4.0/rubydex.so +0 -0
  11. data/lib/rubydex/librubydex_sys.so +0 -0
  12. data/lib/rubydex/mcp_server/protocol.rb +156 -0
  13. data/lib/rubydex/mcp_server/tools/base_tool.rb +109 -0
  14. data/lib/rubydex/mcp_server/tools/codebase_stats_tool.rb +30 -0
  15. data/lib/rubydex/mcp_server/tools/find_constant_references_tool.rb +46 -0
  16. data/lib/rubydex/mcp_server/tools/get_declaration_tool.rb +68 -0
  17. data/lib/rubydex/mcp_server/tools/get_descendants_tool.rb +46 -0
  18. data/lib/rubydex/mcp_server/tools/get_file_declarations_tool.rb +55 -0
  19. data/lib/rubydex/mcp_server/tools/search_declarations_tool.rb +55 -0
  20. data/lib/rubydex/mcp_server.rb +219 -0
  21. data/lib/rubydex/version.rb +1 -1
  22. data/rbi/rubydex.rbi +8 -8
  23. data/rust/Cargo.lock +1 -555
  24. data/rust/Cargo.toml +0 -1
  25. data/rust/rubydex/src/config.rs +54 -34
  26. data/rust/rubydex/src/listing.rs +159 -102
  27. data/rust/rubydex/src/main.rs +148 -5
  28. data/rust/rubydex/src/model/declaration.rs +14 -14
  29. data/rust/rubydex/src/model/graph.rs +92 -9
  30. data/rust/rubydex/src/model/ids.rs +30 -0
  31. data/rust/rubydex/src/model/string_ref.rs +12 -4
  32. data/rust/rubydex/src/operation/applier.rs +0 -2
  33. data/rust/rubydex/src/query.rs +83 -15
  34. data/rust/rubydex/src/resolution.rs +196 -139
  35. data/rust/rubydex/tests/cli.rs +66 -0
  36. data/rust/rubydex-sys/src/declaration_api.rs +2 -2
  37. data/rust/rubydex-sys/src/graph_api.rs +71 -56
  38. metadata +11 -10
  39. data/exe/rubydex_mcp +0 -17
  40. data/lib/rubydex/bin/rubydex_mcp +0 -0
  41. data/rust/rubydex-mcp/Cargo.toml +0 -34
  42. data/rust/rubydex-mcp/src/main.rs +0 -48
  43. data/rust/rubydex-mcp/src/server.rs +0 -1148
  44. data/rust/rubydex-mcp/src/tools.rs +0 -49
  45. data/rust/rubydex-mcp/tests/mcp.rs +0 -302
@@ -1,9 +1,13 @@
1
1
  use clap::{Parser, ValueEnum};
2
- use std::{collections::HashSet, mem};
2
+ use std::{
3
+ fs, mem,
4
+ path::{Path, PathBuf},
5
+ time::{Duration, Instant},
6
+ };
3
7
 
4
8
  use rubydex::{
5
9
  dot,
6
- indexing::{self, IndexerBackend},
10
+ indexing::{self, IndexerBackend, LanguageId, build_local_graph},
7
11
  integrity, listing,
8
12
  model::graph::Graph,
9
13
  resolution::Resolver,
@@ -12,12 +16,17 @@ use rubydex::{
12
16
  timer::{Timer, time_it},
13
17
  },
14
18
  };
19
+ use url::Url;
15
20
 
16
21
  #[derive(Parser, Debug)]
17
22
  #[command(name = "rubydex_cli", about = "A Static Analysis Toolkit for Ruby", version)]
18
23
  #[allow(clippy::struct_excessive_bools)]
19
24
  struct Args {
20
- #[arg(value_name = "PATHS", default_value = ".")]
25
+ #[arg(
26
+ value_name = "PATHS",
27
+ default_value = ".",
28
+ help = "Path(s) to index. If the first path is a directory, it is used as the workspace root for rubydex.toml"
29
+ )]
21
30
  paths: Vec<String>,
22
31
 
23
32
  #[arg(long = "stop-after", help = "Stop after the given stage")]
@@ -52,6 +61,21 @@ struct Args {
52
61
  help = "Write orphan definitions report to specified file"
53
62
  )]
54
63
  report_orphans: Option<String>,
64
+
65
+ #[arg(
66
+ long = "incremental_cycle",
67
+ value_name = "N",
68
+ help = "After the initial build, run N incremental resolution cycles and report their timings"
69
+ )]
70
+ incremental_cycle: Option<usize>,
71
+
72
+ #[arg(
73
+ long = "incremental_files",
74
+ value_name = "N",
75
+ default_value_t = 1,
76
+ help = "Number of files to re-index per incremental cycle"
77
+ )]
78
+ incremental_files: usize,
55
79
  }
56
80
 
57
81
  #[derive(Debug, Clone, ValueEnum)]
@@ -85,6 +109,11 @@ fn exit(print_stats: bool) {
85
109
  std::process::exit(0);
86
110
  }
87
111
 
112
+ fn workspace_path_for(paths: &[String]) -> Option<PathBuf> {
113
+ let first_path = paths.first()?;
114
+ fs::canonicalize(first_path).ok().filter(|path| path.is_dir())
115
+ }
116
+
88
117
  fn main() {
89
118
  let args = Args::parse();
90
119
 
@@ -92,9 +121,21 @@ fn main() {
92
121
  Timer::set_global_timer(Timer::new());
93
122
  }
94
123
 
124
+ let mut graph = Graph::new();
125
+
126
+ if let Some(workspace_path) = workspace_path_for(&args.paths) {
127
+ graph.set_workspace_path(workspace_path);
128
+ if let Err(error) = graph.load_config(None) {
129
+ eprintln!("{error}");
130
+ std::process::exit(1);
131
+ }
132
+ }
133
+
95
134
  // Listing
96
135
 
97
- let (file_paths, errors) = time_it!(listing, { listing::collect_file_paths(args.paths, &HashSet::new()) });
136
+ let (file_paths, errors) = time_it!(listing, {
137
+ listing::collect_file_paths(args.paths, &graph.excluded_patterns())
138
+ });
98
139
 
99
140
  for error in errors {
100
141
  eprintln!("{error}");
@@ -106,8 +147,16 @@ fn main() {
106
147
 
107
148
  // Indexing
108
149
 
109
- let mut graph = Graph::new();
110
150
  let backend = IndexerBackend::from(&args.indexer);
151
+
152
+ // The incremental benchmark re-indexes files after the initial build, so keep a copy of the
153
+ // paths before `index_files` consumes them.
154
+ let incremental_paths = if args.incremental_cycle.is_some() {
155
+ file_paths.clone()
156
+ } else {
157
+ Vec::new()
158
+ };
159
+
111
160
  let errors = time_it!(indexing, { indexing::index_files(&mut graph, file_paths, backend) });
112
161
 
113
162
  for error in errors {
@@ -125,6 +174,12 @@ fn main() {
125
174
  resolver.resolve();
126
175
  });
127
176
 
177
+ // Incremental resolution benchmark. Runs before the stop-after check so it can be combined with
178
+ // `--stop-after=resolution`.
179
+ if let Some(cycles) = args.incremental_cycle {
180
+ run_incremental_resolution(&mut graph, &incremental_paths, cycles, args.incremental_files, backend);
181
+ }
182
+
128
183
  if let Some(StopAfter::Resolution) = args.stop_after {
129
184
  return exit(args.stats);
130
185
  }
@@ -186,3 +241,91 @@ fn main() {
186
241
  // Forget the graph so we don't have to wait for deallocation and let the system reclaim the memory at exit
187
242
  mem::forget(graph);
188
243
  }
244
+
245
+ /// Simulates incremental editing to measure incremental resolution cost. For each cycle it
246
+ /// re-indexes a rotating window of `files_per_cycle` files (parsing plus the same invalidation the
247
+ /// LSP performs on save) and then re-runs resolution over the resulting pending work, reporting
248
+ /// per-cycle and aggregate `resolve()` timings.
249
+ ///
250
+ /// With `--stats`, the `compute_descendants` breakdown printed in the timing summary reflects the
251
+ /// last incremental cycle, since it is recorded on every `resolve()`.
252
+ fn run_incremental_resolution(
253
+ graph: &mut Graph,
254
+ paths: &[PathBuf],
255
+ cycles: usize,
256
+ files_per_cycle: usize,
257
+ backend: IndexerBackend,
258
+ ) {
259
+ if paths.is_empty() || cycles == 0 || files_per_cycle == 0 {
260
+ eprintln!("Skipping incremental resolution: nothing to re-index");
261
+ return;
262
+ }
263
+
264
+ let files_per_cycle = files_per_cycle.min(paths.len());
265
+
266
+ let mut reindex_total = Duration::ZERO;
267
+ let mut resolve_total = Duration::ZERO;
268
+ let mut resolve_min = Duration::MAX;
269
+ let mut resolve_max = Duration::ZERO;
270
+
271
+ println!();
272
+ println!("Incremental resolution ({cycles} cycle(s), {files_per_cycle} file(s)/cycle)");
273
+ println!(" Scenario: no-op reindex; files are indexed again without changing their contents.");
274
+ println!(" This should be the fastest incremental resolution path.");
275
+ println!(" Add scenario-based benchmarks before optimizing incremental resolution.");
276
+
277
+ for cycle in 0..cycles {
278
+ // Re-index the window of files for this cycle. Rotating the window across cycles samples
279
+ // different parts of the codebase rather than measuring the same delta repeatedly.
280
+ let reindex_start = Instant::now();
281
+ for i in 0..files_per_cycle {
282
+ let path = &paths[(cycle * files_per_cycle + i) % paths.len()];
283
+ reindex_file(graph, path, backend);
284
+ }
285
+ reindex_total += reindex_start.elapsed();
286
+
287
+ let resolve_start = Instant::now();
288
+ Resolver::new(graph).resolve();
289
+ let elapsed = resolve_start.elapsed();
290
+
291
+ resolve_total += elapsed;
292
+ resolve_min = resolve_min.min(elapsed);
293
+ resolve_max = resolve_max.max(elapsed);
294
+
295
+ println!(
296
+ " cycle {:>3}: resolve {:9.3}ms",
297
+ cycle + 1,
298
+ elapsed.as_secs_f64() * 1000.0
299
+ );
300
+ }
301
+
302
+ let avg = resolve_total / u32::try_from(cycles).expect("cycle count fits in u32");
303
+
304
+ println!(
305
+ " resolve total {:.3}ms avg {:.3}ms min {:.3}ms max {:.3}ms",
306
+ resolve_total.as_secs_f64() * 1000.0,
307
+ avg.as_secs_f64() * 1000.0,
308
+ resolve_min.as_secs_f64() * 1000.0,
309
+ resolve_max.as_secs_f64() * 1000.0,
310
+ );
311
+ println!(
312
+ " reindex+invalidate total {:.3}ms (parse + document merge, excluded from resolve above)",
313
+ reindex_total.as_secs_f64() * 1000.0,
314
+ );
315
+ }
316
+
317
+ /// Re-indexes a single file into the graph, running the same invalidation the LSP performs on save.
318
+ fn reindex_file(graph: &mut Graph, path: &Path, backend: IndexerBackend) {
319
+ let Ok(source) = fs::read_to_string(path) else {
320
+ eprintln!("Failed to read file `{}`", path.display());
321
+ return;
322
+ };
323
+ let Ok(url) = Url::from_file_path(path) else {
324
+ eprintln!("Couldn't build URI from path `{}`", path.display());
325
+ return;
326
+ };
327
+
328
+ let language = path.extension().map_or(LanguageId::Ruby, LanguageId::from);
329
+ let local_graph = build_local_graph(url.to_string(), &source, &language, backend);
330
+ graph.consume_document_changes(local_graph);
331
+ }
@@ -91,7 +91,7 @@ macro_rules! namespace_declaration {
91
91
  #[derive(Debug)]
92
92
  pub struct $name {
93
93
  /// The fully qualified name of this declaration
94
- name: String,
94
+ name: Box<str>,
95
95
  /// The list of definition IDs that compose this declaration
96
96
  definition_ids: Vec<DefinitionId>,
97
97
  /// The set of references that are made to this declaration
@@ -116,7 +116,7 @@ macro_rules! namespace_declaration {
116
116
  #[must_use]
117
117
  pub fn new(name: String, owner_id: DeclarationId) -> Self {
118
118
  Self {
119
- name,
119
+ name: name.into_boxed_str(),
120
120
  definition_ids: Vec::new(),
121
121
  members: IdentityHashMap::default(),
122
122
  references: IdentityHashSet::default(),
@@ -231,7 +231,7 @@ macro_rules! simple_declaration {
231
231
  #[derive(Debug)]
232
232
  pub struct $name {
233
233
  /// The fully qualified name of this declaration
234
- name: String,
234
+ name: Box<str>,
235
235
  /// The list of definition IDs that compose this declaration
236
236
  definition_ids: Vec<DefinitionId>,
237
237
  /// The set of references that are made to this declaration
@@ -244,7 +244,7 @@ macro_rules! simple_declaration {
244
244
  #[must_use]
245
245
  pub fn new(name: String, owner_id: DeclarationId) -> Self {
246
246
  Self {
247
- name,
247
+ name: name.into_boxed_str(),
248
248
  definition_ids: Vec::new(),
249
249
  references: IdentityHashSet::default(),
250
250
  owner_id,
@@ -608,25 +608,25 @@ impl Namespace {
608
608
  }
609
609
 
610
610
  namespace_declaration!(Class, ClassDeclaration);
611
- assert_mem_size!(ClassDeclaration, 192);
611
+ assert_mem_size!(ClassDeclaration, 184);
612
612
  namespace_declaration!(Module, ModuleDeclaration);
613
- assert_mem_size!(ModuleDeclaration, 192);
613
+ assert_mem_size!(ModuleDeclaration, 184);
614
614
  namespace_declaration!(SingletonClass, SingletonClassDeclaration);
615
- assert_mem_size!(SingletonClassDeclaration, 192);
615
+ assert_mem_size!(SingletonClassDeclaration, 184);
616
616
  namespace_declaration!(Todo, TodoDeclaration);
617
- assert_mem_size!(TodoDeclaration, 192);
617
+ assert_mem_size!(TodoDeclaration, 184);
618
618
  simple_declaration!(ConstantDeclaration, ConstantReferenceId);
619
- assert_mem_size!(ConstantDeclaration, 88);
619
+ assert_mem_size!(ConstantDeclaration, 80);
620
620
  simple_declaration!(MethodDeclaration, MethodReferenceId);
621
- assert_mem_size!(MethodDeclaration, 88);
621
+ assert_mem_size!(MethodDeclaration, 80);
622
622
  simple_declaration!(GlobalVariableDeclaration, GlobalVariableReferenceId);
623
- assert_mem_size!(GlobalVariableDeclaration, 88);
623
+ assert_mem_size!(GlobalVariableDeclaration, 80);
624
624
  simple_declaration!(InstanceVariableDeclaration, InstanceVariableReferenceId);
625
- assert_mem_size!(InstanceVariableDeclaration, 88);
625
+ assert_mem_size!(InstanceVariableDeclaration, 80);
626
626
  simple_declaration!(ClassVariableDeclaration, ClassVariableReferenceId);
627
- assert_mem_size!(ClassVariableDeclaration, 88);
627
+ assert_mem_size!(ClassVariableDeclaration, 80);
628
628
  simple_declaration!(ConstantAliasDeclaration, ConstantReferenceId);
629
- assert_mem_size!(ConstantAliasDeclaration, 88);
629
+ assert_mem_size!(ConstantAliasDeclaration, 80);
630
630
 
631
631
  #[cfg(test)]
632
632
  mod tests {
@@ -13,7 +13,10 @@ use crate::model::definitions::{Definition, MethodVisibilityDefinition, Receiver
13
13
  use crate::model::document::Document;
14
14
  use crate::model::encoding::Encoding;
15
15
  use crate::model::identity_maps::{IdentityHashMap, IdentityHashSet};
16
- use crate::model::ids::{ConstantReferenceId, DeclarationId, DefinitionId, MethodReferenceId, NameId, StringId, UriId};
16
+ use crate::model::ids::{
17
+ ConstantReferenceId, DeclarationId, DefinitionId, MethodReferenceId, NameId, StringId, UriId,
18
+ declaration_id_from_lookup_name,
19
+ };
17
20
  use crate::model::name::{Name, NameRef, ParentScope, ResolvedName};
18
21
  use crate::model::references::{ConstantReference, MethodRef};
19
22
  use crate::model::string_ref::StringRef;
@@ -125,16 +128,16 @@ impl Graph {
125
128
  &mut self.declarations
126
129
  }
127
130
 
128
- /// Adds paths to exclude from file discovery during indexing. Excluded directories will be skipped entirely during
129
- /// directory traversal.
130
- pub fn exclude_paths(&mut self, paths: Vec<PathBuf>) {
131
- self.config.exclude_paths(paths);
131
+ /// Adds glob patterns to exclude from file discovery during indexing. Excluded directories will be skipped entirely
132
+ /// during directory traversal.
133
+ pub fn exclude_patterns(&mut self, patterns: Vec<Box<str>>) {
134
+ self.config.exclude_patterns(patterns);
132
135
  }
133
136
 
134
- /// Returns the set of paths excluded from file discovery.
137
+ /// Returns the set of exclusion patterns.
135
138
  #[must_use]
136
- pub fn excluded_paths(&self) -> HashSet<PathBuf> {
137
- self.config.excluded_paths()
139
+ pub fn excluded_patterns(&self) -> HashSet<Box<str>> {
140
+ self.config.excluded_patterns()
138
141
  }
139
142
 
140
143
  /// Returns the root directory of the workspace being indexed.
@@ -578,7 +581,7 @@ impl Graph {
578
581
 
579
582
  #[must_use]
580
583
  pub fn get(&self, name: &str) -> Option<Vec<&Definition>> {
581
- let declaration_id = DeclarationId::from(name);
584
+ let declaration_id = declaration_id_from_lookup_name(name);
582
585
  let declaration = self.declarations.get(&declaration_id)?;
583
586
 
584
587
  Some(
@@ -1270,6 +1273,10 @@ impl Graph {
1270
1273
  let unqualified_str_id = StringId::from(&decl.unqualified_name());
1271
1274
  let owner_id = *decl.owner_id();
1272
1275
  let is_singleton_class = matches!(decl, Declaration::Namespace(Namespace::SingletonClass(_)));
1276
+ let ancestors_to_detach: Vec<Ancestor> = decl
1277
+ .as_namespace()
1278
+ .map(|ns| ns.ancestors().iter().copied().collect())
1279
+ .unwrap_or_default();
1273
1280
 
1274
1281
  for def_id in def_ids {
1275
1282
  self.push_work(Unit::Definition(def_id));
@@ -1284,6 +1291,16 @@ impl Graph {
1284
1291
  ns.remove_member(&unqualified_str_id);
1285
1292
  }
1286
1293
  }
1294
+
1295
+ // Detach from each complete ancestor's descendant set so we don't leave a stale id in descendants
1296
+ for ancestor in ancestors_to_detach {
1297
+ if let Ancestor::Complete(ancestor_id) = ancestor
1298
+ && let Some(anc_decl) = self.declarations.get_mut(&ancestor_id)
1299
+ && let Some(ns) = anc_decl.as_namespace_mut()
1300
+ {
1301
+ ns.remove_descendant(&decl_id);
1302
+ }
1303
+ }
1287
1304
  }
1288
1305
 
1289
1306
  self.declarations.remove(&decl_id);
@@ -1570,6 +1587,10 @@ mod tests {
1570
1587
  assert_members_eq, assert_no_diagnostics, assert_no_members,
1571
1588
  };
1572
1589
 
1590
+ fn definition_ids(definitions: Vec<&Definition>) -> Vec<DefinitionId> {
1591
+ definitions.into_iter().map(Definition::id).collect()
1592
+ }
1593
+
1573
1594
  #[test]
1574
1595
  fn deleting_a_uri() {
1575
1596
  let mut context = GraphTest::new();
@@ -1992,6 +2013,34 @@ mod tests {
1992
2013
  assert_eq!(definitions[0].offset().start(), 6);
1993
2014
  }
1994
2015
 
2016
+ #[test]
2017
+ fn get_accepts_leading_double_colon() {
2018
+ let mut context = GraphTest::new();
2019
+
2020
+ context.index_uri("file:///foo.rb", "module Foo; module Bar; end; end");
2021
+ context.resolve();
2022
+
2023
+ // Built-in declarations (root-scoped):
2024
+ let unqualified = context.graph().get("Object").expect("unqualified `Object` lookup");
2025
+ let qualified = context.graph().get("::Object").expect("qualified `::Object` lookup");
2026
+ assert_eq!(definition_ids(unqualified), definition_ids(qualified));
2027
+
2028
+ // Indexed declarations, top-level and nested:
2029
+ let unqualified = context.graph().get("Foo").expect("unqualified `Foo` lookup");
2030
+ let qualified = context.graph().get("::Foo").expect("qualified `::Foo` lookup");
2031
+ assert_eq!(definition_ids(unqualified), definition_ids(qualified));
2032
+
2033
+ let unqualified = context.graph().get("Foo::Bar").expect("unqualified `Foo::Bar` lookup");
2034
+ let qualified = context
2035
+ .graph()
2036
+ .get("::Foo::Bar")
2037
+ .expect("qualified `::Foo::Bar` lookup");
2038
+ assert_eq!(definition_ids(unqualified), definition_ids(qualified));
2039
+
2040
+ // Unknown names still return None when prefixed:
2041
+ assert!(context.graph().get("::DoesNotExist").is_none());
2042
+ }
2043
+
1995
2044
  #[test]
1996
2045
  fn adding_another_definition_from_a_different_uri() {
1997
2046
  let mut context = GraphTest::new();
@@ -2499,6 +2548,7 @@ mod tests {
2499
2548
 
2500
2549
  #[cfg(test)]
2501
2550
  mod incremental_resolution_tests {
2551
+ use crate::model::ids::DeclarationId;
2502
2552
  use crate::model::name::NameRef;
2503
2553
  use crate::test_utils::GraphTest;
2504
2554
  use crate::{
@@ -4126,4 +4176,37 @@ mod incremental_resolution_tests {
4126
4176
  assert_declaration_exists!(context, "Foo::<Foo>");
4127
4177
  assert_declaration_exists!(context, "Bar::<Bar>");
4128
4178
  }
4179
+
4180
+ #[test]
4181
+ fn constant_references_through_object_inheritance_are_invalidated() {
4182
+ let mut context = GraphTest::new();
4183
+ context.index_uri("file:///a.rb", "class Foo; end");
4184
+ context.index_uri(
4185
+ "file:///b.rb",
4186
+ r"
4187
+ module Wrap
4188
+ class Foo; end
4189
+ ::Foo
4190
+ Foo
4191
+ end
4192
+ ",
4193
+ );
4194
+ context.resolve();
4195
+
4196
+ context.delete_uri("file:///a.rb");
4197
+ context.resolve();
4198
+
4199
+ let kernel = context
4200
+ .graph()
4201
+ .declarations()
4202
+ .get(&DeclarationId::from("Kernel"))
4203
+ .unwrap();
4204
+
4205
+ for id in kernel.as_namespace().unwrap().descendants() {
4206
+ assert!(
4207
+ context.graph().declarations().contains_key(id),
4208
+ "Kernel has stale descendant id {id:?} with no backing declaration"
4209
+ );
4210
+ }
4211
+ }
4129
4212
  } // mod incremental_resolution_tests
@@ -9,6 +9,15 @@ pub struct DeclarationMarker;
9
9
  /// `DeclarationId` represents the ID of a fully qualified name. For example, `Foo::Bar` or `Foo#my_method`
10
10
  pub type DeclarationId = Id<DeclarationMarker>;
11
11
 
12
+ /// Creates a `DeclarationId` from a user-facing lookup name.
13
+ ///
14
+ /// Declaration names are stored without the root-scope marker, so `"::Object"`
15
+ /// and `"Object"` must resolve to the same declaration ID.
16
+ #[must_use]
17
+ pub fn declaration_id_from_lookup_name(name: &str) -> DeclarationId {
18
+ DeclarationId::from(name.strip_prefix("::").unwrap_or(name))
19
+ }
20
+
12
21
  #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone, Copy)]
13
22
  pub struct DefinitionMarker;
14
23
 
@@ -84,3 +93,24 @@ assert_mem_size!(ClassVariableReferenceId, 8);
84
93
  pub struct InstanceVariableMarker;
85
94
  pub type InstanceVariableReferenceId = Id<InstanceVariableMarker>;
86
95
  assert_mem_size!(InstanceVariableReferenceId, 8);
96
+
97
+ #[cfg(test)]
98
+ mod tests {
99
+ use super::*;
100
+
101
+ #[test]
102
+ fn declaration_id_from_lookup_name_accepts_root_scope_marker() {
103
+ assert_eq!(
104
+ DeclarationId::from("Object"),
105
+ declaration_id_from_lookup_name("::Object")
106
+ );
107
+ assert_eq!(
108
+ DeclarationId::from("Foo::Bar"),
109
+ declaration_id_from_lookup_name("::Foo::Bar")
110
+ );
111
+ assert_ne!(
112
+ DeclarationId::from("Object"),
113
+ declaration_id_from_lookup_name("::::Object")
114
+ );
115
+ }
116
+ }
@@ -9,15 +9,23 @@ use std::ops::Deref;
9
9
  /// graph when its count reaches zero.
10
10
  #[derive(Debug)]
11
11
  pub struct StringRef {
12
- value: String,
12
+ value: Box<str>,
13
13
  ref_count: u32,
14
14
  }
15
- assert_mem_size!(StringRef, 32);
15
+ assert_mem_size!(StringRef, 24);
16
16
 
17
17
  impl StringRef {
18
18
  #[must_use]
19
19
  pub fn new(value: String) -> Self {
20
- Self { value, ref_count: 1 }
20
+ Self {
21
+ value: value.into_boxed_str(),
22
+ ref_count: 1,
23
+ }
24
+ }
25
+
26
+ #[must_use]
27
+ pub fn as_str(&self) -> &str {
28
+ &self.value
21
29
  }
22
30
 
23
31
  #[must_use]
@@ -44,7 +52,7 @@ impl StringRef {
44
52
  }
45
53
 
46
54
  impl Deref for StringRef {
47
- type Target = String;
55
+ type Target = str;
48
56
 
49
57
  fn deref(&self) -> &Self::Target {
50
58
  &self.value
@@ -116,9 +116,7 @@ impl OperationApplier {
116
116
  _ => {}
117
117
  }
118
118
  }
119
- }
120
119
 
121
- impl OperationApplier {
122
120
  fn apply_operation(&mut self, op: Operation) {
123
121
  match op {
124
122
  Operation::EnterClass(op) => self.apply_enter_class(op),
@@ -27,12 +27,25 @@ pub enum MatchMode {
27
27
  Exact,
28
28
  }
29
29
 
30
+ /// Searches all declarations in parallel based on fully qualified names. Accepts multiple queries in case the caller
31
+ /// wants to find multiple patterns without having to re-traverse the graph and also accepts match mode.
32
+ ///
33
+ /// Note: an empty query returns all declarations, so if any are included the rest of the queries will be ignored.
34
+ ///
30
35
  /// # Panics
31
36
  ///
32
37
  /// Will panic if any of the threads panic
33
- pub fn declaration_search(graph: &Graph, query: &str, match_mode: &MatchMode) -> Vec<DeclarationId> {
38
+ pub fn declaration_search(graph: &Graph, queries: &[&str], match_mode: &MatchMode) -> Vec<DeclarationId> {
34
39
  let num_threads = thread::available_parallelism().map_or(4, std::num::NonZero::get);
35
40
  let declarations = graph.declarations();
41
+
42
+ // An empty query matches all declarations as per the LSP specification and is equivalent to fetching all of them
43
+ // directly. Since an empty query matches all, there's no point in checking the other queries or pay the price of
44
+ // spawning threads.
45
+ if queries.iter().any(|q| q.is_empty()) {
46
+ return declarations.keys().copied().collect();
47
+ }
48
+
36
49
  let ids: Vec<DeclarationId> = declarations.keys().copied().collect();
37
50
  let chunk_size = ids.len().div_ceil(num_threads);
38
51
 
@@ -48,16 +61,8 @@ pub fn declaration_search(graph: &Graph, query: &str, match_mode: &MatchMode) ->
48
61
  chunk
49
62
  .iter()
50
63
  .filter(|id| {
51
- let declaration = declarations.get(id).unwrap();
52
- let name = declaration.name();
53
- match match_mode {
54
- MatchMode::Fuzzy => {
55
- // When the query is empty, we return everything as per the LSP specification.
56
- // Otherwise, we compute the match score and return anything with a score greater than zero
57
- query.is_empty() || match_score(query, name) > 0
58
- }
59
- MatchMode::Exact => name.contains(query),
60
- }
64
+ let name = declarations.get(id).unwrap().name();
65
+ queries.iter().any(|query| matches_query(query, name, match_mode))
61
66
  })
62
67
  .copied()
63
68
  .collect::<Vec<_>>()
@@ -69,6 +74,15 @@ pub fn declaration_search(graph: &Graph, query: &str, match_mode: &MatchMode) ->
69
74
  })
70
75
  }
71
76
 
77
+ /// Returns whether a single `query` matches `name` under the given [`MatchMode`].
78
+ #[must_use]
79
+ fn matches_query(query: &str, name: &str, match_mode: &MatchMode) -> bool {
80
+ match match_mode {
81
+ MatchMode::Fuzzy => match_score(query, name) > 0,
82
+ MatchMode::Exact => name.contains(query),
83
+ }
84
+ }
85
+
72
86
  #[must_use]
73
87
  fn match_score(query: &str, target: &str) -> usize {
74
88
  let mut query_chars = query.chars().peekable();
@@ -836,7 +850,7 @@ mod tests {
836
850
  assert_results_eq!($context, $query, &MatchMode::default(), $expected);
837
851
  };
838
852
  ($context:expr, $query:expr, $match_mode:expr, $expected:expr) => {
839
- let actual = declaration_search(&$context.graph(), $query, $match_mode);
853
+ let actual = declaration_search(&$context.graph(), &[$query], $match_mode);
840
854
  assert_eq!(
841
855
  actual,
842
856
  $expected
@@ -945,8 +959,8 @@ mod tests {
945
959
  "
946
960
  });
947
961
  context.resolve();
948
- let exact_results = declaration_search(context.graph(), "", &MatchMode::Exact);
949
- let fuzzy_results = declaration_search(context.graph(), "", &MatchMode::Fuzzy);
962
+ let exact_results = declaration_search(context.graph(), &[""], &MatchMode::Exact);
963
+ let fuzzy_results = declaration_search(context.graph(), &[""], &MatchMode::Fuzzy);
950
964
 
951
965
  assert_eq!(exact_results.len(), fuzzy_results.len());
952
966
  assert_eq!(context.graph().declarations().len(), exact_results.len());
@@ -972,6 +986,51 @@ mod tests {
972
986
  assert_results_eq!(context, "#Is_A?()", ["Foo#is_a_foo?()", "Bar#is_a?()"]);
973
987
  }
974
988
 
989
+ #[test]
990
+ fn multiple_queries_return_union_of_matches() {
991
+ let mut context = GraphTest::new();
992
+ context.index_uri("file:///foo.rb", {
993
+ r"
994
+ class Foo
995
+ def foo_method; end
996
+ def bar_method; end
997
+ def other_method; end
998
+ end
999
+ "
1000
+ });
1001
+ context.resolve();
1002
+
1003
+ let results = declaration_search(context.graph(), &["#foo_method()", "#bar_method()"], &MatchMode::Exact);
1004
+ let mut names: Vec<String> = results
1005
+ .iter()
1006
+ .map(|id| context.graph().declarations().get(id).unwrap().name().to_string())
1007
+ .collect();
1008
+ names.sort();
1009
+
1010
+ assert_eq!(names, ["Foo#bar_method()", "Foo#foo_method()"]);
1011
+ }
1012
+
1013
+ #[test]
1014
+ fn overlapping_queries_do_not_duplicate_results() {
1015
+ let mut context = GraphTest::new();
1016
+ context.index_uri("file:///foo.rb", {
1017
+ r"
1018
+ class Foo
1019
+ def is_a_foo?; end
1020
+ end
1021
+ "
1022
+ });
1023
+ context.resolve();
1024
+
1025
+ let results = declaration_search(context.graph(), &["is_a", "foo?"], &MatchMode::Exact);
1026
+ let matches = results
1027
+ .iter()
1028
+ .filter(|id| context.graph().declarations().get(id).unwrap().name() == "Foo#is_a_foo?()")
1029
+ .count();
1030
+
1031
+ assert_eq!(matches, 1);
1032
+ }
1033
+
975
1034
  fn test_root() -> PathBuf {
976
1035
  let root = if cfg!(windows) { "C:\\" } else { "/" };
977
1036
  PathBuf::from_str(root).unwrap()
@@ -2767,7 +2826,16 @@ mod tests {
2767
2826
  self_decl_id: None,
2768
2827
  nesting_name_id,
2769
2828
  },
2770
- ["Bar#@@bar_cvar"]
2829
+ [
2830
+ "Bar",
2831
+ "Bar#@@bar_cvar",
2832
+ "BasicObject",
2833
+ "Class",
2834
+ "Foo",
2835
+ "Kernel",
2836
+ "Module",
2837
+ "Object"
2838
+ ]
2771
2839
  );
2772
2840
  }
2773
2841