rubocop-asjer 0.3.0 → 0.4.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: 421e0eeba6d681b0ee11e88059773f9278c24aa02c30f509e2bd23713ca321e8
4
- data.tar.gz: fc1298e689e874814629eab4120349f7658fc848719fff602608f92feb81bb0c
3
+ metadata.gz: 772cbb0491de18ffeb19b27e41336474005b2c9295f5f2c152c842883e250654
4
+ data.tar.gz: f2b5e866ba2bccd3fee26d59868f80b8f12de62e57b161e203e932f8aa70533f
5
5
  SHA512:
6
- metadata.gz: 5e9d9d5a7d0169b30cafb338aa14c61dacfceca1f423d66892791f51fca3d4f3be8a400cc14a834360cdd3ab5e52153658ff1725922a293bf7bca70d81854c31
7
- data.tar.gz: 9186614b1209094a659de0a9ec93bc3c5ab7d581db77528977f6ecf4f188350bc9ec32348c356dcb8d1b38e78f82aeb7b4a9f03323eefc5c1c788b5ce6e47ee7
6
+ metadata.gz: 891112aad96477909e4a2de0a24660655b5573b6ef8f837cd70fe84927502390a3544f47994a9644b4d5b12b826aba74c8ab09f73dde3d49b044bb5fa6cc62cf
7
+ data.tar.gz: f6c1dd35c8a2fe82392a38576b5c26af9b4958ad732d35493d4ae8e35cd0f423610b4263ff80909ce31c99f28d044a979eb8c0d124a5d4738777f984dc753fb6
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.3.0"
2
+ ".": "0.4.0"
3
3
  }
data/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.0](https://github.com/asjer/rubocop-asjer/compare/v0.3.1...v0.4.0) (2026-01-23)
4
+
5
+
6
+ ### Features
7
+
8
+ * 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))
9
+
10
+ ## [0.3.1](https://github.com/asjer/rubocop-asjer/compare/v0.3.0...v0.3.1) (2026-01-03)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * enable autocorrect for all `default:` option cases in `NoDefaultTranslation` ([fbd359e](https://github.com/asjer/rubocop-asjer/commit/fbd359e6f252188880535c87b3f5abbbd8bac571))
16
+
3
17
  ## [0.3.0](https://github.com/asjer/rubocop-asjer/compare/v0.2.0...v0.3.0) (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.0'
5
+ VERSION = '0.4.0'
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
@@ -34,9 +34,6 @@ module RuboCop
34
34
  def on_send(node)
35
35
  translation_with_default?(node) do |default_pair|
36
36
  add_offense(default_pair) do |corrector|
37
- # Skip autocorrection if default: is the only hash option
38
- next if default_pair.parent.pairs.size == 1
39
-
40
37
  corrector.remove(removal_range(default_pair))
41
38
  end
42
39
  end
@@ -45,7 +42,20 @@ module RuboCop
45
42
  private
46
43
 
47
44
  def removal_range(node)
48
- pairs = node.parent.pairs
45
+ hash_node = node.parent
46
+ pairs = hash_node.pairs
47
+
48
+ return hash_with_comma_range(hash_node) if pairs.size == 1
49
+
50
+ pair_removal_range(node, pairs)
51
+ end
52
+
53
+ def hash_with_comma_range(hash_node)
54
+ with_space = range_with_surrounding_space(range: hash_node.source_range, side: :left)
55
+ range_with_surrounding_comma(with_space, :left)
56
+ end
57
+
58
+ def pair_removal_range(node, pairs)
49
59
  index = pairs.index(node)
50
60
 
51
61
  last_pair?(index, pairs) ? leading_range(node, pairs[index - 1]) : trailing_range(node, pairs[index + 1])
@@ -0,0 +1,181 @@
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
+ # Enforces consistent ordering of declarative methods in Rails models.
60
+ #
61
+ # @see RailsClassOrderDefaults for default method lists
62
+ class RailsClassOrder < Base
63
+ extend AutoCorrector
64
+ 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
69
+
70
+ def on_class(node)
71
+ _name, _superclass, body = *node
72
+ return unless body&.begin_type?
73
+
74
+ targets = target_methods(body)
75
+ return if targets.empty?
76
+
77
+ sorted = sort_methods(targets)
78
+ return if targets == sorted
79
+
80
+ add_offense(body) do |corrector|
81
+ autocorrect(corrector, body, targets, sorted) if contiguous?(body, targets)
82
+ end
83
+ end
84
+
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)
97
+ end
98
+
99
+ def all_target_methods
100
+ @all_target_methods ||= associations + callbacks + others
101
+ end
102
+
103
+ def target_methods(body)
104
+ body.children.select do |child|
105
+ child.send_type? && all_target_methods.include?(child.method_name)
106
+ end
107
+ end
108
+
109
+ def sort_methods(methods)
110
+ # Use sort_by with index to make stable sort (preserve original order for equal elements)
111
+ methods.each_with_index.sort_by do |method, index|
112
+ [
113
+ method_type_order(method),
114
+ method_position_in_type(method),
115
+ index
116
+ ]
117
+ end.map(&:first)
118
+ end
119
+
120
+ def method_type(method)
121
+ name = method.method_name
122
+ return :association if associations.include?(name)
123
+ return :callback if callbacks.include?(name)
124
+
125
+ :other
126
+ end
127
+
128
+ def method_type_order(method)
129
+ TYPE_ORDER[method_type(method)]
130
+ end
131
+
132
+ 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)
177
+ end
178
+ end
179
+ end
180
+ end
181
+ 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.0
4
+ version: 0.4.0
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