Skip to content

Commit

Permalink
Merge pull request #795 from Shopify/uk-refactor-dsl-compilers
Browse files Browse the repository at this point in the history
Refactor DSL compilers
  • Loading branch information
paracycle authored Feb 14, 2022
2 parents 3ff5201 + 0a044d6 commit 445cdcd
Show file tree
Hide file tree
Showing 35 changed files with 861 additions and 773 deletions.
2 changes: 1 addition & 1 deletion lib/tapioca/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ def dsl(*constants)
only: options[:only],
exclude: options[:exclude],
file_header: options[:file_header],
compiler_path: Tapioca::Dsl::DSL_COMPILERS_DIR,
compiler_path: Tapioca::Dsl::Compilers::DIRECTORY,
tapioca_path: TAPIOCA_DIR,
should_verify: options[:verify],
quiet: options[:quiet],
Expand Down
15 changes: 14 additions & 1 deletion lib/tapioca/commands/dsl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ def constantize(constant_names)
sig { params(compiler_names: T::Array[String]).returns(T::Array[T.class_of(Tapioca::Dsl::Compiler)]) }
def constantize_compilers(compiler_names)
compiler_map = compiler_names.to_h do |name|
[name, Tapioca::Dsl::Compiler.resolve(name)]
[name, resolve(name)]
end

unprocessable_compilers = compiler_map.select { |_, v| v.nil? }
Expand All @@ -203,6 +203,19 @@ def constantize_compilers(compiler_names)
T.cast(compiler_map.values, T::Array[T.class_of(Tapioca::Dsl::Compiler)])
end

sig { params(name: String).returns(T.nilable(T.class_of(Tapioca::Dsl::Compiler))) }
def resolve(name)
# Try to find built-in tapioca compiler first, then globally defined compiler.
potentials = Tapioca::Dsl::Compilers::NAMESPACES.map do |namespace|
Object.const_get(namespace + name)
rescue NameError
# Skip if we can't find compiler by the potential name
nil
end

potentials.compact.first
end

sig do
params(
constant_name: String,
Expand Down
66 changes: 28 additions & 38 deletions lib/tapioca/dsl/compiler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,45 +9,34 @@

module Tapioca
module Dsl
DSL_COMPILERS_DIR = T.let(File.expand_path("../compilers", __FILE__).to_s, String)

class Compiler
extend T::Sig
extend T::Helpers
extend T::Generic

include Reflection
extend Reflection

Elem = type_member(upper: Module)

abstract!

sig { returns(T::Set[Module]) }
attr_reader :processable_constants

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

sig { params(name: String).returns(T.nilable(T.class_of(Compiler))) }
def self.resolve(name)
# Try to find built-in tapioca compiler first, then globally defined compiler.
potentials = ["Tapioca::Dsl::Compilers::#{name}", name].map do |potential_name|
Object.const_get(potential_name)
rescue NameError
# Skip if we can't find compiler by the potential name
nil
end
sig { returns(Elem) }
attr_reader :constant

potentials.compact.first
end
sig { returns(RBI::Tree) }
attr_reader :root

sig { params(pipeline: Tapioca::Dsl::Pipeline).void }
def initialize(pipeline)
sig { params(pipeline: Tapioca::Dsl::Pipeline, root: RBI::Tree, constant: Elem).void }
def initialize(pipeline, root, constant)
@pipeline = pipeline
@processable_constants = T.let(Set.new(gather_constants), T::Set[Module])
@processable_constants.compare_by_identity
@root = root
@constant = constant
@errors = T.let([], T::Array[String])
end

sig { params(constant: Module).returns(T::Boolean) }
def handles?(constant)
def self.handles?(constant)
processable_constants.include?(constant)
end

Expand All @@ -56,36 +45,37 @@ def compiler_enabled?(compiler_name)
@pipeline.compiler_enabled?(compiler_name)
end

sig do
abstract
.type_parameters(:T)
.params(
tree: RBI::Tree,
constant: T.type_parameter(:T)
)
.void
end
def decorate(tree, constant); end
sig { abstract.void }
def decorate; end

sig { abstract.returns(T::Enumerable[Module]) }
def gather_constants; end
def self.gather_constants; end

sig { returns(T::Set[Module]) }
def self.processable_constants
@processable_constants ||= T.let(
Set.new(gather_constants).tap(&:compare_by_identity),
T.nilable(T::Set[Module])
)
T.must(@processable_constants)
end

# NOTE: This should eventually accept an `Error` object or `Exception` rather than simply a `String`.
sig { params(error: String).void }
def add_error(error)
@errors << error
@pipeline.add_error(error)
end

private

sig { returns(T::Enumerable[Class]) }
def all_classes
private_class_method def self.all_classes
@all_classes = T.let(@all_classes, T.nilable(T::Enumerable[Class]))
@all_classes ||= T.cast(ObjectSpace.each_object(Class), T::Enumerable[Class]).each
end

sig { returns(T::Enumerable[Module]) }
def all_modules
private_class_method def self.all_modules
@all_modules = T.let(@all_modules, T.nilable(T::Enumerable[Module]))
@all_modules ||= T.cast(ObjectSpace.each_object(Module), T::Enumerable[Module]).each
end
Expand Down
31 changes: 31 additions & 0 deletions lib/tapioca/dsl/compilers.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# typed: strict
# frozen_string_literal: true

require "tapioca/rbi_ext/model"
require "tapioca/dsl/helpers/param_helper"
require "tapioca/dsl/pipeline"

module Tapioca
module Dsl
module Compilers
DIRECTORY = T.let(
File.expand_path("compilers", __dir__),
String
)

# DSL compilers are either built-in to Tapioca and live under the
# `Tapioca::Dsl::Compilers` namespace (i.e. this namespace), and
# can be referred to by just using the class name, or they live in
# a different namespace and can only be referred to using their fully
# qualified name. This constant encapsulates that dual lookup when
# a compiler needs to be resolved by name.
NAMESPACES = T.let(
[
"#{name}::", # compilers in this namespace
"::", # compilers that need to be fully namespaced
],
T::Array[String]
)
end
end
end
10 changes: 6 additions & 4 deletions lib/tapioca/dsl/compilers/aasm.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,10 @@ class AASM < Compiler
T::Array[String]
)

