Class: Rubydex::Graph

Inherits:
Object
  • Object
show all
Defined in:
lib/rubydex/graph.rb,
ext/rubydex/graph.c

Overview

The global graph representing all declarations and their relationships for the workspace

Note: this class is partially defined in C to integrate with the Rust backend

Constant Summary collapse

IGNORED_DIRECTORIES =
[
  ".bundle",
  ".claude",
  ".git",
  ".github",
  ".ruby-lsp",
  ".vscode",
  "log",
  "node_modules",
  "tmp",
].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(workspace_path: Dir.pwd) ⇒ Graph

: (?workspace_path: String) -> void



24
25
26
27
28
# File 'lib/rubydex/graph.rb', line 24

def initialize(workspace_path: Dir.pwd)
  @workspace_path = workspace_path

  exclude_paths(IGNORED_DIRECTORIES.map { |dir| File.join(@workspace_path, dir) })
end

Instance Attribute Details

#workspace_pathObject

: String



21
22
23
# File 'lib/rubydex/graph.rb', line 21

def workspace_path
  @workspace_path
end

Instance Method Details

#[](key) ⇒ Object

Returns a declaration handle for the given ID



226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
# File 'ext/rubydex/graph.c', line 226

static VALUE rdxr_graph_aref(VALUE self, VALUE key) {
    void *graph;
    TypedData_Get_Struct(self, void *, &graph_type, graph);

    if (TYPE(key) != T_STRING) {
        rb_raise(rb_eTypeError, "expected String");
    }

    const CDeclaration *decl = rdx_graph_get_declaration(graph, StringValueCStr(key));
    if (decl == NULL) {
        return Qnil;
    }

    VALUE decl_class = rdxi_declaration_class_for_kind(decl->kind);
    VALUE argv[] = {self, ULL2NUM(decl->id)};
    free_c_declaration(decl);

    return rb_class_new_instance(2, argv, decl_class);
}

#check_integrityObject

Returns an array of IntegrityFailure objects, empty if no issues found



436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
# File 'ext/rubydex/graph.c', line 436

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;
}

#complete_expression(nesting) ⇒ Object

The nesting array represents the lexical scope stack



552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
# File 'ext/rubydex/graph.c', line 552

static VALUE rdxr_graph_complete_expression(VALUE self, VALUE nesting) {
    rdxi_check_array_of_strings(nesting);

    void *graph;
    TypedData_Get_Struct(self, void *, &graph_type, graph);

    size_t nesting_count = RARRAY_LEN(nesting);
    char **converted_nesting = rdxi_str_array_to_char(nesting, nesting_count);

    struct CompletionResult result =
        rdx_graph_complete_expression(graph, (const char *const *)converted_nesting, nesting_count);

    rdxi_free_str_array(converted_nesting, nesting_count);
    return completion_result_to_ruby_array(result, self);
}

#complete_method_argument(name, nesting) ⇒ Object

Returns completion candidates inside a method call’s argument list (e.g., ‘foo.bar(|)`).



594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
# File 'ext/rubydex/graph.c', line 594

static VALUE rdxr_graph_complete_method_argument(VALUE self, VALUE name, VALUE nesting) {
    Check_Type(name, T_STRING);
    rdxi_check_array_of_strings(nesting);

    void *graph;
    TypedData_Get_Struct(self, void *, &graph_type, graph);

    size_t nesting_count = RARRAY_LEN(nesting);
    char **converted_nesting = rdxi_str_array_to_char(nesting, nesting_count);

    struct CompletionResult result = rdx_graph_complete_method_argument(
        graph, StringValueCStr(name), (const char *const *)converted_nesting, nesting_count);

    rdxi_free_str_array(converted_nesting, nesting_count);
    return completion_result_to_ruby_array(result, self);
}

#complete_method_call(name) ⇒ Object

Returns completion candidates after a method call operator (e.g., ‘foo.`).



582
583
584
585
586
587
588
589
590
# File 'ext/rubydex/graph.c', line 582

