rubydex 0.2.5 → 0.2.7
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.
- checksums.yaml +4 -4
- data/README.md +17 -16
- data/THIRD_PARTY_LICENSES.html +45 -12
- data/exe/rdx +2 -0
- data/ext/rubydex/declaration.c +115 -106
- data/ext/rubydex/definition.c +123 -72
- data/ext/rubydex/diagnostic.c +5 -0
- data/ext/rubydex/document.c +51 -23
- data/ext/rubydex/extconf.rb +1 -1
- data/ext/rubydex/graph.c +253 -66
- data/ext/rubydex/handle.h +34 -5
- data/ext/rubydex/location.c +5 -0
- data/ext/rubydex/reference.c +51 -46
- data/ext/rubydex/rubydex.c +5 -0
- data/ext/rubydex/signature.c +5 -0
- data/ext/rubydex/utils.c +13 -0
- data/ext/rubydex/utils.h +4 -0
- data/lib/rubydex/bin/rubydex_mcp.exe +0 -0
- data/lib/rubydex/errors.rb +11 -0
- data/lib/rubydex/graph.rb +9 -28
- data/lib/rubydex/location.rb +24 -0
- data/lib/rubydex/version.rb +1 -1
- data/lib/rubydex.rb +1 -0
- data/rbi/rubydex.rbi +40 -15
- data/rust/Cargo.lock +122 -17
- data/rust/rubydex/Cargo.toml +26 -2
- data/rust/rubydex/benches/graph_memory.rs +46 -0
- data/rust/rubydex/src/config.rs +275 -0
- data/rust/rubydex/src/dot.rs +609 -0
- data/rust/rubydex/src/errors.rs +2 -0
- data/rust/rubydex/src/indexing/rbs_indexer.rs +19 -1
- data/rust/rubydex/src/indexing/ruby_indexer.rs +4 -0
- data/rust/rubydex/src/lib.rs +8 -1
- data/rust/rubydex/src/main.rs +8 -5
- data/rust/rubydex/src/model/built_in.rs +5 -2
- data/rust/rubydex/src/model/comment.rs +2 -0
- data/rust/rubydex/src/model/declaration.rs +13 -51
- data/rust/rubydex/src/model/definitions.rs +13 -1
- data/rust/rubydex/src/model/document.rs +2 -0
- data/rust/rubydex/src/model/encoding.rs +2 -0
- data/rust/rubydex/src/model/graph.rs +88 -27
- data/rust/rubydex/src/model/identity_maps.rs +3 -0
- data/rust/rubydex/src/model/keywords.rs +3 -0
- data/rust/rubydex/src/model/name.rs +2 -0
- data/rust/rubydex/src/model/string_ref.rs +2 -0
- data/rust/rubydex/src/model/visibility.rs +3 -0
- data/rust/rubydex/src/operation/applier.rs +1 -0
- data/rust/rubydex/src/operation/mod.rs +1 -0
- data/rust/rubydex/src/operation/ruby_builder.rs +4 -0
- data/rust/rubydex/src/query.rs +114 -33
- data/rust/rubydex/src/resolution.rs +18 -20
- data/rust/rubydex/src/resolution_tests.rs +132 -0
- data/rust/rubydex/tests/cli.rs +17 -61
- data/rust/rubydex-mcp/Cargo.toml +9 -3
- data/rust/rubydex-sys/Cargo.toml +9 -2
- data/rust/rubydex-sys/src/definition_api.rs +72 -2
- data/rust/rubydex-sys/src/document_api.rs +28 -0
- data/rust/rubydex-sys/src/graph_api.rs +74 -6
- metadata +6 -4
- data/rust/rubydex/src/visualization/dot.rs +0 -192
- 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) =>
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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 = *
|
|
517
|
-
.graph
|
|
518
|
-
|
|
519
|
-
|
|
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
|
-
|
|
706
|
-
|
|
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 {
|
data/rust/rubydex/tests/cli.rs
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
use assert_cmd::{assert::Assert, prelude::*};
|
|
2
2
|
use predicates::prelude::*;
|
|
3
|
-
use
|
|
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("--
|
|
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
|
|
71
|
+
fn dot_flag() {
|
|
81
72
|
with_context(|context| {
|
|
82
73
|
context.write("simple.rb", "class SimpleClass\nend\n");
|
|
83
74
|
|
|
84
|
-
|
|
85
|
-
.
|
|
86
|
-
.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
|
data/rust/rubydex-mcp/Cargo.toml
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "rubydex-mcp"
|
|
3
|
-
version = "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
|
|
data/rust/rubydex-sys/Cargo.toml
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "rubydex-sys"
|
|
3
|
-
version = "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,
|
|
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
|
-
|
|
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`
|
|
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.
|
|
4
|
+
version: 0.2.7
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Shopify
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
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
|
|
@@ -52,6 +52,7 @@ files:
|
|
|
52
52
|
- lib/rubydex/comment.rb
|
|
53
53
|
- lib/rubydex/declaration.rb
|
|
54
54
|
- lib/rubydex/diagnostic.rb
|
|
55
|
+
- lib/rubydex/errors.rb
|
|
55
56
|
- lib/rubydex/failures.rb
|
|
56
57
|
- lib/rubydex/graph.rb
|
|
57
58
|
- lib/rubydex/keyword.rb
|
|
@@ -87,8 +88,11 @@ files:
|
|
|
87
88
|
- rust/rubydex-sys/src/signature_api.rs
|
|
88
89
|
- rust/rubydex-sys/src/utils.rs
|
|
89
90
|
- rust/rubydex/Cargo.toml
|
|
91
|
+
- rust/rubydex/benches/graph_memory.rs
|
|
90
92
|
- rust/rubydex/src/compile_assertions.rs
|
|
93
|
+
- rust/rubydex/src/config.rs
|
|
91
94
|
- rust/rubydex/src/diagnostic.rs
|
|
95
|
+
- rust/rubydex/src/dot.rs
|
|
92
96
|
- rust/rubydex/src/errors.rs
|
|
93
97
|
- rust/rubydex/src/indexing.rs
|
|
94
98
|
- rust/rubydex/src/indexing/local_graph.rs
|
|
@@ -133,8 +137,6 @@ files:
|
|
|
133
137
|
- rust/rubydex/src/test_utils/context.rs
|
|
134
138
|
- rust/rubydex/src/test_utils/graph_test.rs
|
|
135
139
|
- rust/rubydex/src/test_utils/local_graph_test.rs
|
|
136
|
-
- rust/rubydex/src/visualization.rs
|
|
137
|
-
- rust/rubydex/src/visualization/dot.rs
|
|
138
140
|
- rust/rubydex/tests/cli.rs
|
|
139
141
|
- rust/rustfmt.toml
|
|
140
142
|
homepage: https://github.com/Shopify/rubydex
|