From a4824e5c8897a46dc6bc7163b099cbf2e5a085f7 Mon Sep 17 00:00:00 2001 From: Kenneth Bogner Date: Fri, 22 Aug 2025 12:28:20 -0400 Subject: [PATCH 1/6] Introduce UnprocessableContentStatus cop --- .../add_unprocessable_content_status_cop.md | 1 + config/default.yml | 8 ++ .../cop/rails/unprocessable_content_status.rb | 82 +++++++++++++++++++ lib/rubocop/cop/rails_cops.rb | 1 + .../unprocessable_content_status_spec.rb | 66 +++++++++++++++ 5 files changed, 158 insertions(+) create mode 100644 changelog/add_unprocessable_content_status_cop.md create mode 100644 lib/rubocop/cop/rails/unprocessable_content_status.rb create mode 100644 spec/rubocop/cop/rails/unprocessable_content_status_spec.rb diff --git a/changelog/add_unprocessable_content_status_cop.md b/changelog/add_unprocessable_content_status_cop.md new file mode 100644 index 0000000000..52e9243381 --- /dev/null +++ b/changelog/add_unprocessable_content_status_cop.md @@ -0,0 +1 @@ +* [#1520](https://github.com/rubocop/rubocop-rails/pull/1520): Add `Rails/UnprocessableContentStatus` cop ([@tuxagon][]) diff --git a/config/default.yml b/config/default.yml index 74bebd3142..ecad5febb1 100644 --- a/config/default.yml +++ b/config/default.yml @@ -1232,6 +1232,14 @@ Rails/UnknownEnv: - test - production +Rails/UnprocessableContentStatus: + Description: 'Use `:unprocessable_content` instead of `:unprocessable_entity`.' + Enabled: pending + Severity: warning + VersionAdded: '<>' + Include: + - '**/app/controllers/**/*.rb' + Rails/UnusedIgnoredColumns: Description: 'Remove a column that does not exist from `ignored_columns`.' Enabled: false diff --git a/lib/rubocop/cop/rails/unprocessable_content_status.rb b/lib/rubocop/cop/rails/unprocessable_content_status.rb new file mode 100644 index 0000000000..4e37e3dc4c --- /dev/null +++ b/lib/rubocop/cop/rails/unprocessable_content_status.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + module Rails + # Enforces the use of :unprocessable_content instead of :unprocessable_entity + # for HTTP status codes, as :unprocessable_entity is deprecated in Rack 3.1. + # + # @example + # # bad + # render json: { error: "Invalid data" }, status: :unprocessable_entity + # head :unprocessable_entity + # + # # good + # render json: { error: "Invalid data" }, status: :unprocessable_content + # head :unprocessable_content + # + class UnprocessableContentStatus < Base + extend AutoCorrector + + MSG = 'Use `:unprocessable_content` instead of `:unprocessable_entity`. ' \ + 'The `:unprocessable_entity` status is deprecated.' + + def_node_matcher :unprocessable_entity_symbol?, <<~PATTERN + (sym :unprocessable_entity) + PATTERN + + def_node_matcher :status_argument?, <<~PATTERN + (pair (sym :status) (sym :unprocessable_entity)) + PATTERN + + def on_sym(node) + return unless rack_3_1_or_newer? + + return unless unprocessable_entity_symbol?(node) + return if in_hash_key_context?(node) + + return unless status_related_context?(node) + + add_offense(node) do |corrector| + corrector.replace(node, ':unprocessable_content') + end + end + + def on_pair(node) + return unless rack_3_1_or_newer? + + status_argument?(node) do + add_offense(node.value) do |corrector| + corrector.replace(node.value, ':unprocessable_content') + end + end + end + + private + + def rack_3_1_or_newer? + Gem::Version.new(Rack::VERSION) >= Gem::Version.new('3.1.0') + rescue ArgumentError, NoMethodError + false + end + + def in_hash_key_context?(node) + node.parent&.pair_type? && node.parent.key == node + end + + def status_related_context?(node) + parent = node.parent + return false unless parent + + if parent.send_type? + method_name = parent.method_name + return %i[head render redirect_to].include?(method_name) + end + + # Variable assignment or ternary expression + %i[lvasgn if].include?(parent.type) + end + end + end + end +end diff --git a/lib/rubocop/cop/rails_cops.rb b/lib/rubocop/cop/rails_cops.rb index a8c7202dac..372e6551f5 100644 --- a/lib/rubocop/cop/rails_cops.rb +++ b/lib/rubocop/cop/rails_cops.rb @@ -136,6 +136,7 @@ require_relative 'rails/uniq_before_pluck' require_relative 'rails/unique_validation_without_index' require_relative 'rails/unknown_env' +require_relative 'rails/unprocessable_content_status' require_relative 'rails/unused_ignored_columns' require_relative 'rails/unused_render_content' require_relative 'rails/validation' diff --git a/spec/rubocop/cop/rails/unprocessable_content_status_spec.rb b/spec/rubocop/cop/rails/unprocessable_content_status_spec.rb new file mode 100644 index 0000000000..5c5f33a3f5 --- /dev/null +++ b/spec/rubocop/cop/rails/unprocessable_content_status_spec.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +RSpec.describe RuboCop::Cop::Rails::UnprocessableContentStatus, :config do + context 'when Rack is older than 3.1 or not available' do + before do + allow(cop).to receive(:rack_3_1_or_newer?).and_return(false) + end + + it 'does nothing' do + expect_no_offenses(<<~RUBY) + render json: { error: 'Invalid data' }, status: :unprocessable_entity + RUBY + end + end + + context 'when Rack is 3.1 or later' do + before do + allow(cop).to receive(:rack_3_1_or_newer?).and_return(true) + end + + it 'registers an offense when using :unprocessable_entity in hash argument' do + expect_offense(<<~RUBY) + render json: { error: 'Invalid data' }, status: :unprocessable_entity + ^^^^^^^^^^^^^^^^^^^^^ Use `:unprocessable_content` instead of `:unprocessable_entity`. The `:unprocessable_entity` status is deprecated. + RUBY + + expect_correction(<<~RUBY) + render json: { error: 'Invalid data' }, status: :unprocessable_content + RUBY + end + + it 'registers an offense when using :unprocessable_entity in head' do + expect_offense(<<~RUBY) + head :unprocessable_entity + ^^^^^^^^^^^^^^^^^^^^^ Use `:unprocessable_content` instead of `:unprocessable_entity`. The `:unprocessable_entity` status is deprecated. + RUBY + + expect_correction(<<~RUBY) + head :unprocessable_content + RUBY + end + + it 'registers an offense when using :unprocessable_entity in ternary expression' do + expect_offense(<<~RUBY) + render json: { error: 'Invalid data' }, status: :unprocessable_entity ? :unprocessable_content : :ok + ^^^^^^^^^^^^^^^^^^^^^ Use `:unprocessable_content` instead of `:unprocessable_entity`. The `:unprocessable_entity` status is deprecated. + RUBY + + expect_correction(<<~RUBY) + render json: { error: 'Invalid data' }, status: :unprocessable_content ? :unprocessable_content : :ok + RUBY + end + + it 'does not register an offense when using :unprocessable_content' do + expect_no_offenses(<<~RUBY) + render json: { error: 'Invalid data' }, status: :unprocessable_content + RUBY + end + + it 'does not register an offense when using :unprocessable_entity in hash key' do + expect_no_offenses(<<~RUBY) + { unprocessable_entity: 'Invalid data' } + RUBY + end + end +end From cbd47258e825b2ffad21300adfbf6fd89238c5af Mon Sep 17 00:00:00 2001 From: Kenneth Bogner Date: Fri, 22 Aug 2025 17:07:13 -0400 Subject: [PATCH 2/6] Rename cop and general for deprecated statuses --- .../add_unprocessable_content_status_cop.md | 1 - .../new_deprecated_http_status_names_cop.md | 1 + config/default.yml | 16 +- .../cop/rails/deprecated_http_status_names.rb | 81 ++++++++ .../cop/rails/unprocessable_content_status.rb | 82 -------- lib/rubocop/cop/rails_cops.rb | 2 +- .../deprecated_http_status_names_spec.rb | 194 ++++++++++++++++++ .../unprocessable_content_status_spec.rb | 66 ------ 8 files changed, 285 insertions(+), 158 deletions(-) delete mode 100644 changelog/add_unprocessable_content_status_cop.md create mode 100644 changelog/new_deprecated_http_status_names_cop.md create mode 100644 lib/rubocop/cop/rails/deprecated_http_status_names.rb delete mode 100644 lib/rubocop/cop/rails/unprocessable_content_status.rb create mode 100644 spec/rubocop/cop/rails/deprecated_http_status_names_spec.rb delete mode 100644 spec/rubocop/cop/rails/unprocessable_content_status_spec.rb diff --git a/changelog/add_unprocessable_content_status_cop.md b/changelog/add_unprocessable_content_status_cop.md deleted file mode 100644 index 52e9243381..0000000000 --- a/changelog/add_unprocessable_content_status_cop.md +++ /dev/null @@ -1 +0,0 @@ -* [#1520](https://github.com/rubocop/rubocop-rails/pull/1520): Add `Rails/UnprocessableContentStatus` cop ([@tuxagon][]) diff --git a/changelog/new_deprecated_http_status_names_cop.md b/changelog/new_deprecated_http_status_names_cop.md new file mode 100644 index 0000000000..d52b47a873 --- /dev/null +++ b/changelog/new_deprecated_http_status_names_cop.md @@ -0,0 +1 @@ +* [#1520](https://github.com/rubocop/rubocop-rails/pull/1520): New `Rails/DeprecatedHttpStatusNames` cop. ([@tuxagon][]) diff --git a/config/default.yml b/config/default.yml index ecad5febb1..82fb968010 100644 --- a/config/default.yml +++ b/config/default.yml @@ -394,6 +394,14 @@ Rails/DeprecatedActiveModelErrorsMethods: VersionAdded: '2.14' VersionChanged: '2.18' +Rails/DeprecatedHttpStatusNames: + Description: 'Use the new HTTP status names instead of deprecated ones.' + Enabled: pending + Severity: warning + VersionAdded: '<>' + Include: + - '**/app/controllers/**/*.rb' + Rails/DotSeparatedKeys: Description: 'Enforces the use of dot-separated keys instead of `:scope` options in `I18n` translation methods.' StyleGuide: 'https://rails.rubystyle.guide/#dot-separated-keys' @@ -1232,14 +1240,6 @@ Rails/UnknownEnv: - test - production -Rails/UnprocessableContentStatus: - Description: 'Use `:unprocessable_content` instead of `:unprocessable_entity`.' - Enabled: pending - Severity: warning - VersionAdded: '<>' - Include: - - '**/app/controllers/**/*.rb' - Rails/UnusedIgnoredColumns: Description: 'Remove a column that does not exist from `ignored_columns`.' Enabled: false diff --git a/lib/rubocop/cop/rails/deprecated_http_status_names.rb b/lib/rubocop/cop/rails/deprecated_http_status_names.rb new file mode 100644 index 0000000000..ac9796a4be --- /dev/null +++ b/lib/rubocop/cop/rails/deprecated_http_status_names.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + module Rails + # Enforces the use of the current HTTP status names instead of deprecated ones. + # + # @example + # # bad + # render json: { error: "Invalid data" }, status: :unprocessable_entity + # head :payload_too_large + # + # # good + # render json: { error: "Invalid data" }, status: :unprocessable_content + # head :content_too_large + # + class DeprecatedHttpStatusNames < Base + extend AutoCorrector + + requires_gem 'rack', '>= 3.1.0' + + RESTRICT_ON_SEND = %i[render redirect_to head assert_response assert_redirected_to].freeze + + DEPRECATED_STATUSES = { + unprocessable_entity: :unprocessable_content, + payload_too_large: :content_too_large + }.freeze + + MSG = 'Use `:%s` instead of `:%s`. The `:%s` status is deprecated.' + + def_node_matcher :status_method_call, <<~PATTERN + { + (send nil? {:render :redirect_to} _ $hash) + (send nil? {:render :redirect_to} $hash) + (send nil? {:head :assert_response} $_ ...) + (send nil? :assert_redirected_to _ $hash ...) + (send nil? :assert_redirected_to $hash ...) + } + PATTERN + + def_node_matcher :status_hash_value, <<~PATTERN + (hash <(pair (sym :status) $_) ...>) + PATTERN + + def on_send(node) + status_method_call(node) do |status_node| + if status_node.hash_type? + # Handle hash arguments like { status: :unprocessable_entity } + status_hash_value(status_node) do |status_value| + find_deprecated_status_names(status_value) + end + else + # Handle positional arguments like head :unprocessable_entity + find_deprecated_status_names(status_node) + end + end + end + + private + + def find_deprecated_status_names(node) + if node.sym_type? && DEPRECATED_STATUSES.key?(node.value) + deprecated_status = node.value + preferred_status = DEPRECATED_STATUSES[deprecated_status] + + message = format(MSG, deprecated: deprecated_status, preferred: preferred_status) + + add_offense(node, message: message) do |corrector| + corrector.replace(node, ":#{preferred_status}") + end + elsif node.respond_to?(:children) + # Recursively search child nodes (handles ternary expressions, etc.) + node.children.each do |child| + find_deprecated_status_names(child) if child.is_a?(Parser::AST::Node) + end + end + end + end + end + end +end diff --git a/lib/rubocop/cop/rails/unprocessable_content_status.rb b/lib/rubocop/cop/rails/unprocessable_content_status.rb deleted file mode 100644 index 4e37e3dc4c..0000000000 --- a/lib/rubocop/cop/rails/unprocessable_content_status.rb +++ /dev/null @@ -1,82 +0,0 @@ -# frozen_string_literal: true - -module RuboCop - module Cop - module Rails - # Enforces the use of :unprocessable_content instead of :unprocessable_entity - # for HTTP status codes, as :unprocessable_entity is deprecated in Rack 3.1. - # - # @example - # # bad - # render json: { error: "Invalid data" }, status: :unprocessable_entity - # head :unprocessable_entity - # - # # good - # render json: { error: "Invalid data" }, status: :unprocessable_content - # head :unprocessable_content - # - class UnprocessableContentStatus < Base - extend AutoCorrector - - MSG = 'Use `:unprocessable_content` instead of `:unprocessable_entity`. ' \ - 'The `:unprocessable_entity` status is deprecated.' - - def_node_matcher :unprocessable_entity_symbol?, <<~PATTERN - (sym :unprocessable_entity) - PATTERN - - def_node_matcher :status_argument?, <<~PATTERN - (pair (sym :status) (sym :unprocessable_entity)) - PATTERN - - def on_sym(node) - return unless rack_3_1_or_newer? - - return unless unprocessable_entity_symbol?(node) - return if in_hash_key_context?(node) - - return unless status_related_context?(node) - - add_offense(node) do |corrector| - corrector.replace(node, ':unprocessable_content') - end - end - - def on_pair(node) - return unless rack_3_1_or_newer? - - status_argument?(node) do - add_offense(node.value) do |corrector| - corrector.replace(node.value, ':unprocessable_content') - end - end - end - - private - - def rack_3_1_or_newer? - Gem::Version.new(Rack::VERSION) >= Gem::Version.new('3.1.0') - rescue ArgumentError, NoMethodError - false - end - - def in_hash_key_context?(node) - node.parent&.pair_type? && node.parent.key == node - end - - def status_related_context?(node) - parent = node.parent - return false unless parent - - if parent.send_type? - method_name = parent.method_name - return %i[head render redirect_to].include?(method_name) - end - - # Variable assignment or ternary expression - %i[lvasgn if].include?(parent.type) - end - end - end - end -end diff --git a/lib/rubocop/cop/rails_cops.rb b/lib/rubocop/cop/rails_cops.rb index 372e6551f5..22b106b185 100644 --- a/lib/rubocop/cop/rails_cops.rb +++ b/lib/rubocop/cop/rails_cops.rb @@ -41,6 +41,7 @@ require_relative 'rails/delegate' require_relative 'rails/delegate_allow_blank' require_relative 'rails/deprecated_active_model_errors_methods' +require_relative 'rails/deprecated_http_status_names' require_relative 'rails/dot_separated_keys' require_relative 'rails/duplicate_association' require_relative 'rails/duplicate_scope' @@ -136,7 +137,6 @@ require_relative 'rails/uniq_before_pluck' require_relative 'rails/unique_validation_without_index' require_relative 'rails/unknown_env' -require_relative 'rails/unprocessable_content_status' require_relative 'rails/unused_ignored_columns' require_relative 'rails/unused_render_content' require_relative 'rails/validation' diff --git a/spec/rubocop/cop/rails/deprecated_http_status_names_spec.rb b/spec/rubocop/cop/rails/deprecated_http_status_names_spec.rb new file mode 100644 index 0000000000..c67d260437 --- /dev/null +++ b/spec/rubocop/cop/rails/deprecated_http_status_names_spec.rb @@ -0,0 +1,194 @@ +# frozen_string_literal: true + +RSpec.describe RuboCop::Cop::Rails::DeprecatedHttpStatusNames, :config do + context 'when Rack is older than 3.1' do + let(:gem_versions) { { 'rack' => '3.0.0' } } + + it 'does nothing' do + expect_no_offenses(<<~RUBY) + render json: { error: 'Invalid data' }, status: :unprocessable_entity + head :payload_too_large + RUBY + end + end + + context 'when Rack is 3.1 or later' do + let(:gem_versions) { { 'rack' => '3.1.0' } } + + context 'with :unprocessable_entity' do + it 'registers an offense when using :unprocessable_entity in render' do + expect_offense(<<~RUBY) + render json: { error: 'Invalid data' }, status: :unprocessable_entity + ^^^^^^^^^^^^^^^^^^^^^ Use `:unprocessable_content` instead of `:unprocessable_entity`. The `:unprocessable_entity` status is deprecated. + RUBY + + expect_correction(<<~RUBY) + render json: { error: 'Invalid data' }, status: :unprocessable_content + RUBY + end + + it 'registers an offense when using :unprocessable_entity in head' do + expect_offense(<<~RUBY) + head :unprocessable_entity + ^^^^^^^^^^^^^^^^^^^^^ Use `:unprocessable_content` instead of `:unprocessable_entity`. The `:unprocessable_entity` status is deprecated. + RUBY + + expect_correction(<<~RUBY) + head :unprocessable_content + RUBY + end + + it 'registers an offense when using :unprocessable_entity in redirect_to' do + expect_offense(<<~RUBY) + redirect_to some_path, status: :unprocessable_entity + ^^^^^^^^^^^^^^^^^^^^^ Use `:unprocessable_content` instead of `:unprocessable_entity`. The `:unprocessable_entity` status is deprecated. + RUBY + + expect_correction(<<~RUBY) + redirect_to some_path, status: :unprocessable_content + RUBY + end + + it 'registers an offense when using :unprocessable_entity in assert_response' do + expect_offense(<<~RUBY) + assert_response :unprocessable_entity + ^^^^^^^^^^^^^^^^^^^^^ Use `:unprocessable_content` instead of `:unprocessable_entity`. The `:unprocessable_entity` status is deprecated. + RUBY + + expect_correction(<<~RUBY) + assert_response :unprocessable_content + RUBY + end + + it 'registers an offense when using :unprocessable_entity in assert_redirected_to' do + expect_offense(<<~RUBY) + assert_redirected_to some_path, status: :unprocessable_entity + ^^^^^^^^^^^^^^^^^^^^^ Use `:unprocessable_content` instead of `:unprocessable_entity`. The `:unprocessable_entity` status is deprecated. + RUBY + + expect_correction(<<~RUBY) + assert_redirected_to some_path, status: :unprocessable_content + RUBY + end + + it 'registers an offense when using :unprocessable_entity in ternary expression' do + expect_offense(<<~RUBY) + render json: { error: 'Invalid data' }, status: some_condition ? :unprocessable_entity : :ok + ^^^^^^^^^^^^^^^^^^^^^ Use `:unprocessable_content` instead of `:unprocessable_entity`. The `:unprocessable_entity` status is deprecated. + RUBY + + expect_correction(<<~RUBY) + render json: { error: 'Invalid data' }, status: some_condition ? :unprocessable_content : :ok + RUBY + end + + it 'does not register an offense when using :unprocessable_content' do + expect_no_offenses(<<~RUBY) + render json: { error: 'Invalid data' }, status: :unprocessable_content + RUBY + end + + it 'does not register an offense when using :unprocessable_entity in hash key' do + expect_no_offenses(<<~RUBY) + { unprocessable_entity: 'Invalid data' } + RUBY + end + end + + context 'with :payload_too_large' do + it 'registers an offense when using :payload_too_large in render' do + expect_offense(<<~RUBY) + render json: { error: 'File too big' }, status: :payload_too_large + ^^^^^^^^^^^^^^^^^^ Use `:content_too_large` instead of `:payload_too_large`. The `:payload_too_large` status is deprecated. + RUBY + + expect_correction(<<~RUBY) + render json: { error: 'File too big' }, status: :content_too_large + RUBY + end + + it 'registers an offense when using :payload_too_large in head' do + expect_offense(<<~RUBY) + head :payload_too_large + ^^^^^^^^^^^^^^^^^^ Use `:content_too_large` instead of `:payload_too_large`. The `:payload_too_large` status is deprecated. + RUBY + + expect_correction(<<~RUBY) + head :content_too_large + RUBY + end + + it 'registers an offense when using :payload_too_large in redirect_to' do + expect_offense(<<~RUBY) + redirect_to some_path, status: :payload_too_large + ^^^^^^^^^^^^^^^^^^ Use `:content_too_large` instead of `:payload_too_large`. The `:payload_too_large` status is deprecated. + RUBY + + expect_correction(<<~RUBY) + redirect_to some_path, status: :content_too_large + RUBY + end + + it 'registers an offense when using :payload_too_large in assert_response' do + expect_offense(<<~RUBY) + assert_response :payload_too_large + ^^^^^^^^^^^^^^^^^^ Use `:content_too_large` instead of `:payload_too_large`. The `:payload_too_large` status is deprecated. + RUBY + + expect_correction(<<~RUBY) + assert_response :content_too_large + RUBY + end + + it 'registers an offense when using :payload_too_large in assert_redirected_to' do + expect_offense(<<~RUBY) + assert_redirected_to some_path, status: :payload_too_large + ^^^^^^^^^^^^^^^^^^ Use `:content_too_large` instead of `:payload_too_large`. The `:payload_too_large` status is deprecated. + RUBY + + expect_correction(<<~RUBY) + assert_redirected_to some_path, status: :content_too_large + RUBY + end + + it 'registers an offense when using :payload_too_large in ternary expression' do + expect_offense(<<~RUBY) + render json: { error: 'File too big' }, status: some_condition ? :payload_too_large : :ok + ^^^^^^^^^^^^^^^^^^ Use `:content_too_large` instead of `:payload_too_large`. The `:payload_too_large` status is deprecated. + RUBY + + expect_correction(<<~RUBY) + render json: { error: 'File too big' }, status: some_condition ? :content_too_large : :ok + RUBY + end + + it 'does not register an offense when using :content_too_large' do + expect_no_offenses(<<~RUBY) + render json: { error: 'File too big' }, status: :content_too_large + RUBY + end + + it 'does not register an offense when using :payload_too_large in hash key' do + expect_no_offenses(<<~RUBY) + { payload_too_large: 'File too big' } + RUBY + end + end + + context 'with mixed deprecated statuses' do + it 'handles multiple deprecated statuses in the same code' do + expect_offense(<<~RUBY) + head :unprocessable_entity + ^^^^^^^^^^^^^^^^^^^^^ Use `:unprocessable_content` instead of `:unprocessable_entity`. The `:unprocessable_entity` status is deprecated. + head :payload_too_large + ^^^^^^^^^^^^^^^^^^ Use `:content_too_large` instead of `:payload_too_large`. The `:payload_too_large` status is deprecated. + RUBY + + expect_correction(<<~RUBY) + head :unprocessable_content + head :content_too_large + RUBY + end + end + end +end diff --git a/spec/rubocop/cop/rails/unprocessable_content_status_spec.rb b/spec/rubocop/cop/rails/unprocessable_content_status_spec.rb deleted file mode 100644 index 5c5f33a3f5..0000000000 --- a/spec/rubocop/cop/rails/unprocessable_content_status_spec.rb +++ /dev/null @@ -1,66 +0,0 @@ -# frozen_string_literal: true - -RSpec.describe RuboCop::Cop::Rails::UnprocessableContentStatus, :config do - context 'when Rack is older than 3.1 or not available' do - before do - allow(cop).to receive(:rack_3_1_or_newer?).and_return(false) - end - - it 'does nothing' do - expect_no_offenses(<<~RUBY) - render json: { error: 'Invalid data' }, status: :unprocessable_entity - RUBY - end - end - - context 'when Rack is 3.1 or later' do - before do - allow(cop).to receive(:rack_3_1_or_newer?).and_return(true) - end - - it 'registers an offense when using :unprocessable_entity in hash argument' do - expect_offense(<<~RUBY) - render json: { error: 'Invalid data' }, status: :unprocessable_entity - ^^^^^^^^^^^^^^^^^^^^^ Use `:unprocessable_content` instead of `:unprocessable_entity`. The `:unprocessable_entity` status is deprecated. - RUBY - - expect_correction(<<~RUBY) - render json: { error: 'Invalid data' }, status: :unprocessable_content - RUBY - end - - it 'registers an offense when using :unprocessable_entity in head' do - expect_offense(<<~RUBY) - head :unprocessable_entity - ^^^^^^^^^^^^^^^^^^^^^ Use `:unprocessable_content` instead of `:unprocessable_entity`. The `:unprocessable_entity` status is deprecated. - RUBY - - expect_correction(<<~RUBY) - head :unprocessable_content - RUBY - end - - it 'registers an offense when using :unprocessable_entity in ternary expression' do - expect_offense(<<~RUBY) - render json: { error: 'Invalid data' }, status: :unprocessable_entity ? :unprocessable_content : :ok - ^^^^^^^^^^^^^^^^^^^^^ Use `:unprocessable_content` instead of `:unprocessable_entity`. The `:unprocessable_entity` status is deprecated. - RUBY - - expect_correction(<<~RUBY) - render json: { error: 'Invalid data' }, status: :unprocessable_content ? :unprocessable_content : :ok - RUBY - end - - it 'does not register an offense when using :unprocessable_content' do - expect_no_offenses(<<~RUBY) - render json: { error: 'Invalid data' }, status: :unprocessable_content - RUBY - end - - it 'does not register an offense when using :unprocessable_entity in hash key' do - expect_no_offenses(<<~RUBY) - { unprocessable_entity: 'Invalid data' } - RUBY - end - end -end From c5c2d7aa8c26b1dfdcb84d202e7bf9446ef5855f Mon Sep 17 00:00:00 2001 From: Kenneth Bogner Date: Mon, 29 Sep 2025 10:32:23 -0400 Subject: [PATCH 3/6] Improve message --- lib/rubocop/cop/rails/deprecated_http_status_names.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rubocop/cop/rails/deprecated_http_status_names.rb b/lib/rubocop/cop/rails/deprecated_http_status_names.rb index ac9796a4be..3709eb331b 100644 --- a/lib/rubocop/cop/rails/deprecated_http_status_names.rb +++ b/lib/rubocop/cop/rails/deprecated_http_status_names.rb @@ -26,7 +26,7 @@ class DeprecatedHttpStatusNames < Base payload_too_large: :content_too_large }.freeze - MSG = 'Use `:%s` instead of `:%s`. The `:%s` status is deprecated.' + MSG = 'Prefer `:%s` over `:%s`.' def_node_matcher :status_method_call, <<~PATTERN { From af2ea770501544ad15be601720dc6f2ae880d297 Mon Sep 17 00:00:00 2001 From: Kenneth Bogner Date: Mon, 29 Sep 2025 10:34:03 -0400 Subject: [PATCH 4/6] Improve descriptions --- config/default.yml | 2 +- lib/rubocop/cop/rails/deprecated_http_status_names.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config/default.yml b/config/default.yml index 82fb968010..c30acb9871 100644 --- a/config/default.yml +++ b/config/default.yml @@ -395,7 +395,7 @@ Rails/DeprecatedActiveModelErrorsMethods: VersionChanged: '2.18' Rails/DeprecatedHttpStatusNames: - Description: 'Use the new HTTP status names instead of deprecated ones.' + Description: 'Enforces consistency by using the current HTTP status names.' Enabled: pending Severity: warning VersionAdded: '<>' diff --git a/lib/rubocop/cop/rails/deprecated_http_status_names.rb b/lib/rubocop/cop/rails/deprecated_http_status_names.rb index 3709eb331b..a371c81932 100644 --- a/lib/rubocop/cop/rails/deprecated_http_status_names.rb +++ b/lib/rubocop/cop/rails/deprecated_http_status_names.rb @@ -3,7 +3,7 @@ module RuboCop module Cop module Rails - # Enforces the use of the current HTTP status names instead of deprecated ones. + # Enforces consistency by using the current HTTP status names. # # @example # # bad From f7ec81e459ab1eb1eeb047fc83bae7bbd382cf13 Mon Sep 17 00:00:00 2001 From: Kenneth Bogner Date: Mon, 29 Sep 2025 10:37:55 -0400 Subject: [PATCH 5/6] Rename cop --- .../new_deprecated_http_status_names_cop.md | 2 +- config/default.yml | 16 +++++----- ...mes.rb => http_status_name_consistency.rb} | 2 +- lib/rubocop/cop/rails_cops.rb | 2 +- ...b => http_status_name_consistency_spec.rb} | 30 +++++++++---------- 5 files changed, 26 insertions(+), 26 deletions(-) rename lib/rubocop/cop/rails/{deprecated_http_status_names.rb => http_status_name_consistency.rb} (98%) rename spec/rubocop/cop/rails/{deprecated_http_status_names_spec.rb => http_status_name_consistency_spec.rb} (74%) diff --git a/changelog/new_deprecated_http_status_names_cop.md b/changelog/new_deprecated_http_status_names_cop.md index d52b47a873..0a26188c4b 100644 --- a/changelog/new_deprecated_http_status_names_cop.md +++ b/changelog/new_deprecated_http_status_names_cop.md @@ -1 +1 @@ -* [#1520](https://github.com/rubocop/rubocop-rails/pull/1520): New `Rails/DeprecatedHttpStatusNames` cop. ([@tuxagon][]) +* [#1520](https://github.com/rubocop/rubocop-rails/pull/1520): New `Rails/HttpStatusNameConsistency` cop. ([@tuxagon][]) diff --git a/config/default.yml b/config/default.yml index c30acb9871..efbd977606 100644 --- a/config/default.yml +++ b/config/default.yml @@ -394,14 +394,6 @@ Rails/DeprecatedActiveModelErrorsMethods: VersionAdded: '2.14' VersionChanged: '2.18' -Rails/DeprecatedHttpStatusNames: - Description: 'Enforces consistency by using the current HTTP status names.' - Enabled: pending - Severity: warning - VersionAdded: '<>' - Include: - - '**/app/controllers/**/*.rb' - Rails/DotSeparatedKeys: Description: 'Enforces the use of dot-separated keys instead of `:scope` options in `I18n` translation methods.' StyleGuide: 'https://rails.rubystyle.guide/#dot-separated-keys' @@ -617,6 +609,14 @@ Rails/HttpStatus: - numeric - symbolic +Rails/HttpStatusNameConsistency: + Description: 'Enforces consistency by using the current HTTP status names.' + Enabled: pending + Severity: warning + VersionAdded: '<>' + Include: + - '**/app/controllers/**/*.rb' + Rails/I18nLazyLookup: Description: 'Checks for places where I18n "lazy" lookup can be used.' StyleGuide: 'https://rails.rubystyle.guide/#lazy-lookup' diff --git a/lib/rubocop/cop/rails/deprecated_http_status_names.rb b/lib/rubocop/cop/rails/http_status_name_consistency.rb similarity index 98% rename from lib/rubocop/cop/rails/deprecated_http_status_names.rb rename to lib/rubocop/cop/rails/http_status_name_consistency.rb index a371c81932..09212f72ac 100644 --- a/lib/rubocop/cop/rails/deprecated_http_status_names.rb +++ b/lib/rubocop/cop/rails/http_status_name_consistency.rb @@ -14,7 +14,7 @@ module Rails # render json: { error: "Invalid data" }, status: :unprocessable_content # head :content_too_large # - class DeprecatedHttpStatusNames < Base + class HttpStatusNameConsistency < Base extend AutoCorrector requires_gem 'rack', '>= 3.1.0' diff --git a/lib/rubocop/cop/rails_cops.rb b/lib/rubocop/cop/rails_cops.rb index 22b106b185..8132d3f1db 100644 --- a/lib/rubocop/cop/rails_cops.rb +++ b/lib/rubocop/cop/rails_cops.rb @@ -41,7 +41,6 @@ require_relative 'rails/delegate' require_relative 'rails/delegate_allow_blank' require_relative 'rails/deprecated_active_model_errors_methods' -require_relative 'rails/deprecated_http_status_names' require_relative 'rails/dot_separated_keys' require_relative 'rails/duplicate_association' require_relative 'rails/duplicate_scope' @@ -67,6 +66,7 @@ require_relative 'rails/helper_instance_variable' require_relative 'rails/http_positional_arguments' require_relative 'rails/http_status' +require_relative 'rails/http_status_name_consistency' require_relative 'rails/i18n_lazy_lookup' require_relative 'rails/i18n_locale_assignment' require_relative 'rails/i18n_locale_texts' diff --git a/spec/rubocop/cop/rails/deprecated_http_status_names_spec.rb b/spec/rubocop/cop/rails/http_status_name_consistency_spec.rb similarity index 74% rename from spec/rubocop/cop/rails/deprecated_http_status_names_spec.rb rename to spec/rubocop/cop/rails/http_status_name_consistency_spec.rb index c67d260437..ae720a1d24 100644 --- a/spec/rubocop/cop/rails/deprecated_http_status_names_spec.rb +++ b/spec/rubocop/cop/rails/http_status_name_consistency_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -RSpec.describe RuboCop::Cop::Rails::DeprecatedHttpStatusNames, :config do +RSpec.describe RuboCop::Cop::Rails::HttpStatusNameConsistency, :config do context 'when Rack is older than 3.1' do let(:gem_versions) { { 'rack' => '3.0.0' } } @@ -19,7 +19,7 @@ it 'registers an offense when using :unprocessable_entity in render' do expect_offense(<<~RUBY) render json: { error: 'Invalid data' }, status: :unprocessable_entity - ^^^^^^^^^^^^^^^^^^^^^ Use `:unprocessable_content` instead of `:unprocessable_entity`. The `:unprocessable_entity` status is deprecated. + ^^^^^^^^^^^^^^^^^^^^^ Prefer `:unprocessable_content` over `:unprocessable_entity`. RUBY expect_correction(<<~RUBY) @@ -30,7 +30,7 @@ it 'registers an offense when using :unprocessable_entity in head' do expect_offense(<<~RUBY) head :unprocessable_entity - ^^^^^^^^^^^^^^^^^^^^^ Use `:unprocessable_content` instead of `:unprocessable_entity`. The `:unprocessable_entity` status is deprecated. + ^^^^^^^^^^^^^^^^^^^^^ Prefer `:unprocessable_content` over `:unprocessable_entity`. RUBY expect_correction(<<~RUBY) @@ -41,7 +41,7 @@ it 'registers an offense when using :unprocessable_entity in redirect_to' do expect_offense(<<~RUBY) redirect_to some_path, status: :unprocessable_entity - ^^^^^^^^^^^^^^^^^^^^^ Use `:unprocessable_content` instead of `:unprocessable_entity`. The `:unprocessable_entity` status is deprecated. + ^^^^^^^^^^^^^^^^^^^^^ Prefer `:unprocessable_content` over `:unprocessable_entity`. RUBY expect_correction(<<~RUBY) @@ -52,7 +52,7 @@ it 'registers an offense when using :unprocessable_entity in assert_response' do expect_offense(<<~RUBY) assert_response :unprocessable_entity - ^^^^^^^^^^^^^^^^^^^^^ Use `:unprocessable_content` instead of `:unprocessable_entity`. The `:unprocessable_entity` status is deprecated. + ^^^^^^^^^^^^^^^^^^^^^ Prefer `:unprocessable_content` over `:unprocessable_entity`. RUBY expect_correction(<<~RUBY) @@ -63,7 +63,7 @@ it 'registers an offense when using :unprocessable_entity in assert_redirected_to' do expect_offense(<<~RUBY) assert_redirected_to some_path, status: :unprocessable_entity - ^^^^^^^^^^^^^^^^^^^^^ Use `:unprocessable_content` instead of `:unprocessable_entity`. The `:unprocessable_entity` status is deprecated. + ^^^^^^^^^^^^^^^^^^^^^ Prefer `:unprocessable_content` over `:unprocessable_entity`. RUBY expect_correction(<<~RUBY) @@ -74,7 +74,7 @@ it 'registers an offense when using :unprocessable_entity in ternary expression' do expect_offense(<<~RUBY) render json: { error: 'Invalid data' }, status: some_condition ? :unprocessable_entity : :ok - ^^^^^^^^^^^^^^^^^^^^^ Use `:unprocessable_content` instead of `:unprocessable_entity`. The `:unprocessable_entity` status is deprecated. + ^^^^^^^^^^^^^^^^^^^^^ Prefer `:unprocessable_content` over `:unprocessable_entity`. RUBY expect_correction(<<~RUBY) @@ -99,7 +99,7 @@ it 'registers an offense when using :payload_too_large in render' do expect_offense(<<~RUBY) render json: { error: 'File too big' }, status: :payload_too_large - ^^^^^^^^^^^^^^^^^^ Use `:content_too_large` instead of `:payload_too_large`. The `:payload_too_large` status is deprecated. + ^^^^^^^^^^^^^^^^^^ Prefer `:content_too_large` over `:payload_too_large`. RUBY expect_correction(<<~RUBY) @@ -110,7 +110,7 @@ it 'registers an offense when using :payload_too_large in head' do expect_offense(<<~RUBY) head :payload_too_large - ^^^^^^^^^^^^^^^^^^ Use `:content_too_large` instead of `:payload_too_large`. The `:payload_too_large` status is deprecated. + ^^^^^^^^^^^^^^^^^^ Prefer `:content_too_large` over `:payload_too_large`. RUBY expect_correction(<<~RUBY) @@ -121,7 +121,7 @@ it 'registers an offense when using :payload_too_large in redirect_to' do expect_offense(<<~RUBY) redirect_to some_path, status: :payload_too_large - ^^^^^^^^^^^^^^^^^^ Use `:content_too_large` instead of `:payload_too_large`. The `:payload_too_large` status is deprecated. + ^^^^^^^^^^^^^^^^^^ Prefer `:content_too_large` over `:payload_too_large`. RUBY expect_correction(<<~RUBY) @@ -132,7 +132,7 @@ it 'registers an offense when using :payload_too_large in assert_response' do expect_offense(<<~RUBY) assert_response :payload_too_large - ^^^^^^^^^^^^^^^^^^ Use `:content_too_large` instead of `:payload_too_large`. The `:payload_too_large` status is deprecated. + ^^^^^^^^^^^^^^^^^^ Prefer `:content_too_large` over `:payload_too_large`. RUBY expect_correction(<<~RUBY) @@ -143,7 +143,7 @@ it 'registers an offense when using :payload_too_large in assert_redirected_to' do expect_offense(<<~RUBY) assert_redirected_to some_path, status: :payload_too_large - ^^^^^^^^^^^^^^^^^^ Use `:content_too_large` instead of `:payload_too_large`. The `:payload_too_large` status is deprecated. + ^^^^^^^^^^^^^^^^^^ Prefer `:content_too_large` over `:payload_too_large`. RUBY expect_correction(<<~RUBY) @@ -154,7 +154,7 @@ it 'registers an offense when using :payload_too_large in ternary expression' do expect_offense(<<~RUBY) render json: { error: 'File too big' }, status: some_condition ? :payload_too_large : :ok - ^^^^^^^^^^^^^^^^^^ Use `:content_too_large` instead of `:payload_too_large`. The `:payload_too_large` status is deprecated. + ^^^^^^^^^^^^^^^^^^ Prefer `:content_too_large` over `:payload_too_large`. RUBY expect_correction(<<~RUBY) @@ -179,9 +179,9 @@ it 'handles multiple deprecated statuses in the same code' do expect_offense(<<~RUBY) head :unprocessable_entity - ^^^^^^^^^^^^^^^^^^^^^ Use `:unprocessable_content` instead of `:unprocessable_entity`. The `:unprocessable_entity` status is deprecated. + ^^^^^^^^^^^^^^^^^^^^^ Prefer `:unprocessable_content` over `:unprocessable_entity`. head :payload_too_large - ^^^^^^^^^^^^^^^^^^ Use `:content_too_large` instead of `:payload_too_large`. The `:payload_too_large` status is deprecated. + ^^^^^^^^^^^^^^^^^^ Prefer `:content_too_large` over `:payload_too_large`. RUBY expect_correction(<<~RUBY) From c8357276e15ea22bd9a28d7a45aa01eee720a9ef Mon Sep 17 00:00:00 2001 From: Kenneth Bogner Date: Mon, 29 Sep 2025 10:42:15 -0400 Subject: [PATCH 6/6] Rename constant --- lib/rubocop/cop/rails/http_status_name_consistency.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/rubocop/cop/rails/http_status_name_consistency.rb b/lib/rubocop/cop/rails/http_status_name_consistency.rb index 09212f72ac..89ac0bb3bd 100644 --- a/lib/rubocop/cop/rails/http_status_name_consistency.rb +++ b/lib/rubocop/cop/rails/http_status_name_consistency.rb @@ -21,7 +21,7 @@ class HttpStatusNameConsistency < Base RESTRICT_ON_SEND = %i[render redirect_to head assert_response assert_redirected_to].freeze - DEPRECATED_STATUSES = { + PREFERRED_STATUSES = { unprocessable_entity: :unprocessable_content, payload_too_large: :content_too_large }.freeze @@ -59,9 +59,9 @@ def on_send(node) private def find_deprecated_status_names(node) - if node.sym_type? && DEPRECATED_STATUSES.key?(node.value) + if node.sym_type? && PREFERRED_STATUSES.key?(node.value) deprecated_status = node.value - preferred_status = DEPRECATED_STATUSES[deprecated_status] + preferred_status = PREFERRED_STATUSES[deprecated_status] message = format(MSG, deprecated: deprecated_status, preferred: preferred_status)