rubydex 0.2.5-aarch64-linux → 0.2.7-aarch64-linux

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +17 -16
  3. data/THIRD_PARTY_LICENSES.html +45 -12
  4. data/exe/rdx +2 -0
  5. data/ext/rubydex/declaration.c +115 -106
  6. data/ext/rubydex/definition.c +123 -72
  7. data/ext/rubydex/diagnostic.c +5 -0
  8. data/ext/rubydex/document.c +51 -23
  9. data/ext/rubydex/extconf.rb +1 -1
  10. data/ext/rubydex/graph.c +253 -66
  11. data/ext/rubydex/handle.h +34 -5
  12. data/ext/rubydex/location.c +5 -0
  13. data/ext/rubydex/reference.c +51 -46
  14. data/ext/rubydex/rubydex.c +5 -0
  15. data/ext/rubydex/signature.c +5 -0
  16. data/ext/rubydex/utils.c +13 -0
  17. data/ext/rubydex/utils.h +4 -0
  18. data/lib/rubydex/3.2/rubydex.so +0 -0
  19. data/lib/rubydex/3.3/rubydex.so +0 -0
  20. data/lib/rubydex/3.4/rubydex.so +0 -0
  21. data/lib/rubydex/4.0/rubydex.so +0 -0
  22. data/lib/rubydex/bin/rubydex_mcp +0 -0
  23. data/lib/rubydex/errors.rb +11 -0
  24. data/lib/rubydex/graph.rb +9 -28
  25. data/lib/rubydex/librubydex_sys.so +0 -0
  26. data/lib/rubydex/location.rb +24 -0
  27. data/lib/rubydex/version.rb +1 -1
  28. data/lib/rubydex.rb +1 -0
  29. data/rbi/rubydex.rbi +40 -15
  30. data/rust/Cargo.lock +122 -17
  31. data/rust/rubydex/Cargo.toml +26 -2
  32. data/rust/rubydex/benches/graph_memory.rs +46 -0
  33. data/rust/rubydex/src/config.rs +275 -0
  34. data/rust/rubydex/src/dot.rs +609 -0
  35. data/rust/rubydex/src/errors.rs +2 -0
  36. data/rust/rubydex/src/indexing/rbs_indexer.rs +19 -1
  37. data/rust/rubydex/src/indexing/ruby_indexer.rs +4 -0
  38. data/rust/rubydex/src/lib.rs +8 -1
  39. data/rust/rubydex/src/main.rs +8 -5
  40. data/rust/rubydex/src/model/built_in.rs +5 -2
  41. data/rust/rubydex/src/model/comment.rs +2 -0
  42. data/rust/rubydex/src/model/declaration.rs +13 -51
  43. data/rust/rubydex/src/model/definitions.rs +13 -1
  44. data/rust/rubydex/src/model/document.rs +2 -0
  45. data/rust/rubydex/src/model/encoding.rs +2 -0
  46. data/rust/rubydex/src/model/graph.rs +88 -27
  47. data/rust/rubydex/src/model/identity_maps.rs +3 -0
  48. data/rust/rubydex/src/model/keywords.rs +3 -0
  49. data/rust/rubydex/src/model/name.rs +2 -0
  50. data/rust/rubydex/src/model/string_ref.rs +2 -0
  51. data/rust/rubydex/src/model/visibility.rs +3 -0
  52. data/rust/rubydex/src/operation/applier.rs +1 -0
  53. data/rust/rubydex/src/operation/mod.rs +1 -0
  54. data/rust/rubydex/src/operation/ruby_builder.rs +4 -0
  55. data/rust/rubydex/src/query.rs +114 -33
  56. data/rust/rubydex/src/resolution.rs +18 -20
  57. data/rust/rubydex/src/resolution_tests.rs +132 -0
  58. data/rust/rubydex/tests/cli.rs +17 -61
  59. data/rust/rubydex-mcp/Cargo.toml +9 -3
  60. data/rust/rubydex-sys/Cargo.toml +9 -2
  61. data/rust/rubydex-sys/src/definition_api.rs +72 -2
  62. data/rust/rubydex-sys/src/document_api.rs +28 -0
  63. data/rust/rubydex-sys/src/graph_api.rs +74 -6
  64. metadata +6 -4
  65. data/rust/rubydex/src/visualization/dot.rs +0 -192
  66. data/rust/rubydex/src/visualization.rs +0 -6
