rubocop-asjer 0.3.1 → 0.4.1

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: a228c899c0ddc18cd12cc18f5d43dced28ade2a8e230b61db7d2b4eff348671b
4
- data.tar.gz: 3bee7420da10095a5d2c2eb4cbbde60f3fbb787f46bf73875288da5a07eb4372
3
+ metadata.gz: 6401e9b5d10c63a9c3ff7f3292aac36e8368a54fdd4df3062efda177ee331c11
4
+ data.tar.gz: a7e90b0a705e59b161b575fdedb087d82b54dd8af3cfec7509bc54553012c5f5
5
5
  SHA512:
6
- metadata.gz: c63346eb316976ed2c79a48d52354f55b533d2c45aff4b9ebecb1989a5f023b2456a9078bc7ce92fd29f774960bb616da32841f9280c48424fc81005c66bf9ca
7
- data.tar.gz: 9dd4c7226b6f413d716a5cb4687d6233f301284d21fb1c9c9e67850a92ed5f394b685fbe2518712573d261ece51fb7e3517ffa7dbfc9327030efa1923cf50787
6
+ metadata.gz: a0f223e4e32b83736f21750b9ebb06ab49eacd9e2ccb45d37f047b231d763cb4f4dd1effa7bb87e0376f04db89287b48a7727c544b84b96585b657a02d2fa092
7
+ data.tar.gz: '0810af3f8f3b56e7ae3b4ee4295805a9a01ee0cb34911da8d6197db60ff1d0f8ad3a37d596d8411ced98e2eae0fb13c60f904903bebea26b2302b52cdc66d3de'
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.3.1"
2
+ ".": "0.4.1"
3
3
  }
