rubydex 0.2.6 → 0.2.8

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.
@@ -19,6 +19,9 @@ fn prints_help() {
19
19
  .success()
20
20
  .stdout(predicate::str::contains("A Static Analysis Toolkit for Ruby"))
21
21
  .stdout(predicate::str::contains("Usage:"))
22
+ .stdout(predicate::str::contains(
23
+ "If the first path is a directory, it is used as the workspace root for rubydex.toml",
24
+ ))
22
25
  .stdout(predicate::str::contains("--stats"))
23
26
  .stdout(predicate::str::contains("--dot"))
24
27
  .stdout(predicate::str::contains("--stop-after"));
@@ -52,6 +55,69 @@ fn paths_argument_variants() {
52
55
  });
53
56
  }
54
57
 
58
+ #[test]
59
+ fn single_directory_argument_is_workspace_root_for_config() {
60
+ with_context(|context| {
61
+ context.write("included.rb", "class Included\nend\n");
62
+ context.write("excluded/skipped.rb", "class Skipped\nend\n");
63
+ context.write("rubydex.toml", "exclude = [\"excluded\"]\n");
64
+
65
+ rdx(&[context.absolute_path().to_str().unwrap()])
66
+ .success()
67
+ .stderr(predicate::str::is_empty())
68
+ .stdout(predicate::str::contains("Indexed 2 files"));
69
+ });
70
+ }
71
+
72
+ #[test]
73
+ fn first_directory_argument_is_workspace_root_for_config() {
74
+ with_context(|context| {
75
+ context.write("included/kept.rb", "class Included\nend\n");
76
+ context.write("excluded/skipped.rb", "class Skipped\nend\n");
77
+ context.write("rubydex.toml", "exclude = [\"excluded\"]\n");
78
+
79
+ rdx(&[
80
+ context.absolute_path().to_str().unwrap(),
81
+ context.absolute_path_to("included").to_str().unwrap(),
82
+ context.absolute_path_to("excluded").to_str().unwrap(),
83
+ ])
84
+ .success()
85
+ .stderr(predicate::str::is_empty())
86
+ .stdout(predicate::str::contains("Indexed 2 files"));
87
+ });
88
+ }
89
+
90
+ #[test]
91
+ fn single_file_argument_is_not_used_as_workspace_root() {
92
+ with_context(|context| {
93
+ context.write("included.rb", "class Included\nend\n");
94
+ context.write("excluded/skipped.rb", "class Skipped\nend\n");
95
+ context.write("rubydex.toml", "exclude = [\"excluded\"]\n");
96
+
97
+ rdx(&[context.absolute_path_to("excluded/skipped.rb").to_str().unwrap()])
98
+ .success()
99
+ .stderr(predicate::str::is_empty())
100
+ .stdout(predicate::str::contains("Indexed 2 files"));
101
+ });
102
+ }
103
+
104
+ #[test]
105
+ fn first_file_argument_is_not_used_as_workspace_root() {
106
+ with_context(|context| {
107
+ context.write("included.rb", "class Included\nend\n");
108
+ context.write("excluded/skipped.rb", "class Skipped\nend\n");
109
+ context.write("rubydex.toml", "exclude = [\"excluded\"]\n");
110
+
111
+ rdx(&[
112
+ context.absolute_path_to("included.rb").to_str().unwrap(),
113
+ context.absolute_path_to("excluded").to_str().unwrap(),
114
+ ])
115
+ .success()
116
+ .stderr(predicate::str::is_empty())
117
+ .stdout(predicate::str::contains("Indexed 3 files"));
118
+ });
119
+ }
120
+
55
121
  #[test]
