rubydex 0.2.9 → 0.3.0

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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +69 -3
  3. data/THIRD_PARTY_LICENSES.html +34 -1
  4. data/exe/rdx +131 -55
  5. data/ext/rubydex/declaration.c +1 -1
  6. data/ext/rubydex/definition.c +32 -4
  7. data/ext/rubydex/graph.c +14 -3
  8. data/ext/rubydex/query.c +105 -0
  9. data/ext/rubydex/query.h +8 -0
  10. data/ext/rubydex/reference.c +60 -0
  11. data/ext/rubydex/rubydex.c +2 -0
  12. data/ext/rubydex/utils.c +12 -0
  13. data/ext/rubydex/utils.h +5 -0
  14. data/lib/rubydex/version.rb +1 -1
  15. data/rbi/rubydex.rbi +22 -0
  16. data/rust/Cargo.lock +7 -0
  17. data/rust/rubydex/Cargo.toml +1 -0
  18. data/rust/rubydex/benches/graph_memory.rs +22 -4
  19. data/rust/rubydex/src/compile_assertions.rs +15 -0
  20. data/rust/rubydex/src/diagnostic.rs +1 -1
  21. data/rust/rubydex/src/indexing/rbs_indexer.rs +14 -2
  22. data/rust/rubydex/src/indexing/ruby_indexer.rs +49 -58
  23. data/rust/rubydex/src/indexing/ruby_indexer_tests.rs +72 -16
  24. data/rust/rubydex/src/main.rs +2 -124
  25. data/rust/rubydex/src/model/declaration.rs +0 -11
  26. data/rust/rubydex/src/model/definitions.rs +27 -26
  27. data/rust/rubydex/src/model/document.rs +43 -7
  28. data/rust/rubydex/src/model/graph.rs +40 -28
  29. data/rust/rubydex/src/model/id.rs +55 -0
  30. data/rust/rubydex/src/model/ids.rs +21 -9
  31. data/rust/rubydex/src/model/name.rs +35 -7
  32. data/rust/rubydex/src/model/references.rs +16 -13
  33. data/rust/rubydex/src/operation/ruby_builder.rs +48 -59
  34. data/rust/rubydex/src/query/cypher/schema.rs +790 -0
  35. data/rust/rubydex/src/query/cypher/schema_info.rs +161 -0
  36. data/rust/rubydex/src/query/cypher/tests.rs +228 -0
  37. data/rust/rubydex/src/query/cypher.rs +57 -0
  38. data/rust/rubydex/src/query.rs +2 -0
  39. data/rust/rubydex/src/resolution.rs +248 -227
  40. data/rust/rubydex/src/resolution_tests.rs +263 -65
  41. data/rust/rubydex-sys/src/declaration_api.rs +6 -3
  42. data/rust/rubydex-sys/src/definition_api.rs +27 -7
  43. data/rust/rubydex-sys/src/graph_api.rs +158 -14
  44. data/rust/rubydex-sys/src/reference_api.rs +58 -12
  45. metadata +8 -2