@@ -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)) => {
@@ -702,18 +710,8 @@ impl<'a> Resolver<'a> {
702
710
  offset,
703
711
  format!("undefined method `{owner_name}#{method_name}` for visibility change"),
704
712
  );
705
- if is_singleton {
706
- // Document-scoped: the singleton class may be synthetic (created by this
707
- // visibility resolution) and won't be cleaned up on file delete, so attaching
708
- // the diagnostic to the declaration would leave it orphaned.
709
- self.graph.add_document_diagnostic(uri_id, diagnostic);
710
- } else {
711
- self.graph
712
- .declarations_mut()
713
- .get_mut(&owner_id)
714
- .unwrap()
715
- .add_diagnostic(diagnostic);
716
- }
713
+
714
+ self.graph.add_document_diagnostic(uri_id, diagnostic);
717
715
  }
718
716
  }
719
717
 
@@ -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"
@@ -7,6 +7,7 @@ use crate::reference_api::CConstantReference;
7
7
  use libc::c_char;
8
8
  use rubydex::model::definitions::{Definition, Mixin};
9
9
  use rubydex::model::ids::DefinitionId;
10
+ use rubydex::query::AliasResolutionError;
10
11
  use std::ffi::CString;
11
12
  use std::ptr;
12
13
 
@@ -346,8 +347,8 @@ pub unsafe extern "C" fn rdx_definition_is_deprecated(pointer: GraphPointer, def
346
347
  }
347
348
 
348
349
  /// Returns a newly allocated `Location` for the name portion of a definition id.
349
- /// For class, module, and singleton class definitions, this returns the location of just
350
- /// the name (e.g., "Bar" in `class Foo::Bar`).
350
+ /// For class, module, singleton class, and method definitions, this returns the location of just
351
+ /// the name (e.g., "Bar" in `class Foo::Bar`, or "foo" in `def foo`).
351
352
  /// For other definition types, returns NULL.
352
353
  /// Caller must free the returned pointer with `rdx_location_free`.
353
354
  ///
@@ -491,3 +492,72 @@ pub unsafe extern "C" fn rdx_definition_mixins(pointer: GraphPointer, definition
491
492
  MixinsIter::new(entries.into_boxed_slice())
492
493
  })
493
494
  }
