lib/rubocop/cop/utils/regexp_ranges.rb
# frozen_string_literal: true
module RuboCop
module Cop
module Utils
# Helper to abstract complexity of building range pairs
# with octal escape reconstruction (needed for regexp_parser < 2.7).
class RegexpRanges
attr_reader :root
def initialize(root)
@root = root
@compound_token = []
@pairs = []
@populated = false
end
def compound_token
populate_all unless @populated
@compound_token
end
def pairs
populate_all unless @populated
@pairs
end
private
def populate_all
populate(@root)
# If either bound is a compound the first one is an escape
# and that's all we need to work with.
# If there are any cops that wanted to operate on the compound
# expression we could wrap it with a facade class.
@pairs.map! { |pair| pair.map(&:first) }
@populated = true
end
def populate(expr)
expressions = expr.expressions.to_a
until expressions.empty?
current = expressions.shift
if escaped_octal?(current)
@compound_token << current
@compound_token.concat(pop_octal_digits(expressions))
# If we have all the digits we can discard.
end
next unless current.type == :set
process_set(expressions, current)
@compound_token.clear
end
end
def process_set(expressions, current)
case current.token
when :range
@pairs << compose_range(expressions, current)
when :character
# Child expressions may include the range we are looking for.
populate(current)
when :intersection
# Each child expression could have child expressions that lead to ranges.
current.expressions.each do |intersected|
populate(intersected)
end
end
end
def compose_range(expressions, current)
range_start, range_end = current.expressions
range_start = if @compound_token.size.between?(1, 2) && octal_digit?(range_start.text)
@compound_token.dup << range_start
else
[range_start]
end
range_end = [range_end]
range_end.concat(pop_octal_digits(expressions)) if escaped_octal?(range_end.first)
[range_start, range_end]
end
def escaped_octal?(expr)
expr.text.valid_encoding? && expr.text =~ /^\\[0-7]$/
end
def octal_digit?(char)
('0'..'7').cover?(char)
end
def pop_octal_digits(expressions)
digits = []
2.times do
next unless (next_child = expressions.first)
next unless next_child.type == :literal && next_child.text =~ /^[0-7]$/
digits << expressions.shift
end
digits
end
end
end
end
end