rubocop-asjer 0.4.5 → 0.6.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e5ca85da8803a2b7cf65f61600adea61a52c32cd8c327b5e42d83b84a312035c
4
- data.tar.gz: 82c413cfd54796c72251bfb67b53ac369a829e41ea64e91265de42dbebea1810
3
+ metadata.gz: 33824991e93fac5f0a6dc2ac7170e29f1f19ced20457f9ce1e3ce65fe1fb2073
4
+ data.tar.gz: 360f3e9dd1884e3f59325d639771ab7204c35a86fdb2ced78eadf14333d70dda
5
5
  SHA512:
6
- metadata.gz: 78f2ca3c66f118973d282f8e1ce6ad7e79a47a2c8e4e627c7aa25c006bb914d80ff2c0afde079cb47e5ace29b191d4565fc87f028a6fdc362816103e4f271846
7
- data.tar.gz: fb1910e981dca843eeb1b0425d098b7d4fed04c96d3ee1e6b2bf15dda990b4ff92ea30db3fadfde05d36ef1772ecfe3d255a4f750910af1d0117839f91e9b72a
6
+ metadata.gz: 81cb50c70f94b0a6ef221022971961b8939519f786e492a926e7fb5dff0b49dabad85c9f8ac9fd8490e55f68af32136667238b438679f4e0440c006279729398
7
+ data.tar.gz: fcd0440e6123b3f3e871de30276b62eea4bcd80e466f829e1ee97b49f6c6668ba709cc353aefb2045fc3260a111b88e501c821affae1d21f3092b40261aef964
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.4.5"
2
+ ".": "0.6.0"
3
3
  }