495
+
496
+ /// Status of a `MethodAliasDefinition#target` resolution.
497
+ #[repr(u8)]
498
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
499
+ pub enum CMethodAliasResolution {
500
+ /// The alias chain resolved successfully; `declaration` is valid.
501
+ Resolved = 0,
502
+ /// The chain could not be resolved because the target name does not exist on the owner, or the owner itself was
503
+ /// never resolved. Treated as `nil` on the Ruby side.
504
+ NotFound = 1,
505
+ /// The alias chain forms a cycle. Surfaced as a `Rubydex::AliasCycleError` on the Ruby side.
506
+ Cycle = 2,
507
+ }
508
+
509
+ #[repr(C)]
510
+ #[derive(Debug)]
511
+ pub struct CMethodAliasTargetResult {
512
+ pub status: CMethodAliasResolution,
513
+ pub declaration: *const CDeclaration,
514
+ }
515
+
516
+ /// Resolves a `MethodAliasDefinition` to its target method declaration via `query::follow_method_alias` and reports the
517
+ /// outcome as a tagged status. The `declaration` pointer is non-null only when `status == Resolved`; the caller is
518
+ /// responsible for freeing it with `free_c_declaration`.
519
+ ///
520
+ /// # Safety
521
+ /// - `pointer` must be a valid pointer previously returned by `rdx_graph_new`.
522
+ /// - `definition_id` must be a valid definition id for a `MethodAliasDefinition`.
523
+ ///
524
+ /// # Panics
525
+ /// Panics on graph inconsistencies (the definition is not a method alias, or the alias resolved to a non-method
526
+ /// declaration).
527
+ #[unsafe(no_mangle)]
528
+ pub unsafe extern "C" fn rdx_method_alias_definition_target(
529
+ pointer: GraphPointer,
530
+ definition_id: u64,
531
+ ) -> CMethodAliasTargetResult {
532
+ with_graph(pointer, |graph| {
533
+ let def_id = DefinitionId::new(definition_id);
534
+
535
+ match rubydex::query::follow_method_alias(graph, def_id) {
536
+ Ok(target_id) => {
537
+ let target_decl = graph
538
+ .declarations()
539
+ .get(&target_id)
540
+ .expect("target declaration must exist");
541
+ let boxed = Box::new(CDeclaration::from_declaration(target_id, target_decl));
542
+
543
+ CMethodAliasTargetResult {
544
+ status: CMethodAliasResolution::Resolved,
545
+ declaration: Box::into_raw(boxed).cast_const(),
546
+ }
547
+ }
548
+ Err(AliasResolutionError::TargetNotFound | AliasResolutionError::UnresolvedOwner) => {
549
+ CMethodAliasTargetResult {
550
+ status: CMethodAliasResolution::NotFound,
551
+ declaration: ptr::null(),
552
+ }
553
+ }
554
+ Err(AliasResolutionError::Cycle) => CMethodAliasTargetResult {
555
+ status: CMethodAliasResolution::Cycle,
556
+ declaration: ptr::null(),
557
+ },
558
+ Err(err @ (AliasResolutionError::NotAnAlias | AliasResolutionError::TargetNotMethod)) => {
559
+ panic!("graph inconsistency in method alias resolution: {err:?}")
560
+ }
561
+ }
562
+ })
563
+ }
@@ -6,6 +6,7 @@ use std::ptr;
6
6
 
7
7
  use crate::definition_api::{DefinitionsIter, rdx_definitions_iter_new_from_ids};
8
8
  use crate::graph_api::{GraphPointer, with_graph};
9
+ use crate::reference_api::{CMethodReference, MethodReferencesIter};
9
10
  use rubydex::model::ids::UriId;
10
11
 
11
12
  #[derive(Debug)]
@@ -83,3 +84,30 @@ pub unsafe extern "C" fn rdx_document_definitions_iter_new(pointer: GraphPointer
83
84
  }
84
85
  })
85
86
  }
87
+
88
+ /// Creates a new iterator over method reference IDs for a given document by snapshotting the current set of IDs.
89
+ ///
90
+ /// # Safety
91
+ ///
92
+ /// - `pointer` must be a valid `GraphPointer` previously returned by this crate.
93
+ /// - The returned pointer must be freed with `rdx_method_references_iter_free`.
94
+ #[unsafe(no_mangle)]
95
+ pub unsafe extern "C" fn rdx_document_method_references_iter_new(
96
+ pointer: GraphPointer,
97
+ uri_id: u64,
98
+ ) -> *mut MethodReferencesIter {
99
+ with_graph(pointer, |graph| {
100
+ let uri_id = UriId::new(uri_id);
101
+ if let Some(doc) = graph.documents().get(&uri_id) {
102
+ let entries: Vec<_> = doc
103
+ .method_references()
104
+ .iter()
105
+ .map(|ref_id| CMethodReference { id: **ref_id })
106
+ .collect();
107
+
108
+ MethodReferencesIter::new(entries.into_boxed_slice())
109
+ } else {
110
+ MethodReferencesIter::new(Vec::<_>::new().into_boxed_slice())
111
+ }
112
+ })
113
+ }
@@ -7,6 +7,7 @@ use crate::document_api::DocumentsIter;
7
7
  use crate::reference_api::{CConstantReference, CMethodReference, ConstantReferencesIter, MethodReferencesIter};
8
8
  use crate::{name_api, utils};
9
9
  use libc::{c_char, c_void};
10
+ use rubydex::errors::Errors;
10
11
  use rubydex::indexing::LanguageId;
11
12
  use rubydex::model::encoding::Encoding;
12
13
  use rubydex::model::graph::Graph;
@@ -18,7 +19,7 @@ use rubydex::query::{CompletionCandidate, CompletionContext, CompletionReceiver}
18
19
  use rubydex::resolution::Resolver;
