nucop 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/bin/nucop +8 -0
- data/lib/nucop.rb +11 -0
- data/lib/nucop/cli.rb +268 -0
- data/lib/nucop/cops/explicit_factory_bot_usage.rb +43 -0
- data/lib/nucop/cops/no_core_method_overrides.rb +31 -0
- data/lib/nucop/cops/no_wip_specs.rb +35 -0
- data/lib/nucop/cops/ordered_hash.rb +42 -0
- data/lib/nucop/cops/release_toggles_use_symbols.rb +43 -0
- data/lib/nucop/cops/shadowing_factory_bot_creation_methods.rb +37 -0
- data/lib/nucop/formatters/git_diff_formatter.rb +52 -0
- data/lib/nucop/formatters/junit_formatter.rb +45 -0
- data/lib/nucop/helpers/cop_counter.rb +51 -0
- data/lib/nucop/helpers/cop_set.rb +48 -0
- data/lib/nucop/helpers/factory_bot_helper.rb +30 -0
- data/lib/nucop/helpers/file_path_helper.rb +13 -0
- data/lib/nucop/helpers/next_cop_for_promotion.rb +54 -0
- data/lib/nucop/version.rb +3 -0
- data/spec/spec_helper.rb +14 -0
- metadata +176 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: f9fc165f5dbcc9624bfd5ed3c1c372f3f81ff325ca44d59990c4db36a2f0da39
|
4
|
+
data.tar.gz: a10769b0dc3d5de55c23442b4d27ea6bedecde3db859b3e679b5583c2b3e9846
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 43218b9441df727be126479a83813cbfc048af1a472f1fc1a5dc365871eaad0cf95bab8b091abf34238243dd3f3843fe0d0408a57e59a17193de288f0e2ed697
|
7
|
+
data.tar.gz: 986be2c74320253d2746d25229fd9ad724f61029e1200d7b447b7373aa83ec813921af6bd3745eff5d0cd92e5f51c69a7176881bde42a2f95526cfc1311bd6ab
|
data/bin/nucop
ADDED
data/lib/nucop.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require "nucop/version"
|
2
|
+
|
3
|
+
require "rubocop"
|
4
|
+
require "git_diff_parser"
|
5
|
+
|
6
|
+
Dir[File.join(__dir__, "nucop/helpers/**/*.rb")].each { |f| require f }
|
7
|
+
Dir[File.join(__dir__, "nucop/formatters/**/*.rb")].each { |f| require f }
|
8
|
+
Dir[File.join(__dir__, "nucop/cops/**/*.rb")].each { |f| require f }
|
9
|
+
|
10
|
+
module Nucop
|
11
|
+
end
|
data/lib/nucop/cli.rb
ADDED
@@ -0,0 +1,268 @@
|
|
1
|
+
require "thor"
|
2
|
+
require "open3"
|
3
|
+
|
4
|
+
RUBOCOP_DEFAULT_CONFIG_FILE = ".rubocop.yml"
|
5
|
+
CONFIGURATION_FILEPATH = ".nucop.yml"
|
6
|
+
|
7
|
+
module Nucop
|
8
|
+
class CLI < Thor
|
9
|
+
desc "diff_enforced", "run RuboCop on the current diff using only the enforced cops"
|
10
|
+
method_option "commit-spec", default: "origin/master", desc: "the commit used to determine the diff."
|
11
|
+
method_option "auto-correct", type: :boolean, default: false, desc: "runs RuboCop with auto-correct option"
|
12
|
+
method_option "junit_report", type: :string, default: "", desc: "runs RuboCop with junit formatter option"
|
13
|
+
def diff_enforced
|
14
|
+
invoke :diff, nil, options.merge(only: cops_to_enforce.join(","))
|
15
|
+
end
|
16
|
+
|
17
|
+
desc "diff", "run RuboCop on the current diff"
|
18
|
+
method_option "commit-spec", default: "origin/master", desc: "the commit used to determine the diff."
|
19
|
+
method_option "only", desc: "run only specified cop(s) and/or cops in the specified departments"
|
20
|
+
method_option "auto-correct", type: :boolean, default: false, desc: "runs RuboCop with auto-correct option"
|
21
|
+
method_option "ignore", type: :boolean, default: true, desc: "ignores files specified in #{options[:diffignore_file]}"
|
22
|
+
method_option "added-only", type: :boolean, default: false, desc: "runs RuboCop only on files that have been added (not on files that have been modified)"
|
23
|
+
method_option "exit", type: :boolean, default: true, desc: "disable to prevent task from exiting. Used by other Thor tasks when invoking this task, to prevent parent task from exiting"
|
24
|
+
def diff
|
25
|
+
puts "Running on files changed relative to '#{options[:"commit-spec"]}' (specify using the 'commit-spec' option)"
|
26
|
+
diff_filter = options[:"added-only"] ? "A" : "d"
|
27
|
+
diff_base = capture_std_out("git merge-base HEAD #{options[:"commit-spec"]}").chomp
|
28
|
+
|
29
|
+
files, diff_status = Open3.capture2("git diff #{diff_base} --diff-filter=#{diff_filter} --name-only | grep \"\\.rb$\"")
|
30
|
+
|
31
|
+
if diff_status != 0
|
32
|
+
if options[:exit]
|
33
|
+
puts "There are no rb files present in diff. Exiting."
|
34
|
+
exit 0
|
35
|
+
else
|
36
|
+
puts "There are no rb files present in diff."
|
37
|
+
return true
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
if options[:ignore] && File.exist?(options[:diffignore_file]) && !File.zero?(options[:diffignore_file])
|
42
|
+
files, non_ignored_diff_status = Open3.capture2("grep -v -f #{options[:diffignore_file]}", stdin_data: files)
|
43
|
+
|
44
|
+
if non_ignored_diff_status != 0
|
45
|
+
if options[:exit]
|
46
|
+
puts "There are no non-ignored rb files present in diff. Exiting."
|
47
|
+
exit 0
|
48
|
+
else
|
49
|
+
puts "There are no non-ignored rb files present in diff."
|
50
|
+
return true
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
no_violations_detected = invoke :rubocop, [multi_line_to_single_line(files)], options
|
56
|
+
|
57
|
+
exit 1 unless no_violations_detected
|
58
|
+
return true unless options[:exit]
|
59
|
+
exit 0
|
60
|
+
end
|
61
|
+
|
62
|
+
desc "rubocop", "run RuboCop on files provided"
|
63
|
+
method_option "only", desc: "run only specified cop(s) and/or cops in the specified departments"
|
64
|
+
method_option "auto-correct", type: :boolean, default: false, desc: "runs RuboCop with auto-correct option"
|
65
|
+
method_option "exclude-backlog", type: :boolean, default: false, desc: "when true, uses config which excludes violations in the RuboCop backlog"
|
66
|
+
def rubocop(files = nil)
|
67
|
+
print_cops_being_run(options[:only])
|
68
|
+
config_file = options[:"exclude-backlog"] ? RUBOCOP_DEFAULT_CONFIG_FILE : options[:rubocop_todo_config_file]
|
69
|
+
junit_report_path = options[:"junit_report"]
|
70
|
+
junit_report_options = junit_report_path.to_s.empty? ? "" : "--format Nucop::Formatters::JUnitFormatter --out #{junit_report_path} --format progress"
|
71
|
+
|
72
|
+
rubocop_requires = [
|
73
|
+
"--require rubocop-rspec",
|
74
|
+
"--require rubocop-performance",
|
75
|
+
"--require rubocop-rails"
|
76
|
+
]
|
77
|
+
|
78
|
+
system("bundle exec rubocop --parallel #{rubocop_requires.join(' ')} #{junit_report_options} --force-exclusion --config #{config_file} #{pass_through_option(options, 'auto-correct')} #{pass_through_flag(options, 'only')} #{files}")
|
79
|
+
end
|
80
|
+
|
81
|
+
desc "regen_backlog", "update the RuboCop backlog, disabling offending files and excluding all cops with over 500 violating files."
|
82
|
+
method_option "exclude-limit", type: :numeric, default: 500, desc: "Limit files listed to this limit. Passed to RuboCop"
|
83
|
+
def regen_backlog
|
84
|
+
regenerate_rubocop_todos
|
85
|
+
update_enforced_cops
|
86
|
+
end
|
87
|
+
|
88
|
+
desc "update_enforced", "update the enforced cops list with file with cops that no longer have violations"
|
89
|
+
def update_enforced
|
90
|
+
update_enforced_cops
|
91
|
+
end
|
92
|
+
|
93
|
+
desc "modified_lines", "display RuboCop violations for ONLY modified lines"
|
94
|
+
method_option "commit-spec", default: "master", desc: "the commit used to determine the diff."
|
95
|
+
def modified_lines
|
96
|
+
diff_files, diff_status = Open3.capture2("git diff #{options[:'commit-spec']} --diff-filter=d --name-only | grep \"\\.rb$\"")
|
97
|
+
|
98
|
+
exit 1 unless diff_status.exitstatus.zero?
|
99
|
+
|
100
|
+
command = [
|
101
|
+
"bundle exec rubocop",
|
102
|
+
"--parallel",
|
103
|
+
"--format Nucop::Formatters::GitDiffFormatter",
|
104
|
+
"--config #{options[:rubocop_todo_config_file]}",
|
105
|
+
multi_line_to_single_line(diff_files).to_s
|
106
|
+
].join(" ")
|
107
|
+
|
108
|
+
# HACK: use ENVVAR to parameterize GitDiffFormatter
|
109
|
+
system({ "RUBOCOP_COMMIT_SPEC" => options[:"commit-spec"] }, command)
|
110
|
+
end
|
111
|
+
|
112
|
+
desc "ready_for_promotion", "display the next n cops with the fewest offenses"
|
113
|
+
method_option "n", type: :numeric, default: 1, desc: "number of cops to display"
|
114
|
+
def ready_for_promotion
|
115
|
+
finder = Helpers::NextCopForPromotion.new(options[:rubocop_todo_file])
|
116
|
+
todo_config = YAML.load_file(options[:rubocop_todo_file])
|
117
|
+
|
118
|
+
puts "The following cop(s) are ready to be promoted to enforced. Good luck!"
|
119
|
+
puts "Remember to run `nucop:regen_backlog` to capture your hard work."
|
120
|
+
puts
|
121
|
+
finder.find(options["n"].to_i).each do |todo|
|
122
|
+
puts "#{todo.name} with #{todo.offenses} offenses:"
|
123
|
+
puts
|
124
|
+
|
125
|
+
files = todo_config.fetch(todo.name, {}).fetch("Exclude", [])
|
126
|
+
|
127
|
+
system("bundle exec rubocop --parallel --config #{options[:rubocop_todo_config_file]} --only #{todo.name} #{files.join(' ')}")
|
128
|
+
puts("*" * 100) if options["n"] > 1
|
129
|
+
puts
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
private
|
134
|
+
|
135
|
+
# some cops cannot be used with the --only option and will raise an error
|
136
|
+
# this filters them out
|
137
|
+
def cops_to_enforce
|
138
|
+
cops = enforced_cops
|
139
|
+
|
140
|
+
cops.delete("Lint/UnneededCopDisableDirective")
|
141
|
+
|
142
|
+
cops
|
143
|
+
end
|
144
|
+
|
145
|
+
def enforced_cops
|
146
|
+
@enforced_cops ||= YAML.load_file(options[:enforced_cops_file])
|
147
|
+
end
|
148
|
+
|
149
|
+
def capture_std_out(command, error_message = nil, stdin_data = nil)
|
150
|
+
std_out, std_error, status = Open3.capture3(command, stdin_data: stdin_data)
|
151
|
+
print_errors_and_exit(std_error, error_message) unless status.success?
|
152
|
+
|
153
|
+
std_out
|
154
|
+
end
|
155
|
+
|
156
|
+
def print_errors_and_exit(std_error, message = "An error has occurred")
|
157
|
+
warn message
|
158
|
+
puts std_error
|
159
|
+
puts "Exiting"
|
160
|
+
exit 1
|
161
|
+
end
|
162
|
+
|
163
|
+
def print_cops_being_run(only_option)
|
164
|
+
if only_option
|
165
|
+
enforced_cops_count = Helpers::CopCounter.count(enabled_cops, only_option.split(","))
|
166
|
+
puts "Running with a force of #{enforced_cops_count} cops. See '#{options[:enforced_cops_file]}' for more details."
|
167
|
+
else
|
168
|
+
puts "Running all cops (specify using the 'only' option)"
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def multi_line_to_single_line(str)
|
173
|
+
str.split(/\n+/).join(" ")
|
174
|
+
end
|
175
|
+
|
176
|
+
def pass_through_flag(options, option)
|
177
|
+
pass_through_option(options, option, true)
|
178
|
+
end
|
179
|
+
|
180
|
+
def pass_through_option(options, option, is_flag_option = false)
|
181
|
+
return nil unless options[option]
|
182
|
+
"--#{option} #{options[option] if is_flag_option}"
|
183
|
+
end
|
184
|
+
|
185
|
+
def files_changed_since(commit_spec)
|
186
|
+
`git diff #{commit_spec} HEAD --name-only`
|
187
|
+
.split("\n")
|
188
|
+
.select { |e| e.end_with?(".rb") }
|
189
|
+
end
|
190
|
+
|
191
|
+
def regenerate_rubocop_todos
|
192
|
+
puts "Regenerating '#{options[:rubocop_todo_file]}'. Please be patient..."
|
193
|
+
|
194
|
+
rubocop_options = [
|
195
|
+
"--auto-gen-config",
|
196
|
+
"--config #{options[:rubocop_todo_config_file]}",
|
197
|
+
"--exclude-limit #{options[:'exclude-limit']}",
|
198
|
+
"--require rubocop-rspec",
|
199
|
+
"--require rubocop-performance",
|
200
|
+
"--require rubocop-rails"
|
201
|
+
]
|
202
|
+
|
203
|
+
rubocop_command = "DISABLE_SPRING=1 bundle exec rubocop #{rubocop_options.join(' ')}"
|
204
|
+
|
205
|
+
system(rubocop_command)
|
206
|
+
|
207
|
+
# RuboCop wants to inherit from our todos (options[:rubocop_todo_file]) in our backlog configuration file (options[:rubocop_todo_config_file])
|
208
|
+
# However, that means the next time we try to update our backlog, it will NOT include the violations recorded as todo
|
209
|
+
# For now, we ignore any changes in our backlog config
|
210
|
+
system("git checkout #{options[:rubocop_todo_config_file]}")
|
211
|
+
end
|
212
|
+
|
213
|
+
def update_enforced_cops
|
214
|
+
puts "Updating enforced cops list..."
|
215
|
+
|
216
|
+
current_enforced_cops = Helpers::CopSet.new(enforced_cops)
|
217
|
+
cops_without_violations.each do |cop|
|
218
|
+
current_enforced_cops.add_cop(cop)
|
219
|
+
end
|
220
|
+
|
221
|
+
if current_enforced_cops.cop_added?
|
222
|
+
File.open(options[:enforced_cops_file], "w+") do |f|
|
223
|
+
f.write(current_enforced_cops.to_a.sort.to_yaml)
|
224
|
+
end
|
225
|
+
puts "Updated '#{options[:enforced_cops_file]}'!"
|
226
|
+
else
|
227
|
+
puts "No new cops are clear of violations"
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
def cops_without_violations
|
232
|
+
cops_with_violations = YAML.load_file(options[:rubocop_todo_file]).map(&:first)
|
233
|
+
|
234
|
+
enabled_cops - cops_with_violations
|
235
|
+
end
|
236
|
+
|
237
|
+
def enabled_cops
|
238
|
+
YAML.load(`bundle exec rubocop --parallel --show-cops`).select { |_, config| config["Enabled"] }.map(&:first)
|
239
|
+
end
|
240
|
+
|
241
|
+
# Override Thor's options method to include Nucop's options
|
242
|
+
def options
|
243
|
+
return @_options if defined?(@_options)
|
244
|
+
|
245
|
+
original_options = super
|
246
|
+
@_options = Thor::CoreExt::HashWithIndifferentAccess.new(
|
247
|
+
configuration_options.merge(original_options)
|
248
|
+
)
|
249
|
+
end
|
250
|
+
|
251
|
+
def configuration_options
|
252
|
+
if File.exist?(CONFIGURATION_FILEPATH)
|
253
|
+
default_configuration.merge(YAML.load_file(CONFIGURATION_FILEPATH))
|
254
|
+
else
|
255
|
+
default_configuration
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
def default_configuration
|
260
|
+
{
|
261
|
+
enforced_cops_file: ".rubocop.enforced.yml",
|
262
|
+
rubocop_todo_file: ".rubocop_todo.yml",
|
263
|
+
rubocop_todo_config_file: ".rubocop.backlog.yml",
|
264
|
+
diffignore_file: ".nucop_diffignore"
|
265
|
+
}
|
266
|
+
end
|
267
|
+
end
|
268
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Nucop
|
2
|
+
# This cop looks for usages of `FactoryGirl.create`, etc.
|
3
|
+
# See FactoryBotHelper::FACTORY_BOT_METHODS constant for a complete list.
|
4
|
+
#
|
5
|
+
# The factory methods listed are included everywhere, so referencing the constant should rarely be necessary.
|
6
|
+
#
|
7
|
+
# @example
|
8
|
+
#
|
9
|
+
# # bad
|
10
|
+
#
|
11
|
+
# job = FactoryGirl.create(:job, project: project)
|
12
|
+
# FactoryGirl.build(:project, code: "Super Project")
|
13
|
+
#
|
14
|
+
# # good
|
15
|
+
#
|
16
|
+
# job = create(:job, project: project)
|
17
|
+
# build(:project, code: "Super Project")
|
18
|
+
class ExplicitFactoryBotUsage < ::RuboCop::Cop::Cop
|
19
|
+
include Helpers::FilePathHelper
|
20
|
+
|
21
|
+
MSG = "Do not explicitly use `%<constant>s` to build objects. The factory method `%<method>s` is globally available."
|
22
|
+
|
23
|
+
def_node_matcher :explicit_factory_bot_usage, <<~PATTERN
|
24
|
+
(send (const nil? {:FactoryGirl :FactoryBot}) {#{Helpers::FactoryBotHelper.factory_bot_methods_pattern}} ...)
|
25
|
+
PATTERN
|
26
|
+
|
27
|
+
def on_send(node)
|
28
|
+
explicit_factory_bot_usage(node) do
|
29
|
+
add_offense(node, location: :expression, message: format(MSG, constant: node.receiver.const_name, method: node.method_name))
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def relevant_file?(file)
|
34
|
+
acceptance_or_spec_file?(file) && super
|
35
|
+
end
|
36
|
+
|
37
|
+
def autocorrect(node)
|
38
|
+
->(corrector) do
|
39
|
+
corrector.replace(node.source_range, node.source.sub(/(?:FactoryGirl|FactoryBot)[.]/, ""))
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Nucop
|
2
|
+
# This cop looks for core method overrides
|
3
|
+
#
|
4
|
+
# @example
|
5
|
+
#
|
6
|
+
# # bad
|
7
|
+
#
|
8
|
+
# def blank?
|
9
|
+
# # ...
|
10
|
+
# end
|
11
|
+
#
|
12
|
+
# # good
|
13
|
+
#
|
14
|
+
# def anything_other_than_blank?
|
15
|
+
# # ...
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
class NoCoreMethodOverrides < ::RuboCop::Cop::Cop
|
19
|
+
MSG = "Core method overridden".freeze
|
20
|
+
|
21
|
+
def_node_matcher :core_methods, <<~PATTERN
|
22
|
+
(def ${:present? :blank? :empty?} ...)
|
23
|
+
PATTERN
|
24
|
+
|
25
|
+
def on_def(node)
|
26
|
+
core_methods(node) do |method|
|
27
|
+
add_offense(node, location: :expression, message: format(MSG, method: method))
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Nucop
|
2
|
+
# This cop looks for WIP specs
|
3
|
+
#
|
4
|
+
# @example
|
5
|
+
#
|
6
|
+
# # bad
|
7
|
+
#
|
8
|
+
# it "tests some stuff", :wip do
|
9
|
+
# # ...
|
10
|
+
# end
|
11
|
+
#
|
12
|
+
# # good
|
13
|
+
#
|
14
|
+
# it "tests some stuff" do
|
15
|
+
# # ...
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
class NoWipSpecs < ::RuboCop::Cop::Cop
|
19
|
+
MSG = "WIP spec found".freeze
|
20
|
+
|
21
|
+
def_node_matcher :wip_it_specs_present?, <<~PATTERN
|
22
|
+
(send nil? :it ... (sym :wip))
|
23
|
+
PATTERN
|
24
|
+
|
25
|
+
def_node_matcher :wip_describe_specs_present?, <<~PATTERN
|
26
|
+
(send nil? :describe ... (sym :wip))
|
27
|
+
PATTERN
|
28
|
+
|
29
|
+
def on_send(node)
|
30
|
+
return unless wip_it_specs_present?(node) || wip_describe_specs_present?(node)
|
31
|
+
|
32
|
+
add_offense(node)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Nucop
|
2
|
+
# This cop looks for usages of `ActiveSupport::OrderedHash`
|
3
|
+
#
|
4
|
+
# Hashes in Ruby (since 1.9) enumerate their keys in the order they are
|
5
|
+
# inserted:
|
6
|
+
#
|
7
|
+
# "Hashes enumerate their values in the order that the corresponding keys were inserted."
|
8
|
+
# http://ruby-doc.org/core-2.1.6/Hash.html
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
#
|
12
|
+
# # bad
|
13
|
+
#
|
14
|
+
# hash = ActiveSupport::OrderedHash.new
|
15
|
+
#
|
16
|
+
# # good
|
17
|
+
#
|
18
|
+
# hash = {}
|
19
|
+
class OrderedHash < ::RuboCop::Cop::Cop
|
20
|
+
MSG = "Ruby hashes after 1.9 enumerate keys in order of insertion"
|
21
|
+
|
22
|
+
def_node_matcher :ordered_hash_usage, <<~PATTERN
|
23
|
+
(send (const (const nil? :ActiveSupport) :OrderedHash) :new)
|
24
|
+
PATTERN
|
25
|
+
|
26
|
+
def on_send(node)
|
27
|
+
ordered_hash_usage(node) do
|
28
|
+
add_offense(
|
29
|
+
node,
|
30
|
+
location: :expression,
|
31
|
+
message: MSG
|
32
|
+
)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def autocorrect(node)
|
37
|
+
->(corrector) do
|
38
|
+
corrector.replace(node.source_range, "{}")
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Nucop
|
2
|
+
# This cop checks that a symbol is used when using a Release Toggle name
|
3
|
+
#
|
4
|
+
# @example
|
5
|
+
#
|
6
|
+
# # bad
|
7
|
+
#
|
8
|
+
# release_toggle_enabled?("move_out_of_wip_autocomplete")
|
9
|
+
# release_toggle_enabled_for_any_site?("versioned_production_specification_ui")
|
10
|
+
# ReleaseToggles.enabled?("test_toggle", site_id: current_user.site_id)
|
11
|
+
# ReleaseToggles.disabled?("test_toggl"e, site_id: current_user.site_id)
|
12
|
+
# ReleaseToggles.enable("test_toggle", site_id: current_user.site_id)
|
13
|
+
# ReleaseToggles.disable!("test_toggle", site_id: current_user.site_id)
|
14
|
+
#
|
15
|
+
# # good
|
16
|
+
#
|
17
|
+
# release_toggle_enabled?(:move_out_of_wip_autocomplete)
|
18
|
+
#
|
19
|
+
class ReleaseTogglesUseSymbols < ::RuboCop::Cop::Cop
|
20
|
+
MSG = "Use a symbol when refering to a Release Toggle's by name".freeze
|
21
|
+
|
22
|
+
def_node_matcher :test_helper?, <<~PATTERN
|
23
|
+
(send nil? {:release_toggle_enabled? :release_toggle_enabled_for_any_site?} (str _))
|
24
|
+
PATTERN
|
25
|
+
|
26
|
+
def_node_matcher :release_toggles_public_api_method?, <<~PATTERN
|
27
|
+
(send (const nil? :ReleaseToggles) {:enabled? :disabled? :enable :disable :enable! :disable!} (str _) ...)
|
28
|
+
PATTERN
|
29
|
+
|
30
|
+
def on_send(node)
|
31
|
+
test_helper?(node) { add_offense(node, message: MSG, location: node.children[2].loc.expression) }
|
32
|
+
release_toggles_public_api_method?(node) { add_offense(node, message: MSG, location: node.children[2].loc.expression) }
|
33
|
+
end
|
34
|
+
|
35
|
+
def autocorrect(node)
|
36
|
+
->(corrector) do
|
37
|
+
toggle_name = node.children[2].value
|
38
|
+
|
39
|
+
corrector.replace(node.children[2].source_range, ":#{toggle_name}")
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Nucop
|
2
|
+
# This cop looks for defined methods in spec files that would shadow methods defined in FactoryBot::Syntax::Methods.
|
3
|
+
# See FactoryBotHelper::FACTORY_BOT_METHODS constant for a complete list.
|
4
|
+
#
|
5
|
+
# @example
|
6
|
+
#
|
7
|
+
# # bad
|
8
|
+
#
|
9
|
+
# def create(args)
|
10
|
+
# ...
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
# # good
|
14
|
+
#
|
15
|
+
# def create_transfer_pallet(args)
|
16
|
+
# ...
|
17
|
+
# end
|
18
|
+
class ShadowingFactoryBotCreationMethods < ::RuboCop::Cop::Cop
|
19
|
+
include Helpers::FilePathHelper
|
20
|
+
|
21
|
+
MSG = "Method `%<method>s` shadows a FactoryBot method. Please rename it to be more specific."
|
22
|
+
|
23
|
+
def_node_matcher :factory_bot_methods, <<~PATTERN
|
24
|
+
(def ${#{Helpers::FactoryBotHelper.factory_bot_methods_pattern}} ...)
|
25
|
+
PATTERN
|
26
|
+
|
27
|
+
def on_def(node)
|
28
|
+
factory_bot_methods(node) do |method|
|
29
|
+
add_offense(node, location: :expression, message: format(MSG, method: method))
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def relevant_file?(file)
|
34
|
+
acceptance_or_spec_file?(file) && super
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Nucop
|
2
|
+
module Formatters
|
3
|
+
class GitDiffFormatter < RuboCop::Formatter::ProgressFormatter
|
4
|
+
def initialize(output, options = {})
|
5
|
+
super
|
6
|
+
|
7
|
+
populate_history_from_git
|
8
|
+
@offenses_per_file = {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def file_finished(file, offenses)
|
12
|
+
return unless file_touched?(file)
|
13
|
+
|
14
|
+
offenses_in_changes = offenses_from_git_history(file, offenses)
|
15
|
+
@offenses_per_file[file] = offenses_in_changes.size
|
16
|
+
|
17
|
+
# modify parent Formatter to print what we want
|
18
|
+
@offenses_for_files[file] = offenses_in_changes if offenses_in_changes.any?
|
19
|
+
report_file_as_mark(offenses_in_changes)
|
20
|
+
end
|
21
|
+
|
22
|
+
def finished(_inspected_files)
|
23
|
+
@total_offense_count = @offenses_per_file.values.reduce(0, :+)
|
24
|
+
@total_correction_count = 0
|
25
|
+
|
26
|
+
super
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def populate_history_from_git
|
32
|
+
commit_spec = ENV["RUBOCOP_COMMIT_SPEC"] || "master"
|
33
|
+
|
34
|
+
diff = `git --no-pager diff #{commit_spec}`
|
35
|
+
|
36
|
+
@git_history = ::GitDiffParser.parse(diff).each_with_object({}) do |patch, acc|
|
37
|
+
next if patch.changed_line_numbers.empty?
|
38
|
+
|
39
|
+
acc[File.expand_path(patch.file)] = patch.changed_line_numbers
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def file_touched?(file)
|
44
|
+
@git_history.key?(file)
|
45
|
+
end
|
46
|
+
|
47
|
+
def offenses_from_git_history(file, offenses)
|
48
|
+
offenses.select { |offense| @git_history[file].include?(offense.line) }
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require "rexml/document"
|
2
|
+
|
3
|
+
module Nucop
|
4
|
+
module Formatters
|
5
|
+
class JUnitFormatter < ::RuboCop::Formatter::BaseFormatter
|
6
|
+
# This gives all cops - we really want all _enabled_ cops, but
|
7
|
+
# that is difficult to obtain - no access to config object here.
|
8
|
+
COPS = RuboCop::Cop::Cop.all
|
9
|
+
|
10
|
+
def started(_target_file)
|
11
|
+
@document = REXML::Document.new.tap do |d|
|
12
|
+
d << REXML::XMLDecl.new
|
13
|
+
end
|
14
|
+
@testsuites = REXML::Element.new("testsuites", @document)
|
15
|
+
@testsuite = REXML::Element.new("testsuite", @testsuites).tap do |el|
|
16
|
+
el.add_attributes("name" => "rubocop")
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def file_finished(file, offences)
|
21
|
+
# One test case per cop per file
|
22
|
+
COPS.each do |cop|
|
23
|
+
cop_offences = offences.select { |offence| offence.cop_name == cop.cop_name }
|
24
|
+
unless cop_offences.empty?
|
25
|
+
REXML::Element.new("testcase", @testsuite).tap do |f|
|
26
|
+
f.attributes["classname"] = file.gsub(/\.rb\Z/, "").gsub("#{Dir.pwd}/", "").tr("/", ".")
|
27
|
+
f.attributes["name"] = "Rubocop: #{cop.cop_name}"
|
28
|
+
f.attributes["file"] = cop.cop_name
|
29
|
+
cop_offences.each do |offence|
|
30
|
+
REXML::Element.new("failure", f).tap do |e|
|
31
|
+
e.add_text("#{offence.message}\n\n")
|
32
|
+
e.add_text(offence.location.to_s.sub("/usr/src/app/", ""))
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def finished(_inspected_files)
|
41
|
+
@document.write(output, 2)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# class to count the number of cops from a list of RuboCop "only" options
|
2
|
+
# i.e. it accounts for whole "Departments"
|
3
|
+
#
|
4
|
+
# Examaples:
|
5
|
+
# "Style/Blah" is 1 cops
|
6
|
+
# "Layout" may represent 70 cops
|
7
|
+
module Nucop
|
8
|
+
module Helpers
|
9
|
+
class CopCounter
|
10
|
+
def self.count(all_cops, cops_or_departments)
|
11
|
+
new(all_cops).count(cops_or_departments)
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(cops)
|
15
|
+
@cops_by_department = group_by_department(cops)
|
16
|
+
end
|
17
|
+
|
18
|
+
def count(cops_or_departments)
|
19
|
+
cops_or_departments
|
20
|
+
.map do |cop_or_department|
|
21
|
+
if department?(cop_or_department)
|
22
|
+
@cops_by_department.fetch(cop_or_department, []).length
|
23
|
+
else
|
24
|
+
1
|
25
|
+
end
|
26
|
+
end
|
27
|
+
.reduce(0, &:+)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def group_by_department(cop_names)
|
33
|
+
cop_names.group_by do |cop_name|
|
34
|
+
if department?(cop_name)
|
35
|
+
raise "Expected fully-qualified cops by name (i.e. Department/Cop). Got: #{cop_name}"
|
36
|
+
end
|
37
|
+
|
38
|
+
department(cop_name)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def department?(cop_name)
|
43
|
+
!cop_name.include?("/")
|
44
|
+
end
|
45
|
+
|
46
|
+
def department(cop_name)
|
47
|
+
cop_name.split("/").first
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require "set"
|
2
|
+
|
3
|
+
module Nucop
|
4
|
+
module Helpers
|
5
|
+
class CopSet
|
6
|
+
def initialize(initial_cops = [])
|
7
|
+
@cops = Set.new
|
8
|
+
|
9
|
+
add_cops(initial_cops)
|
10
|
+
|
11
|
+
@new_cop_added = false
|
12
|
+
end
|
13
|
+
|
14
|
+
def add_cops(cops)
|
15
|
+
cops.each(&method(:add_cop))
|
16
|
+
end
|
17
|
+
|
18
|
+
# add a single cop to the set
|
19
|
+
# if a cops department is already included,
|
20
|
+
# the cop is not added (it is part of the department already)
|
21
|
+
def add_cop(cop)
|
22
|
+
department = find_department(cop)
|
23
|
+
|
24
|
+
return if department && @cops.include?(department)
|
25
|
+
return if @cops.include?(cop)
|
26
|
+
|
27
|
+
@cops << cop
|
28
|
+
@new_cop_added = true
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_a
|
32
|
+
@cops.to_a
|
33
|
+
end
|
34
|
+
|
35
|
+
def cop_added?
|
36
|
+
@new_cop_added
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def find_department(cop)
|
42
|
+
return unless cop.include?("/")
|
43
|
+
|
44
|
+
cop.split("/").first
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Nucop
|
2
|
+
module Helpers
|
3
|
+
module FactoryBotHelper
|
4
|
+
extend self
|
5
|
+
|
6
|
+
FACTORY_BOT_METHODS = [
|
7
|
+
:build,
|
8
|
+
:build_list,
|
9
|
+
:build_pair,
|
10
|
+
|
11
|
+
:create,
|
12
|
+
:create_list,
|
13
|
+
:create_pair,
|
14
|
+
|
15
|
+
:build_stubbed,
|
16
|
+
:build_stubbed_list,
|
17
|
+
:build_stubbed_pair,
|
18
|
+
|
19
|
+
:attributes_for,
|
20
|
+
:attributes_for_list,
|
21
|
+
:attributes_for_pair
|
22
|
+
]
|
23
|
+
private_constant :FACTORY_BOT_METHODS
|
24
|
+
|
25
|
+
def factory_bot_methods_pattern
|
26
|
+
FACTORY_BOT_METHODS.map(&:inspect).join(" ")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Nucop
|
2
|
+
module Helpers
|
3
|
+
module FilePathHelper
|
4
|
+
def acceptance_or_spec_file?(filepath)
|
5
|
+
Pathname.new(filepath).fnmatch?(File.join("**", "{spec,acceptance}", "**"), File::FNM_EXTGLOB)
|
6
|
+
end
|
7
|
+
|
8
|
+
def support_file?(filepath)
|
9
|
+
Pathname.new(filepath).fnmatch?(File.join("**", "support", "**"), File::FNM_EXTGLOB)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Nucop
|
2
|
+
module Helpers
|
3
|
+
class NextCopForPromotion
|
4
|
+
Todo = Struct.new(:name, :offenses)
|
5
|
+
|
6
|
+
def initialize(todo_filepath)
|
7
|
+
@todo_filepath = todo_filepath
|
8
|
+
extract_todos
|
9
|
+
sort_todos_by_offense_count
|
10
|
+
end
|
11
|
+
|
12
|
+
def find(how_many = 5)
|
13
|
+
@todos.take(how_many)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def extract_todos
|
19
|
+
@todos =
|
20
|
+
extract_offense_counts_and_names
|
21
|
+
.each_slice(2)
|
22
|
+
.map { |count, name| Todo.new(name, count.to_i) }
|
23
|
+
end
|
24
|
+
|
25
|
+
def extract_offense_counts_and_names
|
26
|
+
data = []
|
27
|
+
|
28
|
+
each_line do |line|
|
29
|
+
if (count_match = line.match(/Offense count: (\d+)/))
|
30
|
+
data << count_match.captures.first
|
31
|
+
end
|
32
|
+
|
33
|
+
if (name_match = line.match(/^(\w+[\/]\w+):$/))
|
34
|
+
data << name_match.captures.first
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
data
|
39
|
+
end
|
40
|
+
|
41
|
+
def each_line
|
42
|
+
File.open(@todo_filepath, "r") do |file|
|
43
|
+
file.each_line do |line|
|
44
|
+
yield line
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def sort_todos_by_offense_count
|
50
|
+
@todos.sort! { |i, j| i.offenses <=> j.offenses }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require "bundler/setup"
|
2
|
+
require "nucop"
|
3
|
+
|
4
|
+
RSpec.configure do |config|
|
5
|
+
# Enable flags like --only-failures and --next-failure
|
6
|
+
config.example_status_persistence_file_path = ".rspec_status"
|
7
|
+
|
8
|
+
# Disable RSpec exposing methods globally on `Module` and `main`
|
9
|
+
config.disable_monkey_patching!
|
10
|
+
|
11
|
+
config.expect_with(:rspec) do |c|
|
12
|
+
c.syntax = :expect
|
13
|
+
end
|
14
|
+
end
|
metadata
ADDED
@@ -0,0 +1,176 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: nucop
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jason Schweier
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-11-01 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rake
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 13.0.0
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 13.0.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rspec
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 3.9.0
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 3.9.0
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: git_diff_parser
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.2'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.2'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rubocop
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - '='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 0.75.1
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - '='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 0.75.1
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rubocop-performance
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - '='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 1.5.0
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - '='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 1.5.0
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rubocop-rails
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - '='
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 2.3.2
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - '='
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 2.3.2
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rubocop-rspec
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - '='
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 1.36.0
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - '='
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 1.36.0
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: ruby-progressbar
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '1.10'
|
118
|
+
type: :runtime
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '1.10'
|
125
|
+
description:
|
126
|
+
email:
|
127
|
+
- jasons@nulogy.com
|
128
|
+
executables:
|
129
|
+
- nucop
|
130
|
+
extensions: []
|
131
|
+
extra_rdoc_files: []
|
132
|
+
files:
|
133
|
+
- bin/nucop
|
134
|
+
- lib/nucop.rb
|
135
|
+
- lib/nucop/cli.rb
|
136
|
+
- lib/nucop/cops/explicit_factory_bot_usage.rb
|
137
|
+
- lib/nucop/cops/no_core_method_overrides.rb
|
138
|
+
- lib/nucop/cops/no_wip_specs.rb
|
139
|
+
- lib/nucop/cops/ordered_hash.rb
|
140
|
+
- lib/nucop/cops/release_toggles_use_symbols.rb
|
141
|
+
- lib/nucop/cops/shadowing_factory_bot_creation_methods.rb
|
142
|
+
- lib/nucop/formatters/git_diff_formatter.rb
|
143
|
+
- lib/nucop/formatters/junit_formatter.rb
|
144
|
+
- lib/nucop/helpers/cop_counter.rb
|
145
|
+
- lib/nucop/helpers/cop_set.rb
|
146
|
+
- lib/nucop/helpers/factory_bot_helper.rb
|
147
|
+
- lib/nucop/helpers/file_path_helper.rb
|
148
|
+
- lib/nucop/helpers/next_cop_for_promotion.rb
|
149
|
+
- lib/nucop/version.rb
|
150
|
+
- spec/spec_helper.rb
|
151
|
+
homepage:
|
152
|
+
licenses: []
|
153
|
+
metadata: {}
|
154
|
+
post_install_message:
|
155
|
+
rdoc_options: []
|
156
|
+
require_paths:
|
157
|
+
- lib
|
158
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
159
|
+
requirements:
|
160
|
+
- - ">="
|
161
|
+
- !ruby/object:Gem::Version
|
162
|
+
version: '0'
|
163
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
164
|
+
requirements:
|
165
|
+
- - ">="
|
166
|
+
- !ruby/object:Gem::Version
|
167
|
+
version: '0'
|
168
|
+
requirements: []
|
169
|
+
rubyforge_project:
|
170
|
+
rubygems_version: 2.7.6
|
171
|
+
signing_key:
|
172
|
+
specification_version: 4
|
173
|
+
summary: Nulogy's implementation of RuboCop, including custom cops and additional
|
174
|
+
tooling.
|
175
|
+
test_files:
|
176
|
+
- spec/spec_helper.rb
|