Skip to content
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

Implement maven version specification #10524

Merged
merged 8 commits into from
Sep 9, 2024
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
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ deps_shared_with_common = %w(
rubocop-sorbet
simplecov
stackprof
strscan
turbo_tests
vcr
webmock
Expand Down
71 changes: 71 additions & 0 deletions maven/lib/dependabot/maven/new_version.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# typed: strict
amazimbe marked this conversation as resolved.
Show resolved Hide resolved
# frozen_string_literal: true

require "dependabot/maven/version_parser"
require "dependabot/version"
require "dependabot/utils"

# See https://maven.apache.org/pom.html#Version_Order_Specification for details.

module Dependabot
module Maven
class NewVersion
abdulapopoola marked this conversation as resolved.
Show resolved Hide resolved
extend T::Sig
extend T::Helpers

PRERELEASE_QUALIFIERS = T.let([
Dependabot::Maven::VersionParser::ALPHA,
Dependabot::Maven::VersionParser::BETA,
Dependabot::Maven::VersionParser::MILESTONE,
Dependabot::Maven::VersionParser::RC,
Dependabot::Maven::VersionParser::SNAPSHOT
].freeze, T::Array[Integer])

sig { returns(Dependabot::Maven::TokenBucket) }
attr_accessor :token_bucket

sig { params(version: String).returns(T::Boolean) }
def self.correct?(version)
amazimbe marked this conversation as resolved.
Show resolved Hide resolved
return false if version.empty?

Dependabot::Maven::VersionParser.parse(version.to_s).to_a.any?
rescue Dependabot::BadRequirementError
Dependabot.logger.info("Malformed version string - #{version}")
false
end

sig { params(version: String).void }
def initialize(version)
@version_string = T.let(version, String)
@token_bucket = T.let(Dependabot::Maven::VersionParser.parse(version), Dependabot::Maven::TokenBucket)
end

sig { returns(String) }
def inspect
"#<#{self.class} #{version_string}>"
end

sig { returns(String) }
def to_s
version_string
end

sig { returns(T::Boolean) }
def prerelease?
token_bucket.to_a.flatten.any? do |token|
token.is_a?(Integer) && token.negative?
end
end

sig { params(other: ::Dependabot::Maven::NewVersion).returns(Integer) }
def <=>(other)
T.must(token_bucket <=> other.token_bucket)
end

private

sig { returns(String) }
attr_reader :version_string
end
end
end
99 changes: 99 additions & 0 deletions maven/lib/dependabot/maven/token_bucket.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# typed: strong
# frozen_string_literal: true

require "sorbet-runtime"
require "dependabot/maven/version_parser"

# See https://maven.apache.org/pom.html#Version_Order_Specification for details
#
module Dependabot
module Maven
class TokenBucket < T::Struct
extend T::Sig
extend T::Helpers
include Comparable

prop :tokens, T::Array[T.untyped]
prop :addition, T.nilable(TokenBucket)

sig { returns(T::Array[T.untyped]) }
def to_a
return tokens if addition.nil?

tokens.clone.append(addition.to_a)
end

sig { params(other: TokenBucket).returns(T.nilable(Integer)) }
def <=>(other)
cmp = compare_tokens(tokens, other.tokens)
return cmp unless cmp&.zero?

compare_additions(addition, other.addition)
end

sig do
params(
first: T::Array[T.any(String, Integer)],
second: T::Array[T.any(String, Integer)]
).returns(T.nilable(Integer))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any reason this is a nilable Integer?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one is mostly because the call on line 43 is nilable. I tried to change this one but sorbet keeps complaining.

end
def compare_tokens(first, second)
max_idx = [first.size, second.size].max - 1
(0..max_idx).each do |idx|
cmp = compare_token_pair(first[idx], second[idx])
return cmp unless T.must(cmp).zero?
end
0
end

sig do
params(
first: T.nilable(T.any(String, Integer)),
second: T.nilable(T.any(String, Integer))
).returns(T.nilable(Integer))
amazimbe marked this conversation as resolved.
Show resolved Hide resolved
end
def compare_token_pair(first = 0, second = 0) # rubocop:disable Metrics/PerceivedComplexity,Metrics/CyclomaticComplexity
first ||= 0
second ||= 0

if first.is_a?(Integer) && second.is_a?(String)
return first <= 0 ? -1 : 1
end

if first.is_a?(String) && second.is_a?(Integer)
return second <= 0 ? 1 : -1
end

if first == Dependabot::Maven::VersionParser::SP &&
second.is_a?(String) && second != Dependabot::Maven::VersionParser::SP
return -1
end

if second == Dependabot::Maven::VersionParser::SP &&
first.is_a?(String) && first != Dependabot::Maven::VersionParser::SP
return 1
end

if first.is_a?(Integer) && second.is_a?(Integer)
first <=> second
elsif first.is_a?(String) && second.is_a?(String)
first <=> second
end
end

sig do
params(first: T.nilable(TokenBucket), second: T.nilable(TokenBucket)).returns(T.nilable(Integer))
end
def compare_additions(first, second)
return 0 if first.nil? && second.nil?

(first || empty_addition) <=> (second || empty_addition)
end

sig { returns(TokenBucket) }
def empty_addition
TokenBucket.new(tokens: [])
end
end
end
end
139 changes: 139 additions & 0 deletions maven/lib/dependabot/maven/version_parser.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# typed: strict
# frozen_string_literal: true

require "sorbet-runtime"
require "strscan"
require "dependabot/maven/token_bucket"

# See https://maven.apache.org/pom.html#Version_Order_Specification for details
#
module Dependabot
module Maven
class VersionParser
amazimbe marked this conversation as resolved.
Show resolved Hide resolved
extend T::Sig
extend T::Helpers

ALPHA = -5
BETA = -4
MILESTONE = -3
RC = -2
SNAPSHOT = -1
SP = "sp"

sig { params(version: T.nilable(String)).returns(TokenBucket) }
def self.parse(version)
raise BadRequirementError, "Malformed version string - string is nil" if version.nil?
raise BadRequirementError, "Malformed version string - string is empty" if version.empty?

new(version).parse
end

sig { params(version: String).void }
def initialize(version)
@version = version
@token_bucket = T.let(TokenBucket.new(tokens: []), T.nilable(TokenBucket))
@parse_result = T.let(@token_bucket, T.nilable(TokenBucket))
@scanner = T.let(StringScanner.new(version.downcase), StringScanner)
end

sig { returns(TokenBucket) }
def parse
parse_version(false)

# no tokens: version is just one of the tokens we split on e.g '.' or '-'
raise BadRequirementError, "Malformed version string - #{version}" if parse_result.to_a.empty?

T.must(parse_result)
end

private

sig { returns(StringScanner) }
attr_reader :scanner

sig { returns(String) }
attr_reader :version

sig { returns(T.nilable(TokenBucket)) }
attr_reader :parse_result

sig { params(token: T.nilable(T.any(String, Integer))).void }
def parse_addition(token = nil)
@token_bucket&.addition = TokenBucket.new(tokens: [token].compact)
@token_bucket = @token_bucket&.addition

scanner.skip(/-+/)
parse_version(true)
end

sig { params(number_begins_partition: T.nilable(T::Boolean)).void }
def parse_version(number_begins_partition) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
# skip leading v if any
scanner.skip(/v/)

until scanner.eos?
if (s = scanner.scan(/\d+/))
if number_begins_partition
parse_addition(s.to_i)
else
T.must(@token_bucket).tokens << s.to_i
end

elsif (s = scanner.match?(/a\d+/))
# aN is equivalent to alpha-N
scanner.skip("a")
parse_addition(ALPHA)

elsif (s = scanner.match?(/b\d+/))
# bN is equivalent to beta-N
scanner.skip("b")
parse_addition(BETA)

elsif (s = scanner.match?(/m\d+/))
# mN is equivalent to milestone-N
scanner.skip("m")
parse_addition(MILESTONE)

elsif (s = scanner.scan(/(alpha|beta|milestone|rc|cr|sp|ga|final|release|snapshot)[a-z]+/))
# process "alpha" and others as normal lexical tokens if they're followed by a letter
parse_addition(s)

elsif (s = scanner.scan("alpha"))
# handle alphaN, alpha-X, alpha.X, or ending alpha
parse_addition(ALPHA)

elsif (s = scanner.scan("beta"))
parse_addition(BETA)
elsif (s = scanner.scan("milestone"))
parse_addition(MILESTONE)

elsif (s = scanner.scan(/(rc|cr)/))
parse_addition(RC)

elsif (s = scanner.scan("snapshot"))
parse_addition(SNAPSHOT)

elsif (s = scanner.scan(/ga|final|release/))
parse_addition

elsif (s = scanner.scan("sp"))
parse_addition(SP)

# `+` is parsed as an addition as stated in maven version spec
elsif (s = scanner.scan(/[a-z_+]+/))
parse_addition(s)

elsif (s = scanner.scan("."))
number_begins_partition = false

elsif (s = scanner.scan("-"))
number_begins_partition = true

else
raise BadRequirementError, "Malformed version string - #{version}"
end
end
end
end
end
end
Loading
Loading