rubydex 0.1.0.beta11 → 0.1.0.beta13

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 (108) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +23 -23
  3. data/README.md +125 -125
  4. data/THIRD_PARTY_LICENSES.html +2018 -945
  5. data/exe/rdx +47 -47
  6. data/ext/rubydex/declaration.c +453 -388
  7. data/ext/rubydex/declaration.h +23 -23
  8. data/ext/rubydex/definition.c +284 -197
  9. data/ext/rubydex/definition.h +28 -28
  10. data/ext/rubydex/diagnostic.c +6 -6
  11. data/ext/rubydex/diagnostic.h +11 -11
  12. data/ext/rubydex/document.c +97 -98
  13. data/ext/rubydex/document.h +10 -10
  14. data/ext/rubydex/extconf.rb +146 -127
  15. data/ext/rubydex/graph.c +701 -512
  16. data/ext/rubydex/graph.h +10 -10
  17. data/ext/rubydex/handle.h +44 -44
  18. data/ext/rubydex/location.c +22 -22
  19. data/ext/rubydex/location.h +15 -15
  20. data/ext/rubydex/reference.c +123 -104
  21. data/ext/rubydex/reference.h +15 -16
  22. data/ext/rubydex/rubydex.c +22 -22
  23. data/ext/rubydex/utils.c +108 -86
  24. data/ext/rubydex/utils.h +34 -28
  25. data/lib/rubydex/comment.rb +17 -17
  26. data/lib/rubydex/declaration.rb +11 -0
  27. data/lib/rubydex/diagnostic.rb +21 -21
  28. data/lib/rubydex/failures.rb +15 -15
  29. data/lib/rubydex/graph.rb +98 -92
  30. data/lib/rubydex/keyword.rb +17 -0
  31. data/lib/rubydex/keyword_parameter.rb +13 -0
  32. data/lib/rubydex/location.rb +90 -90
  33. data/lib/rubydex/mixin.rb +22 -0
  34. data/lib/rubydex/version.rb +5 -5
  35. data/lib/rubydex.rb +24 -20
  36. data/rbi/rubydex.rbi +425 -310
  37. data/rust/Cargo.lock +1851 -1851
  38. data/rust/Cargo.toml +29 -29
  39. data/rust/about.toml +10 -10
  40. data/rust/{about.hbs → about_templates/about.hbs} +81 -78
  41. data/rust/about_templates/mingw_licenses.hbs +1071 -0
  42. data/rust/rubydex/Cargo.toml +42 -42
  43. data/rust/rubydex/src/compile_assertions.rs +13 -13
  44. data/rust/rubydex/src/diagnostic.rs +110 -109
  45. data/rust/rubydex/src/errors.rs +28 -28
  46. data/rust/rubydex/src/indexing/local_graph.rs +224 -224
  47. data/rust/rubydex/src/indexing/rbs_indexer.rs +1551 -1554
  48. data/rust/rubydex/src/indexing/ruby_indexer.rs +2329 -6753
  49. data/rust/rubydex/src/indexing/ruby_indexer_tests.rs +4962 -0
  50. data/rust/rubydex/src/indexing.rs +210 -210
  51. data/rust/rubydex/src/integrity.rs +279 -278
  52. data/rust/rubydex/src/job_queue.rs +199 -205
  53. data/rust/rubydex/src/lib.rs +17 -17
  54. data/rust/rubydex/src/listing.rs +371 -272
  55. data/rust/rubydex/src/main.rs +160 -160
  56. data/rust/rubydex/src/model/built_in.rs +83 -0
  57. data/rust/rubydex/src/model/comment.rs +24 -24
  58. data/rust/rubydex/src/model/declaration.rs +679 -588
  59. data/rust/rubydex/src/model/definitions.rs +1682 -1602
  60. data/rust/rubydex/src/model/document.rs +222 -252
  61. data/rust/rubydex/src/model/encoding.rs +22 -22
  62. data/rust/rubydex/src/model/graph.rs +3782 -3556
  63. data/rust/rubydex/src/model/id.rs +110 -110
  64. data/rust/rubydex/src/model/identity_maps.rs +58 -58
  65. data/rust/rubydex/src/model/ids.rs +60 -38
  66. data/rust/rubydex/src/model/keywords.rs +256 -256
  67. data/rust/rubydex/src/model/name.rs +298 -298
  68. data/rust/rubydex/src/model/references.rs +111 -111
  69. data/rust/rubydex/src/model/string_ref.rs +50 -50
  70. data/rust/rubydex/src/model/visibility.rs +41 -41
  71. data/rust/rubydex/src/model.rs +15 -14
  72. data/rust/rubydex/src/offset.rs +147 -147
  73. data/rust/rubydex/src/position.rs +6 -6
  74. data/rust/rubydex/src/query.rs +1841 -1700
  75. data/rust/rubydex/src/resolution.rs +1852 -5895
  76. data/rust/rubydex/src/resolution_tests.rs +4701 -0
  77. data/rust/rubydex/src/stats/memory.rs +71 -71
  78. data/rust/rubydex/src/stats/orphan_report.rs +264 -263
  79. data/rust/rubydex/src/stats/timer.rs +127 -127
  80. data/rust/rubydex/src/stats.rs +11 -11
  81. data/rust/rubydex/src/test_utils/context.rs +226 -226
  82. data/rust/rubydex/src/test_utils/graph_test.rs +730 -679
  83. data/rust/rubydex/src/test_utils/local_graph_test.rs +602 -602
  84. data/rust/rubydex/src/test_utils.rs +52 -52
  85. data/rust/rubydex/src/visualization/dot.rs +192 -176
  86. data/rust/rubydex/src/visualization.rs +6 -6
  87. data/rust/rubydex/tests/cli.rs +185 -167
  88. data/rust/rubydex-mcp/Cargo.toml +28 -28
  89. data/rust/rubydex-mcp/src/main.rs +48 -48
  90. data/rust/rubydex-mcp/src/server.rs +1145 -1145
  91. data/rust/rubydex-mcp/src/tools.rs +49 -49
  92. data/rust/rubydex-mcp/tests/mcp.rs +302 -302
  93. data/rust/rubydex-sys/Cargo.toml +20 -20
  94. data/rust/rubydex-sys/build.rs +14 -14
  95. data/rust/rubydex-sys/cbindgen.toml +12 -12
  96. data/rust/rubydex-sys/src/declaration_api.rs +485 -469
  97. data/rust/rubydex-sys/src/definition_api.rs +443 -352
  98. data/rust/rubydex-sys/src/diagnostic_api.rs +99 -99
  99. data/rust/rubydex-sys/src/document_api.rs +85 -54
  100. data/rust/rubydex-sys/src/graph_api.rs +1017 -700
  101. data/rust/rubydex-sys/src/lib.rs +79 -9
  102. data/rust/rubydex-sys/src/location_api.rs +79 -79
  103. data/rust/rubydex-sys/src/name_api.rs +187 -135
  104. data/rust/rubydex-sys/src/reference_api.rs +267 -195
  105. data/rust/rubydex-sys/src/utils.rs +70 -70
  106. data/rust/rustfmt.toml +2 -2
  107. metadata +16 -9
  108. data/lib/rubydex/librubydex_sys.so +0 -0
