rubocop-asjer 0.4.0 → 0.4.2

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: 772cbb0491de18ffeb19b27e41336474005b2c9295f5f2c152c842883e250654
4
- data.tar.gz: f2b5e866ba2bccd3fee26d59868f80b8f12de62e57b161e203e932f8aa70533f
3
+ metadata.gz: 447e874612f95f1d94bce6efa0da2fcfe9e06c1b72f624fb707ddc147c5280c3
4
+ data.tar.gz: a815ca7e0101b2e2d1bac4bfed12718ef10e95da304dfafa83c2bc74ef10b094
5
5
  SHA512:
6
- metadata.gz: 891112aad96477909e4a2de0a24660655b5573b6ef8f837cd70fe84927502390a3544f47994a9644b4d5b12b826aba74c8ab09f73dde3d49b044bb5fa6cc62cf
7
- data.tar.gz: f6c1dd35c8a2fe82392a38576b5c26af9b4958ad732d35493d4ae8e35cd0f423610b4263ff80909ce31c99f28d044a979eb8c0d124a5d4738777f984dc753fb6
6
+ metadata.gz: 59c42f2eb2390c5356d9afbe628a48652707c9ff3527306e2e83c29e48e9fbd04f1cfefd01f0d6b7d6da06198777d4a31721f2cce51319652ba24325d93a71a7
7
+ data.tar.gz: f0486413521d14aded52af6e496addd3f015c44ca832e393bb445b127c065f82e0aa0d3e906dd72bcda71439f5a451fbf68729afb89dec09182e7de06639d81a
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.4.0"
2
+ ".": "0.4.2"
3
3
  }
