shard-ameba/src/ameba/ast/scope.cr

181 lines
5 KiB
Crystal

require "./variabling/*"
module Ameba::AST
# Represents a context of the local variable visibility.
# This is where the local variables belong to.
class Scope
# Link to local variables
getter variables = [] of Variable
# Link to all variable references in currency scope
getter references = [] of Reference
# Link to the arguments in current scope
getter arguments = [] of Argument
# Link to the instance variables used in current scope
getter ivariables = [] of InstanceVariable
# Link to the outer scope
getter outer_scope : Scope?
# List of inner scopes
getter inner_scopes = [] of Scope
# The actual AST node that represents a current scope.
getter node : Crystal::ASTNode
delegate to_s, to: node
delegate location, to: node
delegate end_location, to: node
def_equals_and_hash node, location
# Creates a new scope. Accepts the AST node and the outer scope.
#
# ```
# scope = Scope.new(class_node, nil)
# ```
def initialize(@node, @outer_scope = nil)
@outer_scope.try &.inner_scopes.<<(self)
end
# Creates a new variable in the current scope.
#
# ```
# scope = Scope.new(class_node, nil)
# scope.add_variable(var_node)
# ```
def add_variable(node)
variables << Variable.new(node, self)
end
# Creates a new argument in the current scope.
#
# ```
# scope = Scope.new(class_node, nil)
# scope.add_argument(arg_node)
# ```
def add_argument(node)
add_variable Crystal::Var.new(node.name).at(node)
arguments << Argument.new(node, variables.last)
end
# Adds a new instance variable to the current scope.
#
# ```
# scope = Scope.new(class_node, nil)
# scope.add_ivariable(ivar_node)
# ```
def add_ivariable(node)
ivariables << InstanceVariable.new(node)
end
# Returns variable by its name or `nil` if it does not exist.
#
# ```
# scope = Scope.new(class_node, nil)
# scope.find_variable("foo")
# ```
def find_variable(name : String)
variables.find(&.name.==(name)) || outer_scope.try &.find_variable(name)
end
# Creates a new assignment for the variable.
#
# ```
# scope = Scope.new(class_node, nil)
# scope.assign_variable(var_name, assign_node)
# ```
def assign_variable(name, node)
find_variable(name).try &.assign(node, self)
end
# Returns `true` if current scope represents a block (or proc),
# `false` otherwise.
def block?
node.is_a?(Crystal::Block) || node.is_a?(Crystal::ProcLiteral)
end
# Returns `true` if current scope represents a spawn block, e. g.
#
# ```
# spawn do
# # ...
# end
# ```
def spawn_block?
return false unless node.is_a?(Crystal::Block)
call = node.as(Crystal::Block).call
call.try(&.name) == "spawn"
end
# Returns `true` if current scope sits inside a macro.
def in_macro?
(node.is_a?(Crystal::Macro) || node.is_a?(Crystal::MacroFor)) ||
!!outer_scope.try(&.in_macro?)
end
# Returns `true` if instance variable is assinged in this scope.
def assigns_ivar?(name)
arguments.find(&.name.== name) &&
ivariables.find(&.name.== "@#{name}")
end
# Returns `true` if and only if current scope represents some
# type definition, for example a class.
def type_definition?
node.is_a?(Crystal::ClassDef) ||
node.is_a?(Crystal::ModuleDef) ||
node.is_a?(Crystal::LibDef) ||
node.is_a?(Crystal::FunDef) ||
node.is_a?(Crystal::TypeDef) ||
node.is_a?(Crystal::CStructOrUnionDef)
end
# Returns true if current scope (or any of inner scopes) references variable,
# `false` otherwise.
def references?(variable : Variable, check_inner_scopes = true)
variable.references.any? do |reference|
return true if reference.scope == self
check_inner_scopes && inner_scopes.any?(&.references?(variable))
end || variable.used_in_macro?
end
# Returns `true` if current scope is a def, `false` otherwise.
def def?
node.is_a?(Crystal::Def)
end
# Returns `true` if this scope is a top level scope, `false` otherwise.
def top_level?
outer_scope.nil?
end
# Returns true if var is an argument in current scope, `false` otherwise.
def arg?(var)
case current_node = node
when Crystal::Def
var.is_a?(Crystal::Arg) && any_arg?(current_node.args, var)
when Crystal::Block
var.is_a?(Crystal::Var) && any_arg?(current_node.args, var)
when Crystal::ProcLiteral
var.is_a?(Crystal::Var) && any_arg?(current_node.def.args, var)
else
false
end
end
private def any_arg?(args, var)
args.any? { |arg| arg.name == var.name && arg.location == var.location }
end
# Returns `true` if the *node* represents exactly
# the same Crystal node as `@node`.
def eql?(node)
node == @node &&
node.location &&
node.location == @node.location
end
end
end