From 662dfba1dffbffb3aa249133772ab97be0899177 Mon Sep 17 00:00:00 2001 From: Andy Waite Date: Fri, 16 Feb 2024 13:31:11 -0500 Subject: [PATCH] Automatically install ruby-lsp-rails as part of custom bundle --- lib/ruby_lsp/setup_bundler.rb | 52 ++++++++++++++-------- test/setup_bundler_test.rb | 81 ++++++++++++++++++++++++++++++++++- 2 files changed, 114 insertions(+), 19 deletions(-) diff --git a/lib/ruby_lsp/setup_bundler.rb b/lib/ruby_lsp/setup_bundler.rb index d32f2e043..90563f868 100644 --- a/lib/ruby_lsp/setup_bundler.rb +++ b/lib/ruby_lsp/setup_bundler.rb @@ -57,15 +57,18 @@ def initialize(project_path, **options) def setup! raise BundleNotLocked if @gemfile&.exist? && !@lockfile&.exist? - # Do not setup a custom bundle if both `ruby-lsp` and `debug` are already in the Gemfile - if @dependencies["ruby-lsp"] && @dependencies["debug"] + # Do not set up a custom bundle if LSP dependencies are already in the Gemfile + if @dependencies["ruby-lsp"] && + @dependencies["debug"] && + (@dependencies["rails"] ? @dependencies["ruby-lsp-rails"] : true) $stderr.puts( - "Ruby LSP> Skipping custom bundle setup since both `ruby-lsp` and `debug` are already in #{@gemfile}", + "Ruby LSP> Skipping custom bundle setup since LSP dependencies are already in #{@gemfile}", ) - # If the user decided to add the `ruby-lsp` and `debug` to their Gemfile after having already run the Ruby LSP, - # then we need to remove the `.ruby-lsp` folder, otherwise we will run `bundle install` for the top level and - # try to execute the Ruby LSP using the custom bundle, which will fail since the gems are not installed there + # If the user decided to add `ruby-lsp` and `debug` (and potentially `ruby-lsp-rails`) to their Gemfile after + # having already run the Ruby LSP, then we need to remove the `.ruby-lsp` folder, otherwise we will run `bundle + # install` for the top level and try to execute the Ruby LSP using the custom bundle, which will fail since the + # gems are not installed there @custom_dir.rmtree if @custom_dir.exist? return run_bundle_install end @@ -143,6 +146,10 @@ def write_custom_gemfile parts << 'gem "debug", require: false, group: :development, platforms: :mri' end + if @dependencies["rails"] && !@dependencies["ruby-lsp-rails"] + parts << 'gem "ruby-lsp-rails", require: false, group: :development' + end + content = parts.join("\n") @custom_gemfile.write(content) unless @custom_gemfile.exist? && @custom_gemfile.read == content end @@ -183,22 +190,24 @@ def run_bundle_install(bundle_gemfile = @gemfile) local_config_path = File.join(Dir.pwd, ".bundle") env["BUNDLE_APP_CONFIG"] = local_config_path if Dir.exist?(local_config_path) - # If both `ruby-lsp` and `debug` are already in the Gemfile, then we shouldn't try to upgrade them or else we'll - # produce undesired source control changes. If the custom bundle was just created and either `ruby-lsp` or `debug` - # weren't a part of the Gemfile, then we need to run `bundle install` for the first time to generate the - # Gemfile.lock with them included or else Bundler will complain that they're missing. We can only update if the - # custom `.ruby-lsp/Gemfile.lock` already exists and includes both gems + # If `ruby-lsp` and `debug` (and potentially `ruby-lsp-rails`) are already in the Gemfile, then we shouldn't try + # to upgrade them or else we'll produce undesired source control changes. If the custom bundle was just created + # and any of `ruby-lsp`, `ruby-lsp-rails` or `debug` weren't a part of the Gemfile, then we need to run `bundle + # install` for the first time to generate the Gemfile.lock with them included or else Bundler will complain that + # they're missing. We can only update if the custom `.ruby-lsp/Gemfile.lock` already exists and includes all gems # When not updating, we run `(bundle check || bundle install)` # When updating, we run `((bundle check && bundle update ruby-lsp debug) || bundle install)` command = +"(bundle check" if should_bundle_update? - # If ruby-lsp or debug are not in the Gemfile, try to update them to the latest version + # If any of `ruby-lsp`, `ruby-lsp-rails` or `debug` are not in the Gemfile, try to update them to the latest + # version command.prepend("(") command << " && bundle update " command << "ruby-lsp " unless @dependencies["ruby-lsp"] command << "debug " unless @dependencies["debug"] + command << "ruby-lsp-rails " if @dependencies["rails"] && !@dependencies["ruby-lsp-rails"] command << "--pre" if @experimental command.delete_suffix!(" ") command << ")" @@ -221,13 +230,20 @@ def run_bundle_install(bundle_gemfile = @gemfile) sig { returns(T::Boolean) } def should_bundle_update? - # If both `ruby-lsp` and `debug` are in the Gemfile, then we shouldn't try to upgrade them or else it will produce - # version control changes - return false if @dependencies["ruby-lsp"] && @dependencies["debug"] + # If `ruby-lsp`, `ruby-lsp-rails` and `debug` are in the Gemfile, then we shouldn't try to upgrade them or else it + # will produce version control changes + if @dependencies["rails"] + return false if @dependencies.values_at("ruby-lsp", "ruby-lsp-rails", "debug").all? + + # If the custom lockfile doesn't include `ruby-lsp`, `ruby-lsp-rails` or `debug`, we need to run bundle install + # before updating + return false if custom_bundle_dependencies.values_at("ruby-lsp", "debug", "ruby-lsp-rails").any?(&:nil?) + else + return false if @dependencies.values_at("ruby-lsp", "debug").all? - # If the custom lockfile doesn't include either the `ruby-lsp` or `debug`, we need to run bundle install before - # updating - return false if custom_bundle_dependencies["ruby-lsp"].nil? || custom_bundle_dependencies["debug"].nil? + # If the custom lockfile doesn't include `ruby-lsp` or `debug`, we need to run bundle install before updating + return false if custom_bundle_dependencies.values_at("ruby-lsp", "debug").any?(&:nil?) + end # If the last updated file doesn't exist or was updated more than 4 hours ago, we should update !@last_updated_path.exist? || Time.parse(@last_updated_path.read) < (Time.now - FOUR_HOURS) diff --git a/test/setup_bundler_test.rb b/test/setup_bundler_test.rb index a26ce4839..cd2e85776 100644 --- a/test/setup_bundler_test.rb +++ b/test/setup_bundler_test.rb @@ -12,6 +12,18 @@ def test_does_nothing_if_both_ruby_lsp_and_debug_are_in_the_bundle refute_path_exists(".ruby-lsp") end + def test_does_nothing_if_both_ruby_lsp_and_debug_are_in_the_bundle2 + Object.any_instance.expects(:system).with(bundle_env, "(bundle check || bundle install) 1>&2") + Bundler::LockfileParser.any_instance.expects(:dependencies).returns({ + "ruby-lsp" => true, + "rails" => true, + "ruby-lsp-rails" => true, + "debug" => true, + }) + run_script + refute_path_exists(".ruby-lsp") + end + def test_removes_ruby_lsp_folder_if_both_gems_were_added_to_the_bundle Object.any_instance.expects(:system).with(bundle_env, "(bundle check || bundle install) 1>&2") Bundler::LockfileParser.any_instance.expects(:dependencies).returns({ "ruby-lsp" => true, "debug" => true }) @@ -22,6 +34,27 @@ def test_removes_ruby_lsp_folder_if_both_gems_were_added_to_the_bundle FileUtils.rm_r(".ruby-lsp") if Dir.exist?(".ruby-lsp") end + def test_in_a_rails_app_does_nothing_if_ruby_lsp_and_ruby_lsp_rails_and_debug_are_in_the_bundle + Object.any_instance.expects(:system).with(bundle_env, "(bundle check || bundle install) 1>&2") + Bundler::LockfileParser.any_instance.expects(:dependencies) + .returns({ "ruby-lsp" => true, "ruby-lsp-rails" => true, "debug" => true }) + run_script + refute_path_exists(".ruby-lsp") + ensure + FileUtils.rm_r(".ruby-lsp") if Dir.exist?(".ruby-lsp") + end + + def test_in_a_rails_app_removes_ruby_lsp_folder_if_all_gems_were_added_to_the_bundle + Object.any_instance.expects(:system).with(bundle_env, "(bundle check || bundle install) 1>&2") + Bundler::LockfileParser.any_instance.expects(:dependencies) + .returns({ "ruby-lsp" => true, "ruby-lsp-rails" => true, "debug" => true }) + FileUtils.mkdir(".ruby-lsp") + run_script + refute_path_exists(".ruby-lsp") + ensure + FileUtils.rm_r(".ruby-lsp") if Dir.exist?(".ruby-lsp") + end + def test_creates_custom_bundle Object.any_instance.expects(:system).with(bundle_env(".ruby-lsp/Gemfile"), "(bundle check || bundle install) 1>&2") Bundler::LockfileParser.any_instance.expects(:dependencies).returns({}).at_least_once @@ -32,9 +65,26 @@ def test_creates_custom_bundle assert_path_exists(".ruby-lsp/Gemfile.lock") assert_path_exists(".ruby-lsp/main_lockfile_hash") assert_match("ruby-lsp", File.read(".ruby-lsp/Gemfile")) + refute_match("ruby-lsp-rails", File.read(".ruby-lsp/Gemfile")) assert_match("debug", File.read(".ruby-lsp/Gemfile")) ensure - FileUtils.rm_r(".ruby-lsp") + FileUtils.rm_r(".ruby-lsp") if Dir.exist?(".ruby-lsp") + end + + def test_creates_custom_bundle_for_a_rails_app + Object.any_instance.expects(:system).with(bundle_env(".ruby-lsp/Gemfile"), "(bundle check || bundle install) 1>&2") + Bundler::LockfileParser.any_instance.expects(:dependencies).returns({ "rails" => true }).at_least_once + run_script + + assert_path_exists(".ruby-lsp") + assert_path_exists(".ruby-lsp/Gemfile") + assert_path_exists(".ruby-lsp/Gemfile.lock") + assert_path_exists(".ruby-lsp/main_lockfile_hash") + assert_match("ruby-lsp", File.read(".ruby-lsp/Gemfile")) + assert_match("debug", File.read(".ruby-lsp/Gemfile")) + assert_match("ruby-lsp-rails", File.read(".ruby-lsp/Gemfile")) + ensure + FileUtils.rm_r(".ruby-lsp") if Dir.exist?(".ruby-lsp") end def test_changing_lockfile_causes_custom_bundle_to_be_rebuilt @@ -422,6 +472,35 @@ def test_ensures_lockfile_remotes_are_relative_to_default_gemfile end end + def test_ruby_lsp_rails_is_automatically_included_in_rails_apps + Dir.mktmpdir do |dir| + Dir.chdir(dir) do + File.write(File.join(dir, "Gemfile"), <<~GEMFILE) + source "https://rubygems.org" + gem "rails" + GEMFILE + + capture_subprocess_io do + Bundler.with_unbundled_env do + # Run bundle install to generate the lockfile + system("bundle install") + end + end + + Object.any_instance.expects(:system).with( + bundle_env(".ruby-lsp/Gemfile"), + "(bundle check || bundle install) 1>&2", + ) + Bundler.with_unbundled_env do + run_script + end + + assert_path_exists(".ruby-lsp/Gemfile") + assert_match('gem "ruby-lsp-rails"', File.read(".ruby-lsp/Gemfile")) + end + end + end + private # This method runs the script and then immediately unloads it. This allows us to make assertions against the effects