rubydex 0.4.0 → 0.4.2

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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +9 -14
  3. data/THIRD_PARTY_LICENSES.html +13 -24
  4. data/ext/rubydex/definition.c +34 -0
  5. data/ext/rubydex/definition.h +3 -0
  6. data/ext/rubydex/diagnostic.c +2 -49
  7. data/ext/rubydex/extconf.rb +4 -2
  8. data/ext/rubydex/graph.c +59 -11
  9. data/lib/rubydex/cli/command/dead_code.rb +59 -0
  10. data/lib/rubydex/cli/command/lint/explain.rb +8 -6
  11. data/lib/rubydex/cli/command/lint.rb +10 -4
  12. data/lib/rubydex/linter/rule_loader.rb +1 -1
  13. data/lib/rubydex/mcp_server/tools/codebase_stats_tool.rb +1 -1
  14. data/lib/rubydex/mcp_server/tools/find_constant_references_tool.rb +4 -4
  15. data/lib/rubydex/mcp_server/tools/find_dead_code_candidates_tool.rb +33 -0
  16. data/lib/rubydex/mcp_server/tools/get_declaration_tool.rb +2 -2
  17. data/lib/rubydex/mcp_server/tools/get_descendants_tool.rb +4 -4
  18. data/lib/rubydex/mcp_server/tools/get_file_declarations_tool.rb +2 -2
  19. data/lib/rubydex/mcp_server/tools/search_declarations_tool.rb +6 -6
  20. data/lib/rubydex/mcp_server.rb +24 -12
  21. data/lib/rubydex/rules/dynamic_ancestor.rb +29 -0
  22. data/lib/rubydex/rules/dynamic_constant_reference.rb +25 -0
  23. data/lib/rubydex/rules/dynamic_singleton_definition.rb +30 -0
  24. data/lib/rubydex/rules/invalid_constant_visibility.rb +28 -0
  25. data/lib/rubydex/rules/invalid_method_visibility.rb +28 -0
  26. data/lib/rubydex/rules/parse_error.rb +25 -0
  27. data/lib/rubydex/rules/parse_warning.rb +28 -0
  28. data/lib/rubydex/rules/top_level_mixin_self.rb +27 -0
  29. data/lib/rubydex/rules/undefined_constant_visibility_target.rb +27 -0
  30. data/lib/rubydex/rules/undefined_method_visibility_target.rb +27 -0
  31. data/lib/rubydex/skill.rb +1 -0
  32. data/lib/rubydex/version.rb +1 -1
  33. data/lib/rubydex.rb +2 -0
  34. data/lib/rubydex_linter/rules/rule_structure.rb +19 -4
  35. data/rbi/rubydex.rbi +32 -130
  36. data/rust/Cargo.lock +17 -11
  37. data/rust/Cargo.toml +3 -0
  38. data/rust/rubydex/Cargo.toml +6 -2
  39. data/rust/rubydex/src/bin/generate_ruby_rules.rs +94 -0
  40. data/rust/rubydex/src/compile_assertions.rs +2 -0
  41. data/rust/rubydex/src/config.rs +1 -1
  42. data/rust/rubydex/src/diagnostic.rs +142 -51
  43. data/rust/rubydex/src/indexing/rbs_indexer.rs +4 -4
  44. data/rust/rubydex/src/indexing/ruby_indexer.rs +26 -18
  45. data/rust/rubydex/src/indexing/ruby_indexer_tests.rs +42 -2
  46. data/rust/rubydex/src/indexing.rs +6 -6
  47. data/rust/rubydex/src/listing.rs +12 -12
  48. data/rust/rubydex/src/model/built_in.rs +1 -1
  49. data/rust/rubydex/src/model/comment.rs +4 -4
  50. data/rust/rubydex/src/model/declaration.rs +25 -17
  51. data/rust/rubydex/src/model/document.rs +11 -11
  52. data/rust/rubydex/src/model/graph.rs +3 -3
  53. data/rust/rubydex/src/offset.rs +1 -13
  54. data/rust/rubydex/src/operation/ruby_builder.rs +29 -19
  55. data/rust/rubydex/src/query.rs +193 -2
  56. data/rust/rubydex/src/test_utils/graph_test.rs +2 -2
  57. data/rust/rubydex/src/test_utils/local_graph_test.rs +13 -6
  58. data/rust/rubydex-sys/Cargo.toml +2 -2
  59. data/rust/rubydex-sys/src/definition_api.rs +69 -2
  60. data/rust/rubydex-sys/src/diagnostic_api.rs +0 -35
  61. data/rust/rubydex-sys/src/graph_api.rs +193 -45
  62. data/rust/rubydex-sys/src/name_api.rs +219 -48
  63. metadata +191 -180
