rubydex 0.3.0 → 0.4.1

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 (96) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +29 -7
  3. data/THIRD_PARTY_LICENSES.html +238 -2
  4. data/exe/rdx +2 -149
  5. data/ext/rubydex/config.c +140 -0
  6. data/ext/rubydex/config.h +16 -0
  7. data/ext/rubydex/definition.c +34 -0
  8. data/ext/rubydex/definition.h +3 -0
  9. data/ext/rubydex/diagnostic.c +28 -1
  10. data/ext/rubydex/diagnostic.h +2 -0
  11. data/ext/rubydex/extconf.rb +4 -2
  12. data/ext/rubydex/graph.c +71 -55
  13. data/ext/rubydex/graph.h +6 -0
  14. data/ext/rubydex/query.c +398 -16
  15. data/ext/rubydex/rubydex.c +2 -0
  16. data/ext/rubydex/utils.c +11 -4
  17. data/lib/ruby_lsp/rubydex/addon.rb +211 -0
  18. data/lib/rubydex/cli/command/console.rb +55 -0
  19. data/lib/rubydex/cli/command/lint/explain.rb +76 -0
  20. data/lib/rubydex/cli/command/lint.rb +208 -0
  21. data/lib/rubydex/cli/command/mcp.rb +30 -0
  22. data/lib/rubydex/cli/command/query.rb +70 -0
  23. data/lib/rubydex/cli/command/skill.rb +69 -0
  24. data/lib/rubydex/cli/command.rb +168 -0
  25. data/lib/rubydex/cli.rb +93 -0
  26. data/lib/rubydex/config.rb +59 -0
  27. data/lib/rubydex/diagnostic.rb +12 -3
  28. data/lib/rubydex/errors.rb +42 -1
  29. data/lib/rubydex/graph.rb +10 -3
  30. data/lib/rubydex/linter/custom_rule.rb +97 -0
  31. data/lib/rubydex/linter/helpers/path_helpers.rb +78 -0
  32. data/lib/rubydex/linter/helpers/source_access_helpers.rb +31 -0
  33. data/lib/rubydex/linter/rule_loader.rb +36 -0
  34. data/lib/rubydex/linter/rule_test_case.rb +343 -0
  35. data/lib/rubydex/linter/runner.rb +56 -0
  36. data/lib/rubydex/linter.rb +19 -0
  37. data/lib/rubydex/location.rb +3 -0
  38. data/lib/rubydex/mcp_server.rb +1 -2
  39. data/lib/rubydex/related_information.rb +17 -0
  40. data/lib/rubydex/rule.rb +33 -0
  41. data/lib/rubydex/rules/dynamic_ancestor.rb +29 -0
  42. data/lib/rubydex/rules/dynamic_constant_reference.rb +25 -0
  43. data/lib/rubydex/rules/dynamic_singleton_definition.rb +30 -0
  44. data/lib/rubydex/rules/invalid_constant_visibility.rb +28 -0
  45. data/lib/rubydex/rules/invalid_method_visibility.rb +28 -0
  46. data/lib/rubydex/rules/parse_error.rb +25 -0
  47. data/lib/rubydex/rules/parse_warning.rb +28 -0
  48. data/lib/rubydex/rules/top_level_mixin_self.rb +27 -0
  49. data/lib/rubydex/rules/undefined_constant_visibility_target.rb +27 -0
  50. data/lib/rubydex/rules/undefined_method_visibility_target.rb +27 -0
  51. data/lib/rubydex/severity.rb +70 -0
  52. data/lib/rubydex/skill.rb +89 -0
  53. data/lib/rubydex/skill_registry.rb +62 -0
  54. data/lib/rubydex/version.rb +1 -1
  55. data/lib/rubydex.rb +8 -0
  56. data/lib/rubydex_linter/rules/rule_structure.rb +140 -0
  57. data/rbi/rubydex.rbi +464 -21
  58. data/rust/Cargo.lock +2 -2
  59. data/rust/rubydex/Cargo.toml +5 -1
  60. data/rust/rubydex/benches/graph_memory.rs +3 -5
  61. data/rust/rubydex/src/bin/generate_ruby_rules.rs +94 -0
  62. data/rust/rubydex/src/config.rs +538 -157
  63. data/rust/rubydex/src/diagnostic.rs +162 -45
  64. data/rust/rubydex/src/errors.rs +0 -1
  65. data/rust/rubydex/src/indexing/local_graph.rs +6 -5
  66. data/rust/rubydex/src/indexing/rbs_indexer.rs +270 -6
  67. data/rust/rubydex/src/indexing/ruby_indexer.rs +10 -12
  68. data/rust/rubydex/src/indexing/ruby_indexer_tests.rs +134 -81
  69. data/rust/rubydex/src/lib.rs +1 -0
  70. data/rust/rubydex/src/listing.rs +26 -1
  71. data/rust/rubydex/src/main.rs +7 -4
  72. data/rust/rubydex/src/model/declaration.rs +302 -219
  73. data/rust/rubydex/src/model/graph.rs +27 -40
  74. data/rust/rubydex/src/model/name.rs +58 -17
  75. data/rust/rubydex/src/operation/ruby_builder.rs +30 -45
  76. data/rust/rubydex/src/path_helpers.rs +77 -0
  77. data/rust/rubydex/src/query/cypher/schema.rs +63 -0
  78. data/rust/rubydex/src/query/cypher/tests.rs +26 -1
  79. data/rust/rubydex/src/query/cypher.rs +8 -11
  80. data/rust/rubydex/src/query.rs +314 -44
  81. data/rust/rubydex/src/resolution.rs +139 -187
  82. data/rust/rubydex/src/resolution_tests.rs +247 -19
  83. data/rust/rubydex/src/test_utils/context.rs +2 -1
  84. data/rust/rubydex/src/test_utils/graph_test.rs +26 -12
  85. data/rust/rubydex/src/test_utils/local_graph_test.rs +19 -0
  86. data/rust/rubydex/tests/cli.rs +4 -4
  87. data/rust/rubydex-sys/src/config_api.rs +205 -0
  88. data/rust/rubydex-sys/src/cypher_api.rs +791 -0
  89. data/rust/rubydex-sys/src/definition_api.rs +68 -1
  90. data/rust/rubydex-sys/src/diagnostic_api.rs +42 -8
  91. data/rust/rubydex-sys/src/graph_api.rs +125 -205
  92. data/rust/rubydex-sys/src/lib.rs +2 -0
  93. data/rust/rubydex-sys/src/name_api.rs +49 -17
  94. data/rust/rubydex-sys/src/utils.rs +37 -0
  95. data/skills/send-private-method/SKILL.md +133 -0
  96. metadata +42 -2