sig { override.params(root: RBI::Tree, constant: T.all(::AASM::ClassMethods, Class)).void }
def decorate(root, constant)
Elem = type_member(fixed: T.all(::AASM::ClassMethods, Class))

sig { override.void }
def decorate
aasm = constant.aasm
return if !aasm || aasm.states.empty?

Expand Down Expand Up @@ -103,7 +105,7 @@ def decorate(root, constant)
event.create_method(
method,
parameters: [
create_block_param("block", type: "T.proc.bind(#{constant.name}).void"),
create_block_param("block", type: "T.proc.bind(#{name_of(constant)}).void"),
]
)
end
Expand All @@ -113,7 +115,7 @@ def decorate(root, constant)
end

sig { override.returns(T::Enumerable[Module]) }
def gather_constants
def self.gather_constants
T.cast(ObjectSpace.each_object(::AASM::ClassMethods), T::Enumerable[Module])
end
end
Expand Down
23 changes: 8 additions & 15 deletions lib/tapioca/dsl/compilers/action_controller_helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,10 @@ module Compilers
class ActionControllerHelpers < Compiler
extend T::Sig

sig do
override
.params(root: RBI::Tree, constant: T.class_of(::ActionController::Base))
.void
end
def decorate(root, constant)
Elem = type_member(fixed: T.class_of(::ActionController::Base))

sig { override.void }
def decorate
helpers_module = constant._helpers
proxied_helper_methods = constant._helper_methods.map(&:to_s).map(&:to_sym)

Expand Down Expand Up @@ -103,7 +101,7 @@ def decorate(root, constant)
# helper method defined via the `helper_method` call in the controller.
helpers_module.instance_methods(false).each do |method_name|
method = if proxied_helper_methods.include?(method_name)
helper_method_proxy_target(constant, method_name)
helper_method_proxy_target(method_name)
else
helpers_module.instance_method(method_name)
end
Expand All @@ -124,19 +122,14 @@ def decorate(root, constant)
end

sig { override.returns(T::Enumerable[Module]) }
def gather_constants
def self.gather_constants
descendants_of(::ActionController::Base).reject(&:abstract?).select(&:name)
end

private

sig do
params(
constant: T.class_of(::ActionController::Base),
method_name: Symbol
).returns(T.nilable(UnboundMethod))
end
def helper_method_proxy_target(constant, method_name)
sig { params(method_name: Symbol).returns(T.nilable(UnboundMethod)) }
def helper_method_proxy_target(method_name)
# Lookup the proxy target method only if it is defined as a public/protected or private method.
if constant.method_defined?(method_name) || constant.private_method_defined?(method_name)
constant.instance_method(method_name)
Expand Down
8 changes: 5 additions & 3 deletions lib/tapioca/dsl/compilers/action_mailer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,10 @@ module Compilers
class ActionMailer < Compiler
extend T::Sig

