/
commissioner.rb
175 lines (149 loc) · 5.65 KB
/
commissioner.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
167
168
169
170
171
172
173
174
175
# frozen_string_literal: true
module RuboCop
module Cop
# Commissioner class is responsible for processing the AST and delegating
# work to the specified cops.
class Commissioner
include RuboCop::AST::Traversal
RESTRICTED_CALLBACKS = %i[on_send on_csend after_send after_csend].freeze
private_constant :RESTRICTED_CALLBACKS
# How a Commissioner returns the results of the investigation
# as a list of Cop::InvestigationReport and any errors caught
# during the investigation.
# Immutable
# Consider creation API private
InvestigationReport = Struct.new(:processed_source, :cop_reports, :errors) do
def cops
@cops ||= cop_reports.map(&:cop)
end
def offenses_per_cop
@offenses_per_cop ||= cop_reports.map(&:offenses)
end
def correctors
@correctors ||= cop_reports.map(&:corrector)
end
def offenses
@offenses ||= offenses_per_cop.flatten(1)
end
def merge(investigation)
InvestigationReport.new(processed_source,
cop_reports + investigation.cop_reports,
errors + investigation.errors)
end
end
attr_reader :errors
def initialize(cops, forces = [], options = {})
@cops = cops
@forces = forces
@options = options
initialize_callbacks
reset
end
# Create methods like :on_send, :on_super, etc. They will be called
# during AST traversal and try to call corresponding methods on cops.
# A call to `super` is used
# to continue iterating over the children of a node.
# However, if we know that a certain node type (like `int`) never has
# child nodes, there is no reason to pay the cost of calling `super`.
Parser::Meta::NODE_TYPES.each do |node_type|
method_name = :"on_#{node_type}"
next unless method_defined?(method_name)
# Hacky: Comment-out code as needed
r = '#' unless RESTRICTED_CALLBACKS.include?(method_name) # has Restricted?
c = '#' if NO_CHILD_NODES.include?(node_type) # has Children?
class_eval(<<~RUBY, __FILE__, __LINE__ + 1)
def on_#{node_type}(node) # def on_send(node)
trigger_responding_cops(:on_#{node_type}, node) # trigger_responding_cops(:on_send, node)
#{r} trigger_restricted_cops(:on_#{node_type}, node) # trigger_restricted_cops(:on_send, node)
#{c} super(node) # super(node)
#{c} trigger_responding_cops(:after_#{node_type}, node) # trigger_responding_cops(:after_send, node)
#{c}#{r} trigger_restricted_cops(:after_#{node_type}, node) # trigger_restricted_cops(:after_send, node)
end # end
RUBY
end
# @return [InvestigationReport]
def investigate(processed_source)
reset
@cops.each { |cop| cop.send :begin_investigation, processed_source }
if processed_source.valid_syntax?
invoke(:on_new_investigation, @cops)
invoke(:investigate, @forces, processed_source)
walk(processed_source.ast) unless @cops.empty?
invoke(:on_investigation_end, @cops)
else
invoke(:on_other_file, @cops)
end
reports = @cops.map { |cop| cop.send(:complete_investigation) }
InvestigationReport.new(processed_source, reports, @errors)
end
private
def trigger_responding_cops(callback, node)
@callbacks[callback]&.each do |cop|
with_cop_error_handling(cop, node) do
cop.send(callback, node)
end
end
end
def reset
@errors = []
end
def initialize_callbacks
@callbacks = build_callbacks(@cops)
@restricted_map = restrict_callbacks(@callbacks)
end
def build_callbacks(cops)
callbacks = {}
cops.each do |cop|
cop.callbacks_needed.each do |callback|
(callbacks[callback] ||= []) << cop
end
end
callbacks
end
def restrict_callbacks(callbacks)
restricted = {}
RESTRICTED_CALLBACKS.each do |callback|
restricted[callback] = restricted_map(callbacks[callback])
end
restricted
end
def trigger_restricted_cops(event, node)
name = node.method_name
@restricted_map[event][name]&.each do |cop|
with_cop_error_handling(cop, node) do
cop.send(event, node)
end
end
end
# Note: mutates `callbacks` in place
def restricted_map(callbacks)
map = {}
callbacks&.select! do |cop|
restrictions = cop.class.send :restrict_on_send
restrictions.each do |name|
(map[name] ||= []) << cop
end
restrictions.empty?
end
map
end
def invoke(callback, cops, *args)
cops.each do |cop|
with_cop_error_handling(cop) do
cop.send(callback, *args)
end
end
end
# Allow blind rescues here, since we're absorbing and packaging or
# re-raising exceptions that can be raised from within the individual
# cops' `#investigate` methods.
def with_cop_error_handling(cop, node = nil)
yield
rescue StandardError => e
raise e if @options[:raise_error]
err = ErrorWithAnalyzedFileLocation.new(cause: e, node: node, cop: cop)
@errors << err
end
end
end
end