@@ -5,7 +5,7 @@ use std::thread;
5
5
 
6
6
  use url::Url;
7
7
 
8
- use crate::model::built_in::OBJECT_ID;
8
+ use crate::model::built_in::{BUILT_IN_URI_ID, OBJECT_ID};
9
9
  use crate::model::declaration::{Ancestor, Declaration, Namespace};
10
10
  use crate::model::definitions::{Definition, Parameter};
11
11
  use crate::model::graph::Graph;
@@ -833,6 +833,63 @@ pub fn follow_method_alias(graph: &Graph, alias_id: DefinitionId) -> Result<Decl
833
833
  }
834
834
  }
835
835
 
836
+ /// Returns `true` when any definition of `declaration` was seeded by rubydex rather than read from indexed source.
837
+ ///
838
+ /// Dead code candidates are reported at declaration granularity, so a seeded definition disqualifies the declaration
839
+ /// even when user code also reopens it.
840
+ fn is_built_in(graph: &Graph, declaration: &Declaration) -> bool {
841
+ declaration.definitions().iter().any(|definition_id| {
842
+ graph
843
+ .definitions()
844
+ .get(definition_id)
845
+ .is_some_and(|definition| definition.uri_id() == &*BUILT_IN_URI_ID)
846
+ })
847
+ }
848
+
849
+ /// Returns a list of declarations that may be unused in the codebase, which means
850
+ /// that the analysis could not find any references to them. This misses
851
+ /// meta-programming or untyped code, which is why the term candidates is used.
852
+ ///
853
+ /// Currently, only supports constants.
854
+ ///
855
+ /// # Panics
856
+ ///
857
+ /// Will panic if any of the threads panic
858
+ pub fn dead_code_candidates(graph: &Graph) -> Vec<DeclarationId> {
859
+ let num_threads = thread::available_parallelism().map_or(4, std::num::NonZero::get);
860
+ let declarations = graph.declarations();
861
+
862
+ let ids: Vec<DeclarationId> = declarations.keys().copied().collect();
863
+ let chunk_size = ids.len().div_ceil(num_threads);
864
+
865
+ if chunk_size == 0 {
866
+ return Vec::new();
867
+ }
868
+
869
+ thread::scope(|s| {
870
+ let handles: Vec<_> = ids
871
+ .chunks(chunk_size)
872
+ .map(|chunk| {
873
+ s.spawn(|| {
874
+ chunk
875
+ .iter()
876
+ .filter(|id| {
877
+ let decl = declarations.get(id).unwrap();
878
+ decl.constant_references().is_some_and(HashSet::is_empty)
879
+ && !matches!(decl, Declaration::Namespace(Namespace::SingletonClass(_)))
880
+ && !decl.has_no_definitions()
881
+ && !is_built_in(graph, decl)
882
+ })
883
+ .copied()
884
+ .collect::<Vec<_>>()
885
+ })
886
+ })
887
+ .collect();
888
+
889
+ handles.into_iter().flat_map(|h| h.join().unwrap()).collect()
890
+ })
891
+ }
892
+
836
893
  #[cfg(test)]
