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