@@ -15,47 +15,51 @@ use rubydex::model::ids::{DeclarationId, NameId, UriId, declaration_id_from_look
15
15
  use rubydex::model::keywords;
16
16
  use rubydex::model::name::NameRef;
17
17
  use rubydex::model::visibility::Visibility;
18
+ use rubydex::query::cypher::{self, OutputFormat};
18
19
  use rubydex::query::{CompletionCandidate, CompletionContext, CompletionReceiver};
19
20
  use rubydex::resolution::Resolver;
20
21
  use rubydex::{indexing, integrity, listing, query};
21
22
  use std::ffi::CString;
22
23
  use std::path::{Path, PathBuf};
23
- use std::{mem, ptr};
24
+ use std::{mem, ptr, sync::RwLock};
24
25
 
25
26
  pub type GraphPointer = *mut c_void;
26
27
 
27
- /// Creates a new graph within a mutex. This is meant to be used when creating new Graph objects in Ruby
28
+ /// Creates a new graph wrapped in a `Box<RwLock<Graph>>` for thread-safe shared access across Ractors.
28
29
  #[unsafe(no_mangle)]
29
30
  pub extern "C" fn rdx_graph_new() -> GraphPointer {
30
- Box::into_raw(Box::new(Graph::new())) as GraphPointer
31
+ Box::into_raw(Box::new(RwLock::new(Graph::new()))) as GraphPointer
31
32
  }
32
33
 
33
34
  /// Frees a Graph through its pointer
34
35
  #[unsafe(no_mangle)]
35
36
  pub extern "C" fn rdx_graph_free(pointer: GraphPointer) {
36
37
  unsafe {
37
- let _ = Box::from_raw(pointer.cast::<Graph>());
38
+ let _ = Box::from_raw(pointer.cast::<RwLock<Graph>>());
38
39
  }
39
40
  }
40
41
 
42
+ /// Runs `action` against the graph referenced by `pointer` under a read lock.
43
+ ///
44
+ /// # Panics
45
+ ///
46
+ /// Panics if the `RwLock` is poisoned (a writer panicked while holding the lock).
41
47
  pub fn with_graph<F, T>(pointer: GraphPointer, action: F) -> T
42
48
  where
43
49
  F: FnOnce(&Graph) -> T,
44
50
  {
45
- let mut graph = unsafe { Box::from_raw(pointer.cast::<Graph>()) };
46
- let result = action(&mut graph);
47
- mem::forget(graph);
48
- result
51
+ let rwlock = unsafe { &*pointer.cast::<RwLock<Graph>>() };
52
+ let guard = rwlock.read().unwrap();
53
+ action(&guard)
49
54
  }
50
55
 
51
56
  fn with_mut_graph<F, T>(pointer: GraphPointer, action: F) -> T
52
57
  where
53
58
  F: FnOnce(&mut Graph) -> T,
54
59
  {
55
- let mut graph = unsafe { Box::from_raw(pointer.cast::<Graph>()) };
56
- let result = action(&mut graph);
57
- mem::forget(graph);
58
- result
60
+ let rwlock = unsafe { &*pointer.cast::<RwLock<Graph>>() };
61
+ let mut guard = rwlock.write().unwrap();
62
+ action(&mut guard)
59
63
  }
60
64
 
61
65
  /// Searches the graph using exact substring matching, returning every declaration whose name matches any of the
@@ -1062,6 +1066,145 @@ pub unsafe extern "C" fn rdx_keyword_get(name: *const c_char) -> *const CKeyword
1062
1066
  }
1063
1067
  }
1064
1068
 