data/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.6.0](https://github.com/asjer/rubocop-asjer/compare/v0.5.0...v0.6.0) (2026-10-28)
4
+
5
+
6
+ ### Features
7
+
8
+ * **cop:** flag bare login_as calls in system specs ([c4d99a4](https://github.com/asjer/rubocop-asjer/commit/c4d99a42f25072bfc55f5b381c3fe89ff303e391))
9
+
10
+ ## [0.5.0](https://github.com/asjer/rubocop-asjer/compare/v0.4.5...v0.5.0) (2026-10-15)
11
+
12
+
13
+ ### Features
14
+
15
+ * add MigrationSchemaOnly and ExplicitMigrationVersionInSpec cops ([316938a](https://github.com/asjer/rubocop-asjer/commit/316938ae96076d82458186759c2f341b2a44849f)), closes [#54](https://github.com/asjer/rubocop-asjer/issues/54)
16
+
3
17
  ## [0.4.5](https://github.com/asjer/rubocop-asjer/compare/v0.4.4...v0.4.5) (2026-03-08)
4
18
 
5
19
 
data/README.md CHANGED
@@ -96,6 +96,106 @@ Asjer/RailsClassOrder:
96
96
  - serialize
97
97
  ```
98
98
 
99
+ ### Asjer/MigrationSchemaOnly
100
+
101
+ Allows only schema statements in Rails migrations. Raw SQL, model calls and other data changes skip validations and callbacks, are often irreversible, and belong in a maintenance task.
102
+
103
+ The cop uses an allowlist rather than a list of forbidden methods, so new raw-SQL routes are rejected automatically. Inside an `ActiveRecord::Migration` class:
104
+
105
+ - implicit-receiver calls must be schema statements (`AllowedMethods`) or methods the migration defines itself;
106
+ - only `AllowedConstants` and constants the migration defines may be referenced;
107
+ - calls on block variables such as `t.string` and `direction.up` are fine.
108
+ - `send`, `public_send` and `eval`-style calls are rejected on any receiver except a block variable.
109
+ - `self` may only be used as a direct receiver; assigning or passing it would hide calls from the check.
110
+
111
+ The cop recognizes classes that inherit from `ActiveRecord::Migration[x.y]`, `ActiveRecord::Migration` or `ActiveRecord::Migration::Current`; migrations with a custom base class are not checked.
112
+
113
+ Check constraints and expression indexes stay allowed: they keep the database consistent alongside model validations, which `update_all`, `update_column` and `insert_all` skip. `change_column` and `t.change` with `using:` are rejected because that is always a SQL cast, and so is the replacement value of `change_column_null`, which updates existing rows.
114
+
115
+ ```ruby
116
+ # bad
117
+ execute "UPDATE bookings SET source = 'web'"
118
+ select_value('SELECT 1')
119
+ connection.exec_query('SELECT 1')
120
+ send(:execute, 'SELECT 1')
121
+ Booking.where(source: nil).update_all(source: 'web')
122
+ add_index :bookings, Arel.sql('lower(code)')
123
+ change_column :bookings, :date, :date, using: 'date::date'
124
+
125
+ # good
126
+ create_table :examples do |t|
127
+ t.string :status, null: false
128
+ end
129
+ add_index :examples, 'lower(status)'
130
+ add_index :examples, :status, using: :gin, opclass: :gin_trgm_ops
131
+ add_check_constraint :examples, "status IN ('open', 'closed')", name: :examples_status
132
+ ```
133
+
134
+ By default, this cop runs on `db/migrate/**/*.rb`. To adopt it in an existing project without rewriting migrations that have already run, set `StartAfter` to the version of the latest existing migration; migrations up to and including that version are skipped:
135
+
136
+ ```yaml
137
+ Asjer/MigrationSchemaOnly:
138
+ StartAfter: 20261014120000
139
+ AllowedConstants:
140
+ - ActiveRecord::IrreversibleMigration
141
+ - Rails
142
+ - Pay::Adapter # e.g. for migrations generated by a gem
143
+ ```
144
+
145
+ `AllowedMethods` replaces the default list of schema statements; see `config/default.yml`.
146
+
147
+ ### Asjer/ExplicitMigrationVersionInSpec
148
+
149
+ Requires migration specs to run migrations to an explicit version. `rollback`, `forward` and `migrate` without a version (or with `nil`) move relative to the latest migration, so the spec breaks as soon as anyone adds an unrelated migration.
150
+
151
+ ```ruby
152
+ # bad
153
+ connection.pool.migration_context.rollback(1)
154
+ migration_context.forward
155
+ migration_context.migrate
156
+
157
+ # good
158
+ connection.pool.migration_context.run(:down, 20261012170000)
159
+ migration_context.migrate(20261012170000)
160
+ ```
161
+
162
+ By default, this cop runs on `spec/migrations/**/*_spec.rb`.
163
+
164
+ ### Asjer/SystemSpecLoginAs
165
+
166
+ Requires unqualified `login_as` calls in system specs to be replaced with an application-provided helper such as `sign_in_and_visit(user, path)`. Warden's handoff applies to whichever request reaches the app server next, not necessarily the browser's intended visit.
167
+
168
+ Every matching call is reported, including calls in `before` blocks, whether a `visit` follows immediately, later, or not at all. Calls with an explicit receiver, such as `foo.login_as(user)`, are outside this cop's scope.
169
+
170
+ ```ruby
171
+ # bad
172
+ login_as(user)
173
+ visit dashboard_path
174
+
175
+ before do
176
+ login_as(user)
177
+ end
178
+
179
+ # good
180
+ sign_in_and_visit(user, dashboard_path)
181
+ ```
182
+
183
+ By default, this cop only runs on `spec/system/**/*.rb` and is `Enabled: pending`. Opt in explicitly (or enable new cops with `AllCops/NewCops: enable`):
184
+
185
+ ```yaml
186
+ Asjer/SystemSpecLoginAs:
187
+ Enabled: true
188
+ Include:
189
+ - 'spec/system/**/*.rb'
190
+ MethodNames:
191
+ - login_as
192
+ PreferredHelper: sign_in_and_visit
193
+ ```
194
+
195
+ `MethodNames` replaces the default list of methods to flag; `PreferredHelper` changes the helper named in the offense message. Define that helper in your application's system-spec support; this gem does not provide it.
196
+
197
+ **No autocorrect:** the cop cannot infer the destination path or safely move authentication out of a `before` block.
198
+
99
199
  ## Development
100
200
 
101
201
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rspec` to run the tests.
data/config/default.yml CHANGED
@@ -1,3 +1,86 @@
1
+ Asjer/ExplicitMigrationVersionInSpec:
2
+ Description: Requires migration specs to run migrations to an explicit version.
3
+ Enabled: true
4
+ VersionAdded: "0.5.0"
5
+ Include:
6
+ - 'spec/migrations/**/*_spec.rb'
7
+
8
+ Asjer/MigrationSchemaOnly:
9
+ Description: Allows only schema statements in Rails migrations; move data changes and raw SQL to a maintenance task.
10
+ Enabled: true
11
+ VersionAdded: "0.5.0"
12
+ Include:
13
+ - 'db/migrate/**/*.rb'
14
+ # Version of the last existing migration; earlier migrations are not checked.
15
+ StartAfter: ~
16
+ AllowedMethods:
17
+ - create_table
18
+ - create_join_table
19
+ - change_table
20
+ - rename_table
21
+ - drop_table
22
+ - drop_join_table
23
+ - add_column
24
+ - remove_column
25
+ - remove_columns
26
+ - rename_column
27
+ - change_column
28
+ - change_column_default
29
+ - change_column_null
30
+ - change_column_comment
31
+ - change_table_comment
32
+ - add_timestamps
33
+ - remove_timestamps
34
+ - add_index
35
+ - remove_index
36
+ - rename_index
37
+ - add_reference
38
+ - remove_reference
39
+ - add_belongs_to
40
+ - remove_belongs_to
41
+ - add_foreign_key
42
+ - remove_foreign_key
43
+ - add_check_constraint
44
+ - remove_check_constraint
45
+ - validate_check_constraint
46
+ - validate_foreign_key
47
+ - validate_constraint
48
+ - add_exclusion_constraint
49
+ - remove_exclusion_constraint
50
+ - add_unique_constraint
51
+ - remove_unique_constraint
52
+ - enable_extension
53
+ - disable_extension
54
+ - create_enum
55
+ - drop_enum
56
+ - rename_enum
57
+ - add_enum_value
58
+ - rename_enum_value
59
+ - create_schema
60
+ - drop_schema
61
+ - create_virtual_table
62
+ - drop_virtual_table
63
+ - table_exists?
64
+ - column_exists?
65
+ - index_exists?
66
+ - index_name_exists?
67
+ - foreign_key_exists?
68
+ - check_constraint_exists?
69
+ - extension_enabled?
70
+ - view_exists?
71
+ - reversible
72
+ - revert
73
+ - up_only
74
+ - say
75
+ - say_with_time
76
+ - suppress_messages
77
+ - disable_ddl_transaction!
78
+ - raise
79
+ - private
80
+ AllowedConstants:
81
+ - ActiveRecord::IrreversibleMigration
82
+ - Rails
83
+
1
84
  Asjer/NoDefaultTranslation:
2
85
  Description: Checks for use of the `default:` option in translation calls.
3
86
  Enabled: true
@@ -82,3 +165,13 @@ Asjer/RailsClassOrder:
82
165
  - has_secure_token
83
166
  - generates_token_for
84
167
  - composed_of
168
+
169
+ Asjer/SystemSpecLoginAs:
170
+ Description: Requires system specs to use a helper that signs in and visits together instead of a bare Warden handoff.
171
+ Enabled: pending
172
+ VersionAdded: "0.6.0"
173
+ Include:
174
+ - 'spec/system/**/*.rb'
175
+ MethodNames:
176
+ - login_as
177
+ PreferredHelper: sign_in_and_visit
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RuboCop
4
4
  module Asjer
5
- VERSION = '0.4.5'
5
+ VERSION = '0.6.0'
6
6
  end
7
7
  end
data/lib/rubocop/asjer.rb CHANGED
@@ -5,7 +5,10 @@ require 'rubocop'
5
5
  require_relative 'asjer/version'
6
6
  require_relative 'asjer/plugin'
7
7
  require_relative 'cop/asjer/no_default_translation'
8
+ require_relative 'cop/asjer/explicit_migration_version_in_spec'
9
+ require_relative 'cop/asjer/migration_schema_only'
8
10
  require_relative 'cop/asjer/rails_class_order'
11
+ require_relative 'cop/asjer/system_spec_login_as'
9
12
 
10
13
  module RuboCop
11
14
  module Asjer
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Asjer
6
+ # Requires migration specs to run migrations to an explicit version.
7
+ #
8
+ # `rollback`, `forward` and `migrate` without a version (or with `nil`) move relative to
9
+ # the latest migration, so the spec breaks as soon as anyone adds an
10
+ # unrelated migration. Name the version the spec is about instead.
11
+ #
12
+ # @example
13
+ # # bad
14
+ # connection.pool.migration_context.rollback(1)
15
+ # migration_context.forward
16
+ # migration_context.migrate
17
+ #
18
+ # # good
19
+ # connection.pool.migration_context.run(:down, 20261012170000)
20
+ # migration_context.migrate(20261012170000)
21
+ #
22
+ class ExplicitMigrationVersionInSpec < Base
23
+ MSG = 'Run migrations to an explicit version, e.g. `migration_context.run(:down, VERSION)`; ' \
24
+ '`%<name>s` depends on the latest migration.'
25
+
26
+ RESTRICT_ON_SEND = %i[rollback forward migrate].freeze
27
+
28
+ def on_send(node)
29
+ return if node.method?(:migrate) && node.arguments? && !node.first_argument.nil_type?
30
+
31
+ add_offense(node.loc.selector, message: format(MSG, name: node.method_name))
32
+ end
33
+ alias on_csend on_send
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,242 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Asjer
6
+ # Default lists for MigrationSchemaOnly.
7
+ module MigrationSchemaOnlyDefaults
8
+ ALLOWED_METHODS = %w[
9
+ create_table create_join_table change_table rename_table drop_table drop_join_table
10
+ add_column remove_column remove_columns rename_column change_column
11
+ change_column_default change_column_null change_column_comment change_table_comment
12
+ add_timestamps remove_timestamps
13
+ add_index remove_index rename_index
14
+ add_reference remove_reference add_belongs_to remove_belongs_to
15
+ add_foreign_key remove_foreign_key
16
+ add_check_constraint remove_check_constraint validate_check_constraint
17
+ validate_foreign_key validate_constraint
18
+ add_exclusion_constraint remove_exclusion_constraint
19
+ add_unique_constraint remove_unique_constraint
20
+ enable_extension disable_extension
21
+ create_enum drop_enum rename_enum add_enum_value rename_enum_value
22
+ create_schema drop_schema
23
+ create_virtual_table drop_virtual_table
24
+ table_exists? column_exists? index_exists? index_name_exists?
25
+ foreign_key_exists? check_constraint_exists? extension_enabled? view_exists?
26
+ reversible revert up_only
27
+ say say_with_time suppress_messages
28
+ disable_ddl_transaction!
29
+ raise private
30
+ ].freeze
31
+
32
+ ALLOWED_CONSTANTS = %w[ActiveRecord::IrreversibleMigration Rails].freeze
33
+
34
+ # Calls that run code chosen at runtime; only block variables such as `t` may receive them.
35
+ DYNAMIC_CALLS = %i[
36
+ send public_send __send__ eval instance_eval instance_exec class_eval class_exec module_eval module_exec
37
+ ].freeze
38
+ end
39
+
40
+ # Checks the options of column changes for SQL casts and row updates.
41
+ module MigrationSchemaOnlyColumnChanges
42
+ CAST_MSG = '`change_column` with `using:` runs a SQL cast; add a new column and copy the data ' \
43
+ 'in a maintenance task.'
44
+ OPTIONS_MSG = 'Write out the `change_column` options so `using:` SQL casts can be checked.'
45
+ NULL_MSG = 'The `change_column_null` replacement value updates existing rows; ' \
46
+ 'backfill them in a maintenance task first.'
47
+
48
+ private
49
+
50
+ # `change_column`, or `t.change` inside a `change_table` block. Any local receiver counts,
51
+ # so an alias such as `table = t` is checked too.
52
+ def column_change?(node)
53
+ node.method?(:change_column) || (node.method?(:change) && node.receiver&.lvar_type?)
54
+ end
55
+
56
+ # The replacement value of `change_column_null`, or of `t.change_null` inside a `change_table` block.
57
+ def null_replacement(node)
58
+ if node.method?(:change_column_null)
59
+ node.arguments[3]
60
+ elsif node.method?(:change_null) && node.receiver&.lvar_type?
61
+ node.arguments[2]
62
+ end
63
+ end
64
+
65
+ def check_null_replacement(replacement)
66
+ add_offense(replacement, message: NULL_MSG) unless replacement.nil? || replacement.nil_type?
67
+ end
68
+
69
+ # Only the options themselves: `using:` inside a JSON default is data, not a cast.
70
+ def check_cast(node)
71
+ node.arguments.select(&:hash_type?).flat_map(&:children).each { |option| check_cast_option(option) }
72
+ end
73
+
74
+ def check_cast_option(option)
75
+ if option.kwsplat_type?
76
+ splatted = option.children.first
77
+ if splatted.hash_type?
78
+ splatted.children.each { |nested| check_cast_option(nested) }
79
+ else
80
+ add_offense(option, message: OPTIONS_MSG)
81
+ end
82
+ elsif option.key.sym_type? && option.key.value == :using
83
+ add_offense(option, message: CAST_MSG)
84
+ end
85
+ end
86
+ end
87
+
88
+ # Allows only schema statements in Rails migrations.
89
+ #
90
+ # Migrations change the schema. Raw SQL, model calls and other data
91
+ # changes skip validations and callbacks, are often irreversible, and
92
+ # belong in a maintenance task. This cop uses an allowlist: inside a
93
+ # migration class, implicit-receiver calls must be schema statements
94
+ # (`AllowedMethods`) or methods the migration defines itself, and only
95
+ # `AllowedConstants` and constants the migration defines may be
96
+ # referenced. Calls on block variables such as `t.string` and
97
+ # `direction.up` are fine. Anything else, including `execute`,
98
+ # `connection`, `send` and model constants, is an offense, so new
99
+ # raw-SQL routes are rejected without updating a denylist.
100
+ #
101
+ # Check constraints and expression indexes stay allowed: they keep the
102
+ # database consistent alongside model validations. `change_column` with
103
+ # `using:` is rejected because it is always a SQL cast.
104
+ #
105
+ # Set `StartAfter` to the version of the last existing migration to
106
+ # check only new migrations.
107
+ #
108
+ # @example
109
+ # # bad
110
+ # execute "UPDATE bookings SET source = 'web'"
111
+ # select_value('SELECT 1')
112
+ # connection.exec_query('SELECT 1')
113
+ # Booking.where(source: nil).update_all(source: 'web')
114
+ # add_index :bookings, Arel.sql('lower(code)')
115
+ # change_column :bookings, :date, :date, using: 'date::date'
116
+ #
117
+ # # good
118
+ # create_table :examples do |t|
119
+ # t.string :status, null: false
120
+ # end
121
+ # add_index :examples, 'lower(status)'
122
+ # add_check_constraint :examples, "status IN ('open', 'closed')", name: :examples_status
123
+ #
124
+ class MigrationSchemaOnly < Base
125
+ include MigrationSchemaOnlyDefaults
126
+ include MigrationSchemaOnlyColumnChanges
127
+
128
+ CALL_MSG = 'Use only schema statements in migrations; move `%<name>s` and other data changes ' \
129
+ 'or raw SQL to a maintenance task.'
130
+ CONSTANT_MSG = 'Do not use `%<name>s` in migrations; move data changes and raw SQL to a maintenance task.'
131
+ SELF_MSG = 'Call schema statements on `self` directly; passing `self` around hides calls from this check.'
132
+
133
+ # @!method migration_class?(node)
134
+ def_node_matcher :migration_class?, <<~PATTERN
135
+ (class _ {
136
+ (send (const (const {nil? cbase} :ActiveRecord) :Migration) :[] ...)
137
+ (const (const {nil? cbase} :ActiveRecord) :Migration)
138
+ (const (const (const {nil? cbase} :ActiveRecord) :Migration) :Current)
139
+ } _)
140
+ PATTERN
141
+
142
+ def on_class(node)
143
+ return unless migration_class?(node) && node.body && checked_file?
144
+
145
+ @defined_methods = node.body.each_node(:def, :defs).map(&:method_name)
146
+ @defined_constants = defined_constants(node)
147
+ node.body.each_node(:send, :csend, :const, :self) { |child| check(child) }
148
+ end
149
+
150
+ private
151
+
152
+ def checked_file?
153
+ start_after = cop_config['StartAfter']
154
+ version = File.basename(processed_source.file_path.to_s)[/\A\d+/]
155
+ start_after.nil? || version.nil? || version.to_i > start_after.to_i
156
+ end
157
+
158
+ # Bare and class-qualified names, such as `CONSTRAINT_NAME` and `Example::CONSTRAINT_NAME`.
159
+ def defined_constants(node)
160
+ names = node.body.each_node(:casgn).filter_map { |casgn| casgn.name.to_s unless casgn.namespace }
161
+ names + names.map { |name| "#{node.identifier.source}::#{name}" }
162
+ end
163
+
164
+ def check(node)
165
+ case node.type
166
+ when :const then check_constant(node)
167
+ when :self then check_self(node)
168
+ else check_call(node)
169
+ end
170
+ end
171
+
172
+ def check_constant(node)
173
+ return if namespace?(node) || reverted_migration?(node) || allowed_constant?(node.source)
174
+
175
+ add_offense(node, message: format(CONSTANT_MSG, name: node.source))
176
+ end
177
+
178
+ # The `ActiveRecord` in `ActiveRecord::Base`; the full constant is checked instead.
179
+ def namespace?(node)
180
+ node.parent&.const_type? && node.parent.namespace.equal?(node)
181
+ end
182
+
183
+ def reverted_migration?(node)
184
+ node.parent&.send_type? && node.parent.method?(:revert)
185
+ end
186
+
187
+ def allowed_constant?(name)
188
+ allowed_constants.include?(name.delete_prefix('::')) || @defined_constants.include?(name)
189
+ end
190
+
191
+ def allowed_constants
192
+ cop_config.fetch('AllowedConstants', ALLOWED_CONSTANTS)
193
+ end
194
+
195
+ def check_self(node)
196
+ return if node.parent&.call_type? && node.parent.receiver.equal?(node)
197
+ # The target of `def self.name` or `class << self`; their bodies are still checked.
198
+ return if node.parent&.type?(:defs, :sclass) && node.parent.children.first.equal?(node)
199
+
200
+ add_offense(node, message: SELF_MSG)
201
+ end
202
+
203
+ def check_call(node)
204
+ if disallowed_call?(node)
205
+ add_offense(node, message: format(CALL_MSG, name: node.method_name))
206
+ elsif column_change?(node)
207
+ check_cast(node)
208
+ elsif (replacement = null_replacement(node))
209
+ check_null_replacement(replacement)
210
+ end
211
+ end
212
+
213
+ def disallowed_call?(node)
214
+ receiver = node.receiver
215
+ if receiver.nil? || receiver.self_type?
216
+ !allowed_call?(node.method_name)
217
+ elsif DYNAMIC_CALLS.include?(node.method_name)
218
+ !block_argument?(receiver)
219
+ else
220
+ receiver.variable? && !receiver.lvar_type?
221
+ end
222
+ end
223
+
224
+ def allowed_call?(name)
225
+ allowed_methods.include?(name.to_s) || @defined_methods.include?(name)
226
+ end
227
+
228
+ def allowed_methods
229
+ cop_config.fetch('AllowedMethods', ALLOWED_METHODS)
230
+ end
231
+
232
+ # A local declared by an enclosing block, such as `t` in `create_table :examples do |t|`.
233
+ # An ordinary local can hold any object, including the migration itself.
234
+ def block_argument?(node)
235
+ node&.lvar_type? && node.each_ancestor(:block).any? do |block|
236
+ block.argument_list.map(&:name).include?(node.children.first)
237
+ end
238
+ end
239
+ end
240
+ end
241
+ end
242
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Asjer
6
+ # Requires system specs to use a helper that signs in and visits together.
7
+ #
8
+ # Warden's handoff applies to the next request reaching the app server,
9
+ # which need not be the browser's intended visit. The application must
10
+ # provide the replacement helper.
11
+ #
12
+ # @example
13
+ # # bad
14
+ # login_as(user)
15
+ # visit dashboard_path
16
+ #
17
+ # # good
18
+ # sign_in_and_visit(user, dashboard_path)
19
+ #
20
+ class SystemSpecLoginAs < Base
21
+ MSG = 'Use `%<helper>s` instead of `%<method>s` in system specs: ' \
22
+ 'the Warden handoff applies to whichever request reaches the app server next, ' \
23
+ "not necessarily the browser's visit."
24
+
25
+ def on_send(node)
26
+ return unless node.receiver.nil?
27
+ return unless cop_config.fetch('MethodNames', ['login_as']).include?(node.method_name.to_s)
28
+
29
+ helper = cop_config.fetch('PreferredHelper', 'sign_in_and_visit')
30
+ add_offense(node.loc.selector, message: format(MSG, helper: helper, method: node.method_name))
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubocop-asjer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.5
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Asjer Querido
@@ -56,8 +56,11 @@ files:
56
56
  - lib/rubocop/asjer.rb
57
57
  - lib/rubocop/asjer/plugin.rb
58
58
  - lib/rubocop/asjer/version.rb
59
+ - lib/rubocop/cop/asjer/explicit_migration_version_in_spec.rb
60
+ - lib/rubocop/cop/asjer/migration_schema_only.rb
59
61
  - lib/rubocop/cop/asjer/no_default_translation.rb
60
62
  - lib/rubocop/cop/asjer/rails_class_order.rb
63
+ - lib/rubocop/cop/asjer/system_spec_login_as.rb
61
64
  - mise.toml
62
65
  - release-please-config.json
63
66
  - sig/rubocop/i18n.rbs