lib/tapioca/generator.rb in tapioca-0.4.0 vs lib/tapioca/generator.rb in tapioca-0.4.1
- old
+ new
@@ -33,20 +33,56 @@
gems_to_generate(gem_names)
.reject { |gem| config.exclude.include?(gem.name) }
.each do |gem|
say("Processing '#{gem.name}' gem:", :green)
indent do
- compile_rbi(gem)
+ 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
sig { void }
+ 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 << rbi_header(
+ config.generate_command,
+ reason: "explicit gem requires",
+ strictness: "false"
+ )
+ 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("tapioca sync", :yellow, :bold)
+ say("Please review changes and commit them, then run #{cmd}.", [:green, :bold])
+ end
+
+ sig { void }
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... ")
@@ -60,11 +96,15 @@
say("Nothing to do", :green)
return
end
content = String.new
- content << rbi_header(config.generate_command, "false")
+ content << rbi_header(
+ config.generate_command,
+ reason: "unresolved constants",
+ strictness: "false"
+ )
content << rbi_string
content << "\n"
outdir = File.dirname(todos_path)
FileUtils.mkdir_p(outdir)
@@ -74,10 +114,37 @@
say("All unresolved constants have been written to #{name}.", [:green, :bold])
say("Please review changes and commit them.", [:green, :bold])
end
+ sig { params(requested_constants: T::Array[String]).void }
+ def build_dsl(requested_constants)
+ load_application(eager_load: requested_constants.empty?)
+ load_dsl_generators
+
+ say("Compiling DSL RBI files...")
+ say("")
+
+ compiler = Compilers::DslCompiler.new(
+ requested_constants: constantize(requested_constants),
+ requested_generators: config.generators,
+ error_handler: ->(error) {
+ say_error(error, :bold, :red)
+ }
+ )
+
+ compiler.run do |constant, contents|
+ compile_dsl_rbi(constant, contents)
+ end
+
+ say("")
+ say("Done", :green)
+
+ say("All operations performed in working directory.", [:green, :bold])
+ say("Please review changes and commit them.", [:green, :bold])
+ end
+
sig { void }
def sync_rbis_with_gemfile
anything_done = [
perform_removals,
perform_additions,
@@ -111,19 +178,87 @@
end
sig { void }
def require_gem_file
say("Requiring all gems to prepare for compiling... ")
- loader.load_bundle(config.prerequire, config.postrequire)
+ begin
+ loader.load_bundle(config.prerequire, config.postrequire)
+ rescue LoadError => e
+ explain_failed_require(config.postrequire, e)
+ exit(1)
+ end
say(" Done", :green)
puts
end
+ sig { params(file: String, error: LoadError).void }
+ 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("tapioca require", :bold, :blue)
+ say_error("you should probably review it and remove the faulty line.", :yellow)
+ end
+
+ sig do
+ params(
+ message: String,
+ color: T.any(Symbol, T::Array[Symbol]),
+ ).void
+ end
+ 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
+
+ sig { params(eager_load: T::Boolean).void }
+ def load_application(eager_load:)
+ say("Loading Rails application... ")
+
+ loader.load_rails(
+ environment_load: true,
+ eager_load: eager_load
+ )
+
+ say("Done", :green)
+ end
+
+ sig { void }
+ 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
+
+ sig { params(constant_names: T::Array[String]).returns(T::Array[Module]) }
+ def constantize(constant_names)
+ constant_names.map do |name|
+ begin
+ name.constantize
+ rescue NameError
+ nil
+ end
+ end.compact
+ end
+
sig { returns(T::Hash[String, String]) }
def existing_rbis
@existing_rbis ||= Pathname.glob((config.outpath / "*@*.rbi").to_s)
- .map { |f| f.basename(".*").to_s.split('@') }
+ .map { |f| T.cast(f.basename(".*").to_s.split('@', 2), [String, String]) }
.to_h
end
sig { returns(T::Hash[String, String]) }
def expected_rbis
@@ -132,26 +267,26 @@
.map { |gem| [gem.name, gem.version.to_s] }
.to_h
end
sig { params(gem_name: String, version: String).returns(Pathname) }
- def rbi_filename(gem_name, version)
+ def gem_rbi_filename(gem_name, version)
config.outpath / "#{gem_name}@#{version}.rbi"
end
sig { params(gem_name: String).returns(Pathname) }
def existing_rbi(gem_name)
- rbi_filename(gem_name, T.must(existing_rbis[gem_name]))
+ gem_rbi_filename(gem_name, T.must(existing_rbis[gem_name]))
end
sig { params(gem_name: String).returns(Pathname) }
def expected_rbi(gem_name)
- rbi_filename(gem_name, T.must(expected_rbis[gem_name]))
+ gem_rbi_filename(gem_name, T.must(expected_rbis[gem_name]))
end
sig { params(gem_name: String).returns(T::Boolean) }
- def rbi_exists?(gem_name)
+ def gem_rbi_exists?(gem_name)
existing_rbis.key?(gem_name)
end
sig { returns(T::Array[String]) }
def removed_rbis
@@ -225,17 +360,17 @@
require_gem_file
gems.each do |gem_name|
filename = expected_rbi(gem_name)
- if rbi_exists?(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_rbi(gem)
+ compile_gem_rbi(gem)
add(filename)
puts
end
end
@@ -263,39 +398,70 @@
end
gem
end
end
- sig { params(command: String, typed_sigil: String).returns(String) }
- def rbi_header(command, typed_sigil)
- <<~HEAD
- # This file is autogenerated. Do not edit it by hand. Regenerate it with:
- # #{command}
+ sig { params(command: String, reason: T.nilable(String), strictness: T.nilable(String)).returns(String) }
+ 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
- # typed: #{typed_sigil}
+ sigil = <<~SIGIL if strictness
+ # typed: #{strictness}
+ SIGIL
- HEAD
+ [statement, sigil].compact.join("\n").strip.concat("\n\n")
end
sig { params(gem: Gemfile::Gem).void }
- def compile_rbi(gem)
+ 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... ")
- typed_sigil = config.typed_overrides[gem.name] || "true"
+ strictness = config.typed_overrides[gem.name] || "true"
- content = compiler.compile(gem)
- content.prepend(rbi_header(config.generate_command, typed_sigil))
+ content = String.new
+ content << rbi_header(
+ config.generate_command,
+ reason: "types exported from the `#{gem.name}` gem",
+ strictness: strictness
+ )
+ content << compiler.compile(gem)
FileUtils.mkdir_p(config.outdir)
filename = config.outpath / gem.rbi_file_name
File.write(filename.to_s, content)
say("Done", :green)
Pathname.glob((config.outpath / "#{gem.name}@*.rbi").to_s) do |file|
remove(file) unless file.basename.to_s == gem.rbi_file_name
end
+ end
+
+ sig { params(constant: Module, contents: String).void }
+ def compile_dsl_rbi(constant, contents)
+ return if contents.nil?
+
+ command = format(config.generate_command, constant.name)
+ constant_name = Module.instance_method(:name).bind(constant).call
+ rbi_name = constant_name.underscore + ".rbi"
+ filename = config.outpath / rbi_name
+
+ out = String.new
+ out << rbi_header(
+ command,
+ reason: "dynamic methods in `#{constant.name}`"
+ )
+ out << contents
+
+ FileUtils.mkdir_p(File.dirname(filename))
+ File.write(filename, out)
+ say("Wrote: ", [:green])
+ say(filename)
end
end
end