56
122
  fn prints_index_metrics() {
57
123
  with_context(|context| {
@@ -241,7 +241,7 @@ impl RubydexServer {
241
241
  .to_string();
242
242
  }
243
243
  };
244
- let ids = rubydex::query::declaration_search(graph, &params.query, &match_mode);
244
+ let ids = rubydex::query::declaration_search(graph, &[params.query.as_str()], &match_mode);
245
245
 
246
246
  let limit = params.limit.filter(|&l| l > 0).unwrap_or(50).min(100); // default 50, max 100
247
247
  let offset = params.offset.unwrap_or(0);
@@ -9,7 +9,7 @@ use crate::definition_api::{DefinitionsIter, rdx_definitions_iter_new_from_ids};
9
9
  use crate::graph_api::{GraphPointer, with_graph};
10
10
  use crate::reference_api::{CConstantReference, CMethodReference, ConstantReferencesIter, MethodReferencesIter};
11
11
  use crate::utils;
12
- use rubydex::model::ids::{DeclarationId, StringId};
12
+ use rubydex::model::ids::{DeclarationId, StringId, declaration_id_from_lookup_name};
13
13
 
14
14
  #[repr(C)]
15
15
  #[derive(Debug, Clone, Copy)]
@@ -80,7 +80,7 @@ pub(crate) unsafe fn decl_id_from_char_ptr(ptr: *const c_char) -> Option<Declara
80
80
  if s.is_empty() {
81
81
  return None;
82
82
  }
83
- Some(DeclarationId::from(s.as_str()))
83
+ Some(declaration_id_from_lookup_name(&s))
84
84
  }
85
85
 
86
86
  /// An iterator over declaration IDs
@@ -7,10 +7,11 @@ 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;
13
- use rubydex::model::ids::{DeclarationId, NameId, UriId};
14
+ use rubydex::model::ids::{DeclarationId, NameId, UriId, declaration_id_from_lookup_name};
14
15
  use rubydex::model::keywords;
15
16
  use rubydex::model::name::NameRef;
16
17
  use rubydex::model::visibility::Visibility;
@@ -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;
@@ -57,64 +58,75 @@ where
57
58
  result
58
59
  }
59
60
 
60
- /// Searches the graph using exact substring matching
61
+ /// Searches the graph using exact substring matching, returning every declaration whose name matches any of the
62
+ /// queries.
61
63
  ///
62
64
  /// # Safety
63
65
  ///
64
- /// Expects both the graph and the query pointers to be valid
66
+ /// Expects `pointer` to be a valid graph and `c_queries` to point to an array of `count` valid, NUL-terminated C
67
+ /// strings. Returns a null pointer when `count` is zero, `c_queries` is null, or any query is not valid UTF-8.
65
68
  #[unsafe(no_mangle)]
66
69
  pub unsafe extern "C" fn rdx_graph_declarations_search(
67
70
  pointer: GraphPointer,
68
- c_query: *const c_char,
71
+ c_queries: *const *const c_char,
72
+ count: usize,
69
73
  ) -> *mut DeclarationsIter {
70
- {
71
- let Ok(query) = (unsafe { utils::convert_char_ptr_to_string(c_query) }) else {
72
- return ptr::null_mut();
73
- };
74
+ if count == 0 || c_queries.is_null() {
75
+ return ptr::null_mut();
76
+ }
74
77
 
75
- let entries = with_graph(pointer, |graph| {
76
- query::declaration_search(graph, &query, &query::MatchMode::Exact)
77
- .into_iter()
78
- .filter_map(|id| {
79
- let decl = graph.declarations().get(&id)?;
80
- Some(CDeclaration::from_declaration(id, decl))
81
- })
82
- .collect::<Vec<CDeclaration>>()
83
- .into_boxed_slice()
84
- });
78
+ let Ok(queries) = (unsafe { utils::convert_double_pointer_to_vec(c_queries, count) }) else {
79
+ return ptr::null_mut();
80
+ };
81
+ let query_refs: Vec<&str> = queries.iter().map(String::as_str).collect();
85
82
 
86
- DeclarationsIter::new(entries)
87
- }
83
+ let entries = with_graph(pointer, |graph| {
84
+ query::declaration_search(graph, &query_refs, &query::MatchMode::Exact)
85
+ .into_iter()
86
+ .filter_map(|id| {
87
+ let decl = graph.declarations().get(&id)?;
88
+ Some(CDeclaration::from_declaration(id, decl))
89
+ })
90
+ .collect::<Vec<CDeclaration>>()
91
+ .into_boxed_slice()
92
+ });
93
+
94
+ DeclarationsIter::new(entries)
88
95
  }
89
96
 
90
- /// Searches the graph using fuzzy matching
97
+ /// Searches the graph using fuzzy matching, returning every declaration whose name matches any of the queries.
91
98
  ///
92
99
  /// # Safety
93
100
  ///
94
- /// Expects both the graph and the query pointers to be valid
101
+ /// Expects `pointer` to be a valid graph and `c_queries` to point to an array of `count` valid, NUL-terminated C
102
+ /// strings. Returns a null pointer when `count` is zero, `c_queries` is null, or any query is not valid UTF-8.
95
103
  #[unsafe(no_mangle)]
