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,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # Identifies places where manually constructed SQL
7
+ # in `where` can be replaced with `where(attribute: value)`.
8
+ #
9
+ # @safety
10
+ # This cop's autocorrection is unsafe because is may change SQL.
11
+ # See: https://github.com/rubocop/rubocop-rails/issues/403
12
+ #
13
+ # @example
14
+ # # bad
15
+ # User.where('name = ?', 'Gabe')
16
+ # User.where('name = :name', name: 'Gabe')
17
+ # User.where('name IS NULL')
18
+ # User.where('name IN (?)', ['john', 'jane'])
19
+ # User.where('name IN (:names)', names: ['john', 'jane'])
20
+ # User.where('users.name = :name', name: 'Gabe')
21
+ #
22
+ # # good
23
+ # User.where(name: 'Gabe')
24
+ # User.where(name: nil)
25
+ # User.where(name: ['john', 'jane'])
26
+ # User.where(users: { name: 'Gabe' })
27
+ class WhereEquals < Base
28
+ include RangeHelp
29
+ extend AutoCorrector
30
+
31
+ MSG = 'Use `%<good_method>s` instead of manually constructing SQL.'
32
+ RESTRICT_ON_SEND = %i[where].freeze
33
+
34
+ def_node_matcher :where_method_call?, <<~PATTERN
35
+ {
36
+ (send _ :where (array $str_type? $_ ?))
37
+ (send _ :where $str_type? $_ ?)
38
+ }
39
+ PATTERN
40
+
41
+ def on_send(node)
42
+ where_method_call?(node) do |template_node, value_node|
43
+ value_node = value_node.first
44
+
45
+ range = offense_range(node)
46
+
47
+ column_and_value = extract_column_and_value(template_node, value_node)
48
+ return unless column_and_value
49
+
50
+ good_method = build_good_method(*column_and_value)
51
+ message = format(MSG, good_method: good_method)
52
+
53
+ add_offense(range, message: message) do |corrector|
54
+ corrector.replace(range, good_method)
55
+ end
56
+ end
57
+ end
58
+
59
+ EQ_ANONYMOUS_RE = /\A([\w.]+)\s+=\s+\?\z/.freeze # column = ?
60
+ IN_ANONYMOUS_RE = /\A([\w.]+)\s+IN\s+\(\?\)\z/i.freeze # column IN (?)
61
+ EQ_NAMED_RE = /\A([\w.]+)\s+=\s+:(\w+)\z/.freeze # column = :column
62
+ IN_NAMED_RE = /\A([\w.]+)\s+IN\s+\(:(\w+)\)\z/i.freeze # column IN (:column)
63
+ IS_NULL_RE = /\A([\w.]+)\s+IS\s+NULL\z/i.freeze # column IS NULL
64
+
65
+ private
66
+
67
+ def offense_range(node)
68
+ range_between(node.loc.selector.begin_pos, node.source_range.end_pos)
69
+ end
70
+
71
+ def extract_column_and_value(template_node, value_node)
72
+ value =
73
+ case template_node.value
74
+ when EQ_ANONYMOUS_RE, IN_ANONYMOUS_RE
75
+ value_node.source
76
+ when EQ_NAMED_RE, IN_NAMED_RE
77
+ return unless value_node&.hash_type?
78
+
79
+ pair = value_node.pairs.find { |p| p.key.value.to_sym == Regexp.last_match(2).to_sym }
80
+ pair.value.source
81
+ when IS_NULL_RE
82
+ 'nil'
83
+ else
84
+ return
85
+ end
86
+
87
+ [Regexp.last_match(1), value]
88
+ end
89
+
90
+ def build_good_method(column, value)
91
+ if column.include?('.')
92
+ table, column = column.split('.')
93
+
94
+ "where(#{table}: { #{column}: #{value} })"
95
+ else
96
+ "where(#{column}: #{value})"
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # Enforces consistent style when using `exists?`.
7
+ #
8
+ # Two styles are supported for this cop. When EnforcedStyle is 'exists'
9
+ # then the cop enforces `exists?(...)` over `where(...).exists?`.
10
+ #
11
+ # When EnforcedStyle is 'where' then the cop enforces
12
+ # `where(...).exists?` over `exists?(...)`.
13
+ #
14
+ # @safety
15
+ # This cop is unsafe for autocorrection because the behavior may change on the following case:
16
+ #
17
+ # [source,ruby]
18
+ # ----
19
+ # Author.includes(:articles).where(articles: {id: id}).exists?
20
+ # #=> Perform `eager_load` behavior (`LEFT JOIN` query) and get result.
21
+ #
22
+ # Author.includes(:articles).exists?(articles: {id: id})
23
+ # #=> Perform `preload` behavior and `ActiveRecord::StatementInvalid` error occurs.
24
+ # ----
25
+ #
26
+ # @example EnforcedStyle: exists (default)
27
+ # # bad
28
+ # User.where(name: 'john').exists?
29
+ # User.where(['name = ?', 'john']).exists?
30
+ # User.where('name = ?', 'john').exists?
31
+ # user.posts.where(published: true).exists?
32
+ #
33
+ # # good
34
+ # User.exists?(name: 'john')
35
+ # User.where('length(name) > 10').exists?
36
+ # user.posts.exists?(published: true)
37
+ #
38
+ # @example EnforcedStyle: where
39
+ # # bad
40
+ # User.exists?(name: 'john')
41
+ # User.exists?(['name = ?', 'john'])
42
+ # User.exists?('name = ?', 'john')
43
+ # user.posts.exists?(published: true)
44
+ #
45
+ # # good
46
+ # User.where(name: 'john').exists?
47
+ # User.where(['name = ?', 'john']).exists?
48
+ # User.where('name = ?', 'john').exists?
49
+ # user.posts.where(published: true).exists?
50
+ # User.where('length(name) > 10').exists?
51
+ class WhereExists < Base
52
+ include ConfigurableEnforcedStyle
53
+ extend AutoCorrector
54
+
55
+ MSG = 'Prefer `%<good_method>s` over `%<bad_method>s`.'
56
+ RESTRICT_ON_SEND = %i[exists?].freeze
57
+
58
+ def_node_matcher :where_exists_call?, <<~PATTERN
59
+ (send (send _ :where $...) :exists?)
60
+ PATTERN
61
+
62
+ def_node_matcher :exists_with_args?, <<~PATTERN
63
+ (send _ :exists? $...)
64
+ PATTERN
65
+
66
+ def on_send(node)
67
+ find_offenses(node) do |args|
68
+ return unless convertable_args?(args)
69
+
70
+ range = correction_range(node)
71
+ good_method = build_good_method(args)
72
+ message = format(MSG, good_method: good_method, bad_method: range.source)
73
+
74
+ add_offense(range, message: message) do |corrector|
75
+ corrector.replace(range, good_method)
76
+ end
77
+ end
78
+ end
79
+
80
+ private
81
+
82
+ def where_style?
83
+ style == :where
84
+ end
85
+
86
+ def exists_style?
87
+ style == :exists
88
+ end
89
+
90
+ def find_offenses(node, &block)
91
+ if exists_style?
92
+ where_exists_call?(node, &block)
93
+ elsif where_style?
94
+ exists_with_args?(node, &block)
95
+ end
96
+ end
97
+
98
+ def convertable_args?(args)
99
+ return false if args.empty?
100
+
101
+ args.size > 1 || args[0].hash_type? || args[0].array_type?
102
+ end
103
+
104
+ def correction_range(node)
105
+ if exists_style?
106
+ node.receiver.loc.selector.join(node.loc.selector)
107
+ elsif where_style?
108
+ node.loc.selector.with(end_pos: node.source_range.end_pos)
109
+ end
110
+ end
111
+
112
+ def build_good_method(args)
113
+ if exists_style?
114
+ build_good_method_exists(args)
115
+ elsif where_style?
116
+ build_good_method_where(args)
117
+ end
118
+ end
119
+
120
+ def build_good_method_exists(args)
121
+ if args.size > 1
122
+ "exists?([#{args.map(&:source).join(', ')}])"
123
+ else
124
+ "exists?(#{args[0].source})"
125
+ end
126
+ end
127
+
128
+ def build_good_method_where(args)
129
+ if args.size > 1
130
+ "where(#{args.map(&:source).join(', ')}).exists?"
131
+ else
132
+ "where(#{args[0].source}).exists?"
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # Use `where.missing(...)` to find missing relationship records.
7
+ #
8
+ # This cop is enabled in Rails 6.1 or higher.
9
+ #
10
+ # @example
11
+ # # bad
12
+ # Post.left_joins(:author).where(authors: { id: nil })
13
+ #
14
+ # # good
15
+ # Post.where.missing(:author)
16
+ #
17
+ class WhereMissing < Base
18
+ include RangeHelp
19
+ extend AutoCorrector
20
+ extend TargetRailsVersion
21
+
22
+ MSG = 'Use `where.missing(:%<left_joins_association>s)` instead of ' \
23
+ '`%<left_joins_method>s(:%<left_joins_association>s).where(%<where_association>s: { id: nil })`.'
24
+ RESTRICT_ON_SEND = %i[left_joins left_outer_joins].freeze
25
+
26
+ minimum_target_rails_version 6.1
27
+
28
+ # @!method where_node_and_argument(node)
29
+ def_node_search :where_node_and_argument, <<~PATTERN
30
+ $(send ... :where (hash <(pair $(sym _) (hash (pair (sym :id) (nil))))...> ))
31
+ PATTERN
32
+
33
+ # @!method missing_relationship(node)
34
+ def_node_search :missing_relationship, <<~PATTERN
35
+ (pair (sym _) (hash (pair (sym :id) (nil))))
36
+ PATTERN
37
+
38
+ def on_send(node)
39
+ return unless node.first_argument.sym_type?
40
+
41
+ root_receiver = root_receiver(node)
42
+ where_node_and_argument(root_receiver) do |where_node, where_argument|
43
+ next unless root_receiver == root_receiver(where_node)
44
+ next unless same_relationship?(where_argument, node.first_argument)
45
+
46
+ range = range_between(node.loc.selector.begin_pos, node.source_range.end_pos)
47
+ register_offense(node, where_node, where_argument, range)
48
+ break
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ def root_receiver(node)
55
+ parent = node.parent
56
+ if !parent&.send_type? || parent.method?(:or) || parent.method?(:and)
57
+ node
58
+ else
59
+ root_receiver(parent)
60
+ end
61
+ end
62
+
63
+ def same_relationship?(where, left_joins)
64
+ where.value.to_s.match?(/^#{left_joins.value}s?$/)
65
+ end
66
+
67
+ def register_offense(node, where_node, where_argument, range)
68
+ add_offense(range, message: message(node, where_argument)) do |corrector|
69
+ corrector.replace(node.loc.selector, 'where.missing')
70
+ if multi_condition?(where_node.first_argument)
71
+ replace_where_method(corrector, where_node)
72
+ else
73
+ remove_where_method(corrector, node, where_node)
74
+ end
75
+ end
76
+ end
77
+
78
+ def replace_where_method(corrector, where_node)
79
+ missing_relationship(where_node) do |where_clause|
80
+ corrector.remove(replace_range(where_clause))
81
+ end
82
+ end
83
+
84
+ def replace_range(child)
85
+ if (right_sibling = child.right_sibling)
86
+ range_between(child.source_range.begin_pos, right_sibling.source_range.begin_pos)
87
+ else
88
+ range_between(child.left_sibling.source_range.end_pos, child.source_range.end_pos)
89
+ end
90
+ end
91
+
92
+ def remove_where_method(corrector, node, where_node)
93
+ range = range_between(where_node.loc.selector.begin_pos, where_node.loc.end.end_pos)
94
+ if node.multiline? && !same_line?(node, where_node)
95
+ range = range_by_whole_lines(range, include_final_newline: true)
96
+ else
97
+ corrector.remove(where_node.loc.dot)
98
+ end
99
+
100
+ corrector.remove(range)
101
+ end
102
+
103
+ def same_line?(left_joins_node, where_node)
104
+ left_joins_node.loc.selector.line == where_node.loc.selector.line
105
+ end
106
+
107
+ def multi_condition?(where_arg)
108
+ where_arg.children.count > 1
109
+ end
110
+
111
+ def message(node, where_argument)
112
+ format(MSG, left_joins_association: node.first_argument.value, left_joins_method: node.method_name,
113
+ where_association: where_argument.value)
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # Identifies places where manually constructed SQL
7
+ # in `where` can be replaced with `where.not(...)`.
8
+ #
9
+ # @example
10
+ # # bad
11
+ # User.where('name != ?', 'Gabe')
12
+ # User.where('name != :name', name: 'Gabe')
13
+ # User.where('name <> ?', 'Gabe')
14
+ # User.where('name <> :name', name: 'Gabe')
15
+ # User.where('name IS NOT NULL')
16
+ # User.where('name NOT IN (?)', ['john', 'jane'])
17
+ # User.where('name NOT IN (:names)', names: ['john', 'jane'])
18
+ # User.where('users.name != :name', name: 'Gabe')
19
+ #
20
+ # # good
21
+ # User.where.not(name: 'Gabe')
22
+ # User.where.not(name: nil)
23
+ # User.where.not(name: ['john', 'jane'])
24
+ # User.where.not(users: { name: 'Gabe' })
25
+ #
26
+ class WhereNot < Base
27
+ include RangeHelp
28
+ extend AutoCorrector
29
+
30
+ MSG = 'Use `%<good_method>s` instead of manually constructing negated SQL in `where`.'
31
+ RESTRICT_ON_SEND = %i[where].freeze
32
+
33
+ def_node_matcher :where_method_call?, <<~PATTERN
34
+ {
35
+ (send _ :where (array $str_type? $_ ?))
36
+ (send _ :where $str_type? $_ ?)
37
+ }
38
+ PATTERN
39
+
40
+ def on_send(node)
41
+ where_method_call?(node) do |template_node, value_node|
42
+ value_node = value_node.first
43
+
44
+ range = offense_range(node)
45
+
46
+ column_and_value = extract_column_and_value(template_node, value_node)
47
+ return unless column_and_value
48
+
49
+ good_method = build_good_method(*column_and_value)
50
+ message = format(MSG, good_method: good_method)
51
+
52
+ add_offense(range, message: message) do |corrector|
53
+ corrector.replace(range, good_method)
54
+ end
55
+ end
56
+ end
57
+
58
+ NOT_EQ_ANONYMOUS_RE = /\A([\w.]+)\s+(?:!=|<>)\s+\?\z/.freeze # column != ?, column <> ?
59
+ NOT_IN_ANONYMOUS_RE = /\A([\w.]+)\s+NOT\s+IN\s+\(\?\)\z/i.freeze # column NOT IN (?)
60
+ NOT_EQ_NAMED_RE = /\A([\w.]+)\s+(?:!=|<>)\s+:(\w+)\z/.freeze # column != :column, column <> :column
61
+ NOT_IN_NAMED_RE = /\A([\w.]+)\s+NOT\s+IN\s+\(:(\w+)\)\z/i.freeze # column NOT IN (:column)
62
+ IS_NOT_NULL_RE = /\A([\w.]+)\s+IS\s+NOT\s+NULL\z/i.freeze # column IS NOT NULL
63
+
64
+ private
65
+
66
+ def offense_range(node)
67
+ range_between(node.loc.selector.begin_pos, node.source_range.end_pos)
68
+ end
69
+
70
+ def extract_column_and_value(template_node, value_node)
71
+ value =
72
+ case template_node.value
73
+ when NOT_EQ_ANONYMOUS_RE, NOT_IN_ANONYMOUS_RE
74
+ value_node.source
75
+ when NOT_EQ_NAMED_RE, NOT_IN_NAMED_RE
76
+ return unless value_node.hash_type?
77
+
78
+ pair = value_node.pairs.find { |p| p.key.value.to_sym == Regexp.last_match(2).to_sym }
79
+ pair.value.source
80
+ when IS_NOT_NULL_RE
81
+ 'nil'
82
+ else
83
+ return
84
+ end
85
+
86
+ [Regexp.last_match(1), value]
87
+ end
88
+
89
+ def build_good_method(column, value)
90
+ if column.include?('.')
91
+ table, column = column.split('.')
92
+
93
+ "where.not(#{table}: { #{column}: #{value} })"
94
+ else
95
+ "where.not(#{column}: #{value})"
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # Identifies calls to `where.not` with multiple hash arguments.
7
+ #
8
+ # The behavior of `where.not` changed in Rails 6.1. Prior to the change,
9
+ # `.where.not(trashed: true, role: 'admin')` evaluated to
10
+ # `WHERE trashed != TRUE AND role != 'admin'`.
11
+ # From Rails 6.1 onwards, this executes the query
12
+ # `WHERE NOT (trashed == TRUE AND roles == 'admin')`.
13
+ #
14
+ # @example
15
+ # # bad
16
+ # User.where.not(trashed: true, role: 'admin')
17
+ # User.where.not(trashed: true, role: ['moderator', 'admin'])
18
+ # User.joins(:posts).where.not(posts: { trashed: true, title: 'Rails' })
19
+ #
20
+ # # good
21
+ # User.where.not(trashed: true)
22
+ # User.where.not(role: ['moderator', 'admin'])
23
+ # User.where.not(trashed: true).where.not(role: ['moderator', 'admin'])
24
+ # User.where.not('trashed = ? OR role = ?', true, 'admin')
25
+ class WhereNotWithMultipleConditions < Base
26
+ MSG = 'Use a SQL statement instead of `where.not` with multiple conditions.'
27
+ RESTRICT_ON_SEND = %i[not].freeze
28
+
29
+ def_node_matcher :where_not_call?, <<~PATTERN
30
+ (send (send _ :where) :not $...)
31
+ PATTERN
32
+
33
+ def on_send(node)
34
+ where_not_call?(node) do |args|
35
+ next unless args[0]&.hash_type?
36
+ next unless multiple_arguments_hash? args[0]
37
+
38
+ range = node.receiver.loc.selector.with(end_pos: node.source_range.end_pos)
39
+
40
+ add_offense(range)
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def multiple_arguments_hash?(hash)
47
+ return true if hash.pairs.size >= 2
48
+ return false unless hash.values[0]&.hash_type?
49
+
50
+ multiple_arguments_hash?(hash.values[0])
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -1,64 +1,134 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module RuboCop
4
- # RuboCop included the Rails cops directly before version 1.0.0.
5
- # We can remove them to avoid warnings about redefining constants.
6
- module Cop
7
- remove_const('Rails') if const_defined?('Rails')
8
- end
9
- end
10
-
3
+ require_relative 'mixin/active_record_helper'
4
+ require_relative 'mixin/active_record_migrations_helper'
5
+ require_relative 'mixin/class_send_node_helper'
6
+ require_relative 'mixin/enforce_superclass'
7
+ require_relative 'mixin/index_method'
8
+ require_relative 'mixin/migrations_helper'
11
9
  require_relative 'mixin/target_rails_version'
12
10
 
11
+ require_relative 'rails/action_controller_flash_before_render'
12
+ require_relative 'rails/action_controller_test_case'
13
13
  require_relative 'rails/action_filter'
14
+ require_relative 'rails/action_order'
14
15
  require_relative 'rails/active_record_aliases'
16
+ require_relative 'rails/active_record_callbacks_order'
15
17
  require_relative 'rails/active_record_override'
16
18
  require_relative 'rails/active_support_aliases'
19
+ require_relative 'rails/active_support_on_load'
20
+ require_relative 'rails/add_column_index'
21
+ require_relative 'rails/after_commit_override'
22
+ require_relative 'rails/application_controller'
17
23
  require_relative 'rails/application_job'
24
+ require_relative 'rails/application_mailer'
18
25
  require_relative 'rails/application_record'
26
+ require_relative 'rails/arel_star'
19
27
  require_relative 'rails/assert_not'
28
+ require_relative 'rails/attribute_default_block_value'
20
29
  require_relative 'rails/belongs_to'
21
30
  require_relative 'rails/blank'
22
31
  require_relative 'rails/bulk_change_table'
32
+ require_relative 'rails/compact_blank'
33
+ require_relative 'rails/content_tag'
23
34
  require_relative 'rails/create_table_with_timestamps'
24
35
  require_relative 'rails/date'
36
+ require_relative 'rails/default_scope'
25
37
  require_relative 'rails/delegate'
26
38
  require_relative 'rails/delegate_allow_blank'
39
+ require_relative 'rails/deprecated_active_model_errors_methods'
40
+ require_relative 'rails/dot_separated_keys'
41
+ require_relative 'rails/duplicate_association'
42
+ require_relative 'rails/duplicate_scope'
43
+ require_relative 'rails/duration_arithmetic'
27
44
  require_relative 'rails/dynamic_find_by'
45
+ require_relative 'rails/eager_evaluation_log_message'
46
+ require_relative 'rails/enum_hash'
28
47
  require_relative 'rails/enum_uniqueness'
29
48
  require_relative 'rails/environment_comparison'
49
+ require_relative 'rails/environment_variable_access'
30
50
  require_relative 'rails/exit'
51
+ require_relative 'rails/expanded_date_range'
31
52
  require_relative 'rails/file_path'
32
53
  require_relative 'rails/find_by'
54
+ require_relative 'rails/find_by_id'
33
55
  require_relative 'rails/find_each'
56
+ require_relative 'rails/freeze_time'
34
57
  require_relative 'rails/has_and_belongs_to_many'
35
58
  require_relative 'rails/has_many_or_has_one_dependent'
36
59
  require_relative 'rails/helper_instance_variable'
37
60
  require_relative 'rails/http_positional_arguments'
38
61
  require_relative 'rails/http_status'
62
+ require_relative 'rails/i18n_lazy_lookup'
63
+ require_relative 'rails/i18n_locale_assignment'
64
+ require_relative 'rails/i18n_locale_texts'
65
+ require_relative 'rails/ignored_columns_assignment'
39
66
  require_relative 'rails/ignored_skip_action_filter_option'
67
+ require_relative 'rails/index_by'
68
+ require_relative 'rails/index_with'
69
+ require_relative 'rails/inquiry'
40
70
  require_relative 'rails/inverse_of'
41
71
  require_relative 'rails/lexically_scoped_action_filter'
42
72
  require_relative 'rails/link_to_blank'
73
+ require_relative 'rails/mailer_name'
74
+ require_relative 'rails/match_route'
75
+ require_relative 'rails/migration_class_name'
76
+ require_relative 'rails/negate_include'
43
77
  require_relative 'rails/not_null_column'
78
+ require_relative 'rails/order_by_id'
44
79
  require_relative 'rails/output'
45
80
  require_relative 'rails/output_safety'
81
+ require_relative 'rails/pick'
82
+ require_relative 'rails/pluck'
83
+ require_relative 'rails/pluck_id'
84
+ require_relative 'rails/pluck_in_where'
46
85
  require_relative 'rails/pluralization_grammar'
47
86
  require_relative 'rails/presence'
48
87
  require_relative 'rails/present'
88
+ require_relative 'rails/rake_environment'
49
89
  require_relative 'rails/read_write_attribute'
50
90
  require_relative 'rails/redundant_allow_nil'
91
+ require_relative 'rails/redundant_foreign_key'
92
+ require_relative 'rails/redundant_presence_validation_on_belongs_to'
51
93
  require_relative 'rails/redundant_receiver_in_with_options'
94
+ require_relative 'rails/redundant_travel_back'
52
95
  require_relative 'rails/reflection_class_name'
53
96
  require_relative 'rails/refute_methods'
54
97
  require_relative 'rails/relative_date_constant'
98
+ require_relative 'rails/render_inline'
99
+ require_relative 'rails/render_plain_text'
55
100
  require_relative 'rails/request_referer'
101
+ require_relative 'rails/require_dependency'
102
+ require_relative 'rails/response_parsed_body'
56
103
  require_relative 'rails/reversible_migration'
104
+ require_relative 'rails/reversible_migration_method_definition'
105
+ require_relative 'rails/root_join_chain'
106
+ require_relative 'rails/root_pathname_methods'
107
+ require_relative 'rails/root_public_path'
57
108
  require_relative 'rails/safe_navigation'
109
+ require_relative 'rails/safe_navigation_with_blank'
58
110
  require_relative 'rails/save_bang'
111
+ require_relative 'rails/schema_comment'
59
112
  require_relative 'rails/scope_args'
113
+ require_relative 'rails/short_i18n'
60
114
  require_relative 'rails/skips_model_validations'
115
+ require_relative 'rails/squished_sql_heredocs'
116
+ require_relative 'rails/strip_heredoc'
117
+ require_relative 'rails/table_name_assignment'
118
+ require_relative 'rails/three_state_boolean_column'
61
119
  require_relative 'rails/time_zone'
120
+ require_relative 'rails/time_zone_assignment'
121
+ require_relative 'rails/to_formatted_s'
122
+ require_relative 'rails/to_s_with_argument'
123
+ require_relative 'rails/top_level_hash_with_indifferent_access'
124
+ require_relative 'rails/transaction_exit_statement'
62
125
  require_relative 'rails/uniq_before_pluck'
126
+ require_relative 'rails/unique_validation_without_index'
63
127
  require_relative 'rails/unknown_env'
128
+ require_relative 'rails/unused_ignored_columns'
64
129
  require_relative 'rails/validation'
130
+ require_relative 'rails/where_equals'
131
+ require_relative 'rails/where_exists'
132
+ require_relative 'rails/where_missing'
133
+ require_relative 'rails/where_not'
134
+ require_relative 'rails/where_not_with_multiple_conditions'
@@ -8,7 +8,7 @@ module RuboCop
8
8
  def self.defaults!
9
9
  path = CONFIG_DEFAULT.to_s
10
10
  hash = ConfigLoader.send(:load_yaml_configuration, path)
11
- config = Config.new(hash, path)
11
+ config = Config.new(hash, path).tap(&:make_excludes_absolute)
12
12
  puts "configuration from #{path}" if ConfigLoader.debug?
13
13
  config = ConfigLoader.merge_with_default(config, path, unset_nil: false)
14
14
  ConfigLoader.instance_variable_set(:@default_configuration, config)