rubocop-asjer 0.4.4 → 0.5.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 +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +14 -0
- data/README.md +65 -0
- data/config/default.yml +83 -0
- data/lib/rubocop/asjer/version.rb +1 -1
- data/lib/rubocop/asjer.rb +2 -0
- data/lib/rubocop/cop/asjer/explicit_migration_version_in_spec.rb +37 -0
- data/lib/rubocop/cop/asjer/migration_schema_only.rb +242 -0
- data/lib/rubocop/cop/asjer/rails_class_order.rb +71 -11
- data/mise.toml +2 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c77e721d22becd80eb1bbaccc3500469cd8212a4da6c7fd11f44a09cabe8aeb8
|
|
4
|
+
data.tar.gz: f5d7b66942be7be87b03f40c77e6464580634bac15962c78472f570cf1c836f8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3fe9c2037c36e265dfb0a175caef40d1132dc4c0eb50b8e70cd47d3cc4b8479196e32eab6ef46f5ed408c0909de1c2f74f52979da414eab3900384b10917503a
|
|
7
|
+
data.tar.gz: 0357f2dfb8e2495a44a756f93e80654a7f0533d085b3e814ba6de18cb80912b1b23689e6e7bdea96a319d620fcf73ad71b8f1ec1763c46a1b0848d85150b33d2
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.0](https://github.com/asjer/rubocop-asjer/compare/v0.4.5...v0.5.0) (2026-10-15)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* add MigrationSchemaOnly and ExplicitMigrationVersionInSpec cops ([316938a](https://github.com/asjer/rubocop-asjer/commit/316938ae96076d82458186759c2f341b2a44849f)), closes [#54](https://github.com/asjer/rubocop-asjer/issues/54)
|
|
9
|
+
|
|
10
|
+
## [0.4.5](https://github.com/asjer/rubocop-asjer/compare/v0.4.4...v0.4.5) (2026-03-08)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* **rails_class_order:** prevent autocorrect from deleting interleaved non-target lines ([f7f1628](https://github.com/asjer/rubocop-asjer/commit/f7f162842e2188e99b24a53e55650bcaa678253f))
|
|
16
|
+
|
|
3
17
|
## [0.4.4](https://github.com/asjer/rubocop-asjer/compare/v0.4.3...v0.4.4) (2026-02-10)
|
|
4
18
|
|
|
5
19
|
|
data/README.md
CHANGED
|
@@ -96,6 +96,71 @@ 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
|
+
|
|
99
164
|
## Development
|
|
100
165
|
|
|
101
166
|
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
|
data/lib/rubocop/asjer.rb
CHANGED
|
@@ -5,6 +5,8 @@ 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'
|
|
9
11
|
|
|
10
12
|
module RuboCop
|
|
@@ -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
|
|
@@ -93,24 +93,37 @@ module RuboCop
|
|
|
93
93
|
# Autocorrect helpers for RailsClassOrder cop
|
|
94
94
|
module RailsClassOrderCorrector
|
|
95
95
|
def autocorrect(corrector, body, original, sorted)
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
96
|
+
ordered_targets = ordered_target_nodes(original)
|
|
97
|
+
corrector.replace(
|
|
98
|
+
correction_range_for(ordered_targets),
|
|
99
|
+
rebuilt_source(
|
|
100
|
+
sorted,
|
|
101
|
+
ordered_targets,
|
|
102
|
+
trailing_sibling: trailing_sibling?(body, ordered_targets.last)
|
|
103
|
+
)
|
|
104
|
+
)
|
|
105
|
+
end
|
|
99
106
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
107
|
+
def ordered_target_nodes(original) = original.sort_by { |method| full_method_range(method).begin_pos }
|
|
108
|
+
|
|
109
|
+
def correction_range_for(ordered_targets)
|
|
110
|
+
# Stop at the last expression boundary so any newline/blank-line
|
|
111
|
+
# suffix after the final target stays in the surrounding source.
|
|
112
|
+
range_between(
|
|
113
|
+
range_with_comments(ordered_targets.first).begin_pos,
|
|
114
|
+
range_with_comments(ordered_targets.last).end_pos
|
|
115
|
+
)
|
|
103
116
|
end
|
|
104
117
|
|
|
118
|
+
def trailing_sibling?(body, last_target) = body.children.index(last_target) < body.children.size - 1
|
|
119
|
+
|
|
105
120
|
def range_with_comments(node)
|
|
106
121
|
comments = preceding_comments(node)
|
|
107
122
|
start_pos = comments.empty? ? node.loc.expression.begin_pos : comments.first.loc.expression.begin_pos
|
|
108
123
|
range_between(start_pos, node.loc.expression.end_pos)
|
|
109
124
|
end
|
|
110
125
|
|
|
111
|
-
def preceding_comments(node)
|
|
112
|
-
collect_adjacent_comments(node.loc.expression)
|
|
113
|
-
end
|
|
126
|
+
def preceding_comments(node) = collect_adjacent_comments(node.loc.expression)
|
|
114
127
|
|
|
115
128
|
def collect_adjacent_comments(node_pos)
|
|
116
129
|
expected_line = node_pos.first_line - 1
|
|
@@ -144,12 +157,59 @@ module RuboCop
|
|
|
144
157
|
pos
|
|
145
158
|
end
|
|
146
159
|
|
|
147
|
-
def build_sorted_source(sorted,
|
|
148
|
-
indent = ' ' *
|
|
160
|
+
def build_sorted_source(sorted, reference_targets)
|
|
161
|
+
indent = ' ' * reference_targets.first.loc.column
|
|
149
162
|
grouped = sorted.group_by { |m| method_type(m) }
|
|
150
163
|
format_grouped_source(grouped, indent)
|
|
151
164
|
end
|
|
152
165
|
|
|
166
|
+
def rebuilt_source(sorted, ordered_targets, trailing_sibling:)
|
|
167
|
+
preserved = preserved_interstitial_source(ordered_targets)
|
|
168
|
+
rebuilt = [
|
|
169
|
+
build_sorted_source(sorted, ordered_targets).rstrip,
|
|
170
|
+
preserved
|
|
171
|
+
].reject(&:empty?).join("\n\n")
|
|
172
|
+
trimmed = trim_trailing_newlines(rebuilt)
|
|
173
|
+
|
|
174
|
+
# Keep end-of-class output flush unless the final preserved chunk
|
|
175
|
+
# already carried a trailing blank line before it was moved.
|
|
176
|
+
return trimmed if trailing_sibling || !trailing_blank_line?(preserved)
|
|
177
|
+
|
|
178
|
+
"#{trimmed}\n"
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def trim_trailing_newlines(source)
|
|
182
|
+
trimmed = source.dup
|
|
183
|
+
trimmed.chop! while trimmed.end_with?("\n")
|
|
184
|
+
trimmed
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def trailing_blank_line?(source)
|
|
188
|
+
return false unless source&.end_with?("\n")
|
|
189
|
+
|
|
190
|
+
source[(source.rindex("\n", source.length - 2)&.+(1) || 0)...-1].strip.empty?
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def preserved_interstitial_source(ordered_targets)
|
|
194
|
+
source = processed_source.buffer.source
|
|
195
|
+
|
|
196
|
+
ordered_targets
|
|
197
|
+
.map { |method| full_method_range(method) }
|
|
198
|
+
.each_cons(2)
|
|
199
|
+
.filter_map do |left, right|
|
|
200
|
+
normalize_interstitial_chunk(source[left.end_pos...right.begin_pos])
|
|
201
|
+
end
|
|
202
|
+
.join
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def normalize_interstitial_chunk(chunk)
|
|
206
|
+
return if chunk.nil? || chunk.strip.empty?
|
|
207
|
+
|
|
208
|
+
# Strip leading blank lines only; trailing ones stay attached so later
|
|
209
|
+
# preserved chunks keep their separator spacing.
|
|
210
|
+
chunk.sub(/\A(?:[ \t]*\n)+/, '')
|
|
211
|
+
end
|
|
212
|
+
|
|
153
213
|
def format_grouped_source(grouped, indent)
|
|
154
214
|
self.class::TYPE_ORDER.keys.filter_map do |type|
|
|
155
215
|
next unless grouped[type]&.any?
|
data/mise.toml
ADDED
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
|
+
version: 0.5.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
|
+
- mise.toml
|
|
61
64
|
- release-please-config.json
|
|
62
65
|
- sig/rubocop/i18n.rbs
|
|
63
66
|
homepage: https://github.com/asjer/rubocop-asjer
|