rubydex 0.2.8 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +69 -3
  3. data/THIRD_PARTY_LICENSES.html +168 -1381
  4. data/exe/rdx +136 -34
  5. data/ext/rubydex/declaration.c +1 -1
  6. data/ext/rubydex/definition.c +32 -4
  7. data/ext/rubydex/extconf.rb +0 -8
  8. data/ext/rubydex/graph.c +29 -18
  9. data/ext/rubydex/query.c +105 -0
  10. data/ext/rubydex/query.h +8 -0
  11. data/ext/rubydex/reference.c +60 -0
  12. data/ext/rubydex/rubydex.c +2 -0
  13. data/ext/rubydex/utils.c +12 -0
  14. data/ext/rubydex/utils.h +5 -0
  15. data/lib/rubydex/mcp_server/protocol.rb +156 -0
  16. data/lib/rubydex/mcp_server/tools/base_tool.rb +109 -0
  17. data/lib/rubydex/mcp_server/tools/codebase_stats_tool.rb +30 -0
  18. data/lib/rubydex/mcp_server/tools/find_constant_references_tool.rb +46 -0
  19. data/lib/rubydex/mcp_server/tools/get_declaration_tool.rb +68 -0
  20. data/lib/rubydex/mcp_server/tools/get_descendants_tool.rb +46 -0
  21. data/lib/rubydex/mcp_server/tools/get_file_declarations_tool.rb +55 -0
  22. data/lib/rubydex/mcp_server/tools/search_declarations_tool.rb +55 -0
  23. data/lib/rubydex/mcp_server.rb +219 -0
  24. data/lib/rubydex/version.rb +1 -1
  25. data/rbi/rubydex.rbi +26 -4
  26. data/rust/Cargo.lock +5 -552
  27. data/rust/Cargo.toml +0 -1
  28. data/rust/rubydex/Cargo.toml +1 -0
  29. data/rust/rubydex/benches/graph_memory.rs +22 -4
  30. data/rust/rubydex/src/compile_assertions.rs +15 -0
  31. data/rust/rubydex/src/config.rs +54 -34
  32. data/rust/rubydex/src/diagnostic.rs +1 -1
  33. data/rust/rubydex/src/indexing/rbs_indexer.rs +14 -2
  34. data/rust/rubydex/src/indexing/ruby_indexer.rs +49 -58
  35. data/rust/rubydex/src/indexing/ruby_indexer_tests.rs +72 -16
  36. data/rust/rubydex/src/listing.rs +159 -102
  37. data/rust/rubydex/src/main.rs +3 -125
  38. data/rust/rubydex/src/model/declaration.rs +0 -11
  39. data/rust/rubydex/src/model/definitions.rs +27 -26
  40. data/rust/rubydex/src/model/document.rs +43 -7
  41. data/rust/rubydex/src/model/graph.rs +47 -35
  42. data/rust/rubydex/src/model/id.rs +55 -0
  43. data/rust/rubydex/src/model/ids.rs +21 -9
  44. data/rust/rubydex/src/model/name.rs +35 -7
  45. data/rust/rubydex/src/model/references.rs +16 -13
  46. data/rust/rubydex/src/operation/applier.rs +0 -2
  47. data/rust/rubydex/src/operation/ruby_builder.rs +48 -59
  48. data/rust/rubydex/src/query/cypher/schema.rs +790 -0
  49. data/rust/rubydex/src/query/cypher/schema_info.rs +161 -0
  50. data/rust/rubydex/src/query/cypher/tests.rs +228 -0
  51. data/rust/rubydex/src/query/cypher.rs +57 -0
  52. data/rust/rubydex/src/query.rs +2 -0
  53. data/rust/rubydex/src/resolution.rs +248 -227
  54. data/rust/rubydex/src/resolution_tests.rs +263 -65
  55. data/rust/rubydex-sys/src/declaration_api.rs +6 -3
  56. data/rust/rubydex-sys/src/definition_api.rs +27 -7
  57. data/rust/rubydex-sys/src/graph_api.rs +175 -27
  58. data/rust/rubydex-sys/src/reference_api.rs +58 -12
  59. metadata +17 -10
  60. data/exe/rubydex_mcp +0 -17
  61. data/lib/rubydex/bin/rubydex_mcp.exe +0 -0
  62. data/rust/rubydex-mcp/Cargo.toml +0 -34
  63. data/rust/rubydex-mcp/src/main.rs +0 -48
  64. data/rust/rubydex-mcp/src/server.rs +0 -1148
  65. data/rust/rubydex-mcp/src/tools.rs +0 -49
  66. data/rust/rubydex-mcp/tests/mcp.rs +0 -302
