Class: RubyIndexer::Index

Inherits:
Object
  • Object
show all
Defined in:
lib/ruby_indexer/lib/ruby_indexer/index.rb

Defined Under Namespace

Classes: IndexNotEmptyError, NonExistingNamespaceError, UnresolvableAliasError

Constant Summary collapse

ENTRY_SIMILARITY_THRESHOLD =

The minimum Jaro-Winkler similarity score for an entry to be considered a match for a given fuzzy search query

0.7

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeIndex

: -> void



52
53
54
55
56
57
58
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
# File 'lib/ruby_indexer/lib/ruby_indexer/index.rb', line 52

def initialize
  # Holds all entries in the index using the following format:
  # {
  #  "Foo" => [#<Entry::Class>, #<Entry::Class>],
  #  "Foo::Bar" => [#<Entry::Class>],
  # }
  @entries = {} #: Hash[String, Array[Entry]]

  # Holds all entries in the index using a prefix tree for searching based on prefixes to provide autocompletion
  @entries_tree = PrefixTree.new #: PrefixTree[Array[Entry]]

  # Holds references to where entries where discovered so that we can easily delete them
  # {
  #  "file:///my/project/foo.rb" => [#<Entry::Class>, #<Entry::Class>],
  #  "file:///my/project/bar.rb" => [#<Entry::Class>],
  #  "untitled:Untitled-1" => [#<Entry::Class>],
  # }
  @uris_to_entries = {} #: Hash[String, Array[Entry]]

  # Holds all require paths for every indexed item so that we can provide autocomplete for requires
  @require_paths_tree = PrefixTree.new #: PrefixTree[URI::Generic]

  # Holds the linearized ancestors list for every namespace
  @ancestors = {} #: Hash[String, Array[String]]

  # Map of module name to included hooks that have to be executed when we include the given module
  @included_hooks = {} #: Hash[String, Array[^(Index index, Entry::Namespace base) -> void]]

  @configuration = RubyIndexer::Configuration.new #: Configuration

  @initial_indexing_completed = false #: bool
end

Instance Attribute Details

#configurationObject (readonly)

: Configuration



14
15
16
# File 'lib/ruby_indexer/lib/ruby_indexer/index.rb', line 14

def configuration
  @configuration
end

#initial_indexing_completedObject (readonly)

: bool



17
18
19
# File 'lib/ruby_indexer/lib/ruby_indexer/index.rb', line 17

def initial_indexing_completed
  @initial_indexing_completed
end

Class Method Details

.actual_nesting(stack, name) ⇒ Object

Returns the real nesting of a constant name taking into account top level references that may be included anywhere in the name or nesting where that constant was found : (Array stack, String? name) -> Array



24
25
26
27
28
29
30
31
32
33
34
35
# File 'lib/ruby_indexer/lib/ruby_indexer/index.rb', line 24

def actual_nesting(stack, name)
  nesting = name ? stack + [name] : stack
  corrected_nesting = []

  nesting.reverse_each do |name|
    corrected_nesting.prepend(name.delete_prefix("::"))

    break if name.start_with?("::")
  end

  corrected_nesting
end

.constant_name(node) ⇒ Object

