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.
Files changed (109) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +23 -0
  3. data/README.md +125 -0
  4. data/THIRD_PARTY_LICENSES.html +4562 -0
  5. data/exe/rdx +47 -0
  6. data/ext/rubydex/declaration.c +453 -0
  7. data/ext/rubydex/declaration.h +23 -0
  8. data/ext/rubydex/definition.c +284 -0
  9. data/ext/rubydex/definition.h +28 -0
  10. data/ext/rubydex/diagnostic.c +6 -0
  11. data/ext/rubydex/diagnostic.h +11 -0
  12. data/ext/rubydex/document.c +97 -0
  13. data/ext/rubydex/document.h +10 -0
  14. data/ext/rubydex/extconf.rb +138 -0
  15. data/ext/rubydex/graph.c +681 -0
  16. data/ext/rubydex/graph.h +10 -0
  17. data/ext/rubydex/handle.h +44 -0
  18. data/ext/rubydex/location.c +22 -0
  19. data/ext/rubydex/location.h +15 -0
  20. data/ext/rubydex/reference.c +123 -0
  21. data/ext/rubydex/reference.h +15 -0
  22. data/ext/rubydex/rubydex.c +22 -0
  23. data/ext/rubydex/utils.c +108 -0
  24. data/ext/rubydex/utils.h +34 -0
  25. data/lib/rubydex/3.2/rubydex.so +0 -0
  26. data/lib/rubydex/3.3/rubydex.so +0 -0
  27. data/lib/rubydex/3.4/rubydex.so +0 -0
  28. data/lib/rubydex/4.0/rubydex.so +0 -0
  29. data/lib/rubydex/comment.rb +17 -0
  30. data/lib/rubydex/diagnostic.rb +21 -0
  31. data/lib/rubydex/failures.rb +15 -0
  32. data/lib/rubydex/graph.rb +98 -0
  33. data/lib/rubydex/keyword.rb +17 -0
  34. data/lib/rubydex/keyword_parameter.rb +13 -0
  35. data/lib/rubydex/librubydex_sys.so +0 -0
  36. data/lib/rubydex/location.rb +90 -0
  37. data/lib/rubydex/mixin.rb +22 -0
  38. data/lib/rubydex/version.rb +5 -0
  39. data/lib/rubydex.rb +23 -0
  40. data/rbi/rubydex.rbi +422 -0
  41. data/rust/Cargo.lock +1851 -0
  42. data/rust/Cargo.toml +29 -0
  43. data/rust/about.hbs +78 -0
  44. data/rust/about.toml +10 -0
  45. data/rust/rubydex/Cargo.toml +42 -0
  46. data/rust/rubydex/src/compile_assertions.rs +13 -0
  47. data/rust/rubydex/src/diagnostic.rs +110 -0
  48. data/rust/rubydex/src/errors.rs +28 -0
  49. data/rust/rubydex/src/indexing/local_graph.rs +224 -0
  50. data/rust/rubydex/src/indexing/rbs_indexer.rs +1551 -0
  51. data/rust/rubydex/src/indexing/ruby_indexer.rs +2329 -0
  52. data/rust/rubydex/src/indexing/ruby_indexer_tests.rs +4962 -0
  53. data/rust/rubydex/src/indexing.rs +210 -0
  54. data/rust/rubydex/src/integrity.rs +279 -0
  55. data/rust/rubydex/src/job_queue.rs +205 -0
  56. data/rust/rubydex/src/lib.rs +17 -0
  57. data/rust/rubydex/src/listing.rs +371 -0
  58. data/rust/rubydex/src/main.rs +160 -0
  59. data/rust/rubydex/src/model/built_in.rs +83 -0
  60. data/rust/rubydex/src/model/comment.rs +24 -0
  61. data/rust/rubydex/src/model/declaration.rs +671 -0
  62. data/rust/rubydex/src/model/definitions.rs +1682 -0
  63. data/rust/rubydex/src/model/document.rs +222 -0
  64. data/rust/rubydex/src/model/encoding.rs +22 -0
  65. data/rust/rubydex/src/model/graph.rs +3754 -0
  66. data/rust/rubydex/src/model/id.rs +110 -0
  67. data/rust/rubydex/src/model/identity_maps.rs +58 -0
  68. data/rust/rubydex/src/model/ids.rs +60 -0
  69. data/rust/rubydex/src/model/keywords.rs +256 -0
  70. data/rust/rubydex/src/model/name.rs +298 -0
  71. data/rust/rubydex/src/model/references.rs +111 -0
  72. data/rust/rubydex/src/model/string_ref.rs +50 -0
  73. data/rust/rubydex/src/model/visibility.rs +41 -0
  74. data/rust/rubydex/src/model.rs +15 -0
  75. data/rust/rubydex/src/offset.rs +147 -0
  76. data/rust/rubydex/src/position.rs +6 -0
  77. data/rust/rubydex/src/query.rs +1841 -0
  78. data/rust/rubydex/src/resolution.rs +6517 -0
  79. data/rust/rubydex/src/stats/memory.rs +71 -0
  80. data/rust/rubydex/src/stats/orphan_report.rs +264 -0
  81. data/rust/rubydex/src/stats/timer.rs +127 -0
  82. data/rust/rubydex/src/stats.rs +11 -0
  83. data/rust/rubydex/src/test_utils/context.rs +226 -0
  84. data/rust/rubydex/src/test_utils/graph_test.rs +730 -0
  85. data/rust/rubydex/src/test_utils/local_graph_test.rs +602 -0
  86. data/rust/rubydex/src/test_utils.rs +52 -0
  87. data/rust/rubydex/src/visualization/dot.rs +192 -0
  88. data/rust/rubydex/src/visualization.rs +6 -0
  89. data/rust/rubydex/tests/cli.rs +185 -0
  90. data/rust/rubydex-mcp/Cargo.toml +28 -0
  91. data/rust/rubydex-mcp/src/main.rs +48 -0
  92. data/rust/rubydex-mcp/src/server.rs +1145 -0
  93. data/rust/rubydex-mcp/src/tools.rs +49 -0
  94. data/rust/rubydex-mcp/tests/mcp.rs +302 -0
  95. data/rust/rubydex-sys/Cargo.toml +20 -0
  96. data/rust/rubydex-sys/build.rs +14 -0
  97. data/rust/rubydex-sys/cbindgen.toml +12 -0
  98. data/rust/rubydex-sys/src/declaration_api.rs +485 -0
  99. data/rust/rubydex-sys/src/definition_api.rs +443 -0
  100. data/rust/rubydex-sys/src/diagnostic_api.rs +99 -0
  101. data/rust/rubydex-sys/src/document_api.rs +85 -0
  102. data/rust/rubydex-sys/src/graph_api.rs +948 -0
  103. data/rust/rubydex-sys/src/lib.rs +79 -0
  104. data/rust/rubydex-sys/src/location_api.rs +79 -0
  105. data/rust/rubydex-sys/src/name_api.rs +135 -0
  106. data/rust/rubydex-sys/src/reference_api.rs +267 -0
  107. data/rust/rubydex-sys/src/utils.rs +70 -0
  108. data/rust/rustfmt.toml +2 -0
  109. 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
+ }