@@ -1,42 +1,105 @@
1
1
  use rubydex::model::{graph::Graph, ids::NameId, name::ParentScope};
2
2
 
3
- /// Takes a constant name and a nesting stack (e.g.: `["Foo", "Bar::Baz", "Qux"]`) and transforms it into a `NameId`,
4
- /// registering each required part in the graph. Returns the `NameId` and a list of name ids that need to be untracked
5
- /// afterwards. Returns `None` if the constant name contains no valid identifier parts (e.g.: `""`, `"::"`, `"Foo::"`).
6
- pub fn nesting_stack_to_name_id(
7
- graph: &mut Graph,
3
+ struct NameTracker<'graph> {
4
+ graph: &'graph mut Graph,
5
+ tracked_name_ids: Vec<NameId>,
6
+ }
7
+
8
+ impl<'graph> NameTracker<'graph> {
9
+ fn new(graph: &'graph mut Graph) -> Self {
10
+ Self {
11
+ graph,
12
+ tracked_name_ids: Vec::new(),
13
+ }
14
+ }
15
+
16
+ fn track_name(&mut self, string: &str, parent_scope: ParentScope, nesting: Option<NameId>) -> NameId {
17
+ let str_id = self.graph.intern_string(string.to_owned());
18
+ let name_id = self.graph.add_name(str_id, parent_scope, nesting);
19
+ self.tracked_name_ids.push(name_id);
20
+ name_id
21
+ }
22
+ }
23
+
24
+ impl Drop for NameTracker<'_> {
25
+ fn drop(&mut self) {
26
+ for name_id in self.tracked_name_ids.drain(..) {
27
+ self.graph.untrack_name(name_id);
28
+ }
29
+ }
30
+ }
31
+
32
+ /// A name temporarily tracked in a graph. Dropping this guard undoes every name and string reference-count increment
33
+ /// made while building the name.
34
+ pub struct TrackedName<'graph> {
35
+ tracker: NameTracker<'graph>,
36
+ name_id: NameId,
37
+ }
38
+
39
+ impl<'graph> TrackedName<'graph> {
40
+ fn new(tracker: NameTracker<'graph>, name_id: NameId) -> Self {
41
+ TrackedName { tracker, name_id }
42
+ }
43
+
44
+ #[must_use]
45
+ pub fn name_id(&self) -> NameId {
46
+ self.name_id
47
+ }
48
+
49
+ #[must_use]
50
+ pub fn graph(&self) -> &Graph {
51
+ &*self.tracker.graph
52
+ }
53
+
54
+ pub fn graph_mut(&mut self) -> &mut Graph {
55
+ &mut *self.tracker.graph
56
+ }
57
+ }
58
+
59
+ /// Takes a constant name and nesting names (e.g.: `["Foo", "Bar::Baz", "Qux"]`) and transforms them into a `NameId`,
60
+ /// registering each required part in the graph. The names are untracked when the returned guard is dropped.
61
+ /// Returns `None` if the constant name contains no valid identifier parts (e.g.: `""`, `"::"`, `"Foo::"`).
62
+ pub fn name_from_nesting_names<'graph>(
63
+ graph: &'graph mut Graph,
8
64
  const_name: &str,
9
65
  nesting: Vec<String>,