96
104
  pub unsafe extern "C" fn rdx_graph_declarations_fuzzy_search(
97
105
  pointer: GraphPointer,
98
- c_query: *const c_char,
106
+ c_queries: *const *const c_char,
107
+ count: usize,
99
108
  ) -> *mut DeclarationsIter {
100
- {
101
- let Ok(query) = (unsafe { utils::convert_char_ptr_to_string(c_query) }) else {
102
- return ptr::null_mut();
103
- };
109
+ if count == 0 || c_queries.is_null() {
110
+ return ptr::null_mut();
111
+ }
104
112
 
105
- let entries = with_graph(pointer, |graph| {
106
- query::declaration_search(graph, &query, &query::MatchMode::Fuzzy)
107
- .into_iter()
108
- .filter_map(|id| {
109
- let decl = graph.declarations().get(&id)?;
110
- Some(CDeclaration::from_declaration(id, decl))
111
- })
112
- .collect::<Vec<CDeclaration>>()
113
- .into_boxed_slice()
114
- });
113
+ let Ok(queries) = (unsafe { utils::convert_double_pointer_to_vec(c_queries, count) }) else {
114
+ return ptr::null_mut();
115
+ };
116
+ let query_refs: Vec<&str> = queries.iter().map(String::as_str).collect();
115
117
 
116
- DeclarationsIter::new(entries)
117
- }
118
+ let entries = with_graph(pointer, |graph| {
119
+ query::declaration_search(graph, &query_refs, &query::MatchMode::Fuzzy)
120
+ .into_iter()
121
+ .filter_map(|id| {
122
+ let decl = graph.declarations().get(&id)?;
123
+ Some(CDeclaration::from_declaration(id, decl))
124
+ })
125
+ .collect::<Vec<CDeclaration>>()
126
+ .into_boxed_slice()
127
+ });
128
+
129
+ DeclarationsIter::new(entries)
118
130
  }
119
131
 
120
132
  /// # Panics
@@ -197,7 +209,13 @@ pub unsafe extern "C" fn rdx_graph_excluded_paths(
197
209
  let c_strings: Vec<*const c_char> = excluded
198
210
  .iter()
199
211
  .filter_map(|path| {
200
- CString::new(path.to_string_lossy().as_ref())
212
+ // Normalize all paths to use forward slashes. Otherwise, you get mixed backslashes and forward slashes
213
+ // on Windows if a configuration file is using forward slashes. For example:
214
+ //
215
+ // C:\project/vendor/bundle
216
+ let normalized = path.to_string_lossy().replace(std::path::MAIN_SEPARATOR, "/");
217
+
218
+ CString::new(normalized)
201
219
  .ok()
202
220
  .map(|c_string| c_string.into_raw().cast_const())
203
221
  })
@@ -210,6 +228,69 @@ pub unsafe extern "C" fn rdx_graph_excluded_paths(
210
228
  })
211
229
  }
212
230
 
231
+ /// Sets the workspace path used as the root directory for indexing and relative path resolution. Silently ignores the
232
+ /// call if the given path is not valid UTF-8, leaving the existing workspace path untouched (mirrors
233
+ /// `rdx_graph_set_encoding`). This avoids unwinding across the FFI boundary on malformed input.
234
+ ///
235
+ /// # Safety
236
+ ///
237
+ /// - `pointer` must be a valid `GraphPointer` previously returned by this crate.
238
+ /// - `path` must be a valid, null-terminated string.
239
+ #[unsafe(no_mangle)]
240
+ pub unsafe extern "C" fn rdx_graph_set_workspace_path(pointer: GraphPointer, path: *const c_char) {
241
+ let Ok(path) = (unsafe { utils::convert_char_ptr_to_string(path) }) else {
242
+ return;
243
+ };
244
+
245
+ with_mut_graph(pointer, |graph| graph.set_workspace_path(PathBuf::from(path)));
246
+ }
247
+
248
+ /// Returns the workspace path as a C string. Caller must free with `free_c_string`.
249
+ ///
250
+ /// # Safety
251
+ ///
252
+ /// - `pointer` must be a valid `GraphPointer` previously returned by this crate.
253
+ #[unsafe(no_mangle)]
254
+ pub unsafe extern "C" fn rdx_graph_workspace_path(pointer: GraphPointer) -> *const c_char {
255
+ with_graph(pointer, |graph| {
256
+ CString::new(graph.workspace_path().to_string_lossy().as_ref())
257
+ .map_or(ptr::null(), |c_string| c_string.into_raw().cast_const())
258
+ })
259
+ }
260
+
261
+ /// Loads configuration into the graph. A null `config_path` attempts to load the default configuration file.
262
+ ///
263
+ /// Returns NULL on success. On failure returns an owned, null-terminated error message that the caller must free with
264
+ /// `free_c_string`.
265
+ ///
266
+ /// A `config_path` that is not valid UTF-8 is reported as an error message.
267
+ ///
268
+ /// # Safety
269
+ ///
270
+ /// - `pointer` must be a valid `GraphPointer` previously returned by this crate.
271
+ /// - `config_path` must either be NULL or a valid, null-terminated string.
272
+ #[unsafe(no_mangle)]
273
+ pub unsafe extern "C" fn rdx_graph_load_config(pointer: GraphPointer, config_path: *const c_char) -> *const c_char {
274
+ let result = with_mut_graph(pointer, |graph| {
275
+ if config_path.is_null() {
276
+ graph.load_config(None)
277
+ } else {
278
+ match unsafe { utils::convert_char_ptr_to_string(config_path) } {
279
+ Ok(config_path) => graph.load_config(Some(Path::new(&config_path))),
280
+ Err(_) => Err(Errors::ConfigError("config file path is not valid UTF-8".to_string())),
281
+ }
282
+ }
283
+ });
284
+
285
+ match result {
286
+ Ok(()) => ptr::null(),
287
+ Err(error) => CString::new(error.to_string())
288
+ .unwrap_or_default()
289
+ .into_raw()
290
+ .cast_const(),
291
+ }
292
+ }
293
+
213
294
  /// Indexes all given file paths in parallel using the provided Graph pointer.
