diff --git a/lib/tapioca/helpers/test/dsl_compiler.rb b/lib/tapioca/helpers/test/dsl_compiler.rb new file mode 100644 index 000000000..490dd4242 --- /dev/null +++ b/lib/tapioca/helpers/test/dsl_compiler.rb @@ -0,0 +1,122 @@ +# typed: strict +# frozen_string_literal: true + +require "tapioca/helpers/test/content" +require "tapioca/helpers/test/isolation" +require "tapioca/helpers/test/template" + +module Tapioca + module Helpers + module Test + module DslCompiler + extend T::Sig + extend T::Helpers + + include Isolation + include Content + include Template + + requires_ancestor { Kernel } + + sig { params(compiler_class: T.class_of(Tapioca::Compilers::Dsl::Base)).void } + def use_dsl_compiler(compiler_class) + @context = T.let(CompilerContext.new(compiler_class), T.nilable(CompilerContext)) + end + + sig { params(compiler_classes: T.class_of(Tapioca::Compilers::Dsl::Base)).void } + def activate_other_dsl_compilers(*compiler_classes) + context.activate_other_dsl_compilers(compiler_classes) + end + + sig { params(constant_name: T.any(Symbol, String)).returns(String) } + def rbi_for(constant_name) + context.rbi_for(constant_name) + end + + sig { returns(T::Array[String]) } + def gathered_constants + context.gathered_constants + end + + sig { returns(T::Array[String]) } + def generated_errors + context.errors + end + + sig { returns(CompilerContext) } + def context + raise "Please call `use_dsl_compiler` before" unless @context + @context + end + + class CompilerContext + extend T::Sig + + sig { returns(T.class_of(Tapioca::Compilers::Dsl::Base)) } + attr_reader :compiler_class + + sig { returns(T::Array[T.class_of(Tapioca::Compilers::Dsl::Base)]) } + attr_reader :other_compiler_classes + + sig { params(compiler_class: T.class_of(Tapioca::Compilers::Dsl::Base)).void } + def initialize(compiler_class) + @compiler_class = compiler_class + @other_compiler_classes = T.let([], T::Array[T.class_of(Tapioca::Compilers::Dsl::Base)]) + @compiler = T.let(nil, T.nilable(Tapioca::Compilers::Dsl::Base)) + @pipeline = T.let(nil, T.nilable(Tapioca::Compilers::DslCompiler)) + end + + sig { params(compiler_classes: T::Array[T.class_of(Tapioca::Compilers::Dsl::Base)]).void } + def activate_other_dsl_compilers(compiler_classes) + @other_compiler_classes = compiler_classes + end + + sig { returns(T::Array[T.class_of(Tapioca::Compilers::Dsl::Base)]) } + def activated_compiler_classes + [compiler_class, *other_compiler_classes] + end + + sig { returns(T::Array[String]) } + def gathered_constants + compiler.processable_constants.map(&:name).compact.sort + end + + sig { params(constant_name: T.any(Symbol, String)).returns(String) } + def rbi_for(constant_name) + # Make sure this is a constant that we can handle. + unless gathered_constants.include?(constant_name.to_s) + raise "`#{constant_name}` is not processable by the `#{compiler_class}` generator." + end + + file = RBI::File.new(strictness: "strong") + constant = Object.const_get(constant_name) + + compiler.decorate(file.root, constant) + + file.transformed_string + end + + sig { returns(T::Array[String]) } + def errors + compiler.errors + end + + private + + sig { returns(Tapioca::Compilers::Dsl::Base) } + def compiler + @compiler ||= T.must(pipeline.generators.grep(compiler_class).first) + end + + sig { returns(Tapioca::Compilers::DslCompiler) } + def pipeline + @pipeline ||= Tapioca::Compilers::DslCompiler.new( + requested_constants: [], + requested_generators: activated_compiler_classes + ) + end + end + end + end + end +end diff --git a/lib/tapioca/helpers/test/template.rb b/lib/tapioca/helpers/test/template.rb index 45fbac0cc..8fca4019f 100644 --- a/lib/tapioca/helpers/test/template.rb +++ b/lib/tapioca/helpers/test/template.rb @@ -7,8 +7,11 @@ module Tapioca module Helpers module Test module Template - include Kernel extend T::Sig + extend T::Helpers + + requires_ancestor { Kernel } + ERB_SUPPORTS_KVARGS = T.let( ::ERB.instance_method(:initialize).parameters.assoc(:key), T.nilable([Symbol, Symbol]) ) @@ -28,6 +31,14 @@ def template(src) erb.result(binding) end + + sig { params(str: String, indent: Integer).returns(String) } + def indented(str, indent) + str.lines.map! do |line| + next line if line.chomp.empty? + (" " * indent) + line + end.join + end end end end diff --git a/spec/dsl_spec_helper.rb b/spec/dsl_spec_helper.rb index 0f3755c20..88aee26ae 100644 --- a/spec/dsl_spec_helper.rb +++ b/spec/dsl_spec_helper.rb @@ -3,72 +3,53 @@ require "sorbet-runtime" require "minitest/spec" -require "tapioca/helpers/test/content" -require "tapioca/helpers/test/template" -require "tapioca/helpers/test/isolation" +require "tapioca/helpers/test/dsl_compiler" class DslSpec < Minitest::Spec extend T::Sig - include Kernel - include Tapioca::Helpers::Test::Content - include Tapioca::Helpers::Test::Template - include Tapioca::Helpers::Test::Isolation + include Tapioca::Helpers::Test::DslCompiler - sig { void } - def after_setup + before do # Require the file that the target class should be loaded from - require(T.unsafe(self).target_class_file) - end - - sig { void } - def teardown - super - T.unsafe(self).subject.errors.clear + require(self.class.target_class_file) + use_dsl_compiler(self.class.target_class) end - subject do - T.bind(self, DslSpec) - # Get the class under test and initialize a new instance of it as the "subject" - generator_for_names(target_class_name) - end - - sig { params(names: String).returns(Tapioca::Compilers::Dsl::Base) } - def generator_for_names(*names) - raise "name is required" if names.empty? - - classes = names.map { |class_name| Object.const_get(class_name) } - - compiler = Tapioca::Compilers::DslCompiler.new( - requested_constants: [], - requested_generators: classes - ) - - T.must(compiler.generators.find { |generator| generator.class.name == names.first }) + after do + generated_errors.clear end sig { returns(Class) } - def spec_test_class - # Find the spec test class - klass = T.unsafe(self).class + def self.spec_test_class # It should be the one that directly inherits from DslSpec - klass = klass.superclass while klass.superclass != DslSpec - klass + class_ancestors = T.cast(ancestors.grep(Class), T::Array[Class]) + + klass = class_ancestors + .take_while { |ancestor| ancestor != DslSpec } + .last + + T.must(klass) end sig { returns(String) } - def target_class_name + def self.target_class_name # Get the name of the class under test from the name of the # test class T.must(spec_test_class.name).gsub(/Spec$/, "") end + sig { returns(T.class_of(Tapioca::Compilers::Dsl::Base)) } + def self.target_class + Object.const_get(target_class_name) + end + sig { returns(String) } - def target_class_file + def self.target_class_file underscore(target_class_name) end sig { params(class_name: String).returns(String) } - def underscore(class_name) + def self.underscore(class_name) return class_name unless /[A-Z-]|::/.match?(class_name) word = class_name.to_s.gsub("::", "/") @@ -79,44 +60,6 @@ def underscore(class_name) word end - sig { params(str: String, indent: Integer).returns(String) } - def indented(str, indent) - str.lines.map! do |line| - next line if line.chomp.empty? - (" " * indent) + line - end.join - end - - sig { returns(T::Array[String]) } - def gathered_constants - T.unsafe(self).subject.processable_constants.map(&:name).sort - end - - sig do - params( - constant_name: T.any(Symbol, String) - ).returns(String) - end - def rbi_for(constant_name) - # Make sure this is a constant that we can handle. - assert_includes(gathered_constants, constant_name.to_s, <<~MSG) - `#{constant_name}` is not processable by the `#{target_class_name}` generator. - MSG - - file = RBI::File.new(strictness: "strong") - - constant = Object.const_get(constant_name) - - T.unsafe(self).subject.decorate(file.root, constant) - - file.transformed_string - end - - sig { returns(T::Array[String]) } - def generated_errors - T.unsafe(self).subject.errors - end - sig { void } def assert_no_generated_errors T.unsafe(self).assert_empty(generated_errors) diff --git a/spec/tapioca/compilers/dsl/active_record_associations_spec.rb b/spec/tapioca/compilers/dsl/active_record_associations_spec.rb index 608bf14bc..28d4d8198 100644 --- a/spec/tapioca/compilers/dsl/active_record_associations_spec.rb +++ b/spec/tapioca/compilers/dsl/active_record_associations_spec.rb @@ -54,10 +54,9 @@ class Current < ActiveRecord::Base end describe "with relations enabled" do - subject do - T.bind(self, DslSpec) + before do require "tapioca/compilers/dsl/active_record_relations" - generator_for_names(target_class_name, "Tapioca::Compilers::Dsl::ActiveRecordRelations") + activate_other_dsl_compilers(Tapioca::Compilers::Dsl::ActiveRecordRelations) end describe "without errors" do @@ -1437,13 +1436,10 @@ def shop=(value); end end describe "decorate_active_storage" do - subject do - T.bind(self, DslSpec) + before do require "tapioca/compilers/dsl/active_record_relations" - generator_for_names(target_class_name, "Tapioca::Compilers::Dsl::ActiveRecordRelations") - end + activate_other_dsl_compilers(Tapioca::Compilers::Dsl::ActiveRecordRelations) - before do add_ruby_file("application.rb", <<~RUBY) ENV["DATABASE_URL"] = "sqlite3::memory:" diff --git a/spec/tapioca/compilers/dsl/active_record_scope_spec.rb b/spec/tapioca/compilers/dsl/active_record_scope_spec.rb index 694bece47..0a2d0b4ec 100644 --- a/spec/tapioca/compilers/dsl/active_record_scope_spec.rb +++ b/spec/tapioca/compilers/dsl/active_record_scope_spec.rb @@ -37,10 +37,9 @@ class User end describe "with relations enabled" do - subject do - T.bind(self, DslSpec) + before do require "tapioca/compilers/dsl/active_record_relations" - generator_for_names(target_class_name, "Tapioca::Compilers::Dsl::ActiveRecordRelations") + activate_other_dsl_compilers(Tapioca::Compilers::Dsl::ActiveRecordRelations) end it "generates an empty RBI file for ActiveRecord classes with no scope field" do @@ -344,17 +343,14 @@ def post_scope(*args, &blk); end end describe "decorate_active_storage" do - subject do - T.bind(self, DslSpec) - require "tapioca/compilers/dsl/active_record_relations" - generator_for_names(target_class_name, "Tapioca::Compilers::Dsl::ActiveRecordRelations") - end - after do - T.unsafe(self).assert_no_generated_errors + assert_no_generated_errors end before do + require "tapioca/compilers/dsl/active_record_relations" + activate_other_dsl_compilers(Tapioca::Compilers::Dsl::ActiveRecordRelations) + require "active_record" require "active_storage/attached" require "active_storage/reflection" diff --git a/spec/tapioca/compilers/dsl/active_record_typed_store_spec.rb b/spec/tapioca/compilers/dsl/active_record_typed_store_spec.rb index 5b6e89968..0f87bdc67 100644 --- a/spec/tapioca/compilers/dsl/active_record_typed_store_spec.rb +++ b/spec/tapioca/compilers/dsl/active_record_typed_store_spec.rb @@ -5,10 +5,9 @@ class Tapioca::Compilers::Dsl::ActiveRecordTypedStoreSpec < DslSpec describe "Tapioca::Compilers::Dsl::ActiveRecordTypedStore" do - before do - add_ruby_file("require.rb", <<~RUBY) - require "active_record" - RUBY + sig { void } + def before_setup + require "active_record" end describe "initialize" do diff --git a/spec/tapioca/compilers/dsl/config_spec.rb b/spec/tapioca/compilers/dsl/config_spec.rb index bd97b6c24..ccad69794 100644 --- a/spec/tapioca/compilers/dsl/config_spec.rb +++ b/spec/tapioca/compilers/dsl/config_spec.rb @@ -1,11 +1,12 @@ -# typed: true +# typed: strict # frozen_string_literal: true require "spec_helper" class Tapioca::Compilers::Dsl::ConfigSpec < DslSpec describe "Tapioca::Compilers::Dsl::Config" do - before do + sig { void } + def before_setup Object.send(:remove_const, :Rails) end diff --git a/spec/tapioca/compilers/dsl/frozen_record_spec.rb b/spec/tapioca/compilers/dsl/frozen_record_spec.rb index 673e593bb..140caf58b 100644 --- a/spec/tapioca/compilers/dsl/frozen_record_spec.rb +++ b/spec/tapioca/compilers/dsl/frozen_record_spec.rb @@ -5,7 +5,8 @@ class Tapioca::Compilers::Dsl::FrozenRecordSpec < DslSpec describe "Tapioca::Compilers::Dsl::FrozenRecord" do - before do + sig { void } + def before_setup require "rails/railtie" require "tapioca/compilers/dsl/extensions/frozen_record" end diff --git a/spec/tapioca/compilers/dsl/identity_cache_spec.rb b/spec/tapioca/compilers/dsl/identity_cache_spec.rb index b3fb7969d..32f03b870 100644 --- a/spec/tapioca/compilers/dsl/identity_cache_spec.rb +++ b/spec/tapioca/compilers/dsl/identity_cache_spec.rb @@ -5,7 +5,8 @@ class Tapioca::Compilers::Dsl::IdentityCacheSpec < DslSpec describe "Tapioca::Compilers::Dsl::IdentityCache" do - before do + sig { void } + def before_setup require "rails/railtie" end