rubydex 0.2.6 → 0.2.7
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/THIRD_PARTY_LICENSES.html +39 -6
- data/exe/rdx +2 -0
- data/ext/rubydex/declaration.c +115 -106
- data/ext/rubydex/definition.c +100 -80
- data/ext/rubydex/diagnostic.c +5 -0
- data/ext/rubydex/document.c +23 -31
- data/ext/rubydex/extconf.rb +1 -1
- data/ext/rubydex/graph.c +233 -60
- data/ext/rubydex/handle.h +13 -0
- data/ext/rubydex/location.c +5 -0
- data/ext/rubydex/reference.c +51 -46
- data/ext/rubydex/rubydex.c +5 -0
- data/ext/rubydex/signature.c +5 -0
- data/ext/rubydex/utils.c +13 -0
- data/ext/rubydex/utils.h +4 -0
- data/lib/rubydex/bin/rubydex_mcp.exe +0 -0
- data/lib/rubydex/errors.rb +3 -0
- data/lib/rubydex/graph.rb +9 -28
- data/lib/rubydex/version.rb +1 -1
- data/rbi/rubydex.rbi +11 -3
- data/rust/Cargo.lock +119 -14
- data/rust/rubydex/Cargo.toml +19 -1
- data/rust/rubydex/benches/graph_memory.rs +46 -0
- data/rust/rubydex/src/config.rs +275 -0
- data/rust/rubydex/src/errors.rs +2 -0
- data/rust/rubydex/src/lib.rs +7 -0
- data/rust/rubydex/src/model/declaration.rs +12 -51
- data/rust/rubydex/src/model/graph.rs +38 -15
- data/rust/rubydex/src/resolution.rs +2 -12
- data/rust/rubydex-sys/src/graph_api.rs +73 -3
- metadata +4 -2
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
use crate::assert_mem_size;
|
|
2
|
+
use crate::errors::Errors;
|
|
3
|
+
use serde::Deserialize;
|
|
4
|
+
use std::collections::HashSet;
|
|
5
|
+
use std::fs;
|
|
6
|
+
use std::io::ErrorKind;
|
|
7
|
+
use std::path::{Path, PathBuf};
|
|
8
|
+
|
|
9
|
+
pub const DEFAULT_EXCLUDED_DIRECTORIES: &[&str] = &[
|
|
10
|
+
".bundle",
|
|
11
|
+
".claude",
|
|
12
|
+
".git",
|
|
13
|
+
".github",
|
|
14
|
+
".ruby-lsp",
|
|
15
|
+
".vscode",
|
|
16
|
+
"log",
|
|
17
|
+
"node_modules",
|
|
18
|
+
"tmp",
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
/// Configuration coming from a config file
|
|
22
|
+
#[derive(Debug, Default, Deserialize)]
|
|
23
|
+
struct ConfigFile {
|
|
24
|
+
/// Paths to exclude from file discovery during indexing.
|
|
25
|
+
#[serde(default)]
|
|
26
|
+
exclude: Vec<PathBuf>,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/// Project configuration
|
|
30
|
+
#[derive(Debug)]
|
|
31
|
+
pub struct Config {
|
|
32
|
+
/// Path to the workspace being analyzed
|
|
33
|
+
workspace_path: Box<Path>,
|
|
34
|
+
/// Paths to exclude from file discovery during indexing.
|
|
35
|
+
excluded_paths: HashSet<PathBuf>,
|
|
36
|
+
}
|
|
37
|
+
assert_mem_size!(Config, 64);
|
|
38
|
+
|
|
39
|
+
impl Default for Config {
|
|
40
|
+
fn default() -> Self {
|
|
41
|
+
Self::new()
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
impl Config {
|
|
46
|
+
/// Creates a configuration whose workspace path defaults to the current working directory.
|
|
47
|
+
#[must_use]
|
|
48
|
+
pub fn new() -> Self {
|
|
49
|
+
Self {
|
|
50
|
+
workspace_path: std::env::current_dir()
|
|
51
|
+
.unwrap_or_else(|_| PathBuf::from("."))
|
|
52
|
+
.into_boxed_path(),
|
|
53
|
+
excluded_paths: DEFAULT_EXCLUDED_DIRECTORIES.iter().map(PathBuf::from).collect(),
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/// Returns the root directory of the workspace being analyzed.
|
|
58
|
+
#[must_use]
|
|
59
|
+
pub fn workspace_path(&self) -> &Path {
|
|
60
|
+
&self.workspace_path
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/// Sets the root directory of the workspace being analyzed.
|
|
64
|
+
pub fn set_workspace_path(&mut self, workspace_path: PathBuf) {
|
|
65
|
+
self.workspace_path = workspace_path.into_boxed_path();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/// Adds paths to exclude from file discovery during indexing. Excluded directories will be skipped entirely during
|
|
69
|
+
/// directory traversal.
|
|
70
|
+
pub fn exclude_paths(&mut self, paths: impl IntoIterator<Item = PathBuf>) {
|
|
71
|
+
self.excluded_paths.extend(paths);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/// Returns the set of paths excluded from file discovery
|
|
75
|
+
#[must_use]
|
|
76
|
+
pub fn excluded_paths(&self) -> HashSet<PathBuf> {
|
|
77
|
+
self.excluded_paths
|
|
78
|
+
.iter()
|
|
79
|
+
.map(|path| self.workspace_path.join(path))
|
|
80
|
+
.collect()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/// Merges the default `rubydex.toml` configuration file from the workspace root into this config, if present.
|
|
84
|
+
///
|
|
85
|
+
/// The default config file is optional, so a missing `rubydex.toml` is silently ignored. Any other failure (an
|
|
86
|
+
/// unreadable or malformed file) is still reported.
|
|
87
|
+
///
|
|
88
|
+
/// # Errors
|
|
89
|
+
///
|
|
90
|
+
/// Will error if the config file exists but cannot be read or has invalid syntax.
|
|
91
|
+
pub fn load_default(&mut self) -> Result<(), Errors> {
|
|
92
|
+
let config_path = self.workspace_path.join("rubydex.toml");
|
|
93
|
+
|
|
94
|
+
match self.load_file(&config_path) {
|
|
95
|
+
Err(Errors::ConfigNotFound(_)) => Ok(()),
|
|
96
|
+
other => other,
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/// Merges the configuration at `config_path` into this config
|
|
101
|
+
///
|
|
102
|
+
/// # Errors
|
|
103
|
+
///
|
|
104
|
+
/// Returns [`Errors::ConfigNotFound`] if the file does not exist or [`Errors::ConfigError`] if it cannot otherwise
|
|
105
|
+
/// be read or has invalid syntax.
|
|
106
|
+
pub fn load_file(&mut self, config_path: &Path) -> Result<(), Errors> {
|
|
107
|
+
let content = match fs::read_to_string(config_path) {
|
|
108
|
+
Ok(content) => content,
|
|
109
|
+
Err(error) if error.kind() == ErrorKind::NotFound => {
|
|
110
|
+
return Err(Errors::ConfigNotFound(format!(
|
|
111
|
+
"Config file `{}` does not exist",
|
|
112
|
+
config_path.display()
|
|
113
|
+
)));
|
|
114
|
+
}
|
|
115
|
+
Err(error) => {
|
|
116
|
+
return Err(Errors::ConfigError(format!(
|
|
117
|
+
"Failed to read config file `{}`: {error}",
|
|
118
|
+
config_path.display()
|
|
119
|
+
)));
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
let parsed = Self::parse(&content).map_err(|error| {
|
|
124
|
+
Errors::ConfigError(format!("Invalid config file `{}`: {error}", config_path.display()))
|
|
125
|
+
})?;
|
|
126
|
+
|
|
127
|
+
self.excluded_paths.extend(parsed.exclude);
|
|
128
|
+
Ok(())
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/// Parses the content into a [`ConfigFile`]
|
|
132
|
+
fn parse(content: &str) -> Result<ConfigFile, toml::de::Error> {
|
|
133
|
+
toml::from_str(content)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
#[cfg(test)]
|
|
138
|
+
mod tests {
|
|
139
|
+
use super::*;
|
|
140
|
+
use std::path::Path;
|
|
141
|
+
|
|
142
|
+
#[test]
|
|
143
|
+
fn excluded_paths_are_resolved_against_the_workspace_path() {
|
|
144
|
+
let mut config = Config::new();
|
|
145
|
+
config.set_workspace_path(PathBuf::from("/workspace"));
|
|
146
|
+
config.exclude_paths([PathBuf::from("vendor"), PathBuf::from("/absolute/path")]);
|
|
147
|
+
|
|
148
|
+
let excluded = config.excluded_paths();
|
|
149
|
+
|
|
150
|
+
// Relative entries (including the defaults) are joined with the workspace path.
|
|
151
|
+
assert!(excluded.contains(Path::new("/workspace/vendor")));
|
|
152
|
+
assert!(excluded.contains(Path::new("/workspace/.git")));
|
|
153
|
+
// Absolute entries pass through unchanged.
|
|
154
|
+
assert!(excluded.contains(Path::new("/absolute/path")));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
#[test]
|
|
158
|
+
fn new_seeds_the_default_excluded_directories() {
|
|
159
|
+
let config = Config::new();
|
|
160
|
+
|
|
161
|
+
for default in DEFAULT_EXCLUDED_DIRECTORIES {
|
|
162
|
+
assert!(
|
|
163
|
+
config.excluded_paths.contains(Path::new(default)),
|
|
164
|
+
"expected `{default}` to be excluded by default"
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
#[test]
|
|
170
|
+
fn load_file_merges_excluded_paths_and_leaves_the_workspace_path_untouched() {
|
|
171
|
+
let dir = tempfile::tempdir().expect("failed to create temp dir");
|
|
172
|
+
let config_path = dir.path().join("rubydex.toml");
|
|
173
|
+
fs::write(&config_path, "exclude = [\"vendor\", \"generated\"]\n").unwrap();
|
|
174
|
+
|
|
175
|
+
let mut config = Config::new();
|
|
176
|
+
config.set_workspace_path(PathBuf::from("/workspace"));
|
|
177
|
+
|
|
178
|
+
config
|
|
179
|
+
.load_file(&config_path)
|
|
180
|
+
.expect("expected the config file to load");
|
|
181
|
+
|
|
182
|
+
let excluded = config.excluded_paths();
|
|
183
|
+
// Entries from the file are merged in and resolved against the workspace path.
|
|
184
|
+
assert!(excluded.contains(Path::new("/workspace/vendor")));
|
|
185
|
+
assert!(excluded.contains(Path::new("/workspace/generated")));
|
|
186
|
+
// Defaults seeded at construction survive the merge.
|
|
187
|
+
assert!(excluded.contains(Path::new("/workspace/node_modules")));
|
|
188
|
+
// A config file cannot override the programmatically-set workspace path.
|
|
189
|
+
assert_eq!(config.workspace_path(), Path::new("/workspace"));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
#[test]
|
|
193
|
+
fn load_file_accumulates_exclusions_across_multiple_loads() {
|
|
194
|
+
let dir = tempfile::tempdir().expect("failed to create temp dir");
|
|
195
|
+
fs::write(dir.path().join("a.toml"), "exclude = [\"vendor\"]\n").unwrap();
|
|
196
|
+
fs::write(dir.path().join("b.toml"), "exclude = [\"generated\"]\n").unwrap();
|
|
197
|
+
|
|
198
|
+
let mut config = Config::new();
|
|
199
|
+
config.set_workspace_path(PathBuf::from("/workspace"));
|
|
200
|
+
config
|
|
201
|
+
.load_file(&dir.path().join("a.toml"))
|
|
202
|
+
.expect("expected the first file to load");
|
|
203
|
+
config
|
|
204
|
+
.load_file(&dir.path().join("b.toml"))
|
|
205
|
+
.expect("expected the second file to load");
|
|
206
|
+
|
|
207
|
+
let excluded = config.excluded_paths();
|
|
208
|
+
assert!(excluded.contains(Path::new("/workspace/vendor")));
|
|
209
|
+
assert!(excluded.contains(Path::new("/workspace/generated")));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
#[test]
|
|
213
|
+
fn load_file_errors_when_the_file_is_missing() {
|
|
214
|
+
let dir = tempfile::tempdir().expect("failed to create temp dir");
|
|
215
|
+
let mut config = Config::new();
|
|
216
|
+
|
|
217
|
+
let error = config
|
|
218
|
+
.load_file(&dir.path().join("does_not_exist.toml"))
|
|
219
|
+
.expect_err("an explicitly requested missing file must be an error");
|
|
220
|
+
|
|
221
|
+
assert!(
|
|
222
|
+
matches!(error, Errors::ConfigNotFound(_)),
|
|
223
|
+
"unexpected error: {error:?}"
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
#[test]
|
|
228
|
+
fn load_default_ignores_a_missing_config_file() {
|
|
229
|
+
let dir = tempfile::tempdir().expect("failed to create temp dir");
|
|
230
|
+
let mut config = Config::new();
|
|
231
|
+
config.set_workspace_path(dir.path().to_path_buf());
|
|
232
|
+
|
|
233
|
+
config
|
|
234
|
+
.load_default()
|
|
235
|
+
.expect("a missing rubydex.toml must not be an error");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
#[test]
|
|
239
|
+
fn load_default_loads_an_existing_config_file() {
|
|
240
|
+
let dir = tempfile::tempdir().expect("failed to create temp dir");
|
|
241
|
+
fs::write(dir.path().join("rubydex.toml"), "exclude = [\"vendor\"]\n").unwrap();
|
|
242
|
+
|
|
243
|
+
let mut config = Config::new();
|
|
244
|
+
config.set_workspace_path(dir.path().to_path_buf());
|
|
245
|
+
config.load_default().expect("expected rubydex.toml to load");
|
|
246
|
+
|
|
247
|
+
assert!(config.excluded_paths().contains(&dir.path().join("vendor")));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
#[test]
|
|
251
|
+
fn load_default_propagates_malformed_config_errors() {
|
|
252
|
+
let dir = tempfile::tempdir().expect("failed to create temp dir");
|
|
253
|
+
fs::write(dir.path().join("rubydex.toml"), "exclude = [\n").unwrap();
|
|
254
|
+
|
|
255
|
+
let mut config = Config::new();
|
|
256
|
+
config.set_workspace_path(dir.path().to_path_buf());
|
|
257
|
+
|
|
258
|
+
let error = config
|
|
259
|
+
.load_default()
|
|
260
|
+
.expect_err("a malformed default config must still be an error");
|
|
261
|
+
|
|
262
|
+
assert!(matches!(error, Errors::ConfigError(_)), "unexpected error: {error:?}");
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
#[test]
|
|
266
|
+
fn parse_defaults_the_excluded_paths_to_empty_when_the_key_is_absent() {
|
|
267
|
+
let file = Config::parse("").expect("an empty config is valid");
|
|
268
|
+
assert!(file.exclude.is_empty());
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
#[test]
|
|
272
|
+
fn parse_rejects_an_exclude_value_of_the_wrong_type() {
|
|
273
|
+
Config::parse("exclude = \"vendor\"").expect_err("exclude must be an array of strings, not a string");
|
|
274
|
+
}
|
|
275
|
+
}
|
data/rust/rubydex/src/errors.rs
CHANGED
data/rust/rubydex/src/lib.rs
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
+
// Setting the global allocator needs to happen during linking and consumers may not always want to change the global
|
|
2
|
+
// allocator. We gate the usage of jemalloc with a cargo feature, so that consumers can decide if they want to use it
|
|
3
|
+
#[cfg(all(feature = "jemalloc", not(target_os = "windows")))]
|
|
4
|
+
#[global_allocator]
|
|
5
|
+
static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;
|
|
6
|
+
|
|
1
7
|
pub mod compile_assertions;
|
|
8
|
+
pub mod config;
|
|
2
9
|
pub mod diagnostic;
|
|
3
10
|
pub mod dot;
|
|
4
11
|
pub mod errors;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
use crate::assert_mem_size;
|
|
2
|
-
use crate::diagnostic::Diagnostic;
|
|
3
2
|
use crate::model::ids::{
|
|
4
3
|
ClassVariableReferenceId, GlobalVariableReferenceId, InstanceVariableReferenceId, MethodReferenceId,
|
|
5
4
|
};
|
|
@@ -111,8 +110,6 @@ macro_rules! namespace_declaration {
|
|
|
111
110
|
descendants: IdentityHashSet<DeclarationId>,
|
|
112
111
|
/// The singleton class associated with this declaration
|
|
113
112
|
singleton_class_id: Option<DeclarationId>,
|
|
114
|
-
/// Diagnostics associated with this declaration
|
|
115
|
-
diagnostics: Vec<Diagnostic>,
|
|
116
113
|
}
|
|
117
114
|
|
|
118
115
|
impl $name {
|
|
@@ -127,13 +124,11 @@ macro_rules! namespace_declaration {
|
|
|
127
124
|
ancestors: Ancestors::Partial(Vec::new()),
|
|
128
125
|
descendants: IdentityHashSet::default(),
|
|
129
126
|
singleton_class_id: None,
|
|
130
|
-
diagnostics: Vec::new(),
|
|
131
127
|
}
|
|
132
128
|
}
|
|
133
129
|
|
|
134
|
-
pub fn extend(&mut self,
|
|
130
|
+
pub fn extend(&mut self, other: Declaration) {
|
|
135
131
|
self.definition_ids.extend(other.definitions());
|
|
136
|
-
self.diagnostics.extend(other.take_diagnostics());
|
|
137
132
|
|
|
138
133
|
match other {
|
|
139
134
|
Declaration::Namespace(namespace) => {
|
|
@@ -243,8 +238,6 @@ macro_rules! simple_declaration {
|
|
|
243
238
|
references: IdentityHashSet<$reference_type>,
|
|
244
239
|
/// The ID of the owner of this declaration
|
|
245
240
|
owner_id: DeclarationId,
|
|
246
|
-
/// Diagnostics associated with this declaration
|
|
247
|
-
diagnostics: Vec<Diagnostic>,
|
|
248
241
|
}
|
|
249
242
|
|
|
250
243
|
impl $name {
|
|
@@ -255,13 +248,11 @@ macro_rules! simple_declaration {
|
|
|
255
248
|
definition_ids: Vec::new(),
|
|
256
249
|
references: IdentityHashSet::default(),
|
|
257
250
|
owner_id,
|
|
258
|
-
diagnostics: Vec::new(),
|
|
259
251
|
}
|
|
260
252
|
}
|
|
261
253
|
|
|
262
|
-
pub fn extend(&mut self,
|
|
254
|
+
pub fn extend(&mut self, other: $name) {
|
|
263
255
|
self.definition_ids.extend(other.definitions());
|
|
264
|
-
self.diagnostics.extend(other.take_diagnostics());
|
|
265
256
|
self.references.extend(other.references());
|
|
266
257
|
}
|
|
267
258
|
|
|
@@ -278,10 +269,6 @@ macro_rules! simple_declaration {
|
|
|
278
269
|
self.references.remove(reference_id);
|
|
279
270
|
}
|
|
280
271
|
|
|
281
|
-
pub fn take_diagnostics(&mut self) -> Vec<Diagnostic> {
|
|
282
|
-
std::mem::take(&mut self.diagnostics)
|
|
283
|
-
}
|
|
284
|
-
|
|
285
272
|
#[must_use]
|
|
286
273
|
pub fn definitions(&self) -> &[DefinitionId] {
|
|
287
274
|
&self.definition_ids
|
|
@@ -436,23 +423,6 @@ impl Declaration {
|
|
|
436
423
|
})
|
|
437
424
|
}
|
|
438
425
|
|
|
439
|
-
#[must_use]
|
|
440
|
-
pub fn diagnostics(&self) -> &[Diagnostic] {
|
|
441
|
-
all_declarations!(self, it => &it.diagnostics)
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
pub fn take_diagnostics(&mut self) -> Vec<Diagnostic> {
|
|
445
|
-
all_declarations!(self, it => std::mem::take(&mut it.diagnostics))
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
pub fn add_diagnostic(&mut self, diagnostic: Diagnostic) {
|
|
449
|
-
all_declarations!(self, it => it.diagnostics.push(diagnostic));
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
pub fn clear_diagnostics(&mut self) {
|
|
453
|
-
all_declarations!(self, it => it.diagnostics.clear());
|
|
454
|
-
}
|
|
455
|
-
|
|
456
426
|
#[must_use]
|
|
457
427
|
pub fn reference_count(&self) -> usize {
|
|
458
428
|
all_declarations!(self, it => it.references.len())
|
|
@@ -546,15 +516,6 @@ impl Namespace {
|
|
|
546
516
|
all_namespaces!(self, it => &it.members)
|
|
547
517
|
}
|
|
548
518
|
|
|
549
|
-
#[must_use]
|
|
550
|
-
pub fn diagnostics(&self) -> &[Diagnostic] {
|
|
551
|
-
all_namespaces!(self, it => &it.diagnostics)
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
pub fn take_diagnostics(&mut self) -> Vec<Diagnostic> {
|
|
555
|
-
all_namespaces!(self, it => std::mem::take(&mut it.diagnostics))
|
|
556
|
-
}
|
|
557
|
-
|
|
558
519
|
pub fn extend(&mut self, other: Declaration) {
|
|
559
520
|
all_namespaces!(self, it => it.extend(other));
|
|
560
521
|
}
|
|
@@ -647,25 +608,25 @@ impl Namespace {
|
|
|
647
608
|
}
|
|
648
609
|
|
|
649
610
|
namespace_declaration!(Class, ClassDeclaration);
|
|
650
|
-
assert_mem_size!(ClassDeclaration,
|
|
611
|
+
assert_mem_size!(ClassDeclaration, 192);
|
|
651
612
|
namespace_declaration!(Module, ModuleDeclaration);
|
|
652
|
-
assert_mem_size!(ModuleDeclaration,
|
|
613
|
+
assert_mem_size!(ModuleDeclaration, 192);
|
|
653
614
|
namespace_declaration!(SingletonClass, SingletonClassDeclaration);
|
|
654
|
-
assert_mem_size!(SingletonClassDeclaration,
|
|
615
|
+
assert_mem_size!(SingletonClassDeclaration, 192);
|
|
655
616
|
namespace_declaration!(Todo, TodoDeclaration);
|
|
656
|
-
assert_mem_size!(TodoDeclaration,
|
|
617
|
+
assert_mem_size!(TodoDeclaration, 192);
|
|
657
618
|
simple_declaration!(ConstantDeclaration, ConstantReferenceId);
|
|
658
|
-
assert_mem_size!(ConstantDeclaration,
|
|
619
|
+
assert_mem_size!(ConstantDeclaration, 88);
|
|
659
620
|
simple_declaration!(MethodDeclaration, MethodReferenceId);
|
|
660
|
-
assert_mem_size!(MethodDeclaration,
|
|
621
|
+
assert_mem_size!(MethodDeclaration, 88);
|
|
661
622
|
simple_declaration!(GlobalVariableDeclaration, GlobalVariableReferenceId);
|
|
662
|
-
assert_mem_size!(GlobalVariableDeclaration,
|
|
623
|
+
assert_mem_size!(GlobalVariableDeclaration, 88);
|
|
663
624
|
simple_declaration!(InstanceVariableDeclaration, InstanceVariableReferenceId);
|
|
664
|
-
assert_mem_size!(InstanceVariableDeclaration,
|
|
625
|
+
assert_mem_size!(InstanceVariableDeclaration, 88);
|
|
665
626
|
simple_declaration!(ClassVariableDeclaration, ClassVariableReferenceId);
|
|
666
|
-
assert_mem_size!(ClassVariableDeclaration,
|
|
627
|
+
assert_mem_size!(ClassVariableDeclaration, 88);
|
|
667
628
|
simple_declaration!(ConstantAliasDeclaration, ConstantReferenceId);
|
|
668
|
-
assert_mem_size!(ConstantAliasDeclaration,
|
|
629
|
+
assert_mem_size!(ConstantAliasDeclaration, 88);
|
|
669
630
|
|
|
670
631
|
#[cfg(test)]
|
|
671
632
|
mod tests {
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
use std::collections::HashSet;
|
|
2
2
|
use std::collections::hash_map::Entry;
|
|
3
|
-
use std::path::PathBuf;
|
|
3
|
+
use std::path::{Path, PathBuf};
|
|
4
4
|
|
|
5
5
|
use crate::assert_mem_size;
|
|
6
|
+
use crate::config::Config;
|
|
6
7
|
use crate::diagnostic::Diagnostic;
|
|
8
|
+
use crate::errors::Errors;
|
|
7
9
|
use crate::indexing::local_graph::LocalGraph;
|
|
8
10
|
use crate::model::built_in::{OBJECT_ID, add_built_in_data};
|
|
9
11
|
use crate::model::declaration::{Ancestor, Declaration, Namespace};
|
|
@@ -85,10 +87,10 @@ pub struct Graph {
|
|
|
85
87
|
/// Drained by `take_pending_work()` before resolution.
|
|
86
88
|
pending_work: Vec<Unit>,
|
|
87
89
|
|
|
88
|
-
///
|
|
89
|
-
|
|
90
|
+
/// Project configuration
|
|
91
|
+
config: Config,
|
|
90
92
|
}
|
|
91
|
-
assert_mem_size!(Graph,
|
|
93
|
+
assert_mem_size!(Graph, 352);
|
|
92
94
|
|
|
93
95
|
impl Graph {
|
|
94
96
|
#[must_use]
|
|
@@ -104,7 +106,7 @@ impl Graph {
|
|
|
104
106
|
position_encoding: Encoding::default(),
|
|
105
107
|
name_dependents: IdentityHashMap::default(),
|
|
106
108
|
pending_work: Vec::default(),
|
|
107
|
-
|
|
109
|
+
config: Config::new(),
|
|
108
110
|
};
|
|
109
111
|
|
|
110
112
|
add_built_in_data(&mut graph);
|
|
@@ -126,13 +128,40 @@ impl Graph {
|
|
|
126
128
|
/// Adds paths to exclude from file discovery during indexing. Excluded directories will be skipped entirely during
|
|
127
129
|
/// directory traversal.
|
|
128
130
|
pub fn exclude_paths(&mut self, paths: Vec<PathBuf>) {
|
|
129
|
-
self.
|
|
131
|
+
self.config.exclude_paths(paths);
|
|
130
132
|
}
|
|
131
133
|
|
|
132
134
|
/// Returns the set of paths excluded from file discovery.
|
|
133
135
|
#[must_use]
|
|
134
|
-
pub fn excluded_paths(&self) ->
|
|
135
|
-
|
|
136
|
+
pub fn excluded_paths(&self) -> HashSet<PathBuf> {
|
|
137
|
+
self.config.excluded_paths()
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/// Returns the root directory of the workspace being indexed.
|
|
141
|
+
#[must_use]
|
|
142
|
+
pub fn workspace_path(&self) -> &Path {
|
|
143
|
+
self.config.workspace_path()
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/// Sets the root directory of the workspace being indexed.
|
|
147
|
+
pub fn set_workspace_path(&mut self, workspace_path: PathBuf) {
|
|
148
|
+
self.config.set_workspace_path(workspace_path);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/// Loads a configuration file. Pass `None` to load the default `rubydex.toml` configuration file if it exists
|
|
152
|
+
///
|
|
153
|
+
/// # Errors
|
|
154
|
+
///
|
|
155
|
+
/// Returns an [`Errors::ConfigNotFound`] if an explicitly requested file does not exist or an
|
|
156
|
+
/// [`Errors::ConfigError`] if a file cannot otherwise be read or its contents are malformed.
|
|
157
|
+
pub fn load_config(&mut self, config_path: Option<&Path>) -> Result<(), Errors> {
|
|
158
|
+
match config_path {
|
|
159
|
+
Some(path) => {
|
|
160
|
+
let path = self.config.workspace_path().join(path);
|
|
161
|
+
self.config.load_file(&path)
|
|
162
|
+
}
|
|
163
|
+
None => self.config.load_default(),
|
|
164
|
+
}
|
|
136
165
|
}
|
|
137
166
|
|
|
138
167
|
/// # Panics
|
|
@@ -486,10 +515,7 @@ impl Graph {
|
|
|
486
515
|
|
|
487
516
|
#[must_use]
|
|
488
517
|
pub fn all_diagnostics(&self) -> Vec<&Diagnostic> {
|
|
489
|
-
|
|
490
|
-
let declaration_diagnostics = self.declarations.values().flat_map(Declaration::diagnostics);
|
|
491
|
-
|
|
492
|
-
document_diagnostics.chain(declaration_diagnostics).collect()
|
|
518
|
+
self.documents.values().flat_map(Document::diagnostics).collect()
|
|
493
519
|
}
|
|
494
520
|
|
|
495
521
|
/// Interns a string in the graph unless already interned. This method is only used to back the
|
|
@@ -1201,9 +1227,6 @@ impl Graph {
|
|
|
1201
1227
|
for def_id in detach_def_ids {
|
|
1202
1228
|
decl.remove_definition(def_id);
|
|
1203
1229
|
}
|
|
1204
|
-
if !detach_def_ids.is_empty() {
|
|
1205
|
-
decl.clear_diagnostics();
|
|
1206
|
-
}
|
|
1207
1230
|
}
|
|
1208
1231
|
|
|
1209
1232
|
let Some(decl) = self.declarations.get(&decl_id) else {
|
|
@@ -710,18 +710,8 @@ impl<'a> Resolver<'a> {
|
|
|
710
710
|
offset,
|
|
711
711
|
format!("undefined method `{owner_name}#{method_name}` for visibility change"),
|
|
712
712
|
);
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
// visibility resolution) and won't be cleaned up on file delete, so attaching
|
|
716
|
-
// the diagnostic to the declaration would leave it orphaned.
|
|
717
|
-
self.graph.add_document_diagnostic(uri_id, diagnostic);
|
|
718
|
-
} else {
|
|
719
|
-
self.graph
|
|
720
|
-
.declarations_mut()
|
|
721
|
-
.get_mut(&owner_id)
|
|
722
|
-
.unwrap()
|
|
723
|
-
.add_diagnostic(diagnostic);
|
|
724
|
-
}
|
|
713
|
+
|
|
714
|
+
self.graph.add_document_diagnostic(uri_id, diagnostic);
|
|
725
715
|
}
|
|
726
716
|
}
|
|
727
717
|
|
|
@@ -7,6 +7,7 @@ use crate::document_api::DocumentsIter;
|
|
|
7
7
|
use crate::reference_api::{CConstantReference, CMethodReference, ConstantReferencesIter, MethodReferencesIter};
|
|
8
8
|
use crate::{name_api, utils};
|
|
9
9
|
use libc::{c_char, c_void};
|
|
10
|
+
use rubydex::errors::Errors;
|
|
10
11
|
use rubydex::indexing::LanguageId;
|
|
11
12
|
use rubydex::model::encoding::Encoding;
|
|
12
13
|
use rubydex::model::graph::Graph;
|
|
@@ -18,7 +19,7 @@ use rubydex::query::{CompletionCandidate, CompletionContext, CompletionReceiver}
|
|
|
18
19
|
use rubydex::resolution::Resolver;
|
|
19
20
|
use rubydex::{indexing, integrity, listing, query};
|
|
20
21
|
use std::ffi::CString;
|
|
21
|
-
use std::path::PathBuf;
|
|
22
|
+
use std::path::{Path, PathBuf};
|
|
22
23
|
use std::{mem, ptr};
|
|
23
24
|
|
|
24
25
|
pub type GraphPointer = *mut c_void;
|
|
@@ -197,7 +198,13 @@ pub unsafe extern "C" fn rdx_graph_excluded_paths(
|
|
|
197
198
|
let c_strings: Vec<*const c_char> = excluded
|
|
198
199
|
.iter()
|
|
199
200
|
.filter_map(|path| {
|
|
200
|
-
|
|
201
|
+
// Normalize all paths to use forward slashes. Otherwise, you get mixed backslashes and forward slashes
|
|
202
|
+
// on Windows if a configuration file is using forward slashes. For example:
|
|
203
|
+
//
|
|
204
|
+
// C:\project/vendor/bundle
|
|
205
|
+
let normalized = path.to_string_lossy().replace(std::path::MAIN_SEPARATOR, "/");
|
|
206
|
+
|
|
207
|
+
CString::new(normalized)
|
|
201
208
|
.ok()
|
|
202
209
|
.map(|c_string| c_string.into_raw().cast_const())
|
|
203
210
|
})
|
|
@@ -210,6 +217,69 @@ pub unsafe extern "C" fn rdx_graph_excluded_paths(
|
|
|
210
217
|
})
|
|
211
218
|
}
|
|
212
219
|
|
|
220
|
+
/// Sets the workspace path used as the root directory for indexing and relative path resolution. Silently ignores the
|
|
221
|
+
/// call if the given path is not valid UTF-8, leaving the existing workspace path untouched (mirrors
|
|
222
|
+
/// `rdx_graph_set_encoding`). This avoids unwinding across the FFI boundary on malformed input.
|
|
223
|
+
///
|
|
224
|
+
/// # Safety
|
|
225
|
+
///
|
|
226
|
+
/// - `pointer` must be a valid `GraphPointer` previously returned by this crate.
|
|
227
|
+
/// - `path` must be a valid, null-terminated string.
|
|
228
|
+
#[unsafe(no_mangle)]
|
|
229
|
+
pub unsafe extern "C" fn rdx_graph_set_workspace_path(pointer: GraphPointer, path: *const c_char) {
|
|
230
|
+
let Ok(path) = (unsafe { utils::convert_char_ptr_to_string(path) }) else {
|
|
231
|
+
return;
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
with_mut_graph(pointer, |graph| graph.set_workspace_path(PathBuf::from(path)));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/// Returns the workspace path as a C string. Caller must free with `free_c_string`.
|
|
238
|
+
///
|
|
239
|
+
/// # Safety
|
|
240
|
+
///
|
|
241
|
+
/// - `pointer` must be a valid `GraphPointer` previously returned by this crate.
|
|
242
|
+
#[unsafe(no_mangle)]
|
|
243
|
+
pub unsafe extern "C" fn rdx_graph_workspace_path(pointer: GraphPointer) -> *const c_char {
|
|
244
|
+
with_graph(pointer, |graph| {
|
|
245
|
+
CString::new(graph.workspace_path().to_string_lossy().as_ref())
|
|
246
|
+
.map_or(ptr::null(), |c_string| c_string.into_raw().cast_const())
|
|
247
|
+
})
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/// Loads configuration into the graph. A null `config_path` attempts to load the default configuration file.
|
|
251
|
+
///
|
|
252
|
+
/// Returns NULL on success. On failure returns an owned, null-terminated error message that the caller must free with
|
|
253
|
+
/// `free_c_string`.
|
|
254
|
+
///
|
|
255
|
+
/// A `config_path` that is not valid UTF-8 is reported as an error message.
|
|
256
|
+
///
|
|
257
|
+
/// # Safety
|
|
258
|
+
///
|
|
259
|
+
/// - `pointer` must be a valid `GraphPointer` previously returned by this crate.
|
|
260
|
+
/// - `config_path` must either be NULL or a valid, null-terminated string.
|
|
261
|
+
#[unsafe(no_mangle)]
|
|
262
|
+
pub unsafe extern "C" fn rdx_graph_load_config(pointer: GraphPointer, config_path: *const c_char) -> *const c_char {
|
|
263
|
+
let result = with_mut_graph(pointer, |graph| {
|
|
264
|
+
if config_path.is_null() {
|
|
265
|
+
graph.load_config(None)
|
|
266
|
+
} else {
|
|
267
|
+
match unsafe { utils::convert_char_ptr_to_string(config_path) } {
|
|
268
|
+
Ok(config_path) => graph.load_config(Some(Path::new(&config_path))),
|
|
269
|
+
Err(_) => Err(Errors::ConfigError("config file path is not valid UTF-8".to_string())),
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
match result {
|
|
275
|
+
Ok(()) => ptr::null(),
|
|
276
|
+
Err(error) => CString::new(error.to_string())
|
|
277
|
+
.unwrap_or_default()
|
|
278
|
+
.into_raw()
|
|
279
|
+
.cast_const(),
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
213
283
|
/// Indexes all given file paths in parallel using the provided Graph pointer.
|
|
214
284
|
/// Returns an array of error message strings and writes the count to `out_error_count`.
|
|
215
285
|
/// Returns NULL if there are no errors. Caller must free with `free_c_string_array`.
|
|
@@ -232,7 +302,7 @@ pub unsafe extern "C" fn rdx_index_all(
|
|
|
232
302
|
let file_paths: Vec<String> = unsafe { utils::convert_double_pointer_to_vec(file_paths, count).unwrap() };
|
|
233
303
|
|
|
234
304
|
with_mut_graph(pointer, |graph| {
|
|
235
|
-
let (file_paths, listing_errors) = listing::collect_file_paths(file_paths, graph.excluded_paths());
|
|
305
|
+
let (file_paths, listing_errors) = listing::collect_file_paths(file_paths, &graph.excluded_paths());
|
|
236
306
|
let indexing_errors = indexing::index_files(graph, file_paths, indexing::IndexerBackend::RubyIndexer);
|
|
237
307
|
|
|
238
308
|
let all_errors: Vec<String> = listing_errors
|