rubydex 0.2.9 → 0.4.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 (96) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +110 -6
  3. data/THIRD_PARTY_LICENSES.html +271 -2
  4. data/exe/rdx +2 -73
  5. data/ext/rubydex/config.c +140 -0
  6. data/ext/rubydex/config.h +16 -0
  7. data/ext/rubydex/declaration.c +1 -1
  8. data/ext/rubydex/definition.c +32 -4
  9. data/ext/rubydex/diagnostic.c +75 -1
  10. data/ext/rubydex/diagnostic.h +2 -0
  11. data/ext/rubydex/graph.c +27 -48
  12. data/ext/rubydex/graph.h +6 -0
  13. data/ext/rubydex/query.c +487 -0
  14. data/ext/rubydex/query.h +8 -0
  15. data/ext/rubydex/reference.c +60 -0
  16. data/ext/rubydex/rubydex.c +4 -0
  17. data/ext/rubydex/utils.c +23 -4
  18. data/ext/rubydex/utils.h +5 -0
  19. data/lib/ruby_lsp/rubydex/addon.rb +211 -0
  20. data/lib/rubydex/cli/command/console.rb +55 -0
  21. data/lib/rubydex/cli/command/lint/explain.rb +74 -0
  22. data/lib/rubydex/cli/command/lint.rb +202 -0
  23. data/lib/rubydex/cli/command/mcp.rb +30 -0
  24. data/lib/rubydex/cli/command/query.rb +70 -0
  25. data/lib/rubydex/cli/command/skill.rb +69 -0
  26. data/lib/rubydex/cli/command.rb +168 -0
  27. data/lib/rubydex/cli.rb +93 -0
  28. data/lib/rubydex/config.rb +59 -0
  29. data/lib/rubydex/diagnostic.rb +12 -3
  30. data/lib/rubydex/errors.rb +42 -1
  31. data/lib/rubydex/graph.rb +10 -3
  32. data/lib/rubydex/linter/custom_rule.rb +97 -0
  33. data/lib/rubydex/linter/helpers/path_helpers.rb +78 -0
  34. data/lib/rubydex/linter/helpers/source_access_helpers.rb +31 -0
  35. data/lib/rubydex/linter/rule_loader.rb +36 -0
  36. data/lib/rubydex/linter/rule_test_case.rb +343 -0
  37. data/lib/rubydex/linter/runner.rb +56 -0
  38. data/lib/rubydex/linter.rb +19 -0
  39. data/lib/rubydex/location.rb +3 -0
  40. data/lib/rubydex/mcp_server.rb +1 -2
  41. data/lib/rubydex/related_information.rb +17 -0
  42. data/lib/rubydex/rule.rb +33 -0
  43. data/lib/rubydex/severity.rb +70 -0
  44. data/lib/rubydex/skill.rb +88 -0
  45. data/lib/rubydex/skill_registry.rb +62 -0
  46. data/lib/rubydex/version.rb +1 -1
  47. data/lib/rubydex.rb +6 -0
  48. data/lib/rubydex_linter/rules/rule_structure.rb +125 -0
  49. data/rbi/rubydex.rbi +578 -15
  50. data/rust/Cargo.lock +7 -0
  51. data/rust/rubydex/Cargo.toml +1 -0
  52. data/rust/rubydex/benches/graph_memory.rs +20 -4
  53. data/rust/rubydex/src/compile_assertions.rs +15 -0
  54. data/rust/rubydex/src/config.rs +538 -157
  55. data/rust/rubydex/src/diagnostic.rs +66 -40
  56. data/rust/rubydex/src/errors.rs +0 -1
  57. data/rust/rubydex/src/indexing/local_graph.rs +6 -5
  58. data/rust/rubydex/src/indexing/rbs_indexer.rs +284 -8
  59. data/rust/rubydex/src/indexing/ruby_indexer.rs +59 -70
  60. data/rust/rubydex/src/indexing/ruby_indexer_tests.rs +195 -86
  61. data/rust/rubydex/src/lib.rs +1 -0
  62. data/rust/rubydex/src/listing.rs +26 -1
  63. data/rust/rubydex/src/main.rs +9 -128
  64. data/rust/rubydex/src/model/declaration.rs +301 -229
  65. data/rust/rubydex/src/model/definitions.rs +27 -26
  66. data/rust/rubydex/src/model/document.rs +43 -7
  67. data/rust/rubydex/src/model/graph.rs +67 -68
  68. data/rust/rubydex/src/model/id.rs +55 -0
  69. data/rust/rubydex/src/model/ids.rs +21 -9
  70. data/rust/rubydex/src/model/name.rs +88 -19
  71. data/rust/rubydex/src/model/references.rs +16 -13
  72. data/rust/rubydex/src/operation/ruby_builder.rs +78 -104
  73. data/rust/rubydex/src/path_helpers.rs +77 -0
  74. data/rust/rubydex/src/query/cypher/schema.rs +853 -0
  75. data/rust/rubydex/src/query/cypher/schema_info.rs +161 -0
  76. data/rust/rubydex/src/query/cypher/tests.rs +253 -0
  77. data/rust/rubydex/src/query/cypher.rs +54 -0
  78. data/rust/rubydex/src/query.rs +125 -43
  79. data/rust/rubydex/src/resolution.rs +368 -395
  80. data/rust/rubydex/src/resolution_tests.rs +504 -78
  81. data/rust/rubydex/src/test_utils/context.rs +2 -1
  82. data/rust/rubydex/src/test_utils/graph_test.rs +26 -12
  83. data/rust/rubydex/src/test_utils/local_graph_test.rs +19 -0
  84. data/rust/rubydex/tests/cli.rs +4 -4
  85. data/rust/rubydex-sys/src/config_api.rs +205 -0
  86. data/rust/rubydex-sys/src/cypher_api.rs +791 -0
  87. data/rust/rubydex-sys/src/declaration_api.rs +6 -3
  88. data/rust/rubydex-sys/src/definition_api.rs +27 -7
  89. data/rust/rubydex-sys/src/diagnostic_api.rs +77 -8
  90. data/rust/rubydex-sys/src/graph_api.rs +31 -68
  91. data/rust/rubydex-sys/src/lib.rs +2 -0
  92. data/rust/rubydex-sys/src/name_api.rs +2 -6
  93. data/rust/rubydex-sys/src/reference_api.rs +58 -12
  94. data/rust/rubydex-sys/src/utils.rs +37 -0
  95. data/skills/send-private-method/SKILL.md +133 -0
  96. metadata +37 -2
