diff --git a/lib/tapioca/dsl/compilers/active_record_fixtures.rb b/lib/tapioca/dsl/compilers/active_record_fixtures.rb index 3fd8e55b1..075702094 100644 --- a/lib/tapioca/dsl/compilers/active_record_fixtures.rb +++ b/lib/tapioca/dsl/compilers/active_record_fixtures.rb @@ -1,7 +1,10 @@ # typed: strict # frozen_string_literal: true -return unless defined?(Rails) && defined?(ActiveSupport::TestCase) && defined?(ActiveRecord::TestFixtures) +return unless defined?(Rails) && + defined?(ActiveSupport::TestCase) && + defined?(ActiveRecord::TestFixtures) && + defined?(ActiveRecord::FixtureSet) module Tapioca module Dsl @@ -127,16 +130,44 @@ def create_fixture_method(mod, name) sig { params(fixture_name: String).returns(String) } def return_type_for_fixture(fixture_name) - model_name_from_fixture_sets = T.unsafe(fixture_loader).fixture_sets[fixture_name] + model_name_from_fixture_files = fixture_file_class_mapping[fixture_name] + return model_name_from_fixture_files if model_name_from_fixture_files + model_name_from_fixture_sets = T.unsafe(fixture_loader).fixture_sets[fixture_name] if model_name_from_fixture_sets - model_name = T.unsafe(ActiveRecord::FixtureSet).default_fixture_model_name(model_name_from_fixture_sets) - + model_name = ActiveRecord::FixtureSet.default_fixture_model_name(model_name_from_fixture_sets) return model_name if Object.const_defined?(model_name) end "T.untyped" end + + sig { returns(T::Hash[String, String]) } + def fixture_file_class_mapping + @fixture_file_class_mapping ||= T.let( + begin + fixture_paths = if T.unsafe(fixture_loader).respond_to?(:fixture_paths) + T.unsafe(fixture_loader).fixture_paths + else + T.unsafe(fixture_loader).fixture_path + end + + fixture_paths.each_with_object({}) do |path, mapping| + Dir["#{path}{.yml,/{**,*}/*.yml}"].select do |file| + next unless ::File.file?(file) + + ActiveRecord::FixtureSet::File.open(file) do |fh| + next unless fh.model_class + + fixuture_name = file.delete_prefix(path.to_s).delete_prefix("/").delete_suffix(".yml") + mapping[fixuture_name] = fh.model_class + end + end + end + end, + T.nilable(T::Hash[String, String]), + ) + end end end end diff --git a/sorbet/rbi/shims/active_record_fixture_set.rbi b/sorbet/rbi/shims/active_record_fixture_set.rbi new file mode 100644 index 000000000..02cafff60 --- /dev/null +++ b/sorbet/rbi/shims/active_record_fixture_set.rbi @@ -0,0 +1,16 @@ +# typed: strict + +# ActiveRecord::TestFixtures can't be loaded outside of a Rails application + +class ActiveRecord::FixtureSet + sig { params(name: String).returns(String) } + def self.default_fixture_model_name(name); end +end + +class ActiveRecord::FixtureSet::File + sig { returns(T.nilable(String)) } + def modle_class; end + + sig params(filename: String, blk: T.proc.params(arg0: ActiveRecord::FixtureSet::File).void) + def self.open(filename, &block); end +end diff --git a/spec/tapioca/dsl/compilers/active_record_fixtures_spec.rb b/spec/tapioca/dsl/compilers/active_record_fixtures_spec.rb index 9c4014f59..9a486e1c8 100644 --- a/spec/tapioca/dsl/compilers/active_record_fixtures_spec.rb +++ b/spec/tapioca/dsl/compilers/active_record_fixtures_spec.rb @@ -124,6 +124,35 @@ def users(fixture_name, *other_fixtures); end assert_equal(expected, rbi_for("ActiveSupport::TestCase")) end + it "generates methods for fixtures with explicit class name" do + add_content_file("test/fixtures/posts_with_other_names.yml", <<~YAML) + _fixture: + model_class: Post + super_post: + title: An incredible Ruby post + author: Johnny Developer + created_at: 2021-09-08 11:00:00 + updated_at: 2021-09-08 11:00:00 + YAML + + add_ruby_file("test_models.rb", <<~RUBY) + class Post < ActiveRecord::Base + end + RUBY + + expected = <<~RBI + # typed: strong + + class ActiveSupport::TestCase + sig { params(fixture_name: T.any(String, Symbol), other_fixtures: NilClass).returns(Post) } + sig { params(fixture_name: T.any(String, Symbol), other_fixtures: T.any(String, Symbol)).returns(T::Array[Post]) } + def posts_with_other_names(fixture_name, *other_fixtures); end + end + RBI + + assert_equal(expected, rbi_for("ActiveSupport::TestCase")) + end + it "generates methods for fixtures with a fallback to T.untyped if no matching model exists" do add_content_file("test/fixtures/posts.yml", <<~YAML) super_post: