class Tapioca::Generator
Constants
- EMPTY_RBI_COMMENT
Attributes
config[R]
Public Class Methods
new(config)
click to toggle source
Calls superclass method
# File lib/tapioca/generator.rb, line 19 def initialize(config) @config = config @bundle = T.let(nil, T.nilable(Gemfile)) @loader = T.let(nil, T.nilable(Loader)) @compiler = T.let(nil, T.nilable(Compilers::SymbolTableCompiler)) @existing_rbis = T.let(nil, T.nilable(T::Hash[String, String])) @expected_rbis = T.let(nil, T.nilable(T::Hash[String, String])) super() end
Public Instance Methods
build_dsl(requested_constants, should_verify: false, quiet: false, verbose: false)
click to toggle source
# File lib/tapioca/generator.rb, line 124 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 say("Compiling DSL RBI files...") end say("") 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: 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 = 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 && !verbose ) if filename rbi_files_to_purge.delete(filename) end end say("") if should_verify perform_dsl_verification(outpath) else purge_stale_dsl_rbi_files(rbi_files_to_purge) say("Done", :green) say("All operations performed in working directory.", [:green, :bold]) say("Please review changes and commit them.", [:green, :bold]) end end
build_gem_rbis(gem_names)
click to toggle source
# File lib/tapioca/generator.rb, line 30 def build_gem_rbis(gem_names) require_gem_file gems_to_generate(gem_names) .reject { |gem| config.exclude.include?(gem.name) } .each do |gem| say("Processing '#{gem.name}' gem:", :green) indent do compile_gem_rbi(gem) puts end end say("All operations performed in working directory.", [:green, :bold]) say("Please review changes and commit them.", [:green, :bold]) end
build_requires()
click to toggle source
# File lib/tapioca/generator.rb, line 48 def build_requires requires_path = Config::DEFAULT_POSTREQUIRE compiler = Compilers::RequiresCompiler.new(Config::SORBET_CONFIG) name = set_color(requires_path, :yellow, :bold) say("Compiling #{name}, this may take a few seconds... ") rb_string = compiler.compile if rb_string.empty? say("Nothing to do", :green) return end # 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 << "# 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) say("Done", :green) say("All requires from this application have been written to #{name}.", [:green, :bold]) cmd = set_color("#{Config::DEFAULT_COMMAND} sync", :yellow, :bold) say("Please review changes and commit them, then run `#{cmd}`.", [:green, :bold]) end
build_todos()
click to toggle source
# File lib/tapioca/generator.rb, line 81 def build_todos todos_path = config.todos_path compiler = Compilers::TodosCompiler.new name = set_color(todos_path, :yellow, :bold) say("Compiling #{name}, this may take a few seconds... ") # Clean all existing unresolved constants before regenerating the list # so Sorbet won't grab them as already resolved. File.delete(todos_path) if File.exist?(todos_path) rbi_string = compiler.compile if rbi_string.empty? say("Nothing to do", :green) return end content = String.new content << rbi_header( "#{Config::DEFAULT_COMMAND} todo", reason: "unresolved constants", strictness: "false" ) content << rbi_string content << "\n" outdir = File.dirname(todos_path) FileUtils.mkdir_p(outdir) File.write(todos_path, content) say("Done", :green) say("All unresolved constants have been written to #{name}.", [:green, :bold]) say("Please review changes and commit them.", [:green, :bold]) end
sync_rbis_with_gemfile(should_verify: false)
click to toggle source
# File lib/tapioca/generator.rb, line 182 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? if anything_done say("All operations performed in working directory.", [:green, :bold]) say("Please review changes and commit them.", [:green, :bold]) else say("No operations performed, all RBIs are up-to-date.", [:green, :bold]) end puts end
Private Instance Methods
abort_if_pending_migrations!()
click to toggle source
# File lib/tapioca/generator.rb, line 697 def abort_if_pending_migrations! return unless File.exist?("config/application.rb") return unless defined?(::Rake) Rails.application.load_tasks Rake::Task["db:abort_if_pending_migrations"].invoke if Rake::Task.task_defined?("db:abort_if_pending_migrations") end
add(filename)
click to toggle source
# File lib/tapioca/generator.rb, line 409 def add(filename) say("++ Adding: #{filename}") end
added_rbis()
click to toggle source
# File lib/tapioca/generator.rb, line 402 def added_rbis expected_rbis.select do |name, value| existing_rbis[name] != value end.keys.sort end
build_error_for_files(cause, files)
click to toggle source
# File lib/tapioca/generator.rb, line 623 def build_error_for_files(cause, files) filenames = files.map do |file| config.outpath / file end.join("\n - ") " File(s) #{cause}:\n - #{filenames}" end
bundle()
click to toggle source
# File lib/tapioca/generator.rb, line 213 def bundle @bundle ||= Gemfile.new end
compile_dsl_rbi(constant_name, contents, outpath: config.outpath, quiet: false)
click to toggle source
# File lib/tapioca/generator.rb, line 566 def compile_dsl_rbi(constant_name, contents, outpath: config.outpath, quiet: false) return if contents.nil? rbi_name = underscore(constant_name) + ".rbi" filename = outpath / rbi_name out = String.new out << rbi_header( "#{Config::DEFAULT_COMMAND} dsl #{constant_name}", reason: "dynamic methods in `#{constant_name}`" ) out << contents FileUtils.mkdir_p(File.dirname(filename)) File.write(filename, out) unless quiet say("Wrote: ", [:green]) say(filename) end filename end
compile_gem_rbi(gem)
click to toggle source
# File lib/tapioca/generator.rb, line 531 def compile_gem_rbi(gem) compiler = Compilers::SymbolTableCompiler.new gem_name = set_color(gem.name, :yellow, :bold) say("Compiling #{gem_name}, this may take a few seconds... ") strictness = config.typed_overrides[gem.name] || "true" rbi_body_content = compiler.compile(gem) content = String.new content << rbi_header( "#{Config::DEFAULT_COMMAND} sync", reason: "types exported from the `#{gem.name}` gem", strictness: strictness ) FileUtils.mkdir_p(config.outdir) filename = config.outpath / gem.rbi_file_name if rbi_body_content.strip.empty? content << EMPTY_RBI_COMMENT say("Done (empty output)", :yellow) else content << rbi_body_content say("Done", :green) end File.write(filename.to_s, content) T.unsafe(Pathname).glob((config.outpath / "#{gem.name}@*.rbi").to_s) do |file| remove(file) unless file.basename.to_s == gem.rbi_file_name end end
compiler()
click to toggle source
# File lib/tapioca/generator.rb, line 223 def compiler @compiler ||= Compilers::SymbolTableCompiler.new end
constantize(constant_names)
click to toggle source
# File lib/tapioca/generator.rb, line 297 def constantize(constant_names) constant_map = constant_names.map do |name| [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, _| say("Error: Cannot find constant '#{name}'", :red) remove(dsl_rbi_filename(name)) end exit(1) end constant_map.values end
constantize_generators(generator_names)
click to toggle source
# File lib/tapioca/generator.rb, line 318 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
dsl_rbi_filename(constant_name)
click to toggle source
# File lib/tapioca/generator.rb, line 372 def dsl_rbi_filename(constant_name) config.outpath / "#{underscore(constant_name)}.rbi" end
existing_rbi(gem_name)
click to toggle source
# File lib/tapioca/generator.rb, line 382 def existing_rbi(gem_name) gem_rbi_filename(gem_name, T.must(existing_rbis[gem_name])) end
existing_rbi_filenames(requested_constants, path: config.outpath)
click to toggle source
# File lib/tapioca/generator.rb, line 344 def existing_rbi_filenames(requested_constants, path: config.outpath) filenames = if requested_constants.empty? Pathname.glob(path / "**/*.rbi") else requested_constants.map do |constant_name| dsl_rbi_filename(constant_name) end end filenames.to_set end
existing_rbis()
click to toggle source
# File lib/tapioca/generator.rb, line 357 def existing_rbis @existing_rbis ||= Pathname.glob((config.outpath / "*@*.rbi").to_s) .map { |f| T.cast(f.basename(".*").to_s.split("@", 2), [String, String]) } .to_h end
expected_rbi(gem_name)
click to toggle source
# File lib/tapioca/generator.rb, line 387 def expected_rbi(gem_name) gem_rbi_filename(gem_name, T.must(expected_rbis[gem_name])) end
expected_rbis()
click to toggle source
# File lib/tapioca/generator.rb, line 364 def expected_rbis @expected_rbis ||= bundle.dependencies .reject { |gem| config.exclude.include?(gem.name) } .map { |gem| [gem.name, gem.version.to_s] } .to_h end
explain_failed_require(file, error)
click to toggle source
# File lib/tapioca/generator.rb, line 245 def explain_failed_require(file, error) say_error("\n\nLoadError: #{error}", :bold, :red) say_error("\nTapioca could not load all the gems required by your application.", :yellow) say_error("If you populated ", :yellow) say_error("#{file} ", :bold, :blue) say_error("with ", :yellow) say_error("`#{Config::DEFAULT_COMMAND} require`", :bold, :blue) say_error("you should probably review it and remove the faulty line.", :yellow) end
gem_rbi_exists?(gem_name)
click to toggle source
# File lib/tapioca/generator.rb, line 392 def gem_rbi_exists?(gem_name) existing_rbis.key?(gem_name) end
gem_rbi_filename(gem_name, version)
click to toggle source
# File lib/tapioca/generator.rb, line 377 def gem_rbi_filename(gem_name, version) config.outpath / "#{gem_name}@#{version}.rbi" end
gems_to_generate(gem_names)
click to toggle source
# File lib/tapioca/generator.rb, line 496 def gems_to_generate(gem_names) return bundle.dependencies if gem_names.empty? gem_names.map do |gem_name| gem = bundle.gem(gem_name) if gem.nil? say("Error: Cannot find gem '#{gem_name}'", :red) exit(1) end gem end end
load_application(eager_load:)
click to toggle source
# File lib/tapioca/generator.rb, line 271 def load_application(eager_load:) say("Loading Rails application... ") loader.load_rails_application( environment_load: true, eager_load: eager_load ) say("Done", :green) end
load_dsl_generators()
click to toggle source
# File lib/tapioca/generator.rb, line 283 def load_dsl_generators say("Loading DSL generator classes... ") Dir.glob([ "#{__dir__}/compilers/dsl/*.rb", "#{Config::TAPIOCA_PATH}/generators/**/*.rb", ]).each do |generator| require File.expand_path(generator) end say("Done", :green) end
loader()
click to toggle source
# File lib/tapioca/generator.rb, line 218 def loader @loader ||= Loader.new end
move(old_filename, new_filename)
click to toggle source
# File lib/tapioca/generator.rb, line 421 def move(old_filename, new_filename) say("-> Moving: #{old_filename} to #{new_filename}") old_filename.rename(new_filename.to_s) end
perform_additions()
click to toggle source
# File lib/tapioca/generator.rb, line 454 def perform_additions say("Generating RBI files of gems that are added or updated:", [:blue, :bold]) puts anything_done = T.let(false, T::Boolean) gems = added_rbis indent do if gems.empty? say("Nothing to do.") else require_gem_file gems.each do |gem_name| filename = expected_rbi(gem_name) if gem_rbi_exists?(gem_name) old_filename = existing_rbi(gem_name) move(old_filename, filename) unless old_filename == filename end gem = T.must(bundle.gem(gem_name)) compile_gem_rbi(gem) add(filename) puts end end anything_done = true end puts anything_done end
perform_dsl_verification(dir)
click to toggle source
# File lib/tapioca/generator.rb, line 639 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
perform_removals()
click to toggle source
# File lib/tapioca/generator.rb, line 427 def perform_removals say("Removing RBI files of gems that have been removed:", [:blue, :bold]) puts anything_done = T.let(false, T::Boolean) gems = removed_rbis indent do if gems.empty? say("Nothing to do.") else gems.each do |removed| filename = existing_rbi(removed) remove(filename) end anything_done = true end end puts anything_done end
perform_sync_verification()
click to toggle source
# File lib/tapioca/generator.rb, line 660 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
purge_stale_dsl_rbi_files(files)
click to toggle source
# File lib/tapioca/generator.rb, line 648 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
rbi_files_in(path)
click to toggle source
# File lib/tapioca/generator.rb, line 632 def rbi_files_in(path) Pathname.glob(path / "**/*.rbi").map do |file| file.relative_path_from(path) end.sort end
rbi_header(command, reason: nil, strictness: nil)
click to toggle source
# File lib/tapioca/generator.rb, line 510 def rbi_header(command, reason: nil, strictness: nil) statement = <<~HEAD # DO NOT EDIT MANUALLY # This is an autogenerated file for #{reason}. # Please instead update this file by running `#{command}`. HEAD sigil = <<~SIGIL if strictness # typed: #{strictness} SIGIL if config.file_header [statement, sigil].compact.join("\n").strip.concat("\n\n") elsif sigil sigil.strip.concat("\n\n") else "" end end
remove(filename)
click to toggle source
# File lib/tapioca/generator.rb, line 414 def remove(filename) return unless filename.exist? say("-- Removing: #{filename}") filename.unlink end
removed_rbis()
click to toggle source
# File lib/tapioca/generator.rb, line 397 def removed_rbis (existing_rbis.keys - expected_rbis.keys).sort end
report_diff_and_exit_if_out_of_date(diff, command)
click to toggle source
# File lib/tapioca/generator.rb, line 677 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} #{command}`", [:green, :bold]) say("Once it is complete, be sure to commit and push any changes", :green) say("") say("Reason:", [:red]) diff.group_by(&:last).sort.each do |cause, diff_for_cause| say(build_error_for_files(cause, diff_for_cause.map(&:first))) end exit(1) end end
require_gem_file()
click to toggle source
# File lib/tapioca/generator.rb, line 228 def require_gem_file say("Requiring all gems to prepare for compiling... ") begin 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) end puts end
say_error(message = "", *color)
click to toggle source
# File lib/tapioca/generator.rb, line 261 def say_error(message = "", *color) force_new_line = (message.to_s !~ /( |\t)\Z/) buffer = prepare_message(*T.unsafe([message, *T.unsafe(color)])) buffer << "\n" if force_new_line && !message.to_s.end_with?("\n") stderr.print(buffer) stderr.flush end
underscore(class_name)
click to toggle source
# File lib/tapioca/generator.rb, line 706 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
verify_dsl_rbi(tmp_dir:)
click to toggle source
# File lib/tapioca/generator.rb, line 591 def verify_dsl_rbi(tmp_dir:) diff = {} existing_rbis = rbi_files_in(config.outpath) new_rbis = rbi_files_in(tmp_dir) added_files = (new_rbis - existing_rbis) added_files.each do |file| diff[file] = :added end removed_files = (existing_rbis - new_rbis) removed_files.each do |file| diff[file] = :removed end common_files = (existing_rbis & new_rbis) changed_files = common_files.map do |filename| filename unless FileUtils.identical?(config.outpath / filename, tmp_dir / filename) end.compact changed_files.each do |file| diff[file] = :changed end diff end