@@ -1,272 +1,371 @@
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" || ext == "rbs") {
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
- [
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
- [baz.to_str().unwrap().to_string(), qux.to_str().unwrap().to_string()]
225
- );
226
- }
227
-
228
- #[test]
229
- fn collect_rbs_files() {
230
- let context = Context::new();
231
- let ruby_file = PathBuf::from("lib").join("foo.rb");
232
- let rbs_file = PathBuf::from("sig").join("foo.rbs");
233
- let txt_file = PathBuf::from("lib").join("notes.txt");
234
- context.touch(&ruby_file);
235
- context.touch(&rbs_file);
236
- context.touch(&txt_file);
237
-
238
- let (files, errors) = collect_document_paths(&context, &["lib", "sig"]);
239
-
240
- assert!(errors.is_empty());
241
-
242
- assert_eq!(
243
- [
244
- ruby_file.to_str().unwrap().to_string(),
245
- rbs_file.to_str().unwrap().to_string(),
246
- ],
247
- files.as_slice()
248
- );
249
- }
250
-
251
- #[test]
252
- fn collect_non_existing_paths() {
253
- let context = Context::new();
254
-
255
- let (files, errors) = collect_file_paths(vec![
256
- context
257
- .absolute_path_to("non_existing_path")
258
- .to_string_lossy()
259
- .into_owned(),
260
- ]);
261
-
262
- assert!(files.is_empty());
263
-
264
- assert_eq!(
265
- errors,
266
- [Errors::FileError(format!(
267
- "Path `{}` does not exist",
268
- context.absolute_path_to("non_existing_path").display()
269
- ))]
270
- );
271
- }
272
- }
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
+ }