rubydex 0.2.5 → 0.2.6

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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +17 -16
  3. data/THIRD_PARTY_LICENSES.html +6 -6
  4. data/ext/rubydex/definition.c +33 -2
  5. data/ext/rubydex/document.c +36 -0
  6. data/ext/rubydex/graph.c +32 -18
  7. data/ext/rubydex/handle.h +21 -5
  8. data/lib/rubydex/bin/rubydex_mcp.exe +0 -0
  9. data/lib/rubydex/errors.rb +8 -0
  10. data/lib/rubydex/location.rb +24 -0
  11. data/lib/rubydex/version.rb +1 -1
  12. data/lib/rubydex.rb +1 -0
  13. data/rbi/rubydex.rbi +29 -12
  14. data/rust/Cargo.lock +3 -3
  15. data/rust/rubydex/Cargo.toml +7 -1
  16. data/rust/rubydex/src/dot.rs +609 -0
  17. data/rust/rubydex/src/indexing/rbs_indexer.rs +19 -1
  18. data/rust/rubydex/src/indexing/ruby_indexer.rs +4 -0
  19. data/rust/rubydex/src/lib.rs +1 -1
  20. data/rust/rubydex/src/main.rs +8 -5
  21. data/rust/rubydex/src/model/built_in.rs +5 -2
  22. data/rust/rubydex/src/model/comment.rs +2 -0
  23. data/rust/rubydex/src/model/declaration.rs +1 -0
  24. data/rust/rubydex/src/model/definitions.rs +13 -1
  25. data/rust/rubydex/src/model/document.rs +2 -0
  26. data/rust/rubydex/src/model/encoding.rs +2 -0
  27. data/rust/rubydex/src/model/graph.rs +51 -13
  28. data/rust/rubydex/src/model/identity_maps.rs +3 -0
  29. data/rust/rubydex/src/model/keywords.rs +3 -0
  30. data/rust/rubydex/src/model/name.rs +2 -0
  31. data/rust/rubydex/src/model/string_ref.rs +2 -0
  32. data/rust/rubydex/src/model/visibility.rs +3 -0
  33. data/rust/rubydex/src/operation/applier.rs +1 -0
  34. data/rust/rubydex/src/operation/mod.rs +1 -0
  35. data/rust/rubydex/src/operation/ruby_builder.rs +4 -0
  36. data/rust/rubydex/src/query.rs +114 -33
  37. data/rust/rubydex/src/resolution.rs +16 -8
  38. data/rust/rubydex/src/resolution_tests.rs +132 -0
  39. data/rust/rubydex/tests/cli.rs +17 -61
  40. data/rust/rubydex-mcp/Cargo.toml +9 -3
  41. data/rust/rubydex-sys/Cargo.toml +9 -2
  42. data/rust/rubydex-sys/src/definition_api.rs +72 -2
  43. data/rust/rubydex-sys/src/document_api.rs +28 -0
  44. data/rust/rubydex-sys/src/graph_api.rs +1 -3
  45. metadata +4 -4
  46. data/rust/rubydex/src/visualization/dot.rs +0 -192
  47. data/rust/rubydex/src/visualization.rs +0 -6
@@ -454,20 +454,7 @@ fn expression_completion<'a>(
454
454
  let NameRef::Resolved(name_ref) = name_ref else {
455
455
  return Err(format!("Expected name {nesting_name_id} to be resolved").into());
456
456
  };
457
- // When no explicit self is given, self is the innermost lexical scope (the nesting's own declaration).
458
- // When explicit, follow constant aliases so callers can pass whatever the expression that set self
459
- // resolves to without having to unwrap aliases themselves. Missing or non-namespace decls are graph
460
- // inconsistencies and surfaced as errors.
461
- let resolved_self_decl_id = match self_decl_id {
462
- Some(id) => resolve_self_namespace(graph, id)?,
463
- None => *name_ref.declaration_id(),
464
- };
465
- let self_decl = graph
466
- .declarations()
467
- .get(&resolved_self_decl_id)
468
- .unwrap()
469
- .as_namespace()
470
- .ok_or("Expected associated declaration to be a namespace")?;
457
+
471
458
  let innermost_lexical_decl = graph