data/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.1](https://github.com/asjer/rubocop-asjer/compare/v0.4.0...v0.4.1) (2026-01-28)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * 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))
9
+
10
+ ## [0.4.0](https://github.com/asjer/rubocop-asjer/compare/v0.3.1...v0.4.0) (2026-01-23)
11
+
12
+
13
+ ### Features
14
+
15
+ * add `RailsClassOrder` cop to enforce method ordering in Rails models ([#20](https://github.com/asjer/rubocop-asjer/issues/20)) ([bce5d9b](https://github.com/asjer/rubocop-asjer/commit/bce5d9b2affbf207ad34c99d574340d7437e3a04))
16
+
3
17
  ## [0.3.1](https://github.com/asjer/rubocop-asjer/compare/v0.3.0...v0.3.1) (2026-01-03)
4
18
 
5
19
 
data/README.md CHANGED
@@ -44,6 +44,58 @@ t('some.key')
44
44
  I18n.t('some.key')
45
45
  ```
46
46
 
47
+ ### Asjer/RailsClassOrder
48
+
49
+ Enforces consistent ordering of declarative methods in Rails models. Methods are grouped into three categories: associations, callbacks, and others. Groups are separated by blank lines.
50
+
51
+ **Supports autocorrect** with `rubocop -a` or `rubocop -A`.
52
+
53
+ ```ruby
54
+ # bad
55
+ class User < ApplicationRecord
56
+ belongs_to :plan
57
+ validate :validate_name
58
+ after_create :after_create_1
59
+ has_many :messages
60
+ attr_readonly :email
61
+ after_create :after_create_2
62
+ belongs_to :role
63
+ before_create :set_name
64
+ end
65
+
66
+ # good
67
+ class User < ApplicationRecord
68
+ belongs_to :plan
69
+ belongs_to :role
70
+ has_many :messages
71
+
72
+ validate :validate_name
73
+ before_create :set_name
74
+ after_create :after_create_1
75
+ after_create :after_create_2
76
+
77
+ attr_readonly :email
78
+ end
79
+ ```
80
+
81
+ By default, this cop only runs on files matching `app/models/**/*.rb`. The method lists are fully configurable:
82
+
83
+ ```yaml
84
+ Asjer/RailsClassOrder:
85
+ Associations:
86
+ - belongs_to
87
+ - has_many
88
+ - has_one
89
+ - has_and_belongs_to_many
90
+ Callbacks:
91
+ - after_initialize
92
+ - after_find
93
+ # ... (see config/default.yml for full list)
94
+ Others:
95
+ - attr_readonly
96
+ - serialize
97
+ ```
98
+
47
99
  ## Development
48
100
 
49
101
  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
@@ -2,3 +2,40 @@ Asjer/NoDefaultTranslation:
2
2
  Description: Checks for use of the `default:` option in translation calls.
3
3
  Enabled: true
4
4
  VersionAdded: "0.1.0"
5
+
6
+ Asjer/RailsClassOrder:
7
+ Description: Enforces consistent ordering of declarative methods in Rails models.
8
+ Enabled: true
9
+ VersionAdded: "0.4.0"
10
+ Include:
11
+ - 'app/models/**/*.rb'
12
+ Associations:
13
+ - belongs_to
14
+ - has_many
15
+ - has_one
16
+ - has_and_belongs_to_many
17
+ Callbacks:
18
+ - after_initialize
19
+ - after_find
20
+ - after_touch
21
+ - before_validation
22
+ - validates
23
+ - validate
24
+ - after_validation
25
+ - before_save
26
+ - around_save
27
+ - before_create
28
+ - around_create
29
+ - before_update
30
+ - around_update
31
+ - before_destroy
32
+ - around_destroy
33
+ - after_destroy
34
+ - after_update
35
+ - after_create
36
+ - after_save
37
+ - after_commit
38
+ - after_rollback
39
+ Others:
40
+ - attr_readonly
41
+ - serialize
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RuboCop
4
4
  module Asjer
5
- VERSION = '0.3.1'
5
+ VERSION = '0.4.1'
6
6
  end
7
7
  end
data/lib/rubocop/asjer.rb CHANGED
@@ -5,6 +5,7 @@ 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/rails_class_order'
8
9
 
9
10
  module RuboCop
10
11
  module Asjer
@@ -0,0 +1,195 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Asjer
6
+ # Enforces consistent ordering of declarative methods in Rails models.
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.
11
+ #
12
+ # The order is: associations, then callbacks, then others.
13
+ #
14
+ # @example
15
+ # # bad
16
+ # class User < ApplicationRecord
17
+ # belongs_to :plan
18
+ # validate :validate_name
19
+ # after_create :after_create_1
20
+ # has_many :messages
21
+ # attr_readonly :email
22
+ # after_create :after_create_2
23
+ # belongs_to :role
24
+ # before_create :set_name
25
+ # end
26
+ #
27
+ # # good
28
+ # class User < ApplicationRecord
29
+ # belongs_to :plan
30
+ # belongs_to :role
31
+ # has_many :messages
32
+ #
33
+ # validate :validate_name
34
+ # before_create :set_name
35
+ # after_create :after_create_1
36
+ # after_create :after_create_2
37
+ #
38
+ # attr_readonly :email
39
+ # end
40
+ #
41
+ # Default method lists for RailsClassOrder cop
42
+ module RailsClassOrderDefaults
43
+ ASSOCIATIONS = %w[
44
+ belongs_to has_many has_one has_and_belongs_to_many
45
+ ].freeze
46
+
47
+ CALLBACKS = %w[
48
+ 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
53
+ after_commit after_rollback
54
+ ].freeze
55
+
56
+ OTHERS = %w[attr_readonly serialize].freeze
57
+ end
58
+
59
+ # Autocorrect helpers for RailsClassOrder cop
60
+ module RailsClassOrderCorrector
61
+ def autocorrect(corrector, body, original, sorted)
62
+ first_target = original.min_by { |m| body.children.index(m) }
63
+ new_source = build_sorted_source(sorted, original)
64
+ corrector.replace(range_with_comments(first_target), new_source.rstrip)
65
+
66
+ (original - [first_target]).each do |method|
67
+ corrector.remove(full_method_range(method))
68
+ end
69
+ end
70
+
71
+ def range_with_comments(node)
72
+ comments = preceding_comments(node)
73
+ start_pos = comments.empty? ? node.loc.expression.begin_pos : comments.first.loc.expression.begin_pos
74
+ range_between(start_pos, node.loc.expression.end_pos)
75
+ end
76
+
77
+ def preceding_comments(node)
78
+ collect_adjacent_comments(node.loc.expression)
79
+ end
80
+
81
+ def collect_adjacent_comments(node_pos)
82
+ expected_line = node_pos.first_line - 1
83
+ comments_before_node(node_pos).take_while do |comment|
84
+ pos = comment.loc.expression
85
+ (pos.last_line == expected_line).tap { expected_line = pos.first_line - 1 }
86
+ end.reverse
87
+ end
88
+
89
+ def comments_before_node(node_pos)
90
+ processed_source.comments.select { |c| c.loc.expression.end_pos < node_pos.begin_pos }.reverse
91
+ end
92
+
93
+ def full_method_range(node)
94
+ range = range_with_comments(node)
95
+ source = processed_source.buffer.source
96
+ line_start = source.rindex("\n", range.begin_pos - 1)&.+(1) || 0
97
+ end_pos = source[range.end_pos] == "\n" ? range.end_pos + 1 : range.end_pos
98
+ range_between(line_start, end_pos)
99
+ end
100
+
101
+ def build_sorted_source(sorted, original)
102
+ indent = ' ' * original.first.loc.column
103
+ grouped = sorted.group_by { |m| method_type(m) }
104
+
105
+ %i[association callback other].filter_map do |type|
106
+ next unless grouped[type]&.any?
107
+
108
+ grouped[type].map { |m| source_with_comments(m) }.join("\n#{indent}")
109
+ end.join("\n\n#{indent}")
110
+ end
111
+
112
+ def source_with_comments(node)
113
+ range = range_with_comments(node)
114
+ processed_source.buffer.source[range.begin_pos...range.end_pos].lstrip
115
+ end
116
+ end
117
+
118
+ # Enforces consistent ordering of declarative methods in Rails models.
119
+ #
120
+ # @see RailsClassOrderDefaults for default method lists
121
+ class RailsClassOrder < Base
122
+ extend AutoCorrector
123
+ include RangeHelp
124
+ include RailsClassOrderCorrector
125
+
126
+ MSG = 'Declarative methods should be sorted by type: associations, callbacks, then others.'
127
+ TYPE_ORDER = { association: 0, callback: 1, other: 2 }.freeze
128
+
129
+ def on_class(node)
130
+ _name, _superclass, body = *node
131
+ return unless body&.begin_type?
132
+
133
+ check_order(body)
134
+ end
135
+
136
+ private
137
+
138
+ def check_order(body)
139
+ targets = target_methods(body)
140
+ return if targets.empty?
141
+
142
+ sorted = sort_methods(targets)
143
+ return if targets == sorted
144
+
145
+ first_misplaced = targets.zip(sorted).find { |a, e| a != e }&.first
146
+ add_offense(first_misplaced) { |corrector| autocorrect(corrector, body, targets, sorted) }
147
+ end
148
+
149
+ def associations
150
+ @associations ||= cop_config.fetch('Associations', RailsClassOrderDefaults::ASSOCIATIONS).map(&:to_sym)
151
+ end
152
+
153
+ def callbacks
154
+ @callbacks ||= cop_config.fetch('Callbacks', RailsClassOrderDefaults::CALLBACKS).map(&:to_sym)
155
+ end
156
+
157
+ def others
158
+ @others ||= cop_config.fetch('Others', RailsClassOrderDefaults::OTHERS).map(&:to_sym)
159
+ end
160
+
161
+ def all_target_methods
162
+ @all_target_methods ||= associations + callbacks + others
163
+ end
164
+
165
+ def target_methods(body)
166
+ body.children.select { |child| child.send_type? && all_target_methods.include?(child.method_name) }
167
+ end
168
+
169
+ def sort_methods(methods)
170
+ methods.each_with_index.sort_by { |m, i| [method_type_order(m), method_position_in_type(m), i] }.map(&:first)
171
+ end
172
+
173
+ def method_type(method)
174
+ name = method.method_name
175
+ return :association if associations.include?(name)
176
+ return :callback if callbacks.include?(name)
177
+
178
+ :other
179
+ end
180
+
181
+ def method_type_order(method)
182
+ TYPE_ORDER[method_type(method)]
183
+ end
184
+
185
+ def method_position_in_type(method)
186
+ method_list_for_type(method_type(method)).index(method.method_name) || 999
187
+ end
188
+
189
+ def method_list_for_type(type)
190
+ { association: associations, callback: callbacks, other: others }[type]
191
+ end
192
+ end
193
+ end
194
+ end
195
+ 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.3.1
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Asjer Querido
@@ -57,6 +57,7 @@ files:
57
57
  - lib/rubocop/asjer/plugin.rb
58
58
  - lib/rubocop/asjer/version.rb
59
59
  - lib/rubocop/cop/asjer/no_default_translation.rb
60
+ - lib/rubocop/cop/asjer/rails_class_order.rb
60
61
  - release-please-config.json
61
62
  - sig/rubocop/i18n.rbs
62
63
  homepage: https://github.com/asjer/rubocop-asjer