rubocop-hq/rubocop-ast

View on GitHub
lib/rubocop/ast/node_pattern/compiler/debug.rb

Summary

Maintainability
A
0 mins
Test Coverage
# frozen_string_literal: true

require 'rainbow'

module RuboCop
  module AST
    class NodePattern
      class Compiler
        # Variant of the Compiler with tracing information for nodes
        class Debug < Compiler
          # Compiled node pattern requires a named parameter `trace`,
          # which should be an instance of this class
          class Trace
            def initialize
              @visit = {}
            end

            def enter(node_id)
              @visit[node_id] = false
              true
            end

            def success(node_id)
              @visit[node_id] = true
            end

            # return nil (not visited), false (not matched) or true (matched)
            def matched?(node_id)
              @visit[node_id]
            end
          end

          attr_reader :node_ids

          # @api private
          class Colorizer
            COLOR_SCHEME = {
              not_visitable: :lightseagreen,
              nil => :yellow,
              false => :red,
              true => :green
            }.freeze

            # Result of a NodePattern run against a particular AST
            # Consider constructor is private
            Result = Struct.new(:colorizer, :trace, :returned, :ruby_ast) do
              # @return [String] a Rainbow colorized version of ruby
              def colorize(color_scheme = COLOR_SCHEME)
                map = color_map(color_scheme)
                ast.source_range.source_buffer.source.chars.map.with_index do |char, i|
                  Rainbow(char).color(map[i])
                end.join
              end

              # @return [Hash] a map for {character_position => color}
              def color_map(color_scheme = COLOR_SCHEME)
                @color_map ||=
                  match_map
                  .transform_values { |matched| color_scheme.fetch(matched) }
                  .map { |node, color| color_map_for(node, color) }
                  .inject(:merge)
                  .tap { |h| h.default = color_scheme.fetch(:not_visitable) }
              end

              # @return [Hash] a map for {node => matched?}, depth-first
              def match_map
                @match_map ||=
                  ast
                  .each_node
                  .to_h { |node| [node, matched?(node)] }
              end

              # @return a value of `Trace#matched?` or `:not_visitable`
              def matched?(node)
                id = colorizer.compiler.node_ids.fetch(node) { return :not_visitable }
                trace.matched?(id)
              end

              private

              def color_map_for(node, color)
                return {} unless (range = node.loc&.expression)

                range.to_a.to_h { |char| [char, color] }
              end

              def ast
                colorizer.node_pattern.ast
              end
            end

            Compiler = Debug

            attr_reader :pattern, :compiler, :node_pattern

            def initialize(pattern, compiler: self.class::Compiler.new)
              @pattern = pattern
              @compiler = compiler
              @node_pattern = ::RuboCop::AST::NodePattern.new(pattern, compiler: @compiler)
            end

            # @return [Node] the Ruby AST
            def test(ruby, trace: self.class::Compiler::Trace.new)
              ruby = ruby_ast(ruby) if ruby.is_a?(String)
              returned = @node_pattern.as_lambda.call(ruby, trace: trace)
              self.class::Result.new(self, trace, returned, ruby)
            end

            private

            def ruby_ast(ruby)
              buffer = ::Parser::Source::Buffer.new('(ruby)', source: ruby)
              ruby_parser.parse(buffer)
            end

            def ruby_parser
              require 'parser/current'
              builder = ::RuboCop::AST::Builder.new
              ::Parser::CurrentRuby.new(builder)
            end
          end

          def initialize
            super
            @node_ids = Hash.new { |h, k| h[k] = h.size }.compare_by_identity
          end

          def named_parameters
            super << :trace
          end

          def parser
            @parser ||= Parser::WithMeta.new
          end

          def_delegators :parser, :comments, :tokens

          # @api private
          module InstrumentationSubcompiler
            def do_compile
              "#{tracer(:enter)} && #{super} && #{tracer(:success)}"
            end

            private

            def tracer(kind)
              "trace.#{kind}(#{node_id})"
            end

            def node_id
              compiler.node_ids[node]
            end
          end

          # @api private
          class NodePatternSubcompiler < Compiler::NodePatternSubcompiler
            include InstrumentationSubcompiler
          end

          # @api private
          class SequenceSubcompiler < Compiler::SequenceSubcompiler
            include InstrumentationSubcompiler
          end
        end
      end
    end
  end
end