common/lib/dependabot/version.rb
# typed: strong
# frozen_string_literal: true

require "sorbet-runtime"

module Dependabot
  class Version < Gem::Version
    extend T::Sig
    extend T::Helpers

    abstract!

    VersionParameter = T.type_alias { T.nilable(T.any(String, Integer, Gem::Version)) }

    sig { override.overridable.params(version: VersionParameter).void }
    def initialize(version)
      @original_version = T.let(version.to_s, String)

      super
    end

    sig { override.overridable.params(version: VersionParameter).returns(Dependabot::Version) }
    def self.new(version)
      T.cast(super, Dependabot::Version)
    end

    # Opt-in to Rubygems 4 behavior
    sig { override.overridable.params(version: VersionParameter).returns(T::Boolean) }
    def self.correct?(version)
      return false if version.nil?

      version.to_s.match?(ANCHORED_VERSION_PATTERN)
    end

    sig { overridable.returns(String) }
    def to_semver
      @original_version
    end

    sig { overridable.returns(T::Array[String]) }
    def ignored_patch_versions
      parts = to_semver.split(".")
      version_parts = parts.fill("0", parts.length...2)
      upper_parts = version_parts.first(1) + [version_parts[1].to_i + 1]
      lower_bound = "> #{to_semver}"
      upper_bound = "< #{upper_parts.join('.')}"

      ["#{lower_bound}, #{upper_bound}"]
    end

    sig { overridable.returns(T::Array[String]) }
    def ignored_minor_versions
      parts = to_semver.split(".")
      version_parts = parts.fill("0", parts.length...2)
      lower_parts = version_parts.first(1) + [version_parts[1].to_i + 1] + [lowest_prerelease_suffix]
      upper_parts = version_parts.first(0) + [version_parts[0].to_i + 1]
      lower_bound = ">= #{lower_parts.join('.')}"
      upper_bound = "< #{upper_parts.join('.')}"

      ["#{lower_bound}, #{upper_bound}"]
    end

    sig { overridable.returns(T::Array[String]) }
    def ignored_major_versions
      version_parts = to_semver.split(".")
      lower_parts = [version_parts[0].to_i + 1] + [lowest_prerelease_suffix]
      lower_bound = ">= #{lower_parts.join('.')}"

      [lower_bound]
    end

    sig { returns(String) }
    def lowest_prerelease_suffix
      "a"
    end

    sig { returns(T.nilable([Integer, Integer, Integer])) }
    def semver_parts
      # Extracts only the numeric major.minor.patch part of the version, ensuring it starts with a number
      match = to_semver.match(/^\d+(?:\.\d+)?(?:\.\d+)?(?=[^\d]|$)/)
      return nil unless match

      first_match = match[0]
      return nil unless first_match

      major, minor, patch = first_match.split(".").map(&:to_i)
      [major || 0, minor || 0, patch || 0]
    end
  end
end