sig { override.params(root: RBI::Tree, constant: T.class_of(::ActionMailer::Base)).void }
def decorate(root, constant)
Elem = type_member(fixed: T.class_of(::ActionMailer::Base))

sig { override.void }
def decorate
root.create_path(constant) do |mailer|
constant.action_methods.to_a.each do |mailer_method|
method_def = constant.instance_method(mailer_method)
Expand All @@ -53,7 +55,7 @@ def decorate(root, constant)
end

sig { override.returns(T::Enumerable[Module]) }
def gather_constants
def self.gather_constants
descendants_of(::ActionMailer::Base).reject(&:abstract?)
end
end
Expand Down
10 changes: 6 additions & 4 deletions lib/tapioca/dsl/compilers/active_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,10 @@ module Compilers
class ActiveJob < Compiler
extend T::Sig

sig { override.params(root: RBI::Tree, constant: T.class_of(::ActiveJob::Base)).void }
def decorate(root, constant)
Elem = type_member(fixed: T.class_of(::ActiveJob::Base))

sig { override.void }
def decorate
return unless constant.instance_methods(false).include?(:perform)

root.create_path(constant) do |job|
Expand All @@ -52,7 +54,7 @@ def decorate(root, constant)
job.create_method(
"perform_later",
parameters: parameters,
return_type: "T.any(#{constant.name}, FalseClass)",
return_type: "T.any(#{name_of(constant)}, FalseClass)",
class_method: true
)

Expand All @@ -66,7 +68,7 @@ def decorate(root, constant)
end

sig { override.returns(T::Enumerable[Module]) }
def gather_constants
def self.gather_constants
descendants_of(::ActiveJob::Base)
end
end
Expand Down
14 changes: 8 additions & 6 deletions lib/tapioca/dsl/compilers/active_model_attributes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,11 @@ module Compilers
class ActiveModelAttributes < Compiler
extend T::Sig

sig { override.params(root: RBI::Tree, constant: T.all(Class, ::ActiveModel::Attributes::ClassMethods)).void }
def decorate(root, constant)
attribute_methods = attribute_methods_for(constant)
Elem = type_member(fixed: T.all(Class, ::ActiveModel::Attributes::ClassMethods))

sig { override.void }
def decorate
attribute_methods = attribute_methods_for_constant
return if attribute_methods.empty?

root.create_path(constant) do |klass|
Expand All @@ -52,16 +54,16 @@ def decorate(root, constant)
end

sig { override.returns(T::Enumerable[Module]) }
def gather_constants
def self.gather_constants
all_classes.grep(::ActiveModel::Attributes::ClassMethods)
end

private

HANDLED_METHOD_TARGETS = T.let(["attribute", "attribute="], T::Array[String])

sig { params(constant: ::ActiveModel::Attributes::ClassMethods).returns(T::Array[[::String, ::String]]) }
def attribute_methods_for(constant)
sig { returns(T::Array[[::String, ::String]]) }
def attribute_methods_for_constant
patterns = if constant.respond_to?(:attribute_method_patterns)
# https://github.com/rails/rails/pull/44367
T.unsafe(constant).attribute_method_patterns
Expand Down
12 changes: 5 additions & 7 deletions lib/tapioca/dsl/compilers/active_model_secure_password.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,10 @@ module Compilers
class ActiveModelSecurePassword < Compiler
extend T::Sig

sig do
override
.params(root: RBI::Tree, constant: T.all(Class, ::ActiveModel::SecurePassword::ClassMethods))
.void
end
def decorate(root, constant)
Elem = type_member(fixed: T.all(Class, ::ActiveModel::SecurePassword::ClassMethods))

sig { override.void }
def decorate
instance_methods_modules = if constant < ActiveModel::SecurePassword::InstanceMethodsOnActivation
# pre Rails 6.0, this used to be a single static module
[ActiveModel::SecurePassword::InstanceMethodsOnActivation]
Expand All @@ -88,7 +86,7 @@ def decorate(root, constant)
end

sig { override.returns(T::Enumerable[Module]) }
def gather_constants
def self.gather_constants
# This selects all classes that are `ActiveModel::SecurePassword::ClassMethods === klass`.
# In other words, we select all classes that have `ActiveModel::SecurePassword::ClassMethods`
# as an ancestor of its singleton class, i.e. all classes that have extended the
Expand Down
Loading

0 comments on commit 445cdcd

Please sign in to comment.