rubydex 0.1.0.beta3-x86_64-linux → 0.1.0.beta4-x86_64-linux
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.
- checksums.yaml +4 -4
- data/README.md +59 -2
- data/ext/rubydex/graph.c +68 -15
- data/lib/rubydex/3.2/rubydex.so +0 -0
- data/lib/rubydex/3.3/rubydex.so +0 -0
- data/lib/rubydex/3.4/rubydex.so +0 -0
- data/lib/rubydex/4.0/rubydex.so +0 -0
- data/lib/rubydex/graph.rb +44 -16
- data/lib/rubydex/librubydex_sys.so +0 -0
- data/lib/rubydex/version.rb +1 -1
- data/rust/rubydex/src/indexing/rbs_indexer.rs +29 -0
- data/rust/rubydex/src/indexing.rs +19 -9
- data/rust/rubydex/src/main.rs +24 -0
- data/rust/rubydex/src/model/definitions.rs +10 -1
- data/rust/rubydex/src/model/graph.rs +149 -70
- data/rust/rubydex/src/offset.rs +0 -2
- data/rust/rubydex/src/stats/orphan_report.rs +262 -0
- data/rust/rubydex/src/stats.rs +2 -0
- data/rust/rubydex/src/test_utils/graph_test.rs +1 -1
- data/rust/rubydex-sys/src/graph_api.rs +85 -22
- data/rust/rubydex-sys/src/utils.rs +20 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5092c197150819521964a1d1d88c5080da95f9a6ca24e11837acf5dd51b6780b
|
|
4
|
+
data.tar.gz: 3aee088ac6701a796266b4f53a0cdebc67077c67387690c1f74fdbac0cc7b498
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5d5fbd813087afac50aa5a09985a6dfd11a1432a63261931e4ffe78e1a9b39df65a3cb09c62154ef40b740047bded05e649ab4aad44faa9169ce45de7312a495
|
|
7
|
+
data.tar.gz: '09cc63dc59639e3636ff5fff1847c3dd1c12cf05108ee5e25670c7787179b9eb9a0c3f795bb6ab191d590357d1990e5898b7e4086b9bba34ef20d536d66e5808'
|
data/README.md
CHANGED
|
@@ -11,13 +11,70 @@ of using the Ruby API:
|
|
|
11
11
|
```ruby
|
|
12
12
|
# Create a new graph representing the current workspace
|
|
13
13
|
graph = Rubydex::Graph.new
|
|
14
|
+
# Configuring graph LSP encoding
|
|
15
|
+
graph.set_encoding("utf16")
|
|
14
16
|
# Index the entire workspace with all dependencies
|
|
15
17
|
graph.index_workspace
|
|
18
|
+
# Or index specific file paths
|
|
19
|
+
graph.index_all(["path/to/file.rb"])
|
|
16
20
|
# Transform the initially collected information into its semantic understanding by running resolution
|
|
17
21
|
graph.resolve
|
|
22
|
+
# Get all diagnostics acquired during the analysis
|
|
23
|
+
graph.diagnostics
|
|
18
24
|
|
|
19
|
-
#
|
|
20
|
-
graph
|
|
25
|
+
# Iterating over graph nodes
|
|
26
|
+
graph.declarations
|
|
27
|
+
graph.documents
|
|
28
|
+
graph.constant_references
|
|
29
|
+
graph.method_references
|
|
30
|
+
|
|
31
|
+
# Analyzing require paths
|
|
32
|
+
graph.resolve_require_path("rails/engine", load_paths) # => document pointed by `rails/engine`
|
|
33
|
+
graph.require_paths(load_paths) # => array of all indexed require paths
|
|
34
|
+
|
|
35
|
+
# Querying
|
|
36
|
+
graph["Foo"] # Get declaration by fully qualified name
|
|
37
|
+
graph.search("Foo#b") # Name search
|
|
38
|
+
graph.resolve_constant("Bar", ["Foo", "Baz::Qux"]) # Resolve constant reference based on nesting
|
|
39
|
+
|
|
40
|
+
# Declarations
|
|
41
|
+
declaration = graph["Foo"]
|
|
42
|
+
|
|
43
|
+
# All declarations include
|
|
44
|
+
declaration.name
|
|
45
|
+
declaration.unqualified_name
|
|
46
|
+
declaration.definitions
|
|
47
|
+
declaration.owner
|
|
48
|
+
|
|
49
|
+
# Namespace declarations include
|
|
50
|
+
declaration.member("bar()")
|
|
51
|
+
declaration.member("@ivar")
|
|
52
|
+
declaration.singleton_class
|
|
53
|
+
declaration.ancestors
|
|
54
|
+
declaration.descendants
|
|
55
|
+
|
|
56
|
+
# Documents
|
|
57
|
+
document = graph.documents.first
|
|
58
|
+
document.uri
|
|
59
|
+
document.definitions # => list of definitions discovered in this document
|
|
60
|
+
|
|
61
|
+
# Definitions
|
|
62
|
+
definition = declaration.definitions.first
|
|
63
|
+
definition.location
|
|
64
|
+
definition.comments
|
|
65
|
+
definition.name
|
|
66
|
+
definition.deprecated?
|
|
67
|
+
definition.name_location
|
|
68
|
+
|
|
69
|
+
# Locations
|
|
70
|
+
location = definition.location
|
|
71
|
+
location.path
|
|
72
|
+
|
|
73
|
+
# Diagnostics
|
|
74
|
+
diagnostic = graph.diagnostics.first
|
|
75
|
+
diagnostic.rule
|
|
76
|
+
diagnostic.message
|
|
77
|
+
diagnostic.location
|
|
21
78
|
```
|
|
22
79
|
|
|
23
80
|
## Contributing
|
data/ext/rubydex/graph.c
CHANGED
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
#include "utils.h"
|
|
10
10
|
|
|
11
11
|
static VALUE cGraph;
|
|
12
|
-
static VALUE eIndexingError;
|
|
13
12
|
|
|
14
13
|
// Free function for the custom Graph allocator. We always have to call into Rust to free data allocated by it
|
|
15
14
|
static void graph_free(void *ptr) {
|
|
@@ -27,8 +26,8 @@ static VALUE rdxr_graph_alloc(VALUE klass) {
|
|
|
27
26
|
return TypedData_Wrap_Struct(klass, &graph_type, graph);
|
|
28
27
|
}
|
|
29
28
|
|
|
30
|
-
// Graph#index_all: (Array[String] file_paths) ->
|
|
31
|
-
//
|
|
29
|
+
// Graph#index_all: (Array[String] file_paths) -> Array[String]
|
|
30
|
+
// Returns an array of IO error messages encountered during indexing
|
|
32
31
|
static VALUE rdxr_graph_index_all(VALUE self, VALUE file_paths) {
|
|
33
32
|
rdxi_check_array_of_strings(file_paths);
|
|
34
33
|
|
|
@@ -36,10 +35,12 @@ static VALUE rdxr_graph_index_all(VALUE self, VALUE file_paths) {
|
|
|
36
35
|
size_t length = RARRAY_LEN(file_paths);
|
|
37
36
|
char **converted_file_paths = rdxi_str_array_to_char(file_paths, length);
|
|
38
37
|
|
|
39
|
-
// Get the
|
|
38
|
+
// Get the underlying graph pointer and then invoke the Rust index all implementation
|
|
40
39
|
void *graph;
|
|
41
40
|
TypedData_Get_Struct(self, void *, &graph_type, graph);
|
|
42
|
-
|
|
41
|
+
|
|
42
|
+
size_t error_count = 0;
|
|
43
|
+
const char *const *errors = rdx_index_all(graph, (const char **)converted_file_paths, length, &error_count);
|
|
43
44
|
|
|
44
45
|
// Free the converted file paths and allow the GC to collect them
|
|
45
46
|
for (size_t i = 0; i < length; i++) {
|
|
@@ -47,15 +48,17 @@ static VALUE rdxr_graph_index_all(VALUE self, VALUE file_paths) {
|
|
|
47
48
|
}
|
|
48
49
|
free(converted_file_paths);
|
|
49
50
|
|
|
50
|
-
|
|
51
|
-
|
|
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();
|
|
56
53
|
}
|
|
57
54
|
|
|
58
|
-
|
|
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;
|
|
59
62
|
}
|
|
60
63
|
|
|
61
64
|
// Size function for the declarations enumerator
|
|
@@ -272,6 +275,25 @@ static VALUE rdxr_graph_method_references(VALUE self) {
|
|
|
272
275
|
return self;
|
|
273
276
|
}
|
|
274
277
|
|
|
278
|
+
// Graph#delete_document: (String uri) -> Document?
|
|
279
|
+
// Deletes a document and all of its definitions from the graph.
|
|
280
|
+
// Returns the removed Document or nil if it doesn't exist.
|
|
281
|
+
static VALUE rdxr_graph_delete_document(VALUE self, VALUE uri) {
|
|
282
|
+
Check_Type(uri, T_STRING);
|
|
283
|
+
|
|
284
|
+
void *graph;
|
|
285
|
+
TypedData_Get_Struct(self, void *, &graph_type, graph);
|
|
286
|
+
const uint64_t *uri_id = rdx_graph_delete_document(graph, StringValueCStr(uri));
|
|
287
|
+
|
|
288
|
+
if (uri_id == NULL) {
|
|
289
|
+
return Qnil;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
VALUE argv[] = {self, ULL2NUM(*uri_id)};
|
|
293
|
+
free_u64(uri_id);
|
|
294
|
+
return rb_class_new_instance(2, argv, cDocument);
|
|
295
|
+
}
|
|
296
|
+
|
|
275
297
|
// Graph#resolve: () -> self
|
|
276
298
|
// Runs the resolver to compute declarations and ownership
|
|
277
299
|
static VALUE rdxr_graph_resolve(VALUE self) {
|
|
@@ -358,6 +380,38 @@ static VALUE rdxr_graph_resolve_require_path(VALUE self, VALUE require_path, VAL
|
|
|
358
380
|
return rb_class_new_instance(2, argv, cDocument);
|
|
359
381
|
}
|
|
360
382
|
|
|
383
|
+
// Graph#require_paths: (Array[String] load_path) -> Array[String]
|
|
384
|
+
// Returns all require paths for completion.
|
|
385
|
+
static VALUE rdxr_graph_require_paths(VALUE self, VALUE load_path) {
|
|
386
|
+
rdxi_check_array_of_strings(load_path);
|
|
387
|
+
|
|
388
|
+
void *graph;
|
|
389
|
+
TypedData_Get_Struct(self, void *, &graph_type, graph);
|
|
390
|
+
|
|
391
|
+
size_t paths_len = RARRAY_LEN(load_path);
|
|
392
|
+
char **converted_paths = rdxi_str_array_to_char(load_path, paths_len);
|
|
393
|
+
|
|
394
|
+
size_t out_count = 0;
|
|
395
|
+
const char *const *results = rdx_require_paths(graph, (const char **)converted_paths, paths_len, &out_count);
|
|
396
|
+
|
|
397
|
+
for (size_t i = 0; i < paths_len; i++) {
|
|
398
|
+
free(converted_paths[i]);
|
|
399
|
+
}
|
|
400
|
+
free(converted_paths);
|
|
401
|
+
|
|
402
|
+
if (results == NULL) {
|
|
403
|
+
return rb_ary_new();
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
VALUE array = rb_ary_new_capa((long)out_count);
|
|
407
|
+
for (size_t i = 0; i < out_count; i++) {
|
|
408
|
+
rb_ary_push(array, rb_utf8_str_new_cstr(results[i]));
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
free_c_string_array(results, out_count);
|
|
412
|
+
return array;
|
|
413
|
+
}
|
|
414
|
+
|
|
361
415
|
// Graph#diagnostics -> Array[Rubydex::Diagnostic]
|
|
362
416
|
static VALUE rdxr_graph_diagnostics(VALUE self) {
|
|
363
417
|
void *graph;
|
|
@@ -392,12 +446,10 @@ static VALUE rdxr_graph_diagnostics(VALUE self) {
|
|
|
392
446
|
}
|
|
393
447
|
|
|
394
448
|
void rdxi_initialize_graph(VALUE mRubydex) {
|
|
395
|
-
VALUE eRubydexError = rb_const_get(mRubydex, rb_intern("Error"));
|
|
396
|
-
eIndexingError = rb_define_class_under(mRubydex, "IndexingError", eRubydexError);
|
|
397
|
-
|
|
398
449
|
cGraph = rb_define_class_under(mRubydex, "Graph", rb_cObject);
|
|
399
450
|
rb_define_alloc_func(cGraph, rdxr_graph_alloc);
|
|
400
451
|
rb_define_method(cGraph, "index_all", rdxr_graph_index_all, 1);
|
|
452
|
+
rb_define_method(cGraph, "delete_document", rdxr_graph_delete_document, 1);
|
|
401
453
|
rb_define_method(cGraph, "resolve", rdxr_graph_resolve, 0);
|
|
402
454
|
rb_define_method(cGraph, "resolve_constant", rdxr_graph_resolve_constant, 2);
|
|
403
455
|
rb_define_method(cGraph, "declarations", rdxr_graph_declarations, 0);
|
|
@@ -409,4 +461,5 @@ void rdxi_initialize_graph(VALUE mRubydex) {
|
|
|
409
461
|
rb_define_method(cGraph, "search", rdxr_graph_search, 1);
|
|
410
462
|
rb_define_method(cGraph, "set_encoding", rdxr_graph_set_encoding, 1);
|
|
411
463
|
rb_define_method(cGraph, "resolve_require_path", rdxr_graph_resolve_require_path, 2);
|
|
464
|
+
rb_define_method(cGraph, "require_paths", rdxr_graph_require_paths, 1);
|
|
412
465
|
}
|
data/lib/rubydex/3.2/rubydex.so
CHANGED
|
Binary file
|
data/lib/rubydex/3.3/rubydex.so
CHANGED
|
Binary file
|
data/lib/rubydex/3.4/rubydex.so
CHANGED
|
Binary file
|
data/lib/rubydex/4.0/rubydex.so
CHANGED
|
Binary file
|
data/lib/rubydex/graph.rb
CHANGED
|
@@ -5,40 +5,68 @@ module Rubydex
|
|
|
5
5
|
#
|
|
6
6
|
# Note: this class is partially defined in C to integrate with the Rust backend
|
|
7
7
|
class Graph
|
|
8
|
+
IGNORED_DIRECTORIES = [
|
|
9
|
+
".bundle",
|
|
10
|
+
".git",
|
|
11
|
+
".github",
|
|
12
|
+
"node_modules",
|
|
13
|
+
"tmp",
|
|
14
|
+
].freeze
|
|
15
|
+
|
|
8
16
|
#: (?workspace_path: String) -> void
|
|
9
17
|
def initialize(workspace_path: Dir.pwd)
|
|
10
18
|
@workspace_path = workspace_path
|
|
11
19
|
end
|
|
12
20
|
|
|
13
21
|
# Index all files and dependencies of the workspace that exists in `@workspace_path`
|
|
14
|
-
#: -> String
|
|
22
|
+
#: -> Array[String]
|
|
15
23
|
def index_workspace
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
index_all(workspace_paths)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Returns all workspace paths that should be indexed, excluding directories that we don't need to descend into such
|
|
28
|
+
# as `.git`, `node_modules`. Also includes any top level Ruby files
|
|
29
|
+
#
|
|
30
|
+
#: -> Array[String]
|
|
31
|
+
def workspace_paths
|
|
32
|
+
paths = []
|
|
33
|
+
|
|
34
|
+
Dir.each_child(@workspace_path) do |entry|
|
|
35
|
+
full_path = File.join(@workspace_path, entry)
|
|
36
|
+
|
|
37
|
+
if File.directory?(full_path)
|
|
38
|
+
paths << full_path unless IGNORED_DIRECTORIES.include?(entry)
|
|
39
|
+
elsif File.extname(entry) == ".rb"
|
|
40
|
+
paths << full_path
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
add_workspace_dependency_paths(paths)
|
|
45
|
+
paths.uniq!
|
|
46
|
+
paths
|
|
23
47
|
end
|
|
24
48
|
|
|
25
49
|
private
|
|
26
50
|
|
|
27
51
|
# Gathers the paths we have to index for all workspace dependencies
|
|
28
|
-
|
|
52
|
+
#: (Array[String]) -> void
|
|
53
|
+
def add_workspace_dependency_paths(paths)
|
|
29
54
|
specs = Bundler.locked_gems&.specs
|
|
30
|
-
return
|
|
55
|
+
return unless specs
|
|
31
56
|
|
|
32
|
-
|
|
57
|
+
specs.each do |lazy_spec|
|
|
33
58
|
spec = Gem::Specification.find_by_name(lazy_spec.name)
|
|
34
|
-
spec.require_paths.
|
|
59
|
+
spec.require_paths.each do |path|
|
|
60
|
+
# For native extensions, RubyGems inserts an absolute require path pointing to
|
|
61
|
+
# `gems/some-gem-1.0.0/extensions`. Those paths don't actually include any Ruby files inside, so we can skip
|
|
62
|
+
# descending them
|
|
63
|
+
next if File.absolute_path?(path)
|
|
64
|
+
|
|
65
|
+
paths << File.join(spec.full_gem_path, path)
|
|
66
|
+
end
|
|
35
67
|
rescue Gem::MissingSpecError
|
|
36
68
|
nil
|
|
37
69
|
end
|
|
38
|
-
|
|
39
|
-
paths.flatten!
|
|
40
|
-
paths.uniq!
|
|
41
|
-
paths
|
|
42
70
|
end
|
|
43
71
|
end
|
|
44
72
|
end
|
|
Binary file
|
data/lib/rubydex/version.rb
CHANGED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
//! Visit the RBS AST and create type definitions.
|
|
2
|
+
|
|
3
|
+
use crate::{
|
|
4
|
+
indexing::local_graph::LocalGraph,
|
|
5
|
+
model::{document::Document, ids::UriId},
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
pub struct RBSIndexer {
|
|
9
|
+
#[allow(dead_code)]
|
|
10
|
+
uri_id: UriId,
|
|
11
|
+
local_graph: LocalGraph,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
impl RBSIndexer {
|
|
15
|
+
#[must_use]
|
|
16
|
+
pub fn new(uri: String, source: &str) -> Self {
|
|
17
|
+
let uri_id = UriId::from(&uri);
|
|
18
|
+
let local_graph = LocalGraph::new(uri_id, Document::new(uri, source));
|
|
19
|
+
|
|
20
|
+
Self { uri_id, local_graph }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
#[must_use]
|
|
24
|
+
pub fn local_graph(self) -> LocalGraph {
|
|
25
|
+
self.local_graph
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
pub fn index(&mut self) {}
|
|
29
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
use crate::{
|
|
2
2
|
errors::Errors,
|
|
3
|
-
indexing::{local_graph::LocalGraph, ruby_indexer::RubyIndexer},
|
|
3
|
+
indexing::{local_graph::LocalGraph, rbs_indexer::RBSIndexer, ruby_indexer::RubyIndexer},
|
|
4
4
|
job_queue::{Job, JobQueue},
|
|
5
5
|
model::graph::Graph,
|
|
6
6
|
};
|
|
@@ -9,16 +9,17 @@ use std::{fs, path::PathBuf, sync::Arc};
|
|
|
9
9
|
use url::Url;
|
|
10
10
|
|
|
11
11
|
pub mod local_graph;
|
|
12
|
+
pub mod rbs_indexer;
|
|
12
13
|
pub mod ruby_indexer;
|
|
13
14
|
|
|
14
|
-
/// Job that indexes a single
|
|
15
|
-
pub struct
|
|
15
|
+
/// Job that indexes a single file
|
|
16
|
+
pub struct IndexingJob {
|
|
16
17
|
path: PathBuf,
|
|
17
18
|
local_graph_tx: Sender<LocalGraph>,
|
|
18
19
|
errors_tx: Sender<Errors>,
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
impl
|
|
22
|
+
impl IndexingJob {
|
|
22
23
|
#[must_use]
|
|
23
24
|
pub fn new(path: PathBuf, local_graph_tx: Sender<LocalGraph>, errors_tx: Sender<Errors>) -> Self {
|
|
24
25
|
Self {
|
|
@@ -35,7 +36,7 @@ impl IndexingRubyFileJob {
|
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
impl Job for
|
|
39
|
+
impl Job for IndexingJob {
|
|
39
40
|
fn run(&self) {
|
|
40
41
|
let Ok(source) = fs::read_to_string(&self.path) else {
|
|
41
42
|
self.send_error(Errors::FileError(format!(
|
|
@@ -55,11 +56,20 @@ impl Job for IndexingRubyFileJob {
|
|
|
55
56
|
return;
|
|
56
57
|
};
|
|
57
58
|
|
|
58
|
-
let
|
|
59
|
-
|
|
59
|
+
let local_graph = if let Some(ext) = self.path.extension()
|
|
60
|
+
&& ext == "rbs"
|
|
61
|
+
{
|
|
62
|
+
let mut indexer = RBSIndexer::new(url.to_string(), &source);
|
|
63
|
+
indexer.index();
|
|
64
|
+
indexer.local_graph()
|
|
65
|
+
} else {
|
|
66
|
+
let mut indexer = RubyIndexer::new(url.to_string(), &source);
|
|
67
|
+
indexer.index();
|
|
68
|
+
indexer.local_graph()
|
|
69
|
+
};
|
|
60
70
|
|
|
61
71
|
self.local_graph_tx
|
|
62
|
-
.send(
|
|
72
|
+
.send(local_graph)
|
|
63
73
|
.expect("graph receiver dropped before merge");
|
|
64
74
|
}
|
|
65
75
|
}
|
|
@@ -75,7 +85,7 @@ pub fn index_files(graph: &mut Graph, paths: Vec<PathBuf>) -> Vec<Errors> {
|
|
|
75
85
|
let (errors_tx, errors_rx) = unbounded();
|
|
76
86
|
|
|
77
87
|
for path in paths {
|
|
78
|
-
queue.push(Box::new(
|
|
88
|
+
queue.push(Box::new(IndexingJob::new(
|
|
79
89
|
path,
|
|
80
90
|
local_graphs_tx.clone(),
|
|
81
91
|
errors_tx.clone(),
|
data/rust/rubydex/src/main.rs
CHANGED
|
@@ -27,6 +27,16 @@ struct Args {
|
|
|
27
27
|
|
|
28
28
|
#[arg(long = "stats", help = "Show detailed performance statistics")]
|
|
29
29
|
stats: bool,
|
|
30
|
+
|
|
31
|
+
#[arg(
|
|
32
|
+
long = "report-orphans",
|
|
33
|
+
value_name = "PATH",
|
|
34
|
+
num_args = 0..=1,
|
|
35
|
+
require_equals = true,
|
|
36
|
+
default_missing_value = "/tmp/rubydex-orphan-report.txt",
|
|
37
|
+
help = "Write orphan definitions report to specified file"
|
|
38
|
+
)]
|
|
39
|
+
report_orphans: Option<String>,
|
|
30
40
|
}
|
|
31
41
|
|
|
32
42
|
#[derive(Debug, Clone, ValueEnum)]
|
|
@@ -101,6 +111,20 @@ fn main() {
|
|
|
101
111
|
MemoryStats::print_memory_usage();
|
|
102
112
|
}
|
|
103
113
|
|
|
114
|
+
// Orphan report
|
|
115
|
+
if let Some(ref path) = args.report_orphans {
|
|
116
|
+
match std::fs::File::create(path) {
|
|
117
|
+
Ok(mut file) => {
|
|
118
|
+
if let Err(e) = graph.write_orphan_report(&mut file) {
|
|
119
|
+
eprintln!("Failed to write orphan report: {e}");
|
|
120
|
+
} else {
|
|
121
|
+
println!("Orphan report written to {path}");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
Err(e) => eprintln!("Failed to create orphan report file: {e}"),
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
104
128
|
// Generate visualization or print statistics
|
|
105
129
|
if args.visualize {
|
|
106
130
|
println!("{}", dot::generate(&graph));
|
|
@@ -142,7 +142,16 @@ impl Definition {
|
|
|
142
142
|
Definition::SingletonClass(d) => Some(d.name_id()),
|
|
143
143
|
Definition::Module(d) => Some(d.name_id()),
|
|
144
144
|
Definition::Constant(d) => Some(d.name_id()),
|
|
145
|
-
|
|
145
|
+
Definition::ConstantAlias(d) => Some(d.name_id()),
|
|
146
|
+
Definition::GlobalVariable(_)
|
|
147
|
+
| Definition::InstanceVariable(_)
|
|
148
|
+
| Definition::ClassVariable(_)
|
|
149
|
+
| Definition::AttrAccessor(_)
|
|
150
|
+
| Definition::AttrReader(_)
|
|
151
|
+
| Definition::AttrWriter(_)
|
|
152
|
+
| Definition::Method(_)
|
|
153
|
+
| Definition::MethodAlias(_)
|
|
154
|
+
| Definition::GlobalVariableAlias(_) => None,
|
|
146
155
|
}
|
|
147
156
|
}
|
|
148
157
|
|
|
@@ -191,60 +191,42 @@ impl Graph {
|
|
|
191
191
|
Definition::ConstantAlias(it) => {
|
|
192
192
|
return self.name_id_to_declaration_id(*it.name_id());
|
|
193
193
|
}
|
|
194
|
-
Definition::GlobalVariable(it) =>
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
Definition::
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
Definition::
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
Definition::AttrWriter(it) => {
|
|
231
|
-
let nesting_definition = it
|
|
232
|
-
.lexical_nesting_id()
|
|
233
|
-
.and_then(|id| self.definitions().get(&id).unwrap().name_id());
|
|
234
|
-
(nesting_definition, it.str_id())
|
|
235
|
-
}
|
|
236
|
-
Definition::Method(it) => {
|
|
237
|
-
let nesting_definition = it
|
|
238
|
-
.lexical_nesting_id()
|
|
239
|
-
.and_then(|id| self.definitions().get(&id).unwrap().name_id());
|
|
240
|
-
(nesting_definition, it.str_id())
|
|
241
|
-
}
|
|
242
|
-
Definition::MethodAlias(it) => {
|
|
243
|
-
let nesting_definition = it
|
|
244
|
-
.lexical_nesting_id()
|
|
245
|
-
.and_then(|id| self.definitions().get(&id).unwrap().name_id());
|
|
246
|
-
(nesting_definition, it.new_name_str_id())
|
|
247
|
-
}
|
|
194
|
+
Definition::GlobalVariable(it) => (
|
|
195
|
+
self.find_enclosing_namespace_name_id(it.lexical_nesting_id().as_ref()),
|
|
196
|
+
it.str_id(),
|
|
197
|
+
),
|
|
198
|
+
Definition::GlobalVariableAlias(it) => (
|
|
199
|
+
self.find_enclosing_namespace_name_id(it.lexical_nesting_id().as_ref()),
|
|
200
|
+
it.new_name_str_id(),
|
|
201
|
+
),
|
|
202
|
+
Definition::InstanceVariable(it) => (
|
|
203
|
+
self.find_enclosing_namespace_name_id(it.lexical_nesting_id().as_ref()),
|
|
204
|
+
it.str_id(),
|
|
205
|
+
),
|
|
206
|
+
Definition::ClassVariable(it) => (
|
|
207
|
+
self.find_enclosing_namespace_name_id(it.lexical_nesting_id().as_ref()),
|
|
208
|
+
it.str_id(),
|
|
209
|
+
),
|
|
210
|
+
Definition::AttrAccessor(it) => (
|
|
211
|
+
self.find_enclosing_namespace_name_id(it.lexical_nesting_id().as_ref()),
|
|
212
|
+
it.str_id(),
|
|
213
|
+
),
|
|
214
|
+
Definition::AttrReader(it) => (
|
|
215
|
+
self.find_enclosing_namespace_name_id(it.lexical_nesting_id().as_ref()),
|
|
216
|
+
it.str_id(),
|
|
217
|
+
),
|
|
218
|
+
Definition::AttrWriter(it) => (
|
|
219
|
+
self.find_enclosing_namespace_name_id(it.lexical_nesting_id().as_ref()),
|
|
220
|
+
it.str_id(),
|
|
221
|
+
),
|
|
222
|
+
Definition::Method(it) => (
|
|
223
|
+
self.find_enclosing_namespace_name_id(it.lexical_nesting_id().as_ref()),
|
|
224
|
+
it.str_id(),
|
|
225
|
+
),
|
|
226
|
+
Definition::MethodAlias(it) => (
|
|
227
|
+
self.find_enclosing_namespace_name_id(it.lexical_nesting_id().as_ref()),
|
|
228
|
+
it.new_name_str_id(),
|
|
229
|
+
),
|
|
248
230
|
};
|
|
249
231
|
|
|
250
232
|
let nesting_declaration_id = match nesting_name_id {
|
|
@@ -253,13 +235,28 @@ impl Graph {
|
|
|
253
235
|
}?;
|
|
254
236
|
|
|
255
237
|
self.declarations
|
|
256
|
-
.get(nesting_declaration_id)
|
|
257
|
-
.
|
|
258
|
-
.as_namespace()
|
|
259
|
-
.unwrap()
|
|
238
|
+
.get(nesting_declaration_id)?
|
|
239
|
+
.as_namespace()?
|
|
260
240
|
.member(member_str_id)
|
|
261
241
|
}
|
|
262
242
|
|
|
243
|
+
/// Finds the closest namespace name ID to connect a definition to its declaration
|
|
244
|
+
fn find_enclosing_namespace_name_id(&self, starting_id: Option<&DefinitionId>) -> Option<&NameId> {
|
|
245
|
+
let mut current = starting_id;
|
|
246
|
+
|
|
247
|
+
while let Some(id) = current {
|
|
248
|
+
let def = self.definitions.get(id).unwrap();
|
|
249
|
+
|
|
250
|
+
if let Some(name_id) = def.name_id() {
|
|
251
|
+
return Some(name_id);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
current = def.lexical_nesting_id().as_ref();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
None
|
|
258
|
+
}
|
|
259
|
+
|
|
263
260
|
#[must_use]
|
|
264
261
|
pub fn name_id_to_declaration_id(&self, name_id: NameId) -> Option<&DeclarationId> {
|
|
265
262
|
let name = self.names.get(&name_id);
|
|
@@ -515,11 +512,13 @@ impl Graph {
|
|
|
515
512
|
}
|
|
516
513
|
}
|
|
517
514
|
|
|
518
|
-
|
|
519
|
-
|
|
515
|
+
/// Handles the deletion of a document identified by `uri`.
|
|
516
|
+
/// Returns the `UriId` of the removed document, or `None` if it didn't exist.
|
|
517
|
+
pub fn delete_document(&mut self, uri: &str) -> Option<UriId> {
|
|
520
518
|
let uri_id = UriId::from(uri);
|
|
521
|
-
self.
|
|
522
|
-
self.
|
|
519
|
+
let document = self.documents.remove(&uri_id)?;
|
|
520
|
+
self.remove_definitions_for_document(&document);
|
|
521
|
+
Some(uri_id)
|
|
523
522
|
}
|
|
524
523
|
|
|
525
524
|
/// Merges everything in `other` into this Graph. This method is meant to merge all graph representations from
|
|
@@ -581,18 +580,17 @@ impl Graph {
|
|
|
581
580
|
// For each URI that was indexed through `other`, check what was discovered and update our current global
|
|
582
581
|
// representation
|
|
583
582
|
let uri_id = other.uri_id();
|
|
584
|
-
self.
|
|
583
|
+
if let Some(document) = self.documents.remove(&uri_id) {
|
|
584
|
+
self.remove_definitions_for_document(&document);
|
|
585
|
+
}
|
|
585
586
|
|
|
586
587
|
self.extend(other);
|
|
587
588
|
}
|
|
588
589
|
|
|
589
|
-
// Removes all nodes and relationships associated to the given
|
|
590
|
-
// document
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
return;
|
|
594
|
-
};
|
|
595
|
-
|
|
590
|
+
// Removes all nodes and relationships associated to the given document. This is used to clean up stale data when a
|
|
591
|
+
// document changes or when a document is deleted and we need to clean up the memory.
|
|
592
|
+
// The document must already have been removed from `self.documents` before calling this.
|
|
593
|
+
fn remove_definitions_for_document(&mut self, document: &Document) {
|
|
596
594
|
// TODO: Remove method references from method declarations once method inference is implemented
|
|
597
595
|
for ref_id in document.method_references() {
|
|
598
596
|
if let Some(method_ref) = self.method_references.remove(ref_id) {
|
|
@@ -1502,4 +1500,85 @@ mod tests {
|
|
|
1502
1500
|
assert!(context.graph().get("Foo::<Foo>").is_none());
|
|
1503
1501
|
assert!(context.graph().get("Foo::<Foo>::<<Foo>>").is_none());
|
|
1504
1502
|
}
|
|
1503
|
+
|
|
1504
|
+
#[test]
|
|
1505
|
+
fn indexing_the_same_document_twice() {
|
|
1506
|
+
let mut context = GraphTest::new();
|
|
1507
|
+
let source = "
|
|
1508
|
+
module Bar; end
|
|
1509
|
+
|
|
1510
|
+
$global_var_1 = 1
|
|
1511
|
+
alias $global_alias_1 $global_var_1
|
|
1512
|
+
ALIAS_CONST_1 = Bar
|
|
1513
|
+
|
|
1514
|
+
class Foo
|
|
1515
|
+
alias $global_alias_2 $global_var_1
|
|
1516
|
+
attr_reader :attr_1
|
|
1517
|
+
attr_writer :attr_2
|
|
1518
|
+
attr_accessor :attr_3
|
|
1519
|
+
ALIAS_CONST_2 = Bar
|
|
1520
|
+
|
|
1521
|
+
$global_var_2 = 1
|
|
1522
|
+
@ivar_1 = 1
|
|
1523
|
+
@@class_var_1 = 1
|
|
1524
|
+
|
|
1525
|
+
def method_1
|
|
1526
|
+
$global_var_3 = 1
|
|
1527
|
+
@ivar_2 = 1
|
|
1528
|
+
@@class_var_2 = 1
|
|
1529
|
+
ALIAS_CONST_3 = Bar
|
|
1530
|
+
end
|
|
1531
|
+
alias_method :aliased_method_1, :method_1
|
|
1532
|
+
|
|
1533
|
+
def self.method_2
|
|
1534
|
+
$global_var_4 = 1
|
|
1535
|
+
@ivar_3 = 1
|
|
1536
|
+
@@class_var_3 = 1
|
|
1537
|
+
ALIAS_CONST_4 = Bar
|
|
1538
|
+
end
|
|
1539
|
+
|
|
1540
|
+
class << self
|
|
1541
|
+
alias $global_alias_3 $global_var_1
|
|
1542
|
+
attr_reader :attr_4
|
|
1543
|
+
attr_writer :attr_5
|
|
1544
|
+
attr_accessor :attr_6
|
|
1545
|
+
ALIAS_CONST_5 = Bar
|
|
1546
|
+
|
|
1547
|
+
$global_var_3 = 1
|
|
1548
|
+
@ivar_4 = 1
|
|
1549
|
+
@@class_var_4 = 1
|
|
1550
|
+
|
|
1551
|
+
def method_3
|
|
1552
|
+
$global_var_4 = 1
|
|
1553
|
+
@ivar_5 = 1
|
|
1554
|
+
@@class_var_5 = 1
|
|
1555
|
+
ALIAS_CONST_6 = Bar
|
|
1556
|
+
end
|
|
1557
|
+
alias_method :aliased_method_1, :method_1
|
|
1558
|
+
|
|
1559
|
+
def self.method_4
|
|
1560
|
+
$global_var_5 = 1
|
|
1561
|
+
@ivar_6 = 1
|
|
1562
|
+
@@class_var_6 = 1
|
|
1563
|
+
ALIAS_CONST_7 = Bar
|
|
1564
|
+
end
|
|
1565
|
+
end
|
|
1566
|
+
end
|
|
1567
|
+
";
|
|
1568
|
+
|
|
1569
|
+
context.index_uri("file:///foo.rb", source);
|
|
1570
|
+
assert_eq!(44, context.graph().definitions.len());
|
|
1571
|
+
assert_eq!(7, context.graph().constant_references.len());
|
|
1572
|
+
assert_eq!(2, context.graph().method_references.len());
|
|
1573
|
+
assert_eq!(1, context.graph().documents.len());
|
|
1574
|
+
assert_eq!(12, context.graph().names.len());
|
|
1575
|
+
assert_eq!(41, context.graph().strings.len());
|
|
1576
|
+
context.index_uri("file:///foo.rb", source);
|
|
1577
|
+
assert_eq!(44, context.graph().definitions.len());
|
|
1578
|
+
assert_eq!(7, context.graph().constant_references.len());
|
|
1579
|
+
assert_eq!(2, context.graph().method_references.len());
|
|
1580
|
+
assert_eq!(1, context.graph().documents.len());
|
|
1581
|
+
assert_eq!(12, context.graph().names.len());
|
|
1582
|
+
assert_eq!(41, context.graph().strings.len());
|
|
1583
|
+
}
|
|
1505
1584
|
}
|
data/rust/rubydex/src/offset.rs
CHANGED
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
//! within a file. It can be used to track positions in source code and convert
|
|
5
5
|
//! between byte offsets and line/column positions.
|
|
6
6
|
|
|
7
|
-
#[cfg(any(test, feature = "test_utils"))]
|
|
8
7
|
use crate::model::document::Document;
|
|
9
8
|
|
|
10
9
|
/// Represents a byte offset range within a specific file.
|
|
@@ -59,7 +58,6 @@ impl Offset {
|
|
|
59
58
|
}
|
|
60
59
|
|
|
61
60
|
/// Converts an offset to a display range like `1:1-1:5`
|
|
62
|
-
#[cfg(any(test, feature = "test_utils"))]
|
|
63
61
|
#[must_use]
|
|
64
62
|
pub fn to_display_range(&self, document: &Document) -> String {
|
|
65
63
|
let line_index = document.line_index();
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
use std::collections::HashSet;
|
|
2
|
+
use std::io::Write;
|
|
3
|
+
|
|
4
|
+
use crate::model::declaration::Declaration;
|
|
5
|
+
use crate::model::definitions::Definition;
|
|
6
|
+
use crate::model::graph::Graph;
|
|
7
|
+
use crate::model::ids::{DefinitionId, NameId, StringId};
|
|
8
|
+
use crate::model::name::{NameRef, ParentScope};
|
|
9
|
+
|
|
10
|
+
impl Graph {
|
|
11
|
+
/// Writes a report of orphan definitions (definitions not linked to any declaration).
|
|
12
|
+
///
|
|
13
|
+
/// Format: `type\tconcatenated_name\tlocation` (TSV)
|
|
14
|
+
///
|
|
15
|
+
/// # Errors
|
|
16
|
+
///
|
|
17
|
+
/// Returns an error if writing fails.
|
|
18
|
+
pub fn write_orphan_report(&self, writer: &mut impl Write) -> std::io::Result<()> {
|
|
19
|
+
// Collect all definition IDs that are linked to declarations
|
|
20
|
+
let linked_definition_ids: HashSet<&DefinitionId> = self
|
|
21
|
+
.declarations()
|
|
22
|
+
.values()
|
|
23
|
+
.flat_map(Declaration::definitions)
|
|
24
|
+
.collect();
|
|
25
|
+
|
|
26
|
+
// Find orphan definitions
|
|
27
|
+
let mut orphans: Vec<_> = self
|
|
28
|
+
.definitions()
|
|
29
|
+
.iter()
|
|
30
|
+
.filter(|(id, _)| !linked_definition_ids.contains(id))
|
|
31
|
+
.collect();
|
|
32
|
+
|
|
33
|
+
// Sort by type, then by location for consistent output
|
|
34
|
+
orphans.sort_by(|(_, a), (_, b)| {
|
|
35
|
+
a.kind()
|
|
36
|
+
.cmp(b.kind())
|
|
37
|
+
.then_with(|| a.uri_id().cmp(b.uri_id()))
|
|
38
|
+
.then_with(|| a.offset().cmp(b.offset()))
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
for (_, definition) in orphans {
|
|
42
|
+
let kind = definition.kind();
|
|
43
|
+
let name = match definition.name_id().copied() {
|
|
44
|
+
Some(id) => self.build_concatenated_name_from_name(id),
|
|
45
|
+
None => self.build_concatenated_name_from_lexical_nesting(definition),
|
|
46
|
+
};
|
|
47
|
+
let location = self.definition_location(definition);
|
|
48
|
+
|
|
49
|
+
writeln!(writer, "{kind}\t{name}\t{location}")?;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
Ok(())
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/// Walks the Name system's `parent_scope` chain to reconstruct the constant path.
|
|
56
|
+
/// Falls back to `nesting` for enclosing scope context when there is no explicit parent scope.
|
|
57
|
+
///
|
|
58
|
+
/// Note: this produces a concatenated name by piecing together name parts, not a properly
|
|
59
|
+
/// resolved qualified name.
|
|
60
|
+
pub(crate) fn build_concatenated_name_from_name(&self, name_id: NameId) -> String {
|
|
61
|
+
let Some(name_ref) = self.names().get(&name_id) else {
|
|
62
|
+
return "<unknown>".to_string();
|
|
63
|
+
};
|
|
64
|
+
let simple_name = self.string_id_to_string(*name_ref.str());
|
|
65
|
+
|
|
66
|
+
match name_ref.parent_scope() {
|
|
67
|
+
ParentScope::Some(parent_id) | ParentScope::Attached(parent_id) => {
|
|
68
|
+
let parent_name = self.build_concatenated_name_from_name(*parent_id);
|
|
69
|
+
format!("{parent_name}::{simple_name}")
|
|
70
|
+
}
|
|
71
|
+
ParentScope::TopLevel => format!("::{simple_name}"),
|
|
72
|
+
ParentScope::None => {
|
|
73
|
+
let prefix = name_ref
|
|
74
|
+
.nesting()
|
|
75
|
+
.as_ref()
|
|
76
|
+
.map(|nesting_id| self.build_nesting_prefix(*nesting_id))
|
|
77
|
+
.unwrap_or_default();
|
|
78
|
+
|
|
79
|
+
if prefix.is_empty() {
|
|
80
|
+
simple_name
|
|
81
|
+
} else {
|
|
82
|
+
format!("{prefix}::{simple_name}")
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/// Resolves the enclosing nesting `NameId` to a string prefix.
|
|
89
|
+
/// For resolved names, uses the declaration's fully qualified name.
|
|
90
|
+
/// For unresolved names, recursively walks the name chain.
|
|
91
|
+
fn build_nesting_prefix(&self, nesting_id: NameId) -> String {
|
|
92
|
+
let Some(name_ref) = self.names().get(&nesting_id) else {
|
|
93
|
+
return String::new();
|
|
94
|
+
};
|
|
95
|
+
match name_ref {
|
|
96
|
+
NameRef::Resolved(resolved) => self
|
|
97
|
+
.declarations()
|
|
98
|
+
.get(resolved.declaration_id())
|
|
99
|
+
.map_or_else(String::new, |decl| decl.name().to_string()),
|
|
100
|
+
NameRef::Unresolved(_) => self.build_concatenated_name_from_name(nesting_id),
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/// Builds a concatenated name for non-constant definitions by walking the `lexical_nesting_id` chain.
|
|
105
|
+
///
|
|
106
|
+
/// Note: this pieces together name parts from the lexical nesting, not a properly resolved
|
|
107
|
+
/// qualified name.
|
|
108
|
+
pub(crate) fn build_concatenated_name_from_lexical_nesting(&self, definition: &Definition) -> String {
|
|
109
|
+
let simple_name = self.string_id_to_string(self.definition_string_id(definition));
|
|
110
|
+
|
|
111
|
+
// Collect enclosing nesting names from inner to outer
|
|
112
|
+
let mut nesting_parts = Vec::new();
|
|
113
|
+
let mut current_nesting = *definition.lexical_nesting_id();
|
|
114
|
+
|
|
115
|
+
while let Some(nesting_id) = current_nesting {
|
|
116
|
+
let Some(nesting_def) = self.definitions().get(&nesting_id) else {
|
|
117
|
+
break;
|
|
118
|
+
};
|
|
119
|
+
nesting_parts.push(self.string_id_to_string(self.definition_string_id(nesting_def)));
|
|
120
|
+
current_nesting = *nesting_def.lexical_nesting_id();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if nesting_parts.is_empty() {
|
|
124
|
+
return simple_name;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Reverse to get outer-to-inner order for the prefix
|
|
128
|
+
nesting_parts.reverse();
|
|
129
|
+
let prefix = nesting_parts.join("::");
|
|
130
|
+
|
|
131
|
+
let separator = match definition {
|
|
132
|
+
Definition::Method(_)
|
|
133
|
+
| Definition::AttrAccessor(_)
|
|
134
|
+
| Definition::AttrReader(_)
|
|
135
|
+
| Definition::AttrWriter(_)
|
|
136
|
+
| Definition::MethodAlias(_)
|
|
137
|
+
| Definition::InstanceVariable(_) => "#",
|
|
138
|
+
Definition::Class(_)
|
|
139
|
+
| Definition::SingletonClass(_)
|
|
140
|
+
| Definition::Module(_)
|
|
141
|
+
| Definition::Constant(_)
|
|
142
|
+
| Definition::ConstantAlias(_)
|
|
143
|
+
| Definition::GlobalVariable(_)
|
|
144
|
+
| Definition::ClassVariable(_)
|
|
145
|
+
| Definition::GlobalVariableAlias(_) => "::",
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
format!("{prefix}{separator}{simple_name}")
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/// Converts a `StringId` to its string value.
|
|
152
|
+
fn string_id_to_string(&self, string_id: StringId) -> String {
|
|
153
|
+
self.strings().get(&string_id).unwrap().to_string()
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/// Get location in the format of `uri#L<line>` for a definition.
|
|
157
|
+
/// The format is clickable in VS Code.
|
|
158
|
+
pub(crate) fn definition_location(&self, definition: &Definition) -> String {
|
|
159
|
+
let uri_id = definition.uri_id();
|
|
160
|
+
|
|
161
|
+
let Some(document) = self.documents().get(uri_id) else {
|
|
162
|
+
return format!("{uri_id}:<unknown>");
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
let uri = document.uri();
|
|
166
|
+
let line_index = document.line_index();
|
|
167
|
+
let start = line_index.line_col(definition.offset().start().into());
|
|
168
|
+
format!("{uri}#L{}", start.line + 1)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
#[cfg(test)]
|
|
173
|
+
mod tests {
|
|
174
|
+
use crate::test_utils::GraphTest;
|
|
175
|
+
|
|
176
|
+
#[test]
|
|
177
|
+
fn build_concatenated_name_from_name_for_constants() {
|
|
178
|
+
let cases = vec![
|
|
179
|
+
("class Foo; end", "Foo"),
|
|
180
|
+
("module Foo; class Bar; end; end", "Foo::Bar"),
|
|
181
|
+
("module Foo; module Bar; class Baz; end; end; end", "Foo::Bar::Baz"),
|
|
182
|
+
];
|
|
183
|
+
|
|
184
|
+
for (source, expected_name) in cases {
|
|
185
|
+
let mut context = GraphTest::new();
|
|
186
|
+
context.index_uri("file:///test.rb", source);
|
|
187
|
+
context.resolve();
|
|
188
|
+
|
|
189
|
+
let definitions = context.graph().get(expected_name).unwrap();
|
|
190
|
+
let definition = definitions.first().unwrap();
|
|
191
|
+
let name_id = *definition.name_id().unwrap();
|
|
192
|
+
let actual = context.graph().build_concatenated_name_from_name(name_id);
|
|
193
|
+
|
|
194
|
+
assert_eq!(actual, expected_name, "For source: {source}");
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
#[test]
|
|
199
|
+
fn build_concatenated_name_from_lexical_nesting_for_methods() {
|
|
200
|
+
let cases = vec![
|
|
201
|
+
("class Foo; def bar; end; end", "Foo#bar()"),
|
|
202
|
+
("module Foo; class Bar; def baz; end; end; end", "Foo::Bar#baz()"),
|
|
203
|
+
("def bar; end", "bar()"),
|
|
204
|
+
];
|
|
205
|
+
|
|
206
|
+
for (source, expected_name) in cases {
|
|
207
|
+
let mut context = GraphTest::new();
|
|
208
|
+
// Index without resolution so methods remain orphans
|
|
209
|
+
context.index_uri("file:///test.rb", source);
|
|
210
|
+
|
|
211
|
+
let definition = context
|
|
212
|
+
.graph()
|
|
213
|
+
.definitions()
|
|
214
|
+
.values()
|
|
215
|
+
.find(|d| d.kind() == "Method" && d.name_id().is_none())
|
|
216
|
+
.unwrap_or_else(|| panic!("No Method definition without name_id found for source: {source}"));
|
|
217
|
+
|
|
218
|
+
let actual = context.graph().build_concatenated_name_from_lexical_nesting(definition);
|
|
219
|
+
assert_eq!(actual, expected_name, "For source: {source}");
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
#[test]
|
|
224
|
+
fn build_concatenated_name_from_lexical_nesting_for_instance_variables() {
|
|
225
|
+
let mut context = GraphTest::new();
|
|
226
|
+
context.index_uri("file:///test.rb", "class Foo; def initialize; @ivar = 1; end; end");
|
|
227
|
+
|
|
228
|
+
let definition = context
|
|
229
|
+
.graph()
|
|
230
|
+
.definitions()
|
|
231
|
+
.values()
|
|
232
|
+
.find(|d| d.kind() == "InstanceVariable")
|
|
233
|
+
.unwrap();
|
|
234
|
+
|
|
235
|
+
let actual = context.graph().build_concatenated_name_from_lexical_nesting(definition);
|
|
236
|
+
assert_eq!(actual, "Foo::initialize()#@ivar");
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
#[test]
|
|
240
|
+
fn definition_location_uses_clickable_uri_fragment() {
|
|
241
|
+
let mut context = GraphTest::new();
|
|
242
|
+
context.index_uri(
|
|
243
|
+
"file:///foo.rb",
|
|
244
|
+
"
|
|
245
|
+
class Foo
|
|
246
|
+
def bar
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
",
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
let definition = context
|
|
253
|
+
.graph()
|
|
254
|
+
.definitions()
|
|
255
|
+
.values()
|
|
256
|
+
.find(|d| d.kind() == "Method")
|
|
257
|
+
.unwrap();
|
|
258
|
+
|
|
259
|
+
let actual = context.graph().definition_location(definition);
|
|
260
|
+
assert_eq!(actual, "file:///foo.rb#L2");
|
|
261
|
+
}
|
|
262
|
+
}
|
data/rust/rubydex/src/stats.rs
CHANGED
|
@@ -115,7 +115,9 @@ pub unsafe extern "C" fn rdx_graph_resolve_constant(
|
|
|
115
115
|
})
|
|
116
116
|
}
|
|
117
117
|
|
|
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`.
|
|
119
121
|
///
|
|
120
122
|
/// # Panics
|
|
121
123
|
///
|
|
@@ -130,34 +132,57 @@ pub unsafe extern "C" fn rdx_index_all(
|
|
|
130
132
|
pointer: GraphPointer,
|
|
131
133
|
file_paths: *const *const c_char,
|
|
132
134
|
count: usize,
|
|
133
|
-
|
|
135
|
+
out_error_count: *mut usize,
|
|
136
|
+
) -> *const *const c_char {
|
|
134
137
|
let file_paths: Vec<String> = unsafe { utils::convert_double_pointer_to_vec(file_paths, count).unwrap() };
|
|
135
|
-
let (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);
|
|
146
139
|
|
|
147
140
|
with_mut_graph(pointer, |graph| {
|
|
148
|
-
let
|
|
141
|
+
let indexing_errors = indexing::index_files(graph, file_paths);
|
|
149
142
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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();
|
|
156
148
|
|
|
157
|
-
|
|
149
|
+
if all_errors.is_empty() {
|
|
150
|
+
unsafe { *out_error_count = 0 };
|
|
151
|
+
return ptr::null();
|
|
158
152
|
}
|
|
159
153
|
|
|
160
|
-
|
|
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>()
|
|
167
|
+
})
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/// Deletes a document and all of its definitions from the graph.
|
|
171
|
+
/// Returns a pointer to the URI ID if the document was found and removed, or NULL if it didn't exist.
|
|
172
|
+
/// Caller must free the returned pointer with `free_u64`.
|
|
173
|
+
///
|
|
174
|
+
/// # Safety
|
|
175
|
+
///
|
|
176
|
+
/// Expects both the graph pointer and uri string pointer to be valid
|
|
177
|
+
#[unsafe(no_mangle)]
|
|
178
|
+
pub unsafe extern "C" fn rdx_graph_delete_document(pointer: GraphPointer, uri: *const c_char) -> *const u64 {
|
|
179
|
+
let Ok(uri_str) = (unsafe { utils::convert_char_ptr_to_string(uri) }) else {
|
|
180
|
+
return ptr::null();
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
with_mut_graph(pointer, |graph| match graph.delete_document(&uri_str) {
|
|
184
|
+
Some(uri_id) => Box::into_raw(Box::new(*uri_id)),
|
|
185
|
+
None => ptr::null(),
|
|
161
186
|
})
|
|
162
187
|
}
|
|
163
188
|
|
|
@@ -443,6 +468,44 @@ pub unsafe extern "C" fn rdx_resolve_require_path(
|
|
|
443
468
|
})
|
|
444
469
|
}
|
|
445
470
|
|
|
471
|
+
/// Returns all require paths for completion.
|
|
472
|
+
/// Returns array of C strings and writes count to `out_count`.
|
|
473
|
+
/// Returns null if `load_path` contain invalid UTF-8.
|
|
474
|
+
/// Caller must free with `free_c_string_array`.
|
|
475
|
+
///
|
|
476
|
+
/// # Safety
|
|
477
|
+
/// - `pointer` must be a valid `GraphPointer` previously returned by this crate.
|
|
478
|
+
/// - `load_path` must be an array of `load_path_count` valid, null-terminated UTF-8 strings.
|
|
479
|
+
/// - `out_count` must be a valid, writable pointer.
|
|
480
|
+
#[unsafe(no_mangle)]
|
|
481
|
+
pub unsafe extern "C" fn rdx_require_paths(
|
|
482
|
+
pointer: GraphPointer,
|
|
483
|
+
load_path: *const *const c_char,
|
|
484
|
+
load_path_count: usize,
|
|
485
|
+
out_count: *mut usize,
|
|
486
|
+
) -> *const *const c_char {
|
|
487
|
+
let Ok(paths_vec) = (unsafe { utils::convert_double_pointer_to_vec(load_path, load_path_count) }) else {
|
|
488
|
+
return ptr::null_mut();
|
|
489
|
+
};
|
|
490
|
+
let paths_vec = paths_vec.into_iter().map(PathBuf::from).collect::<Vec<_>>();
|
|
491
|
+
|
|
492
|
+
let results = with_graph(pointer, |graph| query::require_paths(graph, &paths_vec));
|
|
493
|
+
|
|
494
|
+
let c_strings: Vec<*const c_char> = results
|
|
495
|
+
.into_iter()
|
|
496
|
+
.filter_map(|string| {
|
|
497
|
+
CString::new(string)
|
|
498
|
+
.ok()
|
|
499
|
+
.map(|c_string| c_string.into_raw().cast_const())
|
|
500
|
+
})
|
|
501
|
+
.collect();
|
|
502
|
+
|
|
503
|
+
unsafe { *out_count = c_strings.len() };
|
|
504
|
+
|
|
505
|
+
let boxed = c_strings.into_boxed_slice();
|
|
506
|
+
Box::into_raw(boxed).cast::<*const c_char>()
|
|
507
|
+
}
|
|
508
|
+
|
|
446
509
|
#[cfg(test)]
|
|
447
510
|
mod tests {
|
|
448
511
|
use rubydex::indexing::ruby_indexer::RubyIndexer;
|
|
@@ -48,3 +48,23 @@ pub extern "C" fn free_u64(ptr: *const u64) {
|
|
|
48
48
|
let _ = Box::from_raw(ptr.cast_mut());
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
+
|
|
52
|
+
/// Frees an array of C strings allocated by Rust.
|
|
53
|
+
///
|
|
54
|
+
/// # Safety
|
|
55
|
+
/// - `ptr` must be a pointer to a boxed slice of C strings previously allocated by this crate.
|
|
56
|
+
/// - `count` must be the length of the array.
|
|
57
|
+
/// - `ptr` must not be used after being freed.
|
|
58
|
+
#[unsafe(no_mangle)]
|
|
59
|
+
pub unsafe extern "C" fn free_c_string_array(ptr: *const *const c_char, count: usize) {
|
|
60
|
+
if ptr.is_null() {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let slice = unsafe { Box::from_raw(std::ptr::slice_from_raw_parts_mut(ptr.cast_mut(), count)) };
|
|
65
|
+
let _: Vec<_> = slice
|
|
66
|
+
.iter()
|
|
67
|
+
.filter(|p| !p.is_null())
|
|
68
|
+
.map(|arg| unsafe { CString::from_raw((*arg).cast_mut()) })
|
|
69
|
+
.collect();
|
|
70
|
+
}
|
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.1.0.
|
|
4
|
+
version: 0.1.0.beta4
|
|
5
5
|
platform: x86_64-linux
|
|
6
6
|
authors:
|
|
7
7
|
- Shopify
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-02-
|
|
11
|
+
date: 2026-02-10 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
|
|
@@ -76,6 +76,7 @@ files:
|
|
|
76
76
|
- rust/rubydex/src/errors.rs
|
|
77
77
|
- rust/rubydex/src/indexing.rs
|
|
78
78
|
- rust/rubydex/src/indexing/local_graph.rs
|
|
79
|
+
- rust/rubydex/src/indexing/rbs_indexer.rs
|
|
79
80
|
- rust/rubydex/src/indexing/ruby_indexer.rs
|
|
80
81
|
- rust/rubydex/src/job_queue.rs
|
|
81
82
|
- rust/rubydex/src/lib.rs
|
|
@@ -101,6 +102,7 @@ files:
|
|
|
101
102
|
- rust/rubydex/src/resolution.rs
|
|
102
103
|
- rust/rubydex/src/stats.rs
|
|
103
104
|
- rust/rubydex/src/stats/memory.rs
|
|
105
|
+
- rust/rubydex/src/stats/orphan_report.rs
|
|
104
106
|
- rust/rubydex/src/stats/timer.rs
|
|
105
107
|
- rust/rubydex/src/test_utils.rs
|
|
106
108
|
- rust/rubydex/src/test_utils/context.rs
|