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