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.
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