class RuboCop::Cop::Lint::RedundantRegexpQuantifiers

Checks for redundant quantifiers inside Regexp literals.

It is always allowed when interpolation is used in a regexp literal, because it’s unknown what kind of string will be expanded as a result:

source,ruby

/(?:a*#{interpolation})?/x


@example

# bad
/(?:x+)+/

# good
/(?:x)+/

# good
/(?:x+)/

# bad
/(?:x+)?/

# good
/(?:x)*/

# good
/(?:x*)/

Constants

MSG_REDUNDANT_QUANTIFIER

Public Instance Methods

on_regexp(node) click to toggle source
# File lib/rubocop/cop/lint/redundant_regexp_quantifiers.rb, line 42
def on_regexp(node)
  return if node.interpolation?

  each_redundantly_quantified_pair(node) do |group, child|
    replacement = merged_quantifier(group, child)
    add_offense(
      quantifier_range(group, child),
      message: message(group, child, replacement)
    ) do |corrector|
      # drop outer quantifier
      corrector.replace(group.loc.quantifier, '')
      # replace inner quantifier
      corrector.replace(child.loc.quantifier, replacement)
    end
  end
end

Private Instance Methods

character_set?(expr) click to toggle source
# File lib/rubocop/cop/lint/redundant_regexp_quantifiers.rb, line 83
def character_set?(expr)
  expr.is?(:character, :set)
end
each_redundantly_quantified_pair(node) { |expr, subexp| ... } click to toggle source
# File lib/rubocop/cop/lint/redundant_regexp_quantifiers.rb, line 61
def each_redundantly_quantified_pair(node)
  seen = Set.new
  node.parsed_tree&.each_expression do |(expr)|
    next if seen.include?(expr) || !redundant_group?(expr) || !mergeable_quantifier(expr)

    expr.each_expression do |(subexp)|
      seen << subexp
      break unless redundantly_quantifiable?(subexp)

      yield(expr, subexp) if mergeable_quantifier(subexp)
    end
  end
end
mergeable_quantifier(expr) click to toggle source
# File lib/rubocop/cop/lint/redundant_regexp_quantifiers.rb, line 87
def mergeable_quantifier(expr)
  # Merging reluctant or possessive quantifiers would be more complex,
  # and Ruby does not emit warnings for these cases.
  return unless expr.quantifier&.greedy?

  # normalize quantifiers, e.g. "{1,}" => "+"
  case expr.quantity
  when [0, -1]
    '*'
  when [0, 1]
    '?'
  when [1, -1]
    '+'
  end
end
merged_quantifier(exp1, exp2) click to toggle source
# File lib/rubocop/cop/lint/redundant_regexp_quantifiers.rb, line 103
def merged_quantifier(exp1, exp2)
  quantifier1 = mergeable_quantifier(exp1)
  quantifier2 = mergeable_quantifier(exp2)
  if quantifier1 == quantifier2
    # (?:a+)+ equals (?:a+) ; (?:a*)* equals (?:a*) ; # (?:a?)? equals (?:a?)
    quantifier1
  else
    # (?:a+)*, (?:a+)?, (?:a*)+, (?:a*)?, (?:a?)+, (?:a?)* - all equal (?:a*)
    '*'
  end
end
message(group, child, replacement) click to toggle source
# File lib/rubocop/cop/lint/redundant_regexp_quantifiers.rb, line 119
def message(group, child, replacement)
  format(
    MSG_REDUNDANT_QUANTIFIER,
    inner_quantifier: child.quantifier.to_s,
    outer_quantifier: group.quantifier.to_s,
    replacement: replacement
  )
end
quantifier_range(group, child) click to toggle source
# File lib/rubocop/cop/lint/redundant_regexp_quantifiers.rb, line 115
def quantifier_range(group, child)
  range_between(child.loc.quantifier.begin_pos, group.loc.quantifier.end_pos)
end
redundant_group?(expr) click to toggle source
# File lib/rubocop/cop/lint/redundant_regexp_quantifiers.rb, line 75
def redundant_group?(expr)
  expr.is?(:passive, :group) && expr.count { |child| child.type != :free_space } == 1
end
redundantly_quantifiable?(node) click to toggle source
# File lib/rubocop/cop/lint/redundant_regexp_quantifiers.rb, line 79
def redundantly_quantifiable?(node)
  redundant_group?(node) || character_set?(node) || node.terminal?
end