Skip to content

Avoid requiring add-ons that are copied into the workspace #3669

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 19 additions & 4 deletions lib/ruby_lsp/addon.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,26 @@ def load_addons(global_state, outgoing_queue, include_project_addons: true)

if include_project_addons
project_addons = Dir.glob("#{global_state.workspace_path}/**/ruby_lsp/**/addon.rb")

# Ignore add-ons from dependencies if the bundle is stored inside the project. We already found those with
# `Gem.find_files`
bundle_path = Bundler.bundle_path.to_s
project_addons.reject! { |path| path.start_with?(bundle_path) }
gems_dir = Bundler.bundle_path.join("gems")

# Create an array of rejection glob patterns to ignore add-ons already discovered through Gem.find_files if
# they are also copied inside the workspace for whatever reason. We received reports of projects having gems
# installed in vendor/bundle despite BUNDLE_PATH pointing elsewhere. Without this mechanism, we will
# double-require the same add-on, potentially for different versions of the same gem, which leads to incorrect
# behavior
reject_glob_patterns = addon_files.map do |path|
relative_gem_path = Pathname.new(path).relative_path_from(gems_dir)
first_part, *parts = relative_gem_path.to_s.split(File::SEPARATOR)
first_part&.gsub!(/-([0-9.]+)$/, "*")
"**/#{first_part}/#{parts.join("/")}"
end

project_addons.reject! do |path|
path.start_with?(bundle_path) ||
reject_glob_patterns.any? { |pattern| File.fnmatch?(pattern, path, File::Constants::FNM_PATHNAME) }
end

addon_files.concat(project_addons)
end

Expand Down
34 changes: 34 additions & 0 deletions test/addon_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -225,5 +225,39 @@ def version
end
end
end

def test_loading_project_addons_ignores_vendor_bundle
# Some users have gems installed under `vendor/bundle` despite not having their BUNDLE_PATH configured to be so.
# That leads to loading the same add-on multiple times if they have the same gem installed both in their
# BUNDLE_PATH and in `vendor/bundle`
Dir.mktmpdir do |dir|
addon_dir = File.join(dir, "vendor", "bundle", "rubocop-1.73.0", "lib", "ruby_lsp", "rubocop")
FileUtils.mkdir_p(addon_dir)
File.write(File.join(addon_dir, "addon.rb"), <<~RUBY)
class OldRuboCopAddon < RubyLsp::Addon
def activate(global_state, outgoing_queue)
end

def name
"Old RuboCop Addon"
end

def version
"0.1.0"
end
end
RUBY

@global_state.apply_options({
workspaceFolders: [{ uri: URI::Generic.from_path(path: dir).to_s }],
})

Addon.load_addons(@global_state, @outgoing_queue)

assert_raises(Addon::AddonNotFoundError) do
Addon.get("Project Addon", "0.1.0")
end
end
end
end
end
Loading