From cc66f92f02bed4d1f94133b525be10e167692d46 Mon Sep 17 00:00:00 2001 From: Alex Rocha Date: Wed, 27 Nov 2024 14:57:42 -0800 Subject: [PATCH 01/11] Create lockfile diff parser --- lib/ruby_lsp/tapioca/lockfile_diff_parser.rb | 43 ++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 lib/ruby_lsp/tapioca/lockfile_diff_parser.rb diff --git a/lib/ruby_lsp/tapioca/lockfile_diff_parser.rb b/lib/ruby_lsp/tapioca/lockfile_diff_parser.rb new file mode 100644 index 000000000..efa6c6735 --- /dev/null +++ b/lib/ruby_lsp/tapioca/lockfile_diff_parser.rb @@ -0,0 +1,43 @@ +# typed: true +# frozen_string_literal: true + +module RubyLsp + module Tapioca + class LockfileDiffParser + GEM_NAME_PATTERN = /[\w\-]+/ + DIFF_LINE_PATTERN = /[+-](.*#{GEM_NAME_PATTERN})\s*\(/ + ADDED_LINE_PATTERN = /^\+.*#{GEM_NAME_PATTERN} \(.*\)/ + REMOVED_LINE_PATTERN = /^-.*#{GEM_NAME_PATTERN} \(.*\)/ + + attr_reader :added_or_modified_gems + attr_reader :removed_gems + + def initialize(diff_content) + @diff_content = diff_content.lines + @added_or_modified_gems = parse_added_or_modified_gems + @removed_gems = parse_removed_gems + end + + private + + def parse_added_or_modified_gems + @diff_content + .filter { |line| line.match?(ADDED_LINE_PATTERN) } + .map { |line| extract_gem(line) } + .uniq + end + + def parse_removed_gems + @diff_content + .filter { |line| line.match?(REMOVED_LINE_PATTERN) } + .map { |line| extract_gem(line) } + .reject { |gem| @added_or_modified_gems.include?(gem) } + .uniq + end + + def extract_gem(line) + line.match(DIFF_LINE_PATTERN)[1].strip + end + end + end +end From 01fffb8cb34d4a3de2bc1c1438b597ce7b1d5c3f Mon Sep 17 00:00:00 2001 From: Andy Waite <13400+andyw8@users.noreply.github.com> Date: Mon, 25 Nov 2024 10:33:21 -0500 Subject: [PATCH 02/11] Add test for lockfile diff parser --- .../tapioca/lockfile_diff_parser_spec.rb | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 spec/tapioca/ruby_lsp/tapioca/lockfile_diff_parser_spec.rb diff --git a/spec/tapioca/ruby_lsp/tapioca/lockfile_diff_parser_spec.rb b/spec/tapioca/ruby_lsp/tapioca/lockfile_diff_parser_spec.rb new file mode 100644 index 000000000..139dc3fd2 --- /dev/null +++ b/spec/tapioca/ruby_lsp/tapioca/lockfile_diff_parser_spec.rb @@ -0,0 +1,62 @@ +# typed: strict +# frozen_string_literal: true + +require "spec_helper" +require "ruby_lsp/tapioca/lockfile_diff_parser" + +module RubyLsp + module Tapioca + class LockFileDiffParserSpec < Minitest::Spec + describe "#parse_added_or_modified_gems" do + it "parses added or modified gems from git diff" do + diff_output = <<~DIFF + + new_gem (1.0.0) + + updated_gem (2.0.0) + - removed_gem (1.0.0) + DIFF + + lockfile_parser = RubyLsp::Tapioca::LockfileDiffParser.new(diff_output) + assert_equal ["new_gem", "updated_gem"], lockfile_parser.added_or_modified_gems + end + + it "is empty when there is no diff" do + diff_output = "" + + lockfile_parser = RubyLsp::Tapioca::LockfileDiffParser.new(diff_output) + assert_empty lockfile_parser.added_or_modified_gems + end + end + + describe "#parse_removed_gems" do + it "parses removed gems from git diff" do + diff_output = <<~DIFF + + new_gem (1.0.0) + - removed_gem (1.0.0) + - outdated_gem (2.3.4) + DIFF + + lockfile_parser = RubyLsp::Tapioca::LockfileDiffParser.new(diff_output) + assert_equal ["removed_gem", "outdated_gem"], lockfile_parser.removed_gems + end + end + + it "handles gem names with hyphens and underscores" do + diff_output = <<~DIFF + - my-gem_extra2 (1.0.0.beta1) + DIFF + + lockfile_parser = RubyLsp::Tapioca::LockfileDiffParser.new(diff_output) + assert_equal ["my-gem_extra2"], lockfile_parser.removed_gems + end + + it "handles gem names with multiple hyphens" do + diff_output = <<~DIFF + - sorbet-static-and-runtime (0.5.0) + DIFF + + lockfile_parser = RubyLsp::Tapioca::LockfileDiffParser.new(diff_output) + assert_equal ["sorbet-static-and-runtime"], lockfile_parser.removed_gems + end + end + end +end From 3ac24d3d9fb1da9599a5214b469634c06a88292e Mon Sep 17 00:00:00 2001 From: Alex Rocha Date: Wed, 30 Oct 2024 17:42:04 -0700 Subject: [PATCH 03/11] Add Tapioca Addon gem RBI generation support To support gem RBI generation, we needed a way to detect changes in Gemfile.lock. Currently, changes to this file cause the Ruby LSP to restart, resulting in loss of access to any previous state information. By running git diff on Gemfile.lock, we can detect changes to the file, and trigger the gem RBI generation process. Then we can parse the diff output to determine which gems have been removed, added or modified, and either remove the corresponding RBI files or trigger the RBI generation process for the gems. --- lib/ruby_lsp/tapioca/addon.rb | 27 +++++++++++++++++++++++++++ lib/ruby_lsp/tapioca/server_addon.rb | 17 +++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/lib/ruby_lsp/tapioca/addon.rb b/lib/ruby_lsp/tapioca/addon.rb index b7a035b1a..eef5fa80b 100644 --- a/lib/ruby_lsp/tapioca/addon.rb +++ b/lib/ruby_lsp/tapioca/addon.rb @@ -27,6 +27,7 @@ def initialize @rails_runner_client = T.let(nil, T.nilable(RubyLsp::Rails::RunnerClient)) @index = T.let(nil, T.nilable(RubyIndexer::Index)) @file_checksums = T.let({}, T::Hash[String, String]) + @lockfile_diff = T.let(nil, T.nilable(String)) @outgoing_queue = T.let(nil, T.nilable(Thread::Queue)) end @@ -45,6 +46,8 @@ def activate(global_state, outgoing_queue) @rails_runner_client = addon.rails_runner_client @outgoing_queue << Notification.window_log_message("Activating Tapioca add-on v#{version}") @rails_runner_client.register_server_addon(File.expand_path("server_addon.rb", __dir__)) + + generate_gem_rbis if git_repo? && lockfile_changed? rescue IncompatibleApiError # The requested version for the Rails add-on no longer matches. We need to upgrade and fix the breaking # changes @@ -127,6 +130,30 @@ def file_updated?(change, path) false end + + sig { returns(T::Boolean) } + def git_repo? + Dir.exist?(".git") + end + + sig { returns(T::Boolean) } + def lockfile_changed? + !fetch_lockfile_diff.empty? + end + + sig { returns(String) } + def fetch_lockfile_diff + @lockfile_diff = %x(git diff HEAD Gemfile.lock).strip + end + + sig { void } + def generate_gem_rbis + T.must(@rails_runner_client).delegate_notification( + server_addon_name: "Tapioca", + request_name: "gem", + diff: T.must(@lockfile_diff), + ) + end end end end diff --git a/lib/ruby_lsp/tapioca/server_addon.rb b/lib/ruby_lsp/tapioca/server_addon.rb index 0d38cf93d..a3f4be283 100644 --- a/lib/ruby_lsp/tapioca/server_addon.rb +++ b/lib/ruby_lsp/tapioca/server_addon.rb @@ -2,6 +2,7 @@ # frozen_string_literal: true require "tapioca/internal" +require_relative "lockfile_diff_parser" module RubyLsp module Tapioca @@ -16,6 +17,8 @@ def execute(request, params) fork do dsl(params) end + when "gem" + gem(params) end end @@ -25,6 +28,20 @@ def dsl(params) load("tapioca/cli.rb") # Reload the CLI to reset thor defaults between requests ::Tapioca::Cli.start(["dsl", "--lsp_addon", "--workers=1"] + params[:constants]) end + + def gem(params) + gem_changes = LockfileDiffParser.new(params[:diff]) + + removed_gems = gem_changes.removed_gems + added_or_modified_gems = gem_changes.added_or_modified_gems + + if added_or_modified_gems.any? + load("tapioca/cli.rb") # Reload the CLI to reset thor defaults between requests + ::Tapioca::Cli.start(["gem"] + added_or_modified_gems) + elsif removed_gems.any? + FileUtils.rm_f(Dir.glob("sorbet/rbi/gems/{#{removed_gems.join(",")}}@*.rbi")) + end + end end end end From e19934f22c39f166128fb7e7b1c47edf70281fb6 Mon Sep 17 00:00:00 2001 From: Alex Rocha Date: Wed, 27 Nov 2024 11:38:04 -0800 Subject: [PATCH 04/11] Cleanup orphaned RBIs --- lib/ruby_lsp/tapioca/addon.rb | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/lib/ruby_lsp/tapioca/addon.rb b/lib/ruby_lsp/tapioca/addon.rb index eef5fa80b..0dfc15020 100644 --- a/lib/ruby_lsp/tapioca/addon.rb +++ b/lib/ruby_lsp/tapioca/addon.rb @@ -47,7 +47,9 @@ def activate(global_state, outgoing_queue) @outgoing_queue << Notification.window_log_message("Activating Tapioca add-on v#{version}") @rails_runner_client.register_server_addon(File.expand_path("server_addon.rb", __dir__)) - generate_gem_rbis if git_repo? && lockfile_changed? + if git_repo? + lockfile_changed? ? generate_gem_rbis : cleanup_orphaned_rbis + end rescue IncompatibleApiError # The requested version for the Rails add-on no longer matches. We need to upgrade and fix the breaking # changes @@ -154,6 +156,30 @@ def generate_gem_rbis diff: T.must(@lockfile_diff), ) end + + sig { void } + def cleanup_orphaned_rbis + untracked_files = %x(git ls-files --others --exclude-standard sorbet/rbi/gems/).lines.map(&:strip) + deleted_files = %x(git ls-files --deleted sorbet/rbi/gems/).lines.map(&:strip) + + untracked_files.each do |file| + File.delete(file) + + T.must(@outgoing_queue) << Notification.window_log_message( + "Deleted untracked RBI: #{file}", + type: Constant::MessageType::INFO, + ) + end + + deleted_files.each do |file| + %x(git checkout -- #{file}) + + T.must(@outgoing_queue) << Notification.window_log_message( + "Restored deleted RBI: #{file}", + type: Constant::MessageType::INFO, + ) + end + end end end end From ab9c5fda61615a9271f67ae1518aba3bdf7a1ee4 Mon Sep 17 00:00:00 2001 From: Alex Rocha Date: Wed, 4 Dec 2024 10:25:06 -0800 Subject: [PATCH 05/11] Refactor how we detect git repo --- lib/ruby_lsp/tapioca/addon.rb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/ruby_lsp/tapioca/addon.rb b/lib/ruby_lsp/tapioca/addon.rb index 0dfc15020..d2e755db6 100644 --- a/lib/ruby_lsp/tapioca/addon.rb +++ b/lib/ruby_lsp/tapioca/addon.rb @@ -13,6 +13,7 @@ end require "zlib" +require "open3" module RubyLsp module Tapioca @@ -133,9 +134,11 @@ def file_updated?(change, path) false end - sig { returns(T::Boolean) } + sig { returns(T.nilable(T::Boolean)) } def git_repo? - Dir.exist?(".git") + _, _, status = Open3.capture3("git rev-parse --is-inside-work-tree") + + status.success? end sig { returns(T::Boolean) } From 29c544c0a59f68080b4de45dc9cff674a7d64534 Mon Sep 17 00:00:00 2001 From: Alex Rocha Date: Wed, 4 Dec 2024 16:30:10 -0800 Subject: [PATCH 06/11] Refactor generating gem rbis out of ServerAddon --- lib/ruby_lsp/tapioca/addon.rb | 33 +++++++++++++++++++++++----- lib/ruby_lsp/tapioca/server_addon.rb | 17 -------------- 2 files changed, 28 insertions(+), 22 deletions(-) diff --git a/lib/ruby_lsp/tapioca/addon.rb b/lib/ruby_lsp/tapioca/addon.rb index d2e755db6..be59c8b82 100644 --- a/lib/ruby_lsp/tapioca/addon.rb +++ b/lib/ruby_lsp/tapioca/addon.rb @@ -14,6 +14,7 @@ require "zlib" require "open3" +require_relative "lockfile_diff_parser" module RubyLsp module Tapioca @@ -153,11 +154,33 @@ def fetch_lockfile_diff sig { void } def generate_gem_rbis - T.must(@rails_runner_client).delegate_notification( - server_addon_name: "Tapioca", - request_name: "gem", - diff: T.must(@lockfile_diff), - ) + gem_changes = LockfileDiffParser.new(@lockfile_diff) + + removed_gems = gem_changes.removed_gems + added_or_modified_gems = gem_changes.added_or_modified_gems + + if added_or_modified_gems.any? + # Resetting BUNDLE_GEMFILE to root folder to use the project's Gemfile instead of Ruby LSP's composed Gemfile + stdout, stderr, status = T.unsafe(Open3).capture3( + { "BUNDLE_GEMFILE" => "Gemfile" }, + "bin/tapioca", + "gem", + *added_or_modified_gems, + ) + T.must(@outgoing_queue) << if status.success? + Notification.window_log_message( + stdout, + type: Constant::MessageType::INFO, + ) + else + Notification.window_log_message( + stderr, + type: Constant::MessageType::ERROR, + ) + end + elsif removed_gems.any? + FileUtils.rm_f(Dir.glob("sorbet/rbi/gems/{#{removed_gems.join(",")}}@*.rbi")) + end end sig { void } diff --git a/lib/ruby_lsp/tapioca/server_addon.rb b/lib/ruby_lsp/tapioca/server_addon.rb index a3f4be283..0d38cf93d 100644 --- a/lib/ruby_lsp/tapioca/server_addon.rb +++ b/lib/ruby_lsp/tapioca/server_addon.rb @@ -2,7 +2,6 @@ # frozen_string_literal: true require "tapioca/internal" -require_relative "lockfile_diff_parser" module RubyLsp module Tapioca @@ -17,8 +16,6 @@ def execute(request, params) fork do dsl(params) end - when "gem" - gem(params) end end @@ -28,20 +25,6 @@ def dsl(params) load("tapioca/cli.rb") # Reload the CLI to reset thor defaults between requests ::Tapioca::Cli.start(["dsl", "--lsp_addon", "--workers=1"] + params[:constants]) end - - def gem(params) - gem_changes = LockfileDiffParser.new(params[:diff]) - - removed_gems = gem_changes.removed_gems - added_or_modified_gems = gem_changes.added_or_modified_gems - - if added_or_modified_gems.any? - load("tapioca/cli.rb") # Reload the CLI to reset thor defaults between requests - ::Tapioca::Cli.start(["gem"] + added_or_modified_gems) - elsif removed_gems.any? - FileUtils.rm_f(Dir.glob("sorbet/rbi/gems/{#{removed_gems.join(",")}}@*.rbi")) - end - end end end end From b498fecf0c1bc7a640962b16c1f5d96eb685ad9a Mon Sep 17 00:00:00 2001 From: Alex Rocha Date: Mon, 9 Dec 2024 20:30:10 -0800 Subject: [PATCH 07/11] Add lsp_addon flag to tapioca gem command --- lib/tapioca/cli.rb | 6 ++++++ lib/tapioca/commands/abstract_gem.rb | 6 +++++- lib/tapioca/gem/pipeline.rb | 5 ++++- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/lib/tapioca/cli.rb b/lib/tapioca/cli.rb index 33576f3c5..46699058c 100644 --- a/lib/tapioca/cli.rb +++ b/lib/tapioca/cli.rb @@ -266,6 +266,11 @@ def dsl(*constant_or_paths) type: :boolean, desc: "Halt upon a load error while loading the Rails application", default: true + option :lsp_addon, + type: :boolean, + desc: "Generate Gem RBIs from the LSP addon. Internal to tapioca and not intended for end-users", + default: false, + hide: true def gem(*gems) set_environment(options) @@ -300,6 +305,7 @@ def gem(*gems) dsl_dir: options[:dsl_dir], rbi_formatter: rbi_formatter(options), halt_upon_load_error: options[:halt_upon_load_error], + lsp_addon: options[:lsp_addon], } command = if verify diff --git a/lib/tapioca/commands/abstract_gem.rb b/lib/tapioca/commands/abstract_gem.rb index c4752730d..66dc0c2fe 100644 --- a/lib/tapioca/commands/abstract_gem.rb +++ b/lib/tapioca/commands/abstract_gem.rb @@ -27,6 +27,7 @@ class AbstractGem < Command dsl_dir: String, rbi_formatter: RBIFormatter, halt_upon_load_error: T::Boolean, + lsp_addon: T.nilable(T::Boolean), ).void end def initialize( @@ -45,7 +46,8 @@ def initialize( auto_strictness: true, dsl_dir: DEFAULT_DSL_DIR, rbi_formatter: DEFAULT_RBI_FORMATTER, - halt_upon_load_error: true + halt_upon_load_error: true, + lsp_addon: false ) @gem_names = gem_names @exclude = exclude @@ -59,6 +61,7 @@ def initialize( @auto_strictness = auto_strictness @dsl_dir = dsl_dir @rbi_formatter = rbi_formatter + @lsp_addon = lsp_addon super() @@ -126,6 +129,7 @@ def compile_gem_rbi(gem) error_handler: ->(error) { say_error(error, :bold, :red) }, + lsp_addon: T.must(@lsp_addon), ).compile end diff --git a/lib/tapioca/gem/pipeline.rb b/lib/tapioca/gem/pipeline.rb index 2b1b655d3..15618e5a4 100644 --- a/lib/tapioca/gem/pipeline.rb +++ b/lib/tapioca/gem/pipeline.rb @@ -22,19 +22,22 @@ class Pipeline error_handler: T.proc.params(error: String).void, include_doc: T::Boolean, include_loc: T::Boolean, + lsp_addon: T::Boolean, ).void end def initialize( gem, error_handler:, include_doc: false, - include_loc: false + include_loc: false, + lsp_addon: false ) @root = T.let(RBI::Tree.new, RBI::Tree) @gem = gem @seen = T.let(Set.new, T::Set[String]) @alias_namespace = T.let(Set.new, T::Set[String]) @error_handler = error_handler + @lsp_addon = lsp_addon @events = T.let([], T::Array[Gem::Event]) From 392e86de5a8054112592b7a5430382a18306b067 Mon Sep 17 00:00:00 2001 From: Alex Rocha Date: Mon, 9 Dec 2024 20:41:11 -0800 Subject: [PATCH 08/11] Lazily boot the rails app during tapioca gem Co-authored-by: Kaan Ozkan --- lib/tapioca/commands/abstract_gem.rb | 1 + lib/tapioca/commands/gem_generate.rb | 1 + lib/tapioca/loaders/gem.rb | 5 +- lib/tapioca/loaders/loader.rb | 128 +++------------------------ spec/tapioca/cli/gem_spec.rb | 23 +++++ 5 files changed, 41 insertions(+), 117 deletions(-) diff --git a/lib/tapioca/commands/abstract_gem.rb b/lib/tapioca/commands/abstract_gem.rb index 66dc0c2fe..e0cad5c09 100644 --- a/lib/tapioca/commands/abstract_gem.rb +++ b/lib/tapioca/commands/abstract_gem.rb @@ -196,6 +196,7 @@ def perform_additions postrequire: @postrequire, default_command: default_command(:require), halt_upon_load_error: @halt_upon_load_error, + lsp_addon: T.must(@lsp_addon), ) Executor.new(gems, number_of_workers: @number_of_workers).run_in_parallel do |gem_name| diff --git a/lib/tapioca/commands/gem_generate.rb b/lib/tapioca/commands/gem_generate.rb index bd1c740a7..0523a4719 100644 --- a/lib/tapioca/commands/gem_generate.rb +++ b/lib/tapioca/commands/gem_generate.rb @@ -14,6 +14,7 @@ def execute postrequire: @postrequire, default_command: default_command(:require), halt_upon_load_error: @halt_upon_load_error, + lsp_addon: T.must(@lsp_addon), ) gem_queue = gems_to_generate(@gem_names).reject { |gem| @exclude.include?(gem.name) } diff --git a/lib/tapioca/loaders/gem.rb b/lib/tapioca/loaders/gem.rb index 150593f1e..0a5cf8728 100644 --- a/lib/tapioca/loaders/gem.rb +++ b/lib/tapioca/loaders/gem.rb @@ -16,9 +16,12 @@ class << self postrequire: String, default_command: String, halt_upon_load_error: T::Boolean, + lsp_addon: T::Boolean, ).void end - def load_application(bundle:, prerequire:, postrequire:, default_command:, halt_upon_load_error:) + def load_application(bundle:, prerequire:, postrequire:, default_command:, halt_upon_load_error:, lsp_addon:) + return if lsp_addon + loader = new( bundle: bundle, prerequire: prerequire, diff --git a/lib/tapioca/loaders/loader.rb b/lib/tapioca/loaders/loader.rb index a1242aefb..9a43bbd6d 100644 --- a/lib/tapioca/loaders/loader.rb +++ b/lib/tapioca/loaders/loader.rb @@ -24,18 +24,21 @@ def load; end initialize_file: T.nilable(String), require_file: T.nilable(String), halt_upon_load_error: T::Boolean, + lsp_addon: T::Boolean, ).void end - def load_bundle(gemfile, initialize_file, require_file, halt_upon_load_error) + def load_bundle(gemfile, initialize_file, require_file, halt_upon_load_error, lsp_addon: false) require_helper(initialize_file) - load_rails_application(halt_upon_load_error: halt_upon_load_error) - gemfile.require_bundle - require_helper(require_file) + load_rails_application( + environment_load: true, + halt_upon_load_error: halt_upon_load_error, + lsp_addon: lsp_addon, + ) - load_rails_engines + require_helper(require_file) end sig do @@ -44,9 +47,12 @@ def load_bundle(gemfile, initialize_file, require_file, halt_upon_load_error) eager_load: T::Boolean, app_root: String, halt_upon_load_error: T::Boolean, + lsp_addon: T::Boolean, ).void end - def load_rails_application(environment_load: false, eager_load: false, app_root: ".", halt_upon_load_error: true) + def load_rails_application(environment_load: false, eager_load: false, app_root: ".", halt_upon_load_error: true, + lsp_addon: false) + return if lsp_addon return unless File.exist?(File.expand_path("config/application.rb", app_root)) load_path = if environment_load @@ -85,116 +91,6 @@ def load_rails_application(environment_load: false, eager_load: false, app_root: say("Continuing RBI generation without loading the Rails application.") end - sig { void } - def load_rails_engines - return if engines.empty? - - with_rails_application do - run_initializers - - if zeitwerk_mode? - load_engines_in_zeitwerk_mode - else - load_engines_in_classic_mode - end - end - end - - def run_initializers - engines.each do |engine| - engine.instance.initializers.tsort_each do |initializer| - initializer.run(Rails.application) - rescue ScriptError, StandardError - nil - end - end - end - - sig { void } - def load_engines_in_zeitwerk_mode - # Collect all the directories that are already managed by all existing Zeitwerk loaders. - managed_dirs = Zeitwerk::Registry.loaders.flat_map(&:dirs).to_set - # We use a fresh loader to load the engine directories, so that we don't interfere with - # any of the existing loaders. - autoloader = Zeitwerk::Loader.new - - engines.each do |engine| - eager_load_paths(engine).each do |path| - # Zeitwerk only accepts existing directories in `push_dir`. - next unless File.directory?(path) - # We should not add directories that are already managed by a Zeitwerk loader. - next if managed_dirs.member?(path) - - autoloader.push_dir(path) - end - end - - autoloader.setup - end - - sig { void } - def load_engines_in_classic_mode - # This is code adapted from `Rails::Engine#eager_load!` in - # https://github.com/rails/rails/blob/d9e188dbab81b412f73dfb7763318d52f360af49/railties/lib/rails/engine.rb#L489-L495 - # - # We can't use `Rails::Engine#eager_load!` directly because it will raise as soon as it encounters - # an error, which is not what we want. We want to try to load as much as we can. - engines.each do |engine| - eager_load_paths(engine).each do |load_path| - Dir.glob("#{load_path}/**/*.rb").sort.each do |file| - require_dependency file - end - rescue ScriptError, StandardError - nil - end - end - end - - sig { returns(T::Boolean) } - def zeitwerk_mode? - Rails.respond_to?(:autoloaders) && - Rails.autoloaders.respond_to?(:zeitwerk_enabled?) && - Rails.autoloaders.zeitwerk_enabled? - end - - sig { params(blk: T.proc.void).void } - def with_rails_application(&blk) - # Store the current Rails.application object so that we can restore it - rails_application = T.unsafe(Rails.application) - - # Create a new Rails::Application object, so that we can load the engines. - # Some engines and the `Rails.autoloaders` call might expect `Rails.application` - # to be set, so we need to create one here. - unless rails_application - Rails.application = Class.new(Rails::Application) - end - - blk.call - ensure - Rails.app_class = Rails.application = rails_application - end - - T::Sig::WithoutRuntime.sig { returns(T::Array[T.class_of(Rails::Engine)]) } - def engines - return [] unless defined?(Rails::Engine) - - safe_require("active_support/core_ext/class/subclasses") - - project_path = Bundler.default_gemfile.parent.expand_path - # We can use `Class#descendants` here, since we know Rails is loaded - Rails::Engine - .descendants - .reject(&:abstract_railtie?) - .reject { |engine| gem_in_app_dir?(project_path, engine.config.root.to_path) } - end - - sig { params(path: String).void } - def safe_require(path) - require path - rescue LoadError - nil - end - sig { void } def eager_load_rails_app application = Rails.application diff --git a/spec/tapioca/cli/gem_spec.rb b/spec/tapioca/cli/gem_spec.rb index ce5276092..4e412e6b2 100644 --- a/spec/tapioca/cli/gem_spec.rb +++ b/spec/tapioca/cli/gem_spec.rb @@ -1268,6 +1268,19 @@ class Post RB end + @project.write!("config/application.rb", <<~RB) + module Tapioca + class Application < Rails::Application + config.load_defaults(#{ActiveSupport.gem_version.to_s[0..2]}) + end + end + RB + + @project.write!("config/environment.rb", <<~RB) + require_relative "application" + Rails.application.initialize! + RB + @project.require_real_gem("rails", ActiveSupport.gem_version.to_s) @project.require_mock_gem(foo) @project.bundle_install! @@ -1325,6 +1338,11 @@ class Application < Rails::Application end RB + @project.write!("config/environment.rb", <<~RB) + require_relative "application" + Rails.application.initialize! + RB + response = @project.tapioca("gem turbo-rails") assert_includes(response.out, "Compiled turbo-rails") @@ -1356,6 +1374,11 @@ class Application < Rails::Application end RB + @project.write!("config/environment.rb", <<~RB) + require_relative "application" + Rails.application.initialize! + RB + response = @project.tapioca("gem turbo-rails") assert_includes(response.out, "Compiled turbo-rails") From 712ede4388e05e01c36f886359ecef9a6292876e Mon Sep 17 00:00:00 2001 From: Alex Rocha Date: Mon, 9 Dec 2024 20:47:20 -0800 Subject: [PATCH 09/11] Move gem RBI generation to server addon --- lib/ruby_lsp/tapioca/addon.rb | 33 +++++----------------------- lib/ruby_lsp/tapioca/server_addon.rb | 21 ++++++++++++++++++ 2 files changed, 26 insertions(+), 28 deletions(-) diff --git a/lib/ruby_lsp/tapioca/addon.rb b/lib/ruby_lsp/tapioca/addon.rb index be59c8b82..d2e755db6 100644 --- a/lib/ruby_lsp/tapioca/addon.rb +++ b/lib/ruby_lsp/tapioca/addon.rb @@ -14,7 +14,6 @@ require "zlib" require "open3" -require_relative "lockfile_diff_parser" module RubyLsp module Tapioca @@ -154,33 +153,11 @@ def fetch_lockfile_diff sig { void } def generate_gem_rbis - gem_changes = LockfileDiffParser.new(@lockfile_diff) - - removed_gems = gem_changes.removed_gems - added_or_modified_gems = gem_changes.added_or_modified_gems - - if added_or_modified_gems.any? - # Resetting BUNDLE_GEMFILE to root folder to use the project's Gemfile instead of Ruby LSP's composed Gemfile - stdout, stderr, status = T.unsafe(Open3).capture3( - { "BUNDLE_GEMFILE" => "Gemfile" }, - "bin/tapioca", - "gem", - *added_or_modified_gems, - ) - T.must(@outgoing_queue) << if status.success? - Notification.window_log_message( - stdout, - type: Constant::MessageType::INFO, - ) - else - Notification.window_log_message( - stderr, - type: Constant::MessageType::ERROR, - ) - end - elsif removed_gems.any? - FileUtils.rm_f(Dir.glob("sorbet/rbi/gems/{#{removed_gems.join(",")}}@*.rbi")) - end + T.must(@rails_runner_client).delegate_notification( + server_addon_name: "Tapioca", + request_name: "gem", + diff: T.must(@lockfile_diff), + ) end sig { void } diff --git a/lib/ruby_lsp/tapioca/server_addon.rb b/lib/ruby_lsp/tapioca/server_addon.rb index 0d38cf93d..45219fd27 100644 --- a/lib/ruby_lsp/tapioca/server_addon.rb +++ b/lib/ruby_lsp/tapioca/server_addon.rb @@ -2,6 +2,7 @@ # frozen_string_literal: true require "tapioca/internal" +require_relative "lockfile_diff_parser" module RubyLsp module Tapioca @@ -16,6 +17,8 @@ def execute(request, params) fork do dsl(params) end + when "gem" + gem(params) end end @@ -25,6 +28,24 @@ def dsl(params) load("tapioca/cli.rb") # Reload the CLI to reset thor defaults between requests ::Tapioca::Cli.start(["dsl", "--lsp_addon", "--workers=1"] + params[:constants]) end + + def gem(params) + gem_changes = LockfileDiffParser.new(params[:diff]) + + removed_gems = gem_changes.removed_gems + added_or_modified_gems = gem_changes.added_or_modified_gems + + if added_or_modified_gems.any? + ::Tapioca::Cli.start([ + "gem", + "--lsp_addon", + *added_or_modified_gems, + ]) + elsif removed_gems.any? + FileUtils.rm_f(Dir.glob("sorbet/rbi/gems/{#{removed_gems.join(",")}}@*.rbi")) + $stdout.puts "Removed RBIs for: #{removed_gems.join(", ")}" + end + end end end end From 49d97aa3a4ba6ad7071858aedcf4774ac37f7d5b Mon Sep 17 00:00:00 2001 From: Alex Rocha Date: Mon, 9 Dec 2024 20:49:53 -0800 Subject: [PATCH 10/11] Handle missing gems gracefully in tapioca gem command --- lib/tapioca/commands/abstract_gem.rb | 2 ++ spec/tapioca/cli/gem_spec.rb | 16 ++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/lib/tapioca/commands/abstract_gem.rb b/lib/tapioca/commands/abstract_gem.rb index e0cad5c09..6ffcfd5b1 100644 --- a/lib/tapioca/commands/abstract_gem.rb +++ b/lib/tapioca/commands/abstract_gem.rb @@ -84,6 +84,8 @@ def gems_to_generate(gem_names) gem = @bundle.gem(gem_name) if gem.nil? + next say("Warning: Cannot find gem '#{gem_name}', skipping", :yellow) if @lsp_addon + raise Thor::Error, set_color("Error: Cannot find gem '#{gem_name}'", :red) end diff --git a/spec/tapioca/cli/gem_spec.rb b/spec/tapioca/cli/gem_spec.rb index 4e412e6b2..e97adcf7c 100644 --- a/spec/tapioca/cli/gem_spec.rb +++ b/spec/tapioca/cli/gem_spec.rb @@ -824,6 +824,22 @@ class Secret; end assert_success_status(result) end + it "skips missing gems and continues with warning when --lsp_addon is used" do + result = @project.tapioca("gem non_existent_gem --lsp_addon") + + assert_stdout_includes(result, "Warning: Cannot find gem 'non_existent_gem', skipping") + + assert_empty_stderr(result) + assert_success_status(result) + end + + it "fails with error when gem cannot be found" do + result = @project.tapioca("gem non_existent_gem") + + assert_stderr_includes(result, "Error: Cannot find gem 'non_existent_gem'") + refute_success_status(result) + end + it "does not crash when the extras gem is loaded" do foo = mock_gem("foo", "0.0.1") do write!("lib/foo.rb", FOO_RB) From 490ea902a39974c3d42d1d0d51d6f6cac51c7615 Mon Sep 17 00:00:00 2001 From: Alex Rocha Date: Mon, 9 Dec 2024 20:50:34 -0800 Subject: [PATCH 11/11] Clean up git repo check --- lib/ruby_lsp/tapioca/addon.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ruby_lsp/tapioca/addon.rb b/lib/ruby_lsp/tapioca/addon.rb index d2e755db6..8be57fd62 100644 --- a/lib/ruby_lsp/tapioca/addon.rb +++ b/lib/ruby_lsp/tapioca/addon.rb @@ -136,7 +136,7 @@ def file_updated?(change, path) sig { returns(T.nilable(T::Boolean)) } def git_repo? - _, _, status = Open3.capture3("git rev-parse --is-inside-work-tree") + _, status = Open3.capture2e("git rev-parse --is-inside-work-tree") status.success? end