rubydex 0.4.1 → 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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +11 -0
  3. data/THIRD_PARTY_LICENSES.html +13 -24
  4. data/lib/rubydex/cli/command/dead_code.rb +59 -0
  5. data/lib/rubydex/mcp_server/tools/codebase_stats_tool.rb +1 -1
  6. data/lib/rubydex/mcp_server/tools/find_constant_references_tool.rb +4 -4
  7. data/lib/rubydex/mcp_server/tools/find_dead_code_candidates_tool.rb +33 -0
  8. data/lib/rubydex/mcp_server/tools/get_declaration_tool.rb +2 -2
  9. data/lib/rubydex/mcp_server/tools/get_descendants_tool.rb +4 -4
  10. data/lib/rubydex/mcp_server/tools/get_file_declarations_tool.rb +2 -2
  11. data/lib/rubydex/mcp_server/tools/search_declarations_tool.rb +6 -6
  12. data/lib/rubydex/mcp_server.rb +24 -12
  13. data/lib/rubydex/version.rb +1 -1
  14. data/rust/Cargo.lock +17 -11
  15. data/rust/Cargo.toml +3 -0
  16. data/rust/rubydex/Cargo.toml +2 -2
  17. data/rust/rubydex/src/compile_assertions.rs +2 -0
  18. data/rust/rubydex/src/config.rs +1 -1
  19. data/rust/rubydex/src/indexing/rbs_indexer.rs +4 -4
  20. data/rust/rubydex/src/indexing/ruby_indexer.rs +26 -18
  21. data/rust/rubydex/src/indexing/ruby_indexer_tests.rs +42 -2
  22. data/rust/rubydex/src/indexing.rs +6 -6
  23. data/rust/rubydex/src/listing.rs +12 -12
  24. data/rust/rubydex/src/model/built_in.rs +1 -1
  25. data/rust/rubydex/src/model/comment.rs +4 -4
  26. data/rust/rubydex/src/model/declaration.rs +25 -17
  27. data/rust/rubydex/src/model/document.rs +11 -11
  28. data/rust/rubydex/src/model/graph.rs +3 -3
  29. data/rust/rubydex/src/offset.rs +1 -13
  30. data/rust/rubydex/src/operation/ruby_builder.rs +29 -19
  31. data/rust/rubydex/src/query.rs +3 -2
  32. data/rust/rubydex/src/test_utils/graph_test.rs +2 -2
  33. data/rust/rubydex/src/test_utils/local_graph_test.rs +13 -6
  34. data/rust/rubydex-sys/Cargo.toml +2 -2
  35. data/rust/rubydex-sys/src/definition_api.rs +1 -1
  36. data/rust/rubydex-sys/src/graph_api.rs +96 -49
  37. data/rust/rubydex-sys/src/name_api.rs +193 -58
  38. metadata +191 -191
@@ -1,63 +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(graph, &entry, current_nesting, &mut current_name, &mut names_to_untrack);
72
+ process_qualified_name(&mut tracker, &entry, current_nesting, &mut current_name);
17
73
  current_nesting = current_name.as_ref().copied();
18
74
  current_name = ParentScope::None;
19
75
  }
20
76
 
21
- process_qualified_name(
22
- graph,
23
- const_name,
24
- current_nesting,
25
- &mut current_name,
26
- &mut names_to_untrack,
27
- );
77
+ process_qualified_name(&mut tracker, const_name, current_nesting, &mut current_name);
28
78
 
29
79
  let (ParentScope::Some(name_id) | ParentScope::Attached(name_id)) = current_name else {
30
- // Invalid names may leave temporary parts in the graph (e.g. `Foo` from `Foo::`).
31
- for name_id in names_to_untrack {
32
- graph.untrack_name(name_id);
33
- }
34
80
  return None;
35
81
  };
36
82
 
37
- Some((name_id, names_to_untrack))
83
+ Some(TrackedName::new(tracker, name_id))
38
84
  }
39
85
 
