rubydex 0.2.8 → 0.2.9
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 +3 -3
- data/THIRD_PARTY_LICENSES.html +132 -1378
- data/exe/rdx +28 -2
- data/ext/rubydex/extconf.rb +0 -8
- data/ext/rubydex/graph.c +15 -15
- 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 +4 -4
- data/rust/Cargo.lock +1 -555
- data/rust/Cargo.toml +0 -1
- data/rust/rubydex/src/config.rs +54 -34
- data/rust/rubydex/src/listing.rs +159 -102
- data/rust/rubydex/src/main.rs +1 -1
- data/rust/rubydex/src/model/graph.rs +7 -7
- data/rust/rubydex/src/operation/applier.rs +0 -2
- data/rust/rubydex-sys/src/graph_api.rs +17 -13
- metadata +11 -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/Cargo.toml
CHANGED
data/rust/rubydex/src/config.rs
CHANGED
|
@@ -21,9 +21,9 @@ pub const DEFAULT_EXCLUDED_DIRECTORIES: &[&str] = &[
|
|
|
21
21
|
/// Configuration coming from a config file
|
|
22
22
|
#[derive(Debug, Default, Deserialize)]
|
|
23
23
|
struct ConfigFile {
|
|
24
|
-
///
|
|
24
|
+
/// Patterns to exclude from file discovery during indexing.
|
|
25
25
|
#[serde(default)]
|
|
26
|
-
exclude: Vec<
|
|
26
|
+
exclude: Vec<Box<str>>,
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
/// Project configuration
|
|
@@ -31,8 +31,8 @@ struct ConfigFile {
|
|
|
31
31
|
pub struct Config {
|
|
32
32
|
/// Path to the workspace being analyzed
|
|
33
33
|
workspace_path: Box<Path>,
|
|
34
|
-
///
|
|
35
|
-
|
|
34
|
+
/// Patterns to exclude from file discovery during indexing.
|
|
35
|
+
excluded_patterns: HashSet<Box<str>>,
|
|
36
36
|
}
|
|
37
37
|
assert_mem_size!(Config, 64);
|
|
38
38
|
|
|
@@ -50,7 +50,7 @@ impl Config {
|
|
|
50
50
|
workspace_path: std::env::current_dir()
|
|
51
51
|
.unwrap_or_else(|_| PathBuf::from("."))
|
|
52
52
|
.into_boxed_path(),
|
|
53
|
-
|
|
53
|
+
excluded_patterns: DEFAULT_EXCLUDED_DIRECTORIES.iter().map(|&dir| Box::from(dir)).collect(),
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
|
|
@@ -65,18 +65,24 @@ impl Config {
|
|
|
65
65
|
self.workspace_path = workspace_path.into_boxed_path();
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
/// Adds
|
|
68
|
+
/// Adds patterns to exclude from file discovery during indexing. Excluded directories will be skipped entirely during
|
|
69
69
|
/// directory traversal.
|
|
70
|
-
pub fn
|
|
71
|
-
self.
|
|
70
|
+
pub fn exclude_patterns(&mut self, patterns: impl IntoIterator<Item = Box<str>>) {
|
|
71
|
+
self.excluded_patterns.extend(patterns);
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
/// Returns the set of
|
|
74
|
+
/// Returns the set of exclusion patterns resolved against the workspace path.
|
|
75
75
|
#[must_use]
|
|
76
|
-
pub fn
|
|
77
|
-
self.
|
|
76
|
+
pub fn excluded_patterns(&self) -> HashSet<Box<str>> {
|
|
77
|
+
self.excluded_patterns
|
|
78
78
|
.iter()
|
|
79
|
-
.map(|
|
|
79
|
+
.map(|entry| {
|
|
80
|
+
self.workspace_path
|
|
81
|
+
.join(&**entry)
|
|
82
|
+
.to_string_lossy()
|
|
83
|
+
.into_owned()
|
|
84
|
+
.into_boxed_str()
|
|
85
|
+
})
|
|
80
86
|
.collect()
|
|
81
87
|
}
|
|
82
88
|
|
|
@@ -124,7 +130,7 @@ impl Config {
|
|
|
124
130
|
Errors::ConfigError(format!("Invalid config file `{}`: {error}", config_path.display()))
|
|
125
131
|
})?;
|
|
126
132
|
|
|
127
|
-
self.
|
|
133
|
+
self.excluded_patterns.extend(parsed.exclude);
|
|
128
134
|
Ok(())
|
|
129
135
|
}
|
|
130
136
|
|
|
@@ -139,19 +145,32 @@ mod tests {
|
|
|
139
145
|
use super::*;
|
|
140
146
|
use std::path::Path;
|
|
141
147
|
|
|
148
|
+
fn workspace_exclusion(entry: &str) -> String {
|
|
149
|
+
PathBuf::from("/workspace").join(entry).to_string_lossy().into_owned()
|
|
150
|
+
}
|
|
151
|
+
|
|
142
152
|
#[test]
|
|
143
|
-
fn
|
|
153
|
+
fn excluded_patterns_resolves_patterns_against_the_workspace_path() {
|
|
144
154
|
let mut config = Config::new();
|
|
145
155
|
config.set_workspace_path(PathBuf::from("/workspace"));
|
|
146
|
-
config.
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
156
|
+
config.exclude_patterns([
|
|
157
|
+
Box::from("vendor"),
|
|
158
|
+
Box::from("**/fixtures"),
|
|
159
|
+
Box::from("/absolute/path"),
|
|
160
|
+
]);
|
|
161
|
+
|
|
162
|
+
let excluded = config.excluded_patterns();
|
|
163
|
+
|
|
164
|
+
let vendor = workspace_exclusion("vendor");
|
|
165
|
+
let fixtures = workspace_exclusion("**/fixtures");
|
|
166
|
+
let absolute = PathBuf::from("/absolute/path").to_string_lossy().into_owned();
|
|
167
|
+
let git = workspace_exclusion(".git");
|
|
168
|
+
|
|
169
|
+
assert!(excluded.contains(vendor.as_str()));
|
|
170
|
+
assert!(excluded.contains(fixtures.as_str()));
|
|
171
|
+
assert!(excluded.contains(absolute.as_str()));
|
|
172
|
+
// Defaults are included and resolved as well.
|
|
173
|
+
assert!(excluded.contains(git.as_str()));
|
|
155
174
|
}
|
|
156
175
|
|
|
157
176
|
#[test]
|
|
@@ -160,14 +179,14 @@ mod tests {
|
|
|
160
179
|
|
|
161
180
|
for default in DEFAULT_EXCLUDED_DIRECTORIES {
|
|
162
181
|
assert!(
|
|
163
|
-
config.
|
|
182
|
+
config.excluded_patterns.contains(*default),
|
|
164
183
|
"expected `{default}` to be excluded by default"
|
|
165
184
|
);
|
|
166
185
|
}
|
|
167
186
|
}
|
|
168
187
|
|
|
169
188
|
#[test]
|
|
170
|
-
fn
|
|
189
|
+
fn load_file_merges_excluded_patterns_and_leaves_the_workspace_path_untouched() {
|
|
171
190
|
let dir = tempfile::tempdir().expect("failed to create temp dir");
|
|
172
191
|
let config_path = dir.path().join("rubydex.toml");
|
|
173
192
|
fs::write(&config_path, "exclude = [\"vendor\", \"generated\"]\n").unwrap();
|
|
@@ -179,12 +198,12 @@ mod tests {
|
|
|
179
198
|
.load_file(&config_path)
|
|
180
199
|
.expect("expected the config file to load");
|
|
181
200
|
|
|
182
|
-
let excluded = config.
|
|
201
|
+
let excluded = config.excluded_patterns();
|
|
183
202
|
// Entries from the file are merged in and resolved against the workspace path.
|
|
184
|
-
assert!(excluded.contains(
|
|
185
|
-
assert!(excluded.contains(
|
|
203
|
+
assert!(excluded.contains(workspace_exclusion("vendor").as_str()));
|
|
204
|
+
assert!(excluded.contains(workspace_exclusion("generated").as_str()));
|
|
186
205
|
// Defaults seeded at construction survive the merge.
|
|
187
|
-
assert!(excluded.contains(
|
|
206
|
+
assert!(excluded.contains(workspace_exclusion("node_modules").as_str()));
|
|
188
207
|
// A config file cannot override the programmatically-set workspace path.
|
|
189
208
|
assert_eq!(config.workspace_path(), Path::new("/workspace"));
|
|
190
209
|
}
|
|
@@ -204,9 +223,9 @@ mod tests {
|
|
|
204
223
|
.load_file(&dir.path().join("b.toml"))
|
|
205
224
|
.expect("expected the second file to load");
|
|
206
225
|
|
|
207
|
-
let excluded = config.
|
|
208
|
-
assert!(excluded.contains(
|
|
209
|
-
assert!(excluded.contains(
|
|
226
|
+
let excluded = config.excluded_patterns();
|
|
227
|
+
assert!(excluded.contains(workspace_exclusion("vendor").as_str()));
|
|
228
|
+
assert!(excluded.contains(workspace_exclusion("generated").as_str()));
|
|
210
229
|
}
|
|
211
230
|
|
|
212
231
|
#[test]
|
|
@@ -244,7 +263,8 @@ mod tests {
|
|
|
244
263
|
config.set_workspace_path(dir.path().to_path_buf());
|
|
245
264
|
config.load_default().expect("expected rubydex.toml to load");
|
|
246
265
|
|
|
247
|
-
|
|
266
|
+
let expected = dir.path().join("vendor").to_string_lossy().into_owned();
|
|
267
|
+
assert!(config.excluded_patterns().contains(expected.as_str()));
|
|
248
268
|
}
|
|
249
269
|
|
|
250
270
|
#[test]
|
|
@@ -263,7 +283,7 @@ mod tests {
|
|
|
263
283
|
}
|
|
264
284
|
|
|
265
285
|
#[test]
|
|
266
|
-
fn
|
|
286
|
+
fn parse_defaults_the_excluded_patterns_to_empty_when_the_key_is_absent() {
|
|
267
287
|
let file = Config::parse("").expect("an empty config is valid");
|
|
268
288
|
assert!(file.exclude.is_empty());
|
|
269
289
|
}
|
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
|
@@ -134,7 +134,7 @@ fn main() {
|
|
|
134
134
|
// Listing
|
|
135
135
|
|
|
136
136
|
let (file_paths, errors) = time_it!(listing, {
|
|
137
|
-
listing::collect_file_paths(args.paths, &graph.
|
|
137
|
+
listing::collect_file_paths(args.paths, &graph.excluded_patterns())
|
|
138
138
|
});
|
|
139
139
|
|
|
140
140
|
for error in errors {
|
|
@@ -128,16 +128,16 @@ impl Graph {
|
|
|
128
128
|
&mut self.declarations
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
/// Adds
|
|
132
|
-
/// directory traversal.
|
|
133
|
-
pub fn
|
|
134
|
-
self.config.
|
|
131
|
+
/// Adds glob patterns to exclude from file discovery during indexing. Excluded directories will be skipped entirely
|
|
132
|
+
/// during directory traversal.
|
|
133
|
+
pub fn exclude_patterns(&mut self, patterns: Vec<Box<str>>) {
|
|
134
|
+
self.config.exclude_patterns(patterns);
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
/// Returns the set of
|
|
137
|
+
/// Returns the set of exclusion patterns.
|
|
138
138
|
#[must_use]
|
|
139
|
-
pub fn
|
|
140
|
-
self.config.
|
|
139
|
+
pub fn excluded_patterns(&self) -> HashSet<Box<str>> {
|
|
140
|
+
self.config.excluded_patterns()
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
/// Returns the root directory of the workspace being indexed.
|