rubydex 0.2.8 → 0.3.0
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 +69 -3
- data/THIRD_PARTY_LICENSES.html +168 -1381
- data/exe/rdx +136 -34
- data/ext/rubydex/declaration.c +1 -1
- data/ext/rubydex/definition.c +32 -4
- data/ext/rubydex/extconf.rb +0 -8
- data/ext/rubydex/graph.c +29 -18
- data/ext/rubydex/query.c +105 -0
- data/ext/rubydex/query.h +8 -0
- data/ext/rubydex/reference.c +60 -0
- data/ext/rubydex/rubydex.c +2 -0
- data/ext/rubydex/utils.c +12 -0
- data/ext/rubydex/utils.h +5 -0
- 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 +26 -4
- data/rust/Cargo.lock +5 -552
- data/rust/Cargo.toml +0 -1
- data/rust/rubydex/Cargo.toml +1 -0
- data/rust/rubydex/benches/graph_memory.rs +22 -4
- data/rust/rubydex/src/compile_assertions.rs +15 -0
- data/rust/rubydex/src/config.rs +54 -34
- data/rust/rubydex/src/diagnostic.rs +1 -1
- data/rust/rubydex/src/indexing/rbs_indexer.rs +14 -2
- data/rust/rubydex/src/indexing/ruby_indexer.rs +49 -58
- data/rust/rubydex/src/indexing/ruby_indexer_tests.rs +72 -16
- data/rust/rubydex/src/listing.rs +159 -102
- data/rust/rubydex/src/main.rs +3 -125
- data/rust/rubydex/src/model/declaration.rs +0 -11
- data/rust/rubydex/src/model/definitions.rs +27 -26
- data/rust/rubydex/src/model/document.rs +43 -7
- data/rust/rubydex/src/model/graph.rs +47 -35
- data/rust/rubydex/src/model/id.rs +55 -0
- data/rust/rubydex/src/model/ids.rs +21 -9
- data/rust/rubydex/src/model/name.rs +35 -7
- data/rust/rubydex/src/model/references.rs +16 -13
- data/rust/rubydex/src/operation/applier.rs +0 -2
- data/rust/rubydex/src/operation/ruby_builder.rs +48 -59
- data/rust/rubydex/src/query/cypher/schema.rs +790 -0
- data/rust/rubydex/src/query/cypher/schema_info.rs +161 -0
- data/rust/rubydex/src/query/cypher/tests.rs +228 -0
- data/rust/rubydex/src/query/cypher.rs +57 -0
- data/rust/rubydex/src/query.rs +2 -0
- data/rust/rubydex/src/resolution.rs +248 -227
- data/rust/rubydex/src/resolution_tests.rs +263 -65
- data/rust/rubydex-sys/src/declaration_api.rs +6 -3
- data/rust/rubydex-sys/src/definition_api.rs +27 -7
- data/rust/rubydex-sys/src/graph_api.rs +175 -27
- data/rust/rubydex-sys/src/reference_api.rs +58 -12
- metadata +17 -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/Cargo.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#[cfg(all(feature = "jemalloc", not(target_os = "windows")))]
|
|
2
2
|
mod imp {
|
|
3
|
-
use std::
|
|
3
|
+
use std::time::Instant;
|
|
4
4
|
|
|
5
5
|
use rubydex::{
|
|
6
6
|
indexing::{self, IndexerBackend},
|
|
@@ -18,13 +18,31 @@ mod imp {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
pub fn run() {
|
|
21
|
-
let
|
|
22
|
-
|
|
23
|
-
let
|
|
21
|
+
let mut args = std::env::args().skip(1);
|
|
22
|
+
|
|
23
|
+
let workspace = args
|
|
24
|
+
.next()
|
|
25
|
+
.expect("incorrect usage of cargo bench --bench graph_memory. Please use `utils/bench-graph-memory` instead of invoking this benchmark directly.");
|
|
26
|
+
let workspace_path = std::fs::canonicalize(&workspace).expect("the workspace path must exist");
|
|
27
|
+
assert!(workspace_path.is_dir(), "the workspace path must be a directory");
|
|
24
28
|
|
|
25
29
|
let mut graph = Graph::new();
|
|
30
|
+
graph.set_workspace_path(workspace_path);
|
|
31
|
+
|
|
32
|
+
if let Err(error) = graph.load_config(None) {
|
|
33
|
+
eprintln!("{error}");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let time = Instant::now();
|
|
37
|
+
|
|
38
|
+
let (file_paths, _) = listing::collect_file_paths(args.collect(), &graph.excluded_patterns());
|
|
39
|
+
println!("Listing {:.2}s", time.elapsed().as_secs_f64());
|
|
40
|
+
|
|
26
41
|
let _ = indexing::index_files(&mut graph, file_paths, IndexerBackend::RubyIndexer);
|
|
42
|
+
println!("Indexing {:.2}s", time.elapsed().as_secs_f64());
|
|
43
|
+
|
|
27
44
|
Resolver::new(&mut graph).resolve();
|
|
45
|
+
println!("Resolution {:.2}s", time.elapsed().as_secs_f64());
|
|
28
46
|
|
|
29
47
|
// Compare the total memory used in the allocator before and after dropping the graph
|
|
30
48
|
let before_drop = allocated_bytes();
|
|
@@ -11,3 +11,18 @@ macro_rules! assert_mem_size {
|
|
|
11
11
|
const _: [(); $size] = [(); std::mem::size_of::<$struct>()];
|
|
12
12
|
};
|
|
13
13
|
}
|
|
14
|
+
|
|
15
|
+
/// Asserts at compile time that a type is `Send + Sync`.
|
|
16
|
+
///
|
|
17
|
+
/// The `Graph` is shared across Ractors/threads behind a `RwLock`, so it must stay
|
|
18
|
+
/// `Send + Sync`. This assertion fails the build if a field that breaks either trait
|
|
19
|
+
/// is ever added.
|
|
20
|
+
#[macro_export]
|
|
21
|
+
macro_rules! assert_send_sync {
|
|
22
|
+
($struct:ident) => {
|
|
23
|
+
const _: fn() = || {
|
|
24
|
+
fn assert_send_sync<T: ?Sized + Send + Sync>() {}
|
|
25
|
+
assert_send_sync::<$struct>();
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
}
|
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
|
}
|
|
@@ -80,7 +80,16 @@ impl<'a> RBSIndexer<'a> {
|
|
|
80
80
|
let Node::Symbol(symbol) = path_node else {
|
|
81
81
|
continue;
|
|
82
82
|
};
|
|
83
|
-
|
|
83
|
+
let name_id = self.intern_name(&symbol, parent_scope, nesting_name_id);
|
|
84
|
+
|
|
85
|
+
// Emit a constant reference for each parent-scope segment so it gets its own resolution
|
|
86
|
+
// unit, mirroring the Ruby indexer (see `index_constant_reference`). Otherwise the
|
|
87
|
+
// parent scope is an orphan name that never resolves, leaving qualified references stuck.
|
|
88
|
+
let offset = Offset::from_rbs_location(&symbol.location());
|
|
89
|
+
self.local_graph
|
|
90
|
+
.add_constant_reference(ConstantReference::new(name_id, self.uri_id, offset));
|
|
91
|
+
|
|
92
|
+
parent_scope = ParentScope::Some(name_id);
|
|
84
93
|
}
|
|
85
94
|
|
|
86
95
|
self.intern_name(&type_name.name(), parent_scope, nesting_name_id)
|
|
@@ -486,7 +495,10 @@ impl Visit for RBSIndexer<'_> {
|
|
|
486
495
|
self.uri_id,
|
|
487
496
|
offset,
|
|
488
497
|
comments,
|
|
489
|
-
|
|
498
|
+
// RBS establishes that the constant exists, but its value type is intentionally not
|
|
499
|
+
// represented in the graph. Treat it like a dynamic Ruby assignment so resolution
|
|
500
|
+
// may promote it when a namespace or singleton receiver is required.
|
|
501
|
+
Self::flags(&constant_node.annotations()) | DefinitionFlags::PROMOTABLE,
|
|
490
502
|
lexical_nesting_id,
|
|
491
503
|
)));
|
|
492
504
|
|
|
@@ -386,21 +386,8 @@ impl<'a> RubyIndexer<'a> {
|
|
|
386
386
|
{
|
|
387
387
|
if let Some(arguments) = node.arguments() {
|
|
388
388
|
for argument in &arguments.arguments() {
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
let symbol = argument.as_symbol_node().unwrap();
|
|
392
|
-
|
|
393
|
-
if let Some(value_loc) = symbol.value_loc() {
|
|
394
|
-
let name = Self::location_to_string(&value_loc);
|
|
395
|
-
f(name, value_loc);
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
ruby_prism::Node::StringNode { .. } => {
|
|
399
|
-
let string = argument.as_string_node().unwrap();
|
|
400
|
-
let name = String::from_utf8_lossy(string.unescaped()).to_string();
|
|
401
|
-
f(name, argument.location());
|
|
402
|
-
}
|
|
403
|
-
_ => {}
|
|
389
|
+
if let Some((name, location)) = Self::extract_literal_name(&argument) {
|
|
390
|
+
f(name, location);
|
|
404
391
|
}
|
|
405
392
|
}
|
|
406
393
|
}
|
|
@@ -1193,10 +1180,11 @@ impl<'a> RubyIndexer<'a> {
|
|
|
1193
1180
|
|
|
1194
1181
|
fn handle_constant_visibility(&mut self, node: &ruby_prism::CallNode, visibility: Visibility) {
|
|
1195
1182
|
let receiver = node.receiver();
|
|
1183
|
+
let call_name = String::from_utf8_lossy(node.name().as_slice());
|
|
1196
1184
|
|
|
1197
|
-
let receiver_name_id = match receiver {
|
|
1185
|
+
let receiver_name_id = match &receiver {
|
|
1198
1186
|
Some(ruby_prism::Node::ConstantPathNode { .. } | ruby_prism::Node::ConstantReadNode { .. }) => {
|
|
1199
|
-
self.index_constant_reference(
|
|
1187
|
+
self.index_constant_reference(receiver.as_ref().unwrap(), true)
|
|
1200
1188
|
}
|
|
1201
1189
|
Some(ruby_prism::Node::SelfNode { .. }) | None => match self.nesting_stack.last() {
|
|
1202
1190
|
Some(Nesting::Method(_)) => {
|
|
@@ -1205,20 +1193,20 @@ impl<'a> RubyIndexer<'a> {
|
|
|
1205
1193
|
}
|
|
1206
1194
|
None => {
|
|
1207
1195
|
self.local_graph.add_diagnostic(
|
|
1208
|
-
Rule::
|
|
1196
|
+
Rule::InvalidConstantVisibility,
|
|
1209
1197
|
Offset::from_prism_location(&node.location()),
|
|
1210
|
-
"
|
|
1198
|
+
format!("`{call_name}` called at top level"),
|
|
1211
1199
|
);
|
|
1212
1200
|
self.visit_call_node_parts(node);
|
|
1213
1201
|
return;
|
|
1214
1202
|
}
|
|
1215
1203
|
_ => None,
|
|
1216
1204
|
},
|
|
1217
|
-
|
|
1205
|
+
Some(other) => {
|
|
1218
1206
|
self.local_graph.add_diagnostic(
|
|
1219
|
-
Rule::
|
|
1220
|
-
Offset::from_prism_location(&
|
|
1221
|
-
"Dynamic receiver for
|
|
1207
|
+
Rule::InvalidConstantVisibility,
|
|
1208
|
+
Offset::from_prism_location(&other.location()),
|
|
1209
|
+
format!("Dynamic receiver for `{call_name}`"),
|
|
1222
1210
|
);
|
|
1223
1211
|
self.visit_call_node_parts(node);
|
|
1224
1212
|
return;
|
|
@@ -1230,29 +1218,14 @@ impl<'a> RubyIndexer<'a> {
|
|
|
1230
1218
|
};
|
|
1231
1219
|
|
|
1232
1220
|
for argument in &arguments.arguments() {
|
|
1233
|
-
let (name, location) =
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
}
|
|
1242
|
-
ruby_prism::Node::StringNode { .. } => {
|
|
1243
|
-
let string = argument.as_string_node().unwrap();
|
|
1244
|
-
let name = String::from_utf8_lossy(string.unescaped()).to_string();
|
|
1245
|
-
(name, argument.location())
|
|
1246
|
-
}
|
|
1247
|
-
_ => {
|
|
1248
|
-
self.local_graph.add_diagnostic(
|
|
1249
|
-
Rule::InvalidPrivateConstant,
|
|
1250
|
-
Offset::from_prism_location(&argument.location()),
|
|
1251
|
-
"Private constant called with non-symbol argument".to_string(),
|
|
1252
|
-
);
|
|
1253
|
-
self.visit(&argument);
|
|
1254
|
-
continue;
|
|
1255
|
-
}
|
|
1221
|
+
let Some((name, location)) = Self::extract_literal_name(&argument) else {
|
|
1222
|
+
self.local_graph.add_diagnostic(
|
|
1223
|
+
Rule::InvalidConstantVisibility,
|
|
1224
|
+
Offset::from_prism_location(&argument.location()),
|
|
1225
|
+
format!("`{call_name}` called with a non-literal argument"),
|
|
1226
|
+
);
|
|
1227
|
+
self.visit(&argument);
|
|
1228
|
+
continue;
|
|
1256
1229
|
};
|
|
1257
1230
|
|
|
1258
1231
|
let str_id = self.local_graph.intern_string(name);
|
|
@@ -1414,6 +1387,22 @@ impl<'a> RubyIndexer<'a> {
|
|
|
1414
1387
|
}
|
|
1415
1388
|
}
|
|
1416
1389
|
|
|
1390
|
+
fn extract_literal_name<'b>(arg: &ruby_prism::Node<'b>) -> Option<(String, ruby_prism::Location<'b>)> {
|
|
1391
|
+
match arg {
|
|
1392
|
+
ruby_prism::Node::SymbolNode { .. } => {
|
|
1393
|
+
let symbol = arg.as_symbol_node().unwrap();
|
|
1394
|
+
let value_loc = symbol.value_loc()?;
|
|
1395
|
+
Some((Self::location_to_string(&value_loc), value_loc))
|
|
1396
|
+
}
|
|
1397
|
+
ruby_prism::Node::StringNode { .. } => {
|
|
1398
|
+
let string = arg.as_string_node().unwrap();
|
|
1399
|
+
let name = String::from_utf8_lossy(string.unescaped()).to_string();
|
|
1400
|
+
Some((name, arg.location()))
|
|
1401
|
+
}
|
|
1402
|
+
_ => None,
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1417
1406
|
fn is_attr_call(arg: &ruby_prism::Node) -> bool {
|
|
1418
1407
|
arg.as_call_node().is_some_and(|call| {
|
|
1419
1408
|
let receiver = call.receiver();
|
|
@@ -1452,6 +1441,18 @@ impl<'a> RubyIndexer<'a> {
|
|
|
1452
1441
|
ruby_prism::Node::SymbolNode { .. } | ruby_prism::Node::StringNode { .. }
|
|
1453
1442
|
) {
|
|
1454
1443
|
self.create_method_visibility_definition(&arg, visibility, DefinitionFlags::empty());
|
|
1444
|
+
if visibility == Visibility::ModuleFunction {
|
|
1445
|
+
// `module_function` also creates a public singleton method, so we emit a
|
|
1446
|
+
// second def for the singleton side. The two defs stay separate (rather than
|
|
1447
|
+
// shared) so reverse-lookup invalidation can detach `Foo#bar` and
|
|
1448
|
+
// `Foo::<Foo>#bar` independently. The `SINGLETON_METHOD_VISIBILITY` flag
|
|
1449
|
+
// routes this one to the singleton class.
|
|
1450
|
+
self.create_method_visibility_definition(
|
|
1451
|
+
&arg,
|
|
1452
|
+
visibility,
|
|
1453
|
+
DefinitionFlags::SINGLETON_METHOD_VISIBILITY,
|
|
1454
|
+
);
|
|
1455
|
+
}
|
|
1455
1456
|
} else {
|
|
1456
1457
|
// Unsupported arg — diagnostic + visit for side effects.
|
|
1457
1458
|
let arg_offset = Offset::from_prism_location(&arg.location());
|
|
@@ -1473,18 +1474,8 @@ impl<'a> RubyIndexer<'a> {
|
|
|
1473
1474
|
visibility: Visibility,
|
|
1474
1475
|
flags: DefinitionFlags,
|
|
1475
1476
|
) {
|
|
1476
|
-
let (name, location) =
|
|
1477
|
-
|
|
1478
|
-
let symbol = arg.as_symbol_node().unwrap();
|
|
1479
|
-
let Some(value_loc) = symbol.value_loc() else { return };
|
|
1480
|
-
(Self::location_to_string(&value_loc), value_loc)
|
|
1481
|
-
}
|
|
1482
|
-
ruby_prism::Node::StringNode { .. } => {
|
|
1483
|
-
let string = arg.as_string_node().unwrap();
|
|
1484
|
-
let name = String::from_utf8_lossy(string.unescaped()).to_string();
|
|
1485
|
-
(name, arg.location())
|
|
1486
|
-
}
|
|
1487
|
-
_ => return,
|
|
1477
|
+
let Some((name, location)) = Self::extract_literal_name(arg) else {
|
|
1478
|
+
return;
|
|
1488
1479
|
};
|
|
1489
1480
|
|
|
1490
1481
|
self.create_method_visibility_definition_from_name(&name, &location, visibility, flags);
|
|
@@ -1750,20 +1750,20 @@ mod visibility_tests {
|
|
|
1750
1750
|
fn index_private_constant_calls_diagnostics() {
|
|
1751
1751
|
let context = index_source({
|
|
1752
1752
|
"
|
|
1753
|
-
private_constant :
|
|
1754
|
-
self.private_constant :
|
|
1755
|
-
foo.private_constant :
|
|
1753
|
+
private_constant :BAR # not indexed, called at top level
|
|
1754
|
+
self.private_constant :BAR # not indexed, self receiver at top level
|
|
1755
|
+
foo.private_constant :BAR # not indexed, dynamic receiver
|
|
1756
1756
|
|
|
1757
1757
|
module Foo
|
|
1758
|
-
private_constant
|
|
1758
|
+
private_constant SomeConst, some_method # not indexed, non-literal arguments
|
|
1759
1759
|
private_constant # not indexed, no arguments
|
|
1760
1760
|
|
|
1761
1761
|
def self.qux
|
|
1762
|
-
private_constant :
|
|
1762
|
+
private_constant :BAR # not indexed, inside a method
|
|
1763
1763
|
end
|
|
1764
1764
|
|
|
1765
1765
|
def foo
|
|
1766
|
-
private_constant :
|
|
1766
|
+
private_constant :BAR # not indexed, inside a method
|
|
1767
1767
|
end
|
|
1768
1768
|
end
|
|
1769
1769
|
"
|
|
@@ -1772,15 +1772,52 @@ mod visibility_tests {
|
|
|
1772
1772
|
assert_local_diagnostics_eq!(
|
|
1773
1773
|
&context,
|
|
1774
1774
|
vec![
|
|
1775
|
-
"invalid-
|
|
1776
|
-
"invalid-
|
|
1777
|
-
"invalid-
|
|
1778
|
-
"invalid-
|
|
1779
|
-
"invalid-
|
|
1775
|
+
"invalid-constant-visibility: `private_constant` called at top level (1:1-1:22)",
|
|
1776
|
+
"invalid-constant-visibility: `private_constant` called at top level (2:1-2:27)",
|
|
1777
|
+
"invalid-constant-visibility: Dynamic receiver for `private_constant` (3:1-3:4)",
|
|
1778
|
+
"invalid-constant-visibility: `private_constant` called with a non-literal argument (6:20-6:29)",
|
|
1779
|
+
"invalid-constant-visibility: `private_constant` called with a non-literal argument (6:31-6:42)",
|
|
1780
1780
|
]
|
|
1781
1781
|
);
|
|
1782
1782
|
|
|
1783
|
-
assert_eq!(context.graph().definitions().len(), 3); // Foo, Foo
|
|
1783
|
+
assert_eq!(context.graph().definitions().len(), 3); // Foo, Foo.qux, Foo#foo
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
#[test]
|
|
1787
|
+
fn index_public_constant_calls_diagnostics() {
|
|
1788
|
+
let context = index_source({
|
|
1789
|
+
"
|
|
1790
|
+
public_constant :BAR # not indexed, called at top level
|
|
1791
|
+
self.public_constant :BAR # not indexed, self receiver at top level
|
|
1792
|
+
foo.public_constant :BAR # not indexed, dynamic receiver
|
|
1793
|
+
|
|
1794
|
+
module Foo
|
|
1795
|
+
public_constant SomeConst, some_method # not indexed, non-literal arguments
|
|
1796
|
+
public_constant # not indexed, no arguments
|
|
1797
|
+
|
|
1798
|
+
def self.qux
|
|
1799
|
+
public_constant :BAR # not indexed, inside a method
|
|
1800
|
+
end
|
|
1801
|
+
|
|
1802
|
+
def foo
|
|
1803
|
+
public_constant :BAR # not indexed, inside a method
|
|
1804
|
+
end
|
|
1805
|
+
end
|
|
1806
|
+
"
|
|
1807
|
+
});
|
|
1808
|
+
|
|
1809
|
+
assert_local_diagnostics_eq!(
|
|
1810
|
+
&context,
|
|
1811
|
+
vec![
|
|
1812
|
+
"invalid-constant-visibility: `public_constant` called at top level (1:1-1:21)",
|
|
1813
|
+
"invalid-constant-visibility: `public_constant` called at top level (2:1-2:26)",
|
|
1814
|
+
"invalid-constant-visibility: Dynamic receiver for `public_constant` (3:1-3:4)",
|
|
1815
|
+
"invalid-constant-visibility: `public_constant` called with a non-literal argument (6:19-6:28)",
|
|
1816
|
+
"invalid-constant-visibility: `public_constant` called with a non-literal argument (6:30-6:41)",
|
|
1817
|
+
]
|
|
1818
|
+
);
|
|
1819
|
+
|
|
1820
|
+
assert_eq!(context.graph().definitions().len(), 3); // Foo, Foo.qux, Foo#foo
|
|
1784
1821
|
}
|
|
1785
1822
|
|
|
1786
1823
|
#[test]
|
|
@@ -2021,10 +2058,29 @@ mod visibility_tests {
|
|
|
2021
2058
|
|
|
2022
2059
|
assert_no_local_diagnostics!(&context);
|
|
2023
2060
|
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2061
|
+
let defs = context.all_definitions_at("4:20-4:23");
|
|
2062
|
+
assert_eq!(defs.len(), 2, "expected two MethodVisibility defs");
|
|
2063
|
+
|
|
2064
|
+
let mut instance_side = None;
|
|
2065
|
+
let mut singleton_side = None;
|
|
2066
|
+
for def in defs {
|
|
2067
|
+
let Definition::MethodVisibility(method_vis) = def else {
|
|
2068
|
+
panic!("expected MethodVisibility, got {:?}", def.kind());
|
|
2069
|
+
};
|
|
2070
|
+
if method_vis.flags().is_singleton_method_visibility() {
|
|
2071
|
+
singleton_side = Some(method_vis.as_ref());
|
|
2072
|
+
} else {
|
|
2073
|
+
instance_side = Some(method_vis.as_ref());
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
let instance_side = instance_side.expect("missing instance-side def");
|
|
2078
|
+
assert_def_str_eq!(&context, instance_side, "foo()");
|
|
2079
|
+
assert_eq!(instance_side.visibility(), &Visibility::ModuleFunction);
|
|
2080
|
+
|
|
2081
|
+
let singleton_side = singleton_side.expect("missing singleton-side def");
|
|
2082
|
+
assert_def_str_eq!(&context, singleton_side, "foo()");
|
|
2083
|
+
assert_eq!(singleton_side.visibility(), &Visibility::ModuleFunction);
|
|
2028
2084
|
}
|
|
2029
2085
|
|
|
2030
2086
|
#[test]
|