rubocop-rails 2.13.0 → 2.14.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (30) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +1 -1
  3. data/config/default.yml +75 -4
  4. data/lib/rubocop/cop/mixin/class_send_node_helper.rb +20 -0
  5. data/lib/rubocop/cop/mixin/migrations_helper.rb +26 -0
  6. data/lib/rubocop/cop/rails/action_controller_test_case.rb +47 -0
  7. data/lib/rubocop/cop/rails/after_commit_override.rb +2 -12
  8. data/lib/rubocop/cop/rails/bulk_change_table.rb +20 -6
  9. data/lib/rubocop/cop/rails/compact_blank.rb +22 -13
  10. data/lib/rubocop/cop/rails/deprecated_active_model_errors_methods.rb +108 -0
  11. data/lib/rubocop/cop/rails/duplicate_association.rb +56 -0
  12. data/lib/rubocop/cop/rails/duplicate_scope.rb +46 -0
  13. data/lib/rubocop/cop/rails/duration_arithmetic.rb +2 -1
  14. data/lib/rubocop/cop/rails/i18n_lazy_lookup.rb +94 -0
  15. data/lib/rubocop/cop/rails/i18n_locale_texts.rb +110 -0
  16. data/lib/rubocop/cop/rails/index_by.rb +6 -6
  17. data/lib/rubocop/cop/rails/index_with.rb +6 -6
  18. data/lib/rubocop/cop/rails/inverse_of.rb +17 -1
  19. data/lib/rubocop/cop/rails/migration_class_name.rb +61 -0
  20. data/lib/rubocop/cop/rails/pluck.rb +15 -7
  21. data/lib/rubocop/cop/rails/read_write_attribute.rb +51 -14
  22. data/lib/rubocop/cop/rails/redundant_presence_validation_on_belongs_to.rb +93 -28
  23. data/lib/rubocop/cop/rails/reversible_migration.rb +5 -3
  24. data/lib/rubocop/cop/rails/reversible_migration_method_definition.rb +2 -10
  25. data/lib/rubocop/cop/rails/table_name_assignment.rb +44 -0
  26. data/lib/rubocop/cop/rails/transaction_exit_statement.rb +77 -0
  27. data/lib/rubocop/cop/rails/unused_ignored_columns.rb +2 -0
  28. data/lib/rubocop/cop/rails_cops.rb +11 -0
  29. data/lib/rubocop/rails/version.rb +1 -1
  30. metadata +15 -4
@@ -8,6 +8,10 @@ module RuboCop
8
8
  # explicitly set to `false`. The presence validator is added
9
9
  # automatically, and explicit presence validation is redundant.
10
10
  #
11
+ # @safety
12
+ # This cop's autocorrection is unsafe because it changes the default error message
13
+ # from "can't be blank" to "must exist".
14
+ #
11
15
  # @example
12
16
  # # bad
13
17
  # belongs_to :user
@@ -32,7 +36,7 @@ module RuboCop
32
36
  extend AutoCorrector
33
37
  extend TargetRailsVersion
34
38
 
35
- MSG = 'Remove explicit presence validation for `%<association>s`.'
39
+ MSG = 'Remove explicit presence validation for %<association>s.'
36
40
  RESTRICT_ON_SEND = %i[validates].freeze
37
41
 
38
42
  minimum_target_rails_version 5.0
@@ -43,28 +47,36 @@ module RuboCop
43
47
  # @example source that matches - by association
44
48
  # validates :user, presence: true
45
49
  #
46
- # @example source that matches - with presence options
47
- # validates :user, presence: { message: 'duplicate' }
50
+ # @example source that matches - by association
51
+ # validates :name, :user, presence: true
48
52
  #
49
53
  # @example source that matches - by a foreign key
50
54
  # validates :user_id, presence: true
55
+ #
56
+ # @example source that DOES NOT match - strict validation
57
+ # validates :user_id, presence: true, strict: true
58
+ #
59
+ # @example source that DOES NOT match - custom strict validation
60
+ # validates :user_id, presence: true, strict: MissingUserError
51
61
  def_node_matcher :presence_validation?, <<~PATTERN
