rubocop-obsession 0.1.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/LICENSE.txt +21 -0
- data/README.md +91 -0
- data/config/default.yml +163 -0
- data/lib/rubocop/cop/mixin/files/verbs.txt +8507 -0
- data/lib/rubocop/cop/mixin/helpers.rb +27 -0
- data/lib/rubocop/cop/obsession/graphql/mutation_name.rb +40 -0
- data/lib/rubocop/cop/obsession/method_order.rb +244 -0
- data/lib/rubocop/cop/obsession/no_break_or_next.rb +94 -0
- data/lib/rubocop/cop/obsession/no_paragraphs.rb +62 -0
- data/lib/rubocop/cop/obsession/no_todos.rb +26 -0
- data/lib/rubocop/cop/obsession/rails/callback_one_method.rb +35 -0
- data/lib/rubocop/cop/obsession/rails/fully_defined_json_field.rb +71 -0
- data/lib/rubocop/cop/obsession/rails/migration_belongs_to.rb +44 -0
- data/lib/rubocop/cop/obsession/rails/no_callback_conditions.rb +60 -0
- data/lib/rubocop/cop/obsession/rails/private_callback.rb +59 -0
- data/lib/rubocop/cop/obsession/rails/safety_assured_comment.rb +37 -0
- data/lib/rubocop/cop/obsession/rails/service_name.rb +82 -0
- data/lib/rubocop/cop/obsession/rails/service_perform_method.rb +57 -0
- data/lib/rubocop/cop/obsession/rails/short_after_commit.rb +90 -0
- data/lib/rubocop/cop/obsession/rails/short_validate.rb +36 -0
- data/lib/rubocop/cop/obsession/rails/validate_one_field.rb +33 -0
- data/lib/rubocop/cop/obsession/rails/validation_method_name.rb +32 -0
- data/lib/rubocop/cop/obsession/rspec/describe_public_method.rb +125 -0
- data/lib/rubocop/cop/obsession/rspec/empty_line_after_final_let.rb +36 -0
- data/lib/rubocop/cop/obsession/too_many_paragraphs.rb +56 -0
- data/lib/rubocop/obsession/version.rb +5 -0
- data/lib/rubocop/obsession.rb +9 -0
- metadata +100 -0
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Helpers
|
6
|
+
VERBS = File.read("#{__dir__}/files/verbs.txt").split
|
7
|
+
|
8
|
+
def rails_callback?(callback)
|
9
|
+
return true if callback == 'validate'
|
10
|
+
|
11
|
+
callback.match?(
|
12
|
+
/
|
13
|
+
^(before|after|around)
|
14
|
+
_.*
|
15
|
+
(action|validation|create|update|save|destroy|commit|rollback)$
|
16
|
+
/x
|
17
|
+
)
|
18
|
+
end
|
19
|
+
|
20
|
+
def verb?(string)
|
21
|
+
short_string = string[2..] if string.start_with?('re')
|
22
|
+
|
23
|
+
VERBS.include?(string) || VERBS.include?(short_string)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Obsession
|
6
|
+
module Graphql
|
7
|
+
# This cop checks for mutation names that do not start with a verb.
|
8
|
+
#
|
9
|
+
# Mutation names should start with a verb, because mutations are actions,
|
10
|
+
# and actions are best described with verbs.
|
11
|
+
#
|
12
|
+
# @example
|
13
|
+
#
|
14
|
+
# # bad
|
15
|
+
# module Mutations
|
16
|
+
# class Event < Base
|
17
|
+
# end
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# # good
|
21
|
+
# module Mutations
|
22
|
+
# class TrackEvent < Base
|
23
|
+
# end
|
24
|
+
# end
|
25
|
+
class MutationName < Cop
|
26
|
+
include Helpers
|
27
|
+
|
28
|
+
MSG = 'Mutation name should start with a verb.'
|
29
|
+
|
30
|
+
def on_class(class_node)
|
31
|
+
class_name = class_node.identifier.source
|
32
|
+
class_name_first_word = class_name.underscore.split('_').first
|
33
|
+
|
34
|
+
add_offense(class_node) if !verb?(class_name_first_word)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,244 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Obsession
|
6
|
+
# This cop checks for private/protected methods that are not ordered correctly.
|
7
|
+
#
|
8
|
+
# Code should read from top to bottom: methods should be defined in the
|
9
|
+
# same order as the order when they are first mentioned.
|
10
|
+
# Private/protected methods should follow that rule.
|
11
|
+
#
|
12
|
+
# Note 1: public methods do not have to follow that rule, and can be
|
13
|
+
# defined in any order the developer wants, like by order of
|
14
|
+
# importance. This is because they are usually called outside of the
|
15
|
+
# class and often not called within the class at all. If possible though,
|
16
|
+
# developers should still try to order their public methods from top to
|
17
|
+
# bottom when it makes sense.
|
18
|
+
#
|
19
|
+
# Note 2: method order cannot be computed for methods called by `send`,
|
20
|
+
# metaprogramming, private methods called by superclasses or modules,
|
21
|
+
# etc. This cop's suggestions and autocorrections may be slightly off for
|
22
|
+
# these kinds of edge cases.
|
23
|
+
#
|
24
|
+
# Note 3: for more information on this style of method ordering, see
|
25
|
+
# Robert C. Martin's "Clean Code" book > "Chapter 3: Functions" > "One
|
26
|
+
# level of abstraction per function" > "Reading Code from Top to Bottom:
|
27
|
+
# The Stepdown Rule" chapter.
|
28
|
+
#
|
29
|
+
# @example
|
30
|
+
#
|
31
|
+
# # bad
|
32
|
+
# def perform
|
33
|
+
# return if method_a?
|
34
|
+
# method_b
|
35
|
+
# method_c
|
36
|
+
# end
|
37
|
+
#
|
38
|
+
# private
|
39
|
+
#
|
40
|
+
# def method_c; ...; end
|
41
|
+
# def method_b; ...; end
|
42
|
+
# def method_a?; ...; end
|
43
|
+
#
|
44
|
+
# # good
|
45
|
+
# def perform
|
46
|
+
# return if method_a?
|
47
|
+
# method_b
|
48
|
+
# method_c
|
49
|
+
# end
|
50
|
+
#
|
51
|
+
# private
|
52
|
+
#
|
53
|
+
# def method_a?; ...; end
|
54
|
+
# def method_b; ...; end
|
55
|
+
# def method_c; ...; end
|
56
|
+
class MethodOrder < Base
|
57
|
+
include Helpers
|
58
|
+
include RangeHelp
|
59
|
+
include VisibilityHelp
|
60
|
+
extend AutoCorrector
|
61
|
+
|
62
|
+
MSG = 'Method `%<after>s` should appear below `%<previous>s`.'
|
63
|
+
|
64
|
+
def_node_search :private_node, <<~PATTERN
|
65
|
+
(send nil? {:private :protected})
|
66
|
+
PATTERN
|
67
|
+
|
68
|
+
def_node_matcher :on_callback, <<~PATTERN
|
69
|
+
(send nil? $_ (sym $_) ...)
|
70
|
+
PATTERN
|
71
|
+
|
72
|
+
def_node_search :method_calls, <<~PATTERN
|
73
|
+
(send nil? $_ ...)
|
74
|
+
PATTERN
|
75
|
+
|
76
|
+
class Node
|
77
|
+
attr_accessor :value, :children
|
78
|
+
|
79
|
+
def initialize(value:, children: [])
|
80
|
+
@value = value
|
81
|
+
@children = children
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def on_class(class_node)
|
86
|
+
@class_node = class_node
|
87
|
+
find_private_node || return
|
88
|
+
build_methods || return
|
89
|
+
build_callback_methods
|
90
|
+
|
91
|
+
build_method_call_tree
|
92
|
+
build_ordered_private_methods
|
93
|
+
build_private_methods
|
94
|
+
|
95
|
+
verify_private_methods_order
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
def find_private_node
|
101
|
+
@private_node = private_node(@class_node)&.first
|
102
|
+
end
|
103
|
+
|
104
|
+
def build_methods
|
105
|
+
@methods = {}
|
106
|
+
return false if @class_node&.body&.type != :begin
|
107
|
+
|
108
|
+
@class_node.body.children.each do |child|
|
109
|
+
@methods[child.method_name] = child if child.type == :def
|
110
|
+
end
|
111
|
+
|
112
|
+
@methods.any?
|
113
|
+
end
|
114
|
+
|
115
|
+
def build_callback_methods
|
116
|
+
@callback_methods = []
|
117
|
+
|
118
|
+
@class_node.body.children.each do |node|
|
119
|
+
on_callback(node) do |callback, method_name|
|
120
|
+
if rails_callback?(callback.to_s) && @methods[method_name]
|
121
|
+
@callback_methods << @methods[method_name]
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def build_method_call_tree
|
128
|
+
methods = (@callback_methods + @methods.values).uniq
|
129
|
+
|
130
|
+
@method_call_tree =
|
131
|
+
Node.new(value: nil, children: methods.map { |method| method_call_tree(method) })
|
132
|
+
end
|
133
|
+
|
134
|
+
def method_call_tree(method_node, seen_method_calls = Set.new)
|
135
|
+
method_name = method_node.method_name
|
136
|
+
return nil if seen_method_calls.include?(method_name)
|
137
|
+
called_methods = find_called_methods(method_node)
|
138
|
+
return Node.new(value: method_node, children: []) if called_methods.empty?
|
139
|
+
|
140
|
+
children =
|
141
|
+
called_methods.filter_map do |called_method|
|
142
|
+
method_call_tree(called_method, seen_method_calls + [method_name])
|
143
|
+
end
|
144
|
+
|
145
|
+
Node.new(value: method_node, children: children)
|
146
|
+
end
|
147
|
+
|
148
|
+
def find_called_methods(method_node)
|
149
|
+
called_methods =
|
150
|
+
method_calls(method_node).filter_map { |method_call| @methods[method_call] }
|
151
|
+
|
152
|
+
@called_methods ||= Set.new(@callback_methods)
|
153
|
+
@called_methods += called_methods
|
154
|
+
|
155
|
+
called_methods
|
156
|
+
end
|
157
|
+
|
158
|
+
def build_ordered_private_methods
|
159
|
+
@ordered_private_methods = ordered_private_methods(@method_call_tree)
|
160
|
+
end
|
161
|
+
|
162
|
+
def ordered_private_methods(node)
|
163
|
+
ast_node = node.value
|
164
|
+
method_name = should_ignore?(ast_node) ? nil : ast_node.method_name
|
165
|
+
|
166
|
+
next_names = node.children.flat_map { |child| ordered_private_methods(child) }
|
167
|
+
|
168
|
+
([method_name] + next_names).compact.uniq
|
169
|
+
end
|
170
|
+
|
171
|
+
def should_ignore?(ast_node)
|
172
|
+
ast_node.nil? || node_visibility(ast_node) == :public ||
|
173
|
+
!@called_methods.include?(ast_node)
|
174
|
+
end
|
175
|
+
|
176
|
+
def build_private_methods
|
177
|
+
@private_methods = @methods.keys.intersection(@ordered_private_methods)
|
178
|
+
end
|
179
|
+
|
180
|
+
def verify_private_methods_order
|
181
|
+
@ordered_private_methods.each_with_index do |ordered_method, ordered_index|
|
182
|
+
index = @private_methods.index(ordered_method)
|
183
|
+
|
184
|
+
add_method_offense(ordered_method, ordered_index) && return if index != ordered_index
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
def add_method_offense(method_name, method_index)
|
189
|
+
method = @methods[method_name]
|
190
|
+
previous_method =
|
191
|
+
if method_index > 0
|
192
|
+
previous_method_name = @ordered_private_methods[method_index - 1]
|
193
|
+
@methods[previous_method_name]
|
194
|
+
else
|
195
|
+
@private_node
|
196
|
+
end
|
197
|
+
|
198
|
+
message = format(MSG, previous: previous_method.method_name, after: method_name)
|
199
|
+
|
200
|
+
add_offense(method, message: message) do |corrector|
|
201
|
+
autocorrect(corrector, method, previous_method)
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
def autocorrect(corrector, method, previous_method)
|
206
|
+
previous_method_range = source_range_with_comment(previous_method)
|
207
|
+
method_range = source_range_with_comment(method)
|
208
|
+
|
209
|
+
corrector.insert_after(previous_method_range, method_range.source)
|
210
|
+
corrector.remove(method_range)
|
211
|
+
end
|
212
|
+
|
213
|
+
def source_range_with_comment(node)
|
214
|
+
range_between(start_position_with_comment(node), end_position(node) + 1)
|
215
|
+
end
|
216
|
+
|
217
|
+
def start_position_with_comment(node)
|
218
|
+
first_comment = nil
|
219
|
+
|
220
|
+
(node.first_line - 1).downto(1) do |annotation_line|
|
221
|
+
comment = processed_source.comment_at_line(annotation_line)
|
222
|
+
break if !comment
|
223
|
+
first_comment = comment if whole_line_comment_at_line?(annotation_line)
|
224
|
+
end
|
225
|
+
|
226
|
+
start_line_position(first_comment || node)
|
227
|
+
end
|
228
|
+
|
229
|
+
def whole_line_comment_at_line?(line)
|
230
|
+
/\A\s*#/.match?(processed_source.lines[line - 1])
|
231
|
+
end
|
232
|
+
|
233
|
+
def start_line_position(node)
|
234
|
+
processed_source.buffer.line_range(node.loc.line).begin_pos - 1
|
235
|
+
end
|
236
|
+
|
237
|
+
def end_position(node)
|
238
|
+
end_line = processed_source.buffer.line_for_position(node.loc.expression.end_pos)
|
239
|
+
processed_source.buffer.line_range(end_line).end_pos
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Obsession
|
6
|
+
# This cop checks for `next` (and sometimes `break`) in loops.
|
7
|
+
#
|
8
|
+
# - For big loops, `next` or `break` indicates that the loop body has
|
9
|
+
# significant logic, which means it should be moved into its own method,
|
10
|
+
# and you can convert the `next` or `break` into `return` and the like.
|
11
|
+
# - For small loops, you can just use normal conditions instead of `next`.
|
12
|
+
# `break` is allowed.
|
13
|
+
#
|
14
|
+
# Note: Sometimes loops can also be rethought, like transforming a `loop`
|
15
|
+
# + `break` into a `while`.
|
16
|
+
#
|
17
|
+
# @example
|
18
|
+
#
|
19
|
+
# # bad
|
20
|
+
# github_teams.each do |github_team|
|
21
|
+
# next if github_team['size'] == 0
|
22
|
+
# team = @company.teams.find_or_initialize_by(github_team['id'])
|
23
|
+
#
|
24
|
+
# team.update!(
|
25
|
+
# name: github_team['name'],
|
26
|
+
# description: github_team['description'],
|
27
|
+
# owner: @company,
|
28
|
+
# )
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# # good
|
32
|
+
# github_teams.each do |github_team| { |github_team| upsert_team(github_team) }
|
33
|
+
#
|
34
|
+
# def upsert_team(github_team)
|
35
|
+
# return if github_team['size'] == 0
|
36
|
+
# team = @company.teams.find_or_initialize_by(github_team['id'])
|
37
|
+
#
|
38
|
+
# team.update!(
|
39
|
+
# name: github_team['name'],
|
40
|
+
# description: github_team['description'],
|
41
|
+
# owner: @company,
|
42
|
+
# )
|
43
|
+
# end
|
44
|
+
#
|
45
|
+
# # bad
|
46
|
+
# def highlight
|
47
|
+
# blog_posts.each do |blog_post|
|
48
|
+
# next if !blog_post.published?
|
49
|
+
#
|
50
|
+
# self.highlighted = true
|
51
|
+
# end
|
52
|
+
# end
|
53
|
+
#
|
54
|
+
# # good
|
55
|
+
# def highlight
|
56
|
+
# blog_posts.each do |blog_post|
|
57
|
+
# if blog_post.published?
|
58
|
+
# self.highlighted = true
|
59
|
+
# end
|
60
|
+
# end
|
61
|
+
# end
|
62
|
+
class NoBreakOrNext < Cop
|
63
|
+
BIG_LOOP_MSG =
|
64
|
+
'Avoid `break`/`next` in big loop, decompose into private method or rethink loop.'
|
65
|
+
NO_NEXT_MSG = 'Avoid `next` in loop, use conditions or rethink loop.'
|
66
|
+
BIG_LOOP_MIN_LINES = 7
|
67
|
+
|
68
|
+
def_node_matcher :contains_break_or_next?, <<~PATTERN
|
69
|
+
`(if <({next break}) ...>)
|
70
|
+
PATTERN
|
71
|
+
|
72
|
+
def_node_matcher :contains_next?, <<~PATTERN
|
73
|
+
`(if <(next) ...>)
|
74
|
+
PATTERN
|
75
|
+
|
76
|
+
def on_block(node)
|
77
|
+
return if !contains_break_or_next?(node)
|
78
|
+
|
79
|
+
if big_loop?(node)
|
80
|
+
add_offense(node, message: BIG_LOOP_MSG)
|
81
|
+
elsif contains_next?(node)
|
82
|
+
add_offense(node, message: NO_NEXT_MSG)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def big_loop?(node)
|
89
|
+
node.line_count >= BIG_LOOP_MIN_LINES + 2
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Obsession
|
6
|
+
# This cop checks for methods with many instructions but no paragraphs.
|
7
|
+
#
|
8
|
+
# If your method code has many instructions that are not organized into
|
9
|
+
# paragraphs, you should break it up into multiple paragraphs to make the
|
10
|
+
# code more breathable and readable. The 3 possible paragraphs themes
|
11
|
+
# always are: initialization, action, and result.
|
12
|
+
#
|
13
|
+
# @example
|
14
|
+
#
|
15
|
+
# # bad
|
16
|
+
# def set_seo_content
|
17
|
+
# return if slug.blank?
|
18
|
+
# return if seo_content.present?
|
19
|
+
# template = SeoTemplate.find_by(template_type: 'BlogPost')
|
20
|
+
# return if template.blank?
|
21
|
+
# self.seo_content = build_seo_content(seo_template: template, slug: slug)
|
22
|
+
# Rails.logger.info('Content has been set')
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# # good
|
26
|
+
# def set_seo_content
|
27
|
+
# return if slug.blank?
|
28
|
+
# return if seo_content.present?
|
29
|
+
# template = SeoTemplate.find_by(template_type: 'BlogPost')
|
30
|
+
# return if template.blank?
|
31
|
+
#
|
32
|
+
# self.seo_content = build_seo_content(seo_template: template, slug: slug)
|
33
|
+
#
|
34
|
+
# Rails.logger.info('Content has been set')
|
35
|
+
# end
|
36
|
+
class NoParagraphs < Cop
|
37
|
+
MSG = 'Method has many instructions and should be broken up into paragraphs.'
|
38
|
+
MAX_CONSECUTIVE_INSTRUCTIONS_ALLOWED = 5
|
39
|
+
|
40
|
+
def on_def(node)
|
41
|
+
lines = processed_source.lines[node.first_line..(node.last_line - 2)]
|
42
|
+
return if lines.any?(&:blank?)
|
43
|
+
node_body_type = node&.body&.type
|
44
|
+
return if %i[send if or case array hash].include?(node_body_type)
|
45
|
+
return if %w[asgn str].any? { |string| node_body_type.to_s.include?(string) }
|
46
|
+
|
47
|
+
too_big =
|
48
|
+
case node_body_type
|
49
|
+
when :begin, :block, :rescue
|
50
|
+
node.body.children.count > MAX_CONSECUTIVE_INSTRUCTIONS_ALLOWED &&
|
51
|
+
!node.body.children.all? { |child| child.type.to_s.include?('asgn') }
|
52
|
+
else
|
53
|
+
lines.count > MAX_CONSECUTIVE_INSTRUCTIONS_ALLOWED
|
54
|
+
end
|
55
|
+
|
56
|
+
add_offense(node) if too_big
|
57
|
+
end
|
58
|
+
alias_method :on_defs, :on_def
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Obsession
|
6
|
+
# This cop checks for TODO/FIXME/etc comments.
|
7
|
+
#
|
8
|
+
# Avoid TODO comments, instead create tasks for them in your project
|
9
|
+
# management software, and assign them to the right person. Half of the
|
10
|
+
# TODOs usually never get done, and the code is then polluted with old
|
11
|
+
# stale TODOs. Sometimes developers really mean to work on their TODOs
|
12
|
+
# soon, but then Product re-prioritizes their work, or the developer
|
13
|
+
# leaves the company, and never gets a chance to tackle them.
|
14
|
+
class NoTodos < Cop
|
15
|
+
MSG = 'Avoid TODO comment, create a task in your project management tool instead.'
|
16
|
+
KEYWORD_REGEX = /(^|[^\w])(TODO|FIXME|OPTIMIZE|HACK)($|[^\w])/i
|
17
|
+
|
18
|
+
def on_new_investigation
|
19
|
+
processed_source.comments.each do |comment|
|
20
|
+
add_offense(comment) if comment.text.match?(KEYWORD_REGEX)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Obsession
|
6
|
+
module Rails
|
7
|
+
# This cop checks for Rails callbacks with multiple fields.
|
8
|
+
#
|
9
|
+
# One method per callback definition makes the definition extra clear.
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
#
|
13
|
+
# # bad
|
14
|
+
# after_create :notify_followers, :send_stats
|
15
|
+
#
|
16
|
+
# # good
|
17
|
+
# after_create :notify_followers
|
18
|
+
# after_create :send_stats
|
19
|
+
class CallbackOneMethod < Cop
|
20
|
+
include Helpers
|
21
|
+
|
22
|
+
MSG = 'Declare only one method per callback definition.'
|
23
|
+
|
24
|
+
def_node_matcher :on_callback, <<~PATTERN
|
25
|
+
(send nil? $_ (sym _) (sym _) ...)
|
26
|
+
PATTERN
|
27
|
+
|
28
|
+
def on_send(node)
|
29
|
+
on_callback(node) { |callback| add_offense(node) if rails_callback?(callback.to_s) }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Obsession
|
6
|
+
module Rails
|
7
|
+
# This cop checks for json(b) fields that are not fully defined with
|
8
|
+
# defaults or comments.
|
9
|
+
#
|
10
|
+
# - json(b) fields should have a default value like {} or [] so code can
|
11
|
+
# do my_field['field'] or my_field.first without fear that my_field is
|
12
|
+
# nil.
|
13
|
+
# - It is impossible to know the structure of a json(b) field just by
|
14
|
+
# reading the schema, because json(b) is an unstructured type. That's why
|
15
|
+
# an "Example: ..." Postgres comment should always be present when
|
16
|
+
# defining the field.
|
17
|
+
#
|
18
|
+
# @example
|
19
|
+
#
|
20
|
+
# # bad
|
21
|
+
# add_column :languages, :items, :jsonb
|
22
|
+
#
|
23
|
+
# # good
|
24
|
+
# add_column :languages,
|
25
|
+
# :items,
|
26
|
+
# :jsonb,
|
27
|
+
# default: [],
|
28
|
+
# comment: "Example: [{ 'name': 'ruby' }, { 'name': 'python' }]"
|
29
|
+
class FullyDefinedJsonField < Cop
|
30
|
+
def_node_matcher :json_field?, <<~PATTERN
|
31
|
+
(send nil? :add_column _ _ (sym {:json :jsonb}) ...)
|
32
|
+
PATTERN
|
33
|
+
|
34
|
+
def_node_matcher :has_default?, <<~PATTERN
|
35
|
+
(hash <(pair (sym :default) ...) ...>)
|
36
|
+
PATTERN
|
37
|
+
|
38
|
+
def_node_matcher :has_comment_with_example?, <<~PATTERN
|
39
|
+
(hash <
|
40
|
+
(pair
|
41
|
+
(sym :comment)
|
42
|
+
`{
|
43
|
+
(str /^Example: [\\[\\{].{4,}[\\]\\}]/) |
|
44
|
+
(dstr (str /^Example: [\\[\\{]/) ... (str /[\\]\\}]/) )
|
45
|
+
}
|
46
|
+
)
|
47
|
+
...
|
48
|
+
>)
|
49
|
+
PATTERN
|
50
|
+
|
51
|
+
def on_send(node)
|
52
|
+
return if !json_field?(node)
|
53
|
+
options_node = node.children[5]
|
54
|
+
|
55
|
+
if !has_default?(options_node)
|
56
|
+
add_offense(node, message: 'Add default value of {} or []')
|
57
|
+
end
|
58
|
+
|
59
|
+
if !has_comment_with_example?(options_node)
|
60
|
+
add_offense(
|
61
|
+
node,
|
62
|
+
message:
|
63
|
+
'Add `comment: "Example: <example>"` option with an example array or hash value'
|
64
|
+
)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Obsession
|
6
|
+
module Rails
|
7
|
+
# This cop checks for migrations that use `references` instead of `belongs_to`.
|
8
|
+
#
|
9
|
+
# Instead of adding `references` in migrations, use the `belongs_to`
|
10
|
+
# alias. It reads nicer and is more similar to the `belongs_to`
|
11
|
+
# declarations that you find in model code.
|
12
|
+
#
|
13
|
+
# @example
|
14
|
+
#
|
15
|
+
# # bad
|
16
|
+
# def change
|
17
|
+
# add_reference :blog_posts, :user
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# # good
|
21
|
+
# def change
|
22
|
+
# add_belongs_to :blog_posts, :user
|
23
|
+
# end
|
24
|
+
class MigrationBelongsTo < Cop
|
25
|
+
def_node_matcher :add_reference?, <<~PATTERN
|
26
|
+
(send nil? :add_reference ...)
|
27
|
+
PATTERN
|
28
|
+
|
29
|
+
def_node_matcher :table_reference?, <<~PATTERN
|
30
|
+
(send (lvar :t) :references ...)
|
31
|
+
PATTERN
|
32
|
+
|
33
|
+
def on_send(node)
|
34
|
+
if add_reference?(node)
|
35
|
+
add_offense(node, message: 'Use add_belongs_to instead of add_reference')
|
36
|
+
elsif table_reference?(node)
|
37
|
+
add_offense(node, message: 'Use t.belongs_to instead of t.references')
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|