module Steep
module Services
class GotoService
include ModuleHelper
module SourceHelper
def from_ruby?
from == :ruby
end
def from_rbs?
from == :rbs
end
end
class ConstantQuery < Struct.new(:name, :from, keyword_init: true)
include SourceHelper
end
class MethodQuery < Struct.new(:name, :from, keyword_init: true)
include SourceHelper
end
class TypeNameQuery < Struct.new(:name, keyword_init: true)
end
attr_reader :type_check, :assignment
def initialize(type_check:, assignment:)
@type_check = type_check
@assignment = assignment
end
def project
type_check.project
end
def implementation(path:, line:, column:)
locations = [] #: Array[loc]
queries = query_at(path: path, line: line, column: column)
queries.uniq!
queries.each do |query|
case query
when ConstantQuery
constant_definition_in_ruby(query.name, locations: locations)
when MethodQuery
method_locations(query.name, locations: locations, in_ruby: true, in_rbs: false)
when TypeNameQuery
type_name_locations(query.name, locations: locations)
end
end
locations.uniq
end
def definition(path:, line:, column:)
locations = [] #: Array[loc]
queries = query_at(path: path, line: line, column: column)
queries.uniq!
queries.each do |query|
case query
when ConstantQuery
constant_definition_in_rbs(query.name, locations: locations) if query.from_ruby?
constant_definition_in_ruby(query.name, locations: locations) if query.from_rbs?
when MethodQuery
method_locations(
query.name,
locations: locations,
in_ruby: query.from_rbs?,
in_rbs: query.from_ruby?
)
when TypeNameQuery
type_name_locations(query.name, locations: locations)
end
end
# Drop un-assigned paths here.
# The path assignment makes sense only for `.rbs` files, because un-assigned `.rb` files are already skipped since they are not type checked.
#
locations.uniq.select do |loc|
case loc
when RBS::Location
assignment =~ loc.name
else
true
end
end
end
def type_definition(path:, line:, column:)
locations = [] #: Array[loc]
relative_path = project.relative_path(path)
target = type_check.source_file?(relative_path) or return []
source = type_check.source_files[relative_path]
typing, signature = type_check_path(target: target, path: relative_path, content: source.content, line: line, column: column)
typing or return []
signature or return []
node, *_parents = typing.source.find_nodes(line: line, column: column)
node or return []
type = typing.type_of(node: node)
subtyping = signature.current_subtyping or return []
each_type_name(type).uniq.each do |name|
type_name_locations(name, locations: locations)
end
locations.uniq.select do |loc|
case loc
when RBS::Location
assignment =~ loc.name
else
true
end
end
end
def each_type_name(type, &block)
if block
case type
when AST::Types::Name::Instance, AST::Types::Name::Alias, AST::Types::Name::Singleton, AST::Types::Name::Interface
yield type.name
when AST::Types::Literal
yield type.back_type.name
when AST::Types::Nil
yield RBS::TypeName.new(name: :NilClass, namespace: RBS::Namespace.root)
when AST::Types::Boolean
yield RBS::BuiltinNames::TrueClass.name
yield RBS::BuiltinNames::FalseClass.name
end
type.each_child do |child|
each_type_name(child, &block)
end
else
enum_for :each_type_name, type
end
end
def test_ast_location(loc, line:, column:)
return false if line < loc.line
return false if line == loc.line && column < loc.column
return false if loc.last_line < line
return false if line == loc.last_line && loc.last_column < column
true
end
def query_at(path:, line:, column:)
queries = [] #: Array[query]
relative_path = project.relative_path(path)
case
when target = type_check.source_file?(relative_path)
source = type_check.source_files[relative_path]
typing, _signature = type_check_path(target: target, path: relative_path, content: source.content, line: line, column: column)
if typing
node, *parents = typing.source.find_nodes(line: line, column: column)
if node && parents
case node.type
when :const, :casgn
named_location = (_ = node.location) #: Parser::AST::_NamedLocation
if test_ast_location(named_location.name, line: line, column: column)
if name = typing.source_index.reference(constant_node: node)
queries << ConstantQuery.new(name: name, from: :ruby)
end
end
when :def, :defs
named_location = (_ = node.location) #: Parser::AST::_NamedLocation
if test_ast_location(named_location.name, line: line, column: column)
if method_context = typing.context_at(line: line, column: column).method_context
if method = method_context.method
method.defs.each do |defn|
singleton_method =
case defn.member
when RBS::AST::Members::MethodDefinition
defn.member.singleton?
when RBS::AST::Members::Attribute
defn.member.kind == :singleton
end
name =
if singleton_method
SingletonMethodName.new(type_name: defn.defined_in, method_name: method_context.name)
else
InstanceMethodName.new(type_name: defn.defined_in, method_name: method_context.name)
end
queries << MethodQuery.new(name: name, from: :ruby)
end
end
end
end
when :send
location = (_ = node.location) #: Parser::AST::_SelectorLocation
if test_ast_location(location.selector, line: line, column: column)
if (parent = parents[0]) && parent.type == :block && parent.children[0] == node
node = parents[0]
end
case call = typing.call_of(node: node)
when TypeInference::MethodCall::Typed, TypeInference::MethodCall::Error
call.method_decls.each do |decl|
queries << MethodQuery.new(name: decl.method_name, from: :ruby)
end
when TypeInference::MethodCall::Untyped
# nop
when TypeInference::MethodCall::NoMethodError
# nop
end
end
end
end
end
when target_names = type_check.signature_file?(path) #: Array[Symbol]
target_names.each do |target_name|
signature_service = type_check.signature_services[target_name] #: SignatureService
env = signature_service.latest_env
buffer = env.buffers.find {|buf| buf.name.to_s == relative_path.to_s } or raise
(dirs, decls = env.signatures[buffer]) or raise
locator = RBS::Locator.new(buffer: buffer, dirs: dirs, decls: decls)
last, nodes = locator.find2(line: line, column: column)
nodes or raise
case nodes[0]
when RBS::AST::Declarations::Class, RBS::AST::Declarations::Module
if last == :name
queries << ConstantQuery.new(name: nodes[0].name, from: :rbs)
end
when RBS::AST::Declarations::Constant
if last == :name
queries << ConstantQuery.new(name: nodes[0].name, from: :rbs)
end
when RBS::AST::Members::MethodDefinition
if last == :name
parent_node = nodes[1] #: RBS::AST::Declarations::Class | RBS::AST::Declarations::Module | RBS::AST::Declarations::Interface
type_name = parent_node.name
method_name = nodes[0].name
if nodes[0].instance?
queries << MethodQuery.new(
name: InstanceMethodName.new(type_name: type_name, method_name: method_name),
from: :rbs
)
end
if nodes[0].singleton?
queries << MethodQuery.new(
name: SingletonMethodName.new(type_name: type_name, method_name: method_name),
from: :rbs
)
end
end
when RBS::AST::Members::Include, RBS::AST::Members::Extend, RBS::AST::Members::Prepend
if last == :name
queries << TypeNameQuery.new(name: nodes[0].name)
end
when RBS::Types::ClassInstance, RBS::Types::ClassSingleton, RBS::Types::Interface, RBS::Types::Alias
if last == :name
queries << TypeNameQuery.new(name: nodes[0].name)
end
when RBS::AST::Declarations::Class::Super, RBS::AST::Declarations::Module::Self
if last == :name
queries << TypeNameQuery.new(name: nodes[0].name)
end
end
end
end
queries
end
def type_check_path(target:, path:, content:, line:, column:)
signature_service = type_check.signature_services[target.name]
subtyping = signature_service.current_subtyping or return
source = Source.parse(content, path: path, factory: subtyping.factory)
source = source.without_unrelated_defs(line: line, column: column)
resolver = RBS::Resolver::ConstantResolver.new(builder: subtyping.factory.definition_builder)
[
Services::TypeCheckService.type_check(source: source, subtyping: subtyping, constant_resolver: resolver),
signature_service
]
rescue
nil
end
def constant_definition_in_rbs(name, locations:)
type_check.signature_services.each_value do |signature|
env = signature.latest_env #: RBS::Environment
case entry = env.constant_entry(name)
when RBS::Environment::ConstantEntry
if entry.decl.location
locations << entry.decl.location[:name]
end
when RBS::Environment::ClassEntry, RBS::Environment::ModuleEntry
entry.decls.each do |d|
if d.decl.location
locations << d.decl.location[:name]
end
end
when RBS::Environment::ClassAliasEntry, RBS::Environment::ModuleAliasEntry
if entry.decl.location
locations << entry.decl.location[:new_name]
end
end
end
locations
end
def constant_definition_in_ruby(name, locations:)
type_check.source_files.each do |path, source|
if typing = source.typing
entry = typing.source_index.entry(constant: name)
entry.definitions.each do |node|
case node.type
when :const
locations << node.location.expression
when :casgn
parent = node.children[0]
location =
if parent
parent.location.expression.join(node.location.name)
else
node.location.name
end
locations << location
end
end
end
end
locations
end
def method_locations(name, in_ruby:, in_rbs:, locations:)
if in_ruby
type_check.source_files.each do |path, source|
if typing = source.typing
entry = typing.source_index.entry(method: name)
if entry.definitions.empty?
if name.is_a?(SingletonMethodName) && name.method_name == :new
initialize = InstanceMethodName.new(method_name: :initialize, type_name: name.type_name)
entry = typing.source_index.entry(method: initialize)
end
end
entry.definitions.each do |node|
case node.type
when :def
locations << node.location.name
when :defs
locations << node.location.name
end
end
end
end
end
if in_rbs
type_check.signature_services.each_value do |signature|
index = signature.latest_rbs_index
entry = index.entry(method_name: name)
if entry.declarations.empty?
if name.is_a?(SingletonMethodName) && name.method_name == :new
initialize = InstanceMethodName.new(method_name: :initialize, type_name: name.type_name)
entry = index.entry(method_name: initialize)
end
end
entry.declarations.each do |decl|
case decl
when RBS::AST::Members::MethodDefinition
if decl.location
locations << decl.location[:name]
end
when RBS::AST::Members::Alias
if decl.location
locations << decl.location[:new_name]
end
when RBS::AST::Members::AttrAccessor, RBS::AST::Members::AttrReader, RBS::AST::Members::AttrWriter
if decl.location
locations << decl.location[:name]
end
end
end
end
end
locations
end
def type_name_locations(name, locations: [])
type_check.signature_services.each_value do |signature|
index = signature.latest_rbs_index
entry = index.entry(type_name: name)
entry.declarations.each do |decl|
case decl
when RBS::AST::Declarations::Class, RBS::AST::Declarations::Module, RBS::AST::Declarations::Interface, RBS::AST::Declarations::TypeAlias
if decl.location
locations << decl.location[:name]
end
when RBS::AST::Declarations::AliasDecl
if decl.location
locations << decl.location[:new_name]
end
else
raise
end
end
end
locations
end
end
end
end