837
894
  mod tests {
838
895
  use std::str::FromStr;
@@ -920,6 +977,21 @@ mod tests {
920
977
  };
921
978
  }
922
979
 
980
+ macro_rules! assert_dead_code_candidates {
981
+ ($context:expr, [$($expected:expr),* $(,)?]) => {
982
+ let mut actual: Vec<String> = dead_code_candidates($context.graph())
983
+ .iter()
984
+ .map(|id| $context.graph().declarations().get(id).unwrap().name().to_string())
985
+ .collect();
986
+ actual.sort();
987
+
988
+ let mut expected: Vec<String> = vec![$(String::from($expected)),*];
989
+ expected.sort();
990
+
991
+ assert_eq!(expected, actual);
992
+ };
993
+ }
994
+
923
995
  #[test]
924
996
  fn fuzzy_search_returns_partial_matches() {
925
997
  let mut context = GraphTest::new();
@@ -1173,7 +1245,13 @@ mod tests {
1173
1245
  );
1174
1246
  context.resolve();
1175
1247
 
1176
- let name_id = Name::new(StringId::from("Child"), ParentScope::None, None).id();
1248
+ let name_id = Name::new(
1249
+ context.graph().names(),
1250
+ StringId::from("Child"),
1251
+ ParentScope::None,
1252
+ None,
1253
+ )
1254
+ .id();
1177
1255
  assert_declaration_completion_eq!(
1178
1256
  context,
1179
1257
  CompletionReceiver::Expression {
@@ -1222,7 +1300,13 @@ mod tests {
1222
1300
  );
1223
1301
  context.resolve();
1224
1302
 
1225
- let name_id = Name::new(StringId::from("Child"), ParentScope::None, None).id();
1303
+ let name_id = Name::new(
1304
+ context.graph().names(),
1305
+ StringId::from("Child"),
1306
+ ParentScope::None,
1307
+ None,
1308
+ )
1309
+ .id();
1226
1310
  assert_declaration_completion_eq!(
1227
1311
  context,
1228
1312
  CompletionReceiver::Expression {
@@ -1271,7 +1355,7 @@ mod tests {
1271
1355
  );
1272
1356
  context.resolve();
1273
1357
 
1274
- let name_id = Name::new(StringId::from("Foo"), ParentScope::None, None).id();
1358
+ let name_id = Name::new(context.graph().names(), StringId::from("Foo"), ParentScope::None, None).id();
1275
1359
  assert_declaration_completion_eq!(
1276
1360
  context,
1277
1361
  CompletionReceiver::Expression {
@@ -1320,8 +1404,14 @@ mod tests {
1320
1404
  );
1321
1405
  context.resolve();
1322
1406
 
1323
- let foo_id = Name::new(StringId::from("Foo"), ParentScope::None, None).id();
1324
- let name_id = Name::new(StringId::from("<Foo>"), ParentScope::Attached(foo_id), Some(foo_id)).id();
1407
+ let foo_id = Name::new(context.graph().names(), StringId::from("Foo"), ParentScope::None, None).id();
1408
+ let name_id = Name::new(
1409
+ context.graph().names(),
1410
+ StringId::from("<Foo>"),
1411
+ ParentScope::Attached(foo_id),
1412
+ Some(foo_id),
1413
+ )
1414
+ .id();
1325
1415
  assert_declaration_completion_eq!(
1326
1416
  context,
1327
1417
  CompletionReceiver::Expression {
@@ -1341,7 +1431,7 @@ mod tests {
1341
1431
  ]
1342
1432
  );
1343
1433
 
1344
- let name_id = Name::new(StringId::from("Bar"), ParentScope::None, None).id();
1434
+ let name_id = Name::new(context.graph().names(), StringId::from("Bar"), ParentScope::None, None).id();
1345
1435
  assert_declaration_completion_eq!(
1346
1436
  context,
1347
1437
  CompletionReceiver::Expression {
@@ -1383,8 +1473,14 @@ mod tests {
1383
1473
  );
1384
1474
  context.resolve();
1385
1475
 
1386
- let foo_id = Name::new(StringId::from("Foo"), ParentScope::None, None).id();
1387
- let name_id = Name::new(StringId::from("<Foo>"), ParentScope::Attached(foo_id), Some(foo_id)).id();
1476
+ let foo_id = Name::new(context.graph().names(), StringId::from("Foo"), ParentScope::None, None).id();
1477
+ let name_id = Name::new(
1478
+ context.graph().names(),
1479
+ StringId::from("<Foo>"),
1480
+ ParentScope::Attached(foo_id),
1481
+ Some(foo_id),
1482
+ )
1483
+ .id();
1388
1484
 
1389
1485
  assert_declaration_completion_eq!(
1390
1486
  context,
@@ -1431,9 +1527,10 @@ mod tests {
1431
1527
  context.resolve();
1432
1528
 
1433
1529
  let name_id = Name::new(
1530
+ context.graph().names(),
1434
1531
  StringId::from("Bar"),
1435
1532
  ParentScope::TopLevel,
1436
- Some(Name::new(StringId::from("Foo"), ParentScope::None, None).id()),
1533
+ Some(Name::new(context.graph().names(), StringId::from("Foo"), ParentScope::None, None).id()),
1437
1534
  )
1438
1535
  .id();
1439
1536
 
@@ -1457,7 +1554,7 @@ mod tests {
1457
1554
  ]
1458
1555
  );
1459
1556
 
1460
- let name_id = Name::new(StringId::from("Bar"), ParentScope::None, None).id();
1557
+ let name_id = Name::new(context.graph().names(), StringId::from("Bar"), ParentScope::None, None).id();
1461
1558
  assert_declaration_completion_eq!(
1462
1559
  context,
1463
1560
  CompletionReceiver::Expression {
@@ -1498,8 +1595,14 @@ mod tests {
1498
1595
  );
1499
1596
  context.resolve();
1500
1597
 
1501
- let foo_id = Name::new(StringId::from("Foo"), ParentScope::None, None).id();
1502
- let name_id = Name::new(StringId::from("Bar"), ParentScope::None, Some(foo_id)).id();
1598
+ let foo_id = Name::new(context.graph().names(), StringId::from("Foo"), ParentScope::None, None).id();
1599
+ let name_id = Name::new(
1600
+ context.graph().names(),
1601
+ StringId::from("Bar"),
1602
+ ParentScope::None,
1603
+ Some(foo_id),
1604
+ )
1605
+ .id();
1503
1606
  // Foo::CONST is reachable from Foo::Bar through lexical scoping, so it must appear as a completion candidate
1504
1607
  // when the user types the unqualified name CONST
1505
1608
  assert_declaration_completion_eq!(
@@ -1544,9 +1647,10 @@ mod tests {
1544
1647
  context.resolve();
1545
1648
 
1546
1649
  let name_id = Name::new(
1650
+ context.graph().names(),
1547
1651
  StringId::from("Bar"),
1548
1652
  ParentScope::None,
1549
- Some(Name::new(StringId::from("Foo"), ParentScope::None, None).id()),
1653
+ Some(Name::new(context.graph().names(), StringId::from("Foo"), ParentScope::None, None).id()),
1550
1654
  )
1551
1655
  .id();
1552
1656
  assert_declaration_completion_eq!(
@@ -2078,7 +2182,7 @@ mod tests {
2078
2182
  );
2079
2183
  context.resolve();
2080
2184
 
2081
- let name_id = Name::new(StringId::from("Foo"), ParentScope::None, None).id();
2185
+ let name_id = Name::new(context.graph().names(), StringId::from("Foo"), ParentScope::None, None).id();
2082
2186
  assert_declaration_completion_eq!(
2083
2187
  context,
2084
2188
  CompletionReceiver::MethodArgument {
@@ -2119,7 +2223,7 @@ mod tests {
2119
2223
  );
2120
2224
  context.resolve();
2121
2225
 
2122
- let name_id = Name::new(StringId::from("Foo"), ParentScope::None, None).id();
2226
+ let name_id = Name::new(context.graph().names(), StringId::from("Foo"), ParentScope::None, None).id();
2123
2227
  assert_declaration_completion_eq!(
2124
2228
  context,
2125
2229
  CompletionReceiver::MethodArgument {
@@ -2156,7 +2260,7 @@ mod tests {
2156
2260
  );
2157
2261
  context.resolve();
2158
2262
 
2159
- let name_id = Name::new(StringId::from("Foo"), ParentScope::None, None).id();
2263
+ let name_id = Name::new(context.graph().names(), StringId::from("Foo"), ParentScope::None, None).id();
2160
2264
  assert_declaration_completion_eq!(
2161
2265
  context,
2162
2266
  CompletionReceiver::MethodArgument {
@@ -2182,7 +2286,7 @@ mod tests {
2182
2286
  );
2183
2287
  context.resolve();
2184
2288
 
2185
- let name_id = Name::new(StringId::from("Foo"), ParentScope::None, None).id();
2289
+ let name_id = Name::new(context.graph().names(), StringId::from("Foo"), ParentScope::None, None).id();
2186
2290
  assert_declaration_completion_eq!(
2187
2291
  context,
2188
2292
  CompletionReceiver::MethodArgument {
@@ -2226,7 +2330,7 @@ mod tests {
2226
2330
  );
2227
2331
  context.resolve();
2228
2332
 
2229
- let name_id = Name::new(StringId::from("Foo"), ParentScope::None, None).id();
2333
+ let name_id = Name::new(context.graph().names(), StringId::from("Foo"), ParentScope::None, None).id();
2230
2334
  assert_declaration_completion_eq!(
2231
2335
  context,
2232
2336
  CompletionReceiver::MethodArgument {
@@ -2254,7 +2358,7 @@ mod tests {
2254
2358
  context.index_uri("file:///foo.rb", "class Foo; end");
2255
2359
  context.resolve();
2256
2360
 
2257
- let name_id = Name::new(StringId::from("Foo"), ParentScope::None, None).id();
2361
+ let name_id = Name::new(context.graph().names(), StringId::from("Foo"), ParentScope::None, None).id();
2258
2362
  assert_completion_eq!(
2259
2363
  context,
2260
2364
  CompletionReceiver::Expression {
@@ -2319,7 +2423,7 @@ mod tests {
2319
2423
  context.index_uri("file:///foo.rb", "class Foo; def bar(name:); end; end");
2320
2424
  context.resolve();
2321
2425
 
2322
- let name_id = Name::new(StringId::from("Foo"), ParentScope::None, None).id();
2426
+ let name_id = Name::new(context.graph().names(), StringId::from("Foo"), ParentScope::None, None).id();
2323
2427
  assert_completion_eq!(
2324
2428
  context,
2325
2429
  CompletionReceiver::MethodArgument {
@@ -2440,7 +2544,13 @@ mod tests {
2440
2544
  );
2441
2545
  context.resolve();
2442
2546
 
2443
- let outer_name_id = Name::new(StringId::from("Outer"), ParentScope::None, None).id();
2547
+ let outer_name_id = Name::new(
2548
+ context.graph().names(),
2549
+ StringId::from("Outer"),
2550
+ ParentScope::None,
2551
+ None,
2552
+ )
2553
+ .id();
2444
2554
  assert_declaration_completion_eq!(
2445
2555
  context,
2446
2556
  CompletionReceiver::Expression {
@@ -2479,7 +2589,13 @@ mod tests {
2479
2589
  );
2480
2590
  context.resolve();
2481
2591
 
2482
- let name_id = Name::new(StringId::from("Outer"), ParentScope::None, None).id();
2592
+ let name_id = Name::new(
2593
+ context.graph().names(),
2594
+ StringId::from("Outer"),
2595
+ ParentScope::None,
2596
+ None,
2597
+ )
2598
+ .id();
2483
2599
  // `self_decl_id` points to the alias `Outer::MyAlias`, which is a `ConstantAlias` rather than a `Namespace`.
2484
2600
  // The completion should still collect members from the aliased namespace (`Outer::Original`) instead of
2485
2601
  // returning an error, so callers do not have to unwrap aliases themselves.
@@ -2525,7 +2641,7 @@ mod tests {
2525
2641
  );
2526
2642
  context.resolve();
2527
2643
 
2528
- let name_id = Name::new(StringId::from("Bar"), ParentScope::None, None).id();
2644
+ let name_id = Name::new(context.graph().names(), StringId::from("Bar"), ParentScope::None, None).id();
2529
2645
  assert_declaration_completion_eq!(
2530
2646
  context,
2531
2647
  CompletionReceiver::Expression {
@@ -2568,7 +2684,7 @@ mod tests {
2568
2684
  );
2569
2685
  context.resolve();
2570
2686
 
2571
- let name_id = Name::new(StringId::from("Bar"), ParentScope::None, None).id();
2687
+ let name_id = Name::new(context.graph().names(), StringId::from("Bar"), ParentScope::None, None).id();
2572
2688
  assert_declaration_completion_eq!(
2573
2689
  context,
2574
2690
  CompletionReceiver::Expression {
@@ -2613,7 +2729,7 @@ mod tests {
2613
2729
  );
2614
2730
  context.resolve();
2615
2731
 
2616
- let name_id = Name::new(StringId::from("Bar"), ParentScope::None, None).id();
2732
+ let name_id = Name::new(context.graph().names(), StringId::from("Bar"), ParentScope::None, None).id();
2617
2733
  assert_declaration_completion_eq!(
2618
2734
  context,
2619
2735
  CompletionReceiver::Expression {
@@ -2650,7 +2766,13 @@ mod tests {
2650
2766
  );
2651
2767
  context.resolve();
2652
2768
 
2653
- let name_id = Name::new(StringId::from("Object"), ParentScope::None, None).id();
2769
+ let name_id = Name::new(
2770
+ context.graph().names(),
2771
+ StringId::from("Object"),
2772
+ ParentScope::None,
2773
+ None,
2774
+ )
2775
+ .id();
2654
2776
  assert_declaration_completion_eq!(
2655
2777
  context,
2656
2778
  CompletionReceiver::Expression {
@@ -2677,7 +2799,7 @@ mod tests {
2677
2799
  );
2678
2800
  context.resolve();
2679
2801
 
2680
- let name_id = Name::new(StringId::from("Mod"), ParentScope::None, None).id();
2802
+ let name_id = Name::new(context.graph().names(), StringId::from("Mod"), ParentScope::None, None).id();
2681
2803
  assert_declaration_completion_eq!(
2682
2804
  context,
2683
2805
  CompletionReceiver::Expression {
@@ -2715,7 +2837,7 @@ mod tests {
2715
2837
  );
2716
2838
  context.resolve();
2717
2839
 
2718
- let name_id = Name::new(StringId::from("Mod"), ParentScope::None, None).id();
2840
+ let name_id = Name::new(context.graph().names(), StringId::from("Mod"), ParentScope::None, None).id();
2719
2841
  assert_declaration_completion_eq!(
2720
2842
  context,
2721
2843
  CompletionReceiver::Expression {
@@ -2750,7 +2872,13 @@ mod tests {
2750
2872
  );
2751
2873
  context.resolve();
2752
2874
 
2753
- let name_id = Name::new(StringId::from("Object"), ParentScope::None, None).id();
2875
+ let name_id = Name::new(
2876
+ context.graph().names(),
2877
+ StringId::from("Object"),
2878
+ ParentScope::None,
2879
+ None,
2880
+ )
2881
+ .id();
2754
2882
  assert_declaration_completion_eq!(
2755
2883
  context,
2756
2884
  CompletionReceiver::Expression {
@@ -2785,7 +2913,7 @@ mod tests {
2785
2913
  );
2786
2914
  context.resolve();
2787
2915
 
2788
- let name_id = Name::new(StringId::from("Bar"), ParentScope::None, None).id();
2916
+ let name_id = Name::new(context.graph().names(), StringId::from("Bar"), ParentScope::None, None).id();
2789
2917
  assert_declaration_completion_eq!(
2790
2918
  context,
2791
2919
  CompletionReceiver::Expression {
@@ -2818,9 +2946,21 @@ mod tests {
2818
2946
  );
2819
2947
  context.resolve();
2820
2948
 
2821
- let bar_id = Name::new(StringId::from("Bar"), ParentScope::None, None).id();
2822
- let foo_ref_id = Name::new(StringId::from("Foo"), ParentScope::None, Some(bar_id)).id();
2823
- let nesting_name_id = Name::new(StringId::from("<Foo>"), ParentScope::Attached(foo_ref_id), Some(bar_id)).id();
2949
+ let bar_id = Name::new(context.graph().names(), StringId::from("Bar"), ParentScope::None, None).id();
2950
+ let foo_ref_id = Name::new(
2951
+ context.graph().names(),
2952
+ StringId::from("Foo"),
2953
+ ParentScope::None,
2954
+ Some(bar_id),
2955
+ )
2956
+ .id();
2957
+ let nesting_name_id = Name::new(
2958
+ context.graph().names(),
2959
+ StringId::from("<Foo>"),
2960
+ ParentScope::Attached(foo_ref_id),
2961
+ Some(bar_id),
2962
+ )
2963
+ .id();
2824
2964
 
2825
2965
  assert_declaration_completion_eq!(
2826
2966
  context,
@@ -2859,7 +2999,7 @@ mod tests {
2859
2999
  );
2860
3000
  context.resolve();
2861
3001
 
2862
- let name_id = Name::new(StringId::from("Mod"), ParentScope::None, None).id();
3002
+ let name_id = Name::new(context.graph().names(), StringId::from("Mod"), ParentScope::None, None).id();
2863
3003
  assert_declaration_completion_eq!(
2864
3004
  context,
2865
3005
  CompletionReceiver::Expression {
@@ -2897,8 +3037,14 @@ mod tests {
2897
3037
  );
2898
3038
  context.resolve();
2899
3039
 
2900
- let foo_ref_id = Name::new(StringId::from("Foo"), ParentScope::None, None).id();
2901
- let nesting_name_id = Name::new(StringId::from("<Foo>"), ParentScope::Attached(foo_ref_id), None).id();
3040
+ let foo_ref_id = Name::new(context.graph().names(), StringId::from("Foo"), ParentScope::None, None).id();
3041
+ let nesting_name_id = Name::new(
3042
+ context.graph().names(),
3043
+ StringId::from("<Foo>"),
3044
+ ParentScope::Attached(foo_ref_id),
3045
+ None,
3046
+ )
3047
+ .id();
2902
3048
 
2903
3049
  assert_declaration_completion_eq!(
2904
3050
  context,
@@ -2923,7 +3069,7 @@ mod tests {
2923
3069
  );
2924
3070
  context.resolve();
2925
3071
 
2926
- let name_id = Name::new(StringId::from("Foo"), ParentScope::None, None).id();
3072
+ let name_id = Name::new(context.graph().names(), StringId::from("Foo"), ParentScope::None, None).id();
2927
3073
  let result = completion_candidates(
2928
3074
  context.graph(),
2929
3075
  CompletionContext::new(CompletionReceiver::Expression {
@@ -2949,7 +3095,7 @@ mod tests {
2949
3095
  );
2950
3096
  context.resolve();
2951
3097
 
2952
- let name_id = Name::new(StringId::from("Foo"), ParentScope::None, None).id();
3098
+ let name_id = Name::new(context.graph().names(), StringId::from("Foo"), ParentScope::None, None).id();
2953
3099
  let result = completion_candidates(
2954
3100
  context.graph(),
2955
3101
  CompletionContext::new(CompletionReceiver::Expression {
@@ -2981,7 +3127,13 @@ mod tests {
2981
3127
  );
2982
3128
  context.resolve();
2983
3129
 
2984
- let name_id = Name::new(StringId::from("Outer"), ParentScope::None, None).id();
3130
+ let name_id = Name::new(
3131
+ context.graph().names(),
3132
+ StringId::from("Outer"),
3133
+ ParentScope::None,
3134
+ None,
3135
+ )
3136
+ .id();
2985
3137
  assert_declaration_completion_eq!(
2986
3138
  context,
2987
3139
  CompletionReceiver::Expression {
@@ -3024,7 +3176,7 @@ mod tests {
3024
3176
  );
3025
3177
  context.resolve();
3026
3178
 
3027
- let name_id = Name::new(StringId::from("Bar"), ParentScope::None, None).id();
3179
+ let name_id = Name::new(context.graph().names(), StringId::from("Bar"), ParentScope::None, None).id();
3028
3180
  assert_declaration_completion_eq!(
3029
3181
  context,
3030
3182
  CompletionReceiver::Expression {
@@ -3097,7 +3249,7 @@ mod tests {
3097
3249
  );
3098
3250
  context.resolve();
3099
3251
 
3100
- let foo_name_id = Name::new(StringId::from("Foo"), ParentScope::None, None).id();
3252
+ let foo_name_id = Name::new(context.graph().names(), StringId::from("Foo"), ParentScope::None, None).id();
3101
3253
  assert_declaration_completion_eq!(
3102
3254
  context,
3103
3255
  CompletionReceiver::Expression {
@@ -3117,7 +3269,7 @@ mod tests {
3117
3269
  ]
3118
3270
  );
3119
3271
 
3120
- let bar_name_id = Name::new(StringId::from("Bar"), ParentScope::None, None).id();
3272
+ let bar_name_id = Name::new(context.graph().names(), StringId::from("Bar"), ParentScope::None, None).id();
3121
3273
  assert_declaration_completion_eq!(
3122
3274
  context,
3123
3275
  CompletionReceiver::Expression {
@@ -3433,7 +3585,7 @@ mod tests {
3433
3585
  );
3434
3586
  context.resolve();
3435
3587
 
3436
- let foo_name_id = Name::new(StringId::from("Foo"), ParentScope::None, None).id();
3588
+ let foo_name_id = Name::new(context.graph().names(), StringId::from("Foo"), ParentScope::None, None).id();
3437
3589
  assert_declaration_completion_eq!(
3438
3590
  context,
3439
3591
  CompletionReceiver::Expression {
@@ -3902,4 +4054,122 @@ mod tests {
3902
4054
  Err(FindMemberError::DeclarationNotFound),
3903
4055
  );
3904
4056
  }
4057
+
4058
+ #[test]
4059
+ fn dead_code_candidates_returns_unreferenced_constants() {
4060
+ let mut context = GraphTest::new();
4061
+ context.index_uri(
4062
+ "file:///foo.rb",
4063
+ r"
4064
+ class UnusedClass; end
4065
+ module UnusedModule; end
4066
+ UNUSED_CONSTANT = 1
4067
+ ",
4068
+ );
4069
+ context.resolve();
4070
+
4071
+ assert_dead_code_candidates!(context, ["UnusedClass", "UnusedModule", "UNUSED_CONSTANT"]);
4072
+ }
4073
+
4074
+ #[test]
4075
+ fn dead_code_candidates_excludes_referenced_constants() {
4076
+ let mut context = GraphTest::new();
4077
+ context.index_uri(
4078
+ "file:///foo.rb",
4079
+ r"
4080
+ class UsedClass; end
4081
+ USED_CONSTANT = 1
4082
+ ",
4083
+ );
4084
+ context.index_uri(
4085
+ "file:///bar.rb",
4086
+ r"
4087
+ UsedClass
4088
+ USED_CONSTANT
4089
+ ",
4090
+ );
4091
+ context.resolve();
4092
+
4093
+ assert_dead_code_candidates!(context, []);
4094
+ }
4095
+
4096
+ #[test]
4097
+ fn dead_code_candidates_excludes_methods_and_variables() {
4098
+ let mut context = GraphTest::new();
4099
+ context.index_uri(
4100
+ "file:///foo.rb",
4101
+ r"
4102
+ $global_var = 1
4103
+
4104
+ class Holder
4105
+ @@class_var = 1
4106
+
4107
+ def initialize
4108
+ @instance_var = 1
4109
+ end
4110
+
4111
+ def never_called; end
4112
+ end
4113
+ ",
4114
+ );
4115
+ context.resolve();
4116
+
4117
+ assert_dead_code_candidates!(context, ["Holder"]);
4118
+ }
4119
+
4120
+ #[test]
4121
+ fn dead_code_candidates_counts_references_rather_than_reachability() {
4122
+ let mut context = GraphTest::new();
4123
+ context.index_uri(
4124
+ "file:///foo.rb",
4125
+ r"
4126
+ class Target; end
4127
+ AliasName = Target
4128
+ ",
4129
+ );
4130
+ context.resolve();
4131
+
4132
+ // `AliasName` is itself dead, but it still references `Target`, giving `Target` a non-zero count.
4133
+ // A single pass only peels the outermost layer of a dead subgraph.
4134
+ assert_dead_code_candidates!(context, ["AliasName"]);
4135
+ }
4136
+
4137
+ #[test]
4138
+ fn dead_code_candidates_excludes_built_ins_and_definitionless_declarations() {
4139
+ let mut context = GraphTest::new();
4140
+ context.index_uri(
4141
+ "file:///foo.rb",
4142
+ r"
4143
+ class Unused
4144
+ def self.class_method; end
4145
+ end
4146
+
4147
+ class Class
4148
+ end
4149
+ ",
4150
+ );
4151
+ context.resolve();
4152
+
4153
+ // A reopened built-in remains excluded regardless of definition order.
4154
+ assert_dead_code_candidates!(context, ["Unused"]);
4155
+ }
4156
+
4157
+ #[test]
4158
+ fn dead_code_candidates_excludes_singleton_classes() {
4159
+ let mut context = GraphTest::new();
4160
+ context.index_uri(
4161
+ "file:///foo.rb",
4162
+ r"
4163
+ class Unused; end
4164
+ class Attached; end
4165
+
4166
+ class << Attached
4167
+ def never_called; end
4168
+ end
4169
+ ",
4170
+ );
4171
+ context.resolve();
4172
+
4173
+ assert_dead_code_candidates!(context, ["Unused"]);
4174
+ }
3905
4175
  }