# typed: strict
# frozen_string_literal: true
module Spoom
module Deadcode
class Indexer < SyntaxTree::Visitor
extend T::Sig
sig { returns(String) }
attr_reader :path, :file_name
sig { returns(Index) }
attr_reader :index
sig { params(path: String, source: String, index: Index, plugins: T::Array[Plugins::Base]).void }
def initialize(path, source, index, plugins: [])
super()
@path = path
@file_name = T.let(File.basename(path), String)
@source = source
@index = index
@plugins = plugins
@previous_node = T.let(nil, T.nilable(SyntaxTree::Node))
@names_nesting = T.let([], T::Array[String])
@nodes_nesting = T.let([], T::Array[SyntaxTree::Node])
@in_const_field = T.let(false, T::Boolean)
@in_opassign = T.let(false, T::Boolean)
@in_symbol_literal = T.let(false, T::Boolean)
end
# Visit
sig { override.params(node: T.nilable(SyntaxTree::Node)).void }
def visit(node)
return unless node
@nodes_nesting << node
super
@nodes_nesting.pop
@previous_node = node
end
sig { override.params(node: SyntaxTree::AliasNode).void }
def visit_alias(node)
reference_method(node_string(node.right), node)
end
sig { override.params(node: SyntaxTree::ARef).void }
def visit_aref(node)
super
reference_method("[]", node)
end
sig { override.params(node: SyntaxTree::ARefField).void }
def visit_aref_field(node)
super
reference_method("[]=", node)
end
sig { override.params(node: SyntaxTree::ArgBlock).void }
def visit_arg_block(node)
value = node.value
case value
when SyntaxTree::SymbolLiteral
# If the block call is something like `x.select(&:foo)`, we need to reference the `foo` method
reference_method(symbol_string(value), node)
when SyntaxTree::VCall
# If the block call is something like `x.select { ... }`, we need to visit the block
super
end
end
sig { override.params(node: SyntaxTree::Binary).void }
def visit_binary(node)
super
op = node.operator
# Reference the operator itself
reference_method(op.to_s, node)
case op
when :<, :>, :<=, :>=
# For comparison operators, we also reference the `<=>` method
reference_method("<=>", node)
end
end
sig { override.params(node: SyntaxTree::CallNode).void }
def visit_call(node)
visit_send(
Send.new(
node: node,
name: node_string(node.message),
recv: node.receiver,
args: call_args(node.arguments),
),
)
end
sig { override.params(node: SyntaxTree::ClassDeclaration).void }
def visit_class(node)
const_name = node_string(node.constant)
@names_nesting << const_name
define_class(T.must(const_name.split("::").last), @names_nesting.join("::"), node)
# We do not call `super` here because we don't want to visit the `constant` again
visit(node.superclass) if node.superclass
visit(node.bodystmt)
@names_nesting.pop
end
sig { override.params(node: SyntaxTree::Command).void }
def visit_command(node)
visit_send(
Send.new(
node: node,
name: node_string(node.message),
args: call_args(node.arguments),
block: node.block,
),
)
end
sig { override.params(node: SyntaxTree::CommandCall).void }
def visit_command_call(node)
visit_send(
Send.new(
node: node,
name: node_string(node.message),
recv: node.receiver,
args: call_args(node.arguments),
block: node.block,
),
)
end
sig { override.params(node: SyntaxTree::Const).void }
def visit_const(node)
reference_constant(node.value, node) unless @in_symbol_literal
end
sig { override.params(node: SyntaxTree::ConstPathField).void }
def visit_const_path_field(node)
# We do not call `super` here because we don't want to visit the `constant` again
visit(node.parent)
name = node.constant.value
full_name = [*@names_nesting, node_string(node.parent), name].join("::")
define_constant(name, full_name, node)
end
sig { override.params(node: SyntaxTree::DefNode).void }
def visit_def(node)
super
name = node_string(node.name)
define_method(name, [*@names_nesting, name].join("::"), node)
end
sig { override.params(node: SyntaxTree::Field).void }
def visit_field(node)
visit(node.parent)
name = node.name
case name
when SyntaxTree::Const
name = name.value
full_name = [*@names_nesting, node_string(node.parent), name].join("::")
define_constant(name, full_name, node)
when SyntaxTree::Ident
reference_method(name.value, node) if @in_opassign
reference_method("#{name.value}=", node)
end
end
sig { override.params(node: SyntaxTree::ModuleDeclaration).void }
def visit_module(node)
const_name = node_string(node.constant)
@names_nesting << const_name
define_module(T.must(const_name.split("::").last), @names_nesting.join("::"), node)
# We do not call `super` here because we don't want to visit the `constant` again
visit(node.bodystmt)
@names_nesting.pop
end
sig { override.params(node: SyntaxTree::OpAssign).void }
def visit_opassign(node)
# Both `FOO = x` and `FOO += x` yield a VarField node, but the former is a constant definition and the latter is
# a constant reference. We need to distinguish between the two cases.
@in_opassign = true
super
@in_opassign = false
end
sig { params(send: Send).void }
def visit_send(send)
visit(send.recv)
case send.name
when "attr_reader"
send.args.each do |arg|
next unless arg.is_a?(SyntaxTree::SymbolLiteral)
name = symbol_string(arg)
define_attr_reader(name, [*@names_nesting, name].join("::"), arg)
end
when "attr_writer"
send.args.each do |arg|
next unless arg.is_a?(SyntaxTree::SymbolLiteral)
name = symbol_string(arg)
define_attr_writer("#{name}=", "#{[*@names_nesting, name].join("::")}=", arg)
end
when "attr_accessor"
send.args.each do |arg|
next unless arg.is_a?(SyntaxTree::SymbolLiteral)
name = symbol_string(arg)
full_name = [*@names_nesting, name].join("::")
define_attr_reader(name, full_name, arg)
define_attr_writer("#{name}=", "#{full_name}=", arg)
end
else
@plugins.each do |plugin|
plugin.on_send(self, send)
end
reference_method(send.name, send.node)
visit_all(send.args)
visit(send.block)
end
end
sig { override.params(node: SyntaxTree::SymbolLiteral).void }
def visit_symbol_literal(node)
# Something like `:FOO` will yield a Const node but we do not want to treat it as a constant reference.
# So we need to distinguish between the two cases.
@in_symbol_literal = true
super
@in_symbol_literal = false
end
sig { override.params(node: SyntaxTree::TopConstField).void }
def visit_top_const_field(node)
define_constant(node.constant.value, node.constant.value, node)
end
sig { override.params(node: SyntaxTree::VarField).void }
def visit_var_field(node)
value = node.value
case value
when SyntaxTree::Const
if @in_opassign
reference_constant(value.value, node)
else
name = value.value
define_constant(name, [*@names_nesting, name].join("::"), node)
end
when SyntaxTree::Ident
reference_method(value.value, node) if @in_opassign
reference_method("#{value.value}=", node)
end
end
sig { override.params(node: SyntaxTree::VCall).void }
def visit_vcall(node)
visit_send(Send.new(node: node, name: node_string(node.value)))
end
# Definition indexing
sig { params(name: String, full_name: String, node: SyntaxTree::Node).void }
def define_attr_reader(name, full_name, node)
definition = Definition.new(
kind: Definition::Kind::AttrReader,
name: name,
full_name: full_name,
location: node_location(node),
)
@index.define(definition)
@plugins.each { |plugin| plugin.on_define_accessor(self, definition) }
end
sig { params(name: String, full_name: String, node: SyntaxTree::Node).void }
def define_attr_writer(name, full_name, node)
definition = Definition.new(
kind: Definition::Kind::AttrWriter,
name: name,
full_name: full_name,
location: node_location(node),
)
@index.define(definition)
@plugins.each { |plugin| plugin.on_define_accessor(self, definition) }
end
sig { params(name: String, full_name: String, node: SyntaxTree::Node).void }
def define_class(name, full_name, node)
definition = Definition.new(
kind: Definition::Kind::Class,
name: name,
full_name: full_name,
location: node_location(node),
)
@index.define(definition)
@plugins.each { |plugin| plugin.on_define_class(self, definition) }
end
sig { params(name: String, full_name: String, node: SyntaxTree::Node).void }
def define_constant(name, full_name, node)
definition = Definition.new(
kind: Definition::Kind::Constant,
name: name,
full_name: full_name,
location: node_location(node),
)
@index.define(definition)
@plugins.each { |plugin| plugin.on_define_constant(self, definition) }
end
sig { params(name: String, full_name: String, node: SyntaxTree::Node).void }
def define_method(name, full_name, node)
definition = Definition.new(
kind: Definition::Kind::Method,
name: name,
full_name: full_name,
location: node_location(node),
)
@index.define(definition)
@plugins.each { |plugin| plugin.on_define_method(self, definition) }
end
sig { params(name: String, full_name: String, node: SyntaxTree::Node).void }
def define_module(name, full_name, node)
definition = Definition.new(
kind: Definition::Kind::Module,
name: name,
full_name: full_name,
location: node_location(node),
)
@index.define(definition)
@plugins.each { |plugin| plugin.on_define_module(self, definition) }
end
# Reference indexing
sig { params(name: String, node: SyntaxTree::Node).void }
def reference_constant(name, node)
@index.reference(Reference.new(name: name, kind: Reference::Kind::Constant, location: node_location(node)))
end
sig { params(name: String, node: SyntaxTree::Node).void }
def reference_method(name, node)
@index.reference(Reference.new(name: name, kind: Reference::Kind::Method, location: node_location(node)))
end
# Node utils
sig { params(node: T.any(Symbol, SyntaxTree::Node)).returns(String) }
def node_string(node)
case node
when Symbol
node.to_s
else
T.must(@source[node.location.start_char...node.location.end_char])
end
end
sig { params(node: SyntaxTree::Node).returns(Location) }
def node_location(node)
Location.from_syntax_tree(@path, node.location)
end
sig { params(node: SyntaxTree::Node).returns(String) }
def symbol_string(node)
node_string(node).delete_prefix(":")
end
sig do
params(
node: T.any(SyntaxTree::Args, SyntaxTree::ArgParen, SyntaxTree::ArgsForward, NilClass),
).returns(T::Array[SyntaxTree::Node])
end
def call_args(node)
case node
when SyntaxTree::ArgParen
call_args(node.arguments)
when SyntaxTree::Args
node.parts
else
[]
end
end
end
end
end