40
- /// Takes a constant name and an existing lexical nesting and transforms it into a `NameId`, registering each required
41
- /// part in the graph. Returns the `NameId` and a list of name ids that need to be untracked afterwards.
42
- pub fn name_in_nesting_to_name_id(
43
- graph: &mut Graph,
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,
44
90
  const_name: &str,
45
91
  nesting: Option<NameId>,
46
- ) -> Option<(NameId, Vec<NameId>)> {
92
+ ) -> Option<TrackedName<'graph>> {
47
93
  let mut current_name = ParentScope::None;
48
- let mut names_to_untrack = Vec::new();
94
+ let mut tracker = NameTracker::new(graph);
49
95
 
50
- process_qualified_name(graph, const_name, nesting, &mut current_name, &mut names_to_untrack);
96
+ process_qualified_name(&mut tracker, const_name, nesting, &mut current_name);
51
97
 
52
98
  let (ParentScope::Some(name_id) | ParentScope::Attached(name_id)) = current_name else {
53
- // Invalid names may leave temporary parts in the graph (e.g. `Foo` from `Foo::`).
54
- for name_id in names_to_untrack {
55
- graph.untrack_name(name_id);
56
- }
57
99
  return None;
58
100
  };
59
101
 
60
- Some((name_id, names_to_untrack))
102
+ Some(TrackedName::new(tracker, name_id))
61
103
  }
62
104
 
63
105
  /// Processes a qualified name (e.g., `"Foo::Bar"` or `"<Foo>"`) by splitting on `"::"` and registering each part in the
