rubocop-hq/rubocop

View on GitHub
lib/rubocop/ext/regexp_node.rb

Summary

Maintainability
A
0 mins
Test Coverage
C
73%
# 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