rubydex 0.1.0.beta1-x86_64-linux → 0.1.0.beta2-x86_64-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 (83) hide show
  1. checksums.yaml +4 -4
  2. data/ext/rubydex/declaration.c +146 -0
  3. data/ext/rubydex/declaration.h +10 -0
  4. data/ext/rubydex/definition.c +234 -0
  5. data/ext/rubydex/definition.h +28 -0
  6. data/ext/rubydex/diagnostic.c +6 -0
  7. data/ext/rubydex/diagnostic.h +11 -0
  8. data/ext/rubydex/document.c +98 -0
  9. data/ext/rubydex/document.h +10 -0
  10. data/ext/rubydex/extconf.rb +36 -15
  11. data/ext/rubydex/graph.c +405 -0
  12. data/ext/rubydex/graph.h +10 -0
  13. data/ext/rubydex/handle.h +44 -0
  14. data/ext/rubydex/location.c +22 -0
  15. data/ext/rubydex/location.h +15 -0
  16. data/ext/rubydex/reference.c +104 -0
  17. data/ext/rubydex/reference.h +16 -0
  18. data/ext/rubydex/rubydex.c +22 -0
  19. data/ext/rubydex/utils.c +27 -0
  20. data/ext/rubydex/utils.h +13 -0
  21. data/lib/rubydex/3.2/rubydex.so +0 -0
  22. data/lib/rubydex/3.3/rubydex.so +0 -0
  23. data/lib/rubydex/3.4/rubydex.so +0 -0
  24. data/lib/rubydex/4.0/rubydex.so +0 -0
  25. data/lib/rubydex/librubydex_sys.so +0 -0
  26. data/lib/rubydex/version.rb +1 -1
  27. data/rust/Cargo.lock +1275 -0
  28. data/rust/Cargo.toml +23 -0
  29. data/rust/about.hbs +78 -0
  30. data/rust/about.toml +9 -0
  31. data/rust/rubydex/Cargo.toml +41 -0
  32. data/rust/rubydex/src/diagnostic.rs +108 -0
  33. data/rust/rubydex/src/errors.rs +28 -0
  34. data/rust/rubydex/src/indexing/local_graph.rs +172 -0
  35. data/rust/rubydex/src/indexing/ruby_indexer.rs +5397 -0
  36. data/rust/rubydex/src/indexing.rs +128 -0
  37. data/rust/rubydex/src/job_queue.rs +186 -0
  38. data/rust/rubydex/src/lib.rs +15 -0
  39. data/rust/rubydex/src/listing.rs +249 -0
  40. data/rust/rubydex/src/main.rs +116 -0
  41. data/rust/rubydex/src/model/comment.rs +24 -0
  42. data/rust/rubydex/src/model/declaration.rs +541 -0
  43. data/rust/rubydex/src/model/definitions.rs +1475 -0
  44. data/rust/rubydex/src/model/document.rs +111 -0
  45. data/rust/rubydex/src/model/encoding.rs +22 -0
  46. data/rust/rubydex/src/model/graph.rs +1387 -0
  47. data/rust/rubydex/src/model/id.rs +90 -0
  48. data/rust/rubydex/src/model/identity_maps.rs +54 -0
  49. data/rust/rubydex/src/model/ids.rs +32 -0
  50. data/rust/rubydex/src/model/name.rs +188 -0
  51. data/rust/rubydex/src/model/references.rs +129 -0
  52. data/rust/rubydex/src/model/string_ref.rs +44 -0
  53. data/rust/rubydex/src/model/visibility.rs +41 -0
  54. data/rust/rubydex/src/model.rs +13 -0
  55. data/rust/rubydex/src/offset.rs +70 -0
  56. data/rust/rubydex/src/position.rs +6 -0
  57. data/rust/rubydex/src/query.rs +103 -0
  58. data/rust/rubydex/src/resolution.rs +4421 -0
  59. data/rust/rubydex/src/stats/memory.rs +71 -0
  60. data/rust/rubydex/src/stats/timer.rs +126 -0
  61. data/rust/rubydex/src/stats.rs +9 -0
  62. data/rust/rubydex/src/test_utils/context.rs +226 -0
  63. data/rust/rubydex/src/test_utils/graph_test.rs +229 -0
  64. data/rust/rubydex/src/test_utils/local_graph_test.rs +166 -0
  65. data/rust/rubydex/src/test_utils.rs +52 -0
  66. data/rust/rubydex/src/visualization/dot.rs +176 -0
  67. data/rust/rubydex/src/visualization.rs +6 -0
  68. data/rust/rubydex/tests/cli.rs +167 -0
  69. data/rust/rubydex-sys/Cargo.toml +20 -0
  70. data/rust/rubydex-sys/build.rs +14 -0
  71. data/rust/rubydex-sys/cbindgen.toml +12 -0
  72. data/rust/rubydex-sys/src/declaration_api.rs +114 -0
  73. data/rust/rubydex-sys/src/definition_api.rs +350 -0
  74. data/rust/rubydex-sys/src/diagnostic_api.rs +99 -0
  75. data/rust/rubydex-sys/src/document_api.rs +54 -0
  76. data/rust/rubydex-sys/src/graph_api.rs +493 -0
  77. data/rust/rubydex-sys/src/lib.rs +9 -0
  78. data/rust/rubydex-sys/src/location_api.rs +79 -0
  79. data/rust/rubydex-sys/src/name_api.rs +81 -0
  80. data/rust/rubydex-sys/src/reference_api.rs +191 -0
  81. data/rust/rubydex-sys/src/utils.rs +50 -0
  82. data/rust/rustfmt.toml +2 -0
  83. metadata +77 -2
