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