class Composer::Semver::VersionParser

Version Parser

PHP Authors: Jordi Boggiano <j.boggiano@seld.be>

Ruby Authors: Ioannis Kappas <ikappas@devworks.gr>

Public Class Methods

modifier_regex() click to toggle source
# File lib/composer/semver/version_parser.rb, line 23
def self.modifier_regex
  @modifier_regex ||= '[._-]?(?:(stable|beta|b|RC|alpha|a|patch|pl|p)((?:[.-]?\d+)*+)?)?([.-]?dev)?'.freeze
end
normalize_stability(stability) click to toggle source

Normalize the specified stability

@param stability string The stability to normalize.

@return string

# File lib/composer/semver/version_parser.rb, line 83
def self.normalize_stability(stability)

  # verify supplied arguments
  raise ArgumentError,
        'stability must be specified' unless stability

  raise TypeError,
        'stability must be of type String' unless stability.is_a?(String)

  stability = stability.downcase
  stability === 'rc' ? 'RC' : stability
end
parse_stability(version) click to toggle source

Returns the stability of a version

@param version string The version to parse for stability

@return string The version’s stability

# File lib/composer/semver/version_parser.rb, line 36
def self.parse_stability(version)

  raise ArgumentError,
        'version must be specified' if version.nil?

  raise TypeError,
        'version must be of type String' unless version.is_a?(String)

  version.gsub!(/#.+$/i, '')

  # match dev stability
  if version.start_with?('dev-') or version.end_with?('-dev')
    return 'dev'
  end

  /#{self.modifier_regex}$/i.match(version.downcase) do |matches|

    if !matches[3].nil? && !matches[3].empty?
      return 'dev'
    end

    if !matches[1].nil? && !matches[1].empty?

      if matches[1] === 'beta' || matches[1] === 'b'
        return 'beta'
      end

      if matches[1] === 'alpha' || matches[1] === 'a'
        return 'alpha'
      end

      if matches[1] === 'rc'
        return 'RC'
      end

    end

  end

  'stable'
end
stabilities() click to toggle source
# File lib/composer/semver/version_parser.rb, line 27
def self.stabilities
  @stabilities ||= %w{stable RC beta alpha dev}.freeze
end

Public Instance Methods

normalize(version, full_version = nil) click to toggle source

Normalizes a version string to be able to perform comparisons on it

Params: @param version string The version string to normalize @param full_version string Optional. The complete version string to

give more context

@throws InvalidVersionStringError

@return string The normalized version string.

# File lib/composer/semver/version_parser.rb, line 106
def normalize(version, full_version = nil)

  # verify supplied arguments
  raise ArgumentError,
        'version must be specified' if version.nil?

  raise TypeError,
        'version must be of type String' unless version.is_a?(String)

  # trim the version
  version.strip!

  # set the full_version unless specified
  full_version = version if full_version.nil?

  # set default index
  index = 0

  # strip off aliasing
  /^([^,\s]+) +as +([^,\s]+)$/.match(version) do |matches|
    version = matches[1]
  end

  # strip off build metadata
  /^([^,\s+]+)\+[^\s]+$/.match(version) do |matches|
    version = matches[1]
  end

  # match master-like branches
  if /^(?:dev-)?(?:master|trunk|default)$/i.match(version)
    return '9999999-dev'
  end

  # match dev- prefix versioning
  if version.downcase.start_with?('dev-')
    return "dev-#{version[4..version.size]}"
  end

  # match classical versioning
  if (matches = /^v?(\d{1,5})(\.\d+)?(\.\d+)?(\.\d+)?#{self.class.modifier_regex}$/i.match(version))
    version = ''
    matches.to_a[1..4].each do |c|
      version += c ? c : '.0'
    end
    index = 5
  # match data(time) based versioning
  elsif (matches = /^v?(\d{4}(?:[.:-]?\d{2}){1,6}(?:[.:-]?\d{1,3})?)#{self.class.modifier_regex}$/i.match(version))
    version = matches[1].gsub(/\D/, '.')
    index = 2
  end

  # add version modifiers if a version was matched
  if index > 0

    if !matches[index].nil? && !matches[index].empty?

      if matches[index] === 'stable'
        return version
      end

      stability = expand_stability(matches[index])
      version << "-#{stability}"
      if !matches[index + 1].nil? && !matches[index + 1].empty?
        version << matches[index + 1].gsub(/^[.-]+/, '')
      end

    end

    if !matches[index + 2].nil? && !matches[index + 2].empty?
      version << '-dev'
    end

    return version
  end

  # match dev branches
  /(.*?)[.-]?dev$/i.match(version) do |match|
    begin
      return normalize_branch(match[1])
    rescue
      #skip
    end
  end

  extra_message = ''
  if / +as +#{Regexp.escape(version)}$/.match(full_version)
    extra_message = " in \"#{full_version}\", the alias must be an exact version"
  elsif /^#{Regexp.escape(version)} +as +/.match(full_version)
    extra_message = " in \"#{full_version}\", the alias source must be an exact version, if it is a branch name you should prefix it with dev-"
  end

  raise ArgumentError,
        "Invalid version string \"#{version}\"#{extra_message}"
end
normalize_branch(name) click to toggle source

Normalizes a branch name to be able to perform comparisons on it

@param name string The branch name to normalize

@return string The normalized branch name

# File lib/composer/semver/version_parser.rb, line 218
def normalize_branch(name)

  # verify supplied arguments
  raise ArgumentError,
        'name must be specified' unless name

  raise TypeError,
        'name must be of type String' unless name.is_a?(String)

  raise ArgumentError,
        'name string must not be empty' if name.empty?

  name.strip!
  if %w{master trunk default}.include?(name)
    normalize(name)
  elsif (matches = /^v?(\d+)(\.(?:\d+|[xX*]))?(\.(?:\d+|[xX*]))?(\.(?:\d+|[xX*]))?$/i.match(name))
    version = ''
    matches.captures.each { |match| version << (match != nil ? match.tr('*', 'x').tr('X', 'x') : '.x') }
    "#{version.gsub('x', '9999999')}-dev"
  else
    "dev-#{name}"
  end
end
parse_constraints(constraints) click to toggle source

Parses a constraint string into MultiConstraint and/or Constraint objects.

@param constraints string The constraints to parse.

@return ::Composer::Semver::Constraint::Base

# File lib/composer/semver/version_parser.rb, line 247
def parse_constraints(constraints)

  # verify supplied arguments
  raise ArgumentError,
        'version must be specified' unless constraints

  raise TypeError,
        'version must be of type String' unless constraints.is_a?(String)

  raise ArgumentError,
        'version string must not be empty' if constraints.empty?

  pretty_constraint = constraints

  # match stabilities constraints
  /^([^,\s]*?)@(#{self.class.stabilities.join('|')})$/i.match(constraints) do |match|
    constraints = match[1].nil? || match[1].empty? ? '*' : match[1]
  end

  # match dev constraints
  /^(dev-[^,\s@]+?|[^,\s@]+?\.x-dev)#.+$/i.match(constraints) do |match|
    constraints = match[1]
  end

  or_groups = []
  or_constraints = constraints.strip.split(/\s*\|\|?\s*/)
  or_constraints.each do |or_constraint|

    and_constraints = or_constraint.split(/(?<!^|as|[=>< ,]) *(?<!-)[, ](?!-) *(?!,|as|$)/)

    if and_constraints.length > 1
      constraint_objects = []
      and_constraints.each do |and_constraint|
        parse_constraint(and_constraint).each {|parsed_constraint| constraint_objects << parsed_constraint }
      end
    else
      constraint_objects = parse_constraint(and_constraints[0])
    end

    if constraint_objects.length.equal?(1)
      constraint = constraint_objects[0]
    else
      constraint = ::Composer::Semver::Constraint::MultiConstraint.new(constraint_objects)
    end

    or_groups << constraint
  end

  if or_groups.length.equal?(1)
    constraint = or_groups[0]
  else
    constraint = ::Composer::Semver::Constraint::MultiConstraint.new(or_groups, false)
  end

  constraint.pretty_string = pretty_constraint

  constraint
end
parse_numeric_alias_prefix(branch) click to toggle source

Extract numeric prefix from alias, if it is in numeric format, suitable for version comparison.

@param branch string The branch name to parse (e.g. 2.1.x-dev)

@return string|false The numeric prefix if present (e.g. 2.1.) or false

# File lib/composer/semver/version_parser.rb, line 206
def parse_numeric_alias_prefix(branch)
  /^(?<version>(\d+\.)*\d+)(?:\.x)?-dev$/i.match(branch) do |matches|
    return "#{matches['version']}."
  end
  false
end

Private Instance Methods

expand_stability(stability) click to toggle source
# File lib/composer/semver/version_parser.rb, line 564
def expand_stability(stability)
  stability = stability.downcase
  case stability
  when 'a'
    'alpha'
  when 'b'
    'beta'
  when 'p', 'pl'
    'patch'
  when 'rc'
    'RC'
  else
    stability
  end
end
manipulate_version_string(matches, position, increment = 0, pad = '0') click to toggle source

Increment, decrement, or simply pad a version number. Support function for {@link parse_constraint()}

Params: matches Array with version parts in array indexes 1,2,3,4 position Integer 1,2,3,4 - which segment of the version to decrement increment Integer pad String The string to pad version parts after position

Returns: string The new version

# File lib/composer/semver/version_parser.rb, line 544
def manipulate_version_string(matches, position, increment = 0, pad = '0')
  component = !matches.kind_of?(Array) ? matches.to_a : matches
  4.downto(1).each do |i|
    if i > position
      component[i] = pad
    elsif i === position && increment
      component[i] = component[i].to_i + increment
      # If component[i] was 0, carry the decrement
      if component[i] < 0
        component[i] = pad
        position -= 1

        # Return nil on a carry overflow
        return nil if i === 1
      end
    end
  end
  "#{component[1]}.#{component[2]}.#{component[3]}.#{component[4]}"
end
parse_constraint(constraint) click to toggle source

Parse a single constraint

@param constraint string The constraint to parse.

@raises UnexpectedValueError

@return array

# File lib/composer/semver/version_parser.rb, line 317
def parse_constraint(constraint)

  error = nil
  stabilities = ::Composer::Semver::VersionParser::stabilities.join('|')
  stability_modifier = ''
  /^([^,\s]+?)@(#{stabilities})$/i.match(constraint) do |match|
    constraint = match[1]
    if match[2] != 'stable'
      stability_modifier = match[2]
    end
  end

  if /^v?[xX*](\.[xX*])*$/i.match(constraint)
    return [
        Composer::Semver::Constraint::EmptyConstraint.new
    ]
  end

  version_regex = 'v?(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.(\d+))?' + self.class.modifier_regex + '(?:\+[^\s]+)?'

  # Helper method to check presence
  present = lambda {|v| !v.nil? && !v.empty? }

  # Tilde Range
  #
  # Like wildcard constraints, un-suffixed tilde constraints say that they must be greater than the previous
  # version, to ensure that unstable instances of the current version are allowed. However, if a stability
  # suffix is added to the constraint, then a >= match on the current version is used instead.
  /^~>?#{version_regex}$/i.match(constraint) do |matches|

    if constraint.start_with?('~>')
      raise ArgumentError,
            "Could not parse version constraint #{constraint}:
            Invalid operator \"~>\", you probably meant to use the \"~\" operator"
    end

    # Work out which position in the version we are operating at
    if present.call(matches[4])
      position = 4
    elsif present.call(matches[3])
      position = 3
    elsif present.call(matches[2])
      position = 2
    else
      position = 1
    end

    # Calculate the stability suffix
    stability_suffix = ''
    unless matches[5].nil? || matches[5].empty?
      stability_suffix << "-#{expand_stability(matches[5])}"
      unless matches[6].nil? || matches[6].empty?
        stability_suffix << matches[6]
      end
    end

    unless matches[7].nil? || matches[7].empty?
      stability_suffix << '-dev'
    end

    if stability_suffix.empty?
      stability_suffix = '-dev'
    end

    low_version = manipulate_version_string(matches, position, 0) + stability_suffix
    lower_bound = Composer::Semver::Constraint::Constraint.new('>=', low_version)

    # For upper bound, we increment the position of one more significance,
    # but high_position = 0 would be illegal
    high_position = [1, position - 1].max
    high_version = manipulate_version_string(matches, high_position, 1) + '-dev'
    upper_bound = Composer::Semver::Constraint::Constraint.new('<', high_version)

    return [
        lower_bound,
        upper_bound
    ]
  end

  # Caret Range
  #
  # Allows changes that do not modify the left-most non-zero digit in the [major, minor, patch] tuple.
  # In other words, this allows patch and minor updates for versions 1.0.0 and above, patch updates for
  # versions 0.X >=0.1.0, and no updates for versions 0.0.X
  /^\^#{version_regex}($)/i.match(constraint) do |matches|

    # Work out which position in the version we are operating at
    if matches[1] != '0' || matches[2].nil? || matches[2] === ''
      position = 1
    elsif matches[2] != '0' || matches[3].nil? || matches[3] === ''
      position = 2
    else
      position = 3
    end

    # Calculate the stability suffix
    stability_suffix = ''
    if (matches[5].nil? || matches[5].empty?) && (matches[7].nil? || matches[7].empty?)
      stability_suffix << '-dev'
    end

    low_pretty = "#{constraint}#{stability_suffix}"
    low_version = normalize(low_pretty[1..low_pretty.length - 1])
    lower_bound = Composer::Semver::Constraint::Constraint.new('>=', low_version)

    # For upper bound, we increment the position of one more significance,
    # but high_position = 0 would be illegal
    high_version = manipulate_version_string(matches, position, 1) + '-dev'
    upper_bound = Composer::Semver::Constraint::Constraint.new('<', high_version)

    return [
        lower_bound,
        upper_bound
    ]
  end

  # X Range
  #
  # Any of X, x, or * may be used to "stand in" for one of the numeric values in the [major, minor, patch] tuple.
  # A partial version range is treated as an X-Range, so the special character is in fact optional.
  /^v?(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.[xX*])+$/.match(constraint) do |matches|

    if present.call(matches[3])
      position = 3
    elsif present.call(matches[2])
      position = 2
    else
      position = 1
    end

    low_version = manipulate_version_string(matches, position) + '-dev'
    high_version = manipulate_version_string(matches, position, 1) + '-dev'

    if low_version === '0.0.0.0-dev'
      return [
          ::Composer::Semver::Constraint::Constraint.new('<', high_version)
      ]
    end

    return [
        ::Composer::Semver::Constraint::Constraint.new('>=', low_version),
        ::Composer::Semver::Constraint::Constraint.new('<', high_version)
    ]
  end

  # Hyphen Range
  #
  # Specifies an inclusive set. If a partial version is provided as the first version in the inclusive range,
  # then the missing pieces are replaced with zeroes. If a partial version is provided as the second version in
  # the inclusive range, then all versions that start with the supplied parts of the tuple are accepted, but
  # nothing that would be greater than the provided tuple parts.
  #
  # We don't use named groups since ruby will return only the named matches and not the rest
  # /^(?<from>#{version_regex}) +- +(?<to>#{version_regex})($)/i.match(constraint) do |matches|
  /^(#{version_regex}) +- +(#{version_regex})($)/i.match(constraint) do |matches|

    match_from = matches[1]
    match_to = matches[9]

    # calculate the stability suffix
    if (matches[6].nil? || matches[6].empty?) && (matches[8].nil? || matches[8].empty?)
      low_stability_suffix = '-dev'
    else
      low_stability_suffix = ''
    end

    low_version = normalize(match_from)
    lower_bound = ::Composer::Semver::Constraint::Constraint.new('>=', low_version + low_stability_suffix)

    not_zero_or_empty = lambda {|x| (x == 0 || x == '0') ? false : (x.nil? || x.empty?) }

    if (!not_zero_or_empty.call(matches[11]) && !not_zero_or_empty.call(matches[12])) || present.call(matches[14]) || present.call(matches[16])
      high_version = normalize(match_to)
      upper_bound = ::Composer::Semver::Constraint::Constraint.new('<=', high_version)
    else
      high_match = ['', matches[10], matches[11], matches[12], matches[13]]
      high_version = manipulate_version_string(high_match, ( not_zero_or_empty.call(matches[11]) ? 1 : 2), 1) + '-dev'
      upper_bound = ::Composer::Semver::Constraint::Constraint.new('<', high_version)
    end

    return [
        lower_bound,
        upper_bound
    ]
  end

  # Basic Comparators
  /^(<>|!=|>=?|<=?|==?)?\s*(.*)/.match(constraint) do |matches|
    begin

      version = normalize(matches[2])

      if !stability_modifier.empty? && self.class.parse_stability(version) === 'stable'
        version << "-#{stability_modifier}"
      elsif matches[1] === '<' || matches[1] === '>='
        unless /-#{self.class.modifier_regex}$/.match(matches[2].downcase)
          unless matches[2].start_with?('dev-')
            version << '-dev'
          end
        end
      end
      operator = matches[1].nil? ? '=' : matches[1]
      return [
          ::Composer::Semver::Constraint::Constraint.new(operator, version)
      ]
    rescue => e
      error = e
      # ignore
    end
  end

  message = "Could not parse version constraint #{constraint}"
  message << ": #{error.message}" unless error.nil?
  raise ArgumentError, message
end