rubocop-obsession 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|