10
- ) -> Option<(NameId, Vec<NameId>)> {
66
+ ) -> Option<TrackedName<'graph>> {
11
67
  let mut current_nesting = None;
12
68
  let mut current_name = ParentScope::None;
13
- let mut names_to_untrack = Vec::new();
69
+ let mut tracker = NameTracker::new(graph);
14
70
 
15
71
  for entry in nesting {
16
- process_qualified_name(
17
- graph,
18
- &entry,
19
- &mut current_name,
20
- &mut current_nesting,
21
- &mut names_to_untrack,
22
- );
72
+ process_qualified_name(&mut tracker, &entry, current_nesting, &mut current_name);
23
73
  current_nesting = current_name.as_ref().copied();
24
74
  current_name = ParentScope::None;
25
75
  }
26
76
 
27
- process_qualified_name(
28
- graph,
29
- const_name,
30
- &mut current_name,
31
- &mut current_nesting,
32
- &mut names_to_untrack,
33
- );
77
+ process_qualified_name(&mut tracker, const_name, current_nesting, &mut current_name);
34
78
 
35
79
  let (ParentScope::Some(name_id) | ParentScope::Attached(name_id)) = current_name else {
36
80
  return None;
37
81
  };
38
82
 
39
- Some((name_id, names_to_untrack))
83
+ Some(TrackedName::new(tracker, name_id))
84
+ }
85
+
86
+ /// Takes a constant name and an existing lexical nesting and transforms them into a `NameId`, registering each required
87
+ /// part in the graph. The names are untracked when the returned guard is dropped.
88
+ pub fn name_from_lexical_nesting<'graph>(
89
+ graph: &'graph mut Graph,
90
+ const_name: &str,
91
+ nesting: Option<NameId>,
92
+ ) -> Option<TrackedName<'graph>> {
93
+ let mut current_name = ParentScope::None;
94
+ let mut tracker = NameTracker::new(graph);
95
+
96
+ process_qualified_name(&mut tracker, const_name, nesting, &mut current_name);
97
+
98
+ let (ParentScope::Some(name_id) | ParentScope::Attached(name_id)) = current_name else {
99
+ return None;
100
+ };
101
+
102
+ Some(TrackedName::new(tracker, name_id))
40
103
  }
41
104
 
42
105
  /// Processes a qualified name (e.g., `"Foo::Bar"` or `"<Foo>"`) by splitting on `"::"` and registering each part in the
