lib/tapioca/generator.rb in tapioca-0.4.27 vs lib/tapioca/generator.rb in tapioca-0.5.0

- old
+ new

@@ -1,11 +1,11 @@ # typed: strict # frozen_string_literal: true -require 'pathname' -require 'thor' -require "tapioca/core_ext/string" +require "pathname" +require "thor" +require "rake" module Tapioca class Generator < ::Thor::Shell::Color extend(T::Sig) @@ -61,15 +61,12 @@ # Clean all existing requires before regenerating the list so we update # it with the new one found in the client code and remove the old ones. File.delete(requires_path) if File.exist?(requires_path) content = String.new - content << rbi_header( - "#{Config::DEFAULT_COMMAND} require", - reason: "explicit gem requires", - strictness: "false" - ) + content << "# typed: true\n" + content << "# frozen_string_literal: true\n\n" content << rb_string outdir = File.dirname(requires_path) FileUtils.mkdir_p(outdir) File.write(requires_path, content) @@ -119,15 +116,17 @@ sig do params( requested_constants: T::Array[String], should_verify: T::Boolean, - quiet: T::Boolean + quiet: T::Boolean, + verbose: T::Boolean ).void end - def build_dsl(requested_constants, should_verify: false, quiet: false) + def build_dsl(requested_constants, should_verify: false, quiet: false, verbose: false) load_application(eager_load: requested_constants.empty?) + abort_if_pending_migrations! load_dsl_generators if should_verify say("Checking for out-of-date RBIs...") else @@ -138,24 +137,30 @@ outpath = should_verify ? Pathname.new(Dir.mktmpdir) : config.outpath rbi_files_to_purge = existing_rbi_filenames(requested_constants) compiler = Compilers::DslCompiler.new( requested_constants: constantize(requested_constants), - requested_generators: config.generators, + requested_generators: constantize_generators(config.generators), + excluded_generators: constantize_generators(config.exclude_generators), error_handler: ->(error) { say_error(error, :bold, :red) } ) compiler.run do |constant, contents| - constant_name = Module.instance_method(:name).bind(constant).call + constant_name = T.must(Reflection.name_of(constant)) + if verbose && !quiet + say("Processing: ", [:yellow]) + say(constant_name) + end + filename = compile_dsl_rbi( constant_name, contents, outpath: outpath, - quiet: should_verify || quiet + quiet: should_verify || quiet && !verbose ) if filename rbi_files_to_purge.delete(filename) end @@ -172,12 +177,19 @@ say("All operations performed in working directory.", [:green, :bold]) say("Please review changes and commit them.", [:green, :bold]) end end - sig { void } - def sync_rbis_with_gemfile + sig { params(should_verify: T::Boolean).void } + def sync_rbis_with_gemfile(should_verify: false) + if should_verify + say("Checking for out-of-date RBIs...") + say("") + perform_sync_verification + return + end + anything_done = [ perform_removals, perform_additions, ].any? @@ -193,21 +205,21 @@ private EMPTY_RBI_COMMENT = <<~CONTENT # THIS IS AN EMPTY RBI FILE. - # see https://github.com/Shopify/tapioca/blob/master/README.md#manual-gem-requires + # see https://github.com/Shopify/tapioca/wiki/Manual-Gem-Requires CONTENT sig { returns(Gemfile) } def bundle @bundle ||= Gemfile.new end sig { returns(Loader) } def loader - @loader ||= Loader.new(bundle) + @loader ||= Loader.new end sig { returns(Compilers::SymbolTableCompiler) } def compiler @compiler ||= Compilers::SymbolTableCompiler.new @@ -215,19 +227,19 @@ sig { void } def require_gem_file say("Requiring all gems to prepare for compiling... ") begin - loader.load_bundle(config.prerequire, config.postrequire) + loader.load_bundle(bundle, config.prerequire, config.postrequire) rescue LoadError => e explain_failed_require(config.postrequire, e) exit(1) end say(" Done", :green) unless bundle.missing_specs.empty? say(" completed with missing specs: ") - say(bundle.missing_specs.join(', '), :yellow) + say(bundle.missing_specs.join(", "), :yellow) end puts end sig { params(file: String, error: LoadError).void } @@ -283,15 +295,13 @@ end sig { params(constant_names: T::Array[String]).returns(T::Array[Module]) } def constantize(constant_names) constant_map = constant_names.map do |name| - begin - [name, Object.const_get(name)] - rescue NameError - [name, nil] - end + [name, Object.const_get(name)] + rescue NameError + [name, nil] end.to_h unprocessable_constants = constant_map.select { |_, v| v.nil? } unless unprocessable_constants.empty? unprocessable_constants.each do |name, _| @@ -303,10 +313,36 @@ end constant_map.values end + sig { params(generator_names: T::Array[String]).returns(T::Array[T.class_of(Compilers::Dsl::Base)]) } + def constantize_generators(generator_names) + generator_map = generator_names.map do |name| + # Try to find built-in tapioca generator first, then globally defined generator. The + # explicit `break` ensures the class is returned, not the `potential_name`. + generator_klass = ["Tapioca::Compilers::Dsl::#{name}", name].find do |potential_name| + break Object.const_get(potential_name) + rescue NameError + # Skip if we can't find generator by the potential name + end + + [name, generator_klass] + end.to_h + + unprocessable_generators = generator_map.select { |_, v| v.nil? } + unless unprocessable_generators.empty? + unprocessable_generators.each do |name, _| + say("Error: Cannot find generator '#{name}'", :red) + end + + exit(1) + end + + generator_map.values + end + sig { params(requested_constants: T::Array[String], path: Pathname).returns(T::Set[Pathname]) } def existing_rbi_filenames(requested_constants, path: config.outpath) filenames = if requested_constants.empty? Pathname.glob(path / "**/*.rbi") else @@ -319,11 +355,11 @@ end sig { returns(T::Hash[String, String]) } def existing_rbis @existing_rbis ||= Pathname.glob((config.outpath / "*@*.rbi").to_s) - .map { |f| T.cast(f.basename(".*").to_s.split('@', 2), [String, String]) } + .map { |f| T.cast(f.basename(".*").to_s.split("@", 2), [String, String]) } .to_h end sig { returns(T::Hash[String, String]) } def expected_rbis @@ -333,11 +369,11 @@ .to_h end sig { params(constant_name: String).returns(Pathname) } def dsl_rbi_filename(constant_name) - config.outpath / "#{constant_name.underscore}.rbi" + config.outpath / "#{underscore(constant_name)}.rbi" end sig { params(gem_name: String, version: String).returns(Pathname) } def gem_rbi_filename(gem_name, version) config.outpath / "#{gem_name}@#{version}.rbi" @@ -481,11 +517,17 @@ sigil = <<~SIGIL if strictness # typed: #{strictness} SIGIL - [statement, sigil].compact.join("\n").strip.concat("\n\n") + if config.file_header + [statement, sigil].compact.join("\n").strip.concat("\n\n") + elsif sigil + sigil.strip.concat("\n\n") + else + "" + end end sig { params(gem: Gemfile::GemSpec).void } def compile_gem_rbi(gem) compiler = Compilers::SymbolTableCompiler.new @@ -523,11 +565,11 @@ .returns(T.nilable(Pathname)) end def compile_dsl_rbi(constant_name, contents, outpath: config.outpath, quiet: false) return if contents.nil? - rbi_name = constant_name.underscore + ".rbi" + rbi_name = underscore(constant_name) + ".rbi" filename = outpath / rbi_name out = String.new out << rbi_header( "#{Config::DEFAULT_COMMAND} dsl #{constant_name}", @@ -596,15 +638,51 @@ sig { params(dir: Pathname).void } def perform_dsl_verification(dir) diff = verify_dsl_rbi(tmp_dir: dir) + report_diff_and_exit_if_out_of_date(diff, "dsl") + ensure + FileUtils.remove_entry(dir) + end + + sig { params(files: T::Set[Pathname]).void } + def purge_stale_dsl_rbi_files(files) + if files.any? + say("Removing stale RBI files...") + + files.sort.each do |filename| + remove(filename) + end + say("") + end + end + + sig { void } + def perform_sync_verification + diff = {} + + removed_rbis.each do |gem_name| + filename = existing_rbi(gem_name) + diff[filename] = :removed + end + + added_rbis.each do |gem_name| + filename = expected_rbi(gem_name) + diff[filename] = gem_rbi_exists?(gem_name) ? :changed : :added + end + + report_diff_and_exit_if_out_of_date(diff, "sync") + end + + sig { params(diff: T::Hash[String, Symbol], command: String).void } + def report_diff_and_exit_if_out_of_date(diff, command) if diff.empty? say("Nothing to do, all RBIs are up-to-date.") else say("RBI files are out-of-date. In your development environment, please run:", :green) - say(" `#{Config::DEFAULT_COMMAND} dsl`", [:green, :bold]) + say(" `#{Config::DEFAULT_COMMAND} #{command}`", [:green, :bold]) say("Once it is complete, be sure to commit and push any changes", :green) say("") say("Reason:", [:red]) @@ -612,22 +690,28 @@ say(build_error_for_files(cause, diff_for_cause.map(&:first))) end exit(1) end - ensure - FileUtils.remove_entry(dir) end - sig { params(files: T::Set[Pathname]).void } - def purge_stale_dsl_rbi_files(files) - if files.any? - say("Removing stale RBI files...") + sig { void } + def abort_if_pending_migrations! + return unless File.exist?("config/application.rb") - files.sort.each do |filename| - remove(filename) - end - say("") - end + Rails.application.load_tasks + Rake::Task["db:abort_if_pending_migrations"].invoke if Rake::Task.task_defined?("db:abort_if_pending_migrations") + end + + sig { params(class_name: String).returns(String) } + def underscore(class_name) + return class_name unless /[A-Z-]|::/.match?(class_name) + + word = class_name.to_s.gsub("::", "/") + word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2') + word.gsub!(/([a-z\d])([A-Z])/, '\1_\2') + word.tr!("-", "_") + word.downcase! + word end end end