Skip to content

Commit

Permalink
Expand Centralized Ecosystem Format with Requirements Information for…
Browse files Browse the repository at this point in the history
… Bundler Package Manager (#10897)

* added package manager version requirement for bundler
  • Loading branch information
kbukum1 authored Nov 7, 2024
1 parent 9a1500e commit 26692aa
Show file tree
Hide file tree
Showing 10 changed files with 445 additions and 28 deletions.
6 changes: 5 additions & 1 deletion bundler/lib/dependabot/bundler/file_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,11 @@ def ecosystem

sig { returns(Ecosystem::VersionManager) }
def package_manager
@package_manager ||= PackageManager.new(bundler_raw_version)
@package_manager ||= PackageManager.new(bundler_raw_version, package_manager_requirement)
end

def package_manager_requirement
@package_manager_requirement ||= Helpers.bundler_dependency_requirement(dependency_files)
end

sig { returns(T.nilable(Ecosystem::VersionManager)) }
Expand Down
75 changes: 71 additions & 4 deletions bundler/lib/dependabot/bundler/helpers.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# typed: strong
# frozen_string_literal: true

require "dependabot/bundler/requirement"

module Dependabot
module Bundler
module Helpers
Expand All @@ -9,16 +11,18 @@ module Helpers

V1 = "1"
V2 = "2"
# If we are updating a project with no Gemfile.lock, we default to the
# newest version we support
DEFAULT = V2
BUNDLER_MAJOR_VERSION_REGEX = /BUNDLED WITH\s+(?<version>\d+)\./m

GEMFILE = "Gemfile"
GEMSPEC_EXTENSION = ".gemspec"
BUNDLER_GEM_NAME = "bundler"

sig { params(lockfile: T.nilable(Dependabot::DependencyFile)).returns(String) }
def self.bundler_version(lockfile)
return DEFAULT unless lockfile

if (matches = lockfile.content&.match(BUNDLER_MAJOR_VERSION_REGEX))
if (matches = T.let(lockfile.content, T.nilable(String))&.match(BUNDLER_MAJOR_VERSION_REGEX))
matches[:version].to_i >= 2 ? V2 : V1
else
DEFAULT
Expand All @@ -29,12 +33,75 @@ def self.bundler_version(lockfile)
def self.detected_bundler_version(lockfile)
return "unknown" unless lockfile

if (matches = lockfile.content&.match(BUNDLER_MAJOR_VERSION_REGEX))
if (matches = T.let(lockfile.content, T.nilable(String))&.match(BUNDLER_MAJOR_VERSION_REGEX))
matches[:version].to_i.to_s
else
"unspecified"
end
end

# Method to get the Requirement object for the 'bundler' dependency
sig do
params(files: T::Array[Dependabot::DependencyFile]).returns(T.nilable(Dependabot::Bundler::Requirement))
end
def self.bundler_dependency_requirement(files)
constraints = combined_dependency_constraints(files, BUNDLER_GEM_NAME)
return nil if constraints.empty?

combined_constraint = constraints.join(", ")

Dependabot::Bundler::Requirement.new(combined_constraint)
rescue StandardError => e
Dependabot.logger.error(
"Failed to create Requirement with constraints '#{constraints&.join(', ')}': #{e.message}"
)
nil
end

# Method to gather and combine constraints for a specified dependency from multiple files
sig do
params(files: T::Array[Dependabot::DependencyFile], dependency_name: String).returns(T::Array[String])
end
def self.combined_dependency_constraints(files, dependency_name)
files.each_with_object([]) do |file, result|
content = file.content
next unless content

# Select the appropriate regex based on file type
regex = if file.name.end_with?(GEMFILE)
gemfile_dependency_regex(dependency_name)
elsif file.name.end_with?(GEMSPEC_EXTENSION)
gemspec_dependency_regex(dependency_name)
else
next # Skip unsupported file types
end

# Extract constraints using the chosen regex
result.concat(extract_constraints_from_file(content, regex))
end.uniq
end

# Method to generate the regex pattern for a dependency in a Gemfile
sig { params(dependency_name: String).returns(Regexp) }
def self.gemfile_dependency_regex(dependency_name)
/gem\s+['"]#{Regexp.escape(dependency_name)}['"](?:,\s*['"]([^'"]+)['"])?/
end

# Method to generate the regex pattern for a dependency in a gemspec file
sig { params(dependency_name: String).returns(Regexp) }
def self.gemspec_dependency_regex(dependency_name)
/add_(?:runtime_)?dependency\s+['"]#{Regexp.escape(dependency_name)}['"],\s*['"]([^'"]+)['"]/
end

# Extracts constraints from file content based on a dependency regex
sig { params(content: String, regex: Regexp).returns(T::Array[String]) }
def self.extract_constraints_from_file(content, regex)
if content.match(regex)
content.scan(regex).flatten
else
[]
end
end
end
end
end
11 changes: 9 additions & 2 deletions bundler/lib/dependabot/bundler/package_manager.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
require "sorbet-runtime"
require "dependabot/bundler/version"
require "dependabot/ecosystem"
require "dependabot/bundler/requirement"

module Dependabot
module Bundler
Expand All @@ -22,13 +23,19 @@ module Bundler
class PackageManager < Dependabot::Ecosystem::VersionManager
extend T::Sig

sig { params(raw_version: String).void }
def initialize(raw_version)
sig do
params(
raw_version: String,
requirement: T.nilable(Requirement)
).void
end
def initialize(raw_version, requirement = nil)
super(
PACKAGE_MANAGER,
Version.new(raw_version),
DEPRECATED_BUNDLER_VERSIONS,
SUPPORTED_BUNDLER_VERSIONS,
requirement,
)
end
end
Expand Down
31 changes: 29 additions & 2 deletions bundler/spec/dependabot/bundler/file_parser_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -733,8 +733,35 @@
end

describe "#package_manager" do
it "returns the correct package manager" do
expect(parser.ecosystem.package_manager).to be_a(Dependabot::Bundler::PackageManager)
context "when there are no constraints" do
it "returns the correct package manager with no requirement" do
expect(parser.ecosystem.package_manager).to be_a(Dependabot::Bundler::PackageManager)
expect(parser.ecosystem.package_manager.requirement).to be_nil
end
end

context "when there are constraints" do
context "when bundler requirement specified in the Gemfile" do
let(:dependency_files) { bundler_project_dependency_files("bundler_specified") }

it "returns the correct package manager with requirement" do
expect(parser.ecosystem.package_manager).to be_a(Dependabot::Bundler::PackageManager)
expect(parser.ecosystem.package_manager.requirement).to be_a(Dependabot::Bundler::Requirement)
expect(parser.ecosystem.package_manager.requirement.min_version).to eq(Dependabot::Version.new("2.3.0"))
expect(parser.ecosystem.package_manager.requirement.max_version).to eq(Dependabot::Version.new("2.4.0"))
end
end

context "when bundler requirement specified in .gemspec" do
let(:dependency_files) { bundler_project_dependency_files("gemfile_example") }

it "returns the correct package manager with requirement" do
expect(parser.ecosystem.package_manager).to be_a(Dependabot::Bundler::PackageManager)
expect(parser.ecosystem.package_manager.requirement).to be_a(Dependabot::Bundler::Requirement)
expect(parser.ecosystem.package_manager.requirement.min_version).to eq(Dependabot::Version.new("1.12.0"))
expect(parser.ecosystem.package_manager.requirement.max_version).to be_nil
end
end
end
end
end
76 changes: 66 additions & 10 deletions bundler/spec/dependabot/bundler/helper_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,23 +61,23 @@ def described_method(lockfile)
end

it "is 2 if there is no lockfile" do
expect(described_method(no_lockfile)).to eql("2")
expect(described_method(no_lockfile)).to eq("2")
end

it "is 2 if there is no bundled with string" do
expect(described_method(lockfile_bundled_with_missing)).to eql("2")
expect(described_method(lockfile_bundled_with_missing)).to eq("2")
end

it "is 1 if it was bundled with a v1.x version" do
expect(described_method(lockfile_bundled_with_v1)).to eql("1")
expect(described_method(lockfile_bundled_with_v1)).to eq("1")
end

it "is 2 if it was bundled with a v2.x version" do
expect(described_method(lockfile_bundled_with_v2)).to eql("2")
expect(described_method(lockfile_bundled_with_v2)).to eq("2")
end

it "is 2 if it was bundled with a future version" do
expect(described_method(lockfile_bundled_with_future_version)).to eql("2")
expect(described_method(lockfile_bundled_with_future_version)).to eq("2")
end
end

Expand All @@ -87,23 +87,79 @@ def described_method(lockfile)
end

it "is unknown if there is no lockfile" do
expect(described_method(no_lockfile)).to eql("unknown")
expect(described_method(no_lockfile)).to eq("unknown")
end

it "is unspecified if there is no bundled with string" do
expect(described_method(lockfile_bundled_with_missing)).to eql("unspecified")
expect(described_method(lockfile_bundled_with_missing)).to eq("unspecified")
end

it "is 1 if it was bundled with a v1.x version" do
expect(described_method(lockfile_bundled_with_v1)).to eql("1")
expect(described_method(lockfile_bundled_with_v1)).to eq("1")
end

it "is 2 if it was bundled with a v2.x version" do
expect(described_method(lockfile_bundled_with_v2)).to eql("2")
expect(described_method(lockfile_bundled_with_v2)).to eq("2")
end

it "reports the version if it was bundled with a future version" do
expect(described_method(lockfile_bundled_with_future_version)).to eql("3")
expect(described_method(lockfile_bundled_with_future_version)).to eq("3")
end
end

describe "#combined_dependency_constraints" do
let(:gemfile_with_bundler) do
Dependabot::DependencyFile.new(name: "Gemfile", content: <<~GEMFILE)
source 'https://rubygems.org'
gem "bundler", "~> 2.3.0"
gem "rails"
GEMFILE
end

let(:gemspec_with_bundler) do
Dependabot::DependencyFile.new(name: "example.gemspec", content: <<~GEMSPEC)
Gem::Specification.new do |spec|
spec.add_runtime_dependency "bundler", ">= 1.12.0"
spec.add_dependency "rails", "~> 6.0"
end
GEMSPEC
end

it "returns constraints for bundler from Gemfile and gemspec files" do
constraints = described_class.combined_dependency_constraints(
[gemfile_with_bundler, gemspec_with_bundler],
"bundler"
)
expect(constraints).to contain_exactly("~> 2.3.0", ">= 1.12.0")
end
end

describe "#bundler_dependency_requirement" do
let(:gemfile_with_bundler) do
Dependabot::DependencyFile.new(name: "Gemfile", content: <<~GEMFILE)
source 'https://rubygems.org'
gem "bundler", "~> 2.3.0"
gem "rails"
GEMFILE
end

let(:gemspec_with_bundler) do
Dependabot::DependencyFile.new(name: "example.gemspec", content: <<~GEMSPEC)
Gem::Specification.new do |spec|
spec.add_runtime_dependency "bundler", ">= 1.12.0"
spec.add_dependency "rails", "~> 6.0"
end
GEMSPEC
end

it "returns a combined requirement for bundler from multiple files" do
requirement = described_class.bundler_dependency_requirement([gemfile_with_bundler, gemspec_with_bundler])
expect(requirement.constraints).to eq(["~> 2.3.0", ">= 1.12.0"])
end

it "returns nil if no constraints are found" do
requirement = described_class.bundler_dependency_requirement([])
expect(requirement).to be_nil
end
end
end
54 changes: 53 additions & 1 deletion bundler/spec/dependabot/bundler/package_manager_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
require "spec_helper"

RSpec.describe Dependabot::Bundler::PackageManager do
let(:package_manager) { described_class.new(version) }
let(:package_manager) { described_class.new(version, requirement) }
let(:requirement) { nil }

describe "#initialize" do
context "when version is a String" do
Expand Down Expand Up @@ -48,6 +49,57 @@
expect(package_manager.supported_versions).to eq(Dependabot::Bundler::SUPPORTED_BUNDLER_VERSIONS)
end
end

context "when a requirement is provided" do
let(:version) { "2.1" }
let(:requirement) { Dependabot::Bundler::Requirement.new(">= 1.12.0, ~> 2.3.0") }

it "sets the requirement correctly" do
expect(package_manager.requirement.to_s).to eq(">= 1.12.0, ~> 2.3.0")
end

it "calculates the correct min_version" do
expect(package_manager.requirement.min_version).to eq(Dependabot::Version.new("2.3.0"))
end

it "calculates the correct max_version" do
expect(package_manager.requirement.max_version).to eq(Dependabot::Version.new("2.4.0"))
end
end

context "when a single minimum constraint is provided" do
let(:version) { "2.1" }
let(:requirement) { Dependabot::Bundler::Requirement.new(">= 1.5") }

it "sets the requirement correctly" do
expect(package_manager.requirement.to_s).to eq(">= 1.5")
end

it "calculates the correct min_version" do
expect(package_manager.requirement.min_version).to eq(Dependabot::Version.new("1.5"))
end

it "returns nil for max_version" do
expect(package_manager.requirement.max_version).to be_nil
end
end

context "when multiple maximum constraints are provided" do
let(:version) { "2.1" }
let(:requirement) { Dependabot::Bundler::Requirement.new("<= 2.5, < 3.0") }

it "sets the requirement correctly" do
expect(package_manager.requirement.to_s).to eq("<= 2.5, < 3.0")
end

it "calculates the correct max_version" do
expect(package_manager.requirement.max_version).to eq(Dependabot::Version.new("2.5"))
end

it "returns nil for min_version" do
expect(package_manager.requirement.min_version).to be_nil
end
end
end

describe "SUPPORTED_BUNDLER_VERSIONS" do
Expand Down
Loading

0 comments on commit 26692aa

Please sign in to comment.