Returns the unresolved name for a constant reference including all parts of a constant path, or ‘nil` if the constant contains dynamic or incomplete parts : (Prism::Node) -> String?



40
41
42
43
44
45
46
47
48
# File 'lib/ruby_indexer/lib/ruby_indexer/index.rb', line 40

def constant_name(node)
  case node
  when Prism::ConstantPathNode, Prism::ConstantReadNode, Prism::ConstantPathTargetNode
    node.full_name
  end
rescue Prism::ConstantPathNode::DynamicPartsInConstantPathError,
       Prism::ConstantPathNode::MissingNodesInConstantPathError
  nil
end

Instance Method Details

#[](fully_qualified_name) ⇒ Object

: (String fully_qualified_name) -> Array?



137
138
139
# File 'lib/ruby_indexer/lib/ruby_indexer/index.rb', line 137

def [](fully_qualified_name)
  @entries[fully_qualified_name.delete_prefix("::")]
end

#add(entry, skip_prefix_tree: false) ⇒ Object

: (Entry entry, ?skip_prefix_tree: bool) -> void



122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/ruby_indexer/lib/ruby_indexer/index.rb', line 122

def add(entry, skip_prefix_tree: false)
  name = entry.name

  (@entries[name] ||= []) << entry
  (@uris_to_entries[entry.uri.to_s] ||= []) << entry

  unless skip_prefix_tree
    @entries_tree.insert(
      name,
      @entries[name], #: as !nil
    )
  end
end

#class_variable_completion_candidates(name, owner_name) ⇒ Object

: (String name, String owner_name) -> Array



634
635
636
637
638
639
640
641
642
643
# File 'lib/ruby_indexer/lib/ruby_indexer/index.rb', line 634

def class_variable_completion_candidates(name, owner_name)
  entries = prefix_search(name).flatten #: as Array[Entry::ClassVariable]
  # Avoid wasting time linearizing ancestors if we didn't find anything
  return entries if entries.empty?

  ancestors = linearized_attached_ancestors(owner_name)
  variables = entries.select { |e| ancestors.any?(e.owner&.name) }
  variables.uniq!(&:name)
  variables
end

#clear_ancestorsObject

: -> void



683
684
685
# File 'lib/ruby_indexer/lib/ruby_indexer/index.rb', line 683

def clear_ancestors
  @ancestors.clear
end

#constant_completion_candidates(name, nesting) ⇒ Object

: (String name, Array nesting) -> Array[Array[Entry::Constant | Entry::ConstantAlias | Entry::Namespace | Entry::UnresolvedConstantAlias]]



270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
# File 'lib/ruby_indexer/lib/ruby_indexer/index.rb', line 270

def constant_completion_candidates(name, nesting)
  # If we have a top level reference, then we don't need to include completions inside the current nesting
  if name.start_with?("::")
    return @entries_tree.search(name.delete_prefix("::")) #: as Array[Array[Entry::Constant | Entry::ConstantAlias | Entry::Namespace | Entry::UnresolvedConstantAlias]]
  end

  # Otherwise, we have to include every possible constant the user might be referring to. This is essentially the
  # same algorithm as resolve, but instead of returning early we concatenate all unique results

  # Direct constants inside this namespace
  entries = @entries_tree.search(nesting.any? ? "#{nesting.join("::")}::#{name}" : name)

  # Constants defined in enclosing scopes
  nesting.length.downto(1) do |i|
    namespace = nesting[0...i] #: as !nil
      .join("::")
    entries.concat(@entries_tree.search("#{namespace}::#{name}"))
  end

  # Inherited constants
  if name.end_with?("::")
    entries.concat(inherited_constant_completion_candidates(nil, nesting + [name]))
  else
    entries.concat(inherited_constant_completion_candidates(name, nesting))
  end

  # Top level constants
  entries.concat(@entries_tree.search(name))

  # Filter only constants since methods may have names that look like constants
  entries.select! do |definitions|
    definitions.select! do |entry|
      entry.is_a?(Entry::Constant) || entry.is_a?(Entry::ConstantAlias) ||
        entry.is_a?(Entry::Namespace) || entry.is_a?(Entry::UnresolvedConstantAlias)
    end

    definitions.any?
  end

  entries.uniq!
  entries #: as Array[Array[Entry::Constant | Entry::ConstantAlias | Entry::Namespace | Entry::UnresolvedConstantAlias]]
end

#delete(uri, skip_require_paths_tree: false) ⇒ Object

: (URI::Generic uri, ?skip_require_paths_tree: bool) -> void



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
# File 'lib/ruby_indexer/lib/ruby_indexer/index.rb', line 92

def delete(uri, skip_require_paths_tree: false)
  key = uri.to_s
  # For each constant discovered in `path`, delete the associated entry from the index. If there are no entries
  # left, delete the constant from the index.
  @uris_to_entries[key]&.each do |entry|
    name = entry.name
    entries = @entries[name]
    next unless entries

    # Delete the specific entry from the list for this name
    entries.delete(entry)

    # If all entries were deleted, then remove the name from the hash and from the prefix tree. Otherwise, update
    # the prefix tree with the current entries
    if entries.empty?
      @entries.delete(name)
      @entries_tree.delete(name)
    else
      @entries_tree.insert(name, entries)
    end
  end

  @uris_to_entries.delete(key)
  return if skip_require_paths_tree

  require_path = uri.require_path
  @require_paths_tree.delete(require_path) if require_path
end

#empty?Boolean

: -> bool

Returns:

  • (Boolean)


688
689
690
# File 'lib/ruby_indexer/lib/ruby_indexer/index.rb', line 688

def empty?
  @entries.empty?
end

#entries_for(uri, type = nil) ⇒ Object

: [T] (String uri, ?Class[(T & Entry)]? type) -> (Array | Array)?



731
732
733
734
735
736
# File 'lib/ruby_indexer/lib/ruby_indexer/index.rb', line 731

def entries_for(uri, type = nil)
  entries = @uris_to_entries[uri.to_s]
  return entries unless type

  entries&.grep(type)
end

#existing_or_new_singleton_class(name) ⇒ Object

: (String name) -> Entry::SingletonClass



708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
# File 'lib/ruby_indexer/lib/ruby_indexer/index.rb', line 708

def existing_or_new_singleton_class(name)
  *_namespace, unqualified_name = name.split("::")
  full_singleton_name = "#{name}::<Class:#{unqualified_name}>"
  singleton = self[full_singleton_name]&.first #: as Entry::SingletonClass?

  unless singleton
    attached_ancestor = self[name]&.first #: as !nil

    singleton = Entry::SingletonClass.new(
      [full_singleton_name],
      attached_ancestor.uri,
      attached_ancestor.location,
      attached_ancestor.name_location,
      nil,
      nil,
    )
    add(singleton, skip_prefix_tree: true)
  end

  singleton
end

#first_unqualified_const(name) ⇒ Object

Searches for a constant based on an unqualified name and returns the first possible match regardless of whether there are more possible matching entries : (String name) -> Array[Entry::Constant | Entry::ConstantAlias | Entry::Namespace | Entry::UnresolvedConstantAlias]?



149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
# File 'lib/ruby_indexer/lib/ruby_indexer/index.rb', line 149

def first_unqualified_const(name)
  # Look for an exact match first
  _name, entries = @entries.find do |const_name, _entries|
    const_name == name || const_name.end_with?("::#{name}")
  end

  # If an exact match is not found, then try to find a constant that ends with the name
  unless entries
    _name, entries = @entries.find do |const_name, _entries|
      const_name.end_with?(name)
    end
  end

  entries #: as Array[Entry::Constant | Entry::ConstantAlias | Entry::Namespace | Entry::UnresolvedConstantAlias]?
end

#follow_aliased_namespace(name, seen_names = []) ⇒ Object

Follows aliases in a namespace. The algorithm keeps checking if the name is an alias and then recursively follows it. The idea is that we test the name in parts starting from the complete name to the first namespace. For ‘Foo::Bar::Baz`, we would test:

  1. Is ‘Foo::Bar::Baz` an alias? Get the target and recursively follow its target

  2. Is ‘Foo::Bar` an alias? Get the target and recursively follow its target

  3. Is ‘Foo` an alias? Get the target and recursively follow its target

If we find an alias, then we want to follow its target. In the same example, if ‘Foo::Bar` is an alias to `Something::Else`, then we first discover `Something::Else::Baz`. But `Something::Else::Baz` might contain other aliases, so we have to invoke `follow_aliased_namespace` again to check until we only return a real name : (String name, ?Array seen_names) -> String