19
20
  use rubydex::{indexing, integrity, listing, query};
20
21
  use std::ffi::CString;
21
- use std::path::PathBuf;
22
+ use std::path::{Path, PathBuf};
22
23
  use std::{mem, ptr};
23
24
 
24
25
  pub type GraphPointer = *mut c_void;
@@ -197,7 +198,13 @@ pub unsafe extern "C" fn rdx_graph_excluded_paths(
197
198
  let c_strings: Vec<*const c_char> = excluded
198
199
  .iter()
199
200
  .filter_map(|path| {
200
- CString::new(path.to_string_lossy().as_ref())
201
+ // Normalize all paths to use forward slashes. Otherwise, you get mixed backslashes and forward slashes
202
+ // on Windows if a configuration file is using forward slashes. For example:
203
+ //
204
+ // C:\project/vendor/bundle
205
+ let normalized = path.to_string_lossy().replace(std::path::MAIN_SEPARATOR, "/");
206
+
207
+ CString::new(normalized)
201
208
  .ok()
202
209
  .map(|c_string| c_string.into_raw().cast_const())
203
210
  })
@@ -210,6 +217,69 @@ pub unsafe extern "C" fn rdx_graph_excluded_paths(
210
217
  })
211
218
  }
212
219
 
220
+ /// Sets the workspace path used as the root directory for indexing and relative path resolution. Silently ignores the
221
+ /// call if the given path is not valid UTF-8, leaving the existing workspace path untouched (mirrors
222
+ /// `rdx_graph_set_encoding`). This avoids unwinding across the FFI boundary on malformed input.
223
+ ///
224
+ /// # Safety
225
+ ///
226
+ /// - `pointer` must be a valid `GraphPointer` previously returned by this crate.
227
+ /// - `path` must be a valid, null-terminated string.
228
+ #[unsafe(no_mangle)]
229
+ pub unsafe extern "C" fn rdx_graph_set_workspace_path(pointer: GraphPointer, path: *const c_char) {
230
+ let Ok(path) = (unsafe { utils::convert_char_ptr_to_string(path) }) else {
231
+ return;
232
+ };
233
+
234
+ with_mut_graph(pointer, |graph| graph.set_workspace_path(PathBuf::from(path)));
235
+ }
236
+
237
+ /// Returns the workspace path as a C string. Caller must free with `free_c_string`.
238
+ ///
239
+ /// # Safety
240
+ ///
241
+ /// - `pointer` must be a valid `GraphPointer` previously returned by this crate.
242
+ #[unsafe(no_mangle)]
243
+ pub unsafe extern "C" fn rdx_graph_workspace_path(pointer: GraphPointer) -> *const c_char {
244
+ with_graph(pointer, |graph| {
245
+ CString::new(graph.workspace_path().to_string_lossy().as_ref())
246
+ .map_or(ptr::null(), |c_string| c_string.into_raw().cast_const())
247
+ })
248
+ }
249
+
250
+ /// Loads configuration into the graph. A null `config_path` attempts to load the default configuration file.
251
+ ///
252
+ /// Returns NULL on success. On failure returns an owned, null-terminated error message that the caller must free with
253
+ /// `free_c_string`.
254
+ ///
255
+ /// A `config_path` that is not valid UTF-8 is reported as an error message.
256
+ ///
257
+ /// # Safety
258
+ ///
259
+ /// - `pointer` must be a valid `GraphPointer` previously returned by this crate.
260
+ /// - `config_path` must either be NULL or a valid, null-terminated string.
261
+ #[unsafe(no_mangle)]
262
+ pub unsafe extern "C" fn rdx_graph_load_config(pointer: GraphPointer, config_path: *const c_char) -> *const c_char {
263
+ let result = with_mut_graph(pointer, |graph| {
264
+ if config_path.is_null() {
265
+ graph.load_config(None)
266
+ } else {
267
+ match unsafe { utils::convert_char_ptr_to_string(config_path) } {
268
+ Ok(config_path) => graph.load_config(Some(Path::new(&config_path))),
269
+ Err(_) => Err(Errors::ConfigError("config file path is not valid UTF-8".to_string())),
270
+ }
271
+ }
272
+ });
273
+
274
+ match result {
275
+ Ok(()) => ptr::null(),
276
+ Err(error) => CString::new(error.to_string())
277
+ .unwrap_or_default()
278
+ .into_raw()
279
+ .cast_const(),
280
+ }
281
+ }
282
+
213
283
  /// Indexes all given file paths in parallel using the provided Graph pointer.