214
295
  /// Returns an array of error message strings and writes the count to `out_error_count`.
215
296
  /// Returns NULL if there are no errors. Caller must free with `free_c_string_array`.
@@ -232,7 +313,7 @@ pub unsafe extern "C" fn rdx_index_all(
232
313
  let file_paths: Vec<String> = unsafe { utils::convert_double_pointer_to_vec(file_paths, count).unwrap() };
233
314
 
234
315
  with_mut_graph(pointer, |graph| {
235
- let (file_paths, listing_errors) = listing::collect_file_paths(file_paths, graph.excluded_paths());
316
+ let (file_paths, listing_errors) = listing::collect_file_paths(file_paths, &graph.excluded_paths());
236
317
  let indexing_errors = indexing::index_files(graph, file_paths, indexing::IndexerBackend::RubyIndexer);
237
318
 
238
319
  let all_errors: Vec<String> = listing_errors
@@ -428,7 +509,7 @@ pub unsafe extern "C" fn rdx_graph_get_declaration(pointer: GraphPointer, name:
428
509
  };
429
510
 
430
511
  with_graph(pointer, |graph| {
431
- let decl_id = DeclarationId::from(name_str.as_str());
512
+ let decl_id = declaration_id_from_lookup_name(&name_str);
432
513
 
433
514
  if let Some(decl) = graph.declarations().get(&decl_id) {
434
515
  Box::into_raw(Box::new(CDeclaration::from_declaration(decl_id, decl))).cast_const()
@@ -847,7 +928,7 @@ pub unsafe extern "C" fn rdx_graph_complete_namespace_access(
847
928
  graph,
848
929
  CompletionReceiver::NamespaceAccess {
849
930
  self_decl_id,
850
- namespace_decl_id: DeclarationId::from(name_str.as_str()),
931
+ namespace_decl_id: declaration_id_from_lookup_name(&name_str),
851
932
  },
852
933
  Vec::new(),
853
934
  )
@@ -881,7 +962,7 @@ pub unsafe extern "C" fn rdx_graph_complete_method_call(
881
962
  graph,
882
963
  CompletionReceiver::MethodCall {
883
964
  self_decl_id,
884
- receiver_decl_id: DeclarationId::from(name_str.as_str()),
965
+ receiver_decl_id: declaration_id_from_lookup_name(&name_str),
885
966
  },
886
967
  Vec::new(),
887
968
  )
@@ -925,7 +1006,7 @@ pub unsafe extern "C" fn rdx_graph_complete_method_argument(
925
1006
  CompletionReceiver::MethodArgument {
926
1007
  self_decl_id,
927
1008
  nesting_name_id,
928
- method_decl_id: DeclarationId::from(name_str.as_str()),
1009
+ method_decl_id: declaration_id_from_lookup_name(&name_str),
929
1010
  },
930
1011
  names_to_untrack,
931
1012
  )
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.6
4
+ version: 0.2.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shopify
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-06-25 00:00:00.000000000 Z
11
+ date: 2026-07-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
@@ -88,7 +88,9 @@ files:
88
88
  - rust/rubydex-sys/src/signature_api.rs
89
89
  - rust/rubydex-sys/src/utils.rs
90
90
  - rust/rubydex/Cargo.toml
91
+ - rust/rubydex/benches/graph_memory.rs
91
92
  - rust/rubydex/src/compile_assertions.rs
93
+ - rust/rubydex/src/config.rs
92
94
  - rust/rubydex/src/diagnostic.rs
93
95
  - rust/rubydex/src/dot.rs
94
96
  - rust/rubydex/src/errors.rs