424
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
451
452
453
454
455
456
457
# File 'lib/ruby_indexer/lib/ruby_indexer/index.rb', line 424

def follow_aliased_namespace(name, seen_names = [])
  parts = name.split("::")
  real_parts = []

  (parts.length - 1).downto(0) do |i|
    current_name = parts[0..i] #: as !nil
      .join("::")

    entry = unless seen_names.include?(current_name)
      @entries[current_name]&.first
    end

    case entry
    when Entry::ConstantAlias
      target = entry.target
      return follow_aliased_namespace("#{target}::#{real_parts.join("::")}", seen_names)
    when Entry::UnresolvedConstantAlias
      resolved = resolve_alias(entry, seen_names)

      if resolved.is_a?(Entry::UnresolvedConstantAlias)
        raise UnresolvableAliasError, "The constant #{resolved.name} is an alias to a non existing constant"
      end

      target = resolved.target
      return follow_aliased_namespace("#{target}::#{real_parts.join("::")}", seen_names)
    else
      real_parts.unshift(
        parts[i], #: as !nil
      )
    end
  end

  real_parts.join("::")
end

#fuzzy_search(query, &condition) ⇒ Object

Fuzzy searches index entries based on Jaro-Winkler similarity. If no query is provided, all entries are returned : (String? query) ?{ (Entry) -> bool? } -> Array



