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



251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
# File 'ext/rubydex/graph.c', line 251

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



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

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(*args) ⇒ Object

the innermost nesting element.



597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
# File 'ext/rubydex/graph.c', line 597

static VALUE rdxr_graph_complete_expression(int argc, VALUE *argv, VALUE self) {
    VALUE nesting, opts;
    rb_scan_args(argc, argv, "1:", &nesting, &opts);
    rdxi_check_array_of_strings(nesting);

    const char *self_receiver = extract_self_receiver(opts);

    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, self_receiver);

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

#complete_method_argument(*args) ⇒ Object

See complete_expression for semantics of self_receiver.



658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
# File 'ext/rubydex/graph.c', line 658

static VALUE rdxr_graph_complete_method_argument(int argc, VALUE *argv, VALUE self) {
    VALUE name, nesting, opts;
    rb_scan_args(argc, argv, "2:", &name, &nesting, &opts);

    Check_Type(name, T_STRING);
    rdxi_check_array_of_strings(nesting);

    const char *self_receiver = extract_self_receiver(opts);

    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, self_receiver);

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

#complete_method_call(*args) ⇒ Object

visibility checks (private/protected).



640
641
642
643
644
645
646
647
648
649
650
651
652
653
# File 'ext/rubydex/graph.c', line 640

static VALUE rdxr_graph_complete_method_call(int argc, VALUE *argv, VALUE self) {
    VALUE name, opts;
    rb_scan_args(argc, argv, "1:", &name, &opts);
    Check_Type(name, T_STRING);

    const char *self_receiver = extract_self_receiver(opts);

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

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

#complete_namespace_access(*args) ⇒ Object

visibility-restricted singleton methods (e.g., ‘private_class_method`).



621
622
623
624
625
626
627
628
629
630
631
632
633
634
# File 'ext/rubydex/graph.c', line 621

static VALUE rdxr_graph_complete_namespace_access(int argc, VALUE *argv, VALUE self) {
    VALUE name, opts;
    rb_scan_args(argc, argv, "1:", &name, &opts);
    Check_Type(name, T_STRING);

    const char *self_receiver = extract_self_receiver(opts);

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

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

#constant_referencesObject

Returns an enumerator that yields constant references lazily



285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
# File 'ext/rubydex/graph.c', line 285

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



139
140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'ext/rubydex/graph.c', line 139

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.



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

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



504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
# File 'ext/rubydex/graph.c', line 504

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.



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

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



234
235
236
237
238
239
240
241
242
243
244
245
246
247
# File 'ext/rubydex/graph.c', line 234

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



379
380
381
382
383
384
385
386
387
388
389
390
391
# File 'ext/rubydex/graph.c', line 379

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.



682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
# File 'ext/rubydex/graph.c', line 682

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.



700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
# File 'ext/rubydex/graph.c', line 700

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



184
185
186
187
188
189
190
191
192
193
194
195
# File 'ext/rubydex/graph.c', line 184

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



59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'ext/rubydex/graph.c', line 59

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



91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# File 'ext/rubydex/graph.c', line 91

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.



722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
# File 'ext/rubydex/graph.c', line 722

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



315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
# File 'ext/rubydex/graph.c', line 315

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.



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

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



370
371
372
373
374
375
# File 'ext/rubydex/graph.c', line 370

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



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

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.



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

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)



169
170
171
172
173
174
175
176
177
178
179
180
# File 'ext/rubydex/graph.c', line 169

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