From 6ae6fabf36c780449bfe78322ef4c139835ad2d3 Mon Sep 17 00:00:00 2001 From: Emily Giurleo Date: Fri, 15 Apr 2022 17:27:58 -0400 Subject: [PATCH 1/4] Compile RBIs for constants not owned by the current gem --- lib/tapioca/gem/events.rb | 14 +++ lib/tapioca/gem/listeners.rb | 1 + .../gem/listeners/foreign_constants.rb | 43 +++++++ lib/tapioca/gem/pipeline.rb | 27 +++- lib/tapioca/runtime/trackers/mixin.rb | 20 ++- spec/tapioca/cli/gem_spec.rb | 36 ++++++ spec/tapioca/gem/pipeline_spec.rb | 118 ++++++++++++++++++ 7 files changed, 248 insertions(+), 11 deletions(-) create mode 100644 lib/tapioca/gem/listeners/foreign_constants.rb diff --git a/lib/tapioca/gem/events.rb b/lib/tapioca/gem/events.rb index 89be971dc..21fa1723d 100644 --- a/lib/tapioca/gem/events.rb +++ b/lib/tapioca/gem/events.rb @@ -42,6 +42,20 @@ def initialize(symbol, constant) end end + class ForeignConstantFound < ConstantFound + extend T::Sig + + sig { override.returns(Module) } + def constant + T.cast(@constant, Module) + end + + sig { params(symbol: String, constant: Module).void } + def initialize(symbol, constant) + super + end + end + class NodeAdded < Event extend T::Helpers extend T::Sig diff --git a/lib/tapioca/gem/listeners.rb b/lib/tapioca/gem/listeners.rb index 257f6af52..225180873 100644 --- a/lib/tapioca/gem/listeners.rb +++ b/lib/tapioca/gem/listeners.rb @@ -13,4 +13,5 @@ require "tapioca/gem/listeners/sorbet_signatures" require "tapioca/gem/listeners/sorbet_type_variables" require "tapioca/gem/listeners/subconstants" +require "tapioca/gem/listeners/foreign_constants" require "tapioca/gem/listeners/yard_doc" diff --git a/lib/tapioca/gem/listeners/foreign_constants.rb b/lib/tapioca/gem/listeners/foreign_constants.rb new file mode 100644 index 000000000..67c94aa01 --- /dev/null +++ b/lib/tapioca/gem/listeners/foreign_constants.rb @@ -0,0 +1,43 @@ +# typed: strict +# frozen_string_literal: true + +module Tapioca + module Gem + module Listeners + class ForeignConstants < Base + extend T::Sig + + include Runtime::Reflection + + private + + sig { override.params(event: ScopeNodeAdded).void } + def on_scope(event) + mixin = event.constant + return if Class === mixin # Classes can't be mixed into other constants + + # There are cases where we want to process constants not declared by the current + # gem, i.e. "foreign constant". These are constants defined in another gem to which + # this gem is applying a mix-in. This pattern is especially common for gems that add + # behavior to Ruby standard library classes by mixing in modules to them. + # + # The way we identify these "foreign constants" is by asking the mixin tracker which + # constants have mixed in the current module that we are handling. We add all the + # constants that we discover to the pipeline to be processed. + Runtime::Trackers::Mixin.constants_with_mixin(mixin).each do |constant, locations| + location = locations.first + next if location.nil? || !@pipeline.gem.contains_path?(location) + + name = @pipeline.name_of(constant) + @pipeline.push_foreign_constant(name, constant) if name + end + end + + sig { params(constant: Module).returns(T.nilable(String)) } + def constant_name_from_singleton_class(constant) + constant.to_s.match("#")&.captures&.first + end + end + end + end +end diff --git a/lib/tapioca/gem/pipeline.rb b/lib/tapioca/gem/pipeline.rb index fb0c66ec4..48a6c6b30 100644 --- a/lib/tapioca/gem/pipeline.rb +++ b/lib/tapioca/gem/pipeline.rb @@ -41,6 +41,7 @@ def initialize(gem, include_doc: false) @node_listeners << Gem::Listeners::SorbetSignatures.new(self) @node_listeners << Gem::Listeners::Subconstants.new(self) @node_listeners << Gem::Listeners::YardDoc.new(self) if include_doc + @node_listeners << Gem::Listeners::ForeignConstants.new(self) @node_listeners << Gem::Listeners::RemoveEmptyPayloadScopes.new(self) end @@ -60,6 +61,11 @@ def push_constant(symbol, constant) @events << Gem::ConstantFound.new(symbol, constant) end + sig { params(symbol: String, constant: Module).void.checked(:never) } + def push_foreign_constant(symbol, constant) + @events << Gem::ForeignConstantFound.new(symbol, constant) + end + sig { params(symbol: String, constant: Module, node: RBI::Const).void.checked(:never) } def push_const(symbol, constant, node) @events << Gem::ConstNodeAdded.new(symbol, constant, node) @@ -152,11 +158,15 @@ def on_constant(event) return if alias_namespaced?(name) return if seen?(name) - constant = event.constant - return if T::Enum === constant # T::Enum instances are defined via `compile_enums` + return if T::Enum === event.constant # T::Enum instances are defined via `compile_enums` mark_seen(name) - compile_constant(name, constant) + + if event.is_a?(Gem::ForeignConstantFound) + compile_foreign_constant(name, event.constant) + else + compile_constant(name, event.constant) + end end sig { params(event: Gem::NodeAdded).void } @@ -166,6 +176,11 @@ def on_node(event) # Compile + sig { params(symbol: String, constant: Module).void } + def compile_foreign_constant(symbol, constant) + compile_module(symbol, constant, foreign_constant: true) + end + sig { params(symbol: String, constant: BasicObject).void.checked(:never) } def compile_constant(symbol, constant) case constant @@ -228,9 +243,9 @@ def compile_object(name, value) @root << node end - sig { params(name: String, constant: Module).void } - def compile_module(name, constant) - return unless defined_in_gem?(constant, strict: false) + sig { params(name: String, constant: Module, foreign_constant: T::Boolean).void } + def compile_module(name, constant, foreign_constant: false) + return unless defined_in_gem?(constant, strict: false) || foreign_constant return if Tapioca::TypeVariableModule === constant scope = diff --git a/lib/tapioca/runtime/trackers/mixin.rb b/lib/tapioca/runtime/trackers/mixin.rb index 6694b2de9..b14c8ee02 100644 --- a/lib/tapioca/runtime/trackers/mixin.rb +++ b/lib/tapioca/runtime/trackers/mixin.rb @@ -7,7 +7,8 @@ module Trackers module Mixin extend T::Sig - @mixin_map = {}.compare_by_identity + @constants_to_mixin_locations = {}.compare_by_identity + @mixins_to_constants = {}.compare_by_identity class Type < T::Enum enums do @@ -20,26 +21,35 @@ class Type < T::Enum sig do params( constant: Module, - mod: Module, + mixin: Module, mixin_type: Type, locations: T.nilable(T::Array[Thread::Backtrace::Location]) ).void end - def self.register(constant, mod, mixin_type, locations) + def self.register(constant, mixin, mixin_type, locations) locations ||= [] locations.map!(&:absolute_path).uniq! + locs = mixin_locations_for(constant) - locs.fetch(mixin_type).store(mod, T.cast(locations, T::Array[String])) + locs.fetch(mixin_type).store(mixin, T.cast(locations, T::Array[String])) + + constants = constants_with_mixin(mixin) + constants[constant] = T.cast(locations, T::Array[String]) end sig { params(constant: Module).returns(T::Hash[Type, T::Hash[Module, T::Array[String]]]) } def self.mixin_locations_for(constant) - @mixin_map[constant] ||= { + @constants_to_mixin_locations[constant] ||= { Type::Prepend => {}.compare_by_identity, Type::Include => {}.compare_by_identity, Type::Extend => {}.compare_by_identity, } end + + sig { params(mixin: Module).returns(T::Hash[Module, T::Array[String]]) } + def self.constants_with_mixin(mixin) + @mixins_to_constants[mixin] ||= {}.compare_by_identity + end end end end diff --git a/spec/tapioca/cli/gem_spec.rb b/spec/tapioca/cli/gem_spec.rb index 339e8391a..4216b4a0f 100644 --- a/spec/tapioca/cli/gem_spec.rb +++ b/spec/tapioca/cli/gem_spec.rb @@ -1073,6 +1073,42 @@ def foo(a, b, c, d, e, f, g, h); end assert_empty_stderr(result) assert_success_status(result) end + + it "must generate RBIs for constants defined in a different gem but with mixins in this gem" do + foo = mock_gem("foo", "0.0.1") do + write("lib/foo.rb", <<~RBI) + class Foo; end + RBI + end + + bar = mock_gem("bar", "0.0.2") do + write("lib/bar.rb", <<~RBI) + module Bar; end + + Foo.prepend(Bar) + RBI + end + + @project.require_mock_gem(foo) + @project.require_mock_gem(bar) + @project.bundle_install + + @project.tapioca("gem bar") + + assert_project_file_equal("sorbet/rbi/gems/bar@0.0.2.rbi", <<~RBI) + # typed: true + + # DO NOT EDIT MANUALLY + # This is an autogenerated file for types exported from the `bar` gem. + # Please instead update this file by running `bin/tapioca gem bar`. + + module Bar; end + + class Foo + include ::Bar + end + RBI + end end describe "sync" do diff --git a/spec/tapioca/gem/pipeline_spec.rb b/spec/tapioca/gem/pipeline_spec.rb index 1467feebf..d1d1bc10c 100644 --- a/spec/tapioca/gem/pipeline_spec.rb +++ b/spec/tapioca/gem/pipeline_spec.rb @@ -487,6 +487,124 @@ def to_foo(base = T.unsafe(nil)); end assert_equal(output, compile) end + it "compiles extensions to core types without adding methods" do + add_ruby_file("foo.rb", <<~RUBY) + class Foo + def to_s + "Foo" + end + + def bar + "bar" + end + + module Bar; end + end + RUBY + + add_ruby_file("ext.rb", <<~RUBY) + class String + include Foo::Bar + end + + class Hash + extend Foo::Bar + end + + class Array + prepend Foo::Bar + end + RUBY + + output = template(<<~RBI) + class Array + include ::Foo::Bar + include ::Enumerable + include ::JSON::Ext::Generator::GeneratorMethods::Array + end + + class Foo + def bar; end + def to_s; end + end + + module Foo::Bar; end + + class Hash + include ::Enumerable + include ::JSON::Ext::Generator::GeneratorMethods::Hash + extend ::Foo::Bar + end + + class String + include ::Comparable + include ::JSON::Ext::Generator::GeneratorMethods::String + include ::Foo::Bar + extend ::JSON::Ext::Generator::GeneratorMethods::String::Extend + end + + String::BLANK_RE = T.let(T.unsafe(nil), Regexp) + String::ENCODED_BLANKS = T.let(T.unsafe(nil), Concurrent::Map) + RBI + + assert_equal(output, compile) + end + + it "compiles extensions to core types via #extend, #include, and #prepend methods" do + add_ruby_file("foo.rb", <<~RUBY) + class Foo + def to_s + "Foo" + end + + def bar + "bar" + end + + module Bar; end + end + RUBY + + add_ruby_file("ext.rb", <<~RUBY) + String.include(Foo::Bar) + Hash.extend(Foo::Bar) + Array.prepend(Foo::Bar) + RUBY + + output = template(<<~RBI) + class Array + include ::Foo::Bar + include ::Enumerable + include ::JSON::Ext::Generator::GeneratorMethods::Array + end + + class Foo + def bar; end + def to_s; end + end + + module Foo::Bar; end + + class Hash + include ::Enumerable + include ::JSON::Ext::Generator::GeneratorMethods::Hash + extend ::Foo::Bar + end + + class String + include ::Comparable + include ::JSON::Ext::Generator::GeneratorMethods::String + include ::Foo::Bar + extend ::JSON::Ext::Generator::GeneratorMethods::String::Extend + end + + String::BLANK_RE = T.let(T.unsafe(nil), Regexp) + String::ENCODED_BLANKS = T.let(T.unsafe(nil), Concurrent::Map) + RBI + + assert_equal(output, compile) + end + it "compiles without annotations" do add_ruby_file("bar.rb", <<~RUBY) class Bar From 650b7e29354e00a710b77ae332e05276a6ccdb2f Mon Sep 17 00:00:00 2001 From: Emily Giurleo Date: Mon, 30 May 2022 11:33:40 -0400 Subject: [PATCH 2/4] Add special handling for dynamic mixins on singleton classes --- .../gem/listeners/foreign_constants.rb | 12 ++++++++++ spec/tapioca/gem/pipeline_spec.rb | 23 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/lib/tapioca/gem/listeners/foreign_constants.rb b/lib/tapioca/gem/listeners/foreign_constants.rb index 67c94aa01..e3e2b848e 100644 --- a/lib/tapioca/gem/listeners/foreign_constants.rb +++ b/lib/tapioca/gem/listeners/foreign_constants.rb @@ -29,6 +29,18 @@ def on_scope(event) next if location.nil? || !@pipeline.gem.contains_path?(location) name = @pipeline.name_of(constant) + + # Calling Tapioca::Gem::Pipeline#name_of on a singleton class returns `nil`. + # To handle this case, use string parsing to get the name of the singleton class's + # base constant. Then, generate RBIs as if the base constant is extending the mixin, + # which is functionally equivalent to including or prepending to the singleton class. + if !name && constant.singleton_class? + name = constant_name_from_singleton_class(constant) + next unless name + + constant = T.cast(constantize(name), Module) + end + @pipeline.push_foreign_constant(name, constant) if name end end diff --git a/spec/tapioca/gem/pipeline_spec.rb b/spec/tapioca/gem/pipeline_spec.rb index d1d1bc10c..43eddac36 100644 --- a/spec/tapioca/gem/pipeline_spec.rb +++ b/spec/tapioca/gem/pipeline_spec.rb @@ -605,6 +605,29 @@ class String assert_equal(output, compile) end + it "compiles dynamic mixin to singleton class" do + add_ruby_file("foo.rb", <<~RUBY) + class Foo + module Bar; end + end + RUBY + + add_ruby_file("ext.rb", <<~RUBY) + Class.singleton_class.prepend(Foo::Bar) + RUBY + + output = template(<<~RBI) + class Class < ::Module + extend ::Foo::Bar + end + + class Foo; end + module Foo::Bar; end + RBI + + assert_equal(output, compile) + end + it "compiles without annotations" do add_ruby_file("bar.rb", <<~RUBY) class Bar From 3a25760f6460b49800eccfa9ed51c618ca73dab4 Mon Sep 17 00:00:00 2001 From: Emily Giurleo Date: Thu, 12 May 2022 12:34:13 -0400 Subject: [PATCH 3/4] Do not compile constants from mixins that occur in other gems --- .../gem/listeners/foreign_constants.rb | 30 ++++++++++++++++-- spec/tapioca/cli/gem_spec.rb | 31 +++++++++++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/lib/tapioca/gem/listeners/foreign_constants.rb b/lib/tapioca/gem/listeners/foreign_constants.rb index e3e2b848e..c0d659449 100644 --- a/lib/tapioca/gem/listeners/foreign_constants.rb +++ b/lib/tapioca/gem/listeners/foreign_constants.rb @@ -25,8 +25,8 @@ def on_scope(event) # constants have mixed in the current module that we are handling. We add all the # constants that we discover to the pipeline to be processed. Runtime::Trackers::Mixin.constants_with_mixin(mixin).each do |constant, locations| - location = locations.first - next if location.nil? || !@pipeline.gem.contains_path?(location) + next if defined_by_application?(constant) + next unless mixed_in_by_gem?(locations) name = @pipeline.name_of(constant) @@ -45,6 +45,32 @@ def on_scope(event) end end + sig do + params( + locations: T::Array[String] + ).returns(T::Boolean) + end + def mixed_in_by_gem?(locations) + locations.compact.any? { |location| @pipeline.gem.contains_path?(location) } + end + + sig do + params( + constant: Module + ).returns(T::Boolean) + end + def defined_by_application?(constant) + application_dir = (Bundler.default_gemfile / "..").to_s + Tapioca::Runtime::Trackers::ConstantDefinition.files_for(constant).any? do |location| + location.start_with?(application_dir) && !in_bundle_path?(location) + end + end + + sig { params(path: String).returns(T::Boolean) } + def in_bundle_path?(path) + path.start_with?(Bundler.bundle_path.to_s, Bundler.app_cache.to_s) + end + sig { params(constant: Module).returns(T.nilable(String)) } def constant_name_from_singleton_class(constant) constant.to_s.match("#")&.captures&.first diff --git a/spec/tapioca/cli/gem_spec.rb b/spec/tapioca/cli/gem_spec.rb index 4216b4a0f..7c9269a4c 100644 --- a/spec/tapioca/cli/gem_spec.rb +++ b/spec/tapioca/cli/gem_spec.rb @@ -1109,6 +1109,37 @@ class Foo end RBI end + + it "must not generate RBIs for constants that have dynamic mixins performed in other gems" do + bar = mock_gem("bar", "0.0.2") do + write("lib/bar.rb", <<~RBI) + module Bar; end + RBI + end + + foo = mock_gem("foo", "0.0.1") do + write("lib/foo.rb", <<~RBI) + class Foo; end + String.prepend(Bar) + RBI + end + + @project.require_mock_gem(bar) + @project.require_mock_gem(foo) + @project.bundle_install + + @project.tapioca("gem bar") + + assert_project_file_equal("sorbet/rbi/gems/bar@0.0.2.rbi", <<~RBI) + # typed: true + + # DO NOT EDIT MANUALLY + # This is an autogenerated file for types exported from the `bar` gem. + # Please instead update this file by running `bin/tapioca gem bar`. + + module Bar; end + RBI + end end describe "sync" do From fb9ce40244cd28fc73bef3210e486a355f9b89f5 Mon Sep 17 00:00:00 2001 From: Emily Giurleo Date: Mon, 30 May 2022 11:26:43 -0400 Subject: [PATCH 4/4] Only generate mixin RBIs for foreign constants --- lib/tapioca/gem/events.rb | 2 + lib/tapioca/gem/listeners/base.rb | 11 +++++ lib/tapioca/gem/listeners/dynamic_mixins.rb | 5 ++ .../gem/listeners/foreign_constants.rb | 5 ++ lib/tapioca/gem/listeners/methods.rb | 5 ++ .../listeners/remove_empty_payload_scopes.rb | 5 ++ lib/tapioca/gem/listeners/sorbet_enums.rb | 5 ++ lib/tapioca/gem/listeners/sorbet_helpers.rb | 5 ++ lib/tapioca/gem/listeners/sorbet_props.rb | 5 ++ .../listeners/sorbet_required_ancestors.rb | 5 ++ .../gem/listeners/sorbet_signatures.rb | 5 ++ .../gem/listeners/sorbet_type_variables.rb | 5 ++ lib/tapioca/gem/listeners/subconstants.rb | 5 ++ lib/tapioca/gem/listeners/yard_doc.rb | 5 ++ lib/tapioca/gem/pipeline.rb | 18 ++++++- spec/tapioca/cli/gem_spec.rb | 47 ++++++++++++++++++- spec/tapioca/gem/pipeline_spec.rb | 6 --- 17 files changed, 135 insertions(+), 9 deletions(-) diff --git a/lib/tapioca/gem/events.rb b/lib/tapioca/gem/events.rb index 21fa1723d..91442c948 100644 --- a/lib/tapioca/gem/events.rb +++ b/lib/tapioca/gem/events.rb @@ -102,6 +102,8 @@ def initialize(symbol, constant, node) end end + class ForeignScopeNodeAdded < ScopeNodeAdded; end + class MethodNodeAdded < NodeAdded extend T::Sig diff --git a/lib/tapioca/gem/listeners/base.rb b/lib/tapioca/gem/listeners/base.rb index 71b7af109..b758b4e9b 100644 --- a/lib/tapioca/gem/listeners/base.rb +++ b/lib/tapioca/gem/listeners/base.rb @@ -17,6 +17,8 @@ def initialize(pipeline) sig { params(event: NodeAdded).void } def dispatch(event) + return if ignore?(event) + case event when ConstNodeAdded on_const(event) @@ -42,6 +44,15 @@ def on_scope(event) sig { params(event: MethodNodeAdded).void } def on_method(event) end + + sig { params(event: NodeAdded).returns(T::Boolean) } + def ignore?(event) + # Some listeners do not have to take any action on certain events. For example, + # almost every listener should skip ForeignScopeNodeAdded events in order not to generate + # unnecessary RBIs for foreign constants. This method should be overridden by listener + # subclasses to skip any events that aren't relevant to them. + false + end end end end diff --git a/lib/tapioca/gem/listeners/dynamic_mixins.rb b/lib/tapioca/gem/listeners/dynamic_mixins.rb index 8a9fd26a5..2fecdb849 100644 --- a/lib/tapioca/gem/listeners/dynamic_mixins.rb +++ b/lib/tapioca/gem/listeners/dynamic_mixins.rb @@ -26,6 +26,11 @@ def on_scope(event) @pipeline.push_symbol(name) if name end end + + sig { override.params(event: NodeAdded).returns(T::Boolean) } + def ignore?(event) + event.is_a?(Tapioca::Gem::ForeignScopeNodeAdded) + end end end end diff --git a/lib/tapioca/gem/listeners/foreign_constants.rb b/lib/tapioca/gem/listeners/foreign_constants.rb index c0d659449..4fca2f8d2 100644 --- a/lib/tapioca/gem/listeners/foreign_constants.rb +++ b/lib/tapioca/gem/listeners/foreign_constants.rb @@ -75,6 +75,11 @@ def in_bundle_path?(path) def constant_name_from_singleton_class(constant) constant.to_s.match("#")&.captures&.first end + + sig { override.params(event: NodeAdded).returns(T::Boolean) } + def ignore?(event) + event.is_a?(Tapioca::Gem::ForeignScopeNodeAdded) + end end end end diff --git a/lib/tapioca/gem/listeners/methods.rb b/lib/tapioca/gem/listeners/methods.rb index bfc0a3e82..cc8cebc9d 100644 --- a/lib/tapioca/gem/listeners/methods.rb +++ b/lib/tapioca/gem/listeners/methods.rb @@ -202,6 +202,11 @@ def initialize_method_for(constant) rescue nil end + + sig { override.params(event: NodeAdded).returns(T::Boolean) } + def ignore?(event) + event.is_a?(Tapioca::Gem::ForeignScopeNodeAdded) + end end end end diff --git a/lib/tapioca/gem/listeners/remove_empty_payload_scopes.rb b/lib/tapioca/gem/listeners/remove_empty_payload_scopes.rb index ec601ffa6..bc15a0819 100644 --- a/lib/tapioca/gem/listeners/remove_empty_payload_scopes.rb +++ b/lib/tapioca/gem/listeners/remove_empty_payload_scopes.rb @@ -15,6 +15,11 @@ class RemoveEmptyPayloadScopes < Base def on_scope(event) event.node.detach if @pipeline.symbol_in_payload?(event.symbol) && event.node.empty? end + + sig { override.params(event: NodeAdded).returns(T::Boolean) } + def ignore?(event) + event.is_a?(Tapioca::Gem::ForeignScopeNodeAdded) + end end end end diff --git a/lib/tapioca/gem/listeners/sorbet_enums.rb b/lib/tapioca/gem/listeners/sorbet_enums.rb index 06c540a5b..bbeae13cd 100644 --- a/lib/tapioca/gem/listeners/sorbet_enums.rb +++ b/lib/tapioca/gem/listeners/sorbet_enums.rb @@ -20,6 +20,11 @@ def on_scope(event) event.node << RBI::TEnumBlock.new(enums) end + + sig { override.params(event: NodeAdded).returns(T::Boolean) } + def ignore?(event) + event.is_a?(Tapioca::Gem::ForeignScopeNodeAdded) + end end end end diff --git a/lib/tapioca/gem/listeners/sorbet_helpers.rb b/lib/tapioca/gem/listeners/sorbet_helpers.rb index 4c88db12e..cdaaef99c 100644 --- a/lib/tapioca/gem/listeners/sorbet_helpers.rb +++ b/lib/tapioca/gem/listeners/sorbet_helpers.rb @@ -23,6 +23,11 @@ def on_scope(event) node << RBI::Helper.new("final") if T::Private::Final.final_module?(constant) node << RBI::Helper.new("sealed") if T::Private::Sealed.sealed_module?(constant) end + + sig { override.params(event: NodeAdded).returns(T::Boolean) } + def ignore?(event) + event.is_a?(Tapioca::Gem::ForeignScopeNodeAdded) + end end end end diff --git a/lib/tapioca/gem/listeners/sorbet_props.rb b/lib/tapioca/gem/listeners/sorbet_props.rb index 1886dd045..07a26734e 100644 --- a/lib/tapioca/gem/listeners/sorbet_props.rb +++ b/lib/tapioca/gem/listeners/sorbet_props.rb @@ -27,6 +27,11 @@ def on_scope(event) end end end + + sig { override.params(event: NodeAdded).returns(T::Boolean) } + def ignore?(event) + event.is_a?(Tapioca::Gem::ForeignScopeNodeAdded) + end end end end diff --git a/lib/tapioca/gem/listeners/sorbet_required_ancestors.rb b/lib/tapioca/gem/listeners/sorbet_required_ancestors.rb index 88a831efc..81abda12c 100644 --- a/lib/tapioca/gem/listeners/sorbet_required_ancestors.rb +++ b/lib/tapioca/gem/listeners/sorbet_required_ancestors.rb @@ -18,6 +18,11 @@ def on_scope(event) event.node << RBI::RequiresAncestor.new(ancestor.to_s) end end + + sig { override.params(event: NodeAdded).returns(T::Boolean) } + def ignore?(event) + event.is_a?(Tapioca::Gem::ForeignScopeNodeAdded) + end end end end diff --git a/lib/tapioca/gem/listeners/sorbet_signatures.rb b/lib/tapioca/gem/listeners/sorbet_signatures.rb index 9eed97db3..3c262b4f8 100644 --- a/lib/tapioca/gem/listeners/sorbet_signatures.rb +++ b/lib/tapioca/gem/listeners/sorbet_signatures.rb @@ -73,6 +73,11 @@ def signature_final?(signature) final_methods.include?(signature.method_name) end + + sig { override.params(event: NodeAdded).returns(T::Boolean) } + def ignore?(event) + event.is_a?(Tapioca::Gem::ForeignScopeNodeAdded) + end end end end diff --git a/lib/tapioca/gem/listeners/sorbet_type_variables.rb b/lib/tapioca/gem/listeners/sorbet_type_variables.rb index 93e5f8d40..65fc04ef0 100644 --- a/lib/tapioca/gem/listeners/sorbet_type_variables.rb +++ b/lib/tapioca/gem/listeners/sorbet_type_variables.rb @@ -45,6 +45,11 @@ def compile_type_variable_declarations(tree, constant) tree << RBI::Extend.new("T::Generic") end + + sig { override.params(event: NodeAdded).returns(T::Boolean) } + def ignore?(event) + event.is_a?(Tapioca::Gem::ForeignScopeNodeAdded) + end end end end diff --git a/lib/tapioca/gem/listeners/subconstants.rb b/lib/tapioca/gem/listeners/subconstants.rb index 868e99158..a7b23d269 100644 --- a/lib/tapioca/gem/listeners/subconstants.rb +++ b/lib/tapioca/gem/listeners/subconstants.rb @@ -31,6 +31,11 @@ def on_scope(event) @pipeline.push_constant(name, subconstant) end end + + sig { override.params(event: NodeAdded).returns(T::Boolean) } + def ignore?(event) + event.is_a?(Tapioca::Gem::ForeignScopeNodeAdded) + end end end end diff --git a/lib/tapioca/gem/listeners/yard_doc.rb b/lib/tapioca/gem/listeners/yard_doc.rb index eeb574390..5c163b3d3 100644 --- a/lib/tapioca/gem/listeners/yard_doc.rb +++ b/lib/tapioca/gem/listeners/yard_doc.rb @@ -90,6 +90,11 @@ def documentation_comments(name, sigs: []) comments end + + sig { override.params(event: NodeAdded).returns(T::Boolean) } + def ignore?(event) + event.is_a?(Tapioca::Gem::ForeignScopeNodeAdded) + end end end end diff --git a/lib/tapioca/gem/pipeline.rb b/lib/tapioca/gem/pipeline.rb index 48a6c6b30..f7fb64f06 100644 --- a/lib/tapioca/gem/pipeline.rb +++ b/lib/tapioca/gem/pipeline.rb @@ -71,11 +71,20 @@ def push_const(symbol, constant, node) @events << Gem::ConstNodeAdded.new(symbol, constant, node) end - sig { params(symbol: String, constant: Module, node: RBI::Scope).void.checked(:never) } + sig do + params(symbol: String, constant: Module, node: RBI::Scope).void.checked(:never) + end def push_scope(symbol, constant, node) @events << Gem::ScopeNodeAdded.new(symbol, constant, node) end + sig do + params(symbol: String, constant: Module, node: RBI::Scope).void.checked(:never) + end + def push_foreign_scope(symbol, constant, node) + @events << Gem::ForeignScopeNodeAdded.new(symbol, constant, node) + end + sig do params( symbol: String, @@ -256,7 +265,12 @@ def compile_module(name, constant, foreign_constant: false) RBI::Module.new(name) end - push_scope(name, constant, scope) + if foreign_constant + push_foreign_scope(name, constant, scope) + else + push_scope(name, constant, scope) + end + @root << scope end diff --git a/spec/tapioca/cli/gem_spec.rb b/spec/tapioca/cli/gem_spec.rb index 7c9269a4c..fd8f2b2b0 100644 --- a/spec/tapioca/cli/gem_spec.rb +++ b/spec/tapioca/cli/gem_spec.rb @@ -1074,10 +1074,55 @@ def foo(a, b, c, d, e, f, g, h); end assert_success_status(result) end + it "must do mixin attribution properly" do + # This is pattern is taken from the private `typed_parameters` gem. + typed_parameters = mock_gem("typed_parameters", "0.3.0") do + write("lib/typed_parameters.rb", <<~RUBY) + require "action_controller" + module TypedParameters + end + # This dynamic mixin should be generated in the gem RBI + ActionController::Parameters.include(TypedParameters) + RUBY + end + + @project.require_real_gem("actionpack", "6.1.4.4") + @project.require_mock_gem(typed_parameters) + + @project.bundle_install + + response = @project.tapioca("gem actionpack typed_parameters") + + assert_includes(response.out, "Compiled actionpack") + assert_includes(response.out, "Compiled typed_parameters") + + # TODO: Uncomment when addressing part 2 of tapioca#890 + # actionpack_rbi = @project.read("sorbet/rbi/gems/actionpack@6.1.4.4.rbi") + # actionpack RBI should have nothing in it about `TypedParameters` + # refute_includes(actionpack_rbi, "TypedParameters") + + assert_project_file_equal("sorbet/rbi/gems/typed_parameters@0.3.0.rbi", <<~RBI) + # typed: true + + # DO NOT EDIT MANUALLY + # This is an autogenerated file for types exported from the `typed_parameters` gem. + # Please instead update this file by running `bin/tapioca gem typed_parameters`. + + class ActionController::Parameters + include ::TypedParameters + end + + module TypedParameters; end + RBI + end + it "must generate RBIs for constants defined in a different gem but with mixins in this gem" do foo = mock_gem("foo", "0.0.1") do write("lib/foo.rb", <<~RBI) - class Foo; end + class Foo + def baz; end + def buzz; end + end RBI end diff --git a/spec/tapioca/gem/pipeline_spec.rb b/spec/tapioca/gem/pipeline_spec.rb index 43eddac36..5175284a1 100644 --- a/spec/tapioca/gem/pipeline_spec.rb +++ b/spec/tapioca/gem/pipeline_spec.rb @@ -542,9 +542,6 @@ class String include ::Foo::Bar extend ::JSON::Ext::Generator::GeneratorMethods::String::Extend end - - String::BLANK_RE = T.let(T.unsafe(nil), Regexp) - String::ENCODED_BLANKS = T.let(T.unsafe(nil), Concurrent::Map) RBI assert_equal(output, compile) @@ -597,9 +594,6 @@ class String include ::Foo::Bar extend ::JSON::Ext::Generator::GeneratorMethods::String::Extend end - - String::BLANK_RE = T.let(T.unsafe(nil), Regexp) - String::ENCODED_BLANKS = T.let(T.unsafe(nil), Concurrent::Map) RBI assert_equal(output, compile)