Skip to content

Commit 5ae4593

Browse files
Implement sbt version and requirement classes (#14871)
* implement sbt version and requirement classes * bump sorbet typing to strong in version class * add sbt version pattern and reference it
1 parent 88c2309 commit 5ae4593

4 files changed

Lines changed: 280 additions & 18 deletions

File tree

sbt/lib/dependabot/sbt/requirement.rb

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,61 @@
1-
# typed: strong
1+
# typed: strict
22
# frozen_string_literal: true
33

4-
# NOTE: This file was scaffolded automatically but is OPTIONAL.
5-
# If your ecosystem uses standard Gem::Requirement logic,
6-
# you can safely delete this file and remove the require from lib/dependabot/sbt.rb
4+
require "sorbet-runtime"
75

86
require "dependabot/requirement"
97
require "dependabot/utils"
8+
require "dependabot/maven/shared/shared_requirement"
9+
require "dependabot/sbt/version"
1010

1111
module Dependabot
1212
module Sbt
13-
class Requirement < Dependabot::Requirement
13+
# SBT typically uses exact versions ("org" % "artifact" % "1.2.3"),
14+
# but Maven-style version ranges are valid since artifacts resolve
15+
# from Maven repositories.
16+
class Requirement < Dependabot::Maven::Shared::SharedRequirement
1417
extend T::Sig
1518

16-
sig do
17-
override
18-
.params(requirement_string: T.nilable(String))
19-
.returns(T::Array[Dependabot::Requirement])
19+
quoted = OPS.keys.map { |k| Regexp.quote k }.join("|")
20+
PATTERN_RAW = T.let("\\s*(#{quoted})?\\s*(#{Sbt::Version::VERSION_PATTERN})\\s*".freeze, String)
21+
PATTERN = T.let(/\A#{PATTERN_RAW}\z/, Regexp)
22+
RUBY_STYLE_PATTERN = T.let(/\A\s*(#{quoted})\s*(#{Sbt::Version::VERSION_PATTERN})\s*\z/, Regexp)
23+
24+
sig { override.returns(Regexp) }
25+
def self.pattern
26+
PATTERN
27+
end
28+
29+
sig { override.returns(Regexp) }
30+
def self.ruby_style_pattern
31+
RUBY_STYLE_PATTERN
2032
end
33+
34+
sig { override.params(obj: T.any(Gem::Version, String)).returns([String, Gem::Version]) }
35+
def self.parse(obj)
36+
return ["=", Sbt::Version.new(obj.to_s)] if obj.is_a?(Gem::Version)
37+
38+
unless (matches = PATTERN.match(obj.to_s))
39+
msg = "Illformed requirement [#{obj.inspect}]"
40+
raise BadRequirementError, msg
41+
end
42+
43+
return DefaultRequirement if matches[1] == ">=" && matches[2] == "0"
44+
45+
[matches[1] || "=", Sbt::Version.new(T.must(matches[2]))]
46+
end
47+
48+
sig { override.params(requirement_string: T.nilable(String)).returns(T::Array[Requirement]) }
2149
def self.requirements_array(requirement_string)
22-
[new(requirement_string)]
50+
split_java_requirement(requirement_string).map do |str|
51+
new(str)
52+
end
53+
end
54+
55+
sig { override.params(version: Gem::Version).returns(T::Boolean) }
56+
def satisfied_by?(version)
57+
version = Sbt::Version.new(version.to_s)
58+
super
2359
end
2460
end
2561
end

sbt/lib/dependabot/sbt/version.rb

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,19 @@
11
# typed: strong
22
# frozen_string_literal: true
33

4-
# NOTE: This file was scaffolded automatically but is OPTIONAL.
5-
# If your ecosystem uses standard semantic versioning without special logic,
6-
# you can safely delete this file and remove the require from lib/dependabot/sbt.rb
4+
require "sorbet-runtime"
75

8-
require "dependabot/version"
6+
require "dependabot/maven/version"
97
require "dependabot/utils"
108

119
module Dependabot
1210
module Sbt
13-
class Version < Dependabot::Version
11+
# SBT resolves artifacts from Maven repositories and uses the same
12+
# version ordering specification as Maven.
13+
class Version < Dependabot::Maven::Version
1414
extend T::Sig
1515

16-
# TODO: Implement custom version comparison logic if needed
17-
# Example: Handle pre-release versions, build metadata, etc.
18-
# If standard semantic versioning is sufficient, delete this file
16+
VERSION_PATTERN = T.let(Dependabot::Maven::Version::VERSION_PATTERN, String)
1917
end
2018
end
2119
end
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# typed: false
2+
# frozen_string_literal: true
3+
4+
require "spec_helper"
5+
require "dependabot/sbt/requirement"
6+
require "dependabot/sbt/version"
7+
8+
RSpec.describe Dependabot::Sbt::Requirement do
9+
subject(:requirement) { described_class.new(requirement_string) }
10+
11+
let(:requirement_string) { ">=1.0.0" }
12+
13+
describe ".new" do
14+
subject(:requirement) { described_class.new(requirement_string) }
15+
16+
context "with an exact version (typical SBT usage)" do
17+
let(:requirement_string) { "1.0.0" }
18+
19+
it { is_expected.to eq(Gem::Requirement.new("= 1.0.0")) }
20+
end
21+
22+
context "with a pre-release version" do
23+
let(:requirement_string) { "1.3.alpha" }
24+
25+
it { is_expected.to be_satisfied_by(Gem::Version.new("1.3.alpha")) }
26+
end
27+
28+
context "with a range requirement" do
29+
let(:requirement_string) { "[1.0.0,)" }
30+
31+
it { is_expected.to eq(Gem::Requirement.new(">= 1.0.0")) }
32+
33+
context "when needing a > operator" do
34+
let(:requirement_string) { "(1.0.0,)" }
35+
36+
it { is_expected.to eq(Gem::Requirement.new("> 1.0.0")) }
37+
end
38+
39+
context "when needing a > and a < operator" do
40+
let(:requirement_string) { "(1.0.0, 2.0.0)" }
41+
42+
it { is_expected.to eq(Gem::Requirement.new("> 1.0.0", "< 2.0.0")) }
43+
end
44+
45+
context "when needing a >= and a <= operator" do
46+
let(:requirement_string) { "[1.0.0, 2.0.0]" }
47+
48+
it { is_expected.to eq(Gem::Requirement.new(">= 1.0.0", "<= 2.0.0")) }
49+
end
50+
end
51+
52+
context "with a soft requirement" do
53+
let(:requirement_string) { "[1.0.0]" }
54+
55+
it "treats it as an equality matcher" do
56+
expect(requirement).to be_satisfied_by(
57+
Dependabot::Sbt::Version.new("1.0.0")
58+
)
59+
end
60+
end
61+
62+
context "with a wildcard version ending in +" do
63+
let(:requirement_string) { "1.0.+" }
64+
65+
it "converts to a pessimistic constraint" do
66+
expect(requirement).to be_satisfied_by(Dependabot::Sbt::Version.new("1.0.1"))
67+
expect(requirement).to be_satisfied_by(Dependabot::Sbt::Version.new("1.0.9"))
68+
expect(requirement).not_to be_satisfied_by(Dependabot::Sbt::Version.new("1.1.0"))
69+
end
70+
end
71+
72+
context "with a bare wildcard +" do
73+
let(:requirement_string) { "+" }
74+
75+
it "matches any version" do
76+
expect(requirement).to be_satisfied_by(Dependabot::Sbt::Version.new("1.0.0"))
77+
expect(requirement).to be_satisfied_by(Dependabot::Sbt::Version.new("99.0.0"))
78+
end
79+
end
80+
end
81+
82+
describe ".requirements_array" do
83+
subject(:requirements) { described_class.requirements_array(requirement_string) }
84+
85+
context "with a single requirement" do
86+
let(:requirement_string) { "1.0.0" }
87+
88+
it "returns an array with one equality requirement" do
89+
expect(requirements.length).to eq(1)
90+
expect(requirements.first).to eq(Gem::Requirement.new("= 1.0.0"))
91+
end
92+
end
93+
94+
context "with multiple OR requirements" do
95+
let(:requirement_string) { "[1.0,2.0),(3.0,4.0)" }
96+
97+
it "returns an array with the correct split requirements" do
98+
expect(requirements.length).to eq(2)
99+
expect(requirements.first).to eq(Gem::Requirement.new(">= 1.0", "< 2.0"))
100+
expect(requirements.last).to eq(Gem::Requirement.new("> 3.0", "< 4.0"))
101+
end
102+
end
103+
end
104+
105+
describe "#satisfied_by?" do
106+
subject { requirement.satisfied_by?(version) }
107+
108+
let(:requirement_string) { ">=1.0.0" }
109+
110+
context "with a version that satisfies the requirement" do
111+
let(:version) { Dependabot::Sbt::Version.new("1.5.0") }
112+
113+
it { is_expected.to be(true) }
114+
end
115+
116+
context "with a version that does not satisfy the requirement" do
117+
let(:version) { Dependabot::Sbt::Version.new("0.9.0") }
118+
119+
it { is_expected.to be(false) }
120+
end
121+
end
122+
end
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# typed: false
2+
# frozen_string_literal: true
3+
4+
require "spec_helper"
5+
require "dependabot/sbt/version"
6+
7+
RSpec.describe Dependabot::Sbt::Version do
8+
subject(:version) { described_class.new(version_string) }
9+
10+
let(:version_string) { "1.0.0" }
11+
12+
describe ".correct?" do
13+
subject { described_class.correct?(version_string) }
14+
15+
context "with a normal version" do
16+
let(:version_string) { "1.0.0" }
17+
18+
it { is_expected.to be(true) }
19+
end
20+
21+
context "with a named version" do
22+
let(:version_string) { "Finchley" }
23+
24+
it { is_expected.to be(true) }
25+
end
26+
27+
context "with a dynamic version" do
28+
let(:version_string) { "1.+" }
29+
30+
it { is_expected.to be(true) }
31+
end
32+
33+
context "with a nil version" do
34+
let(:version_string) { nil }
35+
36+
it { is_expected.to be(false) }
37+
end
38+
39+
context "with an empty version" do
40+
let(:version_string) { "" }
41+
42+
it { is_expected.to be(false) }
43+
end
44+
end
45+
46+
describe "#to_s" do
47+
subject { version.to_s }
48+
49+
context "with no dashes" do
50+
let(:version_string) { "1.0.0" }
51+
52+
it { is_expected.to eq("1.0.0") }
53+
end
54+
55+
context "with a dash-specified prerelease" do
56+
let(:version_string) { "1.0.0-rc1" }
57+
58+
it { is_expected.to eq("1.0.0-rc1") }
59+
end
60+
61+
context "with underscores" do
62+
let(:version_string) { "1.0_2" }
63+
64+
it { is_expected.to eq("1.0_2") }
65+
end
66+
end
67+
68+
describe "#prerelease?" do
69+
subject { version.prerelease? }
70+
71+
context "with an alpha version" do
72+
let(:version_string) { "1.0.0-alpha" }
73+
74+
it { is_expected.to be(true) }
75+
end
76+
77+
context "with an RC version" do
78+
let(:version_string) { "1.0.0-rc1" }
79+
80+
it { is_expected.to be(true) }
81+
end
82+
83+
context "with a SNAPSHOT version" do
84+
let(:version_string) { "1.0.0-SNAPSHOT" }
85+
86+
it { is_expected.to be(true) }
87+
end
88+
89+
context "with a stable version" do
90+
let(:version_string) { "1.0.0" }
91+
92+
it { is_expected.to be(false) }
93+
end
94+
end
95+
96+
describe "comparison" do
97+
context "when comparing to another version" do
98+
it "orders correctly" do
99+
expect(described_class.new("1.0.0")).to be < described_class.new("1.0.1")
100+
expect(described_class.new("1.0.0")).to be < described_class.new("2.0.0")
101+
expect(described_class.new("1.0.0-alpha")).to be < described_class.new("1.0.0")
102+
expect(described_class.new("1.0.0-SNAPSHOT")).to be < described_class.new("1.0.0")
103+
end
104+
end
105+
end
106+
end

0 commit comments

Comments
 (0)