rubocop-rails 2.0.1 → 2.19.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (144) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +1 -1
  3. data/README.md +52 -5
  4. data/config/default.yml +726 -32
  5. data/config/obsoletion.yml +17 -0
  6. data/lib/rubocop/cop/mixin/active_record_helper.rb +106 -0
  7. data/lib/rubocop/cop/mixin/active_record_migrations_helper.rb +32 -0
  8. data/lib/rubocop/cop/mixin/class_send_node_helper.rb +20 -0
  9. data/lib/rubocop/cop/mixin/enforce_superclass.rb +40 -0
  10. data/lib/rubocop/cop/mixin/index_method.rb +165 -0
  11. data/lib/rubocop/cop/mixin/migrations_helper.rb +26 -0
  12. data/lib/rubocop/cop/rails/action_controller_flash_before_render.rb +112 -0
  13. data/lib/rubocop/cop/rails/action_controller_test_case.rb +47 -0
  14. data/lib/rubocop/cop/rails/action_filter.rb +11 -21
  15. data/lib/rubocop/cop/rails/action_order.rb +116 -0
  16. data/lib/rubocop/cop/rails/active_record_aliases.rb +23 -24
  17. data/lib/rubocop/cop/rails/active_record_callbacks_order.rb +143 -0
  18. data/lib/rubocop/cop/rails/active_record_override.rb +3 -6
  19. data/lib/rubocop/cop/rails/active_support_aliases.rb +13 -22
  20. data/lib/rubocop/cop/rails/active_support_on_load.rb +70 -0
  21. data/lib/rubocop/cop/rails/add_column_index.rb +61 -0
  22. data/lib/rubocop/cop/rails/after_commit_override.rb +81 -0
  23. data/lib/rubocop/cop/rails/application_controller.rb +36 -0
  24. data/lib/rubocop/cop/rails/application_job.rb +9 -4
  25. data/lib/rubocop/cop/rails/application_mailer.rb +39 -0
  26. data/lib/rubocop/cop/rails/application_record.rb +9 -9
  27. data/lib/rubocop/cop/rails/arel_star.rb +47 -0
  28. data/lib/rubocop/cop/rails/assert_not.rb +8 -10
  29. data/lib/rubocop/cop/rails/attribute_default_block_value.rb +90 -0
  30. data/lib/rubocop/cop/rails/belongs_to.rb +12 -24
  31. data/lib/rubocop/cop/rails/blank.rb +40 -36
  32. data/lib/rubocop/cop/rails/bulk_change_table.rb +40 -35
  33. data/lib/rubocop/cop/rails/compact_blank.rb +111 -0
  34. data/lib/rubocop/cop/rails/content_tag.rb +93 -0
  35. data/lib/rubocop/cop/rails/create_table_with_timestamps.rb +22 -15
  36. data/lib/rubocop/cop/rails/date.rb +41 -36
  37. data/lib/rubocop/cop/rails/default_scope.rb +61 -0
  38. data/lib/rubocop/cop/rails/delegate.rb +33 -29
  39. data/lib/rubocop/cop/rails/delegate_allow_blank.rb +9 -10
  40. data/lib/rubocop/cop/rails/deprecated_active_model_errors_methods.rb +168 -0
  41. data/lib/rubocop/cop/rails/dot_separated_keys.rb +71 -0
  42. data/lib/rubocop/cop/rails/duplicate_association.rb +56 -0
  43. data/lib/rubocop/cop/rails/duplicate_scope.rb +46 -0
  44. data/lib/rubocop/cop/rails/duration_arithmetic.rb +98 -0
  45. data/lib/rubocop/cop/rails/dynamic_find_by.rb +76 -31
  46. data/lib/rubocop/cop/rails/eager_evaluation_log_message.rb +82 -0
  47. data/lib/rubocop/cop/rails/enum_hash.rb +75 -0
  48. data/lib/rubocop/cop/rails/enum_uniqueness.rb +30 -12
  49. data/lib/rubocop/cop/rails/environment_comparison.rb +70 -22
  50. data/lib/rubocop/cop/rails/environment_variable_access.rb +67 -0
  51. data/lib/rubocop/cop/rails/exit.rb +7 -13
  52. data/lib/rubocop/cop/rails/expanded_date_range.rb +102 -0
  53. data/lib/rubocop/cop/rails/file_path.rb +48 -31
  54. data/lib/rubocop/cop/rails/find_by.rb +43 -24
  55. data/lib/rubocop/cop/rails/find_by_id.rb +94 -0
  56. data/lib/rubocop/cop/rails/find_each.rb +42 -18
  57. data/lib/rubocop/cop/rails/freeze_time.rb +79 -0
  58. data/lib/rubocop/cop/rails/has_and_belongs_to_many.rb +4 -3
  59. data/lib/rubocop/cop/rails/has_many_or_has_one_dependent.rb +62 -25
  60. data/lib/rubocop/cop/rails/helper_instance_variable.rb +32 -4
  61. data/lib/rubocop/cop/rails/http_positional_arguments.rb +61 -32
  62. data/lib/rubocop/cop/rails/http_status.rb +27 -23
  63. data/lib/rubocop/cop/rails/i18n_lazy_lookup.rb +96 -0
  64. data/lib/rubocop/cop/rails/i18n_locale_assignment.rb +37 -0
  65. data/lib/rubocop/cop/rails/i18n_locale_texts.rb +110 -0
  66. data/lib/rubocop/cop/rails/ignored_columns_assignment.rb +50 -0
  67. data/lib/rubocop/cop/rails/ignored_skip_action_filter_option.rb +9 -16
  68. data/lib/rubocop/cop/rails/index_by.rb +65 -0
  69. data/lib/rubocop/cop/rails/index_with.rb +68 -0
  70. data/lib/rubocop/cop/rails/inquiry.rb +39 -0
  71. data/lib/rubocop/cop/rails/inverse_of.rb +33 -27
  72. data/lib/rubocop/cop/rails/lexically_scoped_action_filter.rb +62 -32
  73. data/lib/rubocop/cop/rails/link_to_blank.rb +31 -32
  74. data/lib/rubocop/cop/rails/mailer_name.rb +90 -0
  75. data/lib/rubocop/cop/rails/match_route.rb +120 -0
  76. data/lib/rubocop/cop/rails/migration_class_name.rb +63 -0
  77. data/lib/rubocop/cop/rails/negate_include.rb +42 -0
  78. data/lib/rubocop/cop/rails/not_null_column.rb +16 -12
  79. data/lib/rubocop/cop/rails/order_by_id.rb +51 -0
  80. data/lib/rubocop/cop/rails/output.rb +29 -10
  81. data/lib/rubocop/cop/rails/output_safety.rb +9 -4
  82. data/lib/rubocop/cop/rails/pick.rb +64 -0
  83. data/lib/rubocop/cop/rails/pluck.rb +96 -0
  84. data/lib/rubocop/cop/rails/pluck_id.rb +59 -0
  85. data/lib/rubocop/cop/rails/pluck_in_where.rb +71 -0
  86. data/lib/rubocop/cop/rails/pluralization_grammar.rb +14 -19
  87. data/lib/rubocop/cop/rails/presence.rb +54 -26
  88. data/lib/rubocop/cop/rails/present.rb +40 -37
  89. data/lib/rubocop/cop/rails/rake_environment.rb +112 -0
  90. data/lib/rubocop/cop/rails/read_write_attribute.rb +56 -18
  91. data/lib/rubocop/cop/rails/redundant_allow_nil.rb +33 -45
  92. data/lib/rubocop/cop/rails/redundant_foreign_key.rb +77 -0
  93. data/lib/rubocop/cop/rails/redundant_presence_validation_on_belongs_to.rb +257 -0
  94. data/lib/rubocop/cop/rails/redundant_receiver_in_with_options.rb +34 -32
  95. data/lib/rubocop/cop/rails/redundant_travel_back.rb +57 -0
  96. data/lib/rubocop/cop/rails/reflection_class_name.rb +56 -7
  97. data/lib/rubocop/cop/rails/refute_methods.rb +56 -35
  98. data/lib/rubocop/cop/rails/relative_date_constant.rb +52 -33
  99. data/lib/rubocop/cop/rails/render_inline.rb +41 -0
  100. data/lib/rubocop/cop/rails/render_plain_text.rb +71 -0
  101. data/lib/rubocop/cop/rails/request_referer.rb +10 -11
  102. data/lib/rubocop/cop/rails/require_dependency.rb +38 -0
  103. data/lib/rubocop/cop/rails/response_parsed_body.rb +57 -0
  104. data/lib/rubocop/cop/rails/reversible_migration.rb +122 -82
  105. data/lib/rubocop/cop/rails/reversible_migration_method_definition.rb +66 -0
  106. data/lib/rubocop/cop/rails/root_join_chain.rb +72 -0
  107. data/lib/rubocop/cop/rails/root_pathname_methods.rb +238 -0
  108. data/lib/rubocop/cop/rails/root_public_path.rb +59 -0
  109. data/lib/rubocop/cop/rails/safe_navigation.rb +55 -43
  110. data/lib/rubocop/cop/rails/safe_navigation_with_blank.rb +50 -0
  111. data/lib/rubocop/cop/rails/save_bang.rb +89 -63
  112. data/lib/rubocop/cop/rails/schema_comment.rb +104 -0
  113. data/lib/rubocop/cop/rails/scope_args.rb +8 -3
  114. data/lib/rubocop/cop/rails/short_i18n.rb +71 -0
  115. data/lib/rubocop/cop/rails/skips_model_validations.rb +53 -16
  116. data/lib/rubocop/cop/rails/squished_sql_heredocs.rb +87 -0
  117. data/lib/rubocop/cop/rails/strip_heredoc.rb +56 -0
  118. data/lib/rubocop/cop/rails/table_name_assignment.rb +44 -0
  119. data/lib/rubocop/cop/rails/three_state_boolean_column.rb +73 -0
  120. data/lib/rubocop/cop/rails/time_zone.rb +83 -67
  121. data/lib/rubocop/cop/rails/time_zone_assignment.rb +37 -0
  122. data/lib/rubocop/cop/rails/to_formatted_s.rb +46 -0
  123. data/lib/rubocop/cop/rails/to_s_with_argument.rb +78 -0
  124. data/lib/rubocop/cop/rails/top_level_hash_with_indifferent_access.rb +49 -0
  125. data/lib/rubocop/cop/rails/transaction_exit_statement.rb +99 -0
  126. data/lib/rubocop/cop/rails/uniq_before_pluck.rb +40 -49
  127. data/lib/rubocop/cop/rails/unique_validation_without_index.rb +172 -0
  128. data/lib/rubocop/cop/rails/unknown_env.rb +52 -21
  129. data/lib/rubocop/cop/rails/unused_ignored_columns.rb +76 -0
  130. data/lib/rubocop/cop/rails/validation.rb +54 -23
  131. data/lib/rubocop/cop/rails/where_equals.rb +102 -0
  132. data/lib/rubocop/cop/rails/where_exists.rb +138 -0
  133. data/lib/rubocop/cop/rails/where_missing.rb +118 -0
  134. data/lib/rubocop/cop/rails/where_not.rb +101 -0
  135. data/lib/rubocop/cop/rails/where_not_with_multiple_conditions.rb +55 -0
  136. data/lib/rubocop/cop/rails_cops.rb +78 -8
  137. data/lib/rubocop/rails/inject.rb +1 -1
  138. data/lib/rubocop/rails/schema_loader/schema.rb +191 -0
  139. data/lib/rubocop/rails/schema_loader.rb +61 -0
  140. data/lib/rubocop/rails/version.rb +5 -1
  141. data/lib/rubocop/rails.rb +3 -1
  142. data/lib/rubocop-rails.rb +22 -0
  143. metadata +120 -19
  144. data/bin/setup +0 -7
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # Checks direct manipulation of ActiveModel#errors as hash.
7
+ # These operations are deprecated in Rails 6.1 and will not work in Rails 7.
8
+ #
9
+ # @safety
10
+ # This cop is unsafe because it can report `errors` manipulation on non-ActiveModel,
11
+ # which is obviously valid.
12
+ # The cop has no way of knowing whether a variable is an ActiveModel or not.
13
+ #
14
+ # @example
15
+ # # bad
16
+ # user.errors[:name] << 'msg'
17
+ # user.errors.messages[:name] << 'msg'
18
+ #
19
+ # # good
20
+ # user.errors.add(:name, 'msg')
21
+ #
22
+ # # bad
23
+ # user.errors[:name].clear
24
+ # user.errors.messages[:name].clear
25
+ #
26
+ # # good
27
+ # user.errors.delete(:name)
28
+ #
29
+ # # bad
30
+ # user.errors.keys.include?(:attr)
31
+ #
32
+ # # good
33
+ # user.errors.attribute_names.include?(:attr)
34
+ #
35
+ class DeprecatedActiveModelErrorsMethods < Base
36
+ include RangeHelp
37
+ extend AutoCorrector
38
+
39
+ MSG = 'Avoid manipulating ActiveModel errors as hash directly.'
40
+ AUTOCORRECTABLE_METHODS = %i[<< clear keys].freeze
41
+ INCOMPATIBLE_METHODS = %i[keys values to_h to_xml].freeze
42
+
43
+ MANIPULATIVE_METHODS = Set[
44
+ *%i[
45
+ << append clear collect! compact! concat
46
+ delete delete_at delete_if drop drop_while fill filter! keep_if
47
+ flatten! insert map! pop prepend push reject! replace reverse!
48
+ rotate! select! shift shuffle! slice! sort! sort_by! uniq! unshift
49
+ ]
50
+ ].freeze
51
+
52
+ def_node_matcher :receiver_matcher_outside_model, '{send ivar lvar}'
53
+ def_node_matcher :receiver_matcher_inside_model, '{nil? send ivar lvar}'
54
+
55
+ def_node_matcher :any_manipulation?, <<~PATTERN
56
+ {
57
+ #root_manipulation?
58
+ #root_assignment?
59
+ #errors_deprecated?
60
+ #messages_details_manipulation?
61
+ #messages_details_assignment?
62
+ }
63
+ PATTERN
64
+
65
+ def_node_matcher :root_manipulation?, <<~PATTERN
66
+ (send
67
+ (send
68
+ (send #receiver_matcher :errors) :[] ...)
69
+ MANIPULATIVE_METHODS
70
+ ...
71
+ )
72
+ PATTERN
73
+
74
+ def_node_matcher :root_assignment?, <<~PATTERN
75
+ (send
76
+ (send #receiver_matcher :errors)
77
+ :[]=
78
+ ...)
79
+ PATTERN
80
+
81
+ def_node_matcher :errors_deprecated?, <<~PATTERN
82
+ (send
83
+ (send #receiver_matcher :errors)
84
+ {:keys :values :to_h :to_xml})
85
+ PATTERN
86
+
87
+ def_node_matcher :messages_details_manipulation?, <<~PATTERN
88
+ (send
89
+ (send
90
+ (send
91
+ (send #receiver_matcher :errors)
92
+ {:messages :details})
93
+ :[]
94
+ ...)
95
+ MANIPULATIVE_METHODS
96
+ ...)
97
+ PATTERN
98
+
99
+ def_node_matcher :messages_details_assignment?, <<~PATTERN
100
+ (send
101
+ (send
102
+ (send #receiver_matcher :errors)
103
+ {:messages :details})
104
+ :[]=
105
+ ...)
106
+ PATTERN
107
+
108
+ def on_send(node)
109
+ any_manipulation?(node) do
110
+ next if target_rails_version <= 6.0 && INCOMPATIBLE_METHODS.include?(node.method_name)
111
+
112
+ add_offense(node) do |corrector|
113
+ next if skip_autocorrect?(node)
114
+
115
+ autocorrect(corrector, node)
116
+ end
117
+ end
118
+ end
119
+
120
+ private
121
+
122
+ def skip_autocorrect?(node)
123
+ return true unless AUTOCORRECTABLE_METHODS.include?(node.method_name)
124
+ return false unless (receiver = node.receiver.receiver)
125
+
126
+ receiver.send_type? && receiver.method?(:details) && node.method?(:<<)
127
+ end
128
+
129
+ def autocorrect(corrector, node)
130
+ receiver = node.receiver
131
+
132
+ range = offense_range(node, receiver)
133
+ replacement = replacement(node, receiver)
134
+
135
+ corrector.replace(range, replacement)
136
+ end
137
+
138
+ def offense_range(node, receiver)
139
+ receiver = receiver.receiver while receiver.send_type? && !receiver.method?(:errors) && receiver.receiver
140
+ range_between(receiver.source_range.end_pos, node.source_range.end_pos)
141
+ end
142
+
143
+ def replacement(node, receiver)
144
+ return '.attribute_names' if node.method?(:keys)
145
+
146
+ key = receiver.first_argument.source
147
+
148
+ case node.method_name
149
+ when :<<
150
+ value = node.first_argument.source
151
+
152
+ ".add(#{key}, #{value})"
153
+ when :clear
154
+ ".delete(#{key})"
155
+ end
156
+ end
157
+
158
+ def receiver_matcher(node)
159
+ model_file? ? receiver_matcher_inside_model(node) : receiver_matcher_outside_model(node)
160
+ end
161
+
162
+ def model_file?
163
+ processed_source.file_path.include?('/models/')
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # Enforces the use of dot-separated locale keys instead of specifying the `:scope` option
7
+ # with an array or a single symbol in `I18n` translation methods.
8
+ # Dot-separated notation is easier to read and trace the hierarchy.
9
+ #
10
+ # @example
11
+ # # bad
12
+ # I18n.t :record_invalid, scope: [:activerecord, :errors, :messages]
13
+ # I18n.t :title, scope: :invitation
14
+ #
15
+ # # good
16
+ # I18n.t 'activerecord.errors.messages.record_invalid'
17
+ # I18n.t :record_invalid, scope: 'activerecord.errors.messages'
18
+ #
19
+ class DotSeparatedKeys < Base
20
+ include RangeHelp
21
+ extend AutoCorrector
22
+
23
+ MSG = 'Use the dot-separated keys instead of specifying the `:scope` option.'
24
+ TRANSLATE_METHODS = %i[translate t].freeze
25
+
26
+ def_node_matcher :translate_with_scope?, <<~PATTERN
27
+ (send {nil? (const {nil? cbase} :I18n)} {:translate :t} ${sym_type? str_type?}
28
+ (hash <$(pair (sym :scope) ${array_type? sym_type?}) ...>)
29
+ )
30
+ PATTERN
31
+
32
+ def on_send(node)
33
+ return unless TRANSLATE_METHODS.include?(node.method_name)
34
+
35
+ translate_with_scope?(node) do |key_node, scope_node|
36
+ return unless should_convert_scope?(scope_node)
37
+
38
+ add_offense(scope_node) do |corrector|
39
+ # Eat the comma on the left.
40
+ range = range_with_surrounding_space(scope_node.source_range, side: :left)
41
+ range = range_with_surrounding_comma(range, :left)
42
+ corrector.remove(range)
43
+
44
+ corrector.replace(key_node, new_key(key_node, scope_node))
45
+ end
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ def should_convert_scope?(scope_node)
52
+ scopes(scope_node).all?(&:basic_literal?)
53
+ end
54
+
55
+ def new_key(key_node, scope_node)
56
+ "'#{scopes(scope_node).map(&:value).join('.')}.#{key_node.value}'".squeeze('.')
57
+ end
58
+
59
+ def scopes(scope_node)
60
+ value = scope_node.value
61
+
62
+ if value.array_type?
63
+ value.values
64
+ else
65
+ [value]
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # Looks for associations that have been defined multiple times in the same file.
7
+ #
8
+ # When an association is defined multiple times on a model, Active Record overrides the
9
+ # previously defined association with the new one. Because of this, this cop's autocorrection
10
+ # simply keeps the last of any duplicates and discards the rest.
11
+ #
12
+ # @example
13
+ #
14
+ # # bad
15
+ # belongs_to :foo
16
+ # belongs_to :bar
17
+ # has_one :foo
18
+ #
19
+ # # good
20
+ # belongs_to :bar
21
+ # has_one :foo
22
+ #
23
+ class DuplicateAssociation < Base
24
+ include RangeHelp
25
+ extend AutoCorrector
26
+ include ClassSendNodeHelper
27
+
28
+ MSG = "Association `%<name>s` is defined multiple times. Don't repeat associations."
29
+
30
+ def_node_matcher :association, <<~PATTERN
31
+ (send nil? {:belongs_to :has_one :has_many :has_and_belongs_to_many} ({sym str} $_) ...)
32
+ PATTERN
33
+
34
+ def on_class(class_node)
35
+ offenses(class_node).each do |name, nodes|
36
+ nodes.each do |node|
37
+ add_offense(node, message: format(MSG, name: name)) do |corrector|
38
+ next if same_line?(nodes.last, node)
39
+
40
+ corrector.remove(range_by_whole_lines(node.source_range, include_final_newline: true))
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ def offenses(class_node)
49
+ class_send_nodes(class_node).select { |node| association(node) }
50
+ .group_by { |node| association(node).to_sym }
51
+ .select { |_, nodes| nodes.length > 1 }
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # Checks for multiple scopes in a model that have the same `where` clause. This
7
+ # often means you copy/pasted a scope, updated the name, and forgot to change the condition.
8
+ #
9
+ # @example
10
+ #
11
+ # # bad
12
+ # scope :visible, -> { where(visible: true) }
13
+ # scope :hidden, -> { where(visible: true) }
14
+ #
15
+ # # good
16
+ # scope :visible, -> { where(visible: true) }
17
+ # scope :hidden, -> { where(visible: false) }
18
+ #
19
+ class DuplicateScope < Base
20
+ include ClassSendNodeHelper
21
+
22
+ MSG = 'Multiple scopes share this same where clause.'
23
+
24
+ def_node_matcher :scope, <<~PATTERN
25
+ (send nil? :scope _ $...)
26
+ PATTERN
27
+
28
+ def on_class(class_node)
29
+ offenses(class_node).each do |node|
30
+ add_offense(node)
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def offenses(class_node)
37
+ class_send_nodes(class_node).select { |node| scope(node) }
38
+ .group_by { |node| scope(node) }
39
+ .select { |_, nodes| nodes.length > 1 }
40
+ .values
41
+ .flatten
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # Checks if a duration is added to or subtracted from `Time.current`.
7
+ #
8
+ # @example
9
+ # # bad
10
+ # Time.current - 1.minute
11
+ # Time.current + 2.days
12
+ #
13
+ # # good - using relative would make it harder to express and read
14
+ # Date.yesterday + 3.days
15
+ # created_at - 1.minute
16
+ # 3.days - 1.hour
17
+ #
18
+ # # good
19
+ # 1.minute.ago
20
+ # 2.days.from_now
21
+ class DurationArithmetic < Base
22
+ extend AutoCorrector
23
+
24
+ MSG = 'Do not add or subtract duration.'
25
+
26
+ RESTRICT_ON_SEND = %i[+ -].freeze
27
+
28
+ DURATIONS = Set[:second, :seconds, :minute, :minutes, :hour, :hours,
29
+ :day, :days, :week, :weeks, :fortnight, :fortnights,
30
+ :month, :months, :year, :years]
31
+
32
+ # @!method duration_arithmetic_argument?(node)
33
+ # Match duration subtraction or addition with current time.
34
+ #
35
+ # @example source that matches
36
+ # Time.current - 1.hour
37
+ #
38
+ # @example source that matches
39
+ # ::Time.zone.now + 1.hour
40
+ #
41
+ # @param node [RuboCop::AST::Node]
42
+ # @yield operator and duration
43
+ def_node_matcher :duration_arithmetic_argument?, <<~PATTERN
44
+ (send #time_current? ${ :+ :- } $#duration?)
45
+ PATTERN
46
+
47
+ # @!method duration?(node)
48
+ # Match a literal Duration
49
+ #
50
+ # @example source that matches
51
+ # 1.hour
52
+ #
53
+ # @example source that matches
54
+ # 9.5.weeks
55
+ #
56
+ # @param node [RuboCop::AST::Node]
57
+ # @return [Boolean] true if matches
58
+ def_node_matcher :duration?, '(send { int float (send nil _) } DURATIONS)'
59
+
60
+ # @!method time_current?(node)
61
+ # Match Time.current
62
+ #
63
+ # @example source that matches
64
+ # Time.current
65
+ #
66
+ # @example source that matches
67
+ # ::Time.zone.now
68
+ #
69
+ # @param node [RuboCop::AST::Node]
70
+ # @return [Boolean] true if matches
71
+ def_node_matcher :time_current?, <<~PATTERN
72
+ {
73
+ (send (const {nil? cbase} :Time) :current)
74
+ (send (send (const {nil? cbase} :Time) :zone) :now)
75
+ }
76
+ PATTERN
77
+
78
+ def on_send(node)
79
+ duration_arithmetic_argument?(node) do |*operation|
80
+ add_offense(node) do |corrector|
81
+ corrector.replace(node, corrected_source(*operation))
82
+ end
83
+ end
84
+ end
85
+
86
+ private
87
+
88
+ def corrected_source(operator, duration)
89
+ if operator == :-
90
+ "#{duration.source}.ago"
91
+ else
92
+ "#{duration.source}.from_now"
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -3,75 +3,106 @@
3
3
  module RuboCop
4
4
  module Cop
5
5
  module Rails
6
- # This cop checks dynamic `find_by_*` methods.
6
+ # Checks dynamic `find_by_*` methods.
7
7
  # Use `find_by` instead of dynamic method.
8
- # See. https://github.com/rubocop-hq/rails-style-guide#find_by
8
+ # See. https://rails.rubystyle.guide#find_by
9
+ #
10
+ # @safety
11
+ # It is certainly unsafe when not configured properly, i.e. user-defined `find_by_xxx`
12
+ # method is not added to cop's `AllowedMethods`.
9
13
  #
10
14
  # @example
11
15
  # # bad
12
16
  # User.find_by_name(name)
13
- #
14
- # # bad
15
17
  # User.find_by_name_and_email(name)
16
- #
17
- # # bad
18
18
  # User.find_by_email!(name)
19
19
  #
20
20
  # # good
21
21
  # User.find_by(name: name)
22
+ # User.find_by(name: name, email: email)
23
+ # User.find_by!(email: email)
24
+ #
25
+ # @example AllowedMethods: ['find_by_sql', 'find_by_token_for'] (default)
26
+ # # bad
27
+ # User.find_by_query(users_query)
28
+ # User.find_by_token_for(:password_reset, token)
22
29
  #
23
30
  # # good
24
- # User.find_by(name: name, email: email)
31
+ # User.find_by_sql(users_sql)
32
+ # User.find_by_token_for(:password_reset, token)
33
+ #
34
+ # @example AllowedReceivers: ['Gem::Specification', 'page'] (default)
35
+ # # bad
36
+ # Specification.find_by_name('backend').gem_dir
37
+ # page.find_by_id('a_dom_id').click
25
38
  #
26
39
  # # good
27
- # User.find_by!(email: email)
28
- class DynamicFindBy < Cop
40
+ # Gem::Specification.find_by_name('backend').gem_dir
41
+ # page.find_by_id('a_dom_id').click
42
+ class DynamicFindBy < Base
43
+ include ActiveRecordHelper
44
+ extend AutoCorrector
45
+
29
46
  MSG = 'Use `%<static_name>s` instead of dynamic `%<method>s`.'
30
47
  METHOD_PATTERN = /^find_by_(.+?)(!)?$/.freeze
48
+ IGNORED_ARGUMENT_TYPES = %i[hash splat].freeze
31
49
 
32
50
  def on_send(node)
33
- method_name = node.method_name.to_s
34
-
35
- return if whitelist.include?(method_name)
51
+ return if (node.receiver.nil? && !inherit_active_record_base?(node)) || allowed_invocation?(node)
36
52
 
53
+ method_name = node.method_name
37
54
  static_name = static_method_name(method_name)
38
-
39
55
  return unless static_name
56
+ return unless dynamic_find_by_arguments?(node)
40
57
 
41
- add_offense(node,
42
- message: format(MSG, static_name: static_name,
43
- method: node.method_name))
58
+ message = format(MSG, static_name: static_name, method: method_name)
59
+ add_offense(node, message: message) do |corrector|
60
+ autocorrect(corrector, node)
61
+ end
44
62
  end
45
63
  alias on_csend on_send
46
64
 
47
- def autocorrect(node)
48
- keywords = column_keywords(node.method_name)
65
+ private
66
+
67
+ def autocorrect(corrector, node)
68
+ autocorrect_method_name(corrector, node)
69
+ autocorrect_argument_keywords(corrector, node, column_keywords(node.method_name))
70
+ end
49
71
 
50
- return if keywords.size != node.arguments.size
72
+ def allowed_invocation?(node)
73
+ allowed_method?(node) || allowed_receiver?(node) || whitelisted?(node)
74
+ end
51
75
 
52
- lambda do |corrector|
53
- autocorrect_method_name(corrector, node)
54
- autocorrect_argument_keywords(corrector, node, keywords)
55
- end
76
+ def allowed_method?(node)
77
+ return unless cop_config['AllowedMethods']
78
+
79
+ cop_config['AllowedMethods'].include?(node.method_name.to_s)
56
80
  end
57
81
 
58
- private
82
+ def allowed_receiver?(node)
83
+ return unless cop_config['AllowedReceivers'] && node.receiver
84
+
85
+ cop_config['AllowedReceivers'].include?(node.receiver.source)
86
+ end
87
+
88
+ # config option `WhiteList` will be deprecated soon
89
+ def whitelisted?(node)
90
+ whitelist_config = cop_config['Whitelist']
91
+ return unless whitelist_config
92
+
93
+ whitelist_config.include?(node.method_name.to_s)
94
+ end
59
95
 
60
96
  def autocorrect_method_name(corrector, node)
61
- corrector.replace(node.loc.selector,
62
- static_method_name(node.method_name.to_s))
97
+ corrector.replace(node.loc.selector, static_method_name(node.method_name.to_s))
63
98
  end
64
99
 
65
100
  def autocorrect_argument_keywords(corrector, node, keywords)
66
101
  keywords.each.with_index do |keyword, idx|
67
- corrector.insert_before(node.arguments[idx].loc.expression, keyword)
102
+ corrector.insert_before(node.arguments[idx], keyword)
68
103
  end
69
104
  end
70
105
 
71
- def whitelist
72
- cop_config['Whitelist']
73
- end
74
-
75
106
  def column_keywords(method)
76
107
  keyword_string = method.to_s[METHOD_PATTERN, 1]
77
108
  keyword_string.split('_and_').map { |keyword| "#{keyword}: " }
@@ -85,6 +116,20 @@ module RuboCop
85
116
 
86
117
  match[2] ? 'find_by!' : 'find_by'
87
118
  end
119
+
120
+ def dynamic_find_by_arguments?(node)
121
+ dynamic_find_by_arguments_count?(node) && dynamic_find_by_arguments_type?(node)
122
+ end
123
+
124
+ def dynamic_find_by_arguments_count?(node)
125
+ column_keywords(node.method_name).size == node.arguments.size
126
+ end
127
+
128
+ def dynamic_find_by_arguments_type?(node)
129
+ node.arguments.none? do |argument|
130
+ IGNORED_ARGUMENT_TYPES.include?(argument.type)
131
+ end
132
+ end
88
133
  end
89
134
  end
90
135
  end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # Checks that blocks are used for interpolated strings passed to
7
+ # `Rails.logger.debug`.
8
+ #
9
+ # By default, Rails production environments use the `:info` log level.
10
+ # At the `:info` log level, `Rails.logger.debug` statements do not result
11
+ # in log output. However, Ruby must eagerly evaluate interpolated string
12
+ # arguments passed as method arguments. Passing a block to
13
+ # `Rails.logger.debug` prevents costly evaluation of interpolated strings
14
+ # when no output would be produced anyway.
15
+ #
16
+ # @example
17
+ # #bad
18
+ # Rails.logger.debug "The time is #{Time.zone.now}."
19
+ #
20
+ # #good
21
+ # Rails.logger.debug { "The time is #{Time.zone.now}." }
22
+ #
23
+ class EagerEvaluationLogMessage < Base
24
+ extend AutoCorrector
25
+
26
+ MSG = 'Pass a block to `Rails.logger.debug`.'
27
+ RESTRICT_ON_SEND = %i[debug].freeze
28
+
29
+ def_node_matcher :interpolated_string_passed_to_debug, <<~PATTERN
30
+ (send
31
+ (send
32
+ (const {cbase nil?} :Rails)
33
+ :logger
34
+ )
35
+ :debug
36
+ $(dstr ...)
37
+ )
38
+ PATTERN
39
+
40
+ def self.autocorrect_incompatible_with
41
+ [Style::MethodCallWithArgsParentheses]
42
+ end
43
+
44
+ def on_send(node)
45
+ return if node.parent&.block_type?
46
+
47
+ interpolated_string_passed_to_debug(node) do |arguments|
48
+ message = format(MSG)
49
+
50
+ range = replacement_range(node)
51
+ replacement = replacement_source(node, arguments)
52
+
53
+ add_offense(range, message: message) do |corrector|
54
+ corrector.replace(range, replacement)
55
+ end
56
+ end
57
+ end
58
+
59
+ private
60
+
61
+ def replacement_range(node)
62
+ stop = node.source_range.end
63
+ start = node.loc.selector.end
64
+
65
+ if node.parenthesized_call?
66
+ stop.with(begin_pos: start.begin_pos)
67
+ else
68
+ stop.with(begin_pos: start.begin_pos + 1)
69
+ end
70
+ end
71
+
72
+ def replacement_source(node, arguments)
73
+ if node.parenthesized_call?
74
+ " { #{arguments.source} }"
75
+ else
76
+ "{ #{arguments.source} }"
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end