Skip to content

Commit efb8e76

Browse files
committed
Refactor Rubocop runner into more objects
Code Climate triggered a warning that this class was too large and looking closely it seems to have several different responsibilities mixed together. At the same time, a lot of the language used in this engine doesn't match the language of how we talk about our domain. These changes aim to break down the Rubocop class into collaborators and to refine the language to bring its concerns closer to our domain language. * Introduce SourceFile * Rename ViolationDecorator to Issue * Move presentation concerns into Issue * Refine remediation point names to better match what they represent * Begin to improve coverage on remediation point generation
1 parent 472aa51 commit efb8e76

10 files changed

+301
-220
lines changed

.rubocop.yml

+7-3
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,14 @@ Style/PercentLiteralDelimiters:
2121
'%W': '[]'
2222
Style/StringLiterals:
2323
Enabled: false
24-
Style/TrailingComma:
25-
Enabled: false
2624
Style/DotPosition:
2725
EnforcedStyle: 'trailing'
2826
Enabled: true
29-
Style/MultilineOperationIndentation:
27+
Style/MultilineMethodCallIndentation:
28+
Enabled: false
29+
Style/TrailingCommaInArguments:
30+
Enabled: false
31+
Style/TrailingCommaInLiteral:
32+
Enabled: false
33+
Style/ConditionalAssignment:
3034
Enabled: false

config/cops.yml

+10-10
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,25 @@
11
Metrics/AbcSize:
22
base_points: 1_000_000
3-
violation_points: 70_000
3+
overage_points: 70_000
44
Metrics/BlockNesting:
5-
base_points: 100_000
5+
remediation_points: 100_000
66
Metrics/ClassLength:
77
base_points: 5_000_000
8-
violation_points: 35_000
9-
Metrics/CyclomaticComplexity: # This check is per method
8+
overage_points: 35_000
9+
Metrics/CyclomaticComplexity:
1010
base_points: 1_000_000
11-
violation_points: 70_000
11+
overage_points: 70_000
1212
Metrics/LineLength:
13-
base_points: 50_000
13+
remediation_points: 50_000
1414
Metrics/MethodLength:
1515
base_points: 1_000_000
16-
violation_points: 70_000
16+
overage_points: 70_000
1717
Metrics/ModuleLength:
1818
base_points: 5_000_000
19-
violation_points: 35_000
19+
overage_points: 35_000
2020
Metrics/ParameterList:
2121
base_points: 500_000
22-
violation_points: 100_000
22+
overage_points: 100_000
2323
Metrics/PerceivedComplexity:
2424
base_points: 1_000_000
25-
violation_points: 70_000
25+
overage_points: 70_000

lib/cc/engine/file_list_resolver.rb

+7-7
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
module CC
22
module Engine
33
class FileListResolver
4-
def initialize(code:, engine_config: {}, rubocop_config_store:)
5-
@code = code
4+
def initialize(root:, engine_config: {}, config_store:)
5+
@root = root
66
@exclude_paths = engine_config["exclude_paths"] || []
77
@include_paths = engine_config["include_paths"]
8-
@rubocop_config_store = rubocop_config_store
8+
@config_store = config_store
99
end
1010

1111
def expanded_list
@@ -39,21 +39,21 @@ def include_based_files_to_inspect
3939
end
4040