214
284
  /// Returns an array of error message strings and writes the count to `out_error_count`.
215
285
  /// Returns NULL if there are no errors. Caller must free with `free_c_string_array`.
@@ -232,7 +302,7 @@ pub unsafe extern "C" fn rdx_index_all(
232
302
  let file_paths: Vec<String> = unsafe { utils::convert_double_pointer_to_vec(file_paths, count).unwrap() };
233
303
 
234
304
  with_mut_graph(pointer, |graph| {
235
- let (file_paths, listing_errors) = listing::collect_file_paths(file_paths, graph.excluded_paths());
305
+ let (file_paths, listing_errors) = listing::collect_file_paths(file_paths, &graph.excluded_paths());
236
306
  let indexing_errors = indexing::index_files(graph, file_paths, indexing::IndexerBackend::RubyIndexer);
237
307
 
238
308
  let all_errors: Vec<String> = listing_errors
@@ -793,9 +863,7 @@ fn run_and_finalize_completion(
793
863
  ///
794
864
  /// - `pointer` must be a valid `GraphPointer` previously returned by this crate.
795
865
  /// - `nesting` must point to `nesting_count` valid, null-terminated UTF-8 strings.
796
- /// - `self_receiver` must be null or a valid, null-terminated UTF-8 string. When non-null, it
797
- /// overrides the self-type (e.g., `"Foo::<Foo>"` for completion inside `def Foo.bar`), while
798
- /// the lexical nesting still comes from `nesting`.
866
+ /// - `self_receiver` is the fully qualified name of the **type of `self`**
799
867
  #[unsafe(no_mangle)]
800
868
  pub unsafe extern "C" fn rdx_graph_complete_expression(
801
869
  pointer: GraphPointer,
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubydex
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.5
4
+ version: 0.2.7
5
5
  platform: aarch64-linux
6
6
  authors:
7
7
  - Shopify
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-05-28 00:00:00.000000000 Z
11
+ date: 2026-06-30 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: A high-performance static analysis suite for Ruby, built in Rust with
14
14
  Ruby APIs
@@ -55,6 +55,7 @@ files:
55
55
  - lib/rubydex/comment.rb
56
56
  - lib/rubydex/declaration.rb
57
57
  - lib/rubydex/diagnostic.rb
58
+ - lib/rubydex/errors.rb
58
59
  - lib/rubydex/failures.rb
59
60
  - lib/rubydex/graph.rb
60
61
  - lib/rubydex/keyword.rb
@@ -91,8 +92,11 @@ files:
91
92
  - rust/rubydex-sys/src/signature_api.rs
92
93
  - rust/rubydex-sys/src/utils.rs
93
94
  - rust/rubydex/Cargo.toml
95
+ - rust/rubydex/benches/graph_memory.rs
94
96
  - rust/rubydex/src/compile_assertions.rs
97
+ - rust/rubydex/src/config.rs
95
98
  - rust/rubydex/src/diagnostic.rs
99
+ - rust/rubydex/src/dot.rs
96
100
  - rust/rubydex/src/errors.rs
97
101
  - rust/rubydex/src/indexing.rs
98
102
  - rust/rubydex/src/indexing/local_graph.rs
@@ -137,8 +141,6 @@ files:
137
141
  - rust/rubydex/src/test_utils/context.rs
138
142
  - rust/rubydex/src/test_utils/graph_test.rs
139
143
  - rust/rubydex/src/test_utils/local_graph_test.rs
140
- - rust/rubydex/src/visualization.rs
141
- - rust/rubydex/src/visualization/dot.rs
142
144
  - rust/rubydex/tests/cli.rs
143
145
  - rust/rustfmt.toml
144
146
  homepage: https://github.com/Shopify/rubydex