1069
+ /// The result of running a Cypher query, carrying either the formatted output or an error message.
1070
+ #[repr(C)]
1071
+ pub struct CQueryResult {
1072
+ /// Non-null on success; null on error. Caller must free with `free_c_string`.
1073
+ pub output: *const c_char,
1074
+ /// Non-null on error; null on success. Caller must free with `free_c_string`.
1075
+ pub error: *const c_char,
1076
+ }
1077
+
1078
+ impl CQueryResult {
1079
+ fn success(output: &str) -> Self {
1080
+ match CString::new(output) {
1081
+ Ok(c_string) => Self {
1082
+ output: c_string.into_raw().cast_const(),
1083
+ error: ptr::null(),
1084
+ },
1085
+ Err(_) => Self::error("query output contained an interior NUL byte"),
1086
+ }
1087
+ }
1088
+
1089
+ fn error(message: &str) -> Self {
1090
+ Self {
1091
+ output: ptr::null(),
1092
+ error: CString::new(message).map_or(ptr::null(), |s| s.into_raw().cast_const()),
1093
+ }
1094
+ }
1095
+ }
1096
+
1097
+ /// The result of parsing a Cypher query into an opaque, reusable parsed-query object.
1098
+ #[repr(C)]
1099
+ pub struct CParseResult {
1100
+ /// Non-null on success: a heap-allocated parsed query. Free with `rdx_cypher_query_free`.
1101
+ pub query: *mut c_void,
1102
+ /// Non-null on error; null on success. Caller must free with `free_c_string`.
1103
+ pub error: *const c_char,
1104
+ }
1105
+
1106
+ /// Parses a Cypher query string into an opaque parsed-query object, without needing a graph.
1107
+ ///
1108
+ /// On success, `query` is a heap-allocated parsed query that can be executed against a graph with
1109
+ /// `rdx_query_run` and must eventually be freed with `rdx_cypher_query_free`. On failure, `error`
1110
+ /// holds the message.
1111
+ ///
1112
+ /// # Safety
1113
+ ///
1114
+ /// - `query` must be a valid, null-terminated UTF-8 string.
1115
+ #[unsafe(no_mangle)]
1116
+ pub unsafe extern "C" fn rdx_cypher_parse(query: *const c_char) -> CParseResult {
1117
+ let Ok(query_str) = (unsafe { utils::convert_char_ptr_to_string(query) }) else {
1118
+ return CParseResult {
1119
+ query: ptr::null_mut(),
1120
+ error: CString::new("query is not valid UTF-8").map_or(ptr::null(), |s| s.into_raw().cast_const()),
1121
+ };
1122
+ };
1123
+
1124
+ match cypher::parse(&query_str) {
1125
+ Ok(parsed) => CParseResult {
1126
+ query: Box::into_raw(Box::new(parsed)).cast::<c_void>(),
1127
+ error: ptr::null(),
1128
+ },
1129
+ Err(error) => CParseResult {
1130
+ query: ptr::null_mut(),
1131
+ error: CString::new(error.to_string()).map_or(ptr::null(), |s| s.into_raw().cast_const()),
1132
+ },
1133
+ }
1134
+ }
1135
+
1136
+ /// Frees a parsed query previously returned by `rdx_cypher_parse`.
1137
+ ///
1138
+ /// # Safety
1139
+ ///
1140
+ /// - `query` must be a pointer returned by `rdx_cypher_parse`, or null. It must not be used after.
1141
+ #[unsafe(no_mangle)]
1142
+ pub unsafe extern "C" fn rdx_cypher_query_free(query: *mut c_void) {
1143
+ if query.is_null() {
1144
+ return;
1145
+ }
1146
+ let _ = unsafe { Box::from_raw(query.cast::<cypher::Query>()) };
1147
+ }
1148
+
1149
+ /// Executes a previously parsed query (from `rdx_cypher_parse`) against the graph and returns the
1150
+ /// formatted output or an error message. `format` must be `"table"` or `"json"`.
1151
+ ///
1152
+ /// # Safety
1153
+ ///
1154
+ /// - `query` must be a valid pointer returned by `rdx_cypher_parse`.
1155
+ /// - `pointer` must be a valid `GraphPointer` previously returned by this crate.
1156
+ /// - `format` must be a valid, null-terminated UTF-8 string.
1157
+ #[unsafe(no_mangle)]
1158
+ pub unsafe extern "C" fn rdx_query_run(
1159
+ query: *const c_void,
1160
+ pointer: GraphPointer,
1161
+ format: *const c_char,
1162
+ ) -> CQueryResult {
1163
+ if query.is_null() {
1164
+ return CQueryResult::error("query is null");
1165
+ }
1166
+
1167
+ let Ok(format_str) = (unsafe { utils::convert_char_ptr_to_string(format) }) else {
1168
+ return CQueryResult::error("format is not valid UTF-8");
1169
+ };
1170
+
1171
+ let output_format = match format_str.as_str() {
1172
+ "table" => OutputFormat::Table,
1173
+ "json" => OutputFormat::Json,
1174
+ other => {
1175
+ return CQueryResult::error(&format!("unknown query format `{other}` (expected `table` or `json`)"));
1176
+ }
1177
+ };
1178
+
1179
+ let parsed = unsafe { &*query.cast::<cypher::Query>() };
1180
+
1181
+ with_graph(pointer, |graph| {
1182
+ match cypher::run_parsed(graph, parsed, output_format) {
1183
+ Ok(output) => CQueryResult::success(&output),
1184
+ Err(error) => CQueryResult::error(&error.to_string()),
1185
+ }
1186
+ })
1187
+ }
1188
+
1189
+ /// Returns a description of the queryable Cypher schema (node labels, relationship types, and
1190
+ /// properties) in the given format (`"table"` or `"json"`). The schema is static and requires no
1191
+ /// graph. Caller must free the returned pointer with `free_c_string`.
1192
+ ///
1193
+ /// # Safety
1194
+ ///
1195
+ /// - `format` must be a valid, null-terminated UTF-8 string.
1196
+ #[unsafe(no_mangle)]
1197
+ pub unsafe extern "C" fn rdx_cypher_schema(format: *const c_char) -> *const c_char {
1198
+ let format_str = unsafe { utils::convert_char_ptr_to_string(format) }.unwrap_or_else(|_| "table".to_string());
1199
+ let output_format = if format_str == "json" {
1200
+ OutputFormat::Json
1201
+ } else {
1202
+ OutputFormat::Table
1203
+ };
1204
+
1205
+ CString::new(cypher::schema(output_format)).map_or(ptr::null(), |s| s.into_raw().cast_const())
1206
+ }
1207
+
1065
1208
  #[repr(u8)]