static VALUE rdxr_graph_complete_method_call(VALUE self, VALUE name) {
    Check_Type(name, T_STRING);

    void *graph;
    TypedData_Get_Struct(self, void *, &graph_type, graph);

    struct CompletionResult result = rdx_graph_complete_method_call(graph, StringValueCStr(name));
    return completion_result_to_ruby_array(result, self);
}

#complete_namespace_access(name) ⇒ Object

Returns completion candidates after a namespace access operator (e.g., ‘Foo::`).



570
571
572
573
574
575
576
577
578
# File 'ext/rubydex/graph.c', line 570

static VALUE rdxr_graph_complete_namespace_access(VALUE self, VALUE name) {
    Check_Type(name, T_STRING);

    void *graph;
    TypedData_Get_Struct(self, void *, &graph_type, graph);

    struct CompletionResult result = rdx_graph_complete_namespace_access(graph, StringValueCStr(name));
    return completion_result_to_ruby_array(result, self);
}

#constant_referencesObject

Returns an enumerator that yields constant references lazily



260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
# File 'ext/rubydex/graph.c', line 260

static VALUE rdxr_graph_constant_references(VALUE self) {
    if (!rb_block_given_p()) {
        return rb_enumeratorize_with_size(self, rb_str_new2("constant_references"), 0, NULL,
                                          graph_constant_references_size);
    }

    void *graph;
    TypedData_Get_Struct(self, void *, &graph_type, graph);

    void *iter = rdx_graph_constant_references_iter_new(graph);
    VALUE args = rb_ary_new_from_args(2, self, ULL2NUM((uintptr_t)iter));
    rb_ensure(rdxi_constant_references_yield, args, rdxi_constant_references_ensure, args);

    return self;
}

#declarationsObject

Returns an enumerator that yields all declarations lazily



114
115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'ext/rubydex/graph.c', line 114

static VALUE rdxr_graph_declarations(VALUE self) {
    if (!rb_block_given_p()) {
        return rb_enumeratorize_with_size(self, rb_str_new2("declarations"), 0, NULL, graph_declarations_size);
    }

    void *graph;
    TypedData_Get_Struct(self, void *, &graph_type, graph);

    void *iter = rdx_graph_declarations_iter_new(graph);
    VALUE args = rb_ary_new_from_args(2, self, ULL2NUM((uintptr_t)iter));
    rb_ensure(rdxi_declarations_yield, args, rdxi_declarations_ensure, args);

    return self;
}

#delete_document(uri) ⇒ Object

Returns the removed Document or nil if it doesn’t exist.



309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
# File 'ext/rubydex/graph.c', line 309

static VALUE rdxr_graph_delete_document(VALUE self, VALUE uri) {
    Check_Type(uri, T_STRING);

    void *graph;
    TypedData_Get_Struct(self, void *, &graph_type, graph);
    const uint64_t *uri_id = rdx_graph_delete_document(graph, StringValueCStr(uri));

    if (uri_id == NULL) {
        return Qnil;
    }

    VALUE argv[] = {self, ULL2NUM(*uri_id)};
    free_u64(uri_id);
    return rb_class_new_instance(2, argv, cDocument);
}

#diagnosticsObject

Graph#diagnostics -> Array



461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
# File 'ext/rubydex/graph.c', line 461

static VALUE rdxr_graph_diagnostics(VALUE self) {
    void *graph;
    TypedData_Get_Struct(self, void *, &graph_type, graph);

    DiagnosticArray *array = rdx_graph_diagnostics(graph);
    if (array == NULL || array->len == 0) {
        if (array != NULL) {
            rdx_diagnostics_free(array);
        }
        return rb_ary_new();
    }

    VALUE diagnostics = rb_ary_new_capa((long)array->len);
    for (size_t i = 0; i < array->len; i++) {
        DiagnosticEntry entry = array->items[i];
        VALUE message = entry.message == NULL ? Qnil : rb_utf8_str_new_cstr(entry.message);
        VALUE rule = rb_str_new2(entry.rule);
        VALUE location = rdxi_build_location_value(entry.location);

        VALUE kwargs = rb_hash_new();
        rb_hash_aset(kwargs, ID2SYM(rb_intern("rule")), rule);
        rb_hash_aset(kwargs, ID2SYM(rb_intern("message")), message);
        rb_hash_aset(kwargs, ID2SYM(rb_intern("location")), location);

        VALUE diagnostic = rb_class_new_instance_kw(1, &kwargs, cDiagnostic, RB_PASS_KEYWORDS);
        rb_ary_push(diagnostics, diagnostic);
    }

    rdx_diagnostics_free(array);
    return diagnostics;
}

#documentsObject

Returns an enumerator that yields all documents lazily



209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'ext/rubydex/graph.c', line 209

static VALUE rdxr_graph_documents(VALUE self) {
    if (!rb_block_given_p()) {
        return rb_enumeratorize_with_size(self, rb_str_new2("documents"), 0, NULL, graph_documents_size);
    }

    void *graph;
    TypedData_Get_Struct(self, void *, &graph_type, graph);

    void *iter = rdx_graph_documents_iter_new(graph);
    VALUE args = rb_ary_new_from_args(2, self, ULL2NUM((uintptr_t)iter));
    rb_ensure(graph_documents_yield, args, graph_documents_ensure, args);

    return self;
}

#encoding=(encoding) ⇒ Object

Sets the encoding used for transforming byte offsets into LSP code unit line/column positions



336
337
338
339
340
341
342
343
344
345
346
347
348
# File 'ext/rubydex/graph.c', line 336

static VALUE rdxr_graph_set_encoding(VALUE self, VALUE encoding) {
    Check_Type(encoding, T_STRING);

    void *graph;
    TypedData_Get_Struct(self, void *, &graph_type, graph);

    char *encoding_string = StringValueCStr(encoding);
    if (!rdx_graph_set_encoding(graph, encoding_string)) {
        rb_raise(rb_eArgError, "invalid encoding `%s` (should be utf8, utf16 or utf32)", encoding_string);
    }

    return Qnil;
}

#exclude_paths(paths) ⇒ Object

Excludes the given paths from file discovery during indexing.



613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
# File 'ext/rubydex/graph.c', line 613

static VALUE rdxr_graph_exclude_paths(VALUE self, VALUE paths) {
    Check_Type(paths, T_ARRAY);
    rdxi_check_array_of_strings(paths);

    size_t length = RARRAY_LEN(paths);
    char **converted_paths = rdxi_str_array_to_char(paths, length);

    void *graph;
    TypedData_Get_Struct(self, void*, &graph_type, graph);

    rdx_graph_exclude_paths(graph, (const char **)converted_paths, length);
    rdxi_free_str_array(converted_paths, length);

    return Qnil;
}

#excluded_pathsObject

Returns the list of paths currently excluded from file discovery.



631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
# File 'ext/rubydex/graph.c', line 631

static VALUE rdxr_graph_excluded_paths(VALUE self) {
    void *graph;
    TypedData_Get_Struct(self, void*, &graph_type, graph);

    size_t out_count = 0;
    const char *const *results = rdx_graph_excluded_paths(graph, &out_count);

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

    VALUE array = rb_ary_new_capa((long)out_count);
    for (size_t i = 0; i < out_count; i++) {
        rb_ary_push(array, rb_utf8_str_new_cstr(results[i]));
    }

    free_c_string_array(results, out_count);
    return array;
}

#fuzzy_search(query) ⇒ Object

Returns an enumerator that yields declarations matching the query fuzzily



159
160
161
162
163
164
165
166
167
168
169
170
# File 'ext/rubydex/graph.c', line 159

static VALUE rdxr_graph_fuzzy_search(VALUE self, VALUE query) {
    Check_Type(query, T_STRING);

    if (!rb_block_given_p()) {
        return rb_enumeratorize(self, rb_str_new2("fuzzy_search"), 1, &query);
    }

    void *graph;
    TypedData_Get_Struct(self, void *, &graph_type, graph);

    return rdxr_graph_yield_search_results(self, rdx_graph_declarations_fuzzy_search(graph, StringValueCStr(query)));
}

#index_all(file_paths) ⇒ Object

Returns an array of IO error messages encountered during indexing



34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'ext/rubydex/graph.c', line 34

static VALUE rdxr_graph_index_all(VALUE self, VALUE file_paths) {
    rdxi_check_array_of_strings(file_paths);

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

    // Get the underlying graph pointer and then invoke the Rust index all implementation
    void *graph;
    TypedData_Get_Struct(self, void *, &graph_type, graph);

    size_t error_count = 0;
    const char *const *errors = rdx_index_all(graph, (const char **)converted_file_paths, length, &error_count);

    rdxi_free_str_array(converted_file_paths, length);

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

    VALUE array = rb_ary_new_capa((long)error_count);
    for (size_t i = 0; i < error_count; i++) {
        rb_ary_push(array, rb_utf8_str_new_cstr(errors[i]));
    }

    free_c_string_array(errors, error_count);
    return array;
}

#index_source(uri, source, language_id) ⇒ Object

Graph#index_source: (String uri, String source, String language_id) -> void



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'ext/rubydex/graph.c', line 66

static VALUE rdxr_graph_index_source(VALUE self, VALUE uri, VALUE source, VALUE language_id) {
    Check_Type(uri, T_STRING);
    Check_Type(source, T_STRING);
    Check_Type(language_id, T_STRING);

    void *graph;
    TypedData_Get_Struct(self, void *, &graph_type, graph);

    const char *uri_str = StringValueCStr(uri);
    const char *language_id_str = StringValueCStr(language_id);
    const char *source_str = RSTRING_PTR(source);
    size_t source_len = RSTRING_LEN(source);

    enum IndexSourceResult result = rdx_index_source(graph, uri_str, source_str, source_len, language_id_str);
    switch (result) {
    case IndexSourceResult_Success:
        break;
    case IndexSourceResult_InvalidUri:
        rb_raise(rb_eArgError, "invalid URI (not valid UTF-8)");
        break;
    case IndexSourceResult_InvalidSource:
        rb_raise(rb_eArgError, "source is not valid UTF-8");
        break;
    case IndexSourceResult_InvalidLanguageId:
        rb_raise(rb_eArgError, "invalid language_id (not valid UTF-8)");
        break;
    case IndexSourceResult_UnsupportedLanguageId:
        rb_raise(rb_eArgError, "unsupported language_id `%s`", language_id_str);
        break;
    }

    return Qnil;
}

#index_workspaceObject

Index all files and dependencies of the workspace that exists in ‘@workspace_path` : -> Array



32
33
34
# File 'lib/rubydex/graph.rb', line 32

def index_workspace
  index_all(workspace_paths)
end

#method_referencesObject

Returns an enumerator that yields method references lazily



290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
# File 'ext/rubydex/graph.c', line 290

static VALUE rdxr_graph_method_references(VALUE self) {
    if (!rb_block_given_p()) {
        return rb_enumeratorize_with_size(self, rb_str_new2("method_references"), 0, NULL,
                                          graph_method_references_size);
    }

    void *graph;
    TypedData_Get_Struct(self, void *, &graph_type, graph);

    void *iter = rdx_graph_method_references_iter_new(graph);
    VALUE args = rb_ary_new_from_args(2, self, ULL2NUM((uintptr_t)iter));
    rb_ensure(rdxi_method_references_yield, args, rdxi_method_references_ensure, args);

    return self;
}

#require_paths(load_path) ⇒ Object

Returns all require paths for completion.



407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
# File 'ext/rubydex/graph.c', line 407

static VALUE rdxr_graph_require_paths(VALUE self, VALUE load_path) {
    rdxi_check_array_of_strings(load_path);

    void *graph;
    TypedData_Get_Struct(self, void *, &graph_type, graph);

    size_t paths_len = RARRAY_LEN(load_path);
    char **converted_paths = rdxi_str_array_to_char(load_path, paths_len);

    size_t out_count = 0;
    const char *const *results = rdx_require_paths(graph, (const char **)converted_paths, paths_len, &out_count);

    rdxi_free_str_array(converted_paths, paths_len);

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

    VALUE array = rb_ary_new_capa((long)out_count);
    for (size_t i = 0; i < out_count; i++) {
        rb_ary_push(array, rb_utf8_str_new_cstr(results[i]));
    }

    free_c_string_array(results, out_count);
    return array;
}

#resolveObject

Runs the resolver to compute declarations and ownership



327
328
329
330
331
332
# File 'ext/rubydex/graph.c', line 327

static VALUE rdxr_graph_resolve(VALUE self) {
    void *graph;
    TypedData_Get_Struct(self, void *, &graph_type, graph);
    rdx_graph_resolve(graph);
    return self;
}

#resolve_constant(const_name, nesting) ⇒ Object

Runs the resolver on a single constant reference to determine what it points to



352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
# File 'ext/rubydex/graph.c', line 352

static VALUE rdxr_graph_resolve_constant(VALUE self, VALUE const_name, VALUE nesting) {
    Check_Type(const_name, T_STRING);
    rdxi_check_array_of_strings(nesting);

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

    void *graph;
    TypedData_Get_Struct(self, void *, &graph_type, graph);

    const CDeclaration *decl =
        rdx_graph_resolve_constant(graph, StringValueCStr(const_name), (const char **)converted_file_paths, length);

    rdxi_free_str_array(converted_file_paths, length);

    if (decl == NULL) {
        return Qnil;
    }

    VALUE decl_class = rdxi_declaration_class_for_kind(decl->kind);
    VALUE argv[] = {self, ULL2NUM(decl->id)};
    free_c_declaration(decl);

    return rb_class_new_instance(2, argv, decl_class);
}

#resolve_require_path(require_path, load_paths) ⇒ Object

Resolves a require path to its Document.



381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
# File 'ext/rubydex/graph.c', line 381

static VALUE rdxr_graph_resolve_require_path(VALUE self, VALUE require_path, VALUE load_paths) {
    Check_Type(require_path, T_STRING);
    rdxi_check_array_of_strings(load_paths);

    void *graph;
    TypedData_Get_Struct(self, void *, &graph_type, graph);
    const char *path_str = StringValueCStr(require_path);

    size_t paths_len = RARRAY_LEN(load_paths);
    char **converted_paths = rdxi_str_array_to_char(load_paths, paths_len);

    const uint64_t *uri_id = rdx_resolve_require_path(graph, path_str, (const char **)converted_paths, paths_len);

    rdxi_free_str_array(converted_paths, paths_len);

    if (uri_id == NULL) {
        return Qnil;
    }

    VALUE argv[] = {self, ULL2NUM(*uri_id)};
    free_u64(uri_id);
    return rb_class_new_instance(2, argv, cDocument);
}

#search(query) ⇒ Object

Returns an enumerator that yields declarations matching the query exactly (substring match)



144
145
146
147
148
149
150
151
152
153
154
155
# File 'ext/rubydex/graph.c', line 144

static VALUE rdxr_graph_search(VALUE self, VALUE query) {
    Check_Type(query, T_STRING);

    if (!rb_block_given_p()) {
        return rb_enumeratorize(self, rb_str_new2("search"), 1, &query);
    }

    void *graph;
    TypedData_Get_Struct(self, void *, &graph_type, graph);

    return rdxr_graph_yield_search_results(self, rdx_graph_declarations_search(graph, StringValueCStr(query)));
}

#workspace_pathsObject

Returns all workspace paths that should be indexed, excluding directories that we don’t need to descend into such as ‘.git`, `node_modules`. Also includes any top level Ruby files

: -> Array



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/rubydex/graph.rb', line 40

def workspace_paths
  paths = []

  Dir.each_child(@workspace_path) do |entry|
    full_path = File.join(@workspace_path, entry)

    if File.directory?(full_path)
      paths << full_path unless IGNORED_DIRECTORIES.include?(entry)
    elsif File.extname(entry) == ".rb"
      paths << full_path
    end
  end

  add_workspace_dependency_paths(paths)
  add_core_rbs_definition_paths(paths)
  paths.uniq!
  paths
end