@@ -45,11 +108,10 @@ pub fn nesting_stack_to_name_id(
45
108
  /// When a singleton is the first part (i.e., `current_name` has no parent), `current_nesting` is used as the attachment
46
109
  /// point.
47
110
  fn process_qualified_name(
48
- graph: &mut Graph,
111
+ tracker: &mut NameTracker<'_>,
49
112
  qualified_name: &str,
113
+ current_nesting: Option<NameId>,
50
114
  current_name: &mut ParentScope,
51
- current_nesting: &mut Option<NameId>,
52
- names_to_untrack: &mut Vec<NameId>,
53
115
  ) {
54
116
  for part in qualified_name.split("::") {
55
117
  if part.is_empty() {
@@ -60,18 +122,16 @@ fn process_qualified_name(
60
122
  let (parent_scope, nesting_for_part) = if part.starts_with('<') {
61
123
  let attached_id = match *current_name {
62
124
  ParentScope::Some(id) | ParentScope::Attached(id) => Some(id),
63
- _ => *current_nesting,
125
+ _ => current_nesting,
64
126
  };
65
127
 
66
128
  let attached = attached_id.map_or(ParentScope::None, ParentScope::Attached);
67
129
  (attached, attached_id)
68
130
  } else {
69
- (*current_name, *current_nesting)
131
+ (*current_name, current_nesting)
70
132
  };
71
133
 
72
- let str_id = graph.intern_string(part.to_owned());
73
- let name_id = graph.add_name(str_id, parent_scope, nesting_for_part);
74
- names_to_untrack.push(name_id);
134
+ let name_id = tracker.track_name(part, parent_scope, nesting_for_part);
75
135
  *current_name = ParentScope::Some(name_id);
76
136
  }
77
137
  }
@@ -86,38 +146,41 @@ mod tests {
86
146
  fn nesting_is_converted_to_name_id() {
87
147
  let mut graph = Graph::new();
88
148
 
89
- let (name_id, _) = nesting_stack_to_name_id(
149
+ let tracked_name = name_from_nesting_names(
90
150
  &mut graph,
91
151
  "Some::CONST",
92
152
  vec!["Foo".into(), "Bar::Zip".into(), "Qux".into()],
93
153
  )
94
154
  .unwrap();
155
+ let name_id = tracked_name.name_id();
95
156
 
96
- let const_name = graph.names().get(&name_id).unwrap();
157
+ let const_name = tracked_name.graph().names().get(&name_id).unwrap();
97
158
  assert_eq!(StringId::from("CONST"), *const_name.str());
98
159
 
99
- let some_name = graph
160
+ let some_name = tracked_name
161
+ .graph()
100
162
  .names()
101
163
  .get(&const_name.parent_scope().expect("Parent scope should exist"))
102
164
  .unwrap();
103
165
  assert_eq!(StringId::from("Some"), *some_name.str());
104
166
  assert_eq!(const_name.nesting(), some_name.nesting());
105
167
 
106
- let qux_name = graph.names().get(&some_name.nesting().unwrap()).unwrap();
168
+ let qux_name = tracked_name.graph().names().get(&some_name.nesting().unwrap()).unwrap();
107
169
  assert_eq!(StringId::from("Qux"), *qux_name.str());
108
170
  assert!(qux_name.parent_scope().is_none());
109
171
 
110
- let zip_name = graph.names().get(&qux_name.nesting().unwrap()).unwrap();
172
+ let zip_name = tracked_name.graph().names().get(&qux_name.nesting().unwrap()).unwrap();
111
173
  assert_eq!(StringId::from("Zip"), *zip_name.str());
112
174
 
113
- let bar_name = graph
175
+ let bar_name = tracked_name
176
+ .graph()
114
177
  .names()
115
178
  .get(&zip_name.parent_scope().expect("Parent scope should exist"))
116
179
  .unwrap();
117
180
  assert_eq!(StringId::from("Bar"), *bar_name.str());
118
181
  assert_eq!(zip_name.nesting(), bar_name.nesting());
119
182
 
120
- let foo_name = graph.names().get(&bar_name.nesting().unwrap()).unwrap();
183
+ let foo_name = tracked_name.graph().names().get(&bar_name.nesting().unwrap()).unwrap();
121
184
  assert_eq!(StringId::from("Foo"), *foo_name.str());
122
185
  assert!(foo_name.parent_scope().is_none());
123
186
  assert!(foo_name.nesting().is_none());
@@ -127,13 +190,18 @@ mod tests {
127
190
  fn top_level_reference_is_converted_to_name_id() {
128
191
  let mut graph = Graph::new();
129
192
 
130
- let (name_id, _) = nesting_stack_to_name_id(&mut graph, "::CONST", vec!["Foo".into()]).unwrap();
193
+ let tracked_name = name_from_nesting_names(&mut graph, "::CONST", vec!["Foo".into()]).unwrap();
194
+ let name_id = tracked_name.name_id();
131
195
 
132
- let const_name = graph.names().get(&name_id).unwrap();
196
+ let const_name = tracked_name.graph().names().get(&name_id).unwrap();
133
197
  assert_eq!(StringId::from("CONST"), *const_name.str());
134
198
  assert!(const_name.parent_scope().is_top_level());
135
199
 
136
- let foo_name = graph.names().get(&const_name.nesting().unwrap()).unwrap();
200
+ let foo_name = tracked_name
201
+ .graph()
202
+ .names()
203
+ .get(&const_name.nesting().unwrap())
204
+ .unwrap();
137
205
  assert_eq!(StringId::from("Foo"), *foo_name.str());
138
206
  assert!(foo_name.nesting().is_none());
139
207
  assert!(foo_name.parent_scope().is_none());
@@ -143,13 +211,18 @@ mod tests {
143
211
  fn singleton_class_names_use_attached_parent_scope() {
144
212
  let mut graph = Graph::new();
145
213
 
146
- let (name_id, _) = nesting_stack_to_name_id(&mut graph, "CONST", vec!["Foo".into(), "<Foo>".into()]).unwrap();
214
+ let tracked_name = name_from_nesting_names(&mut graph, "CONST", vec!["Foo".into(), "<Foo>".into()]).unwrap();
215
+ let name_id = tracked_name.name_id();
147
216
 
148
- let const_name = graph.names().get(&name_id).unwrap();
217
+ let const_name = tracked_name.graph().names().get(&name_id).unwrap();
149
218
  assert_eq!(StringId::from("CONST"), *const_name.str());
150
219
 
151
220
  // The nesting should be <Foo> with an Attached parent scope
152
- let singleton_name = graph.names().get(&const_name.nesting().unwrap()).unwrap();
221
+ let singleton_name = tracked_name
222
+ .graph()
223
+ .names()
224
+ .get(&const_name.nesting().unwrap())
225
+ .unwrap();
153
226
  assert_eq!(StringId::from("<Foo>"), *singleton_name.str());
154
227
  assert!(
155
228
  matches!(singleton_name.parent_scope(), ParentScope::Attached(_)),
@@ -159,7 +232,7 @@ mod tests {
159
232
 
160
233
  // The attached parent should be Foo
161
234
  let foo_id = singleton_name.parent_scope().expect("Attached should have an id");
162
- let foo_name = graph.names().get(&foo_id).unwrap();
235
+ let foo_name = tracked_name.graph().names().get(&foo_id).unwrap();
163
236
  assert_eq!(StringId::from("Foo"), *foo_name.str());
164
237
  }
165
238
 
@@ -167,19 +240,117 @@ mod tests {
167
240
  fn top_level_nesting_is_converted_to_name_id() {
168
241
  let mut graph = Graph::new();
169
242
 
170
- let (name_id, _) = nesting_stack_to_name_id(&mut graph, "CONST", vec!["Foo".into(), "::Bar".into()]).unwrap();
243
+ let tracked_name = name_from_nesting_names(&mut graph, "CONST", vec!["Foo".into(), "::Bar".into()]).unwrap();
244
+ let name_id = tracked_name.name_id();
171
245
 
172
- let const_name = graph.names().get(&name_id).unwrap();
246
+ let const_name = tracked_name.graph().names().get(&name_id).unwrap();
173
247
  assert_eq!(StringId::from("CONST"), *const_name.str());
174
248
  assert!(const_name.parent_scope().is_none());
175
249
 
176
- let bar_name = graph.names().get(&const_name.nesting().unwrap()).unwrap();
250
+ let bar_name = tracked_name
251
+ .graph()
252
+ .names()
253
+ .get(&const_name.nesting().unwrap())
254
+ .unwrap();
177
255
  assert_eq!(StringId::from("Bar"), *bar_name.str());
178
256
  assert!(bar_name.parent_scope().is_top_level());
179
257
 
180
- let foo_name = graph.names().get(&bar_name.nesting().unwrap()).unwrap();
258
+ let foo_name = tracked_name.graph().names().get(&bar_name.nesting().unwrap()).unwrap();
181
259
  assert_eq!(StringId::from("Foo"), *foo_name.str());
182
260
  assert!(foo_name.parent_scope().is_none());
183
261
  assert!(foo_name.nesting().is_none());
184
262
  }
263
+
264
+ #[test]
265
+ fn invalid_names_restore_name_and_string_counts() {
266
+ let mut graph = Graph::new();
267
+ let name_count = graph.names().len();
268
+ let string_count = graph.strings().len();
269
+
270
+ assert!(name_from_nesting_names(&mut graph, "", vec!["Foo".into()]).is_none());
271
+ assert_eq!(name_count, graph.names().len());
272
+ assert_eq!(string_count, graph.strings().len());
273
+
274
+ assert!(name_from_nesting_names(&mut graph, "Foo::", vec!["Bar".into()]).is_none());
275
+ assert_eq!(name_count, graph.names().len());
276
+ assert_eq!(string_count, graph.strings().len());
277
+
278
+ assert!(name_from_lexical_nesting(&mut graph, "Foo::", None).is_none());
279
+ assert_eq!(name_count, graph.names().len());
280
+ assert_eq!(string_count, graph.strings().len());
281
+ }
282
+
283
+ #[test]
284
+ fn invalid_name_restores_existing_name_reference_counts() {
285
+ let mut graph = Graph::new();
286
+ let string_id = graph.intern_string("Foo".to_owned());
287
+ let name_id = graph.add_name(string_id, ParentScope::None, None);
288
+ let name_ref_count = graph.names().get(&name_id).unwrap().ref_count();
289
+ let string_ref_count = graph.strings().get(&string_id).unwrap().ref_count();
290
+
291
+ assert!(name_from_nesting_names(&mut graph, "", vec!["Foo".into()]).is_none());
292
+
293
+ assert_eq!(name_ref_count, graph.names().get(&name_id).unwrap().ref_count());
294
+ assert_eq!(string_ref_count, graph.strings().get(&string_id).unwrap().ref_count());
295
+ }
296
+
297
+ #[test]
298
+ fn tracker_preserves_duplicate_name_ids() {
299
+ let mut graph = Graph::new();
300
+ let name_count = graph.names().len();
301
+ let string_count = graph.strings().len();
302
+
303
+ {
304
+ let mut tracker = NameTracker::new(&mut graph);
305
+ let first_id = tracker.track_name("Foo", ParentScope::None, None);
306
+ let second_id = tracker.track_name("Foo", ParentScope::None, None);
307
+
308
+ assert_eq!(first_id, second_id);
309
+ assert_eq!(2, tracker.graph.names().get(&first_id).unwrap().ref_count());
310
+ }
311
+
312
+ assert_eq!(name_count, graph.names().len());
313
+ assert_eq!(string_count, graph.strings().len());
314
+ }
315
+
316
+ #[test]
317
+ fn tracked_name_is_untracked_when_scope_ends() {
318
+ let mut graph = Graph::new();
319
+ let name_count = graph.names().len();
320
+ let string_count = graph.strings().len();
321
+
322
+ {
323
+ let tracked_name = name_from_nesting_names(&mut graph, "Bar::CONST", vec!["Foo".into()]).unwrap();
324
+ let name_id = tracked_name.name_id();
325
+ let const_name = tracked_name.graph().names().get(&name_id).unwrap();
326
+
327
+ assert_eq!(
328
+ "CONST",
329
+ tracked_name.graph().strings().get(const_name.str()).unwrap().as_str()
330
+ );
331
+ assert_eq!(name_count + 3, tracked_name.graph().names().len());
332
+ assert_eq!(string_count + 3, tracked_name.graph().strings().len());
333
+ }
334
+
335
+ assert_eq!(name_count, graph.names().len());
336
+ assert_eq!(string_count, graph.strings().len());
337
+ }
338
+
339
+ #[test]
340
+ fn tracked_name_is_untracked_during_unwind() {
341
+ let mut graph = Graph::new();
342
+ let name_count = graph.names().len();
343
+ let string_count = graph.strings().len();
344
+
345
+ let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
346
+ let tracked_name = name_from_nesting_names(&mut graph, "Bar::CONST", vec!["Foo".into()]).unwrap();
347
+
348
+ assert_eq!(name_count + 3, tracked_name.graph().names().len());
349
+ panic!("unwind tracked name");
350
+ }));
351
+
352
+ assert!(result.is_err());
353
+ assert_eq!(name_count, graph.names().len());
354
+ assert_eq!(string_count, graph.strings().len());
355
+ }
185
356
  }