199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/ruby_indexer/lib/ruby_indexer/index.rb', line 199

def fuzzy_search(query, &condition)
  unless query
    entries = @entries.filter_map do |_name, entries|
      next if entries.first.is_a?(Entry::SingletonClass)

      entries = entries.select(&condition) if condition
      entries
    end

    return entries.flatten
  end

  normalized_query = query.gsub("::", "").downcase

  results = @entries.filter_map do |name, entries|
    next if entries.first.is_a?(Entry::SingletonClass)

    entries = entries.select(&condition) if condition
    next if entries.empty?

    similarity = DidYouMean::JaroWinkler.distance(name.gsub("::", "").downcase, normalized_query)
    [entries, -similarity] if similarity > ENTRY_SIMILARITY_THRESHOLD
  end
  results.sort_by!(&:last)
  results.flat_map(&:first)
end

#handle_change(uri, source = nil, &block) ⇒ Object

Synchronizes a change made to the given URI. This method will ensure that new declarations are indexed, removed declarations removed and that the ancestor linearization cache is cleared if necessary. If a block is passed, the consumer of this API has to handle deleting and inserting/updating entries in the index instead of passing the document’s source (used to handle unsaved changes to files) : (URI::Generic uri, ?String? source) ?{ (Index index) -> void } -> void



650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
# File 'lib/ruby_indexer/lib/ruby_indexer/index.rb', line 650

def handle_change(uri, source = nil, &block)
  key = uri.to_s
  original_entries = @uris_to_entries[key]

  if block
    block.call(self)
  else
    delete(uri)
    index_single(
      uri,
      source, #: as !nil
    )
  end

  updated_entries = @uris_to_entries[key]
  return unless original_entries && updated_entries

  # A change in one ancestor may impact several different others, which could be including that ancestor through
  # indirect means like including a module that than includes the ancestor. Trying to figure out exactly which
  # ancestors need to be deleted is too expensive. Therefore, if any of the namespace entries has a change to their
  # ancestor hash, we clear all ancestors and start linearizing lazily again from scratch
  original_map = original_entries
    .select { |e| e.is_a?(Entry::Namespace) } #: as Array[Entry::Namespace]
    .to_h { |e| [e.name, e.ancestor_hash] }

  updated_map = updated_entries
    .select { |e| e.is_a?(Entry::Namespace) } #: as Array[Entry::Namespace]
    .to_h { |e| [e.name, e.ancestor_hash] }

  @ancestors.clear if original_map.any? { |name, hash| updated_map[name] != hash }
end

#index_all(uris: @configuration.indexable_uris, &block) ⇒ Object

