lib/ruby_indexer/lib/ruby_indexer/index.rb in ruby-lsp-0.17.3 vs lib/ruby_indexer/lib/ruby_indexer/index.rb in ruby-lsp-0.17.4

- old
+ new

@@ -63,17 +63,17 @@ require_path = indexable.require_path @require_paths_tree.delete(require_path) if require_path end - sig { params(entry: Entry).void } - def <<(entry) + sig { params(entry: Entry, skip_prefix_tree: T::Boolean).void } + def add(entry, skip_prefix_tree: false) name = entry.name (@entries[name] ||= []) << entry (@files_to_entries[entry.file_path] ||= []) << entry - @entries_tree.insert(name, T.must(@entries[name])) + @entries_tree.insert(name, T.must(@entries[name])) unless skip_prefix_tree end sig { params(fully_qualified_name: String).returns(T.nilable(T::Array[Entry])) } def [](fully_qualified_name) @entries[fully_qualified_name.delete_prefix("::")] @@ -116,30 +116,53 @@ end # Fuzzy searches index entries based on Jaro-Winkler similarity. If no query is provided, all entries are returned sig { params(query: T.nilable(String)).returns(T::Array[Entry]) } def fuzzy_search(query) - return @entries.flat_map { |_name, entries| entries } unless query + unless query + entries = @entries.filter_map do |_name, entries| + next if entries.first.is_a?(Entry::SingletonClass) + 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) + 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 - sig { params(name: String, receiver_name: String).returns(T::Array[Entry]) } + sig do + params( + name: T.nilable(String), + receiver_name: String, + ).returns(T::Array[T.any(Entry::Member, Entry::MethodAlias)]) + end def method_completion_candidates(name, receiver_name) ancestors = linearized_ancestors_of(receiver_name) - candidates = prefix_search(name).flatten - candidates.select! do |entry| - entry.is_a?(RubyIndexer::Entry::Member) && ancestors.any?(entry.owner&.name) + + candidates = name ? prefix_search(name).flatten : @entries.values.flatten + candidates.filter_map do |entry| + case entry + when Entry::Member, Entry::MethodAlias + entry if ancestors.any?(entry.owner&.name) + when Entry::UnresolvedMethodAlias + if ancestors.any?(entry.owner&.name) + resolved_alias = resolve_method_alias(entry, receiver_name) + resolved_alias if resolved_alias.is_a?(Entry::MethodAlias) + end + end end - candidates end # Resolve a constant to its declaration based on its name and the nesting where the reference was found. Parameter # documentation: # @@ -221,10 +244,16 @@ require_path = indexable_path.require_path @require_paths_tree.insert(require_path, indexable_path) if require_path 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 + rescue SystemStackError => e + if e.backtrace&.first&.include?("prism") + $stderr.puts "Prism error indexing #{indexable_path.full_path}: #{e.message}" + else + raise + end end # 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: @@ -267,24 +296,36 @@ real_parts.join("::") end # Attempts to find methods for a resolved fully qualified receiver name. # Returns `nil` if the method does not exist on that receiver - sig { params(method_name: String, receiver_name: String).returns(T.nilable(T::Array[Entry::Member])) } + sig do + params( + method_name: String, + receiver_name: String, + ).returns(T.nilable(T::Array[T.any(Entry::Member, Entry::MethodAlias)])) + end def resolve_method(method_name, receiver_name) method_entries = self[method_name] - ancestors = linearized_ancestors_of(receiver_name.delete_prefix("::")) return unless method_entries + ancestors = linearized_ancestors_of(receiver_name.delete_prefix("::")) ancestors.each do |ancestor| - found = method_entries.select do |entry| - next unless entry.is_a?(Entry::Member) - - entry.owner&.name == ancestor + 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) + resolved_alias if resolved_alias.is_a?(Entry::MethodAlias) + end + end end - return T.cast(found, T::Array[Entry::Member]) if found.any? + return found if found.any? end nil rescue NonExistingNamespaceError nil @@ -507,16 +548,16 @@ seen_names: T::Array[String], ).returns(T.nilable(T::Array[Entry])) end def lookup_ancestor_chain(name, nesting, seen_names) *nesting_parts, constant_name = build_non_redundant_full_name(name, nesting).split("::") - return if T.must(nesting_parts).empty? + return if nesting_parts.empty? - namespace_entries = resolve(T.must(nesting_parts).join("::"), [], seen_names) + namespace_entries = resolve(nesting_parts.join("::"), [], seen_names) return unless namespace_entries - ancestors = T.must(nesting_parts).empty? ? [] : linearized_ancestors_of(T.must(namespace_entries.first).name) + ancestors = nesting_parts.empty? ? [] : linearized_ancestors_of(T.must(namespace_entries.first).name) ancestors.each do |ancestor_name| entries = direct_or_aliased_constant("#{ancestor_name}::#{constant_name}", seen_names) return entries if entries end @@ -564,8 +605,29 @@ end sig { params(name: String, seen_names: T::Array[String]).returns(T.nilable(T::Array[Entry])) } def search_top_level(name, seen_names) @entries[name]&.map { |e| e.is_a?(Entry::UnresolvedAlias) ? resolve_alias(e, seen_names) : e } + end + + # Attempt to resolve a given unresolved method alias. This method returns the resolved alias if we managed to + # identify the target or the same unresolved alias entry if we couldn't + sig do + params( + entry: Entry::UnresolvedMethodAlias, + receiver_name: String, + ).returns(T.any(Entry::MethodAlias, Entry::UnresolvedMethodAlias)) + end + def resolve_method_alias(entry, receiver_name) + return entry if entry.new_name == entry.old_name + + target_method_entries = resolve_method(entry.old_name, receiver_name) + return entry unless target_method_entries + + resolved_alias = Entry::MethodAlias.new(T.must(target_method_entries.first), entry) + original_entries = T.must(@entries[entry.new_name]) + original_entries.delete(entry) + original_entries << resolved_alias + resolved_alias end end end