1066
1209
  #[derive(Debug, Clone, Copy)]
1067
1210
  pub enum CVisibility {
@@ -1170,7 +1313,7 @@ mod tests {
1170
1313
  .ref_count()
1171
1314
  );
1172
1315
 
1173
- let graph_ptr = Box::into_raw(Box::new(graph)) as GraphPointer;
1316
+ let graph_ptr = Box::into_raw(Box::new(RwLock::new(graph))) as GraphPointer;
1174
1317
 
1175
1318
  // Build the nesting array: ["Foo"] since BAR is inside class Foo
1176
1319
  let nesting_strings = [CString::new("Foo").unwrap()];
@@ -1186,7 +1329,8 @@ mod tests {
1186
1329
  assert_eq!((*decl).id(), *DeclarationId::from("Foo::BAR"));
1187
1330
  };
1188
1331
 
1189
- let graph = unsafe { Box::from_raw(graph_ptr.cast::<Graph>()) };
1332
+ let graph = unsafe { Box::from_raw(graph_ptr.cast::<RwLock<Graph>>()) };
1333
+ let graph = graph.read().unwrap();
1190
1334
 
1191
1335
  assert_eq!(
1192
1336
  1,
@@ -120,7 +120,7 @@ pub unsafe extern "C" fn rdx_method_references_iter_free(iter: *mut MethodRefere
120
120
  unsafe { MethodReferencesIter::free(iter) }
121
121
  }
122
122
 
123
- /// Returns the UTF-8 name string for a constant reference id.
123
+ /// Returns the UTF-8 name string for a constant reference id, or NULL if the reference cannot be found.
124
124
  /// Caller must free with `free_c_string`.
125
125
  ///
126
126
  /// # Safety
@@ -129,12 +129,14 @@ pub unsafe extern "C" fn rdx_method_references_iter_free(iter: *mut MethodRefere
129
129
  ///
130
130
  /// # Panics
131
131
  ///
132
- /// This function will panic if the reference cannot be found.
132
+ /// This function will panic if the reference's name cannot be found.
133
133
  #[unsafe(no_mangle)]
134
134
  pub unsafe extern "C" fn rdx_constant_reference_name(pointer: GraphPointer, reference_id: u64) -> *const c_char {
135
135
  with_graph(pointer, |graph| {
136
136
  let ref_id = ConstantReferenceId::new(reference_id);
137
- let reference = graph.constant_references().get(&ref_id).expect("Reference not found");
137
+ let Some(reference) = graph.constant_references().get(&ref_id) else {
138
+ return ptr::null();
139
+ };
138
140
  let name = graph.names().get(reference.name_id()).expect("Name ID should exist");
139
141
 
140
142
  let name_string = graph
@@ -146,7 +148,7 @@ pub unsafe extern "C" fn rdx_constant_reference_name(pointer: GraphPointer, refe
146
148
  })
147
149
  }
148
150
 
149
- /// Returns the UTF-8 name string for a method reference id.
151
+ /// Returns the UTF-8 name string for a method reference id, or NULL if the reference cannot be found.
150
152
  /// Caller must free with `free_c_string`.
151
153
  ///
152
154
  /// # Safety
@@ -155,12 +157,14 @@ pub unsafe extern "C" fn rdx_constant_reference_name(pointer: GraphPointer, refe
155
157
  ///
156
158
  /// # Panics
157
159
  ///
158
- /// This function will panic if the reference cannot be found.
160
+ /// This function will panic if the reference's name cannot be found.
159
161
  #[unsafe(no_mangle)]
160
162
  pub unsafe extern "C" fn rdx_method_reference_name(pointer: GraphPointer, reference_id: u64) -> *const c_char {
161
163
  with_graph(pointer, |graph| {
162
164
  let ref_id = MethodReferenceId::new(reference_id);
163
- let reference = graph.method_references().get(&ref_id).expect("Reference not found");
165
+ let Some(reference) = graph.method_references().get(&ref_id) else {
166
+ return ptr::null();
167
+ };
164
168
  let name = graph
165
169
  .strings()
166
170
  .get(reference.str())
@@ -170,7 +174,7 @@ pub unsafe extern "C" fn rdx_method_reference_name(pointer: GraphPointer, refere
170
174
  })
171
175
  }
172
176
 
173
- /// Returns a newly allocated `Location` for the given constant reference id.
177
+ /// Returns a newly allocated `Location` for the given constant reference id, or NULL if the reference cannot be found.
174
178
  /// Caller must free the returned pointer with `rdx_location_free`.
175
179
  ///
176
180
  /// # Safety
@@ -180,12 +184,14 @@ pub unsafe extern "C" fn rdx_method_reference_name(pointer: GraphPointer, refere
180
184
  ///
181
185
  /// # Panics
182
186
  ///
183
- /// This function will panic if a reference or document cannot be found.
187
+ /// This function will panic if the reference's document cannot be found.
184
188
  #[unsafe(no_mangle)]
185
189
  pub unsafe extern "C" fn rdx_constant_reference_location(pointer: GraphPointer, reference_id: u64) -> *mut Location {
186
190
  with_graph(pointer, |graph| {
187
191
  let ref_id = ConstantReferenceId::new(reference_id);
188
- let reference = graph.constant_references().get(&ref_id).expect("Reference not found");
192
+ let Some(reference) = graph.constant_references().get(&ref_id) else {
193
+ return ptr::null_mut();
194
+ };
189
195
  let document = graph
190
196
  .documents()
191
197
  .get(&reference.uri_id())
@@ -226,6 +232,25 @@ pub unsafe extern "C" fn rdx_resolved_constant_reference_declaration(
226
232
  })
227
233
  }
228
234
 
235
+ /// Returns a pointer to the URI ID of the document a constant reference belongs
236
+ /// to, or NULL if the reference cannot be found. Caller must free the returned
237
+ /// pointer with `free_u64`.
238
+ ///
239
+ /// # Safety
240
+ /// - `pointer` must be a valid pointer previously returned by `rdx_graph_new`.
241
+ /// - `reference_id` must be a valid reference id.
242
+ #[unsafe(no_mangle)]
243
+ pub unsafe extern "C" fn rdx_constant_reference_document(pointer: GraphPointer, reference_id: u64) -> *const u64 {
244
+ with_graph(pointer, |graph| {
245
+ let ref_id = ConstantReferenceId::new(reference_id);
246
+ if let Some(reference) = graph.constant_references().get(&ref_id) {
247
+ Box::into_raw(Box::new(*reference.uri_id())).cast_const()
248
+ } else {
249
+ ptr::null()
250
+ }
251
+ })
252
+ }
253
+
229
254
  /// Returns the declaration of the resolved receiver for the given method reference. Returns NULL when the method
230
255
  /// reference has no tracked receiver or when the receiver could not be resolved. Caller must free with
231
256
  /// `free_c_declaration`.
@@ -263,7 +288,7 @@ pub unsafe extern "C" fn rdx_method_reference_receiver_declaration(
263
288
  })
264
289
  }
265
290
 
266
- /// Returns a newly allocated `Location` for the given method reference id.
291
+ /// Returns a newly allocated `Location` for the given method reference id, or NULL if the reference cannot be found.
267
292
  /// Caller must free the returned pointer with `rdx_location_free`.
268
293
  ///
269
294
  /// # Safety
@@ -273,12 +298,14 @@ pub unsafe extern "C" fn rdx_method_reference_receiver_declaration(
273
298
  ///
274
299
  /// # Panics
275
300
  ///
276
- /// This function will panic if a reference or document cannot be found.
301
+ /// This function will panic if the reference's document cannot be found.
277
302
  #[unsafe(no_mangle)]
278
303
  pub unsafe extern "C" fn rdx_method_reference_location(pointer: GraphPointer, reference_id: u64) -> *mut Location {
279
304
  with_graph(pointer, |graph| {
280
305
  let ref_id = MethodReferenceId::new(reference_id);
281
- let reference = graph.method_references().get(&ref_id).expect("Reference not found");
306
+ let Some(reference) = graph.method_references().get(&ref_id) else {
307
+ return ptr::null_mut();
308
+ };
282
309
  let document = graph
283
310
  .documents()
284
311
  .get(&reference.uri_id())
@@ -288,6 +315,25 @@ pub unsafe extern "C" fn rdx_method_reference_location(pointer: GraphPointer, re
288
315
  })
289
316
  }
290
317
 
318
+ /// Returns a pointer to the URI ID of the document a method reference belongs
319
+ /// to, or NULL if the reference cannot be found. Caller must free the returned
320
+ /// pointer with `free_u64`.
321
+ ///
322
+ /// # Safety
323
+ /// - `pointer` must be a valid pointer previously returned by `rdx_graph_new`.
324
+ /// - `reference_id` must be a valid reference id.
325
+ #[unsafe(no_mangle)]
326
+ pub unsafe extern "C" fn rdx_method_reference_document(pointer: GraphPointer, reference_id: u64) -> *const u64 {
327
+ with_graph(pointer, |graph| {
328
+ let ref_id = MethodReferenceId::new(reference_id);
329
+ if let Some(reference) = graph.method_references().get(&ref_id) {
330
+ Box::into_raw(Box::new(*reference.uri_id())).cast_const()
331
+ } else {
332
+ ptr::null()
333
+ }
334
+ })
335
+ }
336
+
291
337
  /// Frees a `CConstantReference` previously returned by an FFI function.
292
338
  ///
293
339
  /// # Safety
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.9
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shopify
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-07-09 00:00:00.000000000 Z
11
+ date: 2026-07-22 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
@@ -38,6 +38,8 @@ files:
38
38
  - ext/rubydex/handle.h
39
39
  - ext/rubydex/location.c
40
40
  - ext/rubydex/location.h
41
+ - ext/rubydex/query.c
42
+ - ext/rubydex/query.h
41
43
  - ext/rubydex/reference.c
42
44
  - ext/rubydex/reference.h
43
45
  - ext/rubydex/rubydex.c
@@ -128,6 +130,10 @@ files:
128
130
  - rust/rubydex/src/operation/ruby_builder.rs
129
131
  - rust/rubydex/src/position.rs
130
132
  - rust/rubydex/src/query.rs
133
+ - rust/rubydex/src/query/cypher.rs
134
+ - rust/rubydex/src/query/cypher/schema.rs
135
+ - rust/rubydex/src/query/cypher/schema_info.rs
136
+ - rust/rubydex/src/query/cypher/tests.rs
131
137
  - rust/rubydex/src/resolution.rs
132
138
  - rust/rubydex/src/resolution_tests.rs
133
139
  - rust/rubydex/src/stats.rs