Index all files for the given URIs, which defaults to what is configured. A block can be used to track and control indexing progress. That block is invoked with the current progress percentage and should return ‘true` to continue indexing or `false` to stop indexing. : (?uris: Array) ?{ (Integer progress) -> bool } -> void



357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
# File 'lib/ruby_indexer/lib/ruby_indexer/index.rb', line 357

def index_all(uris: @configuration.indexable_uris, &block)
  # When troubleshooting an indexing issue, e.g. through irb, it's not obvious that `index_all` will augment the
  # existing index values, meaning it may contain 'stale' entries. This check ensures that the user is aware of this
  # behavior and can take appropriate action.
  if @initial_indexing_completed
    raise IndexNotEmptyError,
      "The index is not empty. To prevent invalid entries, `index_all` can only be called once."
  end

  RBSIndexer.new(self).index_ruby_core
  # Calculate how many paths are worth 1% of progress
  progress_step = (uris.length / 100.0).ceil

  uris.each_with_index do |uri, index|
    if block && index % progress_step == 0
      progress = (index / progress_step) + 1
      break unless block.call(progress)
    end

    index_file(uri, collect_comments: false)
  end

  @initial_indexing_completed = true
end

#index_file(uri, collect_comments: true) ⇒ Object

Indexes a File URI by reading the contents from disk : (URI::Generic uri, ?collect_comments: bool) -> void



405
406
407
408
409
410
411
# File 'lib/ruby_indexer/lib/ruby_indexer/index.rb', line 405

def index_file(uri, collect_comments: true)
  path = uri.full_path #: as !nil
  index_single(uri, File.read(path), collect_comments: collect_comments)
rescue Errno::EISDIR, Errno::ENOENT
  # If `path` is a directory, just ignore it and continue indexing. If the file doesn't exist, then we also ignore
  # it
end

#index_single(uri, source, collect_comments: true) ⇒ Object

: (URI::Generic uri, String source, ?collect_comments: bool) -> void



383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
# File 'lib/ruby_indexer/lib/ruby_indexer/index.rb', line 383

def index_single(uri, source, collect_comments: true)
  dispatcher = Prism::Dispatcher.new

  result = Prism.parse(source)
  listener = DeclarationListener.new(self, dispatcher, result, uri, collect_comments: collect_comments)
  dispatcher.dispatch(result.value)

  require_path = uri.require_path
  @require_paths_tree.insert(require_path, uri) if require_path

  indexing_errors = listener.indexing_errors.uniq
  indexing_errors.each { |error| $stderr.puts(error) } if indexing_errors.any?
rescue SystemStackError => e
  if e.backtrace&.first&.include?("prism")
    $stderr.puts "Prism error indexing #{uri}: #{e.message}"
  else
    raise
  end
end

#indexed?(name) ⇒ Boolean

: (String name) -> bool

Returns:

  • (Boolean)


698
699
700
# File 'lib/ruby_indexer/lib/ruby_indexer/index.rb', line 698

def indexed?(name)
  @entries.key?(name)
end

#instance_variable_completion_candidates(name, owner_name) ⇒ Object

Returns a list of possible candidates for completion of instance variables for a given owner name. The name must include the ‘@` prefix : (String name, String owner_name) -> Array[(Entry::InstanceVariable | Entry::ClassVariable)]



604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
# File 'lib/ruby_indexer/lib/ruby_indexer/index.rb', line 604

def instance_variable_completion_candidates(name, owner_name)
  entries = prefix_search(name).flatten #: as Array[Entry::InstanceVariable | Entry::ClassVariable]
  # Avoid wasting time linearizing ancestors if we didn't find anything
  return entries if entries.empty?

  ancestors = linearized_ancestors_of(owner_name)

  instance_variables, class_variables = entries.partition { |e| e.is_a?(Entry::InstanceVariable) }
  variables = instance_variables.select { |e| ancestors.any?(e.owner&.name) }

  # Class variables are only owned by the attached class in our representation. If the owner is in a singleton
  # context, we have to search for ancestors of the attached class
  if class_variables.any?
    name_parts = owner_name.split("::")

    if name_parts.last&.start_with?("<Class:")
      attached_name = name_parts[0..-2] #: as !nil
        .join("::")
      attached_ancestors = linearized_ancestors_of(attached_name)
      variables.concat(class_variables.select { |e| attached_ancestors.any?(e.owner&.name) })
    else
      variables.concat(class_variables.select { |e| ancestors.any?(e.owner&.name) })
    end
  end

  variables.uniq!(&:name)
  variables
end

#lengthObject

: -> Integer



703
704
705
# File 'lib/ruby_indexer/lib/ruby_indexer/index.rb', line 703

