Skip to content

Commit 099463e

Browse files
add specs for dart additional dependencies
2 parents c54908e + 25c8aa4 commit 099463e

13 files changed

Lines changed: 1153 additions & 7 deletions

File tree

bundler/lib/dependabot/bundler/requirement.rb

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ module Bundler
1111
class Requirement < Dependabot::Requirement
1212
extend T::Sig
1313

14+
GEM_DEP_SPLIT = T.let(/\A(?<name>[a-zA-Z0-9_\-]+):(?<version>.+)\z/, Regexp)
15+
1416
sig { params(req: T::Hash[Symbol, String], version: Gem::Version).returns(T::Boolean) }
1517
def self.satisfied_by?(req, version)
1618
new(req[:requirement]).satisfied_by?(version)
@@ -24,8 +26,41 @@ def self.requirements_array(requirement_string)
2426
[new(requirement_string)]
2527
end
2628

27-
# Patches Gem::Requirement to make it accept requirement strings like
28-
# "~> 4.2.5, >= 4.2.5.1" without first needing to split them.
29+
sig { params(dep_string: String).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
30+
def self.parse_dep_string(dep_string)
31+
stripped = dep_string.strip
32+
return nil if stripped.empty?
33+
34+
match = stripped.match(GEM_DEP_SPLIT)
35+
return nil unless match
36+
37+
name = T.must(match[:name])
38+
constraint = T.must(match[:version]).strip
39+
40+
return nil if constraint.empty?
41+
42+
version = extract_version(constraint)
43+
44+
{
45+
name: name,
46+
normalised_name: name,
47+
version: version,
48+
requirement: constraint,
49+
extras: nil
50+
}
51+
end
52+
53+
sig { params(constraint: String).returns(T.nilable(String)) }
54+
def self.extract_version(constraint)
55+
version_part = constraint.sub(/\A[~><=!]+\s*/, "").strip
56+
57+
return version_part if version_part.match?(/\A\d+(?:\.\d+)*(?:\.\w+)?\z/)
58+
59+
nil
60+
end
61+
62+
private_class_method :extract_version
63+
2964
sig { params(requirements: T.nilable(T.any(String, T::Array[String]))).void }
3065
def initialize(*requirements)
3166
requirements = requirements.flatten.flat_map do |req_string|
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
# typed: false
2+
# frozen_string_literal: true
3+
4+
require "spec_helper"
5+
require "dependabot/bundler/requirement"
6+
7+
RSpec.describe Dependabot::Bundler::Requirement do
8+
describe ".parse_dep_string" do
9+
context "with a simple exact version" do
10+
it "parses scss_lint:0.52.0" do
11+
result = described_class.parse_dep_string("scss_lint:0.52.0")
12+
expect(result).not_to be_nil
13+
expect(result[:name]).to eq("scss_lint")
14+
expect(result[:normalised_name]).to eq("scss_lint")
15+
expect(result[:version]).to eq("0.52.0")
16+
expect(result[:requirement]).to eq("0.52.0")
17+
expect(result[:extras]).to be_nil
18+
end
19+
20+
it "parses a hyphenated gem name" do
21+
result = described_class.parse_dep_string("rubocop-rails:2.19.0")
22+
expect(result).not_to be_nil
23+
expect(result[:name]).to eq("rubocop-rails")
24+
expect(result[:version]).to eq("2.19.0")
25+
expect(result[:requirement]).to eq("2.19.0")
26+
end
27+
end
28+
29+
context "with range operators" do
30+
it "parses pessimistic version operator ~> 1.50" do
31+
result = described_class.parse_dep_string("rubocop:~> 1.50")
32+
expect(result).not_to be_nil
33+
expect(result[:name]).to eq("rubocop")
34+
expect(result[:version]).to eq("1.50")
35+
expect(result[:requirement]).to eq("~> 1.50")
36+
end
37+
38+
it "parses greater than or equal >= 1.0" do
39+
result = described_class.parse_dep_string("rubocop:>= 1.0")
40+
expect(result).not_to be_nil
41+
expect(result[:name]).to eq("rubocop")
42+
expect(result[:version]).to eq("1.0")
43+
expect(result[:requirement]).to eq(">= 1.0")
44+
end
45+
46+
it "parses exact version constraint = 1.50.0" do
47+
result = described_class.parse_dep_string("rubocop:= 1.50.0")
48+
expect(result).not_to be_nil
49+
expect(result[:name]).to eq("rubocop")
50+
expect(result[:version]).to eq("1.50.0")
51+
expect(result[:requirement]).to eq("= 1.50.0")
52+
end
53+
54+
it "parses greater than > 1.0" do
55+
result = described_class.parse_dep_string("rails:> 6.0")
56+
expect(result).not_to be_nil
57+
expect(result[:name]).to eq("rails")
58+
expect(result[:version]).to eq("6.0")
59+
expect(result[:requirement]).to eq("> 6.0")
60+
end
61+
62+
it "parses less than or equal <= 2.0" do
63+
result = described_class.parse_dep_string("rails:<= 7.0")
64+
expect(result).not_to be_nil
65+
expect(result[:name]).to eq("rails")
66+
expect(result[:version]).to eq("7.0")
67+
expect(result[:requirement]).to eq("<= 7.0")
68+
end
69+
end
70+
71+
context "with pre-release versions" do
72+
it "parses pre-release version with alpha suffix" do
73+
result = described_class.parse_dep_string("rails:7.0.0.alpha1")
74+
expect(result).not_to be_nil
75+
expect(result[:name]).to eq("rails")
76+
expect(result[:version]).to eq("7.0.0.alpha1")
77+
expect(result[:requirement]).to eq("7.0.0.alpha1")
78+
end
79+
80+
it "parses pre-release version with rc suffix" do
81+
result = described_class.parse_dep_string("rails:7.0.0.rc1")
82+
expect(result).not_to be_nil
83+
expect(result[:name]).to eq("rails")
84+
expect(result[:version]).to eq("7.0.0.rc1")
85+
expect(result[:requirement]).to eq("7.0.0.rc1")
86+
end
87+
end
88+
89+
context "with no version" do
90+
it "returns nil for gem name without version" do
91+
result = described_class.parse_dep_string("rubocop")
92+
expect(result).to be_nil
93+
end
94+
95+
it "returns nil for gem name with just colon" do
96+
result = described_class.parse_dep_string("rubocop:")
97+
expect(result).to be_nil
98+
end
99+
end
100+
101+
context "with whitespace" do
102+
it "handles leading and trailing whitespace" do
103+
result = described_class.parse_dep_string(" scss_lint:0.52.0 ")
104+
expect(result).not_to be_nil
105+
expect(result[:name]).to eq("scss_lint")
106+
expect(result[:version]).to eq("0.52.0")
107+
end
108+
109+
it "handles whitespace in version constraint" do
110+
result = described_class.parse_dep_string("rubocop:~> 1.50")
111+
expect(result).not_to be_nil
112+
expect(result[:name]).to eq("rubocop")
113+
expect(result[:version]).to eq("1.50")
114+
expect(result[:requirement]).to eq("~> 1.50")
115+
end
116+
end
117+
118+
context "with invalid input" do
119+
it "returns nil for empty string" do
120+
result = described_class.parse_dep_string("")
121+
expect(result).to be_nil
122+
end
123+
124+
it "returns nil for whitespace only" do
125+
result = described_class.parse_dep_string(" ")
126+
expect(result).to be_nil
127+
end
128+
129+
it "returns nil for @ format (not colon)" do
130+
result = described_class.parse_dep_string("rubocop@1.50.0")
131+
expect(result).to be_nil
132+
end
133+
end
134+
end
135+
end

pre_commit/Dockerfile

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,9 @@ RUN chown -R dependabot:dependabot "$PUB_CACHE"
4545

4646
USER dependabot
4747

48-
# TODO: If you have native helpers, uncomment and configure these lines:
49-
# ENV DEPENDABOT_NATIVE_HELPERS_PATH="/opt"
50-
# COPY --chown=dependabot:dependabot pre_commit/helpers /opt/pre_commit/helpers
51-
# RUN bash /opt/pre_commit/helpers/build
48+
# Install bundler native helpers for Ruby additional_dependencies support
49+
COPY --chown=dependabot:dependabot bundler/helpers /opt/bundler/helpers
50+
RUN bash /opt/bundler/helpers/v2/build
5251

53-
COPY --chown=dependabot:dependabot --parents pre_commit common go_modules cargo npm_and_yarn python pub $DEPENDABOT_HOME/
52+
COPY --chown=dependabot:dependabot --parents pre_commit common pub bundler go_modules cargo npm_and_yarn python $DEPENDABOT_HOME/
5453
COPY --chown=dependabot:dependabot updater $DEPENDABOT_HOME/dependabot-updater
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
# typed: strict
2+
# frozen_string_literal: true
3+
4+
require "excon"
5+
require "json"
6+
require "sorbet-runtime"
7+
require "dependabot/dependency"
8+
require "dependabot/update_checkers"
9+
require "dependabot/pre_commit/additional_dependency_checkers"
10+
require "dependabot/pre_commit/additional_dependency_checkers/base"
11+
12+
module Dependabot
13+
module PreCommit
14+
module AdditionalDependencyCheckers
15+
class Ruby < Base
16+
extend T::Sig
17+
18+
sig { override.returns(T.nilable(String)) }
19+
def latest_version
20+
return nil unless package_name
21+
22+
@latest_version ||= T.let(
23+
fetch_latest_version_via_bundler_checker,
24+
T.nilable(String)
25+
)
26+
end
27+
28+
sig { override.params(latest_version: String).returns(T::Array[T::Hash[Symbol, T.untyped]]) }
29+
def updated_requirements(latest_version)
30+
requirements.map do |original_req|
31+
original_source = original_req[:source]
32+
next original_req unless original_source.is_a?(Hash)
33+
next original_req unless original_source[:type] == "additional_dependency"
34+
35+
original_requirement = original_req[:requirement]
36+
new_requirement = build_updated_requirement(original_requirement, latest_version)
37+
38+
new_original_string = build_original_string(
39+
package_name: original_source[:original_name] || original_source[:package_name],
40+
requirement: new_requirement
41+
)
42+
43+
new_source = original_source.merge(original_string: new_original_string)
44+
45+
original_req.merge(
46+
requirement: new_requirement,
47+
source: new_source
48+
)
49+
end
50+
end
51+
52+
private
53+
54+
sig { returns(T.nilable(String)) }
55+
def fetch_latest_version_via_bundler_checker
56+
bundler_checker = bundler_update_checker
57+
return nil unless bundler_checker
58+
59+
latest = bundler_checker.latest_version
60+
Dependabot.logger.info("Ruby UpdateChecker found latest version: #{latest || 'none'}")
61+
62+
latest&.to_s
63+
rescue Dependabot::DependabotError, Excon::Error, JSON::ParserError => e
64+
Dependabot.logger.debug("Error checking Ruby gem #{package_name}: #{e.message}")
65+
nil
66+
end
67+
68+
sig { returns(T.nilable(Dependabot::UpdateCheckers::Base)) }
69+
def bundler_update_checker
70+
@bundler_update_checker ||= T.let(
71+
build_bundler_update_checker,
72+
T.nilable(Dependabot::UpdateCheckers::Base)
73+
)
74+
end
75+
76+
sig { returns(T.nilable(Dependabot::UpdateCheckers::Base)) }
77+
def build_bundler_update_checker
78+
bundler_dependency = build_bundler_dependency
79+
return nil unless bundler_dependency
80+
81+
Dependabot.logger.info("Delegating to bundler UpdateChecker for gem: #{bundler_dependency.name}")
82+
83+
Dependabot::UpdateCheckers.for_package_manager("bundler").new(
84+
dependency: bundler_dependency,
85+
dependency_files: build_bundler_dependency_files,
86+
credentials: credentials,
87+
ignored_versions: [],
88+
security_advisories: [],
89+
raise_on_ignored: false
90+
)
91+
end
92+
93+
sig { returns(T.nilable(Dependabot::Dependency)) }
94+
def build_bundler_dependency
95+
return nil unless package_name
96+
97+
version = current_version || extract_version_from_requirement
98+
99+
Dependabot::Dependency.new(
100+
name: T.must(package_name),
101+
version: version,
102+
requirements: [{
103+
requirement: version ? "= #{version}" : nil,
104+
groups: [],
105+
file: "Gemfile",
106+
source: nil
107+
}],
108+
package_manager: "bundler"
109+
)
110+
end
111+
112+
sig { returns(T.nilable(String)) }
113+
def extract_version_from_requirement
114+
req_string = requirements.first&.dig(:requirement)
115+
return nil unless req_string
116+
117+
version_part = req_string.sub(/\A[~><=!]+\s*/, "").strip
118+
return version_part if version_part.match?(/\A\d+(?:\.\d+)*(?:\.\w+)?\z/)
119+
120+
nil
121+
end
122+
123+
sig { returns(T::Array[Dependabot::DependencyFile]) }
124+
def build_bundler_dependency_files
125+
version = current_version || extract_version_from_requirement
126+
version_constraint = version ? ", \"#{version}\"" : ""
127+
gemfile_content = "source 'https://rubygems.org'\ngem '#{package_name}'#{version_constraint}\n"
128+
129+
[
130+
Dependabot::DependencyFile.new(
131+
name: "Gemfile",
132+
content: gemfile_content
133+
)
134+
]
135+
end
136+
137+
sig do
138+
params(
139+
package_name: T.nilable(String),
140+
requirement: T.nilable(String)
141+
).returns(String)
142+
end
143+
def build_original_string(package_name:, requirement:)
144+
base = package_name.to_s
145+
base = "#{base}:#{requirement}" if requirement
146+
base
147+
end
148+
149+
sig { params(original_requirement: T.nilable(String), new_version: String).returns(String) }
150+
def build_updated_requirement(original_requirement, new_version)
151+
return new_version unless original_requirement
152+
153+
# Handle Ruby gem version operators: ~>, >=, >, <=, <, =, !=
154+
operator_match = original_requirement.match(/\A(?<op>[~><=!]+)\s*/)
155+
if operator_match
156+
"#{operator_match[:op]} #{new_version}"
157+
else
158+
new_version
159+
end
160+
end
161+
end
162+
end
163+
end
164+
end
165+
166+
Dependabot::PreCommit::AdditionalDependencyCheckers.register(
167+
"ruby",
168+
Dependabot::PreCommit::AdditionalDependencyCheckers::Ruby
169+
)

pre_commit/lib/dependabot/pre_commit/file_parser.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
require "dependabot/npm_and_yarn/requirement"
1515
require "dependabot/pub/requirement"
1616
require "dependabot/python/requirement_parser"
17+
require "dependabot/bundler/requirement"
1718
require "dependabot/go_modules/requirement_parser"
1819

1920
module Dependabot
@@ -32,6 +33,7 @@ class FileParser < Dependabot::FileParsers::Base
3233
"node" => ->(dep_string) { Dependabot::NpmAndYarn::Requirement.parse_dep_string(dep_string) },
3334
"rust" => ->(dep_string) { Dependabot::Cargo::Requirement.parse_dep_string(dep_string) },
3435
"golang" => ->(dep_string) { Dependabot::GoModules::RequirementParser.parse(dep_string) },
36+
"ruby" => ->(dep_string) { Dependabot::Bundler::Requirement.parse_dep_string(dep_string) },
3537
"dart" => ->(dep_string) { Dependabot::Pub::Requirement.parse_dep_string(dep_string) }
3638
}.freeze,
3739
T::Hash[String, T.proc.params(dep_string: String).returns(T.nilable(T::Hash[Symbol, T.untyped]))]

pre_commit/lib/dependabot/pre_commit/update_checker.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
require "dependabot/pre_commit/additional_dependency_checkers/dart"
1111
require "dependabot/pre_commit/additional_dependency_checkers/node"
1212
require "dependabot/pre_commit/additional_dependency_checkers/python"
13+
require "dependabot/pre_commit/additional_dependency_checkers/ruby"
1314
require "dependabot/pre_commit/additional_dependency_checkers/go"
1415
require "dependabot/pre_commit/additional_dependency_checkers/rust"
1516
require "dependabot/update_checkers"

0 commit comments

Comments
 (0)