Skip to content

Commit

Permalink
Merge pull request #557 from Shopify/at-model-builder
Browse files Browse the repository at this point in the history
Introduce Model building
  • Loading branch information
Morriar authored Jun 18, 2024
2 parents e14186e + a6cad31 commit 8458894
Show file tree
Hide file tree
Showing 7 changed files with 981 additions and 0 deletions.
1 change: 1 addition & 0 deletions lib/spoom.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class Error < StandardError; end
require "spoom/file_collector"
require "spoom/context"
require "spoom/colors"
require "spoom/model"
require "spoom/deadcode"
require "spoom/sorbet"
require "spoom/cli"
Expand Down
7 changes: 7 additions & 0 deletions lib/spoom/model.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# typed: strict
# frozen_string_literal: true

require_relative "parse"
require_relative "model/model"
require_relative "model/namespace_visitor"
require_relative "model/builder"
224 changes: 224 additions & 0 deletions lib/spoom/model/builder.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
# typed: strict
# frozen_string_literal: true

module Spoom
class Model
# Populate a Model by visiting the nodes from a Ruby file
class Builder < NamespaceVisitor
extend T::Sig

sig { params(model: Model, file: String).void }
def initialize(model, file)
super()

@model = model
@file = file
@namespace_nesting = T.let([], T::Array[Namespace])
@last_sigs = T.let([], T::Array[Sig])
end

# Classes

sig { override.params(node: Prism::ClassNode).void }
def visit_class_node(node)
@namespace_nesting << Class.new(
@model.register_symbol(@names_nesting.join("::")),
owner: @namespace_nesting.last,
location: node_location(node),
superclass_name: node.superclass&.slice,
)
super
@namespace_nesting.pop
@last_sigs.clear
end

sig { override.params(node: Prism::SingletonClassNode).void }
def visit_singleton_class_node(node)
@namespace_nesting << SingletonClass.new(
@model.register_symbol(@names_nesting.join("::")),
owner: @namespace_nesting.last,
location: node_location(node),
)
super
@namespace_nesting.pop
@last_sigs.clear
end

# Modules

sig { override.params(node: Prism::ModuleNode).void }
def visit_module_node(node)
@namespace_nesting << Module.new(
@model.register_symbol(@names_nesting.join("::")),
owner: @namespace_nesting.last,
location: node_location(node),
)
super
@namespace_nesting.pop
@last_sigs.clear
end

# Constants

sig { override.params(node: Prism::ConstantPathWriteNode).void }
def visit_constant_path_write_node(node)
@last_sigs.clear

name = node.target.slice
full_name = if name.start_with?("::")
name.delete_prefix("::")
else
[*@names_nesting, name].join("::")
end

Constant.new(
@model.register_symbol(full_name),
owner: @namespace_nesting.last,
location: node_location(node),
value: node.value.slice,
)

super
end

sig { override.params(node: Prism::ConstantWriteNode).void }
def visit_constant_write_node(node)
@last_sigs.clear

Constant.new(
@model.register_symbol([*@names_nesting, node.name.to_s].join("::")),
owner: @namespace_nesting.last,
location: node_location(node),
value: node.value.slice,
)

super
end

sig { override.params(node: Prism::MultiWriteNode).void }
def visit_multi_write_node(node)
@last_sigs.clear

node.lefts.each do |const|
case const
when Prism::ConstantTargetNode, Prism::ConstantPathTargetNode
Constant.new(
@model.register_symbol([*@names_nesting, const.slice].join("::")),
owner: @namespace_nesting.last,
location: node_location(const),
value: node.value.slice,
)
end
end

super
end

# Methods

sig { override.params(node: Prism::DefNode).void }
def visit_def_node(node)
recv = node.receiver

if !recv || recv.is_a?(Prism::SelfNode)
Method.new(
@model.register_symbol([*@names_nesting, node.name.to_s].join("::")),
owner: @namespace_nesting.last,
location: node_location(node),
sigs: collect_sigs,
)
end

super
end

# Accessors

sig { override.params(node: Prism::CallNode).void }
def visit_call_node(node)
return if node.receiver

current_namespace = @namespace_nesting.last

case node.name
when :attr_accessor
sigs = collect_sigs
node.arguments&.arguments&.each do |arg|
next unless arg.is_a?(Prism::SymbolNode)

AttrAccessor.new(
@model.register_symbol([*@names_nesting, arg.slice.delete_prefix(":")].join("::")),
owner: current_namespace,
location: node_location(arg),
sigs: sigs,
)
end
when :attr_reader
sigs = collect_sigs
node.arguments&.arguments&.each do |arg|
next unless arg.is_a?(Prism::SymbolNode)

AttrReader.new(
@model.register_symbol([*@names_nesting, arg.slice.delete_prefix(":")].join("::")),
owner: current_namespace,
location: node_location(arg),
sigs: sigs,
)
end
when :attr_writer
sigs = collect_sigs
node.arguments&.arguments&.each do |arg|
next unless arg.is_a?(Prism::SymbolNode)

AttrWriter.new(
@model.register_symbol([*@names_nesting, arg.slice.delete_prefix(":")].join("::")),
owner: current_namespace,
location: node_location(arg),
sigs: sigs,
)
end
when :include
node.arguments&.arguments&.each do |arg|
next unless arg.is_a?(Prism::ConstantReadNode) || arg.is_a?(Prism::ConstantPathNode)
next unless current_namespace

current_namespace.mixins << Include.new(arg.slice)
end
when :prepend
node.arguments&.arguments&.each do |arg|
next unless arg.is_a?(Prism::ConstantReadNode) || arg.is_a?(Prism::ConstantPathNode)
next unless current_namespace

current_namespace.mixins << Prepend.new(arg.slice)
end
when :extend
node.arguments&.arguments&.each do |arg|
next unless arg.is_a?(Prism::ConstantReadNode) || arg.is_a?(Prism::ConstantPathNode)
next unless current_namespace

current_namespace.mixins << Extend.new(arg.slice)
end
when :sig
@last_sigs << Sig.new(node.slice)
else
@last_sigs.clear
super
end
end

private

sig { returns(T::Array[Sig]) }
def collect_sigs
sigs = @last_sigs
@last_sigs = []
sigs
end

sig { params(node: Prism::Node).returns(Location) }
def node_location(node)
Location.from_prism(@file, node.location)
end
end
end
end
Loading

0 comments on commit 8458894

Please sign in to comment.