def length
  @entries.count
end

#linearized_ancestors_of(fully_qualified_name) ⇒ Object

Linearizes the ancestors for a given name, returning the order of namespaces in which Ruby will search for method or constant declarations.

When we add an ancestor in Ruby, that namespace might have ancestors of its own. Therefore, we need to linearize everything recursively to ensure that we are placing ancestors in the right order. For example, if you include a module that prepends another module, then the prepend module appears before the included module.

The order of ancestors is [linearized_prepends, self, linearized_includes, linearized_superclass] : (String fully_qualified_name) -> Array



501
502
503
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
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
# File 'lib/ruby_indexer/lib/ruby_indexer/index.rb', line 501

def linearized_ancestors_of(fully_qualified_name)
  # If we already computed the ancestors for this namespace, return it straight away
  cached_ancestors = @ancestors[fully_qualified_name]
  return cached_ancestors if cached_ancestors

  parts = fully_qualified_name.split("::")
  singleton_levels = 0

  parts.reverse_each do |part|
    break unless part.include?("<Class:")

    singleton_levels += 1
    parts.pop
  end

  attached_class_name = parts.join("::")

  # If we don't have an entry for `name`, raise
  entries = self[fully_qualified_name]

  if singleton_levels > 0 && !entries && indexed?(attached_class_name)
    entries = [existing_or_new_singleton_class(attached_class_name)]
  end

  raise NonExistingNamespaceError, "No entry found for #{fully_qualified_name}" unless entries

  ancestors = [fully_qualified_name]

  # Cache the linearized ancestors array eagerly. This is important because we might have circular dependencies and
  # this will prevent us from falling into an infinite recursion loop. Because we mutate the ancestors array later,
  # the cache will reflect the final result
  @ancestors[fully_qualified_name] = ancestors

  # If none of the entries for `name` are namespaces, raise
  namespaces = entries.filter_map do |entry|
    case entry
    when Entry::Namespace
      entry
    when Entry::ConstantAlias
      self[entry.target]&.grep(Entry::Namespace)
    end
  end.flatten

  raise NonExistingNamespaceError,
    "None of the entries for #{fully_qualified_name} are modules or classes" if namespaces.empty?

  # The original nesting where we discovered this namespace, so that we resolve the correct names of the
  # included/prepended/extended modules and parent classes
  nesting = namespaces.first #: as !nil
    .nesting.flat_map { |n| n.split("::") }

  if nesting.any?
    singleton_levels.times do
      nesting << "<Class:#{nesting.last}>"
    end
  end

  # We only need to run included hooks when linearizing singleton classes. Included hooks are typically used to add
  # new singleton methods or to extend a module through an include. There's no need to support instance methods, the
  # inclusion of another module or the prepending of another module, because those features are already a part of
  # Ruby and can be used directly without any metaprogramming
  run_included_hooks(attached_class_name, nesting) if singleton_levels > 0

  linearize_mixins(ancestors, namespaces, nesting)
  linearize_superclass(
    ancestors,
    attached_class_name,
    fully_qualified_name,
    namespaces,
    nesting,
    singleton_levels,
  )

  ancestors
end

#method_completion_candidates(name, receiver_name) ⇒ Object

: (String? name, String receiver_name) -> Array[(Entry::Member | Entry::MethodAlias)]



227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
# File 'lib/ruby_indexer/lib/ruby_indexer/index.rb', line 227

