# frozen_string_literal: true module RuboCop module Ext # Extensions to AST::RegexpNode for our cached parsed regexp info module RegexpNode ANY = Object.new def ANY.==(_) true end private_constant :ANY # @return [Regexp::Expression::Root, nil] # Note: we extend Regexp nodes to provide `loc` and `expression` # see `ext/regexp_parser`. attr_reader :parsed_tree if Gem::Version.new(Regexp::Parser::VERSION) >= Gem::Version.new('2.0') def assign_properties(*) super str = with_interpolations_blanked @parsed_tree = begin Regexp::Parser.parse(str, options: options) rescue StandardError nil end origin = loc.begin.end @parsed_tree&.each_expression(true) { |e| e.origin = origin } end # Please remove this `else` branch when support for regexp_parser 1.8 will be dropped. # It's for compatibility with regexp_parser 1.8 and will never be maintained. else def assign_properties(*) super str = with_interpolations_blanked begin @parsed_tree = Regexp::Parser.parse(str, options: options) rescue StandardError @parsed_tree = nil else origin = loc.begin.end source = @parsed_tree.to_s @parsed_tree.each_expression(true) do |e| e.origin = origin e.source = source end end end end def each_capture(named: ANY) return enum_for(__method__, named: named) unless block_given? parsed_tree&.traverse do |event, exp, _index| yield(exp) if named_capturing?(exp, event, named) end self end private def named_capturing?(exp, event, named) event == :enter && named == exp.respond_to?(:name) && !exp.text.start_with?('(?<=') && exp.respond_to?(:capturing?) && exp.capturing? end def with_interpolations_blanked # Ignore the trailing regopt node children[0...-1].map do |child| source = child.source # We don't want to consider the contents of interpolations as part of the pattern source, # but need to preserve their width, to allow offsets to correctly line up with the # original source: spaces have no effect, and preserve width. if child.begin_type? ' ' * source.length else source end end.join end AST::RegexpNode.include self end end end