lib/tapioca/compilers/symbol_table/symbol_generator.rb in tapioca-0.4.27 vs lib/tapioca/compilers/symbol_table/symbol_generator.rb in tapioca-0.5.0
- old
+ new
@@ -1,21 +1,18 @@
# typed: true
# frozen_string_literal: true
-require 'pathname'
+require "pathname"
module Tapioca
module Compilers
module SymbolTable
class SymbolGenerator
extend(T::Sig)
+ include(Reflection)
- IGNORED_SYMBOLS = %w{
- YAML
- MiniTest
- Mutex
- }
+ IGNORED_SYMBOLS = ["YAML", "MiniTest", "Mutex"]
attr_reader(:gem, :indent)
sig { params(gem: Gemfile::GemSpec, indent: Integer).void }
def initialize(gem, indent = 0)
@@ -55,12 +52,12 @@
sig { params(symbols: T::Set[String]).returns(T::Set[String]) }
def engine_symbols(symbols)
return Set.new unless Object.const_defined?("Rails::Engine")
- engine = Object.const_get("Rails::Engine")
- .descendants.reject(&:abstract_railtie?)
+ engine = descendants_of(Object.const_get("Rails::Engine"))
+ .reject(&:abstract_railtie?)
.find do |klass|
name = name_of(klass)
!name.nil? && symbols.include?(name)
end
@@ -100,11 +97,11 @@
sig { params(tree: RBI::Tree, name: T.nilable(String), constant: BasicObject).void.checked(:never) }
def compile(tree, name, constant)
return unless constant
return unless name
return if name.strip.empty?
- return if name.start_with?('#<')
+ return if name.start_with?("#<")
return if name.downcase == name
return if alias_namespaced?(name)
return if seen?(name)
return if T::Enum === constant # T::Enum instances are defined via `compile_enums`
@@ -142,11 +139,13 @@
end
sig { params(tree: RBI::Tree, name: String, value: BasicObject).void.checked(:never) }
def compile_object(tree, name, value)
return if symbol_ignored?(name)
+
klass = class_of(value)
+ return if klass == TypeMember || klass == TypeTemplate
klass_name = if klass == ObjectSpace::WeakMap
# WeakMap is an implicit generic with one type variable
"ObjectSpace::WeakMap[T.untyped]"
elsif T::Generic === klass
@@ -187,13 +186,14 @@
compile_subconstants(tree, name, constant)
end
sig { params(tree: RBI::Tree, name: String, constant: Module).void }
def compile_body(tree, name, constant)
+ # Compiling type variables must happen first to populate generic names
+ compile_type_variables(tree, constant)
compile_methods(tree, name, constant)
compile_module_helpers(tree, constant)
- compile_type_variables(tree, constant)
compile_mixins(tree, constant)
compile_mixes_in_class_methods(tree, constant)
compile_props(tree, constant)
compile_enums(tree, constant)
end
@@ -265,32 +265,31 @@
return unless type_variables
# Create a map of subconstants (via their object ids) to their names.
# We need this later when we want to lookup the name of the registered type
# variable via the value of the type variable constant.
- subconstant_to_name_lookup = constants_of(constant).map do |constant_name|
- [
- object_id_of(resolve_constant(constant_name.to_s, namespace: constant)),
- constant_name,
- ]
- end.to_h
+ subconstant_to_name_lookup = constants_of(constant)
+ .each_with_object({}.compare_by_identity) do |constant_name, table|
+ table[resolve_constant(constant_name.to_s, namespace: constant)] = constant_name.to_s
+ end
# Map each type variable to its string representation.
#
# Each entry of `type_variables` maps an object_id to a String,
# and the order they are inserted into the hash is the order they should be
# defined in the source code.
#
# By looping over these entries and then getting the actual constant name
# from the `subconstant_to_name_lookup` we defined above, gives us all the
# information we need to serialize type variable definitions.
- type_variable_declarations = type_variables.map do |type_variable_id, serialized_type_variable|
- constant_name = subconstant_to_name_lookup[type_variable_id]
+ type_variable_declarations = type_variables.map do |type_variable, serialized_type_variable|
+ constant_name = subconstant_to_name_lookup[type_variable]
+ type_variable.name = constant_name
# Here, we know that constant_value will be an instance of
# T::Types::CustomTypeVariable, which knows how to serialize
# itself to a type_member/type_template
- tree << RBI::TypeMember.new(constant_name.to_s, serialized_type_variable)
+ tree << RBI::TypeMember.new(constant_name, serialized_type_variable)
end
return if type_variable_declarations.empty?
tree << RBI::Extend.new("T::Generic")
@@ -390,71 +389,130 @@
qname = qualified_name_of(mod)
tree << RBI::Extend.new(T.must(qname))
end
end
- sig { params(tree: RBI::Tree, constant: Module).void }
- def compile_mixes_in_class_methods(tree, constant)
- return if constant.is_a?(Class)
+ sig { params(constant: Module).returns([T::Array[Module], T::Array[Module]]) }
+ def collect_dynamic_mixins_of(constant)
+ mixins_from_modules = {}.compare_by_identity
- mixins_from_modules = {}
-
Class.new do
- # rubocop:disable Style/MethodMissingSuper, Style/MissingRespondToMissing
- def method_missing(symbol, *args)
+ # Override the `self.include` method
+ define_singleton_method(:include) do |mod|
+ # Take a snapshot of the list of singleton class ancestors
+ # before the actual include
+ before = singleton_class.ancestors
+ # Call the actual `include` method with the supplied module
+ include_result = super(mod)
+ # Take a snapshot of the list of singleton class ancestors
+ # after the actual include
+ after = singleton_class.ancestors
+ # The difference is the modules that are added to the list
+ # of ancestors of the singleton class. Those are all the
+ # modules that were `extend`ed due to the `include` call.
+ #
+ # We record those modules on our lookup table keyed by
+ # the included module with the values being all the modules
+ # that that module pulls into the singleton class.
+ #
+ # We need to reverse the order, since the extend order should
+ # be the inverse of the ancestor order. That is, earlier
+ # extended modules would be later in the ancestor chain.
+ mixins_from_modules[mod] = (after - before).reverse!
+
+ include_result
+ rescue Exception # rubocop:disable Lint/RescueException
+ # this is a best effort, bail if we can't perform this
end
- define_singleton_method(:include) do |mod|
- begin
- before = singleton_class.ancestors
- super(mod).tap do
- mixins_from_modules[mod] = singleton_class.ancestors - before
- end
- rescue Exception # rubocop:disable Lint/RescueException
- # this is a best effort, bail if we can't perform this
- end
+ # rubocop:disable Style/MissingRespondToMissing
+ def method_missing(symbol, *args)
+ # We need this here so that we can handle any random instance
+ # method calls on the fake including class that may be done by
+ # the included module during the `self.included` hook.
end
class << self
def method_missing(symbol, *args)
+ # Similarly, we need this here so that we can handle any
+ # random class method calls on the fake including class
+ # that may be done by the included module during the
+ # `self.included` hook.
end
end
- # rubocop:enable Style/MethodMissingSuper, Style/MissingRespondToMissing
+ # rubocop:enable Style/MissingRespondToMissing
end.include(constant)
- all_dynamic_extends = mixins_from_modules.delete(constant)
- all_dynamic_includes = mixins_from_modules.keys
- dynamic_extends_from_dynamic_includes = mixins_from_modules.values.flatten
- dynamic_extends = all_dynamic_extends - dynamic_extends_from_dynamic_includes
+ [
+ # The value that corresponds to the original included constant
+ # is the list of all dynamically extended modules because of that
+ # constant. We grab that value by deleting the key for the original
+ # constant.
+ T.must(mixins_from_modules.delete(constant)),
+ # Since we deleted the original constant from the list of keys, all
+ # the keys that remain are the ones that are dynamically included modules
+ # during the include of the original constant.
+ mixins_from_modules.keys,
+ ]
+ end
- all_dynamic_includes
- .select { |mod| (name = name_of(mod)) && !name.start_with?("T::") }
- .map do |mod|
- add_to_symbol_queue(name_of(mod))
+ sig { params(constant: Module, dynamic_extends: T::Array[Module]).returns(T::Array[Module]) }
+ def collect_mixed_in_class_methods(constant, dynamic_extends)
+ if Tapioca::Compilers::Sorbet.supports?(:mixes_in_class_methods_multiple_args)
+ # If we can generate multiple mixes_in_class_methods, then
+ # we want to use all dynamic extends that are not the constant itself
+ return dynamic_extends.select { |mod| mod != constant }
+ end
- qname = qualified_name_of(mod)
- tree << RBI::Include.new(T.must(qname))
- end.join("\n")
-
+ # For older Sorbet version, we do an explicit check for an AS::Concern
+ # related ClassMethods module.
ancestors = singleton_class_of(constant).ancestors
extends_as_concern = ancestors.any? do |mod|
qualified_name_of(mod) == "::ActiveSupport::Concern"
end
class_methods_module = resolve_constant("#{name_of(constant)}::ClassMethods")
mixed_in_module = if extends_as_concern && Module === class_methods_module
+ # If this module is a concern and the ClassMethods module exists
+ # then, we prefer to generate a mixes_in_class_methods call for
+ # that module only, since we only have a single shot.
class_methods_module
else
+ # Otherwise, we use the first dynamic extend module that is not
+ # the constant itself. We don't have a better heuristic in the
+ # absence of being able to supply multiple arguments.
dynamic_extends.find { |mod| mod != constant }
end
- return if mixed_in_module.nil?
+ Array(mixed_in_module)
+ end
- qualified_name = qualified_name_of(mixed_in_module)
- return if qualified_name.nil? || qualified_name == ""
+ sig { params(tree: RBI::Tree, constant: Module).void }
+ def compile_mixes_in_class_methods(tree, constant)
+ return if constant.is_a?(Class)
- tree << RBI::MixesInClassMethods.new(qualified_name)
+ dynamic_extends, dynamic_includes = collect_dynamic_mixins_of(constant)
+
+ dynamic_includes
+ .select { |mod| (name = name_of(mod)) && !name.start_with?("T::") }
+ .map do |mod|
+ add_to_symbol_queue(name_of(mod))
+
+ qname = qualified_name_of(mod)
+ tree << RBI::Include.new(T.must(qname))
+ end
+
+ mixed_in_class_methods = collect_mixed_in_class_methods(constant, dynamic_extends)
+ return if mixed_in_class_methods.empty?
+
+ mixed_in_class_methods.each do |mod|
+ add_to_symbol_queue(name_of(mod))
+
+ qualified_name = qualified_name_of(mod)
+ next if qualified_name.nil? || qualified_name.empty?
+ tree << RBI::MixesInClassMethods.new(qualified_name)
+ end
rescue
nil # silence errors
end
sig { params(tree: RBI::Tree, name: String, constant: Module).void }
@@ -484,38 +542,38 @@
.each do |visibility, method_list|
method_list.sort!.map do |name|
next if name == :initialize
vis = case visibility
when :protected
- RBI::Visibility::Protected
+ RBI::Protected.new
when :private
- RBI::Visibility::Private
+ RBI::Private.new
else
- RBI::Visibility::Public
+ RBI::Public.new
end
compile_method(tree, module_name, mod, mod.instance_method(name), vis)
end
end
end
sig { params(mod: Module).returns(T::Hash[Symbol, T::Array[Symbol]]) }
def method_names_by_visibility(mod)
{
- public: Module.instance_method(:public_instance_methods).bind(mod).call,
- protected: Module.instance_method(:protected_instance_methods).bind(mod).call,
- private: Module.instance_method(:private_instance_methods).bind(mod).call,
+ public: public_instance_methods_of(mod),
+ protected: protected_instance_methods_of(mod),
+ private: private_instance_methods_of(mod),
}
end
sig { params(constant: Module, method_name: String).returns(T::Boolean) }
def struct_method?(constant, method_name)
return false unless T::Props::ClassMethods === constant
constant
.props
.keys
- .include?(method_name.gsub(/=$/, '').to_sym)
+ .include?(method_name.gsub(/=$/, "").to_sym)
end
sig do
params(
tree: RBI::Tree,
@@ -523,11 +581,11 @@
constant: Module,
method: T.nilable(UnboundMethod),
visibility: RBI::Visibility
).void
end
- def compile_method(tree, symbol_name, constant, method, visibility = RBI::Visibility::Public)
+ def compile_method(tree, symbol_name, constant, method, visibility = RBI::Public.new)
return unless method
return unless method.owner == constant
return if symbol_ignored?(symbol_name) && !method_in_gem?(method)
signature = signature_of(method)
@@ -613,11 +671,11 @@
type = sanitize_signature_types(parameter_types[name.to_sym].to_s)
add_to_symbol_queue(type)
sig << RBI::SigParam.new(name, type)
end
- return_type = type_of(signature.return_type)
+ return_type = name_of_type(signature.return_type)
sig.return_type = sanitize_signature_types(return_type)
add_to_symbol_queue(sig.return_type)
parameter_types.values.join(", ").scan(TYPE_PARAMETER_MATCHER).flatten.uniq.each do |k, _|
sig.type_params << k
@@ -636,16 +694,26 @@
end
sig
end
+ sig { params(sig_string: String).returns(String) }
+ def sanitize_signature_types(sig_string)
+ sig_string
+ .gsub(".returns(<VOID>)", ".void")
+ .gsub("<VOID>", "void")
+ .gsub("<NOT-TYPED>", "T.untyped")
+ .gsub(".params()", "")
+ end
+
sig { params(symbol_name: String).returns(T::Boolean) }
def symbol_ignored?(symbol_name)
SymbolLoader.ignore_symbol?(symbol_name)
end
- SPECIAL_METHOD_NAMES = %w[! ~ +@ ** -@ * / % + - << >> & | ^ < <= => > >= == === != =~ !~ <=> [] []= `]
+ SPECIAL_METHOD_NAMES = ["!", "~", "+@", "**", "-@", "*", "/", "%", "+", "-", "<<", ">>", "&", "|", "^", "<",
+ "<=", "=>", ">", ">=", "==", "===", "!=", "=~", "!~", "<=>", "[]", "[]=", "`"]
sig { params(name: String).returns(T::Boolean) }
def valid_method_name?(name)
return true if SPECIAL_METHOD_NAMES.include?(name)
!!name.match(/^[[:word:]]+[?!=]?$/)
@@ -706,45 +774,11 @@
constant.instance_method(:initialize)
rescue
nil
end
- sig { params(constant: BasicObject).returns(Class).checked(:never) }
- def class_of(constant)
- Kernel.instance_method(:class).bind(constant).call
- end
-
- sig { params(constant: Module).returns(T::Array[Symbol]) }
- def constants_of(constant)
- Module.instance_method(:constants).bind(constant).call(false)
- end
-
- sig { params(constant: Module).returns(T.nilable(String)) }
- def raw_name_of(constant)
- Module.instance_method(:name).bind(constant).call
- end
-
- sig { params(constant: Module).returns(Class) }
- def singleton_class_of(constant)
- Object.instance_method(:singleton_class).bind(constant).call
- end
-
sig { params(constant: Module).returns(T::Array[Module]) }
- def ancestors_of(constant)
- Module.instance_method(:ancestors).bind(constant).call
- end
-
- sig { params(constant: Module).returns(T::Array[Module]) }
- def inherited_ancestors_of(constant)
- if Class === constant
- ancestors_of(superclass_of(constant) || Object)
- else
- Module.ancestors
- end
- end
-
- sig { params(constant: Module).returns(T::Array[Module]) }
def interesting_ancestors_of(constant)
inherited_ancestors_ids = Set.new(
inherited_ancestors_of(constant).map { |mod| object_id_of(mod) }
)
# TODO: There is actually a bug here where this will drop modules that
@@ -769,13 +803,13 @@
end
end
sig { params(constant: Module).returns(T.nilable(String)) }
def name_of(constant)
- name = name_of_proxy_target(constant)
+ name = name_of_proxy_target(constant, super(class_of(constant)))
return name if name
- name = raw_name_of(constant)
+ name = super(constant)
return if name.nil?
return unless are_equal?(constant, resolve_constant(name, inherit: true))
name = "Struct" if name =~ /^(::)?Struct::[^:]+$/
name
end
@@ -791,70 +825,21 @@
type_variable_names = type_variables.map { "T.untyped" }.join(", ")
"#{type_name}[#{type_variable_names}]"
end
- sig { params(constant: Module).returns(T.nilable(String)) }
- def name_of_proxy_target(constant)
- klass = class_of(constant)
- return unless raw_name_of(klass) == "ActiveSupport::Deprecation::DeprecatedConstantProxy"
+ sig { params(constant: Module, class_name: T.nilable(String)).returns(T.nilable(String)) }
+ def name_of_proxy_target(constant, class_name)
+ return unless class_name == "ActiveSupport::Deprecation::DeprecatedConstantProxy"
# We are dealing with a ActiveSupport::Deprecation::DeprecatedConstantProxy
# so try to get the name of the target class
begin
- target = Kernel.instance_method(:send).bind(constant).call(:target)
+ target = constant.__send__(:target)
rescue NoMethodError
return
end
- raw_name_of(target)
- end
-
- sig { params(constant: Module).returns(T.nilable(String)) }
- def qualified_name_of(constant)
- name = name_of(constant)
- return if name.nil?
-
- if name.start_with?("::")
- name
- else
- "::#{name}"
- end
- end
-
- sig { params(constant: Class).returns(T.nilable(Class)) }
- def superclass_of(constant)
- Class.instance_method(:superclass).bind(constant).call
- end
-
- sig { params(method: T.any(UnboundMethod, Method)).returns(T.untyped) }
- def signature_of(method)
- T::Private::Methods.signature_for_method(method)
- rescue LoadError, StandardError
- nil
- end
-
- sig { params(sig_string: String).returns(String) }
- def sanitize_signature_types(sig_string)
- sig_string
- .gsub(".returns(<VOID>)", ".void")
- .gsub("<VOID>", "void")
- .gsub("<NOT-TYPED>", "T.untyped")
- .gsub(".params()", "")
- end
-
- sig { params(constant: T::Types::Base).returns(String) }
- def type_of(constant)
- constant.to_s.gsub(/\bAttachedClass\b/, "T.attached_class")
- end
-
- sig { params(object: BasicObject).returns(Integer).checked(:never) }
- def object_id_of(object)
- Object.instance_method(:object_id).bind(object).call
- end
-
- sig { params(constant: Module, other: BasicObject).returns(T::Boolean).checked(:never) }
- def are_equal?(constant, other)
- BasicObject.instance_method(:equal?).bind(constant).call(other)
+ name_of(target)
end
end
end
end
end