Skip to content

Commit 58f5486

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 58f5486

File tree

9 files changed

+293
-217
lines changed

9 files changed

+293
-217
lines changed

config/cops.yml

Lines changed: 10 additions & 10 deletions
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

Lines changed: 7 additions & 7 deletions
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

Lines changed: 125 additions & 0 deletions
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.merge!(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

Lines changed: 22 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -4,130 +4,54 @@
44
require "rubocop/cop/method_complexity_patch"
55
require "rubocop/cop/method_length"
66
require "rubocop/cop/class_length"
7+
require "cc/engine/source_file"
78
require "cc/engine/category_parser"
89
require "cc/engine/file_list_resolver"
9-
require "cc/engine/violation_decorator"
10+
require "cc/engine/issue"
1011
require "active_support"
1112
require "active_support/core_ext"
1213

1314
module CC
1415
module Engine
1516
class Rubocop
16-
def initialize(code, engine_config, io)
17-
@code = code
17+
def initialize(root, engine_config, io)
18+
@root = root
1819
@engine_config = engine_config || {}
1920
@io = io
2021
end
2122

2223
def run
23-
Dir.chdir(@code) do
24+
Dir.chdir(root) do
2425
files_to_inspect.each do |path|
25-
inspect_file(path)
26+
SourceFile.new(
27+
config_store: config_store.for(path),
28+
io: io,
29+
path: path,
30+
root: root,
31+
).inspect
2632
end
2733
end
2834
end
2935

3036
private
3137

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
38+
attr_reader :root, :engine_config, :io
5539

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

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
48+
def config_store
49+
@config_store ||= RuboCop::ConfigStore.new.tap do |config_store|
50+
if (config_file = engine_config["config"])
51+
config_store.options_config = config_file
6952
end
7053
end
7154
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
13155
end
13256
end
13357
end

lib/cc/engine/source_file.rb

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
module CC
2+
module Engine
3+
class SourceFile
4+
def initialize(config_store:, io:, path:, root:)
5+
@config_store = config_store
6+
@io = io
7+
@path = path
8+
@root = root
9+
end
10+
11+
def inspect
12+
rubocop_team.inspect_file(processed_source).each do |offense|
13+
next if offense.disabled?
14+
15+
io.print Issue.new(offense, display_path).to_json
16+
io.print "\0"
17+
end
18+
end
19+
20+
private
21+
22+
attr_reader :config_store, :io, :path, :root
23+
24+
def processed_source
25+
RuboCop::ProcessedSource.from_file(path, target_ruby_version)
26+
end
27+
28+
def target_ruby_version
29+
config_store["AllCops"] && config_store["AllCops"]["TargetRubyVersion"]
30+
end
31+
32+
def rubocop_team
33+
RuboCop::Cop::Team.new(RuboCop::Cop::Cop.all, config_store)
34+
end
35+
36+
def display_path
37+
realpath = Pathname.new(root).realpath.to_s
38+
path.gsub(%r{^#{realpath}/}, "")
39+
end
40+
end
41+
end
42+
end

0 commit comments

Comments
 (0)