52
- $(
62
+ (
53
63
  send nil? :validates
54
- (sym $_)
55
- ...
56
- $(hash <$(pair (sym :presence) {true hash}) ...>)
64
+ (sym $_)+
65
+ $[
66
+ (hash <$(pair (sym :presence) true) ...>) # presence: true
67
+ !(hash <$(pair (sym :strict) {true const}) ...>) # strict: true
68
+ ]
57
69
  )
58
70
  PATTERN
59
71
 
60
- # @!method optional_option?(node)
61
- # Match a `belongs_to` association with an optional option in a hash
72
+ # @!method optional?(node)
73
+ # Match a `belongs_to` association with an optional option in a hash
62
74
  def_node_matcher :optional?, <<~PATTERN
63
75
  (send nil? :belongs_to _ ... #optional_option?)
64
76
  PATTERN
65
77
 
66
78
  # @!method optional_option?(node)
67
- # Match an optional option in a hash
79
+ # Match an optional option in a hash
68
80
  def_node_matcher :optional_option?, <<~PATTERN
69
81
  {
70
82
  (hash <(pair (sym :optional) true) ...>) # optional: true
@@ -122,7 +134,7 @@ module RuboCop
122
134
  )
123
135
  PATTERN
124
136
 
125
- # @!method belongs_to_without_fk?(node, fk)
137
+ # @!method belongs_to_without_fk?(node, key)
126
138
  # Match a matching `belongs_to` association, without an explicit `foreign_key` option
127
139
  #
128
140
  # @param node [RuboCop::AST::Node]
@@ -150,21 +162,43 @@ module RuboCop
150
162
  PATTERN
151
163
 
152
164
  def on_send(node)
153
- validation, key, options, presence = presence_validation?(node)
154
- return unless validation
165
+ presence_validation?(node) do |all_keys, options, presence|
166
+ keys = non_optional_belongs_to(node.parent, all_keys)
167
+ return if keys.none?
155
168
 
156
- belongs_to = belongs_to_for(node.parent, key)
157
- return unless belongs_to
158
- return if optional?(belongs_to)
169
+ add_offense_and_correct(node, all_keys, keys, options, presence)
170
+ end
171
+ end
159
172
 
160
- message = format(MSG, association: key.to_s)
173
+ private
161
174
 
162
- add_offense(presence, message: message) do |corrector|
163
- remove_presence_validation(corrector, node, options, presence)
175
+ def add_offense_and_correct(node, all_keys, keys, options, presence)
176
+ add_offense(presence, message: message_for(keys)) do |corrector|
177
+ if options.children.one? # `presence: true` is the only option
178
+ if keys == all_keys
179
+ remove_validation(corrector, node)
180
+ else
181
+ remove_keys_from_validation(corrector, node, keys)
182
+ end
183
+ elsif keys == all_keys
184
+ remove_presence_option(corrector, presence)
185
+ else
186
+ extract_validation_for_keys(corrector, node, keys, options)
187
+ end
164
188
  end
165
189
  end
166
190
 
167
- private
191
+ def message_for(keys)
192
+ display_keys = keys.map { |key| "`#{key}`" }.join('/')
193
+ format(MSG, association: display_keys)
194
+ end
195
+
196
+ def non_optional_belongs_to(node, keys)
197
+ keys.select do |key|
198
+ belongs_to = belongs_to_for(node, key)
199
+ belongs_to && !optional?(belongs_to)
200
+ end
201
+ end
168
202
 
169
203
  def belongs_to_for(model_class_node, key)
170
204
  if key.to_s.end_with?('_id')
@@ -175,17 +209,48 @@ module RuboCop
175
209
  end
176
210
  end
177
211
 
178
- def remove_presence_validation(corrector, node, options, presence)
179
- if options.children.one?
180
- corrector.remove(range_by_whole_lines(node.source_range, include_final_newline: true))
181
- else
182
- range = range_with_surrounding_comma(
183
- range_with_surrounding_space(range: presence.source_range, side: :left),
184
- :left
212
+ def remove_validation(corrector, node)
213
+ corrector.remove(validation_range(node))
214
+ end
215
+
216
+ def remove_keys_from_validation(corrector, node, keys)
217
+ keys.each do |key|
218
+ key_node = node.arguments.find { |arg| arg.value == key }
219
+ key_range = range_with_surrounding_space(
220
+ range: range_with_surrounding_comma(key_node.source_range, :right),
221
+ side: :right
185
222
  )
186
- corrector.remove(range)
223
+ corrector.remove(key_range)
187
224
  end
188
225
  end
226
+
227
+ def remove_presence_option(corrector, presence)
228
+ range = range_with_surrounding_comma(
229
+ range_with_surrounding_space(range: presence.source_range, side: :left),
230
+ :left
231
+ )
232
+ corrector.remove(range)
233
+ end
234
+
235
+ def extract_validation_for_keys(corrector, node, keys, options)
236
+ indentation = ' ' * node.source_range.column
237
+ options_without_presence = options.children.reject { |pair| pair.key.value == :presence }
238
+ source = [
239
+ indentation,
240
+ 'validates ',
241
+ keys.map(&:inspect).join(', '),
242
+ ', ',
243
+ options_without_presence.map(&:source).join(', '),
244
+ "\n"
245
+ ].join
246
+
247
+ remove_keys_from_validation(corrector, node, keys)
248
+ corrector.insert_after(validation_range(node), source)
249
+ end
250
+
251
+ def validation_range(node)
252
+ range_by_whole_lines(node.source_range, include_final_newline: true)
253
+ end
189
254
  end
190
255
  end
191
256
  end
@@ -176,10 +176,12 @@ module RuboCop
176
176
  #
177
177
  # @see https://api.rubyonrails.org/classes/ActiveRecord/Migration/CommandRecorder.html
178
178
  class ReversibleMigration < Base
179
+ include MigrationsHelper
180
+
179
181
  MSG = '%<action>s is not reversible.'
180
182
 
181
183
  def_node_matcher :irreversible_schema_statement_call, <<~PATTERN
182
- (send nil? ${:change_column :execute :remove_belongs_to :remove_reference} ...)
184
+ (send nil? ${:change_column :execute} ...)
183
185
  PATTERN
184
186
 
185
187
  def_node_matcher :drop_table_call, <<~PATTERN
@@ -207,7 +209,7 @@ module RuboCop
207
209
  PATTERN
208
210
 
209
211
  def on_send(node)
210
- return unless within_change_method?(node)
212
+ return unless in_migration?(node) && within_change_method?(node)
211
213
  return if within_reversible_or_up_only_block?(node)
212
214
 
213
215
  check_irreversible_schema_statement_node(node)
@@ -220,7 +222,7 @@ module RuboCop
220
222
  end
221
223
 
222
224
  def on_block(node)
223
- return unless within_change_method?(node)
225
+ return unless in_migration?(node) && within_change_method?(node)
224
226
  return if within_reversible_or_up_only_block?(node)
225
227
  return if node.body.nil?
226
228
 
@@ -43,19 +43,11 @@ module RuboCop
43
43
  # end
44
44
  # end
45
45
  class ReversibleMigrationMethodDefinition < Base
46
+ include MigrationsHelper
47
+
46
48
  MSG = 'Migrations must contain either a `change` method, or ' \
47
49
  'both an `up` and a `down` method.'
48
50
 
49
- def_node_matcher :migration_class?, <<~PATTERN
50
- (class
51
- (const nil? _)
52
- (send
53
- (const (const {nil? cbase} :ActiveRecord) :Migration)
54
- :[]
55
- (float _))
56
- _)
57
- PATTERN
58
-
59
51
  def_node_matcher :change_method?, <<~PATTERN
60
52
  [ #migration_class? `(def :change (args) _) ]
61
53
  PATTERN
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # This cop enforces the absence of explicit table name assignment.
7
+ #
8
+ # `self.table_name=` should only be used for very good reasons,
9
+ # such as not having control over the database, or working
10
+ # on a legacy project.
11
+ #
12
+ # If you need to change how your model's name is translated to
13
+ # a table name, you may want to look at Inflections:
14
+ # https://api.rubyonrails.org/classes/ActiveSupport/Inflector/Inflections.html
15
+ #
16
+ # If you wish to add a prefix in front of your model, or wish to change
17
+ # the default prefix, `self.table_name_prefix` might better suit your needs:
18
+ # https://api.rubyonrails.org/classes/ActiveRecord/ModelSchema.html#method-c-table_name_prefix-3D
19
+ #
20
+ # STI base classes named `Base` are ignored by this cop.
21
+ # For more information: https://api.rubyonrails.org/classes/ActiveRecord/Inheritance.html
22
+ #
23
+ # @example
24
+ # # bad
25
+ # self.table_name = 'some_table_name'
26
+ # self.table_name = :some_other_name
27
+ class TableNameAssignment < Base
28
+ include ActiveRecordHelper
29
+
30
+ MSG = 'Do not use `self.table_name =`.'
31
+
32
+ def_node_matcher :base_class?, <<~PATTERN
33
+ (class (const ... :Base) ...)
34
+ PATTERN
35
+
36
+ def on_class(class_node)
37
+ return if base_class?(class_node)
38
+
39
+ find_set_table_name(class_node).each { |node| add_offense(node) }
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # This cop checks for the use of exit statements (namely `return`,
7
+ # `break` and `throw`) in transactions. This is due to the eventual
8
+ # unexpected behavior when using ActiveRecord >= 7, where transactions
9
+ # exitted using these statements are being rollbacked rather than
10
+ # committed (pre ActiveRecord 7 behavior).
11
+ #
12
+ # As alternatives, it would be more intuitive to explicitly raise an
13
+ # error when rollback is desired, and to use `next` when commit is
14
+ # desired.
15
+ #
16
+ # @example
17
+ # # bad
18
+ # ApplicationRecord.transaction do
19
+ # return if user.active?
20
+ # end
21
+ #
22
+ # # bad
23
+ # ApplicationRecord.transaction do
24
+ # break if user.active?
25
+ # end
26
+ #
27
+ # # bad
28
+ # ApplicationRecord.transaction do
29
+ # throw if user.active?
30
+ # end
31
+ #
32
+ # # good
33
+ # ApplicationRecord.transaction do
34
+ # # Rollback
35
+ # raise "User is active" if user.active?
36
+ # end
37
+ #
38
+ # # good
39
+ # ApplicationRecord.transaction do
40
+ # # Commit
41
+ # next if user.active?
42
+ # end
43
+ #
44
+ # @see https://github.com/rails/rails/commit/15aa4200e083
45
+ class TransactionExitStatement < Base
46
+ MSG = <<~MSG.chomp
47
+ Exit statement `%<statement>s` is not allowed. Use `raise` (rollback) or `next` (commit).
48
+ MSG
49
+
50
+ RESTRICT_ON_SEND = %i[transaction].freeze
51
+
52
+ def_node_search :exit_statements, <<~PATTERN
53
+ ({return | break | send nil? :throw} ...)
54
+ PATTERN
55
+
56
+ def on_send(node)
57
+ parent = node.parent
58
+
59
+ return unless parent&.block_type?
60
+
61
+ exit_statements(parent.body).each do |statement_node|
62
+ statement = if statement_node.return_type?
63
+ 'return'
64
+ elsif statement_node.break_type?
65
+ 'break'
66
+ else
67
+ statement_node.method_name
68
+ end
69
+ message = format(MSG, statement: statement)
70
+
71
+ add_offense(statement_node, message: message)
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -61,6 +61,8 @@ module RuboCop
61
61
 
62
62
  def table(node)
63
63
  klass = class_node(node)
64
+ return unless klass
65
+
64
66
  schema.table_by(name: table_name(klass))
65
67
  end
66
68
  end
@@ -2,10 +2,13 @@
2
2
 
3
3
  require_relative 'mixin/active_record_helper'
4
4
  require_relative 'mixin/active_record_migrations_helper'
5
+ require_relative 'mixin/class_send_node_helper'
5
6
  require_relative 'mixin/enforce_superclass'
6
7
  require_relative 'mixin/index_method'
8
+ require_relative 'mixin/migrations_helper'
7
9
  require_relative 'mixin/target_rails_version'
8
10
 
11
+ require_relative 'rails/action_controller_test_case'
9
12
  require_relative 'rails/action_filter'
10
13
  require_relative 'rails/active_record_aliases'
11
14
  require_relative 'rails/active_record_callbacks_order'
@@ -30,6 +33,9 @@ require_relative 'rails/date'
30
33
  require_relative 'rails/default_scope'
31
34
  require_relative 'rails/delegate'
32
35
  require_relative 'rails/delegate_allow_blank'
36
+ require_relative 'rails/deprecated_active_model_errors_methods'
37
+ require_relative 'rails/duplicate_association'
38
+ require_relative 'rails/duplicate_scope'
33
39
  require_relative 'rails/duration_arithmetic'
34
40
  require_relative 'rails/dynamic_find_by'
35
41
  require_relative 'rails/eager_evaluation_log_message'
@@ -48,7 +54,9 @@ require_relative 'rails/has_many_or_has_one_dependent'
48
54
  require_relative 'rails/helper_instance_variable'
49
55
  require_relative 'rails/http_positional_arguments'
50
56
  require_relative 'rails/http_status'
57
+ require_relative 'rails/i18n_lazy_lookup'
51
58
  require_relative 'rails/i18n_locale_assignment'
59
+ require_relative 'rails/i18n_locale_texts'
52
60
  require_relative 'rails/ignored_skip_action_filter_option'
53
61
  require_relative 'rails/index_by'
54
62
  require_relative 'rails/index_with'
@@ -58,6 +66,7 @@ require_relative 'rails/lexically_scoped_action_filter'
58
66
  require_relative 'rails/link_to_blank'
59
67
  require_relative 'rails/mailer_name'
60
68
  require_relative 'rails/match_route'
69
+ require_relative 'rails/migration_class_name'
61
70
  require_relative 'rails/negate_include'
62
71
  require_relative 'rails/not_null_column'
63
72
  require_relative 'rails/order_by_id'
@@ -95,8 +104,10 @@ require_relative 'rails/scope_args'
95
104
  require_relative 'rails/short_i18n'
96
105
  require_relative 'rails/skips_model_validations'
97
106
  require_relative 'rails/squished_sql_heredocs'
107
+ require_relative 'rails/table_name_assignment'
98
108
  require_relative 'rails/time_zone'
99
109
  require_relative 'rails/time_zone_assignment'
110
+ require_relative 'rails/transaction_exit_statement'
100
111
  require_relative 'rails/uniq_before_pluck'
101
112
  require_relative 'rails/unique_validation_without_index'
102
113
  require_relative 'rails/unknown_env'
@@ -4,7 +4,7 @@ module RuboCop
4
4
  module Rails
5
5
  # This module holds the RuboCop Rails version information.
6
6
  module Version
7
- STRING = '2.13.0'
7
+ STRING = '2.14.0'
8
8
 
9
9
  def self.document_version
10
10
  STRING.match('\d+\.\d+').to_s
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubocop-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.13.0
4
+ version: 2.14.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bozhidar Batsov
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2021-12-25 00:00:00.000000000 Z
13
+ date: 2022-03-15 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activesupport
@@ -79,9 +79,12 @@ files:
79
79
  - lib/rubocop-rails.rb
80
80
  - lib/rubocop/cop/mixin/active_record_helper.rb
81
81
  - lib/rubocop/cop/mixin/active_record_migrations_helper.rb
82
+ - lib/rubocop/cop/mixin/class_send_node_helper.rb
82
83
  - lib/rubocop/cop/mixin/enforce_superclass.rb
83
84
  - lib/rubocop/cop/mixin/index_method.rb
85
+ - lib/rubocop/cop/mixin/migrations_helper.rb
84
86
  - lib/rubocop/cop/mixin/target_rails_version.rb
87
+ - lib/rubocop/cop/rails/action_controller_test_case.rb
85
88
  - lib/rubocop/cop/rails/action_filter.rb
86
89
  - lib/rubocop/cop/rails/active_record_aliases.rb
87
90
  - lib/rubocop/cop/rails/active_record_callbacks_order.rb
@@ -106,6 +109,9 @@ files:
106
109
  - lib/rubocop/cop/rails/default_scope.rb
107
110
  - lib/rubocop/cop/rails/delegate.rb
108
111
  - lib/rubocop/cop/rails/delegate_allow_blank.rb
112
+ - lib/rubocop/cop/rails/deprecated_active_model_errors_methods.rb
113
+ - lib/rubocop/cop/rails/duplicate_association.rb
114
+ - lib/rubocop/cop/rails/duplicate_scope.rb
109
115
  - lib/rubocop/cop/rails/duration_arithmetic.rb
110
116
  - lib/rubocop/cop/rails/dynamic_find_by.rb
111
117
  - lib/rubocop/cop/rails/eager_evaluation_log_message.rb
@@ -124,7 +130,9 @@ files:
124
130
  - lib/rubocop/cop/rails/helper_instance_variable.rb
125
131
  - lib/rubocop/cop/rails/http_positional_arguments.rb
126
132
  - lib/rubocop/cop/rails/http_status.rb
133
+ - lib/rubocop/cop/rails/i18n_lazy_lookup.rb
127
134
  - lib/rubocop/cop/rails/i18n_locale_assignment.rb
135
+ - lib/rubocop/cop/rails/i18n_locale_texts.rb
128
136
  - lib/rubocop/cop/rails/ignored_skip_action_filter_option.rb
129
137
  - lib/rubocop/cop/rails/index_by.rb
130
138
  - lib/rubocop/cop/rails/index_with.rb
@@ -134,6 +142,7 @@ files:
134
142
  - lib/rubocop/cop/rails/link_to_blank.rb
135
143
  - lib/rubocop/cop/rails/mailer_name.rb
136
144
  - lib/rubocop/cop/rails/match_route.rb
145
+ - lib/rubocop/cop/rails/migration_class_name.rb
137
146
  - lib/rubocop/cop/rails/negate_include.rb
138
147
  - lib/rubocop/cop/rails/not_null_column.rb
139
148
  - lib/rubocop/cop/rails/order_by_id.rb
@@ -171,8 +180,10 @@ files:
171
180
  - lib/rubocop/cop/rails/short_i18n.rb
172
181
  - lib/rubocop/cop/rails/skips_model_validations.rb
173
182
  - lib/rubocop/cop/rails/squished_sql_heredocs.rb
183
+ - lib/rubocop/cop/rails/table_name_assignment.rb
174
184
  - lib/rubocop/cop/rails/time_zone.rb
175
185
  - lib/rubocop/cop/rails/time_zone_assignment.rb
186
+ - lib/rubocop/cop/rails/transaction_exit_statement.rb
176
187
  - lib/rubocop/cop/rails/uniq_before_pluck.rb
177
188
  - lib/rubocop/cop/rails/unique_validation_without_index.rb
178
189
  - lib/rubocop/cop/rails/unknown_env.rb
@@ -194,7 +205,7 @@ metadata:
194
205
  homepage_uri: https://docs.rubocop.org/rubocop-rails/
195
206
  changelog_uri: https://github.com/rubocop/rubocop-rails/blob/master/CHANGELOG.md
196
207
  source_code_uri: https://github.com/rubocop/rubocop-rails/
197
- documentation_uri: https://docs.rubocop.org/rubocop-rails/2.13/
208
+ documentation_uri: https://docs.rubocop.org/rubocop-rails/2.14/
198
209
  bug_tracker_uri: https://github.com/rubocop/rubocop-rails/issues
199
210
  rubygems_mfa_required: 'true'
200
211
  post_install_message:
@@ -212,7 +223,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
212
223
  - !ruby/object:Gem::Version
213
224
  version: '0'
214
225
  requirements: []
215
- rubygems_version: 3.2.32
226
+ rubygems_version: 3.3.3
216
227
  signing_key:
217
228
  specification_version: 4
218
229
  summary: Automatic Rails code style checking tool.