rubydex 0.1.0.beta12-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.
- checksums.yaml +7 -0
- data/LICENSE.txt +23 -0
- data/README.md +125 -0
- data/THIRD_PARTY_LICENSES.html +4562 -0
- data/exe/rdx +47 -0
- data/ext/rubydex/declaration.c +453 -0
- data/ext/rubydex/declaration.h +23 -0
- data/ext/rubydex/definition.c +284 -0
- data/ext/rubydex/definition.h +28 -0
- data/ext/rubydex/diagnostic.c +6 -0
- data/ext/rubydex/diagnostic.h +11 -0
- data/ext/rubydex/document.c +97 -0
- data/ext/rubydex/document.h +10 -0
- data/ext/rubydex/extconf.rb +138 -0
- data/ext/rubydex/graph.c +681 -0
- data/ext/rubydex/graph.h +10 -0
- data/ext/rubydex/handle.h +44 -0
- data/ext/rubydex/location.c +22 -0
- data/ext/rubydex/location.h +15 -0
- data/ext/rubydex/reference.c +123 -0
- data/ext/rubydex/reference.h +15 -0
- data/ext/rubydex/rubydex.c +22 -0
- data/ext/rubydex/utils.c +108 -0
- data/ext/rubydex/utils.h +34 -0
- data/lib/rubydex/3.2/rubydex.so +0 -0
- data/lib/rubydex/3.3/rubydex.so +0 -0
- data/lib/rubydex/3.4/rubydex.so +0 -0
- data/lib/rubydex/4.0/rubydex.so +0 -0
- data/lib/rubydex/comment.rb +17 -0
- data/lib/rubydex/diagnostic.rb +21 -0
- data/lib/rubydex/failures.rb +15 -0
- data/lib/rubydex/graph.rb +98 -0
- data/lib/rubydex/keyword.rb +17 -0
- data/lib/rubydex/keyword_parameter.rb +13 -0
- data/lib/rubydex/librubydex_sys.so +0 -0
- data/lib/rubydex/location.rb +90 -0
- data/lib/rubydex/mixin.rb +22 -0
- data/lib/rubydex/version.rb +5 -0
- data/lib/rubydex.rb +23 -0
- data/rbi/rubydex.rbi +422 -0
- data/rust/Cargo.lock +1851 -0
- data/rust/Cargo.toml +29 -0
- data/rust/about.hbs +78 -0
- data/rust/about.toml +10 -0
- data/rust/rubydex/Cargo.toml +42 -0
- data/rust/rubydex/src/compile_assertions.rs +13 -0
- data/rust/rubydex/src/diagnostic.rs +110 -0
- data/rust/rubydex/src/errors.rs +28 -0
- data/rust/rubydex/src/indexing/local_graph.rs +224 -0
- data/rust/rubydex/src/indexing/rbs_indexer.rs +1551 -0
- data/rust/rubydex/src/indexing/ruby_indexer.rs +2329 -0
- data/rust/rubydex/src/indexing/ruby_indexer_tests.rs +4962 -0
- data/rust/rubydex/src/indexing.rs +210 -0
- data/rust/rubydex/src/integrity.rs +279 -0
- data/rust/rubydex/src/job_queue.rs +205 -0
- data/rust/rubydex/src/lib.rs +17 -0
- data/rust/rubydex/src/listing.rs +371 -0
- data/rust/rubydex/src/main.rs +160 -0
- data/rust/rubydex/src/model/built_in.rs +83 -0
- data/rust/rubydex/src/model/comment.rs +24 -0
- data/rust/rubydex/src/model/declaration.rs +671 -0
- data/rust/rubydex/src/model/definitions.rs +1682 -0
- data/rust/rubydex/src/model/document.rs +222 -0
- data/rust/rubydex/src/model/encoding.rs +22 -0
- data/rust/rubydex/src/model/graph.rs +3754 -0
- data/rust/rubydex/src/model/id.rs +110 -0
- data/rust/rubydex/src/model/identity_maps.rs +58 -0
- data/rust/rubydex/src/model/ids.rs +60 -0
- data/rust/rubydex/src/model/keywords.rs +256 -0
- data/rust/rubydex/src/model/name.rs +298 -0
- data/rust/rubydex/src/model/references.rs +111 -0
- data/rust/rubydex/src/model/string_ref.rs +50 -0
- data/rust/rubydex/src/model/visibility.rs +41 -0
- data/rust/rubydex/src/model.rs +15 -0
- data/rust/rubydex/src/offset.rs +147 -0
- data/rust/rubydex/src/position.rs +6 -0
- data/rust/rubydex/src/query.rs +1841 -0
- data/rust/rubydex/src/resolution.rs +6517 -0
- data/rust/rubydex/src/stats/memory.rs +71 -0
- data/rust/rubydex/src/stats/orphan_report.rs +264 -0
- data/rust/rubydex/src/stats/timer.rs +127 -0
- data/rust/rubydex/src/stats.rs +11 -0
- data/rust/rubydex/src/test_utils/context.rs +226 -0
- data/rust/rubydex/src/test_utils/graph_test.rs +730 -0
- data/rust/rubydex/src/test_utils/local_graph_test.rs +602 -0
- data/rust/rubydex/src/test_utils.rs +52 -0
- data/rust/rubydex/src/visualization/dot.rs +192 -0
- data/rust/rubydex/src/visualization.rs +6 -0
- data/rust/rubydex/tests/cli.rs +185 -0
- data/rust/rubydex-mcp/Cargo.toml +28 -0
- data/rust/rubydex-mcp/src/main.rs +48 -0
- data/rust/rubydex-mcp/src/server.rs +1145 -0
- data/rust/rubydex-mcp/src/tools.rs +49 -0
- data/rust/rubydex-mcp/tests/mcp.rs +302 -0
- data/rust/rubydex-sys/Cargo.toml +20 -0
- data/rust/rubydex-sys/build.rs +14 -0
- data/rust/rubydex-sys/cbindgen.toml +12 -0
- data/rust/rubydex-sys/src/declaration_api.rs +485 -0
- data/rust/rubydex-sys/src/definition_api.rs +443 -0
- data/rust/rubydex-sys/src/diagnostic_api.rs +99 -0
- data/rust/rubydex-sys/src/document_api.rs +85 -0
- data/rust/rubydex-sys/src/graph_api.rs +948 -0
- data/rust/rubydex-sys/src/lib.rs +79 -0
- data/rust/rubydex-sys/src/location_api.rs +79 -0
- data/rust/rubydex-sys/src/name_api.rs +135 -0
- data/rust/rubydex-sys/src/reference_api.rs +267 -0
- data/rust/rubydex-sys/src/utils.rs +70 -0
- data/rust/rustfmt.toml +2 -0
- metadata +159 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
pub mod compile_assertions;
|
|
2
|
+
pub mod diagnostic;
|
|
3
|
+
pub mod errors;
|
|
4
|
+
pub mod indexing;
|
|
5
|
+
pub mod integrity;
|
|
6
|
+
pub mod job_queue;
|
|
7
|
+
pub mod listing;
|
|
8
|
+
pub mod model;
|
|
9
|
+
pub mod offset;
|
|
10
|
+
pub mod position;
|
|
11
|
+
pub mod query;
|
|
12
|
+
pub mod resolution;
|
|
13
|
+
pub mod stats;
|
|
14
|
+
pub mod visualization;
|
|
15
|
+
|
|
16
|
+
#[cfg(any(test, feature = "test_utils"))]
|
|
17
|
+
pub mod test_utils;
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
use crate::{
|
|
2
|
+
errors::Errors,
|
|
3
|
+
job_queue::{Job, JobQueue},
|
|
4
|
+
};
|
|
5
|
+
use crossbeam_channel::{Sender, unbounded};
|
|
6
|
+
use std::{
|
|
7
|
+
collections::HashSet,
|
|
8
|
+
fs,
|
|
9
|
+
hash::BuildHasher,
|
|
10
|
+
path::{Path, PathBuf},
|
|
11
|
+
sync::Arc,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
pub struct FileDiscoveryJob {
|
|
15
|
+
path: PathBuf,
|
|
16
|
+
queue: Arc<JobQueue>,
|
|
17
|
+
paths_tx: Sender<PathBuf>,
|
|
18
|
+
errors_tx: Sender<Errors>,
|
|
19
|
+
excluded_paths: Arc<HashSet<PathBuf>>,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
impl FileDiscoveryJob {
|
|
23
|
+
#[must_use]
|
|
24
|
+
pub fn new(
|
|
25
|
+
path: PathBuf,
|
|
26
|
+
queue: Arc<JobQueue>,
|
|
27
|
+
paths_tx: Sender<PathBuf>,
|
|
28
|
+
errors_tx: Sender<Errors>,
|
|
29
|
+
excluded_paths: Arc<HashSet<PathBuf>>,
|
|
30
|
+
) -> Self {
|
|
31
|
+
Self {
|
|
32
|
+
path,
|
|
33
|
+
queue,
|
|
34
|
+
paths_tx,
|
|
35
|
+
errors_tx,
|
|
36
|
+
excluded_paths,
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
impl FileDiscoveryJob {
|
|
42
|
+
fn handle_file(&self, path: &Path) {
|
|
43
|
+
if path.extension().is_some_and(|ext| ext == "rb" || ext == "rbs") {
|
|
44
|
+
self.paths_tx
|
|
45
|
+
.send(path.to_path_buf())
|
|
46
|
+
.expect("file receiver dropped before run completion");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
fn handle_symlink(&self, path: &PathBuf) {
|
|
51
|
+
let Ok(canonicalized) = fs::canonicalize(path) else {
|
|
52
|
+
self.send_error(Errors::FileError(format!(
|
|
53
|
+
"Failed to canonicalize symlink: `{}`",
|
|
54
|
+
path.display(),
|
|
55
|
+
)));
|
|
56
|
+
|
|
57
|
+
return;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
if self.excluded_paths.contains(&canonicalized) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
self.queue.push(Box::new(FileDiscoveryJob::new(
|
|
65
|
+
canonicalized,
|
|
66
|
+
Arc::clone(&self.queue),
|
|
67
|
+
self.paths_tx.clone(),
|
|
68
|
+
self.errors_tx.clone(),
|
|
69
|
+
Arc::clone(&self.excluded_paths),
|
|
70
|
+
)));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
fn send_error(&self, error: Errors) {
|
|
74
|
+
self.errors_tx
|
|
75
|
+
.send(error)
|
|
76
|
+
.expect("error receiver dropped before run completion");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
impl Job for FileDiscoveryJob {
|
|
81
|
+
fn run(&self) {
|
|
82
|
+
if self.path.is_dir() {
|
|
83
|
+
let Ok(read_dir) = self.path.read_dir() else {
|
|
84
|
+
self.send_error(Errors::FileError(format!(
|
|
85
|
+
"Failed to read directory `{}`",
|
|
86
|
+
self.path.display(),
|
|
87
|
+
)));
|
|
88
|
+
|
|
89
|
+
return;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
for result in read_dir {
|
|
93
|
+
let Ok(entry) = result else {
|
|
94
|
+
self.send_error(Errors::FileError(format!(
|
|
95
|
+
"Failed to read directory `{}`: {result:?}",
|
|
96
|
+
self.path.display(),
|
|
97
|
+
)));
|
|
98
|
+
|
|
99
|
+
continue;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
let kind = entry.file_type().unwrap();
|
|
103
|
+
|
|
104
|
+
if kind.is_dir() {
|
|
105
|
+
if self.excluded_paths.contains(&entry.path()) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
self.queue.push(Box::new(FileDiscoveryJob::new(
|
|
110
|
+
entry.path(),
|
|
111
|
+
Arc::clone(&self.queue),
|
|
112
|
+
self.paths_tx.clone(),
|
|
113
|
+
self.errors_tx.clone(),
|
|
114
|
+
Arc::clone(&self.excluded_paths),
|
|
115
|
+
)));
|
|
116
|
+
} else if kind.is_file() {
|
|
117
|
+
self.handle_file(&entry.path());
|
|
118
|
+
} else if kind.is_symlink() {
|
|
119
|
+
self.handle_symlink(&entry.path());
|
|
120
|
+
} else {
|
|
121
|
+
self.send_error(Errors::FileError(format!(
|
|
122
|
+
"Path `{}` is not a file or directory",
|
|
123
|
+
entry.path().display()
|
|
124
|
+
)));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
} else if self.path.is_file() {
|
|
128
|
+
self.handle_file(&self.path);
|
|
129
|
+
} else if self.path.is_symlink() {
|
|
130
|
+
self.handle_symlink(&self.path);
|
|
131
|
+
} else {
|
|
132
|
+
self.send_error(Errors::FileError(format!(
|
|
133
|
+
"Path `{}` is not a file or directory",
|
|
134
|
+
self.path.display()
|
|
135
|
+
)));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/// Recursively collects all Ruby files for the given workspace and dependencies, returning a vector of document instances
|
|
141
|
+
///
|
|
142
|
+
/// # Errors
|
|
143
|
+
///
|
|
144
|
+
/// Returns a `MultipleErrors` if any of the paths do not exist
|
|
145
|
+
///
|
|
146
|
+
/// # Panics
|
|
147
|
+
///
|
|
148
|
+
/// Panics if the errors receiver is dropped before the run completion
|
|
149
|
+
#[must_use]
|
|
150
|
+
pub fn collect_file_paths<S: BuildHasher>(
|
|
151
|
+
paths: Vec<String>,
|
|
152
|
+
excluded: &HashSet<PathBuf, S>,
|
|
153
|
+
) -> (Vec<PathBuf>, Vec<Errors>) {
|
|
154
|
+
let queue = Arc::new(JobQueue::new());
|
|
155
|
+
let (files_tx, files_rx) = unbounded();
|
|
156
|
+
let (errors_tx, errors_rx) = unbounded();
|
|
157
|
+
|
|
158
|
+
// Canonicalize the excluded paths since they may be symlinks
|
|
159
|
+
let excluded: Arc<HashSet<PathBuf>> = Arc::new(excluded.iter().filter_map(|p| fs::canonicalize(p).ok()).collect());
|
|
160
|
+
|
|
161
|
+
for path in paths {
|
|
162
|
+
let Ok(canonicalized) = fs::canonicalize(&path) else {
|
|
163
|
+
errors_tx
|
|
164
|
+
.send(Errors::FileError(format!("Path `{path}` does not exist")))
|
|
165
|
+
.expect("errors receiver dropped before run completion");
|
|
166
|
+
|
|
167
|
+
continue;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
if excluded.contains(&canonicalized) {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
queue.push(Box::new(FileDiscoveryJob::new(
|
|
175
|
+
canonicalized,
|
|
176
|
+
Arc::clone(&queue),
|
|
177
|
+
files_tx.clone(),
|
|
178
|
+
errors_tx.clone(),
|
|
179
|
+
Arc::clone(&excluded),
|
|
180
|
+
)));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
JobQueue::run(&queue);
|
|
184
|
+
|
|
185
|
+
drop(files_tx);
|
|
186
|
+
drop(errors_tx);
|
|
187
|
+
|
|
188
|
+
(files_rx.iter().collect(), errors_rx.iter().collect())
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
#[cfg(test)]
|
|
192
|
+
mod tests {
|
|
193
|
+
use super::*;
|
|
194
|
+
use crate::test_utils::Context;
|
|
195
|
+
|
|
196
|
+
fn collect_document_paths(context: &Context, paths: &[&str]) -> (Vec<String>, Vec<Errors>) {
|
|
197
|
+
collect_document_paths_with_exclusions(context, paths, &HashSet::new())
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
fn collect_document_paths_with_exclusions(
|
|
201
|
+
context: &Context,
|
|
202
|
+
paths: &[&str],
|
|
203
|
+
excluded: &HashSet<PathBuf>,
|
|
204
|
+
) -> (Vec<String>, Vec<Errors>) {
|
|
205
|
+
let (files, errors) = collect_file_paths(
|
|
206
|
+
paths
|
|
207
|
+
.iter()
|
|
208
|
+
.map(|p| context.absolute_path_to(p).to_string_lossy().into_owned())
|
|
209
|
+
.collect(),
|
|
210
|
+
excluded,
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
let mut files: Vec<String> = files
|
|
214
|
+
.iter()
|
|
215
|
+
.map(|path| context.relative_path_to(path).to_string_lossy().into_owned())
|
|
216
|
+
.collect();
|
|
217
|
+
|
|
218
|
+
files.sort();
|
|
219
|
+
|
|
220
|
+
(files, errors)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
#[test]
|
|
224
|
+
fn collect_all_documents() {
|
|
225
|
+
let context = Context::new();
|
|
226
|
+
let baz = PathBuf::from("bar").join("baz.rb");
|
|
227
|
+
let qux = PathBuf::from("bar").join("qux.rb");
|
|
228
|
+
let bar = PathBuf::from("foo").join("bar.rb");
|
|
229
|
+
context.touch(&baz);
|
|
230
|
+
context.touch(&qux);
|
|
231
|
+
context.touch(&bar);
|
|
232
|
+
|
|
233
|
+
let (files, errors) = collect_document_paths(&context, &["foo", "bar"]);
|
|
234
|
+
|
|
235
|
+
assert!(errors.is_empty());
|
|
236
|
+
|
|
237
|
+
assert_eq!(
|
|
238
|
+
files,
|
|
239
|
+
[
|
|
240
|
+
baz.to_str().unwrap().to_string(),
|
|
241
|
+
qux.to_str().unwrap().to_string(),
|
|
242
|
+
bar.to_str().unwrap().to_string()
|
|
243
|
+
]
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
#[test]
|
|
248
|
+
fn collect_some_documents_based_on_paths() {
|
|
249
|
+
let context = Context::new();
|
|
250
|
+
let baz = PathBuf::from("bar").join("baz.rb");
|
|
251
|
+
let qux = PathBuf::from("bar").join("qux.rb");
|
|
252
|
+
let bar = PathBuf::from("foo").join("bar.rb");
|
|
253
|
+
|
|
254
|
+
context.touch(&baz);
|
|
255
|
+
context.touch(&qux);
|
|
256
|
+
context.touch(&bar);
|
|
257
|
+
|
|
258
|
+
let (files, errors) = collect_document_paths(&context, &["bar"]);
|
|
259
|
+
|
|
260
|
+
assert!(errors.is_empty());
|
|
261
|
+
|
|
262
|
+
assert_eq!(
|
|
263
|
+
files,
|
|
264
|
+
[baz.to_str().unwrap().to_string(), qux.to_str().unwrap().to_string()]
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
#[test]
|
|
269
|
+
fn collect_rbs_files() {
|
|
270
|
+
let context = Context::new();
|
|
271
|
+
let ruby_file = PathBuf::from("lib").join("foo.rb");
|
|
272
|
+
let rbs_file = PathBuf::from("sig").join("foo.rbs");
|
|
273
|
+
let txt_file = PathBuf::from("lib").join("notes.txt");
|
|
274
|
+
context.touch(&ruby_file);
|
|
275
|
+
context.touch(&rbs_file);
|
|
276
|
+
context.touch(&txt_file);
|
|
277
|
+
|
|
278
|
+
let (files, errors) = collect_document_paths(&context, &["lib", "sig"]);
|
|
279
|
+
|
|
280
|
+
assert!(errors.is_empty());
|
|
281
|
+
|
|
282
|
+
assert_eq!(
|
|
283
|
+
[
|
|
284
|
+
ruby_file.to_str().unwrap().to_string(),
|
|
285
|
+
rbs_file.to_str().unwrap().to_string(),
|
|
286
|
+
],
|
|
287
|
+
files.as_slice()
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
#[test]
|
|
292
|
+
fn collect_non_existing_paths() {
|
|
293
|
+
let context = Context::new();
|
|
294
|
+
|
|
295
|
+
let (files, errors) = collect_file_paths(
|
|
296
|
+
vec![
|
|
297
|
+
context
|
|
298
|
+
.absolute_path_to("non_existing_path")
|
|
299
|
+
.to_string_lossy()
|
|
300
|
+
.into_owned(),
|
|
301
|
+
],
|
|
302
|
+
&HashSet::new(),
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
assert!(files.is_empty());
|
|
306
|
+
|
|
307
|
+
assert_eq!(
|
|
308
|
+
errors,
|
|
309
|
+
[Errors::FileError(format!(
|
|
310
|
+
"Path `{}` does not exist",
|
|
311
|
+
context.absolute_path_to("non_existing_path").display()
|
|
312
|
+
))]
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
#[test]
|
|
317
|
+
fn collect_files_excludes_directories() {
|
|
318
|
+
let context = Context::new();
|
|
319
|
+
let included = PathBuf::from("included").join("foo.rb");
|
|
320
|
+
let excluded_file = PathBuf::from("excluded").join("bar.rb");
|
|
321
|
+
context.touch(&included);
|
|
322
|
+
context.touch(&excluded_file);
|
|
323
|
+
|
|
324
|
+
let mut excluded = HashSet::new();
|
|
325
|
+
excluded.insert(context.absolute_path_to("excluded"));
|
|
326
|
+
|
|
327
|
+
let (files, errors) = collect_document_paths_with_exclusions(&context, &["included", "excluded"], &excluded);
|
|
328
|
+
|
|
329
|
+
assert!(errors.is_empty());
|
|
330
|
+
assert_eq!(files, [included.to_str().unwrap().to_string()]);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
#[test]
|
|
334
|
+
fn collect_files_excludes_nested_directories() {
|
|
335
|
+
let context = Context::new();
|
|
336
|
+
let kept = PathBuf::from("root").join("kept.rb");
|
|
337
|
+
let nested = PathBuf::from("root").join("skip").join("nested.rb");
|
|
338
|
+
context.touch(&kept);
|
|
339
|
+
context.touch(&nested);
|
|
340
|
+
|
|
341
|
+
let mut excluded = HashSet::new();
|
|
342
|
+
excluded.insert(context.absolute_path_to("root/skip"));
|
|
343
|
+
|
|
344
|
+
let (files, errors) = collect_document_paths_with_exclusions(&context, &["root"], &excluded);
|
|
345
|
+
|
|
346
|
+
assert!(errors.is_empty());
|
|
347
|
+
assert_eq!(files, [kept.to_str().unwrap().to_string()]);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
#[cfg(unix)]
|
|
351
|
+
#[test]
|
|
352
|
+
fn collect_files_excludes_symlinked_directories() {
|
|
353
|
+
let context = Context::new();
|
|
354
|
+
let included = PathBuf::from("included").join("foo.rb");
|
|
355
|
+
let excluded_file = PathBuf::from("real_dir").join("bar.rb");
|
|
356
|
+
context.touch(&included);
|
|
357
|
+
context.touch(&excluded_file);
|
|
358
|
+
|
|
359
|
+
// Create a symlink: link -> real_dir
|
|
360
|
+
std::os::unix::fs::symlink(context.absolute_path_to("real_dir"), context.absolute_path_to("link")).unwrap();
|
|
361
|
+
|
|
362
|
+
// Excluding the real directory while requesting to index the symlink should properly exclude the link
|
|
363
|
+
let mut excluded = HashSet::new();
|
|
364
|
+
excluded.insert(context.absolute_path_to("real_dir"));
|
|
365
|
+
|
|
366
|
+
let (files, errors) = collect_document_paths_with_exclusions(&context, &["included", "link"], &excluded);
|
|
367
|
+
|
|
368
|
+
assert!(errors.is_empty());
|
|
369
|
+
assert_eq!(files, [included.to_str().unwrap().to_string()]);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
use clap::{Parser, ValueEnum};
|
|
2
|
+
use std::{collections::HashSet, mem};
|
|
3
|
+
|
|
4
|
+
use rubydex::{
|
|
5
|
+
indexing, integrity, listing,
|
|
6
|
+
model::graph::Graph,
|
|
7
|
+
resolution::Resolver,
|
|
8
|
+
stats::{
|
|
9
|
+
memory::MemoryStats,
|
|
10
|
+
timer::{Timer, time_it},
|
|
11
|
+
},
|
|
12
|
+
visualization::dot,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
#[derive(Parser, Debug)]
|
|
16
|
+
#[command(name = "rubydex_cli", about = "A Static Analysis Toolkit for Ruby", version)]
|
|
17
|
+
#[allow(clippy::struct_excessive_bools)]
|
|
18
|
+
struct Args {
|
|
19
|
+
#[arg(value_name = "PATHS", default_value = ".")]
|
|
20
|
+
paths: Vec<String>,
|
|
21
|
+
|
|
22
|
+
#[arg(long = "stop-after", help = "Stop after the given stage")]
|
|
23
|
+
stop_after: Option<StopAfter>,
|
|
24
|
+
|
|
25
|
+
#[arg(long = "visualize")]
|
|
26
|
+
visualize: bool,
|
|
27
|
+
|
|
28
|
+
#[arg(long = "stats", help = "Show detailed performance statistics")]
|
|
29
|
+
stats: bool,
|
|
30
|
+
|
|
31
|
+
#[arg(long = "check-integrity", help = "Check the integrity of the graph after resolution")]
|
|
32
|
+
check_integrity: bool,
|
|
33
|
+
|
|
34
|
+
#[arg(
|
|
35
|
+
long = "report-orphans",
|
|
36
|
+
value_name = "PATH",
|
|
37
|
+
num_args = 0..=1,
|
|
38
|
+
require_equals = true,
|
|
39
|
+
default_missing_value = "/tmp/rubydex-orphan-report.txt",
|
|
40
|
+
help = "Write orphan definitions report to specified file"
|
|
41
|
+
)]
|
|
42
|
+
report_orphans: Option<String>,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
#[derive(Debug, Clone, ValueEnum)]
|
|
46
|
+
enum StopAfter {
|
|
47
|
+
Listing,
|
|
48
|
+
Indexing,
|
|
49
|
+
Resolution,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
fn exit(print_stats: bool) {
|
|
53
|
+
if print_stats {
|
|
54
|
+
Timer::print_breakdown();
|
|
55
|
+
MemoryStats::print_memory_usage();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
std::process::exit(0);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
fn main() {
|
|
62
|
+
let args = Args::parse();
|
|
63
|
+
|
|
64
|
+
if args.stats {
|
|
65
|
+
Timer::set_global_timer(Timer::new());
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Listing
|
|
69
|
+
|
|
70
|
+
let (file_paths, errors) = time_it!(listing, { listing::collect_file_paths(args.paths, &HashSet::new()) });
|
|
71
|
+
|
|
72
|
+
for error in errors {
|
|
73
|
+
eprintln!("{error}");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if let Some(StopAfter::Listing) = args.stop_after {
|
|
77
|
+
return exit(args.stats);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Indexing
|
|
81
|
+
|
|
82
|
+
let mut graph = Graph::new();
|
|
83
|
+
let errors = time_it!(indexing, { indexing::index_files(&mut graph, file_paths) });
|
|
84
|
+
|
|
85
|
+
for error in errors {
|
|
86
|
+
eprintln!("{error}");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if let Some(StopAfter::Indexing) = args.stop_after {
|
|
90
|
+
return exit(args.stats);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Resolution
|
|
94
|
+
|
|
95
|
+
time_it!(resolution, {
|
|
96
|
+
let mut resolver = Resolver::new(&mut graph);
|
|
97
|
+
resolver.resolve();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
if let Some(StopAfter::Resolution) = args.stop_after {
|
|
101
|
+
return exit(args.stats);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Integrity check
|
|
105
|
+
if args.check_integrity {
|
|
106
|
+
let errors = time_it!(integrity_check, { integrity::check_integrity(&graph) });
|
|
107
|
+
|
|
108
|
+
if errors.is_empty() {
|
|
109
|
+
println!("Integrity check passed: no issues found");
|
|
110
|
+
} else {
|
|
111
|
+
eprintln!("Integrity check found {} issue(s):", errors.len());
|
|
112
|
+
|
|
113
|
+
for error in &errors {
|
|
114
|
+
eprintln!(" - {error}");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
std::process::exit(1);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Querying
|
|
122
|
+
|
|
123
|
+
if args.stats {
|
|
124
|
+
time_it!(querying, {
|
|
125
|
+
graph.print_query_statistics();
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if args.stats {
|
|
130
|
+
Timer::print_breakdown();
|
|
131
|
+
MemoryStats::print_memory_usage();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Orphan report
|
|
135
|
+
if let Some(ref path) = args.report_orphans {
|
|
136
|
+
match std::fs::File::create(path) {
|
|
137
|
+
Ok(mut file) => {
|
|
138
|
+
if let Err(e) = graph.write_orphan_report(&mut file) {
|
|
139
|
+
eprintln!("Failed to write orphan report: {e}");
|
|
140
|
+
} else {
|
|
141
|
+
println!("Orphan report written to {path}");
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
Err(e) => eprintln!("Failed to create orphan report file: {e}"),
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Generate visualization or print statistics
|
|
149
|
+
if args.visualize {
|
|
150
|
+
println!("{}", dot::generate(&graph));
|
|
151
|
+
} else {
|
|
152
|
+
println!("Indexed {} files", graph.documents().len());
|
|
153
|
+
println!("Found {} names", graph.declarations().len());
|
|
154
|
+
println!("Found {} definitions", graph.definitions().len());
|
|
155
|
+
println!("Found {} URIs", graph.documents().len());
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Forget the graph so we don't have to wait for deallocation and let the system reclaim the memory at exit
|
|
159
|
+
mem::forget(graph);
|
|
160
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
use std::sync::LazyLock;
|
|
2
|
+
|
|
3
|
+
use url::Url;
|
|
4
|
+
|
|
5
|
+
use crate::{
|
|
6
|
+
indexing::{self, LanguageId},
|
|
7
|
+
model::{
|
|
8
|
+
declaration::{ClassDeclaration, Declaration, Namespace},
|
|
9
|
+
graph::Graph,
|
|
10
|
+
ids::DeclarationId,
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
pub static KERNEL_ID: LazyLock<DeclarationId> = LazyLock::new(|| DeclarationId::from("Kernel"));
|
|
15
|
+
pub static BASIC_OBJECT_ID: LazyLock<DeclarationId> = LazyLock::new(|| DeclarationId::from("BasicObject"));
|
|
16
|
+
pub static OBJECT_ID: LazyLock<DeclarationId> = LazyLock::new(|| DeclarationId::from("Object"));
|
|
17
|
+
pub static MODULE_ID: LazyLock<DeclarationId> = LazyLock::new(|| DeclarationId::from("Module"));
|
|
18
|
+
pub static CLASS_ID: LazyLock<DeclarationId> = LazyLock::new(|| DeclarationId::from("Class"));
|
|
19
|
+
|
|
20
|
+
/// Adds core classes and modules data to the graph so that resolution can provide correct results even when not
|
|
21
|
+
/// indexing the complete RBS core definitions
|
|
22
|
+
///
|
|
23
|
+
/// # Panics
|
|
24
|
+
///
|
|
25
|
+
/// Will panic if the built-in URI is invalid
|
|
26
|
+
pub fn add_built_in_data(graph: &mut Graph) {
|
|
27
|
+
// We need definitions to ensure that ancestor linearization happens naturally through the algorithm. Trying to set
|
|
28
|
+
// ancestors directly on declarations doesn't work because the algorithm erases the ancestors and there are no
|
|
29
|
+
// definitions to inform it of the superclasses and mixins.
|
|
30
|
+
let uri = Url::parse("rubydex:built-in").unwrap();
|
|
31
|
+
let source = r"
|
|
32
|
+
class BasicObject
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
module Kernel
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
class Object < BasicObject
|
|
39
|
+
include Kernel
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
class Module < Object
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
class Class < Module
|
|
46
|
+
end
|
|
47
|
+
";
|
|
48
|
+
indexing::index_source(graph, uri.as_ref(), source, &LanguageId::Rbs);
|
|
49
|
+
|
|
50
|
+
// Creating declarations eagerly is still necessary because we need to associate correct ownership data no matter in
|
|
51
|
+
// what order we discover classes and modules
|
|
52
|
+
let declarations = graph.declarations_mut();
|
|
53
|
+
|
|
54
|
+
// Built-in declarations that always exist in the Ruby object model
|
|
55
|
+
declarations.insert(
|
|
56
|
+
*BASIC_OBJECT_ID,
|
|
57
|
+
Declaration::Namespace(Namespace::Class(Box::new(ClassDeclaration::new(
|
|
58
|
+
"BasicObject".to_string(),
|
|
59
|
+
*OBJECT_ID,
|
|
60
|
+
)))),
|
|
61
|
+
);
|
|
62
|
+
declarations.insert(
|
|
63
|
+
*OBJECT_ID,
|
|
64
|
+
Declaration::Namespace(Namespace::Class(Box::new(ClassDeclaration::new(
|
|
65
|
+
"Object".to_string(),
|
|
66
|
+
*OBJECT_ID,
|
|
67
|
+
)))),
|
|
68
|
+
);
|
|
69
|
+
declarations.insert(
|
|
70
|
+
*MODULE_ID,
|
|
71
|
+
Declaration::Namespace(Namespace::Class(Box::new(ClassDeclaration::new(
|
|
72
|
+
"Module".to_string(),
|
|
73
|
+
*OBJECT_ID,
|
|
74
|
+
)))),
|
|
75
|
+
);
|
|
76
|
+
declarations.insert(
|
|
77
|
+
*CLASS_ID,
|
|
78
|
+
Declaration::Namespace(Namespace::Class(Box::new(ClassDeclaration::new(
|
|
79
|
+
"Class".to_string(),
|
|
80
|
+
*OBJECT_ID,
|
|
81
|
+
)))),
|
|
82
|
+
);
|
|
83
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
use crate::offset::Offset;
|
|
2
|
+
|
|
3
|
+
#[derive(Debug, Clone)]
|
|
4
|
+
pub struct Comment {
|
|
5
|
+
offset: Offset,
|
|
6
|
+
string: String,
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
impl Comment {
|
|
10
|
+
#[must_use]
|
|
11
|
+
pub fn new(offset: Offset, string: String) -> Self {
|
|
12
|
+
Self { offset, string }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
#[must_use]
|
|
16
|
+
pub fn offset(&self) -> &Offset {
|
|
17
|
+
&self.offset
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
#[must_use]
|
|
21
|
+
pub fn string(&self) -> &String {
|
|
22
|
+
&self.string
|
|
23
|
+
}
|
|
24
|
+
}
|