lib/dependabot/sem_version2.rb
# typed: strong # frozen_string_literal: true require "sorbet-runtime" # See https://semver.org/spec/v2.0.0.html for semver 2 details # module Dependabot class SemVersion2 extend T::Sig extend T::Helpers include Comparable SEMVER2_REGEX = /^ (0|[1-9]\d*)\. # major (0|[1-9]\d*)\. # minor (0|[1-9]\d*) # patch (?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))? # pre release (?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))? # build metadata $/x sig { returns(String) } attr_accessor :major sig { returns(String) } attr_accessor :minor sig { returns(String) } attr_accessor :patch sig { returns(T.nilable(String)) } attr_accessor :build sig { returns(T.nilable(String)) } attr_accessor :prerelease sig { params(version: String).void } def initialize(version) tokens = parse(version) @major = T.let(T.must(tokens[:major]), String) @minor = T.let(T.must(tokens[:minor]), String) @patch = T.let(T.must(tokens[:patch]), String) @build = T.let(tokens[:build], T.nilable(String)) @prerelease = T.let(tokens[:prerelease], T.nilable(String)) end sig { returns(T::Boolean) } def prerelease? !!prerelease end sig { returns(String) } def to_s value = [major, minor, patch].join(".") value += "-#{prerelease}" if prerelease value += "+#{build}" if build value end sig { returns(String) } def inspect "#<#{self.class} #{self}>" end sig { params(other: ::Dependabot::SemVersion2).returns(T::Boolean) } def eql?(other) other.is_a?(self.class) && to_s == other.to_s end sig { params(other: ::Dependabot::SemVersion2).returns(Integer) } def <=>(other) result = major.to_i <=> other.major.to_i return result unless result.zero? result = minor.to_i <=> other.minor.to_i return result unless result.zero? result = patch.to_i <=> other.patch.to_i return result unless result.zero? compare_prereleases(prerelease, other.prerelease) end sig { params(version: T.nilable(String)).returns(T::Boolean) } def self.correct?(version) return false if version.nil? version.match?(SEMVER2_REGEX) end private sig { params(version: String).returns(T::Hash[Symbol, T.nilable(String)]) } def parse(version) match = version.match(SEMVER2_REGEX) raise ArgumentError, "Malformed version number string #{version}" unless match major, minor, patch, prerelease, build = match.captures { major: major, minor: minor, patch: patch, prerelease: prerelease, build: build } end sig { params(prerelease1: T.nilable(String), prerelease2: T.nilable(String)).returns(Integer) } def compare_prereleases(prerelease1, prerelease2) # rubocop:disable Metrics/PerceivedComplexity return 0 if prerelease1.nil? && prerelease2.nil? return -1 if prerelease2.nil? return 1 if prerelease1.nil? prerelease1_tokens = prerelease1.split(".") prerelease2_tokens = prerelease2.split(".") prerelease1_tokens.zip(prerelease2_tokens) do |t1, t2| return 1 if t2.nil? # t1 is more specific e.g. 1.0.0-rc1.1 vs 1.0.0-rc1 if t1 =~ /^\d+$/ && t2 =~ /^\d+$/ # t1 and t2 are both ints so compare them as such a = t1.to_i b = t2.to_i compare = a <=> b return compare unless compare.zero? end comp = t1 <=> t2 return T.must(comp) unless T.must(comp).zero? end # prereleases are equal or prerelease2 is more specific e.g. 1.0.0-rc1 vs 1.0.0-rc1.1 prerelease1_tokens.length == prerelease2_tokens.length ? 0 : -1 end end end