def method_completion_candidates(name, receiver_name)
  ancestors = linearized_ancestors_of(receiver_name)

  candidates = name ? prefix_search(name).flatten : @entries.values.flatten
  completion_items = candidates.each_with_object({}) do |entry, hash|
    unless entry.is_a?(Entry::Member) || entry.is_a?(Entry::MethodAlias) ||
        entry.is_a?(Entry::UnresolvedMethodAlias)
      next
    end

    entry_name = entry.name
    ancestor_index = ancestors.index(entry.owner&.name)
    existing_entry, existing_entry_index = hash[entry_name]

    # Conditions for matching a method completion candidate:
    # 1. If an ancestor_index was found, it means that this method is owned by the receiver. The exact index is
    # where in the ancestor chain the method was found. For example, if the ancestors are ["A", "B", "C"] and we
    # found the method declared in `B`, then the ancestors index is 1
    #
    # 2. We already established that this method is owned by the receiver. Now, check if we already added a
    # completion candidate for this method name. If not, then we just go and add it (the left hand side of the or)
    #
    # 3. If we had already found a method entry for the same name, then we need to check if the current entry that
    # we are comparing appears first in the hierarchy or not. For example, imagine we have the method `open` defined
    # in both `File` and its parent `IO`. If we first find the method `open` in `IO`, it will be inserted into the
    # hash. Then, when we find the entry for `open` owned by `File`, we need to replace `IO.open` by `File.open`,
    # since `File.open` appears first in the hierarchy chain and is therefore the correct method being invoked. The
    # last part of the conditional checks if the current entry was found earlier in the hierarchy chain, in which
    # case we must update the existing entry to avoid showing the wrong method declaration for overridden methods
    next unless ancestor_index && (!existing_entry || ancestor_index < existing_entry_index)

    if entry.is_a?(Entry::UnresolvedMethodAlias)
      resolved_alias = resolve_method_alias(entry, receiver_name, [])
      hash[entry_name] = [resolved_alias, ancestor_index] if resolved_alias.is_a?(Entry::MethodAlias)
    else
      hash[entry_name] = [entry, ancestor_index]
    end
  end

  completion_items.values.map!(&:first)
end

#namesObject

: -> Array



693
694
695
# File 'lib/ruby_indexer/lib/ruby_indexer/index.rb', line 693

def names
  @entries.keys
end

#prefix_search(query, nesting = nil) ⇒ Object

Searches entries in the index based on an exact prefix, intended for providing autocomplete. All possible matches to the prefix are returned. The return is an array of arrays, where each entry is the array of entries for a given name match. For example: ## Example “‘ruby # If the index has two entries for `Foo::Bar` and one for `Foo::Baz`, then: index.prefix_search(“Foo::B”) # Will return: [