@@ -66,11 +108,10 @@ pub fn name_in_nesting_to_name_id(
66
108
  /// When a singleton is the first part (i.e., `current_name` has no parent), `current_nesting` is used as the attachment
67
109
  /// point.
68
110
  fn process_qualified_name(
69
- graph: &mut Graph,
111
+ tracker: &mut NameTracker<'_>,
70
112
  qualified_name: &str,
71
113
  current_nesting: Option<NameId>,
72
114
  current_name: &mut ParentScope,
73
- names_to_untrack: &mut Vec<NameId>,
74
115
  ) {
75
116
  for part in qualified_name.split("::") {
76
117
  if part.is_empty() {
@@ -90,9 +131,7 @@ fn process_qualified_name(
90
131
  (*current_name, current_nesting)
91
132
  };
92
133
 
93
- let str_id = graph.intern_string(part.to_owned());
94
- let name_id = graph.add_name(str_id, parent_scope, nesting_for_part);
95
- names_to_untrack.push(name_id);
134
+ let name_id = tracker.track_name(part, parent_scope, nesting_for_part);
96
135
  *current_name = ParentScope::Some(name_id);
97
136
  }
98
137
  }
@@ -107,38 +146,41 @@ mod tests {
107
146
  fn nesting_is_converted_to_name_id() {
108
147
  let mut graph = Graph::new();
109
148
 
110
- let (name_id, _) = nesting_stack_to_name_id(
149
+ let tracked_name = name_from_nesting_names(
111
150
  &mut graph,
112
151
  "Some::CONST",
113
152
  vec!["Foo".into(), "Bar::Zip".into(), "Qux".into()],
114
153
  )
115
154
  .unwrap();
155
+ let name_id = tracked_name.name_id();
116
156
 
117
- let const_name = graph.names().get(&name_id).unwrap();
157
+ let const_name = tracked_name.graph().names().get(&name_id).unwrap();
118
158
  assert_eq!(StringId::from("CONST"), *const_name.str());
119
159
 
120
- let some_name = graph
160
+ let some_name = tracked_name
161
+ .graph()
121
162
  .names()
122
163
  .get(&const_name.parent_scope().expect("Parent scope should exist"))
123
164
  .unwrap();
124
165
  assert_eq!(StringId::from("Some"), *some_name.str());
125
166
  assert_eq!(const_name.nesting(), some_name.nesting());
126
167
 
127
- 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();
128
169
  assert_eq!(StringId::from("Qux"), *qux_name.str());
129
170
  assert!(qux_name.parent_scope().is_none());
130
171
 
131
- 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();
132
173
  assert_eq!(StringId::from("Zip"), *zip_name.str());
133
174
 
134
- let bar_name = graph
175
+ let bar_name = tracked_name
176
+ .graph()
135
177
  .names()
136
178
  .get(&zip_name.parent_scope().expect("Parent scope should exist"))
137
179
  .unwrap();
138
180
  assert_eq!(StringId::from("Bar"), *bar_name.str());
139
181
  assert_eq!(zip_name.nesting(), bar_name.nesting());
140
182
 
141
- 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();
142
184
  assert_eq!(StringId::from("Foo"), *foo_name.str());
143
185
  assert!(foo_name.parent_scope().is_none());
144
186
  assert!(foo_name.nesting().is_none());
@@ -148,13 +190,18 @@ mod tests {
148
190
  fn top_level_reference_is_converted_to_name_id() {
149
191
  let mut graph = Graph::new();
150
192
 
151
- 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();
152
195
 
153
- let const_name = graph.names().get(&name_id).unwrap();
196
+ let const_name = tracked_name.graph().names().get(&name_id).unwrap();
154
197
  assert_eq!(StringId::from("CONST"), *const_name.str());
155
198
  assert!(const_name.parent_scope().is_top_level());
156
199
 
157
- 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();
158
205
  assert_eq!(StringId::from("Foo"), *foo_name.str());
159
206
  assert!(foo_name.nesting().is_none());
160
207
  assert!(foo_name.parent_scope().is_none());
@@ -164,13 +211,18 @@ mod tests {
164
211
  fn singleton_class_names_use_attached_parent_scope() {
165
212
  let mut graph = Graph::new();
166
213
 
167
- 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();
168
216
 
169
- let const_name = graph.names().get(&name_id).unwrap();
217
+ let const_name = tracked_name.graph().names().get(&name_id).unwrap();
170
218
  assert_eq!(StringId::from("CONST"), *const_name.str());
171
219
 
172
220
  // The nesting should be <Foo> with an Attached parent scope
173
- 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();
174
226
  assert_eq!(StringId::from("<Foo>"), *singleton_name.str());
175
227
  assert!(
176
228
  matches!(singleton_name.parent_scope(), ParentScope::Attached(_)),
@@ -180,7 +232,7 @@ mod tests {
180
232
 
181
233
  // The attached parent should be Foo
182
234
  let foo_id = singleton_name.parent_scope().expect("Attached should have an id");
183
- let foo_name = graph.names().get(&foo_id).unwrap();
235
+ let foo_name = tracked_name.graph().names().get(&foo_id).unwrap();
184
236
  assert_eq!(StringId::from("Foo"), *foo_name.str());
185
237
  }
186
238
 
@@ -188,33 +240,116 @@ mod tests {
188
240
  fn top_level_nesting_is_converted_to_name_id() {
189
241
  let mut graph = Graph::new();
190
242
 
191
- 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();
192
245
 
193
- let const_name = graph.names().get(&name_id).unwrap();
246
+ let const_name = tracked_name.graph().names().get(&name_id).unwrap();
194
247
  assert_eq!(StringId::from("CONST"), *const_name.str());
195
248
  assert!(const_name.parent_scope().is_none());
196
249
 
197
- 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();
198
255
  assert_eq!(StringId::from("Bar"), *bar_name.str());
199
256
  assert!(bar_name.parent_scope().is_top_level());
200
257
 
201
- 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();
202
259
  assert_eq!(StringId::from("Foo"), *foo_name.str());
203
260
  assert!(foo_name.parent_scope().is_none());
204
261
  assert!(foo_name.nesting().is_none());
205
262
  }
206
263
 
207
264
  #[test]
208
- fn invalid_names_are_untracked() {
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() {
209
299
  let mut graph = Graph::new();
210
300
  let name_count = graph.names().len();
211
301
  let string_count = graph.strings().len();
212
302
 
213
- assert!(nesting_stack_to_name_id(&mut graph, "Foo::", vec!["Bar".into()]).is_none());
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
+
214
312
  assert_eq!(name_count, graph.names().len());
215
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
+ }));
216
351
 
217
- assert!(name_in_nesting_to_name_id(&mut graph, "Foo::", None).is_none());
352
+ assert!(result.is_err());
218
353
  assert_eq!(name_count, graph.names().len());
219
354
  assert_eq!(string_count, graph.strings().len());
220
355
  }