4141
def local_path(path)
42-
realpath = Pathname.new(@code).realpath.to_s
42+
realpath = Pathname.new(@root).realpath.to_s
4343
path.gsub(%r{^#{realpath}/}, '')
4444
end
4545

4646
def rubocop_file_to_include?(file)
4747
if file =~ /\.rb$/
4848
true
4949
else
50-
dir, basename = File.split(file)
51-
@rubocop_config_store.for(dir).file_to_include?(basename)
50+
root, basename = File.split(file)
51+
@config_store.for(root).file_to_include?(basename)
5252
end
5353
end
5454

5555
def rubocop_runner
56-
@rubocop_runner ||= RuboCop::Runner.new({}, @rubocop_config_store)
56+
@rubocop_runner ||= RuboCop::Runner.new({}, @config_store)
5757
end
5858
end
5959
end

lib/cc/engine/issue.rb

+125
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
require 'safe_yaml'
2+
SafeYAML::OPTIONS[:default_mode] = :safe
3+
4+
module CC
5+
module Engine
6+
class Issue < SimpleDelegator
7+
MULTIPLIER_REGEX = %r{\[([\d\.]+)\/([\d\.]+)\]}
8+
DEFAULT_REMEDIATION_POINTS = 50_000
9+
DEFAULT_BASE_POINTS = 200_000
10+
DEFAULT_OVERAGE_POINTS = 50_000
11+
12+
def initialize(issue, path, cop_list: nil)
13+
@path = path
14+
@cop_list = cop_list
15+
super(issue)
16+
end
17+
18+
# rubocop:disable Metrics/MethodLength
19+
def to_json
20+
hash = {
21+
type: "Issue",
22+
check_name: "Rubocop/#{cop_name}",
23+
description: message,
24+
categories: [category],
25+
remediation_points: remediation_points,
26+
location: {
27+
path: path,
28+
positions: positions,
29+
},
30+
}
31+
hash[:content] = { body: content_body } if content_body.present?
32+
hash.to_json
33+
end
34+
35+
def remediation_points
36+
if multiplier?
37+
base_points + overage_points
38+
else
39+
cop_definition.fetch("remediation_points", DEFAULT_REMEDIATION_POINTS)
40+
end
41+
end
42+
43+
private
44+
45+
attr_reader :path
46+
47+
def multiplier?
48+
message.match(MULTIPLIER_REGEX)
49+
end
50+
51+
def base_points
52+
cop_definition.fetch("base_points", DEFAULT_BASE_POINTS)
53+
end
54+
55+
def cop_definition
56+
@cop_definition ||= cop_list.fetch(cop_name, {})
57+
end
58+
59+
def cop_list
60+
@cop_list ||= YAML.load_file(expand_config_path("cops.yml"))
61+
end
62+
63+
def expand_config_path(path)
64+
File.expand_path("../../../../config/#{path}", __FILE__)
65+
end
66+
67+
def overage_points
68+
overage_points = cop_definition.
69+
fetch("overage_points", DEFAULT_OVERAGE_POINTS)
70+
71+
overage_points * multiplier
72+
end
73+
74+
def multiplier
75+
result = message.scan(MULTIPLIER_REGEX)
76+
score, max = result[0]
77+
score.to_i - max.to_i
78+
end
79+
80+
def category
81+
CategoryParser.new(cop_name).category
82+
end
83+
84+
def positions
85+
{
86+
begin: {
87+
column: columns.first,
88+
line: lines.first,
89+
},
90+
end: {
91+
column: columns.last,
92+
line: lines.last,
93+
}
94+
}
95+
end
96+
97+
# Increment column value as columns are 0-based in parser
98+
def columns
99+
return @columns if defined?(@columns)
100+
101+
if location.is_a?(RuboCop::Cop::Lint::Syntax::PseudoSourceRange)
102+
@columns = [location.column + 1, location.column + 1]
103+
else
104+
@columns = [location.column + 1, location.last_column + 1]
105+
end
106+
end
107+
108+
def lines
109+
return @lines if defined?(@lines)
110+
111+
if location.is_a?(RuboCop::Cop::Lint::Syntax::PseudoSourceRange)
112+
@lines = [location.line, location.line]
113+
else
114+
@lines = [location.first_line, location.last_line]
115+
end
116+
end
117+
118+
def content_body
119+
return @content_body if defined?(@content_body)
120+
content_path = expand_config_path("contents/#{cop_name.underscore}.md")
121+
@content_body = File.exist?(content_path) && File.read(content_path)
122+
end
123+
end
124+
end
125+
end

lib/cc/engine/rubocop.rb

+23-98
Original file line numberDiff line numberDiff line change
@@ -1,133 +1,58 @@
11
require "json"
2+
require "delegate"
23
require "pathname"
34
require "rubocop"
45
require "rubocop/cop/method_complexity_patch"
56
require "rubocop/cop/method_length"
67
require "rubocop/cop/class_length"
8+
require "cc/engine/source_file"
79
require "cc/engine/category_parser"
810
require "cc/engine/file_list_resolver"
9-
require "cc/engine/violation_decorator"
11+
require "cc/engine/issue"
1012
require "active_support"
1113
require "active_support/core_ext"
1214

1315
module CC
1416
module Engine
1517
class Rubocop
16-
def initialize(code, engine_config, io)
17-
@code = code
18+
def initialize(root, engine_config, io)
19+
@root = root
1820
@engine_config = engine_config || {}
1921
@io = io
2022
end
2123

2224
def run
23-
Dir.chdir(@code) do
25+
Dir.chdir(root) do
2426
files_to_inspect.each do |path|
25-
inspect_file(path)
27+
SourceFile.new(
28+
config_store: config_store.for(path),
29+
io: io,
30+
path: path,
31+
root: root,
32+
).inspect
2633
end
2734
end
2835
end
2936

3037
private
3138

32-
def files_to_inspect
33-
@file_list_resolver = FileListResolver.new(
34-
code: @code,
35-
engine_config: @engine_config,
36-
rubocop_config_store: rubocop_config_store
37-
)
38-
@file_list_resolver.expanded_list
39-
end
40-
41-
def category(cop_name)
42-
CategoryParser.new(cop_name).category
43-
end
44-
45-
def inspect_file(path)
46-
ruby_version = target_ruby_version(path)
47-
parsed = RuboCop::ProcessedSource.from_file(path, ruby_version)
48-
rubocop_team_for_path(path).inspect_file(parsed).each do |violation|
49-
next if violation.disabled?
50-
decorated_violation = ViolationDecorator.new(violation)
51-
json = violation_json(decorated_violation, local_path(path))
52-
@io.print "#{json}\0"
53-
end
54-
end
39+
attr_reader :root, :engine_config, :io
5540

56-
def local_path(path)
57-
realpath = Pathname.new(@code).realpath.to_s
58-
path.gsub(%r{^#{realpath}/}, '')
41+
def files_to_inspect
42+
@files_to_inspect ||= FileListResolver.new(
43+
config_store: config_store,
44+
engine_config: engine_config,
45+
root: root,
46+
).expanded_list
5947
end
6048

61-
def rubocop_config_store
62-
@rubocop_config_store ||= begin
63-
Dir.chdir(@code) do
64-
config_store = RuboCop::ConfigStore.new
65-
if (config_file = @engine_config["config"])
66-
config_store.options_config = config_file
67-
end
68-
config_store
49+
def config_store
50+
@config_store ||= RuboCop::ConfigStore.new.tap do |config_store|
51+
if (config_file = engine_config["config"])
52+
config_store.options_config = config_file
6953
end
7054
end
7155
end
72-
73-
def rubocop_team_for_path(path)
74-
rubocop_config = rubocop_config_store.for(path)
75-
RuboCop::Cop::Team.new(RuboCop::Cop::Cop.all, rubocop_config)
76-
end
77-
78-
def target_ruby_version(path)
79-
config_store = rubocop_config_store.for(path)
80-
config_store["AllCops"] && config_store["AllCops"]["TargetRubyVersion"]
81-
end
82-
83-
def violation_positions(location)
84-
if location.is_a?(RuboCop::Cop::Lint::Syntax::PseudoSourceRange)
85-
first_line = location.line
86-
last_line = location.line
87-
first_column = location.column
88-
last_column = location.column
89-
else
90-
first_line = location.first_line
91-
last_line = location.last_line
92-
first_column = location.column
93-
last_column = location.last_column
94-
end
95-
96-
{
97-
begin: {
98-
column: first_column + 1, # columns are 0-based in Parser
99-
line: first_line,
100-
},
101-
end: {
102-
column: last_column + 1,
103-
line: last_line,
104-
}
105-
}
106-
end
107-
108-
def violation_json(violation, local_path)
109-
violation_hash = {
110-
type: "Issue",
111-
check_name: "Rubocop/#{violation.cop_name}",
112-
description: violation.message,
113-
categories: [category(violation.cop_name)],
114-
remediation_points: violation.remediation_points,
115-
location: {
116-
path: local_path,
117-
positions: violation_positions(violation.location),
118-
},
119-
}
120-
body = content_body(violation.cop_name)
121-
violation_hash.merge!(content: { body: body }) if body.present?
122-
violation_hash.to_json
123-
end
124-
125-
def content_body(cop_name)
126-
path = File.expand_path(
127-
"../../../../config/contents/#{cop_name.underscore}.md", __FILE__
128-
)
129-
File.exist?(path) ? File.read(path) : nil
130-
end
13156
end
13257
end
13358
end

0 commit comments

Comments
 (0)