472
459
  .declarations()
473
460
  .get(name_ref.declaration_id())
@@ -496,7 +483,16 @@ fn expression_completion<'a>(
496
483
 
497
484
  // Collect methods and instance variables, which are based on the inheritance chain of the `self` type (which may
498
485
  // not match the immediate lexical scope)
499
- collect_methods_and_ivars_from_self(graph, self_decl, &mut context, &mut candidates);
486
+ if let Some(self_decl_id) = self_decl_id.map(|id| resolve_self_namespace(graph, id)).transpose()? {
487
+ let self_decl = graph
488
+ .declarations()
489
+ .get(&self_decl_id)
490
+ .unwrap()
491
+ .as_namespace()
492
+ .ok_or("Expected associated declaration to be a namespace")?;
493
+
494
+ collect_methods_and_ivars_from_self(graph, self_decl, &mut context, &mut candidates);
495
+ }
500
496
 
501
497
  // Keywords are always available in expression contexts
502
498
  candidates.extend(keywords::KEYWORDS.iter().map(CompletionCandidate::Keyword));
@@ -1120,7 +1116,7 @@ mod tests {
1120
1116
  assert_declaration_completion_eq!(
1121
1117
  context,
1122
1118
  CompletionReceiver::Expression {
1123
- self_decl_id: None,
1119
+ self_decl_id: Some(DeclarationId::from("Child")),
1124
1120
  nesting_name_id: name_id,
1125
1121
  },
1126
1122
  [
@@ -1169,7 +1165,7 @@ mod tests {
1169
1165
  assert_declaration_completion_eq!(
1170
1166
  context,
1171
1167
  CompletionReceiver::Expression {
1172
- self_decl_id: None,
1168
+ self_decl_id: Some(DeclarationId::from("Child")),
1173
1169
  nesting_name_id: name_id,
1174
1170
  },
1175
1171
  [
@@ -1218,7 +1214,7 @@ mod tests {
1218
1214
  assert_declaration_completion_eq!(
1219
1215
  context,
1220
1216
  CompletionReceiver::Expression {
1221
- self_decl_id: None,
1217
+ self_decl_id: Some(DeclarationId::from("Foo")),
1222
1218
  nesting_name_id: name_id,
1223
1219
  },
1224
1220
  [
@@ -1268,7 +1264,7 @@ mod tests {
1268
1264
  assert_declaration_completion_eq!(
1269
1265
  context,
1270
1266
  CompletionReceiver::Expression {
1271
- self_decl_id: None,
1267
+ self_decl_id: Some(DeclarationId::from("Foo::<Foo>")),
1272
1268
  nesting_name_id: name_id,
1273
1269
  },
1274
1270
  [
@@ -1288,7 +1284,7 @@ mod tests {
1288
1284
  assert_declaration_completion_eq!(
1289
1285
  context,
1290
1286
  CompletionReceiver::Expression {
1291
- self_decl_id: None,
1287
+ self_decl_id: Some(DeclarationId::from("Bar")),
1292
1288
  nesting_name_id: name_id,
1293
1289
  },
1294
1290
  [
@@ -1305,6 +1301,48 @@ mod tests {
1305
1301
  );
1306
1302
  }
1307
1303
 
1304
+ #[test]
1305
+ fn completion_candidates_for_instance_variables_inside_singleton_class_body() {
1306
+ let mut context = GraphTest::new();
1307
+ context.index_uri(
1308
+ "file:///foo.rb",
1309
+ "
1310
+ class Foo
1311
+ @class_level_ivar = 1
1312
+
1313
+ def initialize
1314
+ @instance_level_ivar = 1
1315
+ end
1316
+
1317
+ class << self
1318
+ @singleton_level_ivar = 1
1319
+ end
1320
+ end
1321
+ ",
1322
+ );
1323
+ context.resolve();
1324
+
1325
+ let foo_id = Name::new(StringId::from("Foo"), ParentScope::None, None).id();
1326
+ let name_id = Name::new(StringId::from("<Foo>"), ParentScope::Attached(foo_id), Some(foo_id)).id();
1327
+
1328
+ assert_declaration_completion_eq!(
1329
+ context,
1330
+ CompletionReceiver::Expression {
1331
+ self_decl_id: Some(DeclarationId::from("Foo::<Foo>::<<Foo>>")),
1332
+ nesting_name_id: name_id,
1333
+ },
1334
+ [
1335
+ "Module",
1336
+ "Class",
1337
+ "Object",
1338
+ "BasicObject",
1339
+ "Kernel",
1340
+ "Foo",
1341
+ "Foo::<Foo>::<<Foo>>#@singleton_level_ivar"
1342
+ ]
1343
+ );
1344
+ }
1345
+
1308
1346
  #[test]
1309
1347
  fn completion_candidates_includes_constants_accessible_within_lexical_scope() {
1310
1348
  let mut context = GraphTest::new();
@@ -1337,10 +1375,11 @@ mod tests {
1337
1375
  Some(Name::new(StringId::from("Foo"), ParentScope::None, None).id()),
1338
1376
  )
1339
1377
  .id();
1378
+
1340
1379
  assert_declaration_completion_eq!(
1341
1380
  context,
1342
1381
  CompletionReceiver::Expression {
1343
- self_decl_id: None,
1382
+ self_decl_id: Some(DeclarationId::from("Bar")),
1344
1383
  nesting_name_id: name_id,
1345
1384
  },
1346
1385
  [
@@ -1361,7 +1400,7 @@ mod tests {
1361
1400
  assert_declaration_completion_eq!(
1362
1401
  context,
1363
1402
  CompletionReceiver::Expression {
1364
- self_decl_id: None,
1403
+ self_decl_id: Some(DeclarationId::from("Bar")),
1365
1404
  nesting_name_id: name_id,
1366
1405
  },
1367
1406
  [
@@ -1405,7 +1444,7 @@ mod tests {
1405
1444
  assert_declaration_completion_eq!(
1406
1445
  context,
1407
1446
  CompletionReceiver::Expression {
1408
- self_decl_id: None,
1447
+ self_decl_id: Some(DeclarationId::from("Foo::Bar")),
1409
1448
  nesting_name_id: name_id,
1410
1449
  },
1411
1450
  [
@@ -1452,7 +1491,7 @@ mod tests {
1452
1491
  assert_declaration_completion_eq!(
1453
1492
  context,
1454
1493
  CompletionReceiver::Expression {
1455
- self_decl_id: None,
1494
+ self_decl_id: Some(DeclarationId::from("Foo::Bar")),
1456
1495
  nesting_name_id: name_id,
1457
1496
  },
1458
1497
  ["Foo::Bar", "$var2", "$var", "Foo::Bar#bar_m()"]
@@ -1982,7 +2021,7 @@ mod tests {
1982
2021
  assert_declaration_completion_eq!(
1983
2022
  context,
1984
2023
  CompletionReceiver::MethodArgument {
1985
- self_decl_id: None,
2024
+ self_decl_id: Some(DeclarationId::from("Foo")),
1986
2025
  nesting_name_id: name_id,
1987
2026
  method_decl_id: DeclarationId::from("Foo#greet()"),
1988
2027
  },
@@ -2000,6 +2039,48 @@ mod tests {
2000
2039
  );
2001
2040
  }
2002
2041
 
2042
+ #[test]
2043
+ fn method_argument_in_body_completion_uses_singleton_self() {
2044
+ let mut context = GraphTest::new();
2045
+ context.index_uri(
2046
+ "file:///foo.rb",
2047
+ "
2048
+ class Foo
2049
+ @class_level_ivar = 1
2050
+
2051
+ def instance_method; end
2052
+
2053
+ def self.configure(name:, label: 'default'); end
2054
+
2055
+ # `configure(...)` is invoked at class body level — cursor inside the args.
2056
+ end
2057
+ ",
2058
+ );
2059
+ context.resolve();
2060
+
2061
+ let name_id = Name::new(StringId::from("Foo"), ParentScope::None, None).id();
2062
+ assert_declaration_completion_eq!(
2063
+ context,
2064
+ CompletionReceiver::MethodArgument {
2065
+ self_decl_id: Some(DeclarationId::from("Foo::<Foo>")),
2066
+ nesting_name_id: name_id,
2067
+ method_decl_id: DeclarationId::from("Foo::<Foo>#configure()"),
2068
+ },
2069
+ [
2070
+ "Module",
2071
+ "Class",
2072
+ "Object",
2073
+ "BasicObject",
2074
+ "Kernel",
2075
+ "Foo",
2076
+ "Foo::<Foo>#configure()",
2077
+ "Foo::<Foo>#@class_level_ivar",
2078
+ "name:",
2079
+ "label:"
2080
+ ]
2081
+ );
2082
+ }
2083
+
2003
2084
  #[test]
2004
2085
  fn method_argument_completion_no_keyword_params() {
2005
2086
  let mut context = GraphTest::new();
@@ -2018,7 +2099,7 @@ mod tests {
2018
2099
  assert_declaration_completion_eq!(
2019
2100
  context,
2020
2101
  CompletionReceiver::MethodArgument {
2021
- self_decl_id: None,
2102
+ self_decl_id: Some(DeclarationId::from("Foo")),
2022
2103
  nesting_name_id: name_id,
2023
2104
  method_decl_id: DeclarationId::from("Foo#bar()"),
2024
2105
  },
@@ -2044,7 +2125,7 @@ mod tests {
2044
2125
  assert_declaration_completion_eq!(
2045
2126
  context,
2046
2127
  CompletionReceiver::MethodArgument {
2047
- self_decl_id: None,
2128
+ self_decl_id: Some(DeclarationId::from("Foo")),
2048
2129
  nesting_name_id: name_id,
2049
2130
  method_decl_id: DeclarationId::from("Foo#search()"),
2050
2131
  },
@@ -2088,7 +2169,7 @@ mod tests {
2088
2169
  assert_declaration_completion_eq!(
2089
2170
  context,
2090
2171
  CompletionReceiver::MethodArgument {
2091
- self_decl_id: None,
2172
+ self_decl_id: Some(DeclarationId::from("Foo")),
2092
2173
  nesting_name_id: name_id,
2093
2174
  method_decl_id: DeclarationId::from("Foo#bar()"),
2094
2175
  },
@@ -2181,7 +2262,7 @@ mod tests {
2181
2262
  assert_completion_eq!(
2182
2263
  context,
2183
2264
  CompletionReceiver::MethodArgument {
2184
- self_decl_id: None,
2265
+ self_decl_id: Some(DeclarationId::from("Foo")),
2185
2266
  nesting_name_id: name_id,
2186
2267
  method_decl_id: DeclarationId::from("Foo#bar()"),
2187
2268
  },
@@ -2612,7 +2693,7 @@ mod tests {
2612
2693
  assert_declaration_completion_eq!(
2613
2694
  context,
2614
2695
  CompletionReceiver::Expression {
2615
- self_decl_id: None,
2696
+ self_decl_id: Some(DeclarationId::from("Object")),
2616
2697
  nesting_name_id: name_id,
2617
2698
  },
2618
2699
  [
@@ -2950,7 +3031,7 @@ mod tests {
2950
3031
  assert_declaration_completion_eq!(
2951
3032
  context,
2952
3033
  CompletionReceiver::Expression {
2953
- self_decl_id: None,
3034
+ self_decl_id: Some(DeclarationId::from("Foo")),
2954
3035
  nesting_name_id: foo_name_id,
2955
3036
  },
2956
3037
  [
@@ -2970,7 +3051,7 @@ mod tests {
2970
3051
  assert_declaration_completion_eq!(
2971
3052
  context,
2972
3053
  CompletionReceiver::Expression {
2973
- self_decl_id: None,
3054
+ self_decl_id: Some(DeclarationId::from("Bar")),
2974
3055
  nesting_name_id: bar_name_id,
2975
3056
  },
2976
3057
  [
@@ -3286,7 +3367,7 @@ mod tests {
3286
3367
  assert_declaration_completion_eq!(
3287
3368
  context,
3288
3369
  CompletionReceiver::Expression {
3289
- self_decl_id: None,
3370
+ self_decl_id: Some(DeclarationId::from("Foo")),
3290
3371
  nesting_name_id: foo_name_id,
3291
3372
  },
3292
3373
  [
@@ -375,10 +375,16 @@ impl<'a> Resolver<'a> {
375
375
  Definition::Method(method) => {
376
376
  if let Some(receiver) = method.receiver() {
377
377
  let receiver_decl_id = match receiver {
378
- Receiver::SelfReceiver(def_id) => *self
379
- .graph
380
- .definition_id_to_declaration_id(*def_id)
381
- .expect("SelfReceiver definition should have a declaration"),
378
+ Receiver::SelfReceiver(def_id) => {
379
+ let Some(&receiver_decl_id) =
380
+ self.graph.definition_id_to_declaration_id(*def_id)
381
+ else {
382
+ self.graph.push_work(Unit::Definition(id));
383
+ continue;
384
+ };
385
+
386
+ receiver_decl_id
387
+ }
382
388
  Receiver::ConstantReceiver(name_id) => {
383
389
  let Some(receiver_decl_id) = self.resolve_constant_receiver(*name_id, id)
384
390
  else {
@@ -513,13 +519,15 @@ impl<'a> Resolver<'a> {
513
519
  let new_name_str_id = *alias.new_name_str_id();
514
520
  let owner_id = match alias.receiver() {
515
521
  Some(Receiver::SelfReceiver(def_id)) => {
516
- let decl_id = *self
517
- .graph
518
- .definition_id_to_declaration_id(*def_id)
519
- .expect("SelfReceiver definition should have a declaration");
522
+ let Some(&decl_id) = self.graph.definition_id_to_declaration_id(*def_id) else {
523
+ self.graph.push_work(Unit::Definition(id));
524
+ continue;
525
+ };
526
+
520
527
  let Some(owner_id) = self.get_or_create_singleton_class(decl_id, true) else {
521
528
  continue;
522
529
  };
530
+
523
531
  owner_id
524
532
  }
525
533
  Some(Receiver::ConstantReceiver(name_id)) => {
@@ -4674,6 +4674,138 @@ mod promotability_tests {
4674
4674
  assert_declaration_does_not_exist!(context, "Foo::<Foo>");
4675
4675
  assert_declaration_does_not_exist!(context, "Foo::<Foo>#bar()");
4676
4676
  }
4677
+
4678
+ #[test]
4679
+ fn ivar_defined_inside_of_undefined_alias_namespace() {
4680
+ let mut context = graph_test();
4681
+ context.index_uri("file:///alias.rb", {
4682
+ r"
4683
+ Aliased = Undefined
4684
+
4685
+ class Aliased::Inner
4686
+ def self.run
4687
+ @ivar = 1
4688
+ end
4689
+ end
4690
+ "
4691
+ });
4692
+
4693
+ context.resolve();
4694
+ assert_no_diagnostics!(&context);
4695
+
4696
+ // Since we have no idea what `Aliased` is, then we cannot create `Inner`, `run()` or `@ivar` declarations
4697
+ assert_declaration_does_not_exist!(context, "Aliased::Inner");
4698
+ assert_declaration_does_not_exist!(context, "Aliased::Inner::<Inner>#run()");
4699
+ assert_declaration_does_not_exist!(context, "Aliased::Inner::<Inner>#@ivar");
4700
+ }
4701
+
4702
+ #[test]
4703
+ fn ivar_inside_undefined_alias_namespace_recovers_when_target_is_defined() {
4704
+ let mut context = graph_test();
4705
+ context.index_uri("file:///alias.rb", {
4706
+ r"
4707
+ Aliased = Undefined
4708
+
4709
+ class Aliased::Inner
4710
+ def self.run
4711
+ @ivar = 1
4712
+ end
4713
+ end
4714
+ "
4715
+ });
4716
+ context.resolve();
4717
+
4718
+ // Nothing can be placed yet: `Aliased` aliases a constant that does not exist.
4719
+ assert_declaration_does_not_exist!(context, "Aliased::Inner");
4720
+
4721
+ // A later edit defines the alias target. The instance variable must not have been
4722
+ // dropped permanently: it should be remembered and placed once its owner exists.
4723
+ context.index_uri("file:///target.rb", {
4724
+ r"
4725
+ module Undefined
4726
+ end
4727
+ "
4728
+ });
4729
+ context.resolve();
4730
+ assert_no_diagnostics!(&context);
4731
+
4732
+ // `Aliased` now resolves to `Undefined`, so the nested declarations materialize under it,
4733
+ // including the previously-deferred instance variable.
4734
+ assert_declaration_kind_eq!(context, "Undefined::Inner", "Class");
4735
+ assert_declaration_kind_eq!(context, "Undefined::Inner::<Inner>", "SingletonClass");
4736
+ assert_declaration_kind_eq!(context, "Undefined::Inner::<Inner>#run()", "Method");
4737
+ assert_declaration_kind_eq!(context, "Undefined::Inner::<Inner>#@ivar", "InstanceVariable");
4738
+ }
4739
+
4740
+ #[test]
4741
+ fn self_method_alias_defined_inside_of_undefined_alias_namespace() {
4742
+ let mut context = graph_test();
4743
+ context.index_uri("file:///alias.rb", {
4744
+ r"
4745
+ Aliased = Undefined
4746
+ "
4747
+ });
4748
+ // RBS singleton method alias (`alias self.x self.y`) nested under the undefined-alias namespace.
4749
+ context.index_rbs_uri(
4750
+ "file:///alias.rbs",
4751
+ r"
4752
+ class Aliased::Inner
4753
+ def self.run: () -> void
4754
+ alias self.execute self.run
4755
+ end
4756
+ ",
4757
+ );
4758
+
4759
+ context.resolve();
4760
+ assert_no_diagnostics!(&context);
4761
+
4762
+ // Since we have no idea what `Aliased` is, none of the nested declarations (including the
4763
+ // singleton method alias) can be created.
4764
+ assert_declaration_does_not_exist!(context, "Aliased::Inner");
4765
+ assert_declaration_does_not_exist!(context, "Aliased::Inner::<Inner>#run()");
4766
+ assert_declaration_does_not_exist!(context, "Aliased::Inner::<Inner>#execute()");
4767
+ }
4768
+
4769
+ #[test]
4770
+ fn self_method_alias_inside_undefined_alias_namespace_recovers_when_target_is_defined() {
4771
+ let mut context = graph_test();
4772
+ context.index_uri("file:///alias.rb", {
4773
+ r"
4774
+ Aliased = Undefined
4775
+ "
4776
+ });
4777
+ context.index_rbs_uri(
4778
+ "file:///alias.rbs",
4779
+ r"
4780
+ class Aliased::Inner
4781
+ def self.run: () -> void
4782
+ alias self.execute self.run
4783
+ end
4784
+ ",
4785
+ );
4786
+ context.resolve();
4787
+
4788
+ // Nothing can be placed yet: `Aliased` aliases a constant that does not exist.
4789
+ assert_declaration_does_not_exist!(context, "Aliased::Inner");
4790
+
4791
+ // A later edit defines the alias target. The singleton method alias must not have been
4792
+ // dropped permanently: it should be remembered and placed once its owner exists.
4793
+ context.index_uri("file:///target.rb", {
4794
+ r"
4795
+ module Undefined
4796
+ end
4797
+ "
4798
+ });
4799
+ context.resolve();
4800
+ assert_no_diagnostics!(&context);
4801
+
4802
+ // `Aliased` now resolves to `Undefined`, so the nested declarations materialize under it,
4803
+ // including the previously-deferred singleton method alias.
4804
+ assert_declaration_kind_eq!(context, "Undefined::Inner", "Class");
4805
+ assert_declaration_kind_eq!(context, "Undefined::Inner::<Inner>", "SingletonClass");
4806
+ assert_declaration_kind_eq!(context, "Undefined::Inner::<Inner>#run()", "Method");
4807
+ assert_declaration_kind_eq!(context, "Undefined::Inner::<Inner>#execute()", "Method");
4808
+ }
4677
4809
  }
4678
4810
 
4679
4811
  mod rbs_tests {
@@ -1,7 +1,6 @@
1
1
  use assert_cmd::{assert::Assert, prelude::*};
2
2
  use predicates::prelude::*;
3
- use regex::Regex;
4
- use rubydex::test_utils::{normalize_indentation, with_context};
3
+ use rubydex::test_utils::with_context;
5
4
  use std::process::Command;
6
5
 
7
6
  fn rdx_cmd(args: &[&str]) -> Command {
@@ -21,7 +20,7 @@ fn prints_help() {
21
20
  .stdout(predicate::str::contains("A Static Analysis Toolkit for Ruby"))
22
21
  .stdout(predicate::str::contains("Usage:"))
23
22
  .stdout(predicate::str::contains("--stats"))
24
- .stdout(predicate::str::contains("--visualize"))
23
+ .stdout(predicate::str::contains("--dot"))
25
24
  .stdout(predicate::str::contains("--stop-after"));
26
25
  }
27
26
 
@@ -68,68 +67,25 @@ fn prints_index_metrics() {
68
67
  });
69
68
  }
70
69
 
71
- fn normalize_visualization_output(output: &str) -> String {
72
- let def_re = Regex::new(r"def_-?[a-f0-9]+").unwrap();
73
- let uri_re = Regex::new(r#"file://[^"]+/([^/"]+\.rb)"#).unwrap();
74
-
75
- let normalized = def_re.replace_all(output, "def_<ID>");
76
- uri_re.replace_all(&normalized, "file://<PATH>/$1").to_string()
77
- }
78
-
79
70
  #[test]
80
- fn visualize_simple_class() {
71
+ fn dot_flag() {
81
72
  with_context(|context| {
82
73
  context.write("simple.rb", "class SimpleClass\nend\n");
83
74
 
84
- let output = rdx_cmd(&[context.absolute_path().to_str().unwrap(), "--visualize"])
85
- .output()
86
- .unwrap();
87
-
88
- assert!(output.status.success());
89
-
90
- let stdout = String::from_utf8_lossy(&output.stdout);
91
- let normalized = normalize_visualization_output(&stdout);
92
-
93
- let expected = normalize_indentation({
94
- r#"
95
- digraph {
96
- rankdir=TB;
97
-
98
- "Name:BasicObject" [label="BasicObject",shape=hexagon];
99
- "Name:BasicObject" -> "def_<ID>" [dir=both];
100
- "Name:Class" [label="Class",shape=hexagon];
101
- "Name:Class" -> "def_<ID>" [dir=both];
102
- "Name:Kernel" [label="Kernel",shape=hexagon];
103
- "Name:Kernel" -> "def_<ID>" [dir=both];
104
- "Name:Module" [label="Module",shape=hexagon];
105
- "Name:Module" -> "def_<ID>" [dir=both];
106
- "Name:Object" [label="Object",shape=hexagon];
107
- "Name:Object" -> "def_<ID>" [dir=both];
108
- "Name:SimpleClass" [label="SimpleClass",shape=hexagon];
109
- "Name:SimpleClass" -> "def_<ID>" [dir=both];
110
-
111
- "def_<ID>" [label="Class(BasicObject)",shape=ellipse];
112
- "def_<ID>" [label="Class(Class)",shape=ellipse];
113
- "def_<ID>" [label="Class(Module)",shape=ellipse];
114
- "def_<ID>" [label="Class(Object)",shape=ellipse];
115
- "def_<ID>" [label="Class(SimpleClass)",shape=ellipse];
116
- "def_<ID>" [label="Module(Kernel)",shape=ellipse];
117
-
118
- "file://<PATH>/simple.rb" [label="simple.rb",shape=box];
119
- "def_<ID>" -> "file://<PATH>/simple.rb";
120
- "rubydex:built-in" [label="rubydex:built-in",shape=box];
121
- "def_<ID>" -> "rubydex:built-in";
122
- "def_<ID>" -> "rubydex:built-in";
123
- "def_<ID>" -> "rubydex:built-in";
124
- "def_<ID>" -> "rubydex:built-in";
125
- "def_<ID>" -> "rubydex:built-in";
126
-
127
- }
128
-
129
- "#
130
- });
131
-
132
- assert_eq!(normalized, expected);
75
+ rdx(&[context.absolute_path().to_str().unwrap(), "--dot"])
76
+ .success()
77
+ .stdout(predicate::str::contains("digraph rubydex"))
78
+ // Document node
79
+ .stdout(predicate::str::contains("Document"))
80
+ .stdout(predicate::str::contains("simple.rb"))
81
+ // Definition node
82
+ .stdout(predicate::str::contains("ClassDef"))
83
+ .stdout(predicate::str::contains("SimpleClass"))
84
+ // Declaration node
85
+ .stdout(predicate::str::contains("ClassDecl"))
86
+ // Edges
87
+ .stdout(predicate::str::contains("defines"))
88
+ .stdout(predicate::str::contains("declares"));
133
89
  });
134
90
  }
135
91
 
@@ -1,16 +1,22 @@
1
1
  [package]
2
2
  name = "rubydex-mcp"
3
- version = "0.1.0"
3
+ version = "0.2.6"
4
4
  edition = "2024"
5
5
  rust-version = "1.89.0"
6
6
  license = "MIT"
7
+ description = "MCP server exposing Rubydex semantic Ruby code intelligence tools."
8
+ homepage = "https://github.com/Shopify/rubydex"
9
+ repository = "https://github.com/Shopify/rubydex"
10
+ readme = "README.md"
11
+ keywords = ["ruby", "mcp", "static-analysis"]
12
+ categories = ["development-tools"]
7
13
 
8
14
  [[bin]]
9
15
  name = "rubydex_mcp"
10
16
  path = "src/main.rs"
11
17
 
12
18
  [dependencies]
13
- rubydex = { path = "../rubydex" }
19
+ rubydex = { version = "0.2.6", path = "../rubydex" }
14
20
  clap = { version = "4.5.16", features = ["derive"] }
15
21
  rmcp = { version = "1.4", features = ["server", "macros", "transport-io", "schemars"] }
16
22
  tokio = { version = "1", features = ["macros", "rt", "io-std"] }
@@ -20,7 +26,7 @@ schemars = "1"
20
26
  url = "2"
21
27
 
22
28
  [dev-dependencies]
23
- rubydex = { path = "../rubydex", features = ["test_utils"] }
29
+ rubydex = { version = "0.2.6", path = "../rubydex", features = ["test_utils"] }
24
30
  assert_cmd = "2.0"
25
31
  serde_json = "1"
26
32
 
@@ -1,14 +1,21 @@
1
1
  [package]
2
2
  name = "rubydex-sys"
3
- version = "0.1.0"
3
+ version = "0.2.6"
4
4
  edition = "2024"
5
+ rust-version = "1.89.0"
5
6
  license = "MIT"
7
+ description = "C FFI bindings for the Rubydex Ruby code indexing engine."
8
+ homepage = "https://github.com/Shopify/rubydex"
9
+ repository = "https://github.com/Shopify/rubydex"
10
+ readme = "README.md"
11
+ keywords = ["ruby", "ffi", "static-analysis"]
12
+ categories = ["development-tools", "external-ffi-bindings"]
6
13
 
7
14
  [lib]
8
15
  crate-type = ["cdylib", "staticlib"]
9
16
 
10
17
  [dependencies]
11
- rubydex = { path = "../rubydex" }
18
+ rubydex = { version = "0.2.6", path = "../rubydex" }
12
19
  libc = "0.2.174"
13
20
  url = "2.5.4"
14
21
  line-index = "0.1.2"