rubocop-gusto 10.0.0
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/CHANGELOG.md +4 -0
- data/LICENSE +21 -0
- data/README.md +53 -0
- data/config/default.yml +781 -0
- data/config/rails.yml +122 -0
- data/exe/gusto-rubocop +12 -0
- data/exe/rubocop-gusto +9 -0
- data/lib/rubocop/cop/gusto/bootsnap_load_file.rb +57 -0
- data/lib/rubocop/cop/gusto/datadog_constant.rb +16 -0
- data/lib/rubocop/cop/gusto/execute_migration.rb +16 -0
- data/lib/rubocop/cop/gusto/factory_classes_or_modules.rb +19 -0
- data/lib/rubocop/cop/gusto/min_by_max_by.rb +45 -0
- data/lib/rubocop/cop/gusto/no_metaprogramming.rb +131 -0
- data/lib/rubocop/cop/gusto/no_rescue_error_message_checking.rb +66 -0
- data/lib/rubocop/cop/gusto/no_send.rb +32 -0
- data/lib/rubocop/cop/gusto/object_in.rb +36 -0
- data/lib/rubocop/cop/gusto/paperclip_or_attachable.rb +17 -0
- data/lib/rubocop/cop/gusto/perform_class_method.rb +73 -0
- data/lib/rubocop/cop/gusto/polymorphic_type_validation.rb +89 -0
- data/lib/rubocop/cop/gusto/prefer_process_last_status.rb +35 -0
- data/lib/rubocop/cop/gusto/rabl_extends.rb +43 -0
- data/lib/rubocop/cop/gusto/rails_env.rb +72 -0
- data/lib/rubocop/cop/gusto/rake_constants.rb +68 -0
- data/lib/rubocop/cop/gusto/regexp_bypass.rb +90 -0
- data/lib/rubocop/cop/gusto/sidekiq_params.rb +21 -0
- data/lib/rubocop/cop/gusto/toplevel_constants.rb +55 -0
- data/lib/rubocop/cop/gusto/use_paint_not_colorize.rb +240 -0
- data/lib/rubocop/cop/gusto/vcr_recordings.rb +49 -0
- data/lib/rubocop/cop/internal_affairs/assignment_first.rb +56 -0
- data/lib/rubocop/cop/internal_affairs/require_restrict_on_send.rb +62 -0
- data/lib/rubocop/gusto/cli.rb +22 -0
- data/lib/rubocop/gusto/config_yml.rb +135 -0
- data/lib/rubocop/gusto/init.rb +59 -0
- data/lib/rubocop/gusto/plugin.rb +29 -0
- data/lib/rubocop/gusto/templates/rubocop.yml +25 -0
- data/lib/rubocop/gusto/version.rb +7 -0
- data/lib/rubocop/gusto.rb +9 -0
- data/lib/rubocop-gusto.rb +13 -0
- metadata +178 -0
@@ -0,0 +1,240 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Gusto
|
6
|
+
# Requires the use of the `paint` gem for terminal color methods on strings
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
#
|
10
|
+
# # bad
|
11
|
+
# "string".cyan
|
12
|
+
# "string".red
|
13
|
+
# "string".green
|
14
|
+
# str = "hello"
|
15
|
+
# str.cyan
|
16
|
+
# "string".colorize(:blue)
|
17
|
+
# "string".colorize(:color => :blue)
|
18
|
+
# "string".colorize(:color => :blue, :background => :red)
|
19
|
+
# "string".blue.on_red
|
20
|
+
# "string".colorize(:blue).on_red
|
21
|
+
# "string".blue.underline
|
22
|
+
#
|
23
|
+
# # good
|
24
|
+
# "string"
|
25
|
+
#
|
26
|
+
# # if color is needed, use `paint` gem
|
27
|
+
# Paint["string", :cyan]
|
28
|
+
# Paint["string", :red]
|
29
|
+
# Paint["string", :green]
|
30
|
+
# Paint[str, :cyan]
|
31
|
+
# Paint["string", :blue]
|
32
|
+
# Paint["string", :blue, :red]
|
33
|
+
#
|
34
|
+
class UsePaintNotColorize < Base
|
35
|
+
extend AutoCorrector
|
36
|
+
|
37
|
+
# Common terminal color methods that should be prevented
|
38
|
+
COLOR_METHODS = Set.new(
|
39
|
+
%i(
|
40
|
+
black
|
41
|
+
red
|
42
|
+
green
|
43
|
+
yellow
|
44
|
+
blue
|
45
|
+
magenta
|
46
|
+
cyan
|
47
|
+
white
|
48
|
+
light_black
|
49
|
+
light_red
|
50
|
+
light_green
|
51
|
+
light_yellow
|
52
|
+
light_blue
|
53
|
+
light_magenta
|
54
|
+
light_cyan
|
55
|
+
light_white
|
56
|
+
colorize
|
57
|
+
on_black
|
58
|
+
on_red
|
59
|
+
on_green
|
60
|
+
on_yellow
|
61
|
+
on_blue
|
62
|
+
on_magenta
|
63
|
+
on_cyan
|
64
|
+
on_white
|
65
|
+
on_light_black
|
66
|
+
on_light_red
|
67
|
+
on_light_green
|
68
|
+
on_light_yellow
|
69
|
+
on_light_blue
|
70
|
+
on_light_magenta
|
71
|
+
on_light_cyan
|
72
|
+
on_light_white
|
73
|
+
bold
|
74
|
+
italic
|
75
|
+
underline
|
76
|
+
blink
|
77
|
+
swap
|
78
|
+
hide
|
79
|
+
uncolorize
|
80
|
+
)
|
81
|
+
).freeze
|
82
|
+
|
83
|
+
# Style modifiers that are applied as additional options in Paint
|
84
|
+
STYLE_MODIFIERS = Set.new(
|
85
|
+
%i(
|
86
|
+
bold
|
87
|
+
italic
|
88
|
+
underline
|
89
|
+
blink
|
90
|
+
swap
|
91
|
+
hide
|
92
|
+
)
|
93
|
+
).freeze
|
94
|
+
|
95
|
+
MSG = 'Use Paint instead of colorize for terminal colors.'
|
96
|
+
PROHIBITED_CLASS = 'String'
|
97
|
+
RESTRICT_ON_SEND = COLOR_METHODS
|
98
|
+
|
99
|
+
def on_send(node)
|
100
|
+
return unless node.receiver
|
101
|
+
return unless string_or_colorized_receiver?(node.receiver)
|
102
|
+
|
103
|
+
add_offense(node) do |corrector|
|
104
|
+
corrector.replace(node, correction(node))
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def on_csend(node)
|
109
|
+
return unless string_or_colorized_receiver?(node.receiver)
|
110
|
+
|
111
|
+
add_offense(node) # no autocorrection for safe navigation due to chained calls
|
112
|
+
end
|
113
|
+
|
114
|
+
private
|
115
|
+
|
116
|
+
def string_or_colorized_receiver?(node)
|
117
|
+
string_receiver?(node) || colorized_string?(node)
|
118
|
+
end
|
119
|
+
|
120
|
+
def string_receiver?(node)
|
121
|
+
node.type?(:str, :dstr) || node.variable?
|
122
|
+
end
|
123
|
+
|
124
|
+
def colorized_string?(node)
|
125
|
+
node.send_type? &&
|
126
|
+
node.receiver.is_a?(RuboCop::AST::Node) &&
|
127
|
+
string_or_colorized_receiver?(node.receiver)
|
128
|
+
end
|
129
|
+
|
130
|
+
def correction(node)
|
131
|
+
# Find the original string and all color/style operations in the chain
|
132
|
+
original_string, color_ops = extract_string_and_operations(node)
|
133
|
+
|
134
|
+
foreground = nil
|
135
|
+
background = nil
|
136
|
+
styles = []
|
137
|
+
|
138
|
+
# Process all the operations to build the Paint parameters
|
139
|
+
color_ops.each do |op|
|
140
|
+
method_name = op[:method]
|
141
|
+
args = op[:args]
|
142
|
+
|
143
|
+
if method_name == :colorize
|
144
|
+
if args.length == 1 && args.first.sym_type?
|
145
|
+
# Single symbol argument, like colorize(:red)
|
146
|
+
foreground = ":#{args.first.value}"
|
147
|
+
elsif args.length == 1 && args.first.hash_type?
|
148
|
+
# Hash argument, like colorize(color: :red, background: :blue)
|
149
|
+
args.first.pairs.each do |pair|
|
150
|
+
break unless pair.value.sym_type? # can't handle non-symbol arguments
|
151
|
+
|
152
|
+
key = pair.key.value
|
153
|
+
value = ":#{pair.value.value}"
|
154
|
+
|
155
|
+
case key
|
156
|
+
when :color
|
157
|
+
foreground = value
|
158
|
+
when :background
|
159
|
+
background = value
|
160
|
+
when :mode
|
161
|
+
styles << value
|
162
|
+
else
|
163
|
+
break # unknown key, skip the rest of the hash
|
164
|
+
end
|
165
|
+
end
|
166
|
+
else
|
167
|
+
# if the argument is not a symbol or hash, we can't handle it
|
168
|
+
break
|
169
|
+
end
|
170
|
+
elsif method_name == :uncolorize
|
171
|
+
# If uncolorize is called, convert to Paint.unpaint
|
172
|
+
return "Paint.unpaint(#{original_string.source})"
|
173
|
+
elsif method_name.start_with?('on_')
|
174
|
+
# Background color
|
175
|
+
color_name = method_name.to_s.delete_prefix('on_')
|
176
|
+
background = ":#{color_name}"
|
177
|
+
elsif STYLE_MODIFIERS.include?(method_name)
|
178
|
+
# Style modifier
|
179
|
+
styles << ":#{method_name}"
|
180
|
+
elsif method_name.start_with?('light_')
|
181
|
+
# Light/bright foreground color
|
182
|
+
color = method_name.to_s.delete_prefix('light_')
|
183
|
+
foreground = ":bright, :#{color}"
|
184
|
+
else
|
185
|
+
# Regular foreground color
|
186
|
+
foreground = ":#{method_name}"
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
return unless foreground || background || styles.any?
|
191
|
+
|
192
|
+
# Build the Paint call
|
193
|
+
build_paint_call(original_string, foreground, background, styles)
|
194
|
+
end
|
195
|
+
|
196
|
+
def extract_string_and_operations(node)
|
197
|
+
operations = []
|
198
|
+
current = node
|
199
|
+
|
200
|
+
# Find the deepest operation in the chain
|
201
|
+
while current.send_type? && COLOR_METHODS.include?(current.method_name)
|
202
|
+
operations.unshift(
|
203
|
+
{
|
204
|
+
method: current.method_name,
|
205
|
+
args: current.arguments,
|
206
|
+
}
|
207
|
+
)
|
208
|
+
|
209
|
+
current = current.receiver
|
210
|
+
end
|
211
|
+
|
212
|
+
# The earliest receiver is the original string
|
213
|
+
[current, operations]
|
214
|
+
end
|
215
|
+
|
216
|
+
def build_paint_call(string_node, foreground, background, styles)
|
217
|
+
# Use string_content for string nodes, or source for variables and other expressions
|
218
|
+
string_expr = string_node.source
|
219
|
+
|
220
|
+
params = [string_expr]
|
221
|
+
|
222
|
+
# Add nil as a placeholder for foreground if we only have a background
|
223
|
+
if background && !foreground
|
224
|
+
params << 'nil'
|
225
|
+
elsif foreground
|
226
|
+
params << foreground
|
227
|
+
end
|
228
|
+
|
229
|
+
# Add background if present
|
230
|
+
params << background if background
|
231
|
+
|
232
|
+
# Add any style modifiers
|
233
|
+
params.concat(styles) unless styles.empty?
|
234
|
+
|
235
|
+
"Paint[#{params.join(', ')}]"
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Gusto
|
6
|
+
# Requires VCR to be set to not record in tests.
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# # bad
|
10
|
+
# vcr: {record: :all}
|
11
|
+
#
|
12
|
+
# # good
|
13
|
+
# vcr: {record: :none}
|
14
|
+
#
|
15
|
+
# @see https://github.com/vcr/vcr
|
16
|
+
#
|
17
|
+
class VcrRecordings < Base
|
18
|
+
extend AutoCorrector
|
19
|
+
|
20
|
+
MSG = 'VCR should be set to not record in tests. Please use vcr: {record: :none}.'
|
21
|
+
|
22
|
+
# @!method vcr_recording?(node)
|
23
|
+
def_node_matcher :vcr_recording?, <<~PATTERN
|
24
|
+
(pair (sym :record) (sym $_))
|
25
|
+
PATTERN
|
26
|
+
|
27
|
+
def on_pair(node)
|
28
|
+
return unless vcr_setting?(node)
|
29
|
+
return unless recording_enabled?(node.key.children.first, node.value.children.first)
|
30
|
+
|
31
|
+
add_offense(node) do |corrector|
|
32
|
+
replacement = node.source.sub(/: :\w*/, ': :none')
|
33
|
+
corrector.replace(node, replacement)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def vcr_setting?(node)
|
40
|
+
node.parent.parent.source.include?('vcr')
|
41
|
+
end
|
42
|
+
|
43
|
+
def recording_enabled?(option, value)
|
44
|
+
option == :record && value != :none
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module InternalAffairs
|
6
|
+
# Check for assignment as the first action in a cop hook.
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# # bad
|
10
|
+
# def on_send(node)
|
11
|
+
# foo = 1
|
12
|
+
# do_something
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# # good
|
16
|
+
# def on_send(node)
|
17
|
+
# do_something
|
18
|
+
# foo = 1
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
class AssignmentFirst < Base
|
22
|
+
HOOKS = %i(
|
23
|
+
on_def
|
24
|
+
on_defs
|
25
|
+
on_send
|
26
|
+
on_csend
|
27
|
+
on_const
|
28
|
+
on_int
|
29
|
+
on_class
|
30
|
+
on_module
|
31
|
+
on_block
|
32
|
+
on_begin
|
33
|
+
on_kwbegin
|
34
|
+
after_int
|
35
|
+
after_def
|
36
|
+
after_send
|
37
|
+
after_csend
|
38
|
+
after_class
|
39
|
+
after_module
|
40
|
+
).to_set.freeze
|
41
|
+
MSG = 'Avoid placing an assignment as the first action in `%{hook}`.'
|
42
|
+
|
43
|
+
def on_def(node)
|
44
|
+
return unless HOOKS.include?(node.method_name)
|
45
|
+
return unless node.body
|
46
|
+
|
47
|
+
# Look through a begin node, e.g. look inside parentheses
|
48
|
+
first_child = node.body.begin_type? ? node.body.children.first : node.body
|
49
|
+
return unless first_child&.assignment?
|
50
|
+
|
51
|
+
add_offense(first_child, message: format(MSG, hook: node.method_name))
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module InternalAffairs
|
6
|
+
# Check for missing `RESTRICT_ON_SEND`.
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# # bad
|
10
|
+
# class FooCop
|
11
|
+
# def on_send(node)
|
12
|
+
# # ...
|
13
|
+
# end
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# # good
|
17
|
+
# class FooCop
|
18
|
+
# RESTRICT_ON_SEND = %i[bad_method].freeze
|
19
|
+
# def on_send(node)
|
20
|
+
# # ...
|
21
|
+
# end
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# NOTE: This works for us because we do not write cops that investigate every send node.
|
25
|
+
# Upstream Rubocop chose not to implement this as there are many cops in Core that investigate every node.
|
26
|
+
class RequireRestrictOnSend < Base
|
27
|
+
MSG = 'Missing `RESTRICT_ON_SEND` declaration when using `on_send` or `after_send`.'
|
28
|
+
|
29
|
+
# @!method defined_send_callback?(node)
|
30
|
+
def_node_search :defined_send_callback?, <<~PATTERN
|
31
|
+
{
|
32
|
+
(def {:on_send :after_send} ...)
|
33
|
+
(alias (sym {:on_send :after_send}) _source ...)
|
34
|
+
(send nil? :alias_method {(sym {:on_send :after_send}) (str {"on_send" "after_send"})} _source ...)
|
35
|
+
}
|
36
|
+
PATTERN
|
37
|
+
|
38
|
+
# @!method restrict_on_send?(node)
|
39
|
+
def_node_search :restrict_on_send?, <<~PATTERN
|
40
|
+
(casgn nil? :RESTRICT_ON_SEND ...)
|
41
|
+
PATTERN
|
42
|
+
|
43
|
+
# from: https://github.com/rubocop/rubocop/blob/e78790e3c9e82f8e605009673a8d2eac40b18c4c/lib/rubocop/cop/internal_affairs/undefined_config.rb#L25
|
44
|
+
# @!method cop_class_def(node)
|
45
|
+
def_node_matcher :cop_class_def, <<~PATTERN
|
46
|
+
(class _
|
47
|
+
(const {nil? (const nil? :Cop) (const (const {cbase nil?} :RuboCop) :Cop)}
|
48
|
+
{:Base :Cop}) ...)
|
49
|
+
PATTERN
|
50
|
+
|
51
|
+
def on_class(node)
|
52
|
+
return if restrict_on_send?(node) # requirement met
|
53
|
+
|
54
|
+
return unless defined_send_callback?(node)
|
55
|
+
return unless cop_class_def(node)
|
56
|
+
|
57
|
+
add_offense(node)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'thor'
|
4
|
+
require 'rubocop/gusto/config_yml'
|
5
|
+
require 'rubocop/gusto/init'
|
6
|
+
|
7
|
+
module RuboCop
|
8
|
+
module Gusto
|
9
|
+
class Cli < Thor
|
10
|
+
register(Init, 'init', 'init', 'Initialize rubocop-gusto and update .rubocop.yml')
|
11
|
+
|
12
|
+
desc 'sort [RUBOCOP_YML_PATH]', 'Sort the cops in a .rubocop.yml file (default: .rubocop.yml)'
|
13
|
+
method_option :output, type: :string, default: nil, desc: 'The path to the output file'
|
14
|
+
def sort(rubocop_yml_path = '.rubocop.yml')
|
15
|
+
say "Sorting #{rubocop_yml_path}..."
|
16
|
+
output_path = options[:output] || rubocop_yml_path
|
17
|
+
ConfigYml.load_file(rubocop_yml_path).sort!.write(output_path)
|
18
|
+
say "Done! #{output_path} sorted."
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pathname'
|
4
|
+
|
5
|
+
module RuboCop
|
6
|
+
module Gusto
|
7
|
+
# A class for reading and writing a .rubocop.yml file.
|
8
|
+
#
|
9
|
+
# You may rightly ask why we don't just use the standard library's YAML.load_file
|
10
|
+
# and YAML.dump. Simple, we want to preserve the comments.
|
11
|
+
class ConfigYml
|
12
|
+
COMMENT_REGEX = /\A\s*#.*\z/
|
13
|
+
COP_HEADER_REGEX = /\A[A-Z0-9][A-Za-z0-9\/:]+:(\s*#.*)?\z/
|
14
|
+
KEY_REGEX = /\A\w[\w\/]+:(\s*#.*)?\z/i # case insensitive
|
15
|
+
PREAMBLE_KEYS = %w(inherit_mode inherit_gem inherit_from plugins require).freeze
|
16
|
+
INDENT_REGEX = /\A( |- )/
|
17
|
+
|
18
|
+
# @param [String] file_path the path to the .rubocop.yml file
|
19
|
+
def self.load_file(file_path = '.rubocop.yml')
|
20
|
+
new(File.readlines(file_path))
|
21
|
+
rescue Errno::ENOENT
|
22
|
+
new([])
|
23
|
+
end
|
24
|
+
|
25
|
+
attr_reader :preamble, :cops
|
26
|
+
|
27
|
+
# @param [Array<String>] lines the lines of the .rubocop.yml file
|
28
|
+
def initialize(lines)
|
29
|
+
@preamble, @cops = chunk_blocks(lines).partition do |block|
|
30
|
+
block.none? { |line| line.rstrip.match?(COP_HEADER_REGEX) }
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Find if there's already an inherit_gem section and add the gem to it if needed
|
35
|
+
def add_inherit_gem(gem_name, *config_paths)
|
36
|
+
update_section_data('inherit_gem') do |data|
|
37
|
+
data ||= {}
|
38
|
+
data[gem_name.to_s] = config_paths.flatten
|
39
|
+
data
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Add a plugin to the plugins section or create it if it doesn't exist
|
44
|
+
def add_plugin(plugins)
|
45
|
+
update_section_data('plugins') do |data|
|
46
|
+
data ||= []
|
47
|
+
data.concat(plugins).uniq
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def update_section_data(section_name, &)
|
52
|
+
# Look for an existing section in the preamble
|
53
|
+
section = preamble.find { |chunk| chunk_name(chunk) == section_name }
|
54
|
+
|
55
|
+
if section
|
56
|
+
comments = section.select { |line| line.match?(COMMENT_REGEX) }
|
57
|
+
data = YAML.load(section.join)[section_name.to_s] # it can be present and nil
|
58
|
+
else
|
59
|
+
comments = []
|
60
|
+
data = nil
|
61
|
+
end
|
62
|
+
|
63
|
+
data = yield data
|
64
|
+
|
65
|
+
section_lines = YAML.dump({ section_name.to_s => data }).lines.drop(1) # drop the ---
|
66
|
+
section_lines.map! { |line| line.sub(/\A(\s*)-/, '\1 -') } # prefer indented lists
|
67
|
+
section_lines.insert(0, *comments) # add the comments back in at the top
|
68
|
+
section_lines << "\n" # ensure there's a trailing newline
|
69
|
+
|
70
|
+
section ? section.replace(section_lines) : preamble.unshift(section_lines)
|
71
|
+
|
72
|
+
self
|
73
|
+
end
|
74
|
+
|
75
|
+
def sort!
|
76
|
+
# Sort the preamble chunks by our preferred order, falling back to key name
|
77
|
+
preamble.sort_by! do |chunk|
|
78
|
+
key = chunk_name(chunk)
|
79
|
+
PREAMBLE_KEYS.index(key)&.to_s || key
|
80
|
+
end
|
81
|
+
|
82
|
+
# Sort the cops by their key name, putting comments at the top
|
83
|
+
cops.sort_by! { |cop| chunk_name(cop) || 'AAAAA/Comment?' }
|
84
|
+
|
85
|
+
self
|
86
|
+
end
|
87
|
+
|
88
|
+
def empty?
|
89
|
+
cops.empty? && preamble.empty?
|
90
|
+
end
|
91
|
+
|
92
|
+
def lines
|
93
|
+
combined = (preamble + cops).flatten
|
94
|
+
combined.pop # there's always one empty newline because of how we parse
|
95
|
+
combined
|
96
|
+
end
|
97
|
+
|
98
|
+
def to_s
|
99
|
+
lines.join
|
100
|
+
end
|
101
|
+
|
102
|
+
def write(file_path)
|
103
|
+
File.write(file_path, to_s)
|
104
|
+
end
|
105
|
+
|
106
|
+
private
|
107
|
+
|
108
|
+
# Return the name of a chunk by finding the root key
|
109
|
+
def chunk_name(chunk)
|
110
|
+
# Try to find a line that exactly matches KEY_REGEX
|
111
|
+
# Use rstrip, not strip, to preserve indentation
|
112
|
+
name_line = chunk.find { |line| line.rstrip.match?(KEY_REGEX) }
|
113
|
+
name_line&.rstrip&.delete_suffix(':')
|
114
|
+
end
|
115
|
+
|
116
|
+
# Splits the lines into blocks whenever we drop from indented to unindented
|
117
|
+
def chunk_blocks(lines)
|
118
|
+
# slice whenever we drop from indented to unindented
|
119
|
+
chunks = lines.slice_when do |prev, line|
|
120
|
+
prev.match?(INDENT_REGEX) && !prev.strip.empty? && !line.match?(INDENT_REGEX)
|
121
|
+
end
|
122
|
+
|
123
|
+
# Process each chunk to remove leading newlines and add 1 trailing newline
|
124
|
+
chunks.filter_map do |chunk|
|
125
|
+
# Remove leading and trailing empty lines
|
126
|
+
chunk.shift while chunk.first.to_s.strip.empty?
|
127
|
+
chunk.pop while chunk.last.to_s.strip.empty?
|
128
|
+
|
129
|
+
# Ensure each chunk ends with a blank newline
|
130
|
+
chunk << "\n"
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pathname'
|
4
|
+
require 'yaml'
|
5
|
+
require 'rubocop/gusto/config_yml'
|
6
|
+
|
7
|
+
module RuboCop
|
8
|
+
module Gusto
|
9
|
+
class Init < Thor::Group
|
10
|
+
include Thor::Actions
|
11
|
+
|
12
|
+
PLUGINS = %w(rubocop-gusto rubocop-rspec rubocop-performance rubocop-rake rubocop-rails).freeze
|
13
|
+
|
14
|
+
class_option :rubocop_yml, type: :string, default: '.rubocop.yml'
|
15
|
+
|
16
|
+
def self.source_root
|
17
|
+
File.expand_path('templates', __dir__)
|
18
|
+
end
|
19
|
+
|
20
|
+
def add_dependencies
|
21
|
+
if rails?
|
22
|
+
# we don't want rubocop-rails to be a dependency of the gem so that we can use this in non-rails gems
|
23
|
+
run 'bundle show rubocop-rails >/dev/null || bundle add rubocop-rails --group development', capture: true
|
24
|
+
end
|
25
|
+
|
26
|
+
run 'bundle binstub rubocop', capture: true
|
27
|
+
end
|
28
|
+
|
29
|
+
def copy_config_files
|
30
|
+
config = ConfigYml.load_file(options[:rubocop_yml])
|
31
|
+
|
32
|
+
if config.empty?
|
33
|
+
template 'rubocop.yml', options[:rubocop_yml]
|
34
|
+
config = ConfigYml.load_file(options[:rubocop_yml])
|
35
|
+
end
|
36
|
+
|
37
|
+
if rails?
|
38
|
+
config.add_inherit_gem('rubocop-gusto', 'config/default.yml', 'config/rails.yml')
|
39
|
+
config.add_plugin(PLUGINS)
|
40
|
+
else
|
41
|
+
config.add_inherit_gem('rubocop-gusto', 'config/default.yml')
|
42
|
+
config.add_plugin(PLUGINS - %w(rubocop-rails))
|
43
|
+
end
|
44
|
+
|
45
|
+
config.sort!
|
46
|
+
config.write(options[:rubocop_yml])
|
47
|
+
say_status 'update', options[:rubocop_yml]
|
48
|
+
|
49
|
+
create_file('.rubocop_todo.yml', skip: true)
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def rails?
|
55
|
+
File.exist?('config/application.rb')
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'lint_roller'
|
4
|
+
|
5
|
+
module RuboCop
|
6
|
+
module Gusto
|
7
|
+
# A plugin that integrates Gusto's standard RuboCop cops and rules.
|
8
|
+
class Plugin < LintRoller::Plugin
|
9
|
+
def about
|
10
|
+
LintRoller::About.new(
|
11
|
+
name: 'rubocop-gusto',
|
12
|
+
version: RuboCop::Gusto::VERSION,
|
13
|
+
homepage: 'https://github.com/Gusto/rubocop-gusto',
|
14
|
+
description: "A collection of Gusto's standard RuboCop cops and rules."
|
15
|
+
)
|
16
|
+
end
|
17
|
+
|
18
|
+
def supported?(context)
|
19
|
+
context.engine == :rubocop
|
20
|
+
end
|
21
|
+
|
22
|
+
def rules(_context)
|
23
|
+
project_root = Pathname.new(__dir__).join('../../..')
|
24
|
+
|
25
|
+
LintRoller::Rules.new(type: :path, config_format: :rubocop, value: project_root.join('config', 'default.yml'))
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
inherit_from: .rubocop_todo.yml
|
2
|
+
|
3
|
+
inherit_mode:
|
4
|
+
merge:
|
5
|
+
- Include
|
6
|
+
|
7
|
+
inherit_gem:
|
8
|
+
rubocop-gusto:
|
9
|
+
- config/default.yml
|
10
|
+
|
11
|
+
plugins:
|
12
|
+
- rubocop-gusto
|
13
|
+
- rubocop-rspec
|
14
|
+
- rubocop-performance
|
15
|
+
- rubocop-rake
|
16
|
+
|
17
|
+
AllCops:
|
18
|
+
NewCops: enable
|
19
|
+
TargetRubyVersion: 3.2
|
20
|
+
|
21
|
+
Layout/LineLength:
|
22
|
+
Enabled: false
|
23
|
+
|
24
|
+
Style/Documentation:
|
25
|
+
Enabled: false
|