Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/indexing_top_gems.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ jobs:
key: ${{ runner.os }}-rust

- name: Run Ruby tests
env:
RUST_BACKTRACE: full
run: bundle exec rake index_top_gems

- name: Save Rust compile cache
Expand Down
31 changes: 30 additions & 1 deletion ext/rubydex/graph.c
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include "utils.h"

static VALUE cGraph;
static VALUE mRubydex;

// Free function for the custom Graph allocator. We always have to call into Rust to free data allocated by it
static void graph_free(void *ptr) {
Expand Down Expand Up @@ -449,6 +450,32 @@ static VALUE rdxr_graph_require_paths(VALUE self, VALUE load_path) {
return array;
}

// Graph#check_integrity: () -> Array[Rubydex::IntegrityFailure]
// Returns an array of IntegrityFailure objects, empty if no issues found
static VALUE rdxr_graph_check_integrity(VALUE self) {
void *graph;
TypedData_Get_Struct(self, void *, &graph_type, graph);

size_t error_count = 0;
const char *const *errors = rdx_check_integrity(graph, &error_count);

if (errors == NULL) {
return rb_ary_new();
}

VALUE cIntegrityError = rb_const_get(mRubydex, rb_intern("IntegrityFailure"));
VALUE array = rb_ary_new_capa((long)error_count);

for (size_t i = 0; i < error_count; i++) {
VALUE argv[] = {rb_utf8_str_new_cstr(errors[i])};
VALUE error = rb_class_new_instance(1, argv, cIntegrityError);
rb_ary_push(array, error);
}

free_c_string_array(errors, error_count);
return array;
}

// Graph#diagnostics -> Array[Rubydex::Diagnostic]
static VALUE rdxr_graph_diagnostics(VALUE self) {
void *graph;
Expand Down Expand Up @@ -482,7 +509,8 @@ static VALUE rdxr_graph_diagnostics(VALUE self) {
return diagnostics;
}

void rdxi_initialize_graph(VALUE mRubydex) {
void rdxi_initialize_graph(VALUE moduleRubydex) {
mRubydex = moduleRubydex;
cGraph = rb_define_class_under(mRubydex, "Graph", rb_cObject);
rb_define_alloc_func(cGraph, rdxr_graph_alloc);
rb_define_method(cGraph, "index_all", rdxr_graph_index_all, 1);
Expand All @@ -495,6 +523,7 @@ void rdxi_initialize_graph(VALUE mRubydex) {
rb_define_method(cGraph, "constant_references", rdxr_graph_constant_references, 0);
rb_define_method(cGraph, "method_references", rdxr_graph_method_references, 0);
rb_define_method(cGraph, "diagnostics", rdxr_graph_diagnostics, 0);
rb_define_method(cGraph, "check_integrity", rdxr_graph_check_integrity, 0);
rb_define_method(cGraph, "[]", rdxr_graph_aref, 1);
rb_define_method(cGraph, "search", rdxr_graph_search, 1);
rb_define_method(cGraph, "encoding=", rdxr_graph_set_encoding, 1);
Expand Down
6 changes: 1 addition & 5 deletions lib/rubydex.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,6 @@

require "bundler"
require "uri"

module Rubydex
class Error < StandardError; end
end

require "rubydex/version"

begin
Expand All @@ -18,6 +13,7 @@ class Error < StandardError; end
require "rubydex/rubydex"
end

require "rubydex/failures"
require "rubydex/location"
require "rubydex/comment"
require "rubydex/diagnostic"
Expand Down
15 changes: 15 additions & 0 deletions lib/rubydex/failures.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# frozen_string_literal: true

module Rubydex
class Failure
#: String
attr_reader :message

#: (String) -> void
def initialize(message)
@message = message
end
end

class IntegrityFailure < Failure; end
end
10 changes: 6 additions & 4 deletions lib/rubydex/location.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ module Rubydex
# A zero based internal location. Intended to be used for tool-to-tool communication, such as a language server
# communicating with an editor.
class Location
class NotFileUriError < StandardError; end

include Comparable

#: String
Expand All @@ -22,9 +24,9 @@ def initialize(uri:, start_line:, end_line:, start_column:, end_column:)
end

#: () -> String
def path
def to_file_path
uri = URI(@uri)
raise Rubydex::Error, "URI is not a file:// URI: #{@uri}" unless uri.scheme == "file"
raise NotFileUriError, "URI is not a file:// URI: #{@uri}" unless uri.scheme == "file"

path = uri.path
# TODO: This has to go away once we have a proper URI abstraction
Expand Down Expand Up @@ -59,7 +61,7 @@ def to_display

#: -> String
def to_s
"#{path}:#{@start_line + 1}:#{@start_column + 1}-#{@end_line + 1}:#{@end_column + 1}"
"#{to_file_path}:#{@start_line + 1}:#{@start_column + 1}-#{@end_line + 1}:#{@end_column + 1}"
end
end

Expand All @@ -82,7 +84,7 @@ def comparable_values

#: -> String
def to_s
"#{path}:#{@start_line}:#{@start_column}-#{@end_line}:#{@end_column}"
"#{to_file_path}:#{@start_line}:#{@start_column}-#{@end_line}:#{@end_column}"
end
end
end
7 changes: 5 additions & 2 deletions rakelib/index_top_gems.rake
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

desc "Index the top 100 gems from rubygems.org"
task index_top_gems: :compile_release do
task index_top_gems: :compile do
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
require "net/http"
require "rubygems/package"
Expand Down Expand Up @@ -51,7 +51,10 @@ task index_top_gems: :compile_release do

# Index the gem's files and yield errors back to the main Ractor
graph = Rubydex::Graph.new
errors = graph.index_all(Dir.glob("#{gem_dir}/**/*.rb"))
indexing_errors = graph.index_all(Dir.glob("#{gem_dir}/**/*.rb"))
graph.resolve

errors = indexing_errors + graph.check_integrity.map(&:message)
next if errors.empty?

@mutex.synchronize { @errors << "#{gem} => #{errors.join(", ")}" }
Expand Down
5 changes: 4 additions & 1 deletion rbi/rubydex.rbi
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,9 @@ module Rubydex
sig { returns(T::Array[String]) }
def workspace_paths; end

sig { returns(T::Array[Failure]) }
def check_integrity; end

private

# Gathers the paths we have to index for all workspace dependencies
Expand Down Expand Up @@ -235,7 +238,7 @@ module Rubydex
def end_line; end

sig { returns(String) }
def path; end
def to_file_path; end

sig { returns(Integer) }
def start_column; end
Expand Down
38 changes: 37 additions & 1 deletion rust/rubydex-sys/src/graph_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use rubydex::model::encoding::Encoding;
use rubydex::model::graph::Graph;
use rubydex::model::ids::DeclarationId;
use rubydex::resolution::Resolver;
use rubydex::{indexing, listing, query};
use rubydex::{indexing, integrity, listing, query};
use std::ffi::CString;
use std::path::PathBuf;
use std::{mem, ptr};
Expand Down Expand Up @@ -196,6 +196,42 @@ pub extern "C" fn rdx_graph_resolve(pointer: GraphPointer) {
});
}

/// Checks the integrity of the graph and returns an array of error message strings. Returns NULL if there are no
/// errors. Caller must free with `free_c_string_array`.
///
/// # Safety
///
/// - `pointer` must be a valid `GraphPointer` previously returned by this crate.
/// - `out_error_count` must be a valid, writable pointer.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn rdx_check_integrity(
pointer: GraphPointer,
out_error_count: *mut usize,
) -> *const *const c_char {
with_graph(pointer, |graph| {
let errors = integrity::check_integrity(graph);

if errors.is_empty() {
unsafe { *out_error_count = 0 };
return ptr::null();
}

let c_strings: Vec<*const c_char> = errors
.into_iter()
.filter_map(|error| {
CString::new(error.to_string())
.ok()
.map(|c_string| c_string.into_raw().cast_const())
})
.collect();

unsafe { *out_error_count = c_strings.len() };

let boxed = c_strings.into_boxed_slice();
Box::into_raw(boxed).cast::<*const c_char>()
})
}

/// # Safety
///
/// Expects both the graph pointer and encoding string pointer to be valid
Expand Down
Loading
Loading