rubydex 0.1.0.beta12-aarch64-linux
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE.txt +23 -0
- data/README.md +125 -0
- data/THIRD_PARTY_LICENSES.html +4562 -0
- data/exe/rdx +47 -0
- data/ext/rubydex/declaration.c +453 -0
- data/ext/rubydex/declaration.h +23 -0
- data/ext/rubydex/definition.c +284 -0
- data/ext/rubydex/definition.h +28 -0
- data/ext/rubydex/diagnostic.c +6 -0
- data/ext/rubydex/diagnostic.h +11 -0
- data/ext/rubydex/document.c +97 -0
- data/ext/rubydex/document.h +10 -0
- data/ext/rubydex/extconf.rb +138 -0
- data/ext/rubydex/graph.c +681 -0
- data/ext/rubydex/graph.h +10 -0
- data/ext/rubydex/handle.h +44 -0
- data/ext/rubydex/location.c +22 -0
- data/ext/rubydex/location.h +15 -0
- data/ext/rubydex/reference.c +123 -0
- data/ext/rubydex/reference.h +15 -0
- data/ext/rubydex/rubydex.c +22 -0
- data/ext/rubydex/utils.c +108 -0
- data/ext/rubydex/utils.h +34 -0
- data/lib/rubydex/3.2/rubydex.so +0 -0
- data/lib/rubydex/3.3/rubydex.so +0 -0
- data/lib/rubydex/3.4/rubydex.so +0 -0
- data/lib/rubydex/4.0/rubydex.so +0 -0
- data/lib/rubydex/comment.rb +17 -0
- data/lib/rubydex/diagnostic.rb +21 -0
- data/lib/rubydex/failures.rb +15 -0
- data/lib/rubydex/graph.rb +98 -0
- data/lib/rubydex/keyword.rb +17 -0
- data/lib/rubydex/keyword_parameter.rb +13 -0
- data/lib/rubydex/librubydex_sys.so +0 -0
- data/lib/rubydex/location.rb +90 -0
- data/lib/rubydex/mixin.rb +22 -0
- data/lib/rubydex/version.rb +5 -0
- data/lib/rubydex.rb +23 -0
- data/rbi/rubydex.rbi +422 -0
- data/rust/Cargo.lock +1851 -0
- data/rust/Cargo.toml +29 -0
- data/rust/about.hbs +78 -0
- data/rust/about.toml +10 -0
- data/rust/rubydex/Cargo.toml +42 -0
- data/rust/rubydex/src/compile_assertions.rs +13 -0
- data/rust/rubydex/src/diagnostic.rs +110 -0
- data/rust/rubydex/src/errors.rs +28 -0
- data/rust/rubydex/src/indexing/local_graph.rs +224 -0
- data/rust/rubydex/src/indexing/rbs_indexer.rs +1551 -0
- data/rust/rubydex/src/indexing/ruby_indexer.rs +2329 -0
- data/rust/rubydex/src/indexing/ruby_indexer_tests.rs +4962 -0
- data/rust/rubydex/src/indexing.rs +210 -0
- data/rust/rubydex/src/integrity.rs +279 -0
- data/rust/rubydex/src/job_queue.rs +205 -0
- data/rust/rubydex/src/lib.rs +17 -0
- data/rust/rubydex/src/listing.rs +371 -0
- data/rust/rubydex/src/main.rs +160 -0
- data/rust/rubydex/src/model/built_in.rs +83 -0
- data/rust/rubydex/src/model/comment.rs +24 -0
- data/rust/rubydex/src/model/declaration.rs +671 -0
- data/rust/rubydex/src/model/definitions.rs +1682 -0
- data/rust/rubydex/src/model/document.rs +222 -0
- data/rust/rubydex/src/model/encoding.rs +22 -0
- data/rust/rubydex/src/model/graph.rs +3754 -0
- data/rust/rubydex/src/model/id.rs +110 -0
- data/rust/rubydex/src/model/identity_maps.rs +58 -0
- data/rust/rubydex/src/model/ids.rs +60 -0
- data/rust/rubydex/src/model/keywords.rs +256 -0
- data/rust/rubydex/src/model/name.rs +298 -0
- data/rust/rubydex/src/model/references.rs +111 -0
- data/rust/rubydex/src/model/string_ref.rs +50 -0
- data/rust/rubydex/src/model/visibility.rs +41 -0
- data/rust/rubydex/src/model.rs +15 -0
- data/rust/rubydex/src/offset.rs +147 -0
- data/rust/rubydex/src/position.rs +6 -0
- data/rust/rubydex/src/query.rs +1841 -0
- data/rust/rubydex/src/resolution.rs +6517 -0
- data/rust/rubydex/src/stats/memory.rs +71 -0
- data/rust/rubydex/src/stats/orphan_report.rs +264 -0
- data/rust/rubydex/src/stats/timer.rs +127 -0
- data/rust/rubydex/src/stats.rs +11 -0
- data/rust/rubydex/src/test_utils/context.rs +226 -0
- data/rust/rubydex/src/test_utils/graph_test.rs +730 -0
- data/rust/rubydex/src/test_utils/local_graph_test.rs +602 -0
- data/rust/rubydex/src/test_utils.rs +52 -0
- data/rust/rubydex/src/visualization/dot.rs +192 -0
- data/rust/rubydex/src/visualization.rs +6 -0
- data/rust/rubydex/tests/cli.rs +185 -0
- data/rust/rubydex-mcp/Cargo.toml +28 -0
- data/rust/rubydex-mcp/src/main.rs +48 -0
- data/rust/rubydex-mcp/src/server.rs +1145 -0
- data/rust/rubydex-mcp/src/tools.rs +49 -0
- data/rust/rubydex-mcp/tests/mcp.rs +302 -0
- data/rust/rubydex-sys/Cargo.toml +20 -0
- data/rust/rubydex-sys/build.rs +14 -0
- data/rust/rubydex-sys/cbindgen.toml +12 -0
- data/rust/rubydex-sys/src/declaration_api.rs +485 -0
- data/rust/rubydex-sys/src/definition_api.rs +443 -0
- data/rust/rubydex-sys/src/diagnostic_api.rs +99 -0
- data/rust/rubydex-sys/src/document_api.rs +85 -0
- data/rust/rubydex-sys/src/graph_api.rs +948 -0
- data/rust/rubydex-sys/src/lib.rs +79 -0
- data/rust/rubydex-sys/src/location_api.rs +79 -0
- data/rust/rubydex-sys/src/name_api.rs +135 -0
- data/rust/rubydex-sys/src/reference_api.rs +267 -0
- data/rust/rubydex-sys/src/utils.rs +70 -0
- data/rust/rustfmt.toml +2 -0
- metadata +159 -0
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
use super::normalize_indentation;
|
|
2
|
+
use crate::indexing::local_graph::LocalGraph;
|
|
3
|
+
use crate::indexing::rbs_indexer::RBSIndexer;
|
|
4
|
+
use crate::indexing::ruby_indexer::RubyIndexer;
|
|
5
|
+
use crate::model::definitions::Definition;
|
|
6
|
+
use crate::model::graph::NameDependent;
|
|
7
|
+
use crate::model::ids::{NameId, StringId, UriId};
|
|
8
|
+
use crate::offset::Offset;
|
|
9
|
+
use crate::position::Position;
|
|
10
|
+
|
|
11
|
+
#[cfg(any(test, feature = "test_utils"))]
|
|
12
|
+
pub struct LocalGraphTest {
|
|
13
|
+
uri: String,
|
|
14
|
+
source: String,
|
|
15
|
+
graph: LocalGraph,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
#[cfg(any(test, feature = "test_utils"))]
|
|
19
|
+
impl LocalGraphTest {
|
|
20
|
+
#[must_use]
|
|
21
|
+
pub fn new(uri: &str, source: &str) -> Self {
|
|
22
|
+
let uri = uri.to_string();
|
|
23
|
+
let source = normalize_indentation(source);
|
|
24
|
+
|
|
25
|
+
let mut indexer = RubyIndexer::new(uri.clone(), &source);
|
|
26
|
+
indexer.index();
|
|
27
|
+
let graph = indexer.local_graph();
|
|
28
|
+
|
|
29
|
+
Self { uri, source, graph }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
#[must_use]
|
|
33
|
+
pub fn new_rbs(uri: &str, source: &str) -> Self {
|
|
34
|
+
let uri = uri.to_string();
|
|
35
|
+
let source = normalize_indentation(source);
|
|
36
|
+
|
|
37
|
+
let mut indexer = RBSIndexer::new(uri.clone(), &source);
|
|
38
|
+
indexer.index();
|
|
39
|
+
let graph = indexer.local_graph();
|
|
40
|
+
|
|
41
|
+
Self { uri, source, graph }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
#[must_use]
|
|
45
|
+
pub fn from_local_graph(uri: &str, source: &str, graph: LocalGraph) -> Self {
|
|
46
|
+
Self {
|
|
47
|
+
uri: uri.to_string(),
|
|
48
|
+
source: source.to_string(),
|
|
49
|
+
graph,
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
#[must_use]
|
|
54
|
+
pub fn uri(&self) -> &str {
|
|
55
|
+
&self.uri
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
#[must_use]
|
|
59
|
+
pub fn source(&self) -> &str {
|
|
60
|
+
&self.source
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
#[must_use]
|
|
64
|
+
pub fn source_at(&self, offset: &Offset) -> &str {
|
|
65
|
+
&self.source[offset.start() as usize..offset.end() as usize]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
#[must_use]
|
|
69
|
+
pub fn graph(&self) -> &LocalGraph {
|
|
70
|
+
&self.graph
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/// # Panics
|
|
74
|
+
///
|
|
75
|
+
/// Panics if a definition cannot be found at the given location.
|
|
76
|
+
#[must_use]
|
|
77
|
+
pub fn all_definitions_at<'a>(&'a self, location: &str) -> Vec<&'a Definition> {
|
|
78
|
+
let (uri, offset) = self.parse_location(&format!("{}:{}", self.uri(), location));
|
|
79
|
+
let uri_id = UriId::from(&uri);
|
|
80
|
+
|
|
81
|
+
let definitions = self
|
|
82
|
+
.graph()
|
|
83
|
+
.definitions()
|
|
84
|
+
.values()
|
|
85
|
+
.filter(|def| def.uri_id() == &uri_id && def.offset() == &offset)
|
|
86
|
+
.collect::<Vec<_>>();
|
|
87
|
+
|
|
88
|
+
assert!(
|
|
89
|
+
!definitions.is_empty(),
|
|
90
|
+
"could not find a definition matching {location}, did you mean one of the following: {:?}",
|
|
91
|
+
{
|
|
92
|
+
let mut offsets = self
|
|
93
|
+
.graph()
|
|
94
|
+
.definitions()
|
|
95
|
+
.values()
|
|
96
|
+
.map(crate::model::definitions::Definition::offset)
|
|
97
|
+
.collect::<Vec<_>>();
|
|
98
|
+
|
|
99
|
+
offsets.sort_by_key(|a| a.start());
|
|
100
|
+
|
|
101
|
+
offsets
|
|
102
|
+
.iter()
|
|
103
|
+
.map(|offset| offset.to_display_range(self.graph.document()))
|
|
104
|
+
.collect::<Vec<_>>()
|
|
105
|
+
}
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
definitions
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/// # Panics
|
|
112
|
+
///
|
|
113
|
+
/// Panics if no definition or multiple definitions are found at the given location.
|
|
114
|
+
#[must_use]
|
|
115
|
+
pub fn definition_at<'a>(&'a self, location: &str) -> &'a Definition {
|
|
116
|
+
let definitions = self.all_definitions_at(location);
|
|
117
|
+
assert!(
|
|
118
|
+
definitions.len() < 2,
|
|
119
|
+
"found more than one definition matching {location}"
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
definitions[0]
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/// Parses a location string like `<file:///foo.rb:3:0-3:5>` into `(uri, start_offset, end_offset)`
|
|
126
|
+
///
|
|
127
|
+
/// Format: uri:start_line:start_column-end_line:end_column
|
|
128
|
+
/// Line and column numbers are 0-indexed
|
|
129
|
+
///
|
|
130
|
+
/// # Panics
|
|
131
|
+
///
|
|
132
|
+
/// Panics if the location format is invalid, the URI has no source, or the positions are invalid.
|
|
133
|
+
#[must_use]
|
|
134
|
+
pub fn parse_location(&self, location: &str) -> (String, Offset) {
|
|
135
|
+
let (uri, start_position, end_position) = Self::parse_location_positions(location);
|
|
136
|
+
let line_index = self.graph.document().line_index();
|
|
137
|
+
|
|
138
|
+
let start_offset = line_index.offset(start_position).unwrap_or(0.into());
|
|
139
|
+
let end_offset = line_index.offset(end_position).unwrap_or(0.into());
|
|
140
|
+
|
|
141
|
+
(uri, Offset::new(start_offset.into(), end_offset.into()))
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
fn parse_location_positions(location: &str) -> (String, Position, Position) {
|
|
145
|
+
let trimmed = location.trim().trim_start_matches('<').trim_end_matches('>');
|
|
146
|
+
|
|
147
|
+
let (start_part, end_part) = trimmed.rsplit_once('-').unwrap_or_else(|| {
|
|
148
|
+
panic!("Invalid location format: {location} (expected uri:start_line:start_column-end_line:end_column)")
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
let (start_prefix, start_column_str) = start_part
|
|
152
|
+
.rsplit_once(':')
|
|
153
|
+
.unwrap_or_else(|| panic!("Invalid location format: missing start column in {location}"));
|
|
154
|
+
let (uri, start_line_str) = start_prefix
|
|
155
|
+
.rsplit_once(':')
|
|
156
|
+
.unwrap_or_else(|| panic!("Invalid location format: missing start line in {location}"));
|
|
157
|
+
|
|
158
|
+
let (end_line_str, end_column_str) = end_part
|
|
159
|
+
.split_once(':')
|
|
160
|
+
.unwrap_or_else(|| panic!("Invalid location format: missing end line or column in {location}"));
|
|
161
|
+
|
|
162
|
+
let start_line = Self::parse_number(start_line_str, "start line", location);
|
|
163
|
+
let start_column = Self::parse_number(start_column_str, "start column", location);
|
|
164
|
+
let end_line = Self::parse_number(end_line_str, "end line", location);
|
|
165
|
+
let end_column = Self::parse_number(end_column_str, "end column", location);
|
|
166
|
+
|
|
167
|
+
(
|
|
168
|
+
uri.to_string(),
|
|
169
|
+
Position {
|
|
170
|
+
line: start_line - 1,
|
|
171
|
+
col: start_column - 1,
|
|
172
|
+
},
|
|
173
|
+
Position {
|
|
174
|
+
line: end_line - 1,
|
|
175
|
+
col: end_column - 1,
|
|
176
|
+
},
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
fn parse_number(value: &str, field: &str, location: &str) -> u32 {
|
|
181
|
+
value
|
|
182
|
+
.parse()
|
|
183
|
+
.unwrap_or_else(|_| panic!("Invalid {field} '{value}' in location {location}"))
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Name dependents helpers
|
|
187
|
+
|
|
188
|
+
/// Finds all `NameId`s matching a path. `"Foo"` matches names with str="Foo" and no
|
|
189
|
+
/// `parent_scope`. `"Bar::Baz"` matches names with str="Baz" and `parent_scope` str="Bar".
|
|
190
|
+
/// Multiple matches are possible when the same constant appears at different nestings.
|
|
191
|
+
///
|
|
192
|
+
/// # Panics
|
|
193
|
+
///
|
|
194
|
+
/// Panics if no names match the given path.
|
|
195
|
+
#[must_use]
|
|
196
|
+
pub fn find_name_ids(&self, path: &str) -> Vec<NameId> {
|
|
197
|
+
let (parent, name) = match path.rsplit_once("::") {
|
|
198
|
+
Some((p, n)) => (Some(p), n),
|
|
199
|
+
None => (None, path),
|
|
200
|
+
};
|
|
201
|
+
let target_str_id = StringId::from(name);
|
|
202
|
+
let ids: Vec<NameId> = self
|
|
203
|
+
.graph()
|
|
204
|
+
.names()
|
|
205
|
+
.iter()
|
|
206
|
+
.filter(|(_, name_ref)| {
|
|
207
|
+
if *name_ref.str() != target_str_id {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
match parent {
|
|
211
|
+
None => name_ref.parent_scope().as_ref().is_none(),
|
|
212
|
+
Some(p) => name_ref.parent_scope().as_ref().is_some_and(|ps_id| {
|
|
213
|
+
let ps = self.graph().names().get(ps_id).unwrap();
|
|
214
|
+
*ps.str() == StringId::from(p)
|
|
215
|
+
}),
|
|
216
|
+
}
|
|
217
|
+
})
|
|
218
|
+
.map(|(id, _)| *id)
|
|
219
|
+
.collect();
|
|
220
|
+
assert!(!ids.is_empty(), "could not find name `{path}`");
|
|
221
|
+
ids
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
#[must_use]
|
|
225
|
+
pub fn name_dependents_for(&self, name_id: NameId) -> Vec<NameDependent> {
|
|
226
|
+
self.graph()
|
|
227
|
+
.name_dependents()
|
|
228
|
+
.get(&name_id)
|
|
229
|
+
.cloned()
|
|
230
|
+
.unwrap_or_default()
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/// # Panics
|
|
234
|
+
///
|
|
235
|
+
/// Panics if the name's string is not in the strings map.
|
|
236
|
+
#[must_use]
|
|
237
|
+
pub fn name_str(&self, name_id: &NameId) -> Option<&str> {
|
|
238
|
+
self.graph()
|
|
239
|
+
.names()
|
|
240
|
+
.get(name_id)
|
|
241
|
+
.map(|n| self.graph().strings().get(n.str()).unwrap().as_str())
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/// Returns the unqualified name string for a `NameDependent`, if available.
|
|
245
|
+
#[must_use]
|
|
246
|
+
pub fn dependent_name_str(&self, dep: &NameDependent) -> Option<&str> {
|
|
247
|
+
match dep {
|
|
248
|
+
NameDependent::ChildName(id) | NameDependent::NestedName(id) => self.name_str(id),
|
|
249
|
+
NameDependent::Definition(id) => self
|
|
250
|
+
.graph()
|
|
251
|
+
.definitions()
|
|
252
|
+
.get(id)
|
|
253
|
+
.and_then(|d| d.name_id())
|
|
254
|
+
.and_then(|name_id| self.name_str(name_id)),
|
|
255
|
+
NameDependent::Reference(id) => self
|
|
256
|
+
.graph()
|
|
257
|
+
.constant_references()
|
|
258
|
+
.get(id)
|
|
259
|
+
.and_then(|r| self.name_str(r.name_id())),
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Primitive assertions
|
|
265
|
+
|
|
266
|
+
/// Asserts that a `NameId` resolves to the expected full path string.
|
|
267
|
+
///
|
|
268
|
+
/// Usage:
|
|
269
|
+
/// - `assert_name_path_eq!(ctx, "Foo::Bar::Baz", name_id)` - asserts the full path `Foo::Bar::Baz`
|
|
270
|
+
/// - `assert_name_path_eq!(ctx, "Baz", name_id)` - asserts just `Baz` with no parent scope
|
|
271
|
+
#[cfg(test)]
|
|
272
|
+
#[macro_export]
|
|
273
|
+
macro_rules! assert_name_path_eq {
|
|
274
|
+
($context:expr, $expect_path:expr, $name_id:expr) => {{
|
|
275
|
+
let mut name_parts = Vec::new();
|
|
276
|
+
let mut current_name_id = Some($name_id);
|
|
277
|
+
|
|
278
|
+
while let Some(name_id) = current_name_id {
|
|
279
|
+
let name = $context.graph().names().get(&name_id).unwrap();
|
|
280
|
+
name_parts.push($context.graph().strings().get(name.str()).unwrap().as_str());
|
|
281
|
+
current_name_id = name.parent_scope().as_ref().copied();
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
name_parts.reverse();
|
|
285
|
+
|
|
286
|
+
let actual_path = name_parts.join("::");
|
|
287
|
+
assert_eq!(
|
|
288
|
+
$expect_path, actual_path,
|
|
289
|
+
"name path mismatch: expected `{}`, got `{}`",
|
|
290
|
+
$expect_path, actual_path
|
|
291
|
+
);
|
|
292
|
+
}};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/// Asserts that a `StringId` resolves to the expected string.
|
|
296
|
+
///
|
|
297
|
+
/// Usage:
|
|
298
|
+
/// - `assert_string_eq!(ctx, str_id, "Foo::Bar::Baz")`
|
|
299
|
+
#[cfg(test)]
|
|
300
|
+
#[macro_export]
|
|
301
|
+
macro_rules! assert_string_eq {
|
|
302
|
+
($context:expr, $str_id:expr, $expected_str:expr) => {{
|
|
303
|
+
let string_name = $context.graph().strings().get($str_id).unwrap().as_str();
|
|
304
|
+
assert_eq!(
|
|
305
|
+
string_name, $expected_str,
|
|
306
|
+
"string mismatch: expected `{}`, got `{}`",
|
|
307
|
+
$expected_str, string_name
|
|
308
|
+
);
|
|
309
|
+
}};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/// Asserts that the source text at a given `Offset` matches the expected string.
|
|
313
|
+
///
|
|
314
|
+
/// Usage:
|
|
315
|
+
/// - `assert_offset_string!(ctx, param.offset(), "String")`
|
|
316
|
+
#[cfg(test)]
|
|
317
|
+
#[macro_export]
|
|
318
|
+
macro_rules! assert_offset_string {
|
|
319
|
+
($context:expr, $offset:expr, $expected:expr) => {{
|
|
320
|
+
let actual = $context.source_at($offset);
|
|
321
|
+
assert_eq!(
|
|
322
|
+
actual, $expected,
|
|
323
|
+
"offset text mismatch: expected `{}`, got `{}`",
|
|
324
|
+
$expected, actual
|
|
325
|
+
);
|
|
326
|
+
}};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Definition assertions
|
|
330
|
+
|
|
331
|
+
#[cfg(test)]
|
|
332
|
+
#[macro_export]
|
|
333
|
+
macro_rules! assert_definition_at {
|
|
334
|
+
($context:expr, $location:expr, $variant:ident, |$var:ident| $body:block) => {{
|
|
335
|
+
let __def = $context.definition_at($location);
|
|
336
|
+
let __kind = __def.kind();
|
|
337
|
+
match __def {
|
|
338
|
+
$crate::model::definitions::Definition::$variant(boxed) => {
|
|
339
|
+
let $var = &*boxed.as_ref();
|
|
340
|
+
$body
|
|
341
|
+
}
|
|
342
|
+
_ => panic!("expected {} definition, got {:?}", stringify!($variant), __kind),
|
|
343
|
+
}
|
|
344
|
+
}};
|
|
345
|
+
|
|
346
|
+
($context:expr, $location:expr, $variant:ident) => {{
|
|
347
|
+
let __def = $context.definition_at($location);
|
|
348
|
+
let __kind = __def.kind();
|
|
349
|
+
match __def {
|
|
350
|
+
$crate::model::definitions::Definition::$variant(_) => {}
|
|
351
|
+
_ => panic!("expected {} definition, got {:?}", stringify!($variant), __kind),
|
|
352
|
+
}
|
|
353
|
+
}};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/// Asserts the full path of a definition's `name_id` matches the expected string.
|
|
357
|
+
///
|
|
358
|
+
/// Usage:
|
|
359
|
+
/// - `assert_def_name_eq!(ctx, def, "Foo::Bar::Baz")` - asserts the full path `Foo::Bar::Baz`
|
|
360
|
+
/// - `assert_def_name_eq!(ctx, def, "Baz")` - asserts just `Baz` with no parent scope
|
|
361
|
+
#[cfg(test)]
|
|
362
|
+
#[macro_export]
|
|
363
|
+
macro_rules! assert_def_name_eq {
|
|
364
|
+
($context:expr, $def:expr, $expect_path:expr) => {{
|
|
365
|
+
$crate::assert_name_path_eq!($context, $expect_path, *$def.name_id());
|
|
366
|
+
}};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/// Asserts that a definition's superclass reference matches the expected name.
|
|
370
|
+
///
|
|
371
|
+
/// Usage:
|
|
372
|
+
/// - `assert_def_superclass_ref_eq!(ctx, def, "Bar::Baz")` - asserts the full path `Bar::Baz`
|
|
373
|
+
#[cfg(test)]
|
|
374
|
+
#[macro_export]
|
|
375
|
+
macro_rules! assert_def_superclass_ref_eq {
|
|
376
|
+
($context:expr, $def:expr, $expected_name:expr) => {{
|
|
377
|
+
let name_id = *$context
|
|
378
|
+
.graph()
|
|
379
|
+
.constant_references()
|
|
380
|
+
.get($def.superclass_ref().unwrap())
|
|
381
|
+
.unwrap()
|
|
382
|
+
.name_id();
|
|
383
|
+
$crate::assert_name_path_eq!($context, $expected_name, name_id);
|
|
384
|
+
}};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/// Asserts that a definition's name offset matches the expected location.
|
|
388
|
+
///
|
|
389
|
+
/// Usage:
|
|
390
|
+
/// - `assert_def_name_offset_eq!(ctx, def, "1:7-1:10")`
|
|
391
|
+
#[cfg(test)]
|
|
392
|
+
#[macro_export]
|
|
393
|
+
macro_rules! assert_def_name_offset_eq {
|
|
394
|
+
($context:expr, $def:expr, $expected_location:expr) => {{
|
|
395
|
+
let (_, expected_offset) = $context.parse_location(&format!("{}:{}", $context.uri(), $expected_location));
|
|
396
|
+
assert_eq!(
|
|
397
|
+
&expected_offset,
|
|
398
|
+
$def.name_offset(),
|
|
399
|
+
"name_offset mismatch: expected `{}`, got `{}`",
|
|
400
|
+
expected_offset.to_display_range($context.graph().document()),
|
|
401
|
+
$def.name_offset().to_display_range($context.graph().document())
|
|
402
|
+
);
|
|
403
|
+
}};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/// Asserts that a definition's string matches the expected string.
|
|
407
|
+
///
|
|
408
|
+
/// Usage:
|
|
409
|
+
/// - `assert_def_str_eq!(ctx, def, "baz()")`
|
|
410
|
+
#[cfg(test)]
|
|
411
|
+
#[macro_export]
|
|
412
|
+
macro_rules! assert_def_str_eq {
|
|
413
|
+
($context:expr, $def:expr, $expect_name_string:expr) => {{
|
|
414
|
+
$crate::assert_string_eq!($context, $def.str_id(), $expect_name_string);
|
|
415
|
+
}};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Comment assertions
|
|
419
|
+
|
|
420
|
+
#[cfg(test)]
|
|
421
|
+
#[macro_export]
|
|
422
|
+
/// Asserts that a definition's comments matches the expected comments.
|
|
423
|
+
///
|
|
424
|
+
/// Usage:
|
|
425
|
+
/// - `assert_def_comments_eq!(ctx, def, ["# Comment 1", "# Comment 2"])`
|
|
426
|
+
macro_rules! assert_def_comments_eq {
|
|
427
|
+
($context:expr, $def:expr, $expected_comments:expr) => {{
|
|
428
|
+
let actual_comments: Vec<String> = $def.comments().iter().map(|c| c.string().to_string()).collect();
|
|
429
|
+
assert_eq!(
|
|
430
|
+
$expected_comments,
|
|
431
|
+
actual_comments.as_slice(),
|
|
432
|
+
"comments mismatch: expected `{:?}`, got `{:?}`",
|
|
433
|
+
$expected_comments,
|
|
434
|
+
actual_comments
|
|
435
|
+
);
|
|
436
|
+
}};
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Mixin assertions
|
|
440
|
+
|
|
441
|
+
/// Asserts that a definition's mixins match the expected names for a given mixin type.
|
|
442
|
+
///
|
|
443
|
+
/// Usage:
|
|
444
|
+
/// - `assert_def_mixins_eq!(ctx, def, Include, ["Foo", "Bar"])`
|
|
445
|
+
#[cfg(test)]
|
|
446
|
+
#[macro_export]
|
|
447
|
+
macro_rules! assert_def_mixins_eq {
|
|
448
|
+
($context:expr, $def:expr, $mixin_type:ident, $expected_names:expr) => {{
|
|
449
|
+
use $crate::model::definitions::Mixin;
|
|
450
|
+
|
|
451
|
+
let actual_names = $def
|
|
452
|
+
.mixins()
|
|
453
|
+
.iter()
|
|
454
|
+
.filter_map(|mixin| {
|
|
455
|
+
if let Mixin::$mixin_type(def) = mixin {
|
|
456
|
+
let name = $context
|
|
457
|
+
.graph()
|
|
458
|
+
.names()
|
|
459
|
+
.get(
|
|
460
|
+
$context
|
|
461
|
+
.graph()
|
|
462
|
+
.constant_references()
|
|
463
|
+
.get(def.constant_reference_id())
|
|
464
|
+
.unwrap()
|
|
465
|
+
.name_id(),
|
|
466
|
+
)
|
|
467
|
+
.unwrap();
|
|
468
|
+
Some($context.graph().strings().get(name.str()).unwrap().as_str())
|
|
469
|
+
} else {
|
|
470
|
+
None
|
|
471
|
+
}
|
|
472
|
+
})
|
|
473
|
+
.collect::<Vec<_>>();
|
|
474
|
+
|
|
475
|
+
assert_eq!(
|
|
476
|
+
$expected_names,
|
|
477
|
+
actual_names.as_slice(),
|
|
478
|
+
"mixins mismatch: expected `{:?}`, got `{:?}`",
|
|
479
|
+
$expected_names,
|
|
480
|
+
actual_names
|
|
481
|
+
);
|
|
482
|
+
}};
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Name dependent assertions
|
|
486
|
+
|
|
487
|
+
/// Asserts that `owner` has dependents matching the given list.
|
|
488
|
+
/// Each entry uses `Variant("name")` syntax. When multiple names match the owner path
|
|
489
|
+
/// (different nestings), any match suffices for each expected dependent.
|
|
490
|
+
///
|
|
491
|
+
/// Usage:
|
|
492
|
+
/// ```ignore
|
|
493
|
+
/// assert_dependents!(ctx, "Bar", [ChildName("Baz"), Definition("Bar")]);
|
|
494
|
+
/// assert_dependents!(ctx, "Bar::Baz", [NestedName("CONST"), Definition("Baz")]);
|
|
495
|
+
/// ```
|
|
496
|
+
#[cfg(test)]
|
|
497
|
+
#[macro_export]
|
|
498
|
+
macro_rules! assert_dependents {
|
|
499
|
+
($ctx:expr, $owner:expr, [$($variant:ident($dep:expr)),* $(,)?]) => {{
|
|
500
|
+
let owner_ids = $ctx.find_name_ids($owner);
|
|
501
|
+
$(
|
|
502
|
+
let found = owner_ids.iter().any(|owner_id| {
|
|
503
|
+
$ctx.name_dependents_for(*owner_id).iter().any(|d| {
|
|
504
|
+
matches!(d, $crate::model::graph::NameDependent::$variant(_))
|
|
505
|
+
&& $ctx.dependent_name_str(d) == Some($dep)
|
|
506
|
+
})
|
|
507
|
+
});
|
|
508
|
+
assert!(
|
|
509
|
+
found,
|
|
510
|
+
"expected {}({}) in {}'s dependents",
|
|
511
|
+
stringify!($variant),
|
|
512
|
+
$dep,
|
|
513
|
+
$owner
|
|
514
|
+
);
|
|
515
|
+
)*
|
|
516
|
+
}};
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Receiver assertions
|
|
520
|
+
|
|
521
|
+
/// Asserts that a method has the expected receiver.
|
|
522
|
+
///
|
|
523
|
+
/// Usage:
|
|
524
|
+
/// - `assert_method_has_receiver!(ctx, method, "Foo")`
|
|
525
|
+
/// - `assert_method_has_receiver!(ctx, method, "<Bar>")`
|
|
526
|
+
#[cfg(test)]
|
|
527
|
+
#[macro_export]
|
|
528
|
+
macro_rules! assert_method_has_receiver {
|
|
529
|
+
($context:expr, $method:expr, $expected_receiver:expr) => {{
|
|
530
|
+
let name_id = match $method.receiver() {
|
|
531
|
+
Some($crate::model::definitions::Receiver::SelfReceiver(def_id)) => {
|
|
532
|
+
let def = $context.graph().definitions().get(def_id).unwrap();
|
|
533
|
+
*def.name_id().expect("SelfReceiver definition should have a name_id")
|
|
534
|
+
}
|
|
535
|
+
Some($crate::model::definitions::Receiver::ConstantReceiver(name_id)) => *name_id,
|
|
536
|
+
None => {
|
|
537
|
+
panic!(
|
|
538
|
+
"Method receiver mismatch: expected `{}`, got `None`",
|
|
539
|
+
$expected_receiver
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
let name = $context.graph().names().get(&name_id).unwrap();
|
|
545
|
+
let actual_name = $context.graph().strings().get(name.str()).unwrap().as_str();
|
|
546
|
+
assert_eq!(
|
|
547
|
+
$expected_receiver, actual_name,
|
|
548
|
+
"method receiver mismatch: expected `{}`, got `{}`",
|
|
549
|
+
$expected_receiver, actual_name
|
|
550
|
+
);
|
|
551
|
+
}};
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Diagnostic assertions
|
|
555
|
+
|
|
556
|
+
#[cfg(test)]
|
|
557
|
+
#[macro_export]
|
|
558
|
+
macro_rules! assert_local_diagnostics_eq {
|
|
559
|
+
($context:expr, $expected_diagnostics:expr) => {{
|
|
560
|
+
let mut diagnostics = $context.graph().diagnostics().iter().collect::<Vec<_>>();
|
|
561
|
+
diagnostics.sort_by_key(|d| d.offset());
|
|
562
|
+
let formatted: Vec<String> = diagnostics
|
|
563
|
+
.iter()
|
|
564
|
+
.map(|d| d.formatted($context.graph().document()))
|
|
565
|
+
.collect();
|
|
566
|
+
assert_eq!(
|
|
567
|
+
$expected_diagnostics,
|
|
568
|
+
formatted.as_slice(),
|
|
569
|
+
"diagnostics mismatch: expected `{:?}`, got `{:?}`",
|
|
570
|
+
$expected_diagnostics,
|
|
571
|
+
formatted
|
|
572
|
+
);
|
|
573
|
+
}};
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
#[cfg(test)]
|
|
577
|
+
#[macro_export]
|
|
578
|
+
macro_rules! assert_no_local_diagnostics {
|
|
579
|
+
($context:expr) => {{
|
|
580
|
+
let diagnostics = $context.graph().diagnostics().iter().collect::<Vec<_>>();
|
|
581
|
+
let formatted: Vec<String> = diagnostics
|
|
582
|
+
.iter()
|
|
583
|
+
.map(|d| d.formatted($context.graph().document()))
|
|
584
|
+
.collect();
|
|
585
|
+
assert!(diagnostics.is_empty(), "expected no diagnostics, got {:?}", formatted);
|
|
586
|
+
}};
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
#[cfg(test)]
|
|
590
|
+
mod tests {
|
|
591
|
+
use super::*;
|
|
592
|
+
|
|
593
|
+
#[test]
|
|
594
|
+
fn parse_locations() {
|
|
595
|
+
let context = LocalGraphTest::new("file://foo.rb", "class Foo; end");
|
|
596
|
+
|
|
597
|
+
let (uri, offset) = context.parse_location("file://foo.rb:1:1-1:14");
|
|
598
|
+
|
|
599
|
+
assert_eq!(uri, "file://foo.rb");
|
|
600
|
+
assert_eq!(offset, Offset::new(0, 13));
|
|
601
|
+
}
|
|
602
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
mod context;
|
|
2
|
+
mod graph_test;
|
|
3
|
+
mod local_graph_test;
|
|
4
|
+
|
|
5
|
+
pub use context::Context;
|
|
6
|
+
pub use context::with_context;
|
|
7
|
+
pub use graph_test::GraphTest;
|
|
8
|
+
pub use local_graph_test::LocalGraphTest;
|
|
9
|
+
|
|
10
|
+
#[must_use]
|
|
11
|
+
pub fn normalize_indentation(input: &str) -> String {
|
|
12
|
+
let input = if let Some(rest) = input.strip_prefix('\n') {
|
|
13
|
+
match rest.chars().next() {
|
|
14
|
+
Some(' ' | '\t') => rest,
|
|
15
|
+
_ => input,
|
|
16
|
+
}
|
|
17
|
+
} else {
|
|
18
|
+
input
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
let lines: Vec<&str> = input.lines().collect();
|
|
22
|
+
if lines.is_empty() {
|
|
23
|
+
return String::new();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let first_non_empty_line = match lines.iter().find(|line| !line.trim().is_empty()) {
|
|
27
|
+
Some(line) => *line,
|
|
28
|
+
None => return input.to_string(),
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
let base_indent = first_non_empty_line.len() - first_non_empty_line.trim_start().len();
|
|
32
|
+
|
|
33
|
+
let mut normalized = lines
|
|
34
|
+
.iter()
|
|
35
|
+
.map(|line| {
|
|
36
|
+
if line.trim().is_empty() {
|
|
37
|
+
""
|
|
38
|
+
} else if line.len() >= base_indent && line.chars().take(base_indent).all(char::is_whitespace) {
|
|
39
|
+
&line[base_indent..]
|
|
40
|
+
} else {
|
|
41
|
+
line
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
.collect::<Vec<_>>()
|
|
45
|
+
.join("\n");
|
|
46
|
+
|
|
47
|
+
if input.ends_with('\n') {
|
|
48
|
+
normalized.push('\n');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
normalized
|
|
52
|
+
}
|