/
debug.rb
166 lines (136 loc) · 4.9 KB
/
debug.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# 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) do # rubocop:disable Metrics/BlockLength
# @return [String] a Rainbow colorized version of ruby
def colorize(color_scheme = COLOR_SCHEME)
map = color_map(color_scheme)
ast.loc.expression.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_descendant
.to_a
.prepend(ast)
.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
attr_reader :pattern, :compiler, :node_pattern
def initialize(pattern)
@pattern = pattern
@compiler = ::RuboCop::AST::NodePattern::Compiler::Debug.new
@node_pattern = ::RuboCop::AST::NodePattern.new(pattern, compiler: @compiler)
end
# @return [Node] the Ruby AST
def test(ruby)
ruby = ruby_ast(ruby) if ruby.is_a?(String)
trace = Trace.new
returned = @node_pattern.as_lambda.call(ruby, trace: trace)
Result.new(self, trace, returned)
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)
id = compiler.node_ids[node]
"trace.#{kind}(#{id})"
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