rubydex 0.2.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4926,6 +4926,25 @@ mod visibility_resolution_tests {
4926
4926
  };
4927
4927
  }
4928
4928
 
4929
+ #[test]
4930
+ fn retroactive_visibility_override_applies_in_source_order() {
4931
+ let mut context = GraphTest::new();
4932
+ context.index_uri(
4933
+ "file:///foo.rb",
4934
+ r"
4935
+ class Foo
4936
+ def bar; end
4937
+ private :bar
4938
+ public :bar
4939
+ end
4940
+ ",
4941
+ );
4942
+ context.resolve();
4943
+
4944
+ assert_no_diagnostics!(&context);
4945
+ assert_visibility_eq!(context, "Foo#bar()", Visibility::Public);
4946
+ }
4947
+
4929
4948
  #[test]
4930
4949
  fn retroactive_visibility_on_direct_method() {
4931
4950
  let mut context = GraphTest::new();
@@ -5066,7 +5085,7 @@ mod visibility_resolution_tests {
5066
5085
  assert_diagnostics_eq!(
5067
5086
  context,
5068
5087
  &[
5069
- "undefined-method-visibility-target: undefined method `nonexistent()` for visibility change in `Foo` (2:12-2:23)"
5088
+ "undefined-method-visibility-target: undefined method `Foo#nonexistent()` for visibility change (2:12-2:23)"
5070
5089
  ]
5071
5090
  );
5072
5091
  }
@@ -5306,4 +5325,139 @@ mod visibility_resolution_tests {
5306
5325
 
5307
5326
  assert_visibility_eq!(context, "Foo::BAR", Visibility::Private);
5308
5327
  }
5328
+
5329
+ #[test]
5330
+ fn retroactive_singleton_method_visibility_on_direct_member() {
5331
+ let mut context = GraphTest::new();
5332
+ context.index_uri(
5333
+ "file:///foo.rb",
5334
+ r"
5335
+ class Foo
5336
+ def self.bar; end
5337
+ def self.baz; end
5338
+
5339
+ private_class_method :bar
5340
+ private_class_method :baz
5341
+ public_class_method :baz
5342
+ end
5343
+ ",
5344
+ );
5345
+ context.resolve();
5346
+
5347
+ assert_no_diagnostics!(&context);
5348
+ assert_visibility_eq!(context, "Foo::<Foo>#bar()", Visibility::Private);
5349
+ assert_visibility_eq!(context, "Foo::<Foo>#baz()", Visibility::Public);
5350
+ }
5351
+
5352
+ #[test]
5353
+ fn retroactive_singleton_method_visibility_on_inherited_method() {
5354
+ let mut context = GraphTest::new();
5355
+ context.index_uri(
5356
+ "file:///foo.rb",
5357
+ r"
5358
+ class Parent
5359
+ def self.foo; end
5360
+ end
5361
+
5362
+ class Child < Parent
5363
+ private_class_method :foo
5364
+ end
5365
+ ",
5366
+ );
5367
+ context.resolve();
5368
+
5369
+ assert_no_diagnostics!(&context);
5370
+ assert_declaration_exists!(context, "Child::<Child>#foo()");
5371
+ assert_visibility_eq!(context, "Child::<Child>#foo()", Visibility::Private);
5372
+ assert_visibility_eq!(context, "Parent::<Parent>#foo()", Visibility::Public);
5373
+ }
5374
+
5375
+ #[test]
5376
+ fn retroactive_singleton_method_visibility_on_undefined_method_emits_diagnostic() {
5377
+ let mut context = GraphTest::new();
5378
+ context.index_uri(
5379
+ "file:///foo.rb",
5380
+ r"
5381
+ class Foo
5382
+ private_class_method :nonexistent
5383
+ end
5384
+ ",
5385
+ );
5386
+ context.resolve();
5387
+
5388
+ assert_diagnostics_eq!(
5389
+ context,
5390
+ &[
5391
+ "undefined-method-visibility-target: undefined method `Foo::<Foo>#nonexistent()` for visibility change (2:25-2:36)"
5392
+ ]
5393
+ );
5394
+ }
5395
+
5396
+ #[test]
5397
+ fn retroactive_singleton_method_visibility_undefined_target_diagnostic_clears_when_file_deleted() {
5398
+ let mut context = GraphTest::new();
5399
+ context.index_uri(
5400
+ "file:///foo.rb",
5401
+ r"
5402
+ class Foo
5403
+ end
5404
+ ",
5405
+ );
5406
+ context.index_uri(
5407
+ "file:///bad.rb",
5408
+ r"
5409
+ class Foo
5410
+ private_class_method :missing
5411
+ end
5412
+ ",
5413
+ );
5414
+ context.resolve();
5415
+
5416
+ assert_diagnostics_eq!(
5417
+ context,
5418
+ &[
5419
+ "undefined-method-visibility-target: undefined method `Foo::<Foo>#missing()` for visibility change (2:25-2:32)"
5420
+ ]
5421
+ );
5422
+
5423
+ context.delete_uri("file:///bad.rb");
5424
+ context.resolve();
5425
+
5426
+ assert_no_diagnostics!(&context);
5427
+ }
5428
+
5429
+ #[test]
5430
+ fn retroactive_singleton_method_visibility_undefined_target_diagnostic_clears_when_target_added() {
5431
+ let mut context = GraphTest::new();
5432
+ context.index_uri(
5433
+ "file:///foo.rb",
5434
+ r"
5435
+ class Foo
5436
+ private_class_method :missing
5437
+ end
5438
+ ",
5439
+ );
5440
+ context.resolve();
5441
+
5442
+ assert_diagnostics_eq!(
5443
+ context,
5444
+ &[
5445
+ "undefined-method-visibility-target: undefined method `Foo::<Foo>#missing()` for visibility change (2:25-2:32)"
5446
+ ]
5447
+ );
5448
+
5449
+ context.index_uri(
5450
+ "file:///foo.rb",
5451
+ r"
5452
+ class Foo
5453
+ def self.missing; end
5454
+ private_class_method :missing
5455
+ end
5456
+ ",
5457
+ );
5458
+ context.resolve();
5459
+
5460
+ assert_no_diagnostics!(&context);
5461
+ assert_visibility_eq!(context, "Foo::<Foo>#missing()", Visibility::Private);
5462
+ }
5309
5463
  }