@@ -3,9 +3,9 @@ use crate::{
3
3
  job_queue::{Job, JobQueue},
4
4
  };
5
5
  use crossbeam_channel::{Sender, unbounded};
6
+ use glob::Pattern;
6
7
  use std::{
7
8
  collections::HashSet,
8
- fs,
9
9
  hash::BuildHasher,
10
10
  path::{Path, PathBuf},
11
11
  sync::Arc,
@@ -16,7 +16,7 @@ pub struct FileDiscoveryJob {
16
16
  queue: Arc<JobQueue>,
17
17
  paths_tx: Sender<PathBuf>,
18
18
  errors_tx: Sender<Errors>,
19
- excluded_paths: Arc<HashSet<PathBuf>>,
19
+ excluded_patterns: Arc<Vec<Pattern>>,
20
20
  }
21
21
 
22
22
  impl FileDiscoveryJob {
@@ -26,24 +26,17 @@ impl FileDiscoveryJob {
26
26
  queue: Arc<JobQueue>,
27
27
  paths_tx: Sender<PathBuf>,
28
28
  errors_tx: Sender<Errors>,
29
- excluded_paths: Arc<HashSet<PathBuf>>,
29
+ excluded_patterns: Arc<Vec<Pattern>>,
30
30
  ) -> Self {
31
31
  Self {
32
32
  path,
33
33
  queue,
34
34
  paths_tx,
35
35
  errors_tx,
36
- excluded_paths,
36
+ excluded_patterns,
37
37
  }
38
38
  }
39
- }
40
-
41
- fn is_indexable_file(path: &Path) -> bool {
42
- path.extension()
43
- .is_some_and(|ext| ext == "rb" || ext == "rake" || ext == "rbs" || ext == "ru")
44
- }
45
39
 
46
- impl FileDiscoveryJob {
47
40
  fn handle_file(&self, path: &Path) {
48
41
  if is_indexable_file(path) {
49
42
  self.paths_tx
@@ -52,29 +45,6 @@ impl FileDiscoveryJob {
52
45
  }
53
46
  }
54
47
 
55
- fn handle_symlink(&self, path: &PathBuf) {
56
- let Ok(canonicalized) = fs::canonicalize(path) else {
57
- self.send_error(Errors::FileError(format!(
58
- "Failed to canonicalize symlink: `{}`",
59
- path.display(),
60
- )));
61
-
62
- return;
63
- };
64
-
65
- if self.excluded_paths.contains(&canonicalized) {
66
- return;
67
- }
68
-
69
- self.queue.push(Box::new(FileDiscoveryJob::new(
70
- canonicalized,
71
- Arc::clone(&self.queue),
72
- self.paths_tx.clone(),
73
- self.errors_tx.clone(),
74
- Arc::clone(&self.excluded_paths),
75
- )));
76
- }
77
-
78
48
  fn send_error(&self, error: Errors) {
79
49
  self.errors_tx
80
50
  .send(error)
@@ -84,64 +54,59 @@ impl FileDiscoveryJob {
84
54
 
85
55
  impl Job for FileDiscoveryJob {
86
56
  fn run(&self) {
87
- if self.path.is_dir() {
88
- let Ok(read_dir) = self.path.read_dir() else {
57
+ let Ok(read_dir) = self.path.read_dir() else {
58
+ if self.path.is_file() {
59
+ self.handle_file(&self.path);
60
+ } else {
89
61
  self.send_error(Errors::FileError(format!(
90
62
  "Failed to read directory `{}`",
63
+ self.path.display()
64
+ )));
65
+ }
66
+
67
+ return;
68
+ };
69
+
70
+ for result in read_dir {
71
+ let Ok(entry) = result else {
72
+ self.send_error(Errors::FileError(format!(
73
+ "Failed to read directory `{}`: {result:?}",
91
74
  self.path.display(),
92
75
  )));
93
76
 
94
- return;
77
+ continue;
95
78
  };
96
79
 
97
- for result in read_dir {
98
- let Ok(entry) = result else {
99
- self.send_error(Errors::FileError(format!(
100
- "Failed to read directory `{}`: {result:?}",
101
- self.path.display(),
102
- )));
103
-
104
- continue;
105
- };
106
-
107
- let kind = entry.file_type().unwrap();
108
-
109
- if kind.is_dir() {
110
- if self.excluded_paths.contains(&entry.path()) {
111
- continue;
112
- }
113
-
114
- self.queue.push(Box::new(FileDiscoveryJob::new(
115
- entry.path(),
116
- Arc::clone(&self.queue),
117
- self.paths_tx.clone(),
118
- self.errors_tx.clone(),
119
- Arc::clone(&self.excluded_paths),
120
- )));
121
- } else if kind.is_file() {
122
- self.handle_file(&entry.path());
123
- } else if kind.is_symlink() {
124
- self.handle_symlink(&entry.path());
125
- } else {
126
- self.send_error(Errors::FileError(format!(
127
- "Path `{}` is not a file or directory",
128
- entry.path().display()
129
- )));
130
- }
80
+ let path = entry.path();
81
+
82
+ if is_excluded(&self.excluded_patterns, &path) {
83
+ continue;
84
+ }
85
+
86
+ if entry.file_type().unwrap().is_dir() {
87
+ self.queue.push(Box::new(FileDiscoveryJob::new(
88
+ path,
89
+ Arc::clone(&self.queue),
90
+ self.paths_tx.clone(),
91
+ self.errors_tx.clone(),
92
+ Arc::clone(&self.excluded_patterns),
93
+ )));
94
+ } else {
95
+ self.handle_file(&path);
131
96
  }
132
- } else if self.path.is_file() {
133
- self.handle_file(&self.path);
134
- } else if self.path.is_symlink() {
135
- self.handle_symlink(&self.path);
136
- } else {
137
- self.send_error(Errors::FileError(format!(
138
- "Path `{}` is not a file or directory",
139
- self.path.display()
140
- )));
141
97
  }
142
98
  }
143
99
  }
144
100
 
101
+ fn is_indexable_file(path: &Path) -> bool {
102
+ path.extension()
103
+ .is_some_and(|ext| ext == "rb" || ext == "rake" || ext == "rbs" || ext == "ru")
104
+ }
105
+
106
+ fn is_excluded(excluded_patterns: &[Pattern], path: &Path) -> bool {
107
+ excluded_patterns.iter().any(|pattern| pattern.matches_path(path))
108
+ }
109
+
145
110
  /// Recursively collects all Ruby files for the given workspace and dependencies, returning a vector of document instances
146
111
  ///
147
112
  /// # Errors
@@ -154,34 +119,42 @@ impl Job for FileDiscoveryJob {
154
119
  #[must_use]
155
120
  pub fn collect_file_paths<S: BuildHasher>(
156
121
  paths: Vec<String>,
157
- excluded: &HashSet<PathBuf, S>,
122
+ excluded: &HashSet<Box<str>, S>,
158
123
  ) -> (Vec<PathBuf>, Vec<Errors>) {
159
124
  let queue = Arc::new(JobQueue::new());
160
125
  let (files_tx, files_rx) = unbounded();
161
126
  let (errors_tx, errors_rx) = unbounded();
162
127
 
163
- // Canonicalize the excluded paths since they may be symlinks
164
- let excluded: Arc<HashSet<PathBuf>> = Arc::new(excluded.iter().filter_map(|p| fs::canonicalize(p).ok()).collect());
128
+ let excluded_patterns: Arc<Vec<Pattern>> =
129
+ Arc::new(excluded.iter().filter_map(|entry| Pattern::new(entry).ok()).collect());
165
130
 
166
131
  for path in paths {
167
- let Ok(canonicalized) = fs::canonicalize(&path) else {
132
+ let Ok(path) = std::path::absolute(&path) else {
168
133
  errors_tx
169
- .send(Errors::FileError(format!("Path `{path}` does not exist")))
134
+ .send(Errors::FileError(format!("Failed to resolve path `{path}`")))
170
135
  .expect("errors receiver dropped before run completion");
171
136
 
172
137
  continue;
173
138
  };
174
139
 
175
- if excluded.contains(&canonicalized) {
140
+ if !path.exists() {
141
+ errors_tx
142
+ .send(Errors::FileError(format!("Path `{}` does not exist", path.display())))
143
+ .expect("errors receiver dropped before run completion");
144
+
145
+ continue;
146
+ }
147
+
148
+ if is_excluded(&excluded_patterns, &path) {
176
149
  continue;
177
150
  }
178
151
 
179
152
  queue.push(Box::new(FileDiscoveryJob::new(
180
- canonicalized,
153
+ path,
181
154
  Arc::clone(&queue),
182
155
  files_tx.clone(),
183
156
  errors_tx.clone(),
184
- Arc::clone(&excluded),
157
+ Arc::clone(&excluded_patterns),
185
158
  )));
186
159
  }
187
160
 
@@ -205,7 +178,7 @@ mod tests {
205
178
  fn collect_document_paths_with_exclusions(
206
179
  context: &Context,
207
180
  paths: &[&str],
208
- excluded: &HashSet<PathBuf>,
181
+ excluded: &HashSet<Box<str>>,
209
182
  ) -> (Vec<String>, Vec<Errors>) {
210
183
  let (files, errors) = collect_file_paths(
211
184
  paths
@@ -324,6 +297,31 @@ mod tests {
324
297
  );
325
298
  }
326
299
 
300
+ #[cfg(unix)]
301
+ #[test]
302
+ fn collect_files_emits_absolute_paths_for_relative_roots() {
303
+ let context = Context::new();
304
+ context.touch(PathBuf::from("project").join("foo.rb"));
305
+
306
+ // Express the project directory as a path relative to the process working directory.
307
+ let working_directory = std::env::current_dir().unwrap();
308
+ let mut relative_root = PathBuf::new();
309
+ for _ in 0..working_directory.components().count() - 1 {
310
+ relative_root.push("..");
311
+ }
312
+ let project = context.absolute_path_to("project");
313
+ let relative_root = relative_root.join(project.strip_prefix("/").unwrap());
314
+
315
+ let (files, errors) = collect_file_paths(vec![relative_root.to_string_lossy().into_owned()], &HashSet::new());
316
+
317
+ assert!(errors.is_empty());
318
+ assert!(!files.is_empty());
319
+ assert!(
320
+ files.iter().all(|path| path.is_absolute()),
321
+ "expected only absolute paths, got {files:?}"
322
+ );
323
+ }
324
+
327
325
  #[test]
328
326
  fn collect_files_excludes_directories() {
329
327
  let context = Context::new();
@@ -333,7 +331,7 @@ mod tests {
333
331
  context.touch(&excluded_file);
334
332
 
335
333
  let mut excluded = HashSet::new();
336
- excluded.insert(context.absolute_path_to("excluded"));
334
+ excluded.insert(context.absolute_path_to("excluded").to_string_lossy().into());
337
335
 
338
336
  let (files, errors) = collect_document_paths_with_exclusions(&context, &["included", "excluded"], &excluded);
339
337
 
@@ -350,7 +348,7 @@ mod tests {
350
348
  context.touch(&nested);
351
349
 
352
350
  let mut excluded = HashSet::new();
353
- excluded.insert(context.absolute_path_to("root/skip"));
351
+ excluded.insert("**/skip".into());
354
352
 
355
353
  let (files, errors) = collect_document_paths_with_exclusions(&context, &["root"], &excluded);
356
354
 
@@ -360,23 +358,82 @@ mod tests {
360
358
 
361
359
  #[cfg(unix)]
362
360
  #[test]
363
- fn collect_files_excludes_symlinked_directories() {
361
+ fn collect_files_indexes_symlinked_files_at_their_own_path() {
364
362
  let context = Context::new();
365
- let included = PathBuf::from("included").join("foo.rb");
366
- let excluded_file = PathBuf::from("real_dir").join("bar.rb");
367
- context.touch(&included);
368
- context.touch(&excluded_file);
363
+ let target = PathBuf::from("outside").join("real.rb");
364
+ context.touch(&target);
365
+ context.mkdir("project");
366
+
367
+ // Create a symlink to a file outside the traversed tree: project/alias.rb -> outside/real.rb
368
+ std::os::unix::fs::symlink(
369
+ context.absolute_path_to("outside/real.rb"),
370
+ context.absolute_path_to("project/alias.rb"),
371
+ )
372
+ .unwrap();
373
+
374
+ let (files, errors) = collect_document_paths(&context, &["project"]);
375
+
376
+ assert!(errors.is_empty());
377
+ // The symlink is indexed at its own path, not resolved to the target.
378
+ let alias = PathBuf::from("project").join("alias.rb");
379
+ assert_eq!(files, [alias.to_str().unwrap().to_string()]);
380
+ }
381
+
382
+ #[cfg(unix)]
383
+ #[test]
384
+ fn collect_files_does_not_follow_symlinked_directories() {
385
+ let context = Context::new();
386
+ let kept = PathBuf::from("project").join("foo.rb");
387
+ let outside = PathBuf::from("outside").join("bar.rb");
388
+ context.touch(&kept);
389
+ context.touch(&outside);
369
390
 
370
- // Create a symlink: link -> real_dir
371
- std::os::unix::fs::symlink(context.absolute_path_to("real_dir"), context.absolute_path_to("link")).unwrap();
391
+ // Create a symlink inside the traversed tree: project/link -> outside
392
+ std::os::unix::fs::symlink(
393
+ context.absolute_path_to("outside"),
394
+ context.absolute_path_to("project/link"),
395
+ )
396
+ .unwrap();
397
+
398
+ let (files, errors) = collect_document_paths(&context, &["project"]);
399
+
400
+ assert!(errors.is_empty());
401
+ // The symlinked directory is not followed, so `outside/bar.rb` is never reached.
402
+ assert_eq!(files, [kept.to_str().unwrap().to_string()]);
403
+ }
404
+
405
+ #[cfg(unix)]
406
+ #[test]
407
+ fn collect_files_indexes_symlinked_directory_roots() {
408
+ let context = Context::new();
409
+ let target = PathBuf::from("real").join("foo.rb");
410
+ context.touch(&target);
411
+
412
+ // A symlink to a directory passed as an explicit root, as `Graph#workspace_paths` does via `File.directory?`.
413
+ std::os::unix::fs::symlink(context.absolute_path_to("real"), context.absolute_path_to("link")).unwrap();
414
+
415
+ let (files, errors) = collect_document_paths(&context, &["link"]);
416
+
417
+ assert!(errors.is_empty());
418
+ // The requested root is traversed; files are indexed under the requested (symlink) path.
419
+ let foo = PathBuf::from("link").join("foo.rb");
420
+ assert_eq!(files, [foo.to_str().unwrap().to_string()]);
421
+ }
422
+
423
+ #[test]
424
+ fn collect_files_excludes_nested_files_matching_globs() {
425
+ let context = Context::new();
426
+ let kept = PathBuf::from("lib").join("foo.rb");
427
+ let excluded_file = PathBuf::from("lib").join("version.rb");
428
+ context.touch(&kept);
429
+ context.touch(&excluded_file);
372
430
 
373
- // Excluding the real directory while requesting to index the symlink should properly exclude the link
374
431
  let mut excluded = HashSet::new();
375
- excluded.insert(context.absolute_path_to("real_dir"));
432
+ excluded.insert("**/version.rb".into());
376
433
 
377
- let (files, errors) = collect_document_paths_with_exclusions(&context, &["included", "link"], &excluded);
434
+ let (files, errors) = collect_document_paths_with_exclusions(&context, &["lib"], &excluded);
378
435
 
379
436
  assert!(errors.is_empty());
380
- assert_eq!(files, [included.to_str().unwrap().to_string()]);
437
+ assert_eq!(files, [kept.to_str().unwrap().to_string()]);
381
438
  }
382
439
  }
@@ -1,13 +1,9 @@
1
1
  use clap::{Parser, ValueEnum};
2
- use std::{
3
- fs, mem,
4
- path::{Path, PathBuf},
5
- time::{Duration, Instant},
6
- };
2
+ use std::{fs, mem, path::PathBuf};
7
3
 
8
4
  use rubydex::{
9
5
  dot,
10
- indexing::{self, IndexerBackend, LanguageId, build_local_graph},
6
+ indexing::{self, IndexerBackend},
11
7
  integrity, listing,
12
8
  model::graph::Graph,
13
9
  resolution::Resolver,
@@ -16,7 +12,6 @@ use rubydex::{
16
12
  timer::{Timer, time_it},
17
13
  },
18
14
  };
19
- use url::Url;
20
15
 
21
16
  #[derive(Parser, Debug)]
22
17
  #[command(name = "rubydex_cli", about = "A Static Analysis Toolkit for Ruby", version)]
@@ -61,21 +56,6 @@ struct Args {
61
56
  help = "Write orphan definitions report to specified file"
62
57
  )]
63
58
  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,
79
59
  }
80
60
 
81
61
  #[derive(Debug, Clone, ValueEnum)]
@@ -134,7 +114,7 @@ fn main() {
134
114
  // Listing
135
115
 
136
116
  let (file_paths, errors) = time_it!(listing, {
137
- listing::collect_file_paths(args.paths, &graph.excluded_paths())
117
+ listing::collect_file_paths(args.paths, &graph.excluded_patterns())
138
118
  });
139
119
 
140
120
  for error in errors {
@@ -149,14 +129,6 @@ fn main() {
149
129
 
150
130
  let backend = IndexerBackend::from(&args.indexer);
151
131
 
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
-
160
132
  let errors = time_it!(indexing, { indexing::index_files(&mut graph, file_paths, backend) });
161
133
 
162
134
  for error in errors {
@@ -174,12 +146,6 @@ fn main() {
174
146
  resolver.resolve();
175
147
  });
176
148
 
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
-
183
149
  if let Some(StopAfter::Resolution) = args.stop_after {
184
150
  return exit(args.stats);
185
151
  }
@@ -241,91 +207,3 @@ fn main() {
241
207
  // Forget the graph so we don't have to wait for deallocation and let the system reclaim the memory at exit
242
208
  mem::forget(graph);
243
209
  }
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
- }
@@ -192,10 +192,6 @@ macro_rules! namespace_declaration {
192
192
  &self.ancestors
193
193
  }
194
194
 
195
- pub fn ancestors_mut(&mut self) -> &mut Ancestors {
196
- &mut self.ancestors
197
- }
198
-
199
195
  #[must_use]
200
196
  pub fn clone_ancestors(&self) -> Ancestors {
201
197
  self.ancestors.clone()
@@ -552,13 +548,6 @@ impl Namespace {
552
548
  all_namespaces!(self, it => it.remove_descendant(descendant_id));
553
549
  }
554
550
 
555
- pub fn for_each_ancestor<F>(&self, mut f: F)
556
- where
557
- F: FnMut(&Ancestor),
558
- {
559
- all_namespaces!(self, it => it.ancestors().iter().for_each(&mut f));
560
- }
561
-
562
551
  pub fn for_each_descendant<F>(&self, mut f: F)
563
552
  where
564
553
  F: FnMut(&DeclarationId),