rubocop-obsession 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (29) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +91 -0
  4. data/config/default.yml +163 -0
  5. data/lib/rubocop/cop/mixin/files/verbs.txt +8507 -0
  6. data/lib/rubocop/cop/mixin/helpers.rb +27 -0
  7. data/lib/rubocop/cop/obsession/graphql/mutation_name.rb +40 -0
  8. data/lib/rubocop/cop/obsession/method_order.rb +244 -0
  9. data/lib/rubocop/cop/obsession/no_break_or_next.rb +94 -0
  10. data/lib/rubocop/cop/obsession/no_paragraphs.rb +62 -0
  11. data/lib/rubocop/cop/obsession/no_todos.rb +26 -0
  12. data/lib/rubocop/cop/obsession/rails/callback_one_method.rb +35 -0
  13. data/lib/rubocop/cop/obsession/rails/fully_defined_json_field.rb +71 -0
  14. data/lib/rubocop/cop/obsession/rails/migration_belongs_to.rb +44 -0
  15. data/lib/rubocop/cop/obsession/rails/no_callback_conditions.rb +60 -0
  16. data/lib/rubocop/cop/obsession/rails/private_callback.rb +59 -0
  17. data/lib/rubocop/cop/obsession/rails/safety_assured_comment.rb +37 -0
  18. data/lib/rubocop/cop/obsession/rails/service_name.rb +82 -0
  19. data/lib/rubocop/cop/obsession/rails/service_perform_method.rb +57 -0
  20. data/lib/rubocop/cop/obsession/rails/short_after_commit.rb +90 -0
  21. data/lib/rubocop/cop/obsession/rails/short_validate.rb +36 -0
  22. data/lib/rubocop/cop/obsession/rails/validate_one_field.rb +33 -0
  23. data/lib/rubocop/cop/obsession/rails/validation_method_name.rb +32 -0
  24. data/lib/rubocop/cop/obsession/rspec/describe_public_method.rb +125 -0
  25. data/lib/rubocop/cop/obsession/rspec/empty_line_after_final_let.rb +36 -0
  26. data/lib/rubocop/cop/obsession/too_many_paragraphs.rb +56 -0
  27. data/lib/rubocop/obsession/version.rb +5 -0
  28. data/lib/rubocop/obsession.rb +9 -0
  29. metadata +100 -0
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Obsession
6
+ module Rails
7
+ # This cop checks for model callbacks with conditions.
8
+ #
9
+ # Model callback with conditions should be avoided because they can
10
+ # quickly degenerate into multiple conditions that pollute the macro
11
+ # definition section, even more so if lambdas are involved. Instead, move
12
+ # the condition inside the callback method.
13
+ #
14
+ # Note: conditions are allowed for `validates :field` callbacks, as it is
15
+ # sometimes not easy to translate them into `validate :validate_field`
16
+ # custom validation callbacks.
17
+ #
18
+ # @example
19
+ #
20
+ # # bad
21
+ # after_update_commit :crawl_rss, if: :rss_changed?
22
+ # def crawl_rss
23
+ # ...
24
+ # end
25
+ #
26
+ # # good
27
+ # after_update_commit :crawl_rss
28
+ # def crawl_rss
29
+ # return if !rss_changed?
30
+ # ...
31
+ # end
32
+ class NoCallbackConditions < Cop
33
+ MSG =
34
+ 'Avoid condition in callback declaration, move it inside the callback method instead.'
35
+
36
+ def_node_matcher :callback_with_condition?, <<~PATTERN
37
+ (
38
+ send nil? _
39
+ (sym _)
40
+ (hash
41
+ ...
42
+ (pair (sym {:if :unless}) ...)
43
+ ...
44
+ )
45
+ )
46
+ PATTERN
47
+
48
+ def on_send(node)
49
+ return if !callback_with_condition?(node)
50
+ callback = node.children[1]
51
+ return if callback == :validates
52
+ return if callback.to_s.include?('around')
53
+
54
+ add_offense(node)
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Obsession
6
+ module Rails
7
+ # This cop checks for callbacks methods that are not private.
8
+ #
9
+ # Callback methods are usually never called outside of the class, so
10
+ # there is no reason to declare them in the public section. They should
11
+ # be private.
12
+ #
13
+ # @example
14
+ #
15
+ # # bad
16
+ # before_action :load_blog_post
17
+ #
18
+ # def load_blog_post
19
+ # ...
20
+ # end
21
+ #
22
+ # # good
23
+ # before_action :load_blog_post
24
+ #
25
+ # private
26
+ #
27
+ # def load_blog_post
28
+ # ...
29
+ # end
30
+ class PrivateCallback < Cop
31
+ include VisibilityHelp
32
+ include Helpers
33
+
34
+ MSG = 'Make callback method private'
35
+
36
+ def_node_matcher :on_callback, <<~PATTERN
37
+ (send nil? $_ (sym $_) ...)
38
+ PATTERN
39
+
40
+ def on_new_investigation
41
+ @callbacks = Set.new
42
+ end
43
+
44
+ def on_send(node)
45
+ on_callback(node) do |callback, method_name|
46
+ @callbacks << method_name if rails_callback?(callback.to_s)
47
+ end
48
+ end
49
+
50
+ def on_def(node)
51
+ if @callbacks.include?(node.method_name) && node_visibility(node) == :public
52
+ add_offense(node, message: MSG)
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Obsession
6
+ module Rails
7
+ # This cop checks uses of strong_migrations' `safety_assured { ... }`
8
+ # without a valid reason.
9
+ #
10
+ # `safety_assured { ... }` should only be used after *carefully*
11
+ # following the instructions from the strong_migrations gem. Always add a
12
+ # `# Safe because <reason>` comment explaining how you assured the safety
13
+ # of the DB migration. The reason should be detailed and reviewed by a
14
+ # knowledgeable PR reviewer. Failure to follow instructions may bring your
15
+ # app down.
16
+ class SafetyAssuredComment < Cop
17
+ MSG =
18
+ 'Add `# Safe because <reason>` comment above safety_assured. ' \
19
+ 'An invalid reason may bring the site down.'
20
+
21
+ def_node_matcher :safety_assured_block?, <<~PATTERN
22
+ (block (send nil? :safety_assured) ...)
23
+ PATTERN
24
+
25
+ def on_block(node)
26
+ return if !safety_assured_block?(node)
27
+ previous_comment = processed_source.comments_before_line(node.first_line)&.first
28
+
29
+ if previous_comment.nil? || !previous_comment.text.match?(/^# Safe because( [^ ]+){4}/)
30
+ add_offense(node)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Obsession
6
+ module Rails
7
+ # This cop checks for services and jobs whose name do not start with a verb.
8
+ #
9
+ # Services and jobs with only one public method should have a name that
10
+ # starts with a verb, because these classes are essentially performing
11
+ # one action, and the best way to describe an action is with a verb.
12
+ #
13
+ # @example
14
+ #
15
+ # # bad
16
+ # class Company
17
+ # def perform
18
+ # ...
19
+ # end
20
+ # end
21
+ #
22
+ # # good
23
+ # class ImportCompany
24
+ # def perform
25
+ # ...
26
+ # end
27
+ # end
28
+ #
29
+ # # bad
30
+ # class BlogPostPopularityJob < ApplicationJob
31
+ # def perform
32
+ # ...
33
+ # end
34
+ # end
35
+ #
36
+ # # good
37
+ # class UpdateBlogPostPopularityJob < ApplicationJob
38
+ # def perform
39
+ # ...
40
+ # end
41
+ # end
42
+ class ServiceName < Base
43
+ include Helpers
44
+
45
+ MSG = 'Service or Job name should start with a verb.'
46
+ IGNORED_PUBLIC_METHODS = %i[initialize lock_period].freeze
47
+
48
+ def_node_matcher :private_section?, <<~PATTERN
49
+ (send nil? {:private :protected})
50
+ PATTERN
51
+
52
+ def on_class(class_node)
53
+ return if public_methods(class_node).length != 1
54
+ class_name = class_node.identifier.source
55
+ class_name_first_word = class_name.underscore.split('_').first
56
+
57
+ add_offense(class_node) if !verb?(class_name_first_word)
58
+ end
59
+
60
+ private
61
+
62
+ def public_methods(class_node)
63
+ return [] if !class_node.body
64
+ public_methods = []
65
+
66
+ case class_node.body.type
67
+ when :def
68
+ public_methods << class_node.body
69
+ when :begin
70
+ class_node.body.children.each do |child|
71
+ public_methods << child if child.type == :def
72
+ break if private_section?(child)
73
+ end
74
+ end
75
+
76
+ public_methods.reject { |method| IGNORED_PUBLIC_METHODS.include?(method.method_name) }
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Obsession
6
+ module Rails
7
+ # This cop checks for services whose single public method is not named
8
+ # `perform`.
9
+ #
10
+ # Services and jobs with only one public method should have their method
11
+ # named `perform` for consistency. The choice of `perform` as a name is
12
+ # inspired from ActiveJob and makes it easier to make services and jobs
13
+ # interchangeable.
14
+ #
15
+ # @example
16
+ #
17
+ # # bad
18
+ # class ImportCompany
19
+ # def import
20
+ # ...
21
+ # end
22
+ # end
23
+ #
24
+ # # bad
25
+ # class ImportCompany
26
+ # def execute
27
+ # ...
28
+ # end
29
+ # end
30
+ #
31
+ # # good
32
+ # class ImportCompany
33
+ # def perform
34
+ # ...
35
+ # end
36
+ # end
37
+ class ServicePerformMethod < ServiceName
38
+ extend AutoCorrector
39
+
40
+ MSG = 'Single public method of Service should be called `perform`'
41
+
42
+ def on_class(class_node)
43
+ public_methods = public_methods(class_node)
44
+ return if public_methods.length != 1
45
+ method = public_methods.first
46
+
47
+ if method.method_name != :perform
48
+ add_offense(method) do |corrector|
49
+ corrector.replace(method, method.source.sub(/#{method.method_name}/, 'perform'))
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Obsession
6
+ module Rails
7
+ # This cop checks for `after_commit` declarations that could be shorter.
8
+ #
9
+ # @example
10
+ #
11
+ # # bad
12
+ # after_commit :send_email, on: :create
13
+ #
14
+ # # good
15
+ # after_create_commit :send_email
16
+ class ShortAfterCommit < Cop
17
+ MSG = 'Use shorter %<prefer>s'
18
+
19
+ def_node_matcher :after_commit?, '(send nil? :after_commit ...)'
20
+
21
+ def_node_matcher :after_commit_create?, <<~PATTERN
22
+ (
23
+ send nil? :after_commit
24
+ (sym _)
25
+ (hash
26
+ (pair (sym :on) {(sym :create)|(array (sym :create))})
27
+ )
28
+ )
29
+ PATTERN
30
+
31
+ def_node_matcher :after_commit_update?, <<~PATTERN
32
+ (
33
+ send nil? :after_commit
34
+ (sym _)
35
+ (hash
36
+ (pair (sym :on) {(sym :update)|(array (sym :update))})
37
+ )
38
+ )
39
+ PATTERN
40
+
41
+ def_node_matcher :after_commit_destroy?, <<~PATTERN
42
+ (
43
+ send nil? :after_commit
44
+ (sym _)
45
+ (hash
46
+ (pair (sym :on) {(sym :destroy)|(array (sym :destroy))})
47
+ )
48
+ )
49
+ PATTERN
50
+
51
+ def_node_matcher :after_commit_create_update?, <<~PATTERN
52
+ (
53
+ send nil? :after_commit
54
+ (sym _)
55
+ (hash
56
+ (pair (sym :on) (array <(sym :create) (sym :update)>))
57
+ )
58
+ )
59
+ PATTERN
60
+
61
+ def_node_matcher :after_commit_all_events?, <<~PATTERN
62
+ (
63
+ send nil? :after_commit
64
+ (sym _)
65
+ (hash
66
+ (pair (sym :on) (array <(sym :create) (sym :update) (sym :destroy)>))
67
+ )
68
+ )
69
+ PATTERN
70
+
71
+ def on_send(node)
72
+ return if !after_commit?(node)
73
+
74
+ if after_commit_create?(node)
75
+ add_offense(node, message: format(MSG, prefer: 'after_create_commit'))
76
+ elsif after_commit_update?(node)
77
+ add_offense(node, message: format(MSG, prefer: 'after_update_commit'))
78
+ elsif after_commit_destroy?(node)
79
+ add_offense(node, message: format(MSG, prefer: 'after_destroy_commit'))
80
+ elsif after_commit_create_update?(node)
81
+ add_offense(node, message: format(MSG, prefer: 'after_save_commit'))
82
+ elsif after_commit_all_events?(node)
83
+ add_offense(node, message: format(MSG, prefer: 'after_commit with no `on:` argument'))
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Obsession
6
+ module Rails
7
+ # This cop checks for `validate` declarations that could be shorter.
8
+ #
9
+ # @example
10
+ #
11
+ # # bad
12
+ # validate :validate_url, on: %i(create update)
13
+ #
14
+ # # good
15
+ # validate :validate_url
16
+ class ShortValidate < Cop
17
+ MSG = 'The `on:` argument is not needed in this validate.'
18
+
19
+ def_node_matcher :validate_with_unneeded_on?, <<~PATTERN
20
+ (
21
+ send nil? :validate
22
+ (sym _)
23
+ (hash
24
+ (pair (sym :on) (array <(sym :create) (sym :update)>))
25
+ )
26
+ )
27
+ PATTERN
28
+
29
+ def on_send(node)
30
+ add_offense(node) if validate_with_unneeded_on?(node)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Obsession
6
+ module Rails
7
+ # This cop checks for `validates` callbacks with multiple fields.
8
+ #
9
+ # One field per `validates` makes the validation extra clear.
10
+ #
11
+ # @example
12
+ #
13
+ # # bad
14
+ # validates :name, :status, presence: true
15
+ #
16
+ # # good
17
+ # validates :name, presence: true
18
+ # validates :status, presence: true
19
+ class ValidateOneField < Cop
20
+ MSG = 'Validate only one field per line.'
21
+
22
+ def_node_matcher :validates_with_more_than_one_field?, <<~PATTERN
23
+ (send nil? :validates (sym _) (sym _) ...)
24
+ PATTERN
25
+
26
+ def on_send(node)
27
+ add_offense(node) if validates_with_more_than_one_field?(node)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Obsession
6
+ module Rails
7
+ # This cop checks for validation methods that do not start with `validate_`.
8
+ #
9
+ # @example
10
+ #
11
+ # # bad
12
+ # validate :at_least_one_admin
13
+ #
14
+ # # good
15
+ # validate :validate_at_least_one_admin
16
+ class ValidationMethodName < Cop
17
+ MSG = 'Prefix custom validation method with validate_'
18
+
19
+ def_node_matcher :on_validate_callback, <<~PATTERN
20
+ (send nil? :validate (sym $_) ...)
21
+ PATTERN
22
+
23
+ def on_send(node)
24
+ on_validate_callback(node) do |method_name|
25
+ add_offense(node) if !method_name.start_with?('validate_')
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Obsession
6
+ module Rspec
7
+ # This cop checks for `describe` blocks that test private methods.
8
+ #
9
+ # If you are doing black box unit testing, it means that you are only
10
+ # interested in testing external behavior, a.k.a public interface,
11
+ # a.k.a public methods. Private methods are considered implementation
12
+ # details and are not directly tested.
13
+ #
14
+ # If you need to test a Rails callback, test it indirectly through its
15
+ # corresponding Rails public method, e.g. #create, #save, etc.
16
+ #
17
+ # @example
18
+ #
19
+ # class Comment < ApplicationRecord
20
+ # after_create_commit :notify_users
21
+ #
22
+ # private
23
+ #
24
+ # def notify_users
25
+ # ...
26
+ # end
27
+ # end
28
+ #
29
+ # # bad
30
+ # describe '#notify_users' do
31
+ # ...
32
+ # end
33
+ #
34
+ # # good
35
+ # describe '#create' do
36
+ # it 'notifies users' do
37
+ # ...
38
+ # end
39
+ # end
40
+ class DescribePublicMethod < Cop
41
+ MSG = 'Only test public methods.'
42
+
43
+ def_node_matcher :on_context_method, <<-PATTERN
44
+ (block (send nil? :describe (str $#method_name?)) ...)
45
+ PATTERN
46
+
47
+ def_node_search :class_nodes, <<~PATTERN
48
+ (class ...)
49
+ PATTERN
50
+
51
+ def_node_matcher :private_section?, <<~PATTERN
52
+ (send nil? {:private :protected})
53
+ PATTERN
54
+
55
+ def on_block(node)
56
+ on_context_method(node) do |method_name|
57
+ method_name = method_name.sub('#', '').to_sym
58
+ add_offense(node) if private_methods.include?(method_name)
59
+ end
60
+ end
61
+
62
+ private
63
+
64
+ def method_name?(description)
65
+ description.start_with?('#')
66
+ end
67
+
68
+ def private_methods
69
+ return @private_methods if @private_methods
70
+ @private_methods = []
71
+
72
+ if File.exist?(tested_file_path)
73
+ node = parse_file(tested_file_path)
74
+ @private_methods = find_private_methods(class_nodes(node).first)
75
+ end
76
+
77
+ @private_methods
78
+ end
79
+
80
+ def tested_file_path
81
+ return @tested_file_path if @tested_file_path
82
+
83
+ spec_path = processed_source.file_path.sub(Dir.pwd, '')
84
+ file_path =
85
+ if spec_path.include?('/lib/')
86
+ spec_path.sub('/spec/lib/', '/lib/')
87
+ else
88
+ spec_path.sub('/spec/', '/app/')
89
+ end
90
+ file_path = file_path.sub('_spec.rb', '.rb')
91
+ file_path = File.join(Dir.pwd, file_path)
92
+
93
+ @tested_file_path = file_path
94
+ end
95
+
96
+ def parse_file(file_path)
97
+ parser_class = ::Parser.const_get(:"Ruby#{target_ruby_version.to_s.sub('.', '')}")
98
+ parser = parser_class.new(RuboCop::AST::Builder.new)
99
+
100
+ buffer = Parser::Source::Buffer.new(file_path, 1)
101
+ buffer.source = File.read(file_path)
102
+
103
+ parser.parse(buffer)
104
+ end
105
+
106
+ def find_private_methods(class_node)
107
+ return [] if class_node&.body&.type != :begin
108
+ private_methods = []
109
+ private_section_found = false
110
+
111
+ class_node.body.children.each do |child|
112
+ if private_section?(child)
113
+ private_section_found = true
114
+ elsif child.type == :def && private_section_found
115
+ private_methods << child.method_name
116
+ end
117
+ end
118
+
119
+ private_methods
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,36 @@
1
+ if defined?(RuboCop::RSpec)
2
+ module RuboCop
3
+ module Cop
4
+ module Obsession
5
+ module Rspec
6
+ # Same as RSpec/EmptyLineAfterFinalLet, but allows `let` to be followed
7
+ # by `it` with no new line, to allow for this style of spec:
8
+ #
9
+ # @example
10
+ #
11
+ # describe '#domain' do
12
+ # context do
13
+ # let(:url) { Url.new('http://www.some-site.com/some-page') }
14
+ # it { expect(url.domain).to eq 'some-site.com' }
15
+ # end
16
+ #
17
+ # context do
18
+ # let(:url) { Url.new('some-site.com') }
19
+ # it { expect(url.domain).to eq 'some-site.com' }
20
+ # end
21
+ # end
22
+ class EmptyLineAfterFinalLet < RSpec::EmptyLineAfterFinalLet
23
+ def missing_separating_line(node)
24
+ line = final_end_location(node).line
25
+ line += 1 while comment_line?(processed_source[line])
26
+ return if processed_source[line].blank?
27
+ return if processed_source[line].match?(/\s*it /)
28
+
29
+ yield offending_loc(line)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end