Skip to content

Commit

Permalink
Statically analyze uses of T::Enum
Browse files Browse the repository at this point in the history
  • Loading branch information
egiurleo committed Mar 14, 2024
1 parent f578ef6 commit 76e1ade
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 0 deletions.
74 changes: 74 additions & 0 deletions lib/spoom/model/builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,28 @@ def visit_module(node)
@namespace.pop
end

sig { override.params(node: SyntaxTree::Assign).void }
def visit_assign(node)
parent = @scopes.last
return unless parent&.descendant_of?("T::Enum")
return if node.child_nodes.empty?

assign_name = node_string(node.child_nodes.first)
return unless assign_name

# Connect the enum value to the parent in the model scope
scope_key = "#{parent.full_name}::#{assign_name}"
@model.scopes[scope_key] ||= []
@model.scopes[scope_key] << parent

parent.enum_values << assign_name
end

sig { override.params(node: SyntaxTree::DefNode).void }
def visit_def(node)
loc = node_loc(node)
current_scope.defs << Method.new(loc, node_string(node.name))
node.child_nodes.each { |child| visit(child) }
end

sig { override.params(node: SyntaxTree::CallNode).void }
Expand Down Expand Up @@ -93,6 +111,40 @@ def visit_vcall(node)
visit_send(Send.new(node: node, name: node_string(node.value)))
end

sig { override.params(node: SyntaxTree::Case).void }
def visit_case(node)
kase = Case.new(node_loc(node), current_scope)
current_scope.cases << kase

curr_node = node.consequent

while curr_node.respond_to?(:consequent)
unless curr_node.is_a?(SyntaxTree::When)
curr_node = curr_node.consequent
next
end

kase.conditions << node_string(curr_node.arguments)
curr_node = curr_node.consequent
end

return unless curr_node.is_a?(SyntaxTree::Else)

statement = curr_node.statements.body.first
return if statement.nil? || !statement.respond_to?(:receiver) || !statement.respond_to?(:message)

receiver = statement.receiver
return if !receiver.respond_to?(:value) || !receiver.value.respond_to?(:value)

message = statement.message
return unless message.respond_to?(:value)

receiver_val = statement.receiver.value.value
method_val = statement.message.value

kase.absurd = true if receiver_val == "T" && method_val == "absurd"
end

private

sig { params(send: Send).void }
Expand All @@ -105,6 +157,8 @@ def visit_send(send)
current_scope.attrs << Attr.new(loc, send.name, name)
end
when "const", "prop"
return unless send.args.size >= 1

loc = node_loc(send.node)
name = node_string(T.must(send.args[0]))
type = node_string(T.must(send.args[1]))
Expand All @@ -117,6 +171,24 @@ def visit_send(send)
send.args.each do |arg|
current_scope.includes << Ref.new(node_string(arg))
end
when "serialize"
recv = send.recv
return unless recv

recv_name = node_string(recv)
send.recv_name = recv_name
send.location = node_loc(send.node)

current_scope.calls_to_serialize << send
when "deserialize"
recv = send.recv
return unless recv

recv_name = node_string(recv)
send.recv_name = recv_name
send.location = node_loc(send.node)

current_scope.calls_to_deserialize << send
end
end

Expand Down Expand Up @@ -170,6 +242,8 @@ class Send < T::Struct
const :recv, T.nilable(SyntaxTree::Node), default: nil
const :args, T::Array[SyntaxTree::Node], default: []
const :block, T.nilable(SyntaxTree::Node), default: nil
prop :location, Location, default: Location.none
prop :recv_name, T.nilable(String), default: nil
end

class << self
Expand Down
56 changes: 56 additions & 0 deletions lib/spoom/model/model.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,18 @@ class Scope < Symbol
sig { returns(T::Array[Prop]) }
attr_reader :props

sig { returns T::Array[String] }
attr_reader :enum_values

sig { returns T::Array[Case] }
attr_reader :cases

sig { returns T::Array[Send] }
attr_reader :calls_to_serialize

sig { returns T::Array[Send] }
attr_reader :calls_to_deserialize

sig { params(location: Location, full_name: String).void }
def initialize(location, full_name)
super(location)
Expand All @@ -67,6 +79,10 @@ def initialize(location, full_name)
@props = T.let([], T::Array[Prop])
@includes = T.let([], T::Array[T.any(Ref, Module)])
@attrs = T.let([], T::Array[Attr])
@enum_values = T.let([], T::Array[String])
@cases = T.let([], T::Array[Case])
@calls_to_serialize = T.let([], T::Array[Send])
@calls_to_deserialize = T.let([], T::Array[Send])
end

sig { params(full_name: String).returns(T::Boolean) }
Expand Down Expand Up @@ -190,6 +206,33 @@ def to_s
end
end

class Case < Symbol
extend T::Sig

sig { returns(T::Array[String]) }
attr_reader :conditions

sig { returns(T::Boolean) }
attr_accessor :absurd

sig { returns(Scope) }
attr_accessor :scope

sig { params(location: Location, scope: Scope).void }
def initialize(location, scope)
super(location)

@conditions = T.let([], T::Array[String])
@absurd = T.let(false, T::Boolean)
@scope = scope
end

sig { returns(T::Boolean) }
def absurd?
absurd
end
end

class Prop < Symbol
extend T::Sig

Expand Down Expand Up @@ -350,6 +393,19 @@ def modules
classes
end

sig { returns(T::Array[Case]) }
def cases
cases = T.let([], T::Array[Case])

@scopes.each do |_full_name, scopes|
scopes.each do |scope|
cases.concat(scope.cases)
end
end

cases
end

sig { params(class_name: String).returns(T::Array[Class]) }
def subclasses_of(class_name)
subclasses = T.let([], T::Array[Class])
Expand Down
12 changes: 12 additions & 0 deletions lib/spoom/model/visitor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ def visit_method(symbol)
def visit_prop(symbol)
# no-op
end

sig { params(symbol: Case).void }
def visit_case(symbol)
# no-op
end
end

sig { params(visitor: Visitor).void }
Expand Down Expand Up @@ -104,5 +109,12 @@ def accept(visitor)
visitor.visit_prop(self)
end
end

class Case
sig { override.params(visitor: Visitor).void }
def accept(visitor)
visitor.visit_case(self)
end
end
end
end

0 comments on commit 76e1ade

Please sign in to comment.