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



454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
# File 'ext/rubydex/graph.c', line 454

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



570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
# File 'ext/rubydex/graph.c', line 570

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(|)`).



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

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.`).



600
601
602
603
604
605
606
607
608
# File 'ext/rubydex/graph.c', line 600

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::`).



588
589
590
591
592
593
594
595
596
# File 'ext/rubydex/graph.c', line 588

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.



327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
# File 'ext/rubydex/graph.c', line 327

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



479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
# File 'ext/rubydex/graph.c', line 479

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

#document(uri) ⇒ Object

Returns the Document for the given URI, or nil if it doesn’t exist.



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

static VALUE rdxr_graph_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_get_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);
}

#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



354
355
356
357
358
359
360
361
362
363
364
365
366
# File 'ext/rubydex/graph.c', line 354

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.



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

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.



649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
# File 'ext/rubydex/graph.c', line 649

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

#keyword(name) ⇒ Object

Returns a Keyword object for the given keyword name, or nil if it is not a keyword.



671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
# File 'ext/rubydex/graph.c', line 671

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

    const CKeyword *kw = rdx_keyword_get(StringValueCStr(name));
    if (kw == NULL) {
        return Qnil;
    }

    VALUE argv[2] = {
        rb_utf8_str_new_cstr(kw->name),
        rb_utf8_str_new_cstr(kw->documentation),
    };

    rdx_keyword_free(kw);
    return rb_class_new_instance(2, argv, cKeyword);
}

#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.



425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
# File 'ext/rubydex/graph.c', line 425

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



345
346
347
348
349
350
# File 'ext/rubydex/graph.c', line 345

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



370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
# File 'ext/rubydex/graph.c', line 370

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.



399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
# File 'ext/rubydex/graph.c', line 399

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