diff --git a/changelog/new_json_symbolize_names_cop.md b/changelog/new_json_symbolize_names_cop.md new file mode 100644 index 0000000000..2bc8899221 --- /dev/null +++ b/changelog/new_json_symbolize_names_cop.md @@ -0,0 +1 @@ +* [#1532](https://github.com/rubocop/rubocop-rails/pull/1532): Add new `Rails/JSONSymbolizeNames` cop. ([@viralpraxis][]) diff --git a/config/default.yml b/config/default.yml index 74bebd3142..fd9d7fbfea 100644 --- a/config/default.yml +++ b/config/default.yml @@ -682,6 +682,11 @@ Rails/InverseOf: Include: - '**/app/models/**/*.rb' +Rails/JSONSymbolizeNames: + Description: 'Use `JSON.parse(json, symbolize_names: true)` instead of `JSON.parse(json).deep_symbolize_keys`.' + Enabled: pending + VersionAdded: '<>' + Rails/LexicallyScopedActionFilter: Description: "Checks that methods specified in the filter's `only` or `except` options are explicitly defined in the class." StyleGuide: 'https://rails.rubystyle.guide#lexically-scoped-action-filter' diff --git a/lib/rubocop/cop/rails/json_symbolize_names.rb b/lib/rubocop/cop/rails/json_symbolize_names.rb new file mode 100644 index 0000000000..89b948d2a8 --- /dev/null +++ b/lib/rubocop/cop/rails/json_symbolize_names.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + module Rails + # Use `JSON.parse(json, symbolize_names: true)` instead of `JSON.parse(json).deep_symbolize_keys`. + + # Using `symbolize_names: true` is more efficient as it creates symbols during parsing + # rather than requiring a second pass through the data structure. + + # @example + # # bad + # JSON.parse(json).deep_symbolize_keys + # + # # good + # JSON.parse(json, symbolize_names: true) + # + class JSONSymbolizeNames < Base + MSG = 'Use `symbolize_names` option.' + + RESTRICT_ON_SEND = %i[deep_symbolize_keys].to_set.freeze + + JSON_PARSING_METHOD_NAMES = %i[load_file load_file! parse parse!].to_set.freeze + + # @!method deep_symbolize_keys?(node) + def_node_matcher :deep_symbolize_keys?, <<~PATTERN + (call + (send (const {nil? cbase} :JSON) JSON_PARSING_METHOD_NAMES ...) :deep_symbolize_keys) + PATTERN + + def on_send(node) + deep_symbolize_keys?(node) do + add_offense(node) + end + end + alias on_csend on_send + end + end + end +end diff --git a/lib/rubocop/cop/rails_cops.rb b/lib/rubocop/cop/rails_cops.rb index a8c7202dac..e9c5a19e84 100644 --- a/lib/rubocop/cop/rails_cops.rb +++ b/lib/rubocop/cop/rails_cops.rb @@ -75,6 +75,7 @@ require_relative 'rails/index_with' require_relative 'rails/inquiry' require_relative 'rails/inverse_of' +require_relative 'rails/json_symbolize_names' require_relative 'rails/lexically_scoped_action_filter' require_relative 'rails/link_to_blank' require_relative 'rails/mailer_name' diff --git a/spec/rubocop/cop/rails/json_symbolize_names_spec.rb b/spec/rubocop/cop/rails/json_symbolize_names_spec.rb new file mode 100644 index 0000000000..4e9516b354 --- /dev/null +++ b/spec/rubocop/cop/rails/json_symbolize_names_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +RSpec.describe RuboCop::Cop::Rails::JSONSymbolizeNames, :config do + %i[load_file load_file! parse parse!].each do |method_name| + context "with `#{method_name}` method" do + it "registers an offense for `JSON.#{method_name}` followed by `deep_symbolize_keys`" do + expect_offense(<<~RUBY, method_name: method_name) + JSON.#{method_name}(json).deep_symbolize_keys + ^^^^^^{method_name}^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `symbolize_names` option. + RUBY + end + + it "registers an offense for `::JSON.#{method_name}` followed by `deep_symbolize_keys`" do + expect_offense(<<~RUBY, method_name: method_name) + ::JSON.#{method_name}(json).deep_symbolize_keys + ^^^^^^^^{method_name}^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `symbolize_names` option. + RUBY + end + + it "registers an offense for `::JSON.#{method_name}` followed by `deep_symbolize_keys` with safe navigation" do + expect_offense(<<~RUBY, method_name: method_name) + ::JSON.#{method_name}("null")&.deep_symbolize_keys + ^^^^^^^^{method_name}^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `symbolize_names` option. + RUBY + end + + it "does not register for `JSON.#{method_name}` with `symbolize_names` option" do + expect_no_offenses(<<~RUBY) + JSON.#{method_name}(json, symbolize_names: true) + RUBY + end + + it "does not register for single `JSON.#{method_name}`" do + expect_no_offenses(<<~RUBY) + JSON.#{method_name}(json) + RUBY + end + end + end +end