diff --git a/lib/ruby_lsp/server.rb b/lib/ruby_lsp/server.rb index c4f2930fd..52cd25d32 100644 --- a/lib/ruby_lsp/server.rb +++ b/lib/ruby_lsp/server.rb @@ -1292,6 +1292,13 @@ def compose_bundle(message) command = "#{Gem.ruby} #{File.expand_path("../../exe/ruby-lsp-launcher", __dir__)} #{@global_state.workspace_uri}" id = message[:id] + begin + Bundler::LockfileParser.new(Bundler.default_lockfile.read) + rescue Bundler::LockfileError => e + send_message(Error.new(id: id, code: BUNDLE_COMPOSE_FAILED_CODE, message: e.message)) + return + end + # We compose the bundle in a thread so that the LSP continues to work while we're checking for its validity. Once # we return the response back to the editor, then the restart is triggered Thread.new do diff --git a/test/server_test.rb b/test/server_test.rb index 1bc287822..d4781e595 100644 --- a/test/server_test.rb +++ b/test/server_test.rb @@ -1056,6 +1056,55 @@ def test_compose_bundle_creates_file_to_skip_next_compose end end + def test_compose_bundle_detects_syntax_errors_in_lockfile + Dir.mktmpdir do |dir| + Dir.chdir(dir) do + @server.process_message({ + id: 1, + method: "initialize", + params: { + initializationOptions: {}, + capabilities: { general: { positionEncodings: ["utf-8"] } }, + workspaceFolders: [{ uri: URI::Generic.from_path(path: dir).to_s }], + }, + }) + + File.write(File.join(dir, "Gemfile"), <<~GEMFILE) + source "https://rubygems.org" + gem "stringio" + GEMFILE + + # Write a lockfile that has a git conflict marker + lockfile_contents = <<~LOCKFILE + GEM + remote: https://rubygems.org/ + specs: + <<<<<<< HEAD + stringio (3.1.0) + >>>>>> 12345 + + PLATFORMS + arm64-darwin-23 + ruby + + DEPENDENCIES + stringio + + BUNDLED WITH + 2.5.7 + LOCKFILE + File.write(File.join(dir, "Gemfile.lock"), lockfile_contents) + + Bundler.with_unbundled_env do + @server.process_message({ id: 2, method: "rubyLsp/composeBundle" }) + end + + error = find_message(RubyLsp::Error) + assert_match("Your Gemfile.lock contains merge conflicts.", error.message) + end + end + end + private def with_uninstalled_rubocop(&block)