Skip to content

Commit a606ae6

Browse files
authored
Merge pull request #577 from Shopify/02-09-return_array_of_io_errors_from_indexing
Return array of IO errors from indexing
2 parents 451cb47 + 9b1275e commit a606ae6

5 files changed

Lines changed: 52 additions & 48 deletions

File tree

ext/rubydex/graph.c

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
#include "utils.h"
1010

1111
static VALUE cGraph;
12-
static VALUE eIndexingError;
1312

1413
// Free function for the custom Graph allocator. We always have to call into Rust to free data allocated by it
1514
static void graph_free(void *ptr) {
@@ -27,35 +26,39 @@ static VALUE rdxr_graph_alloc(VALUE klass) {
2726
return TypedData_Wrap_Struct(klass, &graph_type, graph);
2827
}
2928

30-
// Graph#index_all: (Array[String] file_paths) -> nil
31-
// Raises IndexingError if anything failed during indexing
29+
// Graph#index_all: (Array[String] file_paths) -> Array[String]
30+
// Returns an array of IO error messages encountered during indexing
3231
static VALUE rdxr_graph_index_all(VALUE self, VALUE file_paths) {
3332
rdxi_check_array_of_strings(file_paths);
3433

3534
// Convert the given file paths into a char** array, so that we can pass to Rust
3635
size_t length = RARRAY_LEN(file_paths);
3736
char **converted_file_paths = rdxi_str_array_to_char(file_paths, length);
3837

39-
// Get the underying graph pointer and then invoke the Rust index all implementation
38+
// Get the underlying graph pointer and then invoke the Rust index all implementation
4039
void *graph;
4140
TypedData_Get_Struct(self, void *, &graph_type, graph);
42-
const char *error_messages = rdx_index_all(graph, (const char **)converted_file_paths, length);
41+
42+
size_t error_count = 0;
43+
const char *const *errors = rdx_index_all(graph, (const char **)converted_file_paths, length, &error_count);
4344

4445
// Free the converted file paths and allow the GC to collect them
4546
for (size_t i = 0; i < length; i++) {
4647
free(converted_file_paths[i]);
4748
}
4849
free(converted_file_paths);
4950

50-
// If indexing errors were returned, turn them into a Ruby string, call Rust to free the CString it allocated and
51-
// return the Ruby string
52-
if (error_messages != NULL) {
53-
VALUE error_string = rb_utf8_str_new_cstr(error_messages);
54-
free_c_string(error_messages);
55-
rb_raise(eIndexingError, "%s", StringValueCStr(error_string));
51+
if (errors == NULL) {
52+
return rb_ary_new();
5653
}
5754

58-
return Qnil;
55+
VALUE array = rb_ary_new_capa((long)error_count);
56+
for (size_t i = 0; i < error_count; i++) {
57+
rb_ary_push(array, rb_utf8_str_new_cstr(errors[i]));
58+
}
59+
60+
free_c_string_array(errors, error_count);
61+
return array;
5962
}
6063

6164
// Size function for the declarations enumerator
@@ -443,9 +446,6 @@ static VALUE rdxr_graph_diagnostics(VALUE self) {
443446
}
444447

445448
void rdxi_initialize_graph(VALUE mRubydex) {
446-
VALUE eRubydexError = rb_const_get(mRubydex, rb_intern("Error"));
447-
eIndexingError = rb_define_class_under(mRubydex, "IndexingError", eRubydexError);
448-
449449
cGraph = rb_define_class_under(mRubydex, "Graph", rb_cObject);
450450
rb_define_alloc_func(cGraph, rdxr_graph_alloc);
451451
rb_define_method(cGraph, "index_all", rdxr_graph_index_all, 1);

lib/rubydex/graph.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def initialize(workspace_path: Dir.pwd)
1919
end
2020

2121
# Index all files and dependencies of the workspace that exists in `@workspace_path`
22-
#: -> String?
22+
#: -> Array[String]
2323
def index_workspace
2424
index_all(workspace_paths)
2525
end

rakelib/index_top_gems.rake

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,10 @@ task index_top_gems: :compile_release do
5151

5252
# Index the gem's files and yield errors back to the main Ractor
5353
graph = Rubydex::Graph.new
54-
error = graph.index_all(Dir.glob("#{gem_dir}/**/*.rb"))
55-
next unless error
54+
errors = graph.index_all(Dir.glob("#{gem_dir}/**/*.rb"))
55+
next if errors.empty?
5656

57-
@mutex.synchronize { @errors << "#{gem} => #{error}" }
57+
@mutex.synchronize { @errors << "#{gem} => #{errors.join(", ")}" }
5858
end
5959
end
6060
end

rust/rubydex-sys/src/graph_api.rs

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,9 @@ pub unsafe extern "C" fn rdx_graph_resolve_constant(
115115
})
116116
}
117117

118-
/// Indexes all given file paths in parallel using the provided Graph pointer
118+
/// Indexes all given file paths in parallel using the provided Graph pointer.
119+
/// Returns an array of error message strings and writes the count to `out_error_count`.
120+
/// Returns NULL if there are no errors. Caller must free with `free_c_string_array`.
119121
///
120122
/// # Panics
121123
///
@@ -130,34 +132,38 @@ pub unsafe extern "C" fn rdx_index_all(
130132
pointer: GraphPointer,
131133
file_paths: *const *const c_char,
132134
count: usize,
133-
) -> *const c_char {
135+
out_error_count: *mut usize,
136+
) -> *const *const c_char {
134137
let file_paths: Vec<String> = unsafe { utils::convert_double_pointer_to_vec(file_paths, count).unwrap() };
135-
let (file_paths, errors) = listing::collect_file_paths(file_paths);
136-
137-
if !errors.is_empty() {
138-
let error_messages = errors
139-
.iter()
140-
.map(std::string::ToString::to_string)
141-
.collect::<Vec<_>>()
142-
.join("\n");
143-
144-
return CString::new(error_messages).unwrap().into_raw().cast_const();
145-
}
138+
let (file_paths, listing_errors) = listing::collect_file_paths(file_paths);
146139

147140
with_mut_graph(pointer, |graph| {
148-
let errors = indexing::index_files(graph, file_paths);
141+
let indexing_errors = indexing::index_files(graph, file_paths);
149142

150-
if !errors.is_empty() {
151-
let error_messages = errors
152-
.iter()
153-
.map(std::string::ToString::to_string)
154-
.collect::<Vec<_>>()
155-
.join("\n");
143+
let all_errors: Vec<String> = listing_errors
144+
.into_iter()
145+
.chain(indexing_errors)
146+
.map(|e| e.to_string())
147+
.collect();
156148

157-
return CString::new(error_messages).unwrap().into_raw().cast_const();
149+
if all_errors.is_empty() {
150+
unsafe { *out_error_count = 0 };
151+
return ptr::null();
158152
}
159153

160-
ptr::null()
154+
let c_strings: Vec<*const c_char> = all_errors
155+
.into_iter()
156+
.filter_map(|string| {
157+
CString::new(string)
158+
.ok()
159+
.map(|c_string| c_string.into_raw().cast_const())
160+
})
161+
.collect();
162+
163+
unsafe { *out_error_count = c_strings.len() };
164+
165+
let boxed = c_strings.into_boxed_slice();
166+
Box::into_raw(boxed).cast::<*const c_char>()
161167
})
162168
}
163169

test/graph_test.rb

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ class GraphTest < Minitest::Test
99
def test_indexing_empty_context
1010
with_context do |context|
1111
graph = Rubydex::Graph.new
12-
assert_nil(graph.index_all(context.glob("**/*.rb")))
12+
assert_empty(graph.index_all(context.glob("**/*.rb")))
1313
end
1414
end
1515

@@ -19,19 +19,17 @@ def test_indexing_context_files
1919
context.write!("bar.rb", "class Bar; end")
2020

2121
graph = Rubydex::Graph.new
22-
assert_nil(graph.index_all(context.glob("**/*.rb")))
22+
assert_empty(graph.index_all(context.glob("**/*.rb")))
2323
end
2424
end
2525

2626
def test_indexing_invalid_file_paths
2727
graph = Rubydex::Graph.new
2828

29-
error = assert_raises(Rubydex::IndexingError) do
30-
graph.index_all(["not_found.rb"])
31-
end
29+
errors = graph.index_all(["not_found.rb"])
3230

33-
assert_kind_of(Rubydex::Error, error)
34-
assert_match(/FileError: Path `.*not_found.rb` does not exist/, error.message)
31+
assert_equal(1, errors.length)
32+
assert_match(/FileError: Path `.*not_found.rb` does not exist/, errors.first)
3533
end
3634

3735
def test_indexing_with_parse_errors

0 commit comments

Comments
 (0)