@@ -0,0 +1,128 @@
1
+ use crate::{
2
+ errors::Errors,
3
+ indexing::{local_graph::LocalGraph, ruby_indexer::RubyIndexer},
4
+ job_queue::{Job, JobQueue},
5
+ model::graph::Graph,
6
+ };
7
+ use crossbeam_channel::{Sender, unbounded};
8
+ use std::{fs, path::PathBuf, sync::Arc};
9
+ use url::Url;
10
+
11
+ pub mod local_graph;
12
+ pub mod ruby_indexer;
13
+
14
+ /// Job that indexes a single Ruby file
15
+ pub struct IndexingRubyFileJob {
16
+ path: PathBuf,
17
+ local_graph_tx: Sender<LocalGraph>,
18
+ errors_tx: Sender<Errors>,
19
+ }
20
+
21
+ impl IndexingRubyFileJob {
22
+ #[must_use]
23
+ pub fn new(path: PathBuf, local_graph_tx: Sender<LocalGraph>, errors_tx: Sender<Errors>) -> Self {
24
+ Self {
25
+ path,
26
+ local_graph_tx,
27
+ errors_tx,
28
+ }
29
+ }
30
+
31
+ fn send_error(&self, error: Errors) {
32
+ self.errors_tx
33
+ .send(error)
34
+ .expect("errors receiver dropped before run completion");
35
+ }
36
+ }
37
+
38
+ impl Job for IndexingRubyFileJob {
39
+ fn run(&self) {
40
+ let Ok(source) = fs::read_to_string(&self.path) else {
41
+ self.send_error(Errors::FileError(format!(
42
+ "Failed to read file `{}`",
43
+ self.path.display()
44
+ )));
45
+
46
+ return;
47
+ };
48
+
49
+ let Ok(url) = Url::from_file_path(&self.path) else {
50
+ self.send_error(Errors::FileError(format!(
51
+ "Couldn't build URI from path `{}`",
52
+ self.path.display()
53
+ )));
54
+
55
+ return;
56
+ };
57
+
58
+ let mut ruby_indexer = RubyIndexer::new(url.to_string(), &source);
59
+ ruby_indexer.index();
60
+
61
+ self.local_graph_tx
62
+ .send(ruby_indexer.local_graph())
63
+ .expect("graph receiver dropped before merge");
64
+ }
65
+ }
66
+
67
+ /// Indexes the given paths, reading the content from disk and populating the given `Graph` instance.
68
+ ///
69
+ /// # Panics
70
+ ///
71
+ /// Will panic if the graph cannot be wrapped in an Arc<Mutex<>>
72
+ pub fn index_files(graph: &mut Graph, paths: Vec<PathBuf>) -> Vec<Errors> {
73
+ let queue = Arc::new(JobQueue::new());
74
+ let (local_graphs_tx, local_graphs_rx) = unbounded();
75
+ let (errors_tx, errors_rx) = unbounded();
76
+
77
+ for path in paths {
78
+ queue.push(Box::new(IndexingRubyFileJob::new(
79
+ path,
80
+ local_graphs_tx.clone(),
81
+ errors_tx.clone(),
82
+ )));
83
+ }
84
+
85
+ drop(local_graphs_tx);
86
+ drop(errors_tx);
87
+
88
+ JobQueue::run(&queue);
89
+
90
+ while let Ok(local_graph) = local_graphs_rx.recv() {
91
+ graph.update(local_graph);
92
+ }
93
+
94
+ errors_rx.iter().collect()
95
+ }
96
+
97
+ #[cfg(test)]
98
+ mod tests {
99
+ use std::path::PathBuf;
100
+
101
+ use super::*;
102
+ use crate::test_utils::Context;
103
+ use std::path::Path;
104
+
105
+ #[test]
106
+ fn index_relative_paths() {
107
+ let relative_path = Path::new("foo").join("bar.rb");
108
+ let context = Context::new();
109
+ context.touch(&relative_path);
110
+
111
+ let working_directory = std::env::current_dir().unwrap();
112
+ let absolute_path = context.absolute_path_to("foo/bar.rb");
113
+
114
+ let mut dots = PathBuf::from("..");
115
+
116
+ for _ in 0..working_directory.components().count() - 1 {
117
+ dots = dots.join("..");
118
+ }
119
+
120
+ let relative_to_pwd = &dots.join(absolute_path);
121
+
122
+ let mut graph = Graph::new();
123
+ let errors = index_files(&mut graph, vec![relative_to_pwd.clone()]);
124
+
125
+ assert!(errors.is_empty());
126
+ assert_eq!(graph.documents().len(), 1);
127
+ }
128
+ }
@@ -0,0 +1,186 @@
1
+ use std::panic::{self, AssertUnwindSafe};
2
+ use std::{
3
+ sync::Arc,
4
+ sync::atomic::{AtomicUsize, Ordering},
5
+ thread,
6
+ };
7
+
8
+ use crossbeam_deque::{Injector, Steal, Stealer, Worker};
9
+ use crossbeam_utils::Backoff;
10
+
11
+ /// Work-stealing queue that balances jobs across worker threads.
12
+ ///
13
+ /// Jobs are pushed onto the global `injector`, then workers move them into their
14
+ /// own local queues. Work stealing lets idle workers drain the global injector
15
+ /// first and then steal from peers, keeping the CPU busy without coarse locks.
16
+ /// `in_flight` tracks outstanding jobs so threads can tell when all work
17
+ /// (including work spawned by other jobs) has finished.
18
+ #[derive(Default)]
19
+ pub struct JobQueue {
20
+ /// Global queue feeding newly discovered jobs to workers.
21
+ injector: Injector<Box<dyn Job + Send + 'static>>,
22
+ /// Count of jobs that have been queued but not yet completed.
23
+ in_flight: AtomicUsize,
24
+ }
25
+
26
+ impl JobQueue {
27
+ #[must_use]
28
+ pub fn new() -> Self {
29
+ Self {
30
+ injector: Injector::new(),
31
+ in_flight: AtomicUsize::new(0),
32
+ }
33
+ }
34
+
35
+ /// Enqueue a job for processing.
36
+ pub fn push(&self, job: Box<dyn Job + Send + 'static>) {
37
+ // Increment the count of in-flight jobs to indicate a new job has been enqueued.
38
+ self.in_flight.fetch_add(1, Ordering::Relaxed);
39
+ // Push the job onto the global injector queue for later execution by workers.
40
+ self.injector.push(job);
41
+ }
42
+
43
+ /// Run jobs until the queue is empty.
44
+ ///
45
+ /// Accepts the shared queue so jobs can enqueue more work while the runner is processing.
46
+ pub fn run(queue: &Arc<JobQueue>) {
47
+ // Determine the number of worker threads to launch.
48
+ // Use the number of available logical CPUs, falling back to 4 if detection fails.
49
+ let worker_count = thread::available_parallelism()
50
+ .map(std::num::NonZeroUsize::get)
51
+ .unwrap_or(4);
52
+
53
+ Self::run_with_workers(queue, worker_count);
54
+ }
55
+
56
+ /// Spin up `worker_count` threads, each with its own local queue, and block until all threads finish.
57
+ ///
58
+ /// Workers steal from each other and from the global injector to keep the work balanced.
59
+ fn run_with_workers(queue: &Arc<JobQueue>, worker_count: usize) {
60
+ let mut handles = Vec::with_capacity(worker_count);
61
+ let mut workers = Vec::with_capacity(worker_count);
62
+ let mut stealers = Vec::with_capacity(worker_count);
63
+
64
+ for _ in 0..worker_count {
65
+ let worker = Worker::new_fifo();
66
+ stealers.push(worker.stealer()); // For stealing jobs from this queue
67
+ workers.push(worker);
68
+ }
69
+
70
+ // Wrap all stealers in an Arc so they can be shared across all threads.
71
+ let stealers = Arc::new(stealers);
72
+
73
+ // Start a worker thread for each local queue.
74
+ for worker in workers {
75
+ let queue = Arc::clone(queue);
76
+ let stealers = Arc::clone(&stealers);
77
+ handles.push(thread::spawn(move || queue.worker_loop(&worker, &stealers)));
78
+ }
79
+
80
+ // Wait for all worker threads to finish before returning.
81
+ for handle in handles {
82
+ handle.join().expect("Worker thread panicked");
83
+ }
84
+ }
85
+
86
+ /// Drain work for a single worker.
87
+ ///
88
+ /// The worker prefers its local queue, then the global injector, then other
89
+ /// workers. If no job is immediately available, it backs off briefly and
90
+ /// exits once `in_flight` reaches zero (meaning no pending work anywhere).
91
+ fn worker_loop(
92
+ &self,
93
+ local: &Worker<Box<dyn Job + Send + 'static>>,
94
+ stealers: &Arc<Vec<Stealer<Box<dyn Job + Send + 'static>>>>,
95
+ ) {
96
+ // Create a backoff utility for yielding when no job is immediately available
97
+ let backoff = Backoff::new();
98
+
99
+ // Loop until all work is done (in_flight reaches zero)
100
+ loop {
101
+ // Try to steal the next job to execute. Prioritize own local queue, then global, then peers.
102
+ let Some(job) = Self::steal_job(local, stealers, &self.injector) else {
103
+ if self.in_flight.load(Ordering::Acquire) == 0 {
104
+ // All work is done, exit the worker loop
105
+ break;
106
+ }
107
+ // No work found and still pending jobs: brief pause before retrying
108
+ backoff.snooze();
109
+ continue;
110
+ };
111
+
112
+ // Reset backoff as we've obtained a job to process
113
+ backoff.reset();
114
+
115
+ // Run the job and capture any panics so we can propagate them safely
116
+ let result = panic::catch_unwind(AssertUnwindSafe(|| job.run()));
117
+
118
+ // Job completed; decrement the in-flight job counter
119
+ self.in_flight.fetch_sub(1, Ordering::Relaxed);
120
+
121
+ // If the job panicked, resume the panic on this thread
122
+ if let Err(payload) = result {
123
+ panic::resume_unwind(payload);
124
+ }
125
+ }
126
+ }
127
+
128
+ /// Find the next job for a worker.
129
+ ///
130
+ /// Priority: pop from the worker's local queue, steal a batch from the
131
+ /// global injector, then try stealing from peer workers. Returning `None`
132
+ /// signals the caller to back off or eventually exit if no jobs remain.
133
+ fn steal_job(
134
+ local: &Worker<Box<dyn Job + Send + 'static>>,
135
+ stealers: &[Stealer<Box<dyn Job + Send + 'static>>],
136
+ injector: &Injector<Box<dyn Job + Send + 'static>>,
137
+ ) -> Option<Box<dyn Job + Send + 'static>> {
138
+ // First, try to pop a job from the worker's own local queue (fastest, lowest contention).
139
+ if let Some(job) = local.pop() {
140
+ return Some(job);
141
+ }
142
+
143
+ // If the local queue is empty, try to steal a batch of jobs from the global injector queue.
144
+ match injector.steal_batch_and_pop(local) {
145
+ Steal::Success(job) => return Some(job), // Successfully stole a job from the global injector.
146
+ Steal::Retry => return None, // Transient failure; signal to caller to back off and try again later.
147
+ Steal::Empty => {} // No jobs available in the global injector; continue to peers.
148
+ }
149
+
150
+ // As a last resort, attempt to steal jobs from each of the peer workers' queues.
151
+ for stealer in stealers {
152
+ match stealer.steal_batch_and_pop(local) {
153
+ Steal::Success(job) => return Some(job), // Successfully stole a job from a peer.
154
+ Steal::Retry => return None, // Retry advised; caller should back off and loop.
155
+ Steal::Empty => {} // No jobs in this peer; try next peer.
156
+ }
157
+ }
158
+
159
+ // No jobs available from any source.
160
+ None
161
+ }
162
+ }
163
+
164
+ /// Unit of work scheduled on a `JobQueue`.
165
+ ///
166
+ /// # Example
167
+ ///
168
+ /// ```
169
+ /// use std::sync::Arc;
170
+ /// use rubydex::job_queue::{Job, JobQueue};
171
+ ///
172
+ /// struct PrintJob;
173
+ ///
174
+ /// impl Job for PrintJob {
175
+ /// fn run(&self) {
176
+ /// println!("hello from a worker");
177
+ /// }
178
+ /// }
179
+ ///
180
+ /// let queue = Arc::new(JobQueue::new());
181
+ /// queue.push(Box::new(PrintJob));
182
+ /// JobQueue::run(&queue);
183
+ /// ```
184
+ pub trait Job: Send {
185
+ fn run(&self);
186
+ }
@@ -0,0 +1,15 @@
1
+ pub mod diagnostic;
2
+ pub mod errors;
3
+ pub mod indexing;
4
+ pub mod job_queue;
5
+ pub mod listing;
6
+ pub mod model;
7
+ pub mod offset;
8
+ pub mod position;
9
+ pub mod query;
10
+ pub mod resolution;
11
+ pub mod stats;
12
+ pub mod visualization;
13
+
14
+ #[cfg(any(test, feature = "test_utils"))]
15
+ pub mod test_utils;
@@ -0,0 +1,249 @@
1
+ use crate::{
2
+ errors::Errors,
3
+ job_queue::{Job, JobQueue},
4
+ };
5
+ use crossbeam_channel::{Sender, unbounded};
6
+ use std::{
7
+ fs,
8
+ path::{Path, PathBuf},
9
+ sync::Arc,
10
+ };
11
+
12
+ pub struct FileDiscoveryJob {
13
+ path: PathBuf,
14
+ queue: Arc<JobQueue>,
15
+ paths_tx: Sender<PathBuf>,
16
+ errors_tx: Sender<Errors>,
17
+ }
18
+
19
+ impl FileDiscoveryJob {
20
+ #[must_use]
21
+ pub fn new(path: PathBuf, queue: Arc<JobQueue>, paths_tx: Sender<PathBuf>, errors_tx: Sender<Errors>) -> Self {
22
+ Self {
23
+ path,
24
+ queue,
25
+ paths_tx,
26
+ errors_tx,
27
+ }
28
+ }
29
+ }
30
+
31
+ impl FileDiscoveryJob {
32
+ fn handle_file(&self, path: &Path) {
33
+ if path.extension().is_some_and(|ext| ext == "rb") {
34
+ self.paths_tx
35
+ .send(path.to_path_buf())
36
+ .expect("file receiver dropped before run completion");
37
+ }
38
+ }
39
+
40
+ fn handle_symlink(&self, path: &PathBuf) {
41
+ let Ok(canonicalized) = fs::canonicalize(path) else {
42
+ self.send_error(Errors::FileError(format!(
43
+ "Failed to canonicalize symlink: `{}`",
44
+ path.display(),
45
+ )));
46
+
47
+ return;
48
+ };
49
+
50
+ self.queue.push(Box::new(FileDiscoveryJob::new(
51
+ canonicalized,
52
+ Arc::clone(&self.queue),
53
+ self.paths_tx.clone(),
54
+ self.errors_tx.clone(),
55
+ )));
56
+ }
57
+
58
+ fn send_error(&self, error: Errors) {
59
+ self.errors_tx
60
+ .send(error)
61
+ .expect("error receiver dropped before run completion");
62
+ }
63
+ }
64
+
65
+ impl Job for FileDiscoveryJob {
66
+ fn run(&self) {
67
+ if self.path.is_dir() {
68
+ let Ok(read_dir) = self.path.read_dir() else {
69
+ self.send_error(Errors::FileError(format!(
70
+ "Failed to read directory `{}`",
71
+ self.path.display(),
72
+ )));
73
+
74
+ return;
75
+ };
76
+
77
+ for result in read_dir {
78
+ let Ok(entry) = result else {
79
+ self.send_error(Errors::FileError(format!(
80
+ "Failed to read directory `{}`: {result:?}",
81
+ self.path.display(),
82
+ )));
83
+
84
+ continue;
85
+ };
86
+
87
+ let kind = entry.file_type().unwrap();
88
+
89
+ if kind.is_dir() {
90
+ self.queue.push(Box::new(FileDiscoveryJob::new(
91
+ entry.path(),
92
+ Arc::clone(&self.queue),
93
+ self.paths_tx.clone(),
94
+ self.errors_tx.clone(),
95
+ )));
96
+ } else if kind.is_file() {
97
+ self.handle_file(&entry.path());
98
+ } else if kind.is_symlink() {
99
+ self.handle_symlink(&entry.path());
100
+ } else {
101
+ self.send_error(Errors::FileError(format!(
102
+ "Path `{}` is not a file or directory",
103
+ entry.path().display()
104
+ )));
105
+ }
106
+ }
107
+ } else if self.path.is_file() {
108
+ self.handle_file(&self.path);
109
+ } else if self.path.is_symlink() {
110
+ self.handle_symlink(&self.path);
111
+ } else {
112
+ self.send_error(Errors::FileError(format!(
113
+ "Path `{}` is not a file or directory",
114
+ self.path.display()
115
+ )));
116
+ }
117
+ }
118
+ }
119
+
120
+ /// Recursively collects all Ruby files for the given workspace and dependencies, returning a vector of document instances
121
+ ///
122
+ /// # Errors
123
+ ///
124
+ /// Returns a `MultipleErrors` if any of the paths do not exist
125
+ ///
126
+ /// # Panics
127
+ ///
128
+ /// Panics if the errors receiver is dropped before the run completion
129
+ #[must_use]
130
+ pub fn collect_file_paths(paths: Vec<String>) -> (Vec<PathBuf>, Vec<Errors>) {
131
+ let queue = Arc::new(JobQueue::new());
132
+ let (files_tx, files_rx) = unbounded();
133
+ let (errors_tx, errors_rx) = unbounded();
134
+
135
+ for path in paths {
136
+ let Ok(canonicalized) = fs::canonicalize(&path) else {
137
+ errors_tx
138
+ .send(Errors::FileError(format!("Path `{path}` does not exist")))
139
+ .expect("errors receiver dropped before run completion");
140
+
141
+ continue;
142
+ };
143
+
144
+ queue.push(Box::new(FileDiscoveryJob::new(
145
+ canonicalized,
146
+ Arc::clone(&queue),
147
+ files_tx.clone(),
148
+ errors_tx.clone(),
149
+ )));
150
+ }
151
+
152
+ JobQueue::run(&queue);
153
+
154
+ drop(files_tx);
155
+ drop(errors_tx);
156
+
157
+ (files_rx.iter().collect(), errors_rx.iter().collect())
158
+ }
159
+
160
+ #[cfg(test)]
161
+ mod tests {
162
+ use super::*;
163
+ use crate::test_utils::Context;
164
+
165
+ fn collect_document_paths(context: &Context, paths: &[&str]) -> (Vec<String>, Vec<Errors>) {
166
+ let (files, errors) = collect_file_paths(
167
+ paths
168
+ .iter()
169
+ .map(|p| context.absolute_path_to(p).to_string_lossy().into_owned())
170
+ .collect(),
171
+ );
172
+
173
+ let mut files: Vec<String> = files
174
+ .iter()
175
+ .map(|path| context.relative_path_to(path).to_string_lossy().into_owned())
176
+ .collect();
177
+
178
+ files.sort();
179
+
180
+ (files, errors)
181
+ }
182
+
183
+ #[test]
184
+ fn collect_all_documents() {
185
+ let context = Context::new();
186
+ let baz = PathBuf::from("bar").join("baz.rb");
187
+ let qux = PathBuf::from("bar").join("qux.rb");
188
+ let bar = PathBuf::from("foo").join("bar.rb");
189
+ context.touch(&baz);
190
+ context.touch(&qux);
191
+ context.touch(&bar);
192
+
193
+ let (files, errors) = collect_document_paths(&context, &["foo", "bar"]);
194
+
195
+ assert!(errors.is_empty());
196
+
197
+ assert_eq!(
198
+ files,
199
+ vec![
200
+ baz.to_str().unwrap().to_string(),
201
+ qux.to_str().unwrap().to_string(),
202
+ bar.to_str().unwrap().to_string()
203
+ ]
204
+ );
205
+ }
206
+
207
+ #[test]
208
+ fn collect_some_documents_based_on_paths() {
209
+ let context = Context::new();
210
+ let baz = PathBuf::from("bar").join("baz.rb");
211
+ let qux = PathBuf::from("bar").join("qux.rb");
212
+ let bar = PathBuf::from("foo").join("bar.rb");
213
+
214
+ context.touch(&baz);
215
+ context.touch(&qux);
216
+ context.touch(&bar);
217
+
218
+ let (files, errors) = collect_document_paths(&context, &["bar"]);
219
+
220
+ assert!(errors.is_empty());
221
+
222
+ assert_eq!(
223
+ files,
224
+ vec![baz.to_str().unwrap().to_string(), qux.to_str().unwrap().to_string()]
225
+ );
226
+ }
227
+
228
+ #[test]
229
+ fn collect_non_existing_paths() {
230
+ let context = Context::new();
231
+
232
+ let (files, errors) = collect_file_paths(vec![
233
+ context
234
+ .absolute_path_to("non_existing_path")
235
+ .to_string_lossy()
236
+ .into_owned(),
237
+ ]);
238
+
239
+ assert!(files.is_empty());
240
+
241
+ assert_eq!(
242
+ errors,
243
+ vec![Errors::FileError(format!(
244
+ "Path `{}` does not exist",
245
+ context.absolute_path_to("non_existing_path").display()
246
+ ))]
247
+ );
248
+ }
249
+ }
@@ -0,0 +1,116 @@
1
+ use clap::{Parser, ValueEnum};
2
+ use std::mem;
3
+
4
+ use rubydex::{
5
+ indexing, 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
+
32
+ #[derive(Debug, Clone, ValueEnum)]
33
+ enum StopAfter {
34
+ Listing,
35
+ Indexing,
36
+ Resolution,
37
+ }
38
+
39
+ fn exit(print_stats: bool) {
40
+ if print_stats {
41
+ Timer::print_breakdown();
42
+ MemoryStats::print_memory_usage();
43
+ }
44
+
45
+ std::process::exit(0);
46
+ }
47
+
48
+ fn main() {
49
+ let args = Args::parse();
50
+
51
+ if args.stats {
52
+ Timer::set_global_timer(Timer::new());
53
+ }
54
+
55
+ // Listing
56
+
57
+ let (file_paths, errors) = time_it!(listing, { listing::collect_file_paths(args.paths) });
58
+
59
+ for error in errors {
60
+ eprintln!("{error}");
61
+ }
62
+
63
+ if let Some(StopAfter::Listing) = args.stop_after {
64
+ return exit(args.stats);
65
+ }
66
+
67
+ // Indexing
68
+
69
+ let mut graph = Graph::new();
70
+ let errors = time_it!(indexing, { indexing::index_files(&mut graph, file_paths) });
71
+
72
+ for error in errors {
73
+ eprintln!("{error}");
74
+ }
75
+
76
+ if let Some(StopAfter::Indexing) = args.stop_after {
77
+ return exit(args.stats);
78
+ }
79
+
80
+ // Resolution
81
+
82
+ time_it!(resolution, {
83
+ let mut resolver = Resolver::new(&mut graph);
84
+ resolver.resolve_all();
85
+ });
86
+
87
+ if let Some(StopAfter::Resolution) = args.stop_after {
88
+ return exit(args.stats);
89
+ }
90
+
91
+ // Querying
92
+
93
+ if args.stats {
94
+ time_it!(querying, {
95
+ graph.print_query_statistics();
96
+ });
97
+ }
98
+
99
+ if args.stats {
100
+ Timer::print_breakdown();
101
+ MemoryStats::print_memory_usage();
102
+ }
103
+
104
+ // Generate visualization or print statistics
105
+ if args.visualize {
106
+ println!("{}", dot::generate(&graph));
107
+ } else {
108
+ println!("Indexed {} files", graph.documents().len());
109
+ println!("Found {} names", graph.declarations().len());
110
+ println!("Found {} definitions", graph.definitions().len());
111
+ println!("Found {} URIs", graph.documents().len());
112
+ }
113
+
114
+ // Forget the graph so we don't have to wait for deallocation and let the system reclaim the memory at exit
115
+ mem::forget(graph);
116
+ }