-
-
Notifications
You must be signed in to change notification settings - Fork 3k
/
commissioner.rb
124 lines (104 loc) · 3.71 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
# 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
# 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
@callbacks = Hash.new { |h, k| h[k] = cops_callbacks_for(k) }
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)
define_method(method_name) do |node|
trigger_responding_cops(method_name, node)
super(node) unless NO_CHILD_NODES.include?(node_type)
end
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 cops_callbacks_for(callback)
@cops.select do |cop|
cop.respond_to?(callback)
end
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