shirobai-rails 2026.0709.0000

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 59377ca996914ce8807076603ac3a1b9ea4e36f99122b84ab54d021aa19e5ddc
4
+ data.tar.gz: 63686f3c441b593525971c307bca3b35cf38f14eab0fa6c8988bed3316466268
5
+ SHA512:
6
+ metadata.gz: 71cb62d85e9f072a0f79bd5fd41148b685a44e09088be5e0d4d88e4885ce0082ce4830ae9807116ffadea432de54f6baab4a1516480059fbf7a2892f5f15e140
7
+ data.tar.gz: f75adf3a6314b9173d52822852b44ab59b9a0fb62c6fd5d991bbab2b2b0c170393d3e668acc3a3ab2a94891ea4ae6a38442a549f94a5e176feba3a6d1ded5628
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 fusagiko / takayamaki
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shirobai
4
+ module Cop
5
+ module Rails
6
+ # Drop-in Rust reimplementation of `Rails/ApplicationController`
7
+ # (rubocop-rails 2.35.5). See `ApplicationRecord` for the shared
8
+ # `EnforceSuperclass` semantics and offense/autocorrect ranges.
9
+ #
10
+ # Unlike Record / Mailer / Job this cop has NO `TargetRailsVersion`
11
+ # gate — stock does not declare one, so it runs on every Rails version.
12
+ class ApplicationController < RuboCop::Cop::Base
13
+ include Shirobai::Cop::BundleEligible
14
+ extend RuboCop::Cop::AutoCorrector
15
+
16
+ MSG = "Controllers should subclass `ApplicationController`."
17
+ SUPERCLASS = "ApplicationController"
18
+
19
+ def self.cop_name = "Rails/ApplicationController"
20
+ def self.badge = RuboCop::Cop::Badge.parse(cop_name)
21
+
22
+ def self.bundle_args(_config)
23
+ []
24
+ end
25
+
26
+ def on_new_investigation
27
+ buffer = processed_source.buffer
28
+ source = bundle_eligible? ? processed_source.raw_source : buffer.source
29
+ off = SourceOffsets.for(source)
30
+ resolved_offenses.each do |start_offset, end_offset|
31
+ range = Parser::Source::Range.new(buffer, off[start_offset], off[end_offset])
32
+ add_offense(range, message: MSG) do |corrector|
33
+ corrector.replace(range, self.class::SUPERCLASS)
34
+ end
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def resolved_offenses
41
+ if bundle_eligible?
42
+ Dispatch.offenses_for(processed_source, config, :rails_application_controller)
43
+ else
44
+ Shirobai.check_rails_application_controller(processed_source.buffer.source)
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shirobai
4
+ module Cop
5
+ module Rails
6
+ # Drop-in Rust reimplementation of `Rails/ApplicationJob`
7
+ # (rubocop-rails 2.35.5). See `ApplicationRecord` for the shared
8
+ # `EnforceSuperclass` semantics and offense/autocorrect ranges. Gated on
9
+ # `requires_gem('railties', '>= 5.0')` like stock.
10
+ #
11
+ # (Stock defines an explicit `autocorrect(node)` returning
12
+ # `corrector.replace(node, SUPERCLASS)`; the other three cops reach the
13
+ # same replacement through the `AutoCorrector` block. Both collapse to
14
+ # "replace the offense range with the superclass name", which is what
15
+ # the shared wrapper corrector does here.)
16
+ class ApplicationJob < RuboCop::Cop::Base
17
+ include Shirobai::Cop::BundleEligible
18
+ extend RuboCop::Cop::AutoCorrector
19
+ extend RuboCop::Cop::TargetRailsVersion
20
+
21
+ minimum_target_rails_version 5.0
22
+
23
+ MSG = "Jobs should subclass `ApplicationJob`."
24
+ SUPERCLASS = "ApplicationJob"
25
+
26
+ def self.cop_name = "Rails/ApplicationJob"
27
+ def self.badge = RuboCop::Cop::Badge.parse(cop_name)
28
+
29
+ def self.bundle_args(_config)
30
+ []
31
+ end
32
+
33
+ def on_new_investigation
34
+ buffer = processed_source.buffer
35
+ source = bundle_eligible? ? processed_source.raw_source : buffer.source
36
+ off = SourceOffsets.for(source)
37
+ resolved_offenses.each do |start_offset, end_offset|
38
+ range = Parser::Source::Range.new(buffer, off[start_offset], off[end_offset])
39
+ add_offense(range, message: MSG) do |corrector|
40
+ corrector.replace(range, self.class::SUPERCLASS)
41
+ end
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def resolved_offenses
48
+ if bundle_eligible?
49
+ Dispatch.offenses_for(processed_source, config, :rails_application_job)
50
+ else
51
+ Shirobai.check_rails_application_job(processed_source.buffer.source)
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shirobai
4
+ module Cop
5
+ module Rails
6
+ # Drop-in Rust reimplementation of `Rails/ApplicationMailer`
7
+ # (rubocop-rails 2.35.5). See `ApplicationRecord` for the shared
8
+ # `EnforceSuperclass` semantics and offense/autocorrect ranges. Gated on
9
+ # `requires_gem('railties', '>= 5.0')` like stock.
10
+ class ApplicationMailer < RuboCop::Cop::Base
11
+ include Shirobai::Cop::BundleEligible
12
+ extend RuboCop::Cop::AutoCorrector
13
+ extend RuboCop::Cop::TargetRailsVersion
14
+
15
+ minimum_target_rails_version 5.0
16
+
17
+ MSG = "Mailers should subclass `ApplicationMailer`."
18
+ SUPERCLASS = "ApplicationMailer"
19
+
20
+ def self.cop_name = "Rails/ApplicationMailer"
21
+ def self.badge = RuboCop::Cop::Badge.parse(cop_name)
22
+
23
+ def self.bundle_args(_config)
24
+ []
25
+ end
26
+
27
+ def on_new_investigation
28
+ buffer = processed_source.buffer
29
+ source = bundle_eligible? ? processed_source.raw_source : buffer.source
30
+ off = SourceOffsets.for(source)
31
+ resolved_offenses.each do |start_offset, end_offset|
32
+ range = Parser::Source::Range.new(buffer, off[start_offset], off[end_offset])
33
+ add_offense(range, message: MSG) do |corrector|
34
+ corrector.replace(range, self.class::SUPERCLASS)
35
+ end
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def resolved_offenses
42
+ if bundle_eligible?
43
+ Dispatch.offenses_for(processed_source, config, :rails_application_mailer)
44
+ else
45
+ Shirobai.check_rails_application_mailer(processed_source.buffer.source)
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shirobai
4
+ module Cop
5
+ module Rails
6
+ # Drop-in Rust reimplementation of `Rails/ApplicationRecord`
7
+ # (rubocop-rails 2.35.5).
8
+ #
9
+ # Rust replicates stock's `EnforceSuperclass` mixin: `class X <
10
+ # ActiveRecord::Base` (unless `X`'s terminal name is `ApplicationRecord`)
11
+ # and `Class.new(ActiveRecord::Base)` (unless it is the direct value of
12
+ # an `ApplicationRecord =` constant write, covering the `do..end` / `{}`
13
+ # block forms). The offense range is the superclass / argument const node
14
+ # (leading `::` included); the wrapper replaces it with the bare
15
+ # `ApplicationRecord` name — byte for byte with stock's autocorrect.
16
+ #
17
+ # `TargetRailsVersion`: like stock this cop is gated on
18
+ # `requires_gem('railties', '>= 5.0')`, so it stays silent on Rails <
19
+ # 5.0 or without railties in the target bundle. The `Exclude:
20
+ # db/**/*.rb` from the merged default.yml is resolved by RuboCop through
21
+ # this wrapper's badge, exactly as for the stock cop.
22
+ class ApplicationRecord < RuboCop::Cop::Base
23
+ include Shirobai::Cop::BundleEligible
24
+ extend RuboCop::Cop::AutoCorrector
25
+ extend RuboCop::Cop::TargetRailsVersion
26
+
27
+ minimum_target_rails_version 5.0
28
+
29
+ MSG = "Models should subclass `ApplicationRecord`."
30
+ SUPERCLASS = "ApplicationRecord"
31
+
32
+ def self.cop_name = "Rails/ApplicationRecord"
33
+ def self.badge = RuboCop::Cop::Badge.parse(cop_name)
34
+
35
+ # No behavioral config: the segment is a wake-up flag only, so
36
+ # `bundle_args` contributes nothing.
37
+ def self.bundle_args(_config)
38
+ []
39
+ end
40
+
41
+ def on_new_investigation
42
+ buffer = processed_source.buffer
43
+ source = bundle_eligible? ? processed_source.raw_source : buffer.source
44
+ off = SourceOffsets.for(source)
45
+ resolved_offenses.each do |start_offset, end_offset|
46
+ range = Parser::Source::Range.new(buffer, off[start_offset], off[end_offset])
47
+ add_offense(range, message: MSG) do |corrector|
48
+ corrector.replace(range, self.class::SUPERCLASS)
49
+ end
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ def resolved_offenses
56
+ if bundle_eligible?
57
+ Dispatch.offenses_for(processed_source, config, :rails_application_record)
58
+ else
59
+ Shirobai.check_rails_application_record(processed_source.buffer.source)
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shirobai
4
+ module Cop
5
+ module Rails
6
+ # Shared harness for the Architecture-B Rails cops
7
+ # (`HttpPositionalArguments`, `DeprecatedActiveModelErrorsMethods`).
8
+ #
9
+ # Rust identifies candidate SEND ranges once on the shared walk; this
10
+ # module relocates each parser send node (`Shirobai::NodeLocator`) and
11
+ # hands it to the cop's `investigate_send`, which is stock's `on_send`
12
+ # copied verbatim (renamed so the Commissioner never dispatches a
13
+ # per-node `on_send`). Every guard, matcher, receiver walk and
14
+ # autocorrect runs on the real parser AST, so detection and `-A` bytes
15
+ # match stock exactly; the Rust prefilter only narrows which nodes are
16
+ # visited.
17
+ #
18
+ # The candidate set is a superset — false positives (e.g. a routing-block
19
+ # `get`, a non-model bare `errors`) are dropped by the stock matchers the
20
+ # wrapper re-runs.
21
+ module CandidateSupport
22
+ include Shirobai::Cop::BundleEligible
23
+
24
+ def on_new_investigation
25
+ ranges = resolved_candidates
26
+ return if ranges.nil? || ranges.empty?
27
+
28
+ source = bundle_eligible? ? processed_source.raw_source : processed_source.buffer.source
29
+ off = SourceOffsets.for(source)
30
+ keys = ranges.map { |(start, fin)| [off[start], off[fin]] }
31
+ located = Shirobai::NodeLocator.locate(processed_source, keys)
32
+
33
+ keys.each do |key|
34
+ node = located[key]
35
+ investigate_send(node) if node&.send_type?
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ # Bundle path only when raw_source and the parser buffer agree byte for
42
+ # byte; a gated-off file (nil from Dispatch) falls back to the
43
+ # standalone entry scanning `buffer.source`. The rails origin has no
44
+ # per-file gate, so on a bundle-eligible file the bundle path always
45
+ # wins.
46
+ def resolved_candidates
47
+ if bundle_eligible?
48
+ ranges = Dispatch.offenses_for(processed_source, config, candidate_slot)
49
+ return ranges unless ranges.nil?
50
+ end
51
+ fallback_candidates
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+
5
+ module Shirobai
6
+ module Cop
7
+ module Rails
8
+ # Drop-in Rust-backed reimplementation of
9
+ # `Rails/DeprecatedActiveModelErrorsMethods` (rubocop-rails 2.35.5),
10
+ # Architecture B.
11
+ #
12
+ # Rust supplies candidate SEND ranges (the five errors-chain shapes:
13
+ # `errors[...]`/`errors.messages[...]`/`errors.details[...]` manipulation
14
+ # and assignment, and `errors.{keys,values,to_h,to_xml}`); this wrapper
15
+ # relocates each parser send node and runs stock's `on_send` (renamed
16
+ # `investigate_send`) plus autocorrect VERBATIM. The receiver-model
17
+ # heuristic (`model_file?`), the Rails <= 6.0 gate on the incompatible
18
+ # methods, `skip_autocorrect?` (uncorrectable `details << ...`) and the
19
+ # receiver-walk offense range all run on the parser AST, so offenses and
20
+ # `-A` bytes match stock exactly.
21
+ class DeprecatedActiveModelErrorsMethods < RuboCop::Cop::Base
22
+ include RuboCop::Cop::RangeHelp
23
+ include Shirobai::Cop::Rails::CandidateSupport
24
+ extend RuboCop::Cop::AutoCorrector
25
+
26
+ MSG = 'Avoid manipulating ActiveModel errors as hash directly.'
27
+ AUTOCORRECTABLE_METHODS = %i[<< clear keys].freeze
28
+ INCOMPATIBLE_METHODS = %i[keys values to_h to_xml].freeze
29
+
30
+ MANIPULATIVE_METHODS = Set[
31
+ *%i[
32
+ << append clear collect! compact! concat
33
+ delete delete_at delete_if drop drop_while fill filter! keep_if
34
+ flatten! insert map! pop prepend push reject! replace reverse!
35
+ rotate! select! shift shuffle! slice! sort! sort_by! uniq! unshift
36
+ ]
37
+ ].freeze
38
+
39
+ def self.cop_name = "Rails/DeprecatedActiveModelErrorsMethods"
40
+ def self.badge = RuboCop::Cop::Badge.parse(cop_name)
41
+
42
+ def self.bundle_args(_config)
43
+ []
44
+ end
45
+
46
+ def_node_matcher :receiver_matcher_outside_model, '{send ivar lvar}'
47
+ def_node_matcher :receiver_matcher_inside_model, '{nil? send ivar lvar}'
48
+
49
+ def_node_matcher :any_manipulation?, <<~PATTERN
50
+ {
51
+ #root_manipulation?
52
+ #root_assignment?
53
+ #errors_deprecated?
54
+ #messages_details_manipulation?
55
+ #messages_details_assignment?
56
+ }
57
+ PATTERN
58
+
59
+ def_node_matcher :root_manipulation?, <<~PATTERN
60
+ (send
61
+ (send
62
+ (send #receiver_matcher :errors) :[] ...)
63
+ MANIPULATIVE_METHODS
64
+ ...
65
+ )
66
+ PATTERN
67
+
68
+ def_node_matcher :root_assignment?, <<~PATTERN
69
+ (send
70
+ (send #receiver_matcher :errors)
71
+ :[]=
72
+ ...)
73
+ PATTERN
74
+
75
+ def_node_matcher :errors_deprecated?, <<~PATTERN
76
+ (send
77
+ (send #receiver_matcher :errors)
78
+ {:keys :values :to_h :to_xml})
79
+ PATTERN
80
+
81
+ def_node_matcher :messages_details_manipulation?, <<~PATTERN
82
+ (send
83
+ (send
84
+ (send
85
+ (send #receiver_matcher :errors)
86
+ {:messages :details})
87
+ :[]
88
+ ...)
89
+ MANIPULATIVE_METHODS
90
+ ...)
91
+ PATTERN
92
+
93
+ def_node_matcher :messages_details_assignment?, <<~PATTERN
94
+ (send
95
+ (send
96
+ (send #receiver_matcher :errors)
97
+ {:messages :details})
98
+ :[]=
99
+ ...)
100
+ PATTERN
101
+
102
+ private
103
+
104
+ def candidate_slot = :rails_deprecated_active_model_errors_methods
105
+
106
+ def fallback_candidates
107
+ Shirobai.check_rails_deprecated_active_model_errors_methods(processed_source.buffer.source)
108
+ end
109
+
110
+ # --- stock's methods, copied verbatim (rubocop-rails 2.35.5);
111
+ # `on_send` renamed to `investigate_send` so it is not a node callback.
112
+
113
+ def investigate_send(node)
114
+ any_manipulation?(node) do
115
+ next if target_rails_version <= 6.0 && INCOMPATIBLE_METHODS.include?(node.method_name)
116
+
117
+ add_offense(node) do |corrector|
118
+ next if skip_autocorrect?(node)
119
+
120
+ autocorrect(corrector, node)
121
+ end
122
+ end
123
+ end
124
+
125
+ def skip_autocorrect?(node)
126
+ return true unless AUTOCORRECTABLE_METHODS.include?(node.method_name)
127
+ return false unless (receiver = node.receiver.receiver)
128
+
129
+ receiver.send_type? && receiver.method?(:details) && node.method?(:<<)
130
+ end
131
+
132
+ def autocorrect(corrector, node)
133
+ receiver = node.receiver
134
+
135
+ range = offense_range(node, receiver)
136
+ replacement = replacement(node, receiver)
137
+
138
+ corrector.replace(range, replacement)
139
+ end
140
+
141
+ def offense_range(node, receiver)
142
+ receiver = receiver.receiver while receiver.send_type? && !receiver.method?(:errors) && receiver.receiver
143
+ range_between(receiver.source_range.end_pos, node.source_range.end_pos)
144
+ end
145
+
146
+ def replacement(node, receiver)
147
+ return '.attribute_names' if node.method?(:keys)
148
+
149
+ key = receiver.first_argument.source
150
+
151
+ case node.method_name
152
+ when :<<
153
+ value = node.first_argument.source
154
+
155
+ ".add(#{key}, #{value})"
156
+ when :clear
157
+ ".delete(#{key})"
158
+ end
159
+ end
160
+
161
+ def receiver_matcher(node)
162
+ model_file? ? receiver_matcher_inside_model(node) : receiver_matcher_outside_model(node)
163
+ end
164
+
165
+ def model_file?
166
+ processed_source.file_path.include?('/models/')
167
+ end
168
+ end
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shirobai
4
+ module Cop
5
+ module Rails
6
+ # Drop-in Rust reimplementation of `Rails/DynamicFindBy`
7
+ # (rubocop-rails 2.35.5).
8
+ #
9
+ # Rust replicates the `on_send` / `on_csend` detection (the
10
+ # `/^find_by_(.+?)(!)?$/` name match, the argument count / no-splat /
11
+ # no-hash rules, the `AllowedMethods` / `AllowedReceivers` / `Whitelist`
12
+ # suppressions, and the receiverless-inside-ActiveRecord class check) and
13
+ # the full autocorrect: replace the selector with `find_by` / `find_by!`
14
+ # and insert each `col: ` keyword before its argument. The wrapper owns
15
+ # only the fixed `MSG` (the method name comes from the selector range).
16
+ class DynamicFindBy < RuboCop::Cop::Base
17
+ include Shirobai::Cop::BundleEligible
18
+ extend RuboCop::Cop::AutoCorrector
19
+
20
+ MSG = "Use `%<static_name>s` instead of dynamic `%<method>s`."
21
+
22
+ def self.cop_name = "Rails/DynamicFindBy"
23
+ def self.badge = RuboCop::Cop::Badge.parse(cop_name)
24
+
25
+ # Contributes `[nums, lists]` pieces to the rails segment:
26
+ # `[[], [AllowedMethods, AllowedReceivers, Whitelist]]`.
27
+ def self.bundle_args(config)
28
+ cop = config.for_badge(badge)
29
+ [[], [cop["AllowedMethods"] || [], cop["AllowedReceivers"] || [], cop["Whitelist"] || []]]
30
+ end
31
+
32
+ def on_new_investigation
33
+ buffer = processed_source.buffer
34
+ source = bundle_eligible? ? processed_source.raw_source : buffer.source
35
+ off = SourceOffsets.for(source)
36
+ resolved_offenses.each do |start_o, end_o, static_name, sel_s, sel_e, inserts|
37
+ range = Parser::Source::Range.new(buffer, off[start_o], off[end_o])
38
+ sel_range = Parser::Source::Range.new(buffer, off[sel_s], off[sel_e])
39
+ message = format(MSG, static_name: static_name, method: sel_range.source)
40
+ add_offense(range, message: message) do |corrector|
41
+ corrector.replace(sel_range, static_name)
42
+ inserts.each do |arg_start, keyword|
43
+ pos = off[arg_start]
44
+ corrector.insert_before(Parser::Source::Range.new(buffer, pos, pos), keyword)
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def resolved_offenses
53
+ if bundle_eligible?
54
+ Dispatch.offenses_for(processed_source, config, :rails_dynamic_find_by)
55
+ else
56
+ Shirobai.check_rails_dynamic_find_by(
57
+ processed_source.buffer.source,
58
+ cop_config["AllowedMethods"] || [], cop_config["AllowedReceivers"] || [],
59
+ cop_config["Whitelist"] || []
60
+ )
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shirobai
4
+ module Cop
5
+ module Rails
6
+ # Drop-in Rust-backed reimplementation of `Rails/HttpPositionalArguments`
7
+ # (rubocop-rails 2.35.5), Architecture B.
8
+ #
9
+ # Rust supplies candidate SEND ranges (bare-receiver HTTP verb calls with
10
+ # an action plus a data argument); this wrapper relocates each parser
11
+ # send node and runs stock's `on_send` (renamed `investigate_send`) plus
12
+ # autocorrect VERBATIM. The routing-block / rack-test guards, the
13
+ # `http_request?` / `needs_conversion?` matchers (kwsplat / forwarded /
14
+ # already-keyword hashes) and the full-node source rebuild all run on the
15
+ # parser AST, so offenses and `-A` bytes match stock exactly.
16
+ #
17
+ # `TargetRailsVersion`: like stock, gated on
18
+ # `requires_gem('railties', '>= 5.0')` — silent on Rails < 5.0 or without
19
+ # railties in the target bundle. The `Include: **/spec/**, **/test/**`
20
+ # from the merged default.yml resolves through this wrapper's badge,
21
+ # exactly as for the stock cop.
22
+ class HttpPositionalArguments < RuboCop::Cop::Base
23
+ include RuboCop::Cop::RangeHelp
24
+ include Shirobai::Cop::Rails::CandidateSupport
25
+ extend RuboCop::Cop::AutoCorrector
26
+ extend RuboCop::Cop::TargetRailsVersion
27
+
28
+ MSG = 'Use keyword arguments instead of positional arguments for http call: `%<verb>s`.'
29
+ KEYWORD_ARGS = %i[method params session body flash xhr as headers env to].freeze
30
+ ROUTING_METHODS = %i[draw routes].freeze
31
+ RESTRICT_ON_SEND = %i[get post put patch delete head].freeze
32
+
33
+ minimum_target_rails_version 5.0
34
+
35
+ def self.cop_name = "Rails/HttpPositionalArguments"
36
+ def self.badge = RuboCop::Cop::Badge.parse(cop_name)
37
+
38
+ # No behavioral config: the candidate list is a wake-up flag only.
39
+ def self.bundle_args(_config)
40
+ []
41
+ end
42
+
43
+ def_node_matcher :http_request?, <<~PATTERN
44
+ (send nil? {#{RESTRICT_ON_SEND.map(&:inspect).join(' ')}} !nil? $_ ...)
45
+ PATTERN
46
+
47
+ def_node_matcher :kwsplat_hash?, <<~PATTERN
48
+ (hash (kwsplat _))
49
+ PATTERN
50
+
51
+ def_node_matcher :forwarded_kwrestarg?, <<~PATTERN
52
+ (hash (forwarded-kwrestarg))
53
+ PATTERN
54
+
55
+ def_node_matcher :include_rack_test_methods?, <<~PATTERN
56
+ (send nil? :include
57
+ (const
58
+ (const
59
+ (const {nil? cbase} :Rack) :Test) :Methods))
60
+ PATTERN
61
+
62
+ private
63
+
64
+ def candidate_slot = :rails_http_positional_arguments
65
+
66
+ def fallback_candidates
67
+ Shirobai.check_rails_http_positional_arguments(processed_source.buffer.source)
68
+ end
69
+
70
+ # --- stock's methods, copied verbatim (rubocop-rails 2.35.5);
71
+ # `on_send` renamed to `investigate_send` so it is not a node callback.
72
+
73
+ def investigate_send(node)
74
+ return if in_routing_block?(node) || use_rack_test_methods?
75
+
76
+ http_request?(node) do |data|
77
+ return unless needs_conversion?(data)
78
+
79
+ message = format(MSG, verb: node.method_name)
80
+
81
+ add_offense(highlight_range(node), message: message) do |corrector|
82
+ corrector.replace(node, correction(node))
83
+ end
84
+ end
85
+ end
86
+
87
+ def in_routing_block?(node)
88
+ !!node.each_ancestor(:block).detect { |block| ROUTING_METHODS.include?(block.method_name) }
89
+ end
90
+
91
+ def use_rack_test_methods?
92
+ processed_source.ast.each_descendant(:send).any? do |node|
93
+ include_rack_test_methods?(node)
94
+ end
95
+ end
96
+
97
+ # rubocop:disable Metrics/CyclomaticComplexity
98
+ def needs_conversion?(data)
99
+ return false if data.forwarded_args_type? || forwarded_kwrestarg?(data)
100
+ return true unless data.hash_type?
101
+ return false if kwsplat_hash?(data)
102
+
103
+ data.each_pair.none? do |pair|
104
+ special_keyword_arg?(pair.key) || (format_arg?(pair.key) && data.pairs.one?)
105
+ end
106
+ end
107
+ # rubocop:enable Metrics/CyclomaticComplexity
108
+
109
+ def special_keyword_arg?(node)
110
+ node.sym_type? && KEYWORD_ARGS.include?(node.value)
111
+ end
112
+
113
+ def format_arg?(node)
114
+ node.sym_type? && node.value == :format
115
+ end
116
+
117
+ def highlight_range(node)
118
+ _http_path, *data = *node.arguments
119
+
120
+ range_between(data.first.source_range.begin_pos, data.last.source_range.end_pos)
121
+ end
122
+
123
+ def convert_hash_data(data, type)
124
+ return '' if data.hash_type? && data.empty?
125
+
126
+ hash_data = if data.hash_type?
127
+ format('{ %<data>s }', data: data.pairs.map(&:source).join(', '))
128
+ else
129
+ # user supplies an object,
130
+ # no need to surround with braces
131
+ data.source
132
+ end
133
+
134
+ format(', %<type>s: %<hash_data>s', type: type, hash_data: hash_data)
135
+ end
136
+
137
+ def correction(node)
138
+ http_path, *data = *node.arguments
139
+
140
+ controller_action = http_path.source
141
+ params = convert_hash_data(data.first, 'params')
142
+ session = convert_hash_data(data.last, 'session') if data.size > 1
143
+
144
+ format(correction_template(node), name: node.method_name,
145
+ action: controller_action,
146
+ params: params,
147
+ session: session)
148
+ end
149
+
150
+ def correction_template(node)
151
+ if parentheses?(node)
152
+ '%<name>s(%<action>s%<params>s%<session>s)'
153
+ else
154
+ '%<name>s %<action>s%<params>s%<session>s'
155
+ end
156
+ end
157
+ end
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shirobai
4
+ module Cop
5
+ module Rails
6
+ # Drop-in Rust reimplementation of `Rails/Pluck`
7
+ # (rubocop-rails 2.35.5).
8
+ #
9
+ # Rust detects `map { |x| x[:key] }` / `collect { |x| x[:key] }`
10
+ # patterns replaceable with `pluck(:key)`, including numblock (`_1`)
11
+ # and itblock (`it`) forms. The ancestor block-with-receiver guard
12
+ # (N+1 query risk) and the regexp key / block-arg-in-key exclusions
13
+ # are all handled Rust-side.
14
+ #
15
+ # Autocorrect: replace from the selector (map/collect) through the
16
+ # block end with `pluck(<key source>)`.
17
+ class Pluck < RuboCop::Cop::Base
18
+ include Shirobai::Cop::BundleEligible
19
+ extend RuboCop::Cop::AutoCorrector
20
+ extend RuboCop::Cop::TargetRailsVersion
21
+
22
+ MSG = "Prefer `%<replacement>s` over `%<current>s`."
23
+
24
+ minimum_target_rails_version 5.0
25
+
26
+ def self.cop_name = "Rails/Pluck"
27
+ def self.badge = RuboCop::Cop::Badge.parse(cop_name)
28
+
29
+ # No behavioral config to pack into the segment.
30
+ def self.bundle_args(_config)
31
+ [[], []]
32
+ end
33
+
34
+ def on_new_investigation
35
+ buffer = processed_source.buffer
36
+ source = bundle_eligible? ? processed_source.raw_source : buffer.source
37
+ off = SourceOffsets.for(source)
38
+ resolved_offenses.each do |sel_start, block_end, key_src_start, key_src_end|
39
+ offense_range = Parser::Source::Range.new(buffer, off[sel_start], off[block_end])
40
+ key_source = source.byteslice(key_src_start, key_src_end - key_src_start)
41
+ replacement = "pluck(#{key_source})"
42
+ message = format(MSG, replacement: replacement, current: offense_range.source)
43
+ add_offense(offense_range, message: message) do |corrector|
44
+ corrector.replace(offense_range, replacement)
45
+ end
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ def resolved_offenses
52
+ if bundle_eligible?
53
+ Dispatch.offenses_for(processed_source, config, :rails_pluck)
54
+ else
55
+ Shirobai.check_rails_pluck(processed_source.buffer.source)
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shirobai
4
+ module Cop
5
+ module Rails
6
+ # Drop-in Rust reimplementation of `Rails/UnknownEnv`
7
+ # (rubocop-rails 2.35.5).
8
+ #
9
+ # Rust detects the three `Rails.env` shapes (predicate / comparison /
10
+ # `case`) and emits `[start, end, name]` per unknown environment. The
11
+ # message — including the `DidYouMean` spell suggestion, which must stay
12
+ # Ruby-side — is built here from this cop's own `Environments` config, so
13
+ # the suggestion text is byte-for-byte stock. No autocorrect (stock has
14
+ # none).
15
+ #
16
+ # The `supports_local` view (`local` is a known environment for the
17
+ # predicate form only, and only on Rails >= 7.1) is packed into the
18
+ # segment so the Rust filter matches; the comparison and `case` forms
19
+ # never allow `local`, mirroring stock.
20
+ class UnknownEnv < RuboCop::Cop::Base
21
+ include Shirobai::Cop::BundleEligible
22
+
23
+ MSG = "Unknown environment `%<name>s`."
24
+ MSG_SIMILAR = "Unknown environment `%<name>s`. Did you mean `%<similar>s`?"
25
+
26
+ def self.cop_name = "Rails/UnknownEnv"
27
+ def self.badge = RuboCop::Cop::Badge.parse(cop_name)
28
+
29
+ # Contributes `[nums, lists]` pieces to the rails segment:
30
+ # `[[supports_local], [environments]]`.
31
+ def self.bundle_args(config)
32
+ cop = config.for_badge(badge)
33
+ environments = cop["Environments"] || []
34
+ supports_local = config.target_rails_version >= 7.1
35
+ [[supports_local ? 1 : 0], [environments]]
36
+ end
37
+
38
+ def on_new_investigation
39
+ buffer = processed_source.buffer
40
+ source = bundle_eligible? ? processed_source.raw_source : buffer.source
41
+ off = SourceOffsets.for(source)
42
+ resolved_offenses.each do |start_offset, end_offset, name|
43
+ range = Parser::Source::Range.new(buffer, off[start_offset], off[end_offset])
44
+ add_offense(range, message: message(name))
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def message(name)
51
+ name = name.to_s.chomp("?")
52
+
53
+ # DidYouMean::SpellChecker is not available in all versions of Ruby;
54
+ # feature-check first, exactly like stock.
55
+ similar_names = if defined?(DidYouMean::SpellChecker)
56
+ DidYouMean::SpellChecker.new(dictionary: environments).correct(name)
57
+ else
58
+ []
59
+ end
60
+
61
+ if similar_names.empty?
62
+ format(MSG, name: name)
63
+ else
64
+ format(MSG_SIMILAR, name: name, similar: similar_names.join(", "))
65
+ end
66
+ end
67
+
68
+ def environments
69
+ cop_config["Environments"] || []
70
+ end
71
+
72
+ def supports_local?
73
+ target_rails_version >= 7.1
74
+ end
75
+
76
+ def resolved_offenses
77
+ if bundle_eligible?
78
+ Dispatch.offenses_for(processed_source, config, :rails_unknown_env)
79
+ else
80
+ Shirobai.check_rails_unknown_env(
81
+ processed_source.buffer.source, environments, supports_local?
82
+ )
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shirobai
4
+ module Rails
5
+ # Locked to the shirobai core gem version (release train: both gems
6
+ # ship together and the gemspec pins `shirobai` to this exact version).
7
+ VERSION = "2026.0709.0000"
8
+ end
9
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Load order matters, and this file owns it so users don't have to:
4
+ #
5
+ # 1. `shirobai` first. The core gem loads the native extension, replaces
6
+ # the core cops, and defines `Shirobai::Dispatch` — the registration
7
+ # point for this gem's packed-config segment.
8
+ # 2. `rubocop-rails` second. Stock Rails cop classes must be enlisted in
9
+ # RuboCop's registry BEFORE the wrappers below:
10
+ # `Registry#clear_enrollment_queue` resolves same-badge collisions by
11
+ # last-write-wins, so whoever is defined later owns the badge.
12
+ # Requiring it here (the gemspec pins the exact version) makes the
13
+ # replacement order independent of `.rubocop.yml` require order.
14
+ # 3. Wrapper cop classes last. Defining each class auto-enlists it
15
+ # (`RuboCop::Cop::Base.inherited`) and replaces the stock cop under
16
+ # the same badge.
17
+ #
18
+ # Requiring rubocop-rails here does NOT merge its config/default.yml into
19
+ # RuboCop's default configuration — that is the plugin system's job. Users
20
+ # still declare `plugins: [rubocop-rails]` in `.rubocop.yml` (or legacy
21
+ # `require:`, which RuboCop auto-promotes to a plugin with a deprecation
22
+ # warning) and add `require: [shirobai-rails]`.
23
+ require "shirobai"
24
+ require "rubocop-rails"
25
+
26
+ require_relative "shirobai/rails/version"
27
+ require_relative "shirobai/cop/rails/application_record"
28
+ require_relative "shirobai/cop/rails/application_controller"
29
+ require_relative "shirobai/cop/rails/application_mailer"
30
+ require_relative "shirobai/cop/rails/application_job"
31
+ require_relative "shirobai/cop/rails/unknown_env"
32
+ require_relative "shirobai/cop/rails/dynamic_find_by"
33
+ require_relative "shirobai/cop/rails/pluck"
34
+ require_relative "shirobai/cop/rails/candidate_support"
35
+ require_relative "shirobai/cop/rails/http_positional_arguments"
36
+ require_relative "shirobai/cop/rails/deprecated_active_model_errors_methods"
37
+
38
+ module Shirobai
39
+ # Glue for the shirobai-rails plugin gem: the packed-config segment (just a
40
+ # wake-up flag — the Application* cluster carries no behavioral config).
41
+ #
42
+ # Unlike shirobai-rspec there is NO per-file gate: rubocop-rails cops run on
43
+ # every Ruby file (no department Include like RSpec's `**/*_spec.rb`), so
44
+ # once this gem is loaded the rails origin is always awake. The core packs
45
+ # this origin's segment into every config from now on.
46
+ module Rails
47
+ # Wrapper cop classes, appended as cops land. Kept for parity with the
48
+ # sibling plugins and for plumbing specs.
49
+ COP_CLASSES = [
50
+ Shirobai::Cop::Rails::ApplicationRecord,
51
+ Shirobai::Cop::Rails::ApplicationController,
52
+ Shirobai::Cop::Rails::ApplicationMailer,
53
+ Shirobai::Cop::Rails::ApplicationJob,
54
+ Shirobai::Cop::Rails::UnknownEnv,
55
+ Shirobai::Cop::Rails::DynamicFindBy,
56
+ Shirobai::Cop::Rails::Pluck,
57
+ Shirobai::Cop::Rails::HttpPositionalArguments,
58
+ Shirobai::Cop::Rails::DeprecatedActiveModelErrorsMethods
59
+ ].freeze
60
+
61
+ class << self
62
+ # The rails origin's `[nums, lists]` segment for `config`. The wake-up
63
+ # flag is always `1` once the gem is loaded; the rest is each config-
64
+ # bearing cop's own `bundle_args` (the single source of its config).
65
+ #
66
+ # Segment layout (crates/shirobai-core/src/rules/rails_config.rs):
67
+ # nums = [enabled, unknown_env_supports_local]
68
+ # lists = [unknown_env_environments,
69
+ # dynamic_find_by_allowed_methods,
70
+ # dynamic_find_by_allowed_receivers,
71
+ # dynamic_find_by_whitelist]
72
+ #
73
+ # Per-cop gating that DOES vary (the `Rails/ApplicationRecord`
74
+ # `Exclude: db/**/*.rb`, the `TargetRailsVersion` gates, and each cop's
75
+ # `Enabled`) is NOT in this segment: RuboCop resolves it through each
76
+ # wrapper's own cop config exactly as for the stock cop, so a file a
77
+ # wrapper does not run on simply drops that cop's Rust-computed slot
78
+ # while the other cops' slots (from the same shared walk) are consumed.
79
+ def segment(config)
80
+ ue = Shirobai::Cop::Rails::UnknownEnv.bundle_args(config)
81
+ dfb = Shirobai::Cop::Rails::DynamicFindBy.bundle_args(config)
82
+ nums = [1, *ue[0]]
83
+ lists = [*ue[1], *dfb[1]]
84
+ [nums, lists]
85
+ end
86
+ end
87
+ end
88
+ end
89
+
90
+ # Wake up the rails origin in the shared bundle: from now on every packed
91
+ # config carries this origin's segment with `enabled=1` (segment layout:
92
+ # crates/shirobai-core/src/rules/bundle.rs BundleConfig). No gate — the
93
+ # Application* cops run on every Ruby file. Without this gem the core packs
94
+ # the dormant segment and the Rust side skips the Rails rules entirely.
95
+ Shirobai::Dispatch.register_plugin_packer(:rails) { |config| Shirobai::Rails.segment(config) }
metadata ADDED
@@ -0,0 +1,78 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: shirobai-rails
3
+ version: !ruby/object:Gem::Version
4
+ version: 2026.0709.0000
5
+ platform: ruby
6
+ authors:
7
+ - fusagiko / takayamaki
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: shirobai
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - '='
17
+ - !ruby/object:Gem::Version
18
+ version: 2026.0709.0000
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - '='
24
+ - !ruby/object:Gem::Version
25
+ version: 2026.0709.0000
26
+ - !ruby/object:Gem::Dependency
27
+ name: rubocop-rails
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - '='
31
+ - !ruby/object:Gem::Version
32
+ version: 2.35.5
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - '='
38
+ - !ruby/object:Gem::Version
39
+ version: 2.35.5
40
+ executables: []
41
+ extensions: []
42
+ extra_rdoc_files: []
43
+ files:
44
+ - LICENSE.txt
45
+ - lib/shirobai-rails.rb
46
+ - lib/shirobai/cop/rails/application_controller.rb
47
+ - lib/shirobai/cop/rails/application_job.rb
48
+ - lib/shirobai/cop/rails/application_mailer.rb
49
+ - lib/shirobai/cop/rails/application_record.rb
50
+ - lib/shirobai/cop/rails/candidate_support.rb
51
+ - lib/shirobai/cop/rails/deprecated_active_model_errors_methods.rb
52
+ - lib/shirobai/cop/rails/dynamic_find_by.rb
53
+ - lib/shirobai/cop/rails/http_positional_arguments.rb
54
+ - lib/shirobai/cop/rails/pluck.rb
55
+ - lib/shirobai/cop/rails/unknown_env.rb
56
+ - lib/shirobai/rails/version.rb
57
+ homepage: https://github.com/takayamaki/shirobai
58
+ licenses:
59
+ - MIT
60
+ metadata: {}
61
+ rdoc_options: []
62
+ require_paths:
63
+ - lib
64
+ required_ruby_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '3.1'
69
+ required_rubygems_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ requirements: []
75
+ rubygems_version: 4.0.10
76
+ specification_version: 4
77
+ summary: Drop-in Rust replacement for rubocop-rails cops
78
+ test_files: []