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.
- checksums.yaml +4 -4
- data/README.md +69 -3
- data/THIRD_PARTY_LICENSES.html +168 -1381
- data/exe/rdx +136 -34
- data/ext/rubydex/declaration.c +1 -1
- data/ext/rubydex/definition.c +32 -4
- data/ext/rubydex/extconf.rb +0 -8
- data/ext/rubydex/graph.c +29 -18
- data/ext/rubydex/query.c +105 -0
- data/ext/rubydex/query.h +8 -0
- data/ext/rubydex/reference.c +60 -0
- data/ext/rubydex/rubydex.c +2 -0
- data/ext/rubydex/utils.c +12 -0
- data/ext/rubydex/utils.h +5 -0
- data/lib/rubydex/mcp_server/protocol.rb +156 -0
- data/lib/rubydex/mcp_server/tools/base_tool.rb +109 -0
- data/lib/rubydex/mcp_server/tools/codebase_stats_tool.rb +30 -0
- data/lib/rubydex/mcp_server/tools/find_constant_references_tool.rb +46 -0
- data/lib/rubydex/mcp_server/tools/get_declaration_tool.rb +68 -0
- data/lib/rubydex/mcp_server/tools/get_descendants_tool.rb +46 -0
- data/lib/rubydex/mcp_server/tools/get_file_declarations_tool.rb +55 -0
- data/lib/rubydex/mcp_server/tools/search_declarations_tool.rb +55 -0
- data/lib/rubydex/mcp_server.rb +219 -0
- data/lib/rubydex/version.rb +1 -1
- data/rbi/rubydex.rbi +26 -4
- data/rust/Cargo.lock +5 -552
- data/rust/Cargo.toml +0 -1
- data/rust/rubydex/Cargo.toml +1 -0
- data/rust/rubydex/benches/graph_memory.rs +22 -4
- data/rust/rubydex/src/compile_assertions.rs +15 -0
- data/rust/rubydex/src/config.rs +54 -34
- data/rust/rubydex/src/diagnostic.rs +1 -1
- data/rust/rubydex/src/indexing/rbs_indexer.rs +14 -2
- data/rust/rubydex/src/indexing/ruby_indexer.rs +49 -58
- data/rust/rubydex/src/indexing/ruby_indexer_tests.rs +72 -16
- data/rust/rubydex/src/listing.rs +159 -102
- data/rust/rubydex/src/main.rs +3 -125
- data/rust/rubydex/src/model/declaration.rs +0 -11
- data/rust/rubydex/src/model/definitions.rs +27 -26
- data/rust/rubydex/src/model/document.rs +43 -7
- data/rust/rubydex/src/model/graph.rs +47 -35
- data/rust/rubydex/src/model/id.rs +55 -0
- data/rust/rubydex/src/model/ids.rs +21 -9
- data/rust/rubydex/src/model/name.rs +35 -7
- data/rust/rubydex/src/model/references.rs +16 -13
- data/rust/rubydex/src/operation/applier.rs +0 -2
- data/rust/rubydex/src/operation/ruby_builder.rs +48 -59
- data/rust/rubydex/src/query/cypher/schema.rs +790 -0
- data/rust/rubydex/src/query/cypher/schema_info.rs +161 -0
- data/rust/rubydex/src/query/cypher/tests.rs +228 -0
- data/rust/rubydex/src/query/cypher.rs +57 -0
- data/rust/rubydex/src/query.rs +2 -0
- data/rust/rubydex/src/resolution.rs +248 -227
- data/rust/rubydex/src/resolution_tests.rs +263 -65
- data/rust/rubydex-sys/src/declaration_api.rs +6 -3
- data/rust/rubydex-sys/src/definition_api.rs +27 -7
- data/rust/rubydex-sys/src/graph_api.rs +175 -27
- data/rust/rubydex-sys/src/reference_api.rs +58 -12
- metadata +17 -10
- data/exe/rubydex_mcp +0 -17
- data/lib/rubydex/bin/rubydex_mcp.exe +0 -0
- data/rust/rubydex-mcp/Cargo.toml +0 -34
- data/rust/rubydex-mcp/src/main.rs +0 -48
- data/rust/rubydex-mcp/src/server.rs +0 -1148
- data/rust/rubydex-mcp/src/tools.rs +0 -49
- data/rust/rubydex-mcp/tests/mcp.rs +0 -302
data/rust/rubydex/src/listing.rs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
88
|
-
|
|
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
|
-
|
|
77
|
+
continue;
|
|
95
78
|
};
|
|
96
79
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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<
|
|
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
|
-
|
|
164
|
-
|
|
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(
|
|
132
|
+
let Ok(path) = std::path::absolute(&path) else {
|
|
168
133
|
errors_tx
|
|
169
|
-
.send(Errors::FileError(format!("
|
|
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
|
|
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
|
-
|
|
153
|
+
path,
|
|
181
154
|
Arc::clone(&queue),
|
|
182
155
|
files_tx.clone(),
|
|
183
156
|
errors_tx.clone(),
|
|
184
|
-
Arc::clone(&
|
|
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<
|
|
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(
|
|
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
|
|
361
|
+
fn collect_files_indexes_symlinked_files_at_their_own_path() {
|
|
364
362
|
let context = Context::new();
|
|
365
|
-
let
|
|
366
|
-
|
|
367
|
-
context.
|
|
368
|
-
|
|
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 ->
|
|
371
|
-
std::os::unix::fs::symlink(
|
|
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(
|
|
432
|
+
excluded.insert("**/version.rb".into());
|
|
376
433
|
|
|
377
|
-
let (files, errors) = collect_document_paths_with_exclusions(&context, &["
|
|
434
|
+
let (files, errors) = collect_document_paths_with_exclusions(&context, &["lib"], &excluded);
|
|
378
435
|
|
|
379
436
|
assert!(errors.is_empty());
|
|
380
|
-
assert_eq!(files, [
|
|
437
|
+
assert_eq!(files, [kept.to_str().unwrap().to_string()]);
|
|
381
438
|
}
|
|
382
439
|
}
|
data/rust/rubydex/src/main.rs
CHANGED
|
@@ -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
|
|
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.
|
|
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),
|