data/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.2](https://github.com/asjer/rubocop-asjer/compare/v0.4.1...v0.4.2) (2026-01-28)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * add missing rails classes to `RailsClassOrder` cop configuration ([0ddc16a](https://github.com/asjer/rubocop-asjer/commit/0ddc16a165530b0aa5c4e0d37195324779fca9a8))
9
+
10
+ ## [0.4.1](https://github.com/asjer/rubocop-asjer/compare/v0.4.0...v0.4.1) (2026-01-28)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * improve autocorrect functionality for `RailsClassOrder` cop and add Ruby 4.0 to ci ([#23](https://github.com/asjer/rubocop-asjer/issues/23)) ([16fc637](https://github.com/asjer/rubocop-asjer/commit/16fc63770f05f74e2bc005becd136af7d8ccb0d1))
16
+
3
17
  ## [0.4.0](https://github.com/asjer/rubocop-asjer/compare/v0.3.1...v0.4.0) (2026-01-23)
4
18
 
5
19
 
data/config/default.yml CHANGED
@@ -9,33 +9,71 @@ Asjer/RailsClassOrder:
9
9
  VersionAdded: "0.4.0"
10
10
  Include:
11
11
  - 'app/models/**/*.rb'
12
+ Scopes:
13
+ - default_scope
14
+ - scope
15
+ Attributes:
16
+ - attr_accessor
17
+ - attr_reader
18
+ - attr_writer
19
+ - attr_readonly
20
+ - attribute
21
+ - serialize
22
+ - store
23
+ - store_accessor
24
+ Enums:
25
+ - enum
12
26
  Associations:
13
27
  - belongs_to
14
- - has_many
15
28
  - has_one
29
+ - has_many
16
30
  - has_and_belongs_to_many
31
+ - has_one_attached
32
+ - has_many_attached
33
+ Validations:
34
+ - validates
35
+ - validates_acceptance_of
36
+ - validates_associated
37
+ - validates_comparison_of
38
+ - validates_confirmation_of
39
+ - validates_each
40
+ - validates_exclusion_of
41
+ - validates_format_of
42
+ - validates_inclusion_of
43
+ - validates_length_of
44
+ - validates_size_of
45
+ - validates_numericality_of
46
+ - validates_presence_of
47
+ - validates_uniqueness_of
48
+ - validates_with
49
+ - validate
17
50
  Callbacks:
18
51
  - after_initialize
19
52
  - after_find
20
53
  - after_touch
21
54
  - before_validation
22
- - validates
23
- - validate
24
55
  - after_validation
25
56
  - before_save
26
57
  - around_save
27
58
  - before_create
28
59
  - around_create
60
+ - after_create
29
61
  - before_update
30
62
  - around_update
63
+ - after_update
64
+ - after_save
31
65
  - before_destroy
32
66
  - around_destroy
33
67
  - after_destroy
34
- - after_update
35
- - after_create
36
- - after_save
37
68
  - after_commit
38
69
  - after_rollback
39
70
  Others:
40
- - attr_readonly
41
- - serialize
71
+ - encrypts
72
+ - normalizes
73
+ - delegate
74
+ - delegate_missing_to
75
+ - accepts_nested_attributes_for
76
+ - has_secure_password
77
+ - has_secure_token
78
+ - generates_token_for
79
+ - composed_of
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RuboCop
4
4
  module Asjer
5
- VERSION = '0.4.0'
5
+ VERSION = '0.4.2'
6
6
  end
7
7
  end
@@ -5,11 +5,12 @@ module RuboCop
5
5
  module Asjer
6
6
  # Enforces consistent ordering of declarative methods in Rails models.
7
7
  #
8
- # Methods are grouped into three categories: associations, callbacks,
9
- # and others. Within each category, methods are sorted by their position
10
- # in the configured list. Groups are separated by blank lines.
8
+ # Methods are grouped into seven categories following Rails Style Guide:
9
+ # scopes, attributes, enums, associations, validations, callbacks, and others.
10
+ # Within each category, methods are sorted by their position in the configured list.
11
+ # Groups are separated by blank lines.
11
12
  #
12
- # The order is: associations, then callbacks, then others.
13
+ # The order is: scopes, attributes, enums, associations, validations, callbacks, then others.
13
14
  #
14
15
  # @example
15
16
  # # bad
@@ -18,7 +19,9 @@ module RuboCop
18
19
  # validate :validate_name
19
20
  # after_create :after_create_1
20
21
  # has_many :messages
22
+ # scope :active, -> { where(active: true) }
21
23
  # attr_readonly :email
24
+ # enum :status, [:pending, :active]
22
25
  # after_create :after_create_2
23
26
  # belongs_to :role
24
27
  # before_create :set_name
@@ -26,34 +29,126 @@ module RuboCop
26
29
  #
27
30
  # # good
28
31
  # class User < ApplicationRecord
32
+ # scope :active, -> { where(active: true) }
33
+ #
34
+ # attr_readonly :email
35
+ #
36
+ # enum :status, [:pending, :active]
37
+ #
29
38
  # belongs_to :plan
30
39
  # belongs_to :role
31
40
  # has_many :messages
32
41
  #
33
42
  # validate :validate_name
43
+ #
34
44
  # before_create :set_name
35
45
  # after_create :after_create_1
36
46
  # after_create :after_create_2
37
- #
38
- # attr_readonly :email
39
47
  # end
40
48
  #
41
49
  # Default method lists for RailsClassOrder cop
42
50
  module RailsClassOrderDefaults
51
+ SCOPES = %w[default_scope scope].freeze
52
+
53
+ ATTRIBUTES = %w[
54
+ attr_accessor attr_reader attr_writer attr_readonly
55
+ attribute serialize store store_accessor
56
+ ].freeze
57
+
58
+ ENUMS = %w[enum].freeze
59
+
43
60
  ASSOCIATIONS = %w[
44
- belongs_to has_many has_one has_and_belongs_to_many
61
+ belongs_to has_one has_many has_and_belongs_to_many
62
+ has_one_attached has_many_attached
63
+ ].freeze
64
+
65
+ VALIDATIONS = %w[
66
+ validates validates_acceptance_of validates_associated
67
+ validates_comparison_of validates_confirmation_of validates_each
68
+ validates_exclusion_of validates_format_of validates_inclusion_of
69
+ validates_length_of validates_size_of validates_numericality_of
70
+ validates_presence_of validates_uniqueness_of validates_with
71
+ validate
45
72
  ].freeze
46
73
 
47
74
  CALLBACKS = %w[
48
75
  after_initialize after_find after_touch
49
- before_validation validates validate after_validation
50
- before_save around_save before_create around_create
51
- before_update around_update before_destroy around_destroy
52
- after_destroy after_update after_create after_save
76
+ before_validation after_validation
77
+ before_save around_save
78
+ before_create around_create after_create
79
+ before_update around_update after_update
80
+ after_save
81
+ before_destroy around_destroy after_destroy
53
82
  after_commit after_rollback
54
83
  ].freeze
55
84
 
56
- OTHERS = %w[attr_readonly serialize].freeze
85
+ OTHERS = %w[
86
+ encrypts normalizes delegate delegate_missing_to
87
+ accepts_nested_attributes_for has_secure_password
88
+ has_secure_token generates_token_for composed_of
89
+ ].freeze
90
+ end
91
+
92
+ # Autocorrect helpers for RailsClassOrder cop
93
+ module RailsClassOrderCorrector
94
+ def autocorrect(corrector, body, original, sorted)
95
+ first_target = original.min_by { |m| body.children.index(m) }
96
+ new_source = build_sorted_source(sorted, original)
97
+ corrector.replace(range_with_comments(first_target), new_source.rstrip)
98
+
99
+ (original - [first_target]).each do |method|
100
+ corrector.remove(full_method_range(method))
101
+ end
102
+ end
103
+
104
+ def range_with_comments(node)
105
+ comments = preceding_comments(node)
106
+ start_pos = comments.empty? ? node.loc.expression.begin_pos : comments.first.loc.expression.begin_pos
107
+ range_between(start_pos, node.loc.expression.end_pos)
108
+ end
109
+
110
+ def preceding_comments(node)
111
+ collect_adjacent_comments(node.loc.expression)
112
+ end
113
+
114
+ def collect_adjacent_comments(node_pos)
115
+ expected_line = node_pos.first_line - 1
116
+ comments_before_node(node_pos).take_while do |comment|
117
+ pos = comment.loc.expression
118
+ (pos.last_line == expected_line).tap { expected_line = pos.first_line - 1 }
119
+ end.reverse
120
+ end
121
+
122
+ def comments_before_node(node_pos)
123
+ processed_source.comments.select { |c| c.loc.expression.end_pos < node_pos.begin_pos }.reverse
124
+ end
125
+
126
+ def full_method_range(node)
127
+ range = range_with_comments(node)
128
+ source = processed_source.buffer.source
129
+ line_start = source.rindex("\n", range.begin_pos - 1)&.+(1) || 0
130
+ end_pos = source[range.end_pos] == "\n" ? range.end_pos + 1 : range.end_pos
131
+ range_between(line_start, end_pos)
132
+ end
133
+
134
+ def build_sorted_source(sorted, original)
135
+ indent = ' ' * original.first.loc.column
136
+ grouped = sorted.group_by { |m| method_type(m) }
137
+ format_grouped_source(grouped, indent)
138
+ end
139
+
140
+ def format_grouped_source(grouped, indent)
141
+ self.class::TYPE_ORDER.keys.filter_map do |type|
142
+ next unless grouped[type]&.any?
143
+
144
+ grouped[type].map { |m| source_with_comments(m) }.join("\n#{indent}")
145
+ end.join("\n\n#{indent}")
146
+ end
147
+
148
+ def source_with_comments(node)
149
+ range = range_with_comments(node)
150
+ processed_source.buffer.source[range.begin_pos...range.end_pos].lstrip
151
+ end
57
152
  end
58
153
 
59
154
  # Enforces consistent ordering of declarative methods in Rails models.
@@ -62,66 +157,79 @@ module RuboCop
62
157
  class RailsClassOrder < Base
63
158
  extend AutoCorrector
64
159
  include RangeHelp
65
-
66
- MSG = 'Declarative methods should be sorted by type: associations, callbacks, then others.'
67
-
68
- TYPE_ORDER = { association: 0, callback: 1, other: 2 }.freeze
160
+ include RailsClassOrderCorrector
161
+
162
+ MSG = 'Declarative methods should be sorted by type: scopes, attributes, enums, ' \
163
+ 'associations, validations, callbacks, then others.'
164
+
165
+ TYPE_ORDER = {
166
+ scope: 0,
167
+ attribute: 1,
168
+ enum: 2,
169
+ association: 3,
170
+ validation: 4,
171
+ callback: 5,
172
+ other: 6
173
+ }.freeze
174
+
175
+ CATEGORY_CONFIG = {
176
+ scope: { key: 'Scopes', const: :SCOPES },
177
+ attribute: { key: 'Attributes', const: :ATTRIBUTES },
178
+ enum: { key: 'Enums', const: :ENUMS },
179
+ association: { key: 'Associations', const: :ASSOCIATIONS },
180
+ validation: { key: 'Validations', const: :VALIDATIONS },
181
+ callback: { key: 'Callbacks', const: :CALLBACKS },
182
+ other: { key: 'Others', const: :OTHERS }
183
+ }.freeze
69
184
 
70
185
  def on_class(node)
71
186
  _name, _superclass, body = *node
72
187
  return unless body&.begin_type?
73
188
 
189
+ check_order(body)
190
+ end
191
+
192
+ private
193
+
194
+ def check_order(body)
74
195
  targets = target_methods(body)
75
196
  return if targets.empty?
76
197
 
77
198
  sorted = sort_methods(targets)
78
199
  return if targets == sorted
79
200
 
80
- add_offense(body) do |corrector|
81
- autocorrect(corrector, body, targets, sorted) if contiguous?(body, targets)
82
- end
201
+ first_misplaced = targets.zip(sorted).find { |a, e| a != e }&.first
202
+ add_offense(first_misplaced) { |corrector| autocorrect(corrector, body, targets, sorted) }
83
203
  end
84
204
 
85
- private
86
-
87
- def associations
88
- @associations ||= cop_config.fetch('Associations', RailsClassOrderDefaults::ASSOCIATIONS).map(&:to_sym)
89
- end
90
-
91
- def callbacks
92
- @callbacks ||= cop_config.fetch('Callbacks', RailsClassOrderDefaults::CALLBACKS).map(&:to_sym)
93
- end
94
-
95
- def others
96
- @others ||= cop_config.fetch('Others', RailsClassOrderDefaults::OTHERS).map(&:to_sym)
205
+ def category_methods(category)
206
+ cfg = CATEGORY_CONFIG[category]
207
+ instance_variable_get(:"@#{category}") ||
208
+ instance_variable_set(
209
+ :"@#{category}",
210
+ cop_config.fetch(cfg[:key], RailsClassOrderDefaults.const_get(cfg[:const])).map(&:to_sym)
211
+ )
97
212
  end
98
213
 
99
214
  def all_target_methods
100
- @all_target_methods ||= associations + callbacks + others
215
+ @all_target_methods ||= TYPE_ORDER.keys.flat_map { |cat| category_methods(cat) }
101
216
  end
102
217
 
103
218
  def target_methods(body)
104
- body.children.select do |child|
105
- child.send_type? && all_target_methods.include?(child.method_name)
106
- end
219
+ body.children.select { |child| child.send_type? && all_target_methods.include?(child.method_name) }
107
220
  end
108
221
 
109
222
  def sort_methods(methods)
110
- # Use sort_by with index to make stable sort (preserve original order for equal elements)
111
223
  methods.each_with_index.sort_by do |method, index|
112
- [
113
- method_type_order(method),
114
- method_position_in_type(method),
115
- index
116
- ]
224
+ [method_type_order(method), method_position_in_type(method), index]
117
225
  end.map(&:first)
118
226
  end
119
227
 
120
228
  def method_type(method)
121
229
  name = method.method_name
122
- return :association if associations.include?(name)
123
- return :callback if callbacks.include?(name)
124
-
230
+ TYPE_ORDER.each_key do |category|
231
+ return category if category_methods(category).include?(name)
232
+ end
125
233
  :other
126
234
  end
127
235
 
@@ -130,50 +238,8 @@ module RuboCop
130
238
  end
131
239
 
132
240
  def method_position_in_type(method)
133
- name = method.method_name
134
- list = method_list_for_type(method_type(method))
135
- list.index(name) || list.size
136
- end
137
-
138
- def method_list_for_type(type)
139
- { association: associations, callback: callbacks, other: others }[type]
140
- end
141
-
142
- def contiguous?(body, targets)
143
- indices = targets.map { |t| body.children.index(t) }
144
- indices.max - indices.min + 1 == indices.size
145
- end
146
-
147
- def autocorrect(corrector, _body, original, sorted)
148
- grouped = group_by_type(sorted)
149
- new_source = build_grouped_source(grouped, original)
150
- range = methods_range(original)
151
- corrector.replace(range, new_source)
152
- end
153
-
154
- def group_by_type(methods)
155
- methods.group_by { |m| method_type(m) }
156
- end
157
-
158
- def build_grouped_source(grouped, original)
159
- indent = ' ' * original.first.loc.column
160
-
161
- groups = []
162
- %i[association callback other].each do |type|
163
- next unless grouped[type]&.any?
164
-
165
- group_lines = grouped[type].map(&:source)
166
- groups << group_lines.join("\n#{indent}")
167
- end
168
-
169
- groups.join("\n\n#{indent}")
170
- end
171
-
172
- def methods_range(methods)
173
- first = methods.min_by { |m| m.loc.expression.begin_pos }
174
- last = methods.max_by { |m| m.loc.expression.end_pos }
175
-
176
- range_between(first.loc.expression.begin_pos, last.loc.expression.end_pos)
241
+ list = category_methods(method_type(method))
242
+ list.index(method.method_name) || list.size
177
243
  end
178
244
  end
179
245
  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.0
4
+ version: 0.4.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Asjer Querido