[#<Entry::Class name="Foo::Bar">, #<Entry::Class name="Foo::Bar">],
[#<Entry::Class name="Foo::Baz">],

] “‘ : (String query, ?Array? nesting) -> Array[Array]



179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
# File 'lib/ruby_indexer/lib/ruby_indexer/index.rb', line 179

def prefix_search(query, nesting = nil)
  unless nesting
    results = @entries_tree.search(query)
    results.uniq!
    return results
  end

  results = nesting.length.downto(0).flat_map do |i|
    prefix = nesting[0...i] #: as !nil
      .join("::")
    namespaced_query = prefix.empty? ? query : "#{prefix}::#{query}"
    @entries_tree.search(namespaced_query)
  end

  results.uniq!
  results
end

#register_included_hook(module_name, &hook) ⇒ Object

Register an included ‘hook` that will be executed when `module_name` is included into any namespace : (String module_name) { (Index index, Entry::Namespace base) -> void } -> void



87
88
89
# File 'lib/ruby_indexer/lib/ruby_indexer/index.rb', line 87

def register_included_hook(module_name, &hook)
  (@included_hooks[module_name] ||= []) << hook
end

#resolve(name, nesting, seen_names = []) ⇒ Object

Resolve a constant to its declaration based on its name and the nesting where the reference was found. Parameter documentation:

name: the name of the reference how it was found in the source code (qualified or not) nesting: the nesting structure where the reference was found (e.g.: [“Foo”, “Bar”]) seen_names: this parameter should not be used by consumers of the api. It is used to avoid infinite recursion when resolving circular references : (String name, Array nesting, ?Array seen_names) -> Array[Entry::Constant | Entry::ConstantAlias | Entry::Namespace | Entry::UnresolvedConstantAlias]?



321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
# File 'lib/ruby_indexer/lib/ruby_indexer/index.rb', line 321

def resolve(name, nesting, seen_names = [])
  # If we have a top level reference, then we just search for it straight away ignoring the nesting
  if name.start_with?("::")
    entries = direct_or_aliased_constant(name.delete_prefix("::"), seen_names)
    return entries if entries
  end

  # Non qualified reference path
  full_name = nesting.any? ? "#{nesting.join("::")}::#{name}" : name

  # When the name is not qualified with any namespaces, Ruby will take several steps to try to the resolve the
  # constant. First, it will try to find the constant in the exact namespace where the reference was found
  entries = direct_or_aliased_constant(full_name, seen_names)
  return entries if entries

  # If the constant is not found yet, then Ruby will try to find the constant in the enclosing lexical scopes,
  # unwrapping each level one by one. Important note: the top level is not included because that's the fallback of
  # the algorithm after every other possibility has been exhausted
  entries = lookup_enclosing_scopes(name, nesting, seen_names)
  return entries if entries

  # If the constant does not exist in any enclosing scopes, then Ruby will search for it in the ancestors of the
  # specific namespace where the reference was found
  entries = lookup_ancestor_chain(name, nesting, seen_names)
  return entries if entries

  # Finally, as a fallback, Ruby will search for the constant in the top level namespace
  direct_or_aliased_constant(name, seen_names)
rescue UnresolvableAliasError
  nil
end

#resolve_class_variable(variable_name, owner_name) ⇒ Object

: (String variable_name, String owner_name) -> Array?



591
592
593
594
595
596
597
598
599
# File 'lib/ruby_indexer/lib/ruby_indexer/index.rb', line 591

def resolve_class_variable(variable_name, owner_name)
  entries = self[variable_name]&.grep(Entry::ClassVariable)
  return unless entries&.any?

  ancestors = linearized_attached_ancestors(owner_name)
  return if ancestors.empty?

  entries.select { |e| ancestors.include?(e.owner&.name) }
end

#resolve_instance_variable(variable_name, owner_name) ⇒ Object

Resolves an instance variable name for a given owner name. This method will linearize the ancestors of the owner and find inherited instance variables as well : (String variable_name, String owner_name) -> Array?



580
581
582
583
584
585
586
587
588
# File 'lib/ruby_indexer/lib/ruby_indexer/index.rb', line 580

def resolve_instance_variable(variable_name, owner_name)
  entries = self[variable_name] #: as Array[Entry::InstanceVariable]?
  return unless entries

  ancestors = linearized_ancestors_of(owner_name)
  return if ancestors.empty?

  entries.select { |e| ancestors.include?(e.owner&.name) }
end

#resolve_method(method_name, receiver_name, seen_names = [], inherited_only: false) ⇒ Object

Attempts to find methods for a resolved fully qualified receiver name. Do not provide the ‘seen_names` parameter as it is used only internally to prevent infinite loops when resolving circular aliases Returns `nil` if the method does not exist on that receiver : (String method_name, String receiver_name, ?Array seen_names, ?inherited_only: bool) -> Array[(Entry::Member | Entry::MethodAlias)]?



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
# File 'lib/ruby_indexer/lib/ruby_indexer/index.rb', line 463

def resolve_method(method_name, receiver_name, seen_names = [], inherited_only: false)
  method_entries = self[method_name]
  return unless method_entries

  ancestors = linearized_ancestors_of(receiver_name.delete_prefix("::"))
  ancestors.each do |ancestor|
    next if inherited_only && ancestor == receiver_name

    found = method_entries.filter_map do |entry|
      case entry
      when Entry::Member, Entry::MethodAlias
        entry if entry.owner&.name == ancestor
      when Entry::UnresolvedMethodAlias
        # Resolve aliases lazily as we find them
        if entry.owner&.name == ancestor
          resolved_alias = resolve_method_alias(entry, receiver_name, seen_names)
          resolved_alias if resolved_alias.is_a?(Entry::MethodAlias)
        end
      end
    end

    return found if found.any?
  end

  nil
rescue NonExistingNamespaceError
  nil
end

#search_require_paths(query) ⇒ Object

: (String query) -> Array



142
143
144
# File 'lib/ruby_indexer/lib/ruby_indexer/index.rb', line 142

def search_require_paths(query)
  @require_paths_tree.search(query)
end