@@ -291,7 +291,8 @@ pub unsafe extern "C" fn rdx_declaration_singleton_class(pointer: GraphPointer,
291
291
  })
292
292
  }
293
293
 
294
- /// Returns the owner of the declaration (attached object in the case of singleton classes)
294
+ /// Returns the owner of the declaration (attached object in the case of singleton classes), or NULL if the declaration
295
+ /// cannot be found.
295
296
  ///
296
297
  /// # Safety
297
298
  ///
@@ -299,11 +300,13 @@ pub unsafe extern "C" fn rdx_declaration_singleton_class(pointer: GraphPointer,
299
300
  ///
300
301
  /// # Panics
301
302
  ///
302
- /// Will panic if invoked on a non-existing or non-namespace declaration
303
+ /// This function will panic if the owner declaration cannot be found.
303
304
  #[unsafe(no_mangle)]
304
305
  pub unsafe extern "C" fn rdx_declaration_owner(pointer: GraphPointer, decl_id: u64) -> *const CDeclaration {
305
306
  with_graph(pointer, |graph| {
306
- let declaration = graph.declarations().get(&DeclarationId::new(decl_id)).unwrap();
307
+ let Some(declaration) = graph.declarations().get(&DeclarationId::new(decl_id)) else {
308
+ return ptr::null();
309
+ };
307
310
  let owner_id = *declaration.owner_id();
308
311
  Box::into_raw(Box::new(CDeclaration::from_declaration(
309
312
  owner_id,
@@ -156,21 +156,22 @@ pub struct CommentArray {
156
156
  pub len: usize,
157
157
  }
158
158
 
159
- /// Returns a newly allocated array of comments (string and location) for the given definition id.
160
- /// Caller must free the returned pointer with `rdx_definition_comments_free` and each inner string with `free_c_string` if needed.
159
+ /// Returns a newly allocated array of comments (string and location) for the given definition id, or NULL if the
160
+ /// definition cannot be found. Caller must free the returned pointer and its contents with
161
+ /// `rdx_definition_comments_free`.
161
162
  ///
162
163
  /// # Safety
163
164
  /// - `pointer` must be a valid pointer previously returned by `rdx_graph_new`.
164
165
  /// - `definition_id` must be a valid definition id.
165
166
  ///
166
167
  /// # Panics
167
- /// This function will panic if a definition or document cannot be found.
168
+ /// This function will panic if the definition's document cannot be found.
168
169
  #[unsafe(no_mangle)]
169
170
  pub unsafe extern "C" fn rdx_definition_comments(pointer: GraphPointer, definition_id: u64) -> *mut CommentArray {
170
171
  with_graph(pointer, |graph| {
171
172
  let def_id = DefinitionId::new(definition_id);
172
173
  let Some(defn) = graph.definitions().get(&def_id) else {
173
- panic!("Definition not found: {definition_id:?}");
174
+ return ptr::null_mut();
174
175
  };
175
176
 
176
177
  let uri_id = *defn.uri_id();
@@ -229,7 +230,7 @@ pub unsafe extern "C" fn rdx_definition_comments_free(ptr: *mut CommentArray) {
229
230
  // arr is dropped here
230
231
  }
231
232
 
232
- /// Returns a newly allocated `Location` for the given definition id.
233
+ /// Returns a newly allocated `Location` for the given definition id, or NULL if the definition cannot be found.
233
234
  /// Caller must free the returned pointer with `rdx_location_free`.
234
235
  ///
235
236
  /// # Safety
@@ -238,13 +239,13 @@ pub unsafe extern "C" fn rdx_definition_comments_free(ptr: *mut CommentArray) {
238
239
  ///
239
240
  /// # Panics
240
241
  ///
241
- /// This function will panic if a definition or document cannot be found.
242
+ /// This function will panic if the definition's document cannot be found.
242
243
  #[unsafe(no_mangle)]
243
244
  pub unsafe extern "C" fn rdx_definition_location(pointer: GraphPointer, definition_id: u64) -> *mut Location {
244
245
  with_graph(pointer, |graph| {
245
246
  let def_id = DefinitionId::new(definition_id);
246
247
  let Some(defn) = graph.definitions().get(&def_id) else {
247
- panic!("Definition not found: {definition_id:?}");
248
+ return ptr::null_mut();
248
249
  };
249
250
 
250
251
  let document = graph.documents().get(defn.uri_id()).expect("document should exist");
@@ -493,6 +494,25 @@ pub unsafe extern "C" fn rdx_definition_mixins(pointer: GraphPointer, definition
493
494
  })
494
495
  }
495
496
 
497
+ /// Returns a pointer to the URI ID of the document a definition belongs to, or
498
+ /// NULL if the definition cannot be found. Caller must free the returned pointer
499
+ /// with `free_u64`.
500
+ ///
501
+ /// # Safety
502
+ /// - `pointer` must be a valid pointer previously returned by `rdx_graph_new`.
503
+ /// - `definition_id` must be a valid definition id.
504
+ #[unsafe(no_mangle)]
505
+ pub unsafe extern "C" fn rdx_definition_document(pointer: GraphPointer, definition_id: u64) -> *const u64 {
506
+ with_graph(pointer, |graph| {
507
+ let def_id = DefinitionId::new(definition_id);
508
+ if let Some(defn) = graph.definitions().get(&def_id) {
509
+ Box::into_raw(Box::new(**defn.uri_id())).cast_const()
510
+ } else {
511
+ ptr::null()
512
+ }
513
+ })
514
+ }
515
+
496
516
  /// Status of a `MethodAliasDefinition#target` resolution.
497
517
  #[repr(u8)]
498
518
  #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -3,12 +3,87 @@
3
3
  use crate::graph_api::{GraphPointer, with_graph};
4
4
  use crate::location_api::{Location, create_location_for_uri_and_offset};
5
5
  use libc::c_char;
6
+ use rubydex::diagnostic::{Rule, Severity};
6
7
  use std::{ffi::CString, mem, ptr};
7
8
 
9
+ /// C-compatible enum representing diagnostic severity levels.
10
+ #[repr(C)]
11
+ #[derive(Debug, Copy, Clone, PartialEq, Eq)]
12
+ pub enum DiagnosticSeverity {
13
+ Error = 1,
14
+ Warning = 2,
15
+ Information = 3,
16
+ Hint = 4,
17
+ }
18
+
19
+ impl From<Severity> for DiagnosticSeverity {
20
+ fn from(severity: Severity) -> Self {
21
+ match severity {
22
+ Severity::Error => DiagnosticSeverity::Error,
23
+ Severity::Warning => DiagnosticSeverity::Warning,
24
+ Severity::Information => DiagnosticSeverity::Information,
25
+ Severity::Hint => DiagnosticSeverity::Hint,
26
+ }
27
+ }
28
+ }
29
+
30
+ #[repr(C)]
31
+ #[derive(Debug)]
32
+ pub struct CRule {
33
+ pub name: *const c_char,
34
+ pub name_length: usize,
35
+ pub default_severity: DiagnosticSeverity,
36
+ }
37
+
38
+ impl From<Rule> for CRule {
39
+ fn from(rule: Rule) -> Self {
40
+ let name = rule.name();
41
+
42
+ Self {
43
+ name: name.as_ptr().cast::<c_char>(),
44
+ name_length: name.len(),
45
+ default_severity: DiagnosticSeverity::from(rule.default_severity()),
46
+ }
47
+ }
48
+ }
49
+
50
+ #[repr(C)]
51
+ pub struct CRuleArray {
52
+ pub items: *mut CRule,
53
+ pub len: usize,
54
+ }
55
+
56
+ /// Returns every rule the graph can report. Caller must free it with `rdx_rules_free`.
57
+ #[unsafe(no_mangle)]
58
+ pub extern "C" fn rdx_rules() -> CRuleArray {
59
+ let items = Rule::all().iter().copied().map(CRule::from).collect::<Box<[CRule]>>();
60
+
61
+ CRuleArray {
62
+ len: items.len(),
63
+ items: Box::into_raw(items).cast::<CRule>(),
64
+ }
65
+ }
66
+
67
+ /// Frees an array previously returned by `rdx_rules`.
68
+ ///
69
+ /// # Safety
70
+ ///
71
+ /// - `rules` must have been returned by `rdx_rules` and must not be used afterwards.
72
+ #[unsafe(no_mangle)]
73
+ pub unsafe extern "C" fn rdx_rules_free(rules: CRuleArray) {
74
+ if rules.items.is_null() {
75
+ return;
76
+ }
77
+
78
+ unsafe {
79
+ let _ = Box::from_raw(ptr::slice_from_raw_parts_mut(rules.items, rules.len));
80
+ }
81
+ }
82
+
8
83
  /// C-compatible struct representing a diagnostic entry.
9
84
  #[repr(C)]
10
85
  pub struct DiagnosticEntry {
11
- pub rule: *const c_char,
86
+ pub rule: CRule,
12
87
  pub message: *const c_char,
13
88
  pub location: *mut Location,
14
89
  }
@@ -50,10 +125,7 @@ pub unsafe extern "C" fn rdx_graph_diagnostics(pointer: GraphPointer) -> *mut Di
50
125
  let location = create_location_for_uri_and_offset(graph, document, diagnostic.offset());
51
126
 
52
127
  DiagnosticEntry {
53
- rule: CString::new(diagnostic.rule().to_string())
54
- .unwrap()
55
- .into_raw()
56
- .cast_const(),
128
+ rule: CRule::from(*diagnostic.rule()),
57
129
  message: CString::new(diagnostic.message()).unwrap().into_raw().cast_const(),
58
130
  location,
59
131
  }
@@ -82,9 +154,6 @@ pub unsafe extern "C" fn rdx_diagnostics_free(ptr: *mut DiagnosticArray) {
82
154
  let mut boxed_slice: Box<[DiagnosticEntry]> = unsafe { Box::from_raw(slice_ptr) };
83
155
 
84
156
  for entry in &mut *boxed_slice {
85
- if !entry.rule.is_null() {
86
- let _ = unsafe { CString::from_raw(entry.rule.cast_mut()) };
87
- }
88
157
  if !entry.message.is_null() {
89
158
  let _ = unsafe { CString::from_raw(entry.message.cast_mut()) };
90
159
  }
@@ -1,5 +1,6 @@
1
1
  //! This file provides the C API for the Graph object
2
2
 
3
+ use crate::config_api::ConfigPointer;
3
4
  use crate::declaration_api::CDeclaration;
4
5
  use crate::declaration_api::DeclarationsIter;
5
6
  use crate::declaration_api::decl_id_from_char_ptr;
@@ -7,7 +8,7 @@ use crate::document_api::DocumentsIter;
7
8
  use crate::reference_api::{CConstantReference, CMethodReference, ConstantReferencesIter, MethodReferencesIter};
8
9
  use crate::{name_api, utils};
9
10
  use libc::{c_char, c_void};
10
- use rubydex::errors::Errors;
11
+ use rubydex::config::Config;
11
12
  use rubydex::indexing::LanguageId;
12
13
  use rubydex::model::encoding::Encoding;
13
14
  use rubydex::model::graph::Graph;
@@ -19,43 +20,46 @@ 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
- use std::path::{Path, PathBuf};
23
- use std::{mem, ptr};
23
+ use std::path::PathBuf;
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
@@ -212,14 +216,8 @@ pub unsafe extern "C" fn rdx_graph_excluded_patterns(
212
216
 
213
217
  let c_strings: Vec<*const c_char> = excluded
214
218
  .iter()
215
- .filter_map(|path| {
216
- // Normalize all paths to use forward slashes. Otherwise, you get mixed backslashes and forward slashes
217
- // on Windows if a configuration file is using forward slashes. For example:
218
- //
219
- // C:\project/vendor/bundle
220
- let normalized = path.replace(std::path::MAIN_SEPARATOR, "/");
221
-
222
- CString::new(normalized)
219
+ .filter_map(|pattern| {
220
+ CString::new(pattern.as_ref())
223
221
  .ok()
224
222
  .map(|c_string| c_string.into_raw().cast_const())
225
223
  })
@@ -232,23 +230,6 @@ pub unsafe extern "C" fn rdx_graph_excluded_patterns(
232
230
  })
233
231
  }
234
232
 
235
- /// Sets the workspace path used as the root directory for indexing and relative path resolution. Silently ignores the
236
- /// call if the given path is not valid UTF-8, leaving the existing workspace path untouched (mirrors
237
- /// `rdx_graph_set_encoding`). This avoids unwinding across the FFI boundary on malformed input.
238
- ///
239
- /// # Safety
240
- ///
241
- /// - `pointer` must be a valid `GraphPointer` previously returned by this crate.
242
- /// - `path` must be a valid, null-terminated string.
243
- #[unsafe(no_mangle)]
244
- pub unsafe extern "C" fn rdx_graph_set_workspace_path(pointer: GraphPointer, path: *const c_char) {
245
- let Ok(path) = (unsafe { utils::convert_char_ptr_to_string(path) }) else {
246
- return;
247
- };
248
-
249
- with_mut_graph(pointer, |graph| graph.set_workspace_path(PathBuf::from(path)));
250
- }
251
-
252
233
  /// Returns the workspace path as a C string. Caller must free with `free_c_string`.
253
234
  ///
254
235
  /// # Safety
@@ -257,42 +238,23 @@ pub unsafe extern "C" fn rdx_graph_set_workspace_path(pointer: GraphPointer, pat
257
238
  #[unsafe(no_mangle)]
258
239
  pub unsafe extern "C" fn rdx_graph_workspace_path(pointer: GraphPointer) -> *const c_char {
259
240
  with_graph(pointer, |graph| {
260
- CString::new(graph.workspace_path().to_string_lossy().as_ref())
241
+ CString::new(utils::interop_path(graph.workspace_path()))
261
242
  .map_or(ptr::null(), |c_string| c_string.into_raw().cast_const())
262
243
  })
263
244
  }
264
245
 
265
- /// Loads configuration into the graph. A null `config_path` attempts to load the default configuration file.
266
- ///
267
- /// Returns NULL on success. On failure returns an owned, null-terminated error message that the caller must free with
268
- /// `free_c_string`.
269
- ///
270
- /// A `config_path` that is not valid UTF-8 is reported as an error message.
246
+ /// Applies a parsed configuration file to the graph, which adopts the workspace it was loaded for along with the
247
+ /// settings of its `[graph]` section. This is the only way to point the graph at a workspace other than the current
248
+ /// directory, and it replaces any previously applied configuration. Tool-specific sections are ignored.
271
249
  ///
272
250
  /// # Safety
273
251
  ///
274
252
  /// - `pointer` must be a valid `GraphPointer` previously returned by this crate.
275
- /// - `config_path` must either be NULL or a valid, null-terminated string.
253
+ /// - `config` must be a valid `ConfigPointer` previously returned by `rdx_config_load`.
276
254
  #[unsafe(no_mangle)]
277
- pub unsafe extern "C" fn rdx_graph_load_config(pointer: GraphPointer, config_path: *const c_char) -> *const c_char {
278
- let result = with_mut_graph(pointer, |graph| {
279
- if config_path.is_null() {
280
- graph.load_config(None)
281
- } else {
282
- match unsafe { utils::convert_char_ptr_to_string(config_path) } {
283
- Ok(config_path) => graph.load_config(Some(Path::new(&config_path))),
284
- Err(_) => Err(Errors::ConfigError("config file path is not valid UTF-8".to_string())),
285
- }
286
- }
287
- });
288
-
289
- match result {
290
- Ok(()) => ptr::null(),
291
- Err(error) => CString::new(error.to_string())
292
- .unwrap_or_default()
293
- .into_raw()
294
- .cast_const(),
295
- }
255
+ pub unsafe extern "C" fn rdx_graph_load_config(pointer: GraphPointer, config: ConfigPointer) {
256
+ let config = unsafe { &*config.cast::<Config>() };
257
+ with_mut_graph(pointer, |graph| graph.load_config(config));
296
258
  }
297
259
 
298
260
  /// Indexes all given file paths in parallel using the provided Graph pointer.
@@ -1170,7 +1132,7 @@ mod tests {
1170
1132
  .ref_count()
1171
1133
  );
1172
1134
 
1173
- let graph_ptr = Box::into_raw(Box::new(graph)) as GraphPointer;
1135
+ let graph_ptr = Box::into_raw(Box::new(RwLock::new(graph))) as GraphPointer;
1174
1136
 
1175
1137
  // Build the nesting array: ["Foo"] since BAR is inside class Foo
1176
1138
  let nesting_strings = [CString::new("Foo").unwrap()];
@@ -1186,7 +1148,8 @@ mod tests {
1186
1148
  assert_eq!((*decl).id(), *DeclarationId::from("Foo::BAR"));
1187
1149
  };
1188
1150
 
1189
- let graph = unsafe { Box::from_raw(graph_ptr.cast::<Graph>()) };
1151
+ let graph = unsafe { Box::from_raw(graph_ptr.cast::<RwLock<Graph>>()) };
1152
+ let graph = graph.read().unwrap();
1190
1153
 
1191
1154
  assert_eq!(
1192
1155
  1,
@@ -68,6 +68,8 @@ macro_rules! iterator {
68
68
  };
69
69
  }
70
70
 
71
+ pub mod config_api;
72
+ pub mod cypher_api;
71
73
  pub mod declaration_api;
72
74
  pub mod definition_api;
73
75
  pub mod diagnostic_api;
@@ -1,8 +1,4 @@
1
- use rubydex::model::{
2
- graph::Graph,
3
- ids::NameId,
4
- name::{Name, ParentScope},
5
- };
1
+ use rubydex::model::{graph::Graph, ids::NameId, name::ParentScope};
6
2
 
7
3
  /// Takes a constant name and a nesting stack (e.g.: `["Foo", "Bar::Baz", "Qux"]`) and transforms it into a `NameId`,
8
4
  /// registering each required part in the graph. Returns the `NameId` and a list of name ids that need to be untracked
@@ -74,7 +70,7 @@ fn process_qualified_name(
74
70
  };
75
71
 
76
72
  let str_id = graph.intern_string(part.to_owned());
77
- let name_id = graph.add_name(Name::new(str_id, parent_scope, nesting_for_part));
73
+ let name_id = graph.add_name(str_id, parent_scope, nesting_for_part);
78
74
  names_to_untrack.push(name_id);
79
75
  *current_name = ParentScope::Some(name_id);
80
76
  }
@@ -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
@@ -1,5 +1,6 @@
1
1
  use libc::{c_char, size_t};
2
2
  use std::ffi::{CStr, CString};
3
+ use std::path::Path;
3
4
  use std::slice;
4
5
  use std::str::Utf8Error;
5
6
 
@@ -68,3 +69,39 @@ pub unsafe extern "C" fn free_c_string_array(ptr: *const *const c_char, count: u
68
69
  .map(|arg| unsafe { CString::from_raw((*arg).cast_mut()) })
69
70
  .collect();
70
71
  }
72
+
73
+ /// Converts a Rust `&str` to an owned C string (`*const c_char`), suitable for returning across the
74
+ /// FFI boundary. The caller is responsible for freeing it with `free_c_string`.
75
+ ///
76
+ /// # Panics
77
+ ///
78
+ /// Panics if `value` contains an interior null byte, which should never occur in rubydex data.
79
+ #[must_use]
80
+ pub fn cstring_raw(value: &str) -> *const c_char {
81
+ CString::new(value).unwrap().into_raw().cast_const()
82
+ }
83
+
84
+ /// Rust uses backslashes as separators on Windows, but Ruby prefers forward slashes everywhere. We need to make sure
85
+ /// we're maintaining the right separators at the boundary.
86
+ #[must_use]
87
+ pub fn interop_path(path: &Path) -> String {
88
+ path.to_string_lossy().replace(std::path::MAIN_SEPARATOR, "/")
89
+ }
90
+
91
+ #[cfg(test)]
92
+ mod tests {
93
+ use super::*;
94
+
95
+ #[cfg(unix)]
96
+ #[test]
97
+ fn interop_path_leaves_a_unix_path_untouched() {
98
+ // A backslash is an ordinary character in a Unix file name, so rewriting it would name a different file.
99
+ assert_eq!(interop_path(Path::new(r"/tmp/we\ird")), r"/tmp/we\ird");
100
+ }
101
+
102
+ #[cfg(windows)]
103
+ #[test]
104
+ fn interop_path_separates_a_windows_path_with_forward_slashes() {
105
+ assert_eq!(interop_path(Path::new(r"D:\a\_temp\project")), "D:/a/_temp/project");
106
+ }
107
+ }