@@ -197,42 +197,19 @@ pub unsafe extern "C" fn rdx_declaration_find_member(
197
197
 
198
198
  with_graph(pointer, |graph| {
199
199
  let id = DeclarationId::new(declaration_id);
200
+ let member_id = StringId::from(member_str.as_str());
200
201
 
201
- let Some(Declaration::Namespace(decl)) = graph.declarations().get(&id) else {
202
- return ptr::null();
202
+ let member_decl_id = match rubydex::query::find_member_in_ancestors(graph, id, member_id, only_inherited) {
203
+ Ok(decl_id) => decl_id,
204
+ Err(rubydex::query::FindMemberError::MemberNotFound) => return ptr::null(),
205
+ Err(err) => unreachable!(
206
+ "Namespace#find_member is only exposed on namespace declarations, so the declaration must exist and be \
207
+ a namespace, got {err:?}"
208
+ ),
203
209
  };
204
210
 
205
- let member_id = StringId::from(member_str.as_str());
206
- let mut found_main_namespace = false;
207
-
208
- decl.ancestors()
209
- .iter()
210
- .find_map(|ancestor| match ancestor {
211
- Ancestor::Complete(ancestor_id) => {
212
- // When only_inherited, skip self and prepended modules
213
- if only_inherited {
214
- let is_self = *ancestor_id == id;
215
- if is_self {
216
- found_main_namespace = true;
217
- }
218
- if is_self || !found_main_namespace {
219
- return None;
220
- }
221
- }
222
-
223
- let ancestor_decl = graph.declarations().get(ancestor_id).unwrap().as_namespace().unwrap();
224
-
225
- if let Some(member_decl_id) = ancestor_decl.member(&member_id) {
226
- return Some((member_decl_id, graph.declarations().get(member_decl_id).unwrap()));
227
- }
228
-
229
- None
230
- }
231
- Ancestor::Partial(_) => None,
232
- })
233
- .map_or(ptr::null(), |(member_decl_id, member_decl)| {
234
- Box::into_raw(Box::new(CDeclaration::from_declaration(*member_decl_id, member_decl))).cast_const()
235
- })
211
+ let member_decl = graph.declarations().get(&member_decl_id).unwrap();
212
+ Box::into_raw(Box::new(CDeclaration::from_declaration(member_decl_id, member_decl))).cast_const()
236
213
  })
237
214
  }
238
215
 
@@ -76,4 +76,5 @@ pub mod graph_api;
76
76
  pub mod location_api;
77
77
  pub mod name_api;
78
78
  pub mod reference_api;
79
+ pub mod signature_api;
79
80
  pub mod utils;
@@ -0,0 +1,209 @@
1
+ //! C API for method signature accessors
2
+
3
+ use crate::graph_api::{GraphPointer, with_graph};
4
+ use crate::location_api::{Location, create_location_for_uri_and_offset};
5
+ use libc::c_char;
6
+ use rubydex::model::declaration::Declaration;
7
+ use rubydex::model::definitions::{Definition, MethodDefinition, Parameter};
8
+ use rubydex::model::graph::Graph;
9
+ use rubydex::model::ids::DefinitionId;
10
+ use std::ffi::CString;
11
+ use std::ptr;
12
+
13
+ /// C-compatible enum representing the kind of a parameter.
14
+ #[repr(C)]
15
+ #[derive(Copy, Clone, Debug, PartialEq, Eq)]
16
+ pub enum ParameterKind {
17
+ RequiredPositional = 0,
18
+ OptionalPositional = 1,
19
+ RestPositional = 2,
20
+ Post = 3,
21
+ RequiredKeyword = 4,
22
+ OptionalKeyword = 5,
23
+ RestKeyword = 6,
24
+ Forward = 7,
25
+ Block = 8,
26
+ }
27
+
28
+ fn map_parameter_kind(param: &Parameter) -> ParameterKind {
29
+ match param {
30
+ Parameter::RequiredPositional(_) => ParameterKind::RequiredPositional,
31
+ Parameter::Post(_) => ParameterKind::Post,
32
+ Parameter::OptionalPositional(_) => ParameterKind::OptionalPositional,
33
+ Parameter::RestPositional(_) => ParameterKind::RestPositional,
34
+ Parameter::RequiredKeyword(_) => ParameterKind::RequiredKeyword,
35
+ Parameter::OptionalKeyword(_) => ParameterKind::OptionalKeyword,
36
+ Parameter::RestKeyword(_) => ParameterKind::RestKeyword,
37
+ Parameter::Block(_) => ParameterKind::Block,
38
+ Parameter::Forward(_) => ParameterKind::Forward,
39
+ }
40
+ }
41
+
42
+ /// C-compatible struct representing a single parameter with its name, kind, and location.
43
+ #[repr(C)]
44
+ pub struct ParameterEntry {
45
+ pub name: *const c_char,
46
+ pub location: *mut Location,
47
+ pub kind: ParameterKind,
48
+ }
49
+
50
+ /// C-compatible struct representing a single method signature (a list of parameters).
51
+ #[repr(C)]
52
+ pub struct SignatureEntry {
53
+ pub parameters: *mut ParameterEntry,
54
+ pub parameters_len: usize,
55
+ }
56
+
57
+ /// C-compatible array of signatures.
58
+ #[repr(C)]
59
+ pub struct SignatureArray {
60
+ pub items: *mut SignatureEntry,
61
+ pub len: usize,
62
+ }
63
+
64
+ /// Returns a newly allocated array of signatures for the given method definition id.
65
+ /// Caller must free the returned pointer with `rdx_definition_signatures_free`.
66
+ ///
67
+ /// # Safety
68
+ /// - `pointer` must be a valid pointer previously returned by `rdx_graph_new`.
69
+ /// - `definition_id` must be a valid definition id.
70
+ ///
71
+ /// # Panics
72
+ /// Panics if `definition_id` does not exist or is not a `MethodDefinition`.
73
+ #[unsafe(no_mangle)]
74
+ pub unsafe extern "C" fn rdx_definition_signatures(pointer: GraphPointer, definition_id: u64) -> *mut SignatureArray {
75
+ with_graph(pointer, |graph| {
76
+ let def_id = DefinitionId::new(definition_id);
77
+ let Definition::Method(method_def) = graph.definitions().get(&def_id).expect("definition should exist") else {
78
+ panic!("expected a method definition");
79
+ };
80
+
81
+ let sig_entries = collect_method_signatures(graph, method_def);
82
+
83
+ let len = sig_entries.len();
84
+ let items_ptr = Box::into_raw(sig_entries.into_boxed_slice()).cast::<SignatureEntry>();
85
+
86
+ Box::into_raw(Box::new(SignatureArray { items: items_ptr, len }))
87
+ })
88
+ }
89
+
90
+ /// Helper: build signature entries from a `MethodDefinition`.
91
+ fn collect_method_signatures(graph: &Graph, method_def: &MethodDefinition) -> Vec<SignatureEntry> {
92
+ let uri_id = *method_def.uri_id();
93
+ let document = graph.documents().get(&uri_id).expect("document should exist");
94
+
95
+ method_def
96
+ .signatures()
97
+ .as_slice()
98
+ .iter()
99
+ .map(|sig| {
100
+ let param_entries: Vec<ParameterEntry> = sig
101
+ .iter()
102
+ .map(|param| {
103
+ let param_struct = param.inner();
104
+ let name = graph
105
+ .strings()
106
+ .get(param_struct.str())
107
+ .expect("parameter name string should exist");
108
+ let name_str = CString::new(name.as_str()).unwrap().into_raw().cast_const();
109
+
110
+ ParameterEntry {
111
+ name: name_str,
112
+ kind: map_parameter_kind(param),
113
+ location: create_location_for_uri_and_offset(graph, document, param_struct.offset()),
114
+ }
115
+ })
116
+ .collect();
117
+
118
+ let parameters_len = param_entries.len();
119
+ let parameters_ptr = Box::into_raw(param_entries.into_boxed_slice()).cast::<ParameterEntry>();
120
+
121
+ SignatureEntry {
122
+ parameters: parameters_ptr,
123
+ parameters_len,
124
+ }
125
+ })
126
+ .collect()
127
+ }
128
+
129
+ /// Returns signatures for a `MethodAliasDefinition` by following the alias chain.
130
+ /// Always returns a valid `SignatureArray` pointer (possibly with `len == 0`).
131
+ /// Errors during alias resolution (unresolved receivers, circular chains, etc.)
132
+ /// are silently ignored.
133
+ ///
134
+ /// # Safety
135
+ /// - `pointer` must be a valid pointer previously returned by `rdx_graph_new`.
136
+ /// - `definition_id` must be a valid definition id.
137
+ ///
138
+ /// # Panics
139
+ /// Panics if `definition_id` does not exist or is not a `MethodAliasDefinition`.
140
+ #[unsafe(no_mangle)]
141
+ pub unsafe extern "C" fn rdx_method_alias_definition_signatures(
142
+ pointer: GraphPointer,
143
+ definition_id: u64,
144
+ ) -> *mut SignatureArray {
145
+ with_graph(pointer, |graph| {
146
+ let def_id = DefinitionId::new(definition_id);
147
+ let resolved = rubydex::query::follow_method_alias(graph, def_id);
148
+
149
+ let mut sig_entries: Vec<SignatureEntry> = Vec::new();
150
+
151
+ if let Ok(declaration_id) = resolved {
152
+ if let Some(Declaration::Method(method_def)) = graph.declarations().get(&declaration_id) {
153
+ for definition_id in method_def.definitions() {
154
+ if let Some(Definition::Method(method_definition)) = graph.definitions().get(definition_id) {
155
+ sig_entries.extend(collect_method_signatures(graph, method_definition));
156
+ }
157
+ }
158
+ } else {
159
+ panic!("expected a method declaration");
160
+ }
161
+ }
162
+
163
+ let mut boxed = sig_entries.into_boxed_slice();
164
+ let len = boxed.len();
165
+ let items_ptr = boxed.as_mut_ptr();
166
+ std::mem::forget(boxed);
167
+
168
+ Box::into_raw(Box::new(SignatureArray { items: items_ptr, len }))
169
+ })
170
+ }
171
+
172
+ /// Frees a `SignatureArray` previously returned by `rdx_definition_signatures`.
173
+ ///
174
+ /// # Safety
175
+ /// - `ptr` must be a valid pointer previously returned by `rdx_definition_signatures`.
176
+ /// - `ptr` must not be used after being freed.
177
+ #[unsafe(no_mangle)]
178
+ pub unsafe extern "C" fn rdx_definition_signatures_free(ptr: *mut SignatureArray) {
179
+ if ptr.is_null() {
180
+ return;
181
+ }
182
+
183
+ let arr = unsafe { Box::from_raw(ptr) };
184
+
185
+ if arr.items.is_null() || arr.len == 0 {
186
+ return;
187
+ }
188
+
189
+ let slice_ptr = ptr::slice_from_raw_parts_mut(arr.items, arr.len);
190
+ let sig_slice: Box<[SignatureEntry]> = unsafe { Box::from_raw(slice_ptr) };
191
+
192
+ for sig_entry in &*sig_slice {
193
+ if sig_entry.parameters.is_null() || sig_entry.parameters_len == 0 {
194
+ continue;
195
+ }
196
+
197
+ let param_slice_ptr = ptr::slice_from_raw_parts_mut(sig_entry.parameters, sig_entry.parameters_len);
198
+ let param_slice: Box<[ParameterEntry]> = unsafe { Box::from_raw(param_slice_ptr) };
199
+
200
+ for param_entry in &*param_slice {
201
+ if !param_entry.name.is_null() {
202
+ drop(unsafe { CString::from_raw(param_entry.name.cast_mut()) });
203
+ }
204
+ if !param_entry.location.is_null() {
205
+ unsafe { crate::location_api::rdx_location_free(param_entry.location) };
206
+ }
207
+ }
208
+ }
209
+ }
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.1
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shopify
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-05-06 00:00:00.000000000 Z
11
+ date: 2026-05-08 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
@@ -41,6 +41,8 @@ files:
41
41
  - ext/rubydex/reference.c
42
42
  - ext/rubydex/reference.h
43
43
  - ext/rubydex/rubydex.c
44
+ - ext/rubydex/signature.c
45
+ - ext/rubydex/signature.h
44
46
  - ext/rubydex/utils.c
45
47
  - ext/rubydex/utils.h
46
48
  - lib/rubydex.rb
@@ -53,6 +55,7 @@ files:
53
55
  - lib/rubydex/keyword_parameter.rb
54
56
  - lib/rubydex/location.rb
55
57
  - lib/rubydex/mixin.rb
58
+ - lib/rubydex/signature.rb
56
59
  - lib/rubydex/version.rb
57
60
  - rbi/rubydex.rbi
58
61
  - rust/Cargo.lock
@@ -77,6 +80,7 @@ files:
77
80
  - rust/rubydex-sys/src/location_api.rs
78
81
  - rust/rubydex-sys/src/name_api.rs
79
82
  - rust/rubydex-sys/src/reference_api.rs
83
+ - rust/rubydex-sys/src/signature_api.rs
80
84
  - rust/rubydex-sys/src/utils.rs
81
85
  - rust/rubydex/Cargo.toml
82
86
  - rust/rubydex/src/compile_assertions.rs