Skip to content

Commit 5d3d3cd

Browse files
committed
Add new Rails/JSONSymbolizeNames cop
I've seen this pattern a lot: ```ruby JSON.parse(large_json).deep_symbolize_keys ``` instead of travesing Ruby hash once more we can use the `symbolize_names` option: ```ruby JSON.parse(large_json, symbolize_names: true) ``` Caveats / FP scenarios: 1. `symbolize_names` does not work if `create_addition` option is provided. 2. User might use both `symbolize_names: false` and `deep_symbolize_keys`. 3. There's no autocorrection yet, but it should be easy to add.
1 parent 60dd19b commit 5d3d3cd

File tree

5 files changed

+87
-0
lines changed

5 files changed

+87
-0
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* [#1532](https://github.com/rubocop/rubocop-rails/pull/1532): Add new `Rails/JSONSymbolizeNames` cop. ([@viralpraxis][])

config/default.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -682,6 +682,11 @@ Rails/InverseOf:
682682
Include:
683683
- '**/app/models/**/*.rb'
684684

685+
Rails/JSONSymbolizeNames:
686+
Description: 'Use `JSON.parse(json, symbolize_names: true)` instead of `JSON.parse(json).deep_symbolize_keys`.'
687+
Enabled: pending
688+
VersionAdded: '<<next>>'
689+
685690
Rails/LexicallyScopedActionFilter:
686691
Description: "Checks that methods specified in the filter's `only` or `except` options are explicitly defined in the class."
687692
StyleGuide: 'https://rails.rubystyle.guide#lexically-scoped-action-filter'
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# frozen_string_literal: true
2+
3+
module RuboCop
4+
module Cop
5+
module Rails
6+
# Use `JSON.parse(json, symbolize_names: true)` instead of `JSON.parse(json).deep_symbolize_keys`.
7+
8+
# Using `symbolize_names: true` is more efficient as it creates symbols during parsing
9+
# rather than requiring a second pass through the data structure.
10+
11+
# @example
12+
# # bad
13+
# JSON.parse(json).deep_symbolize_keys
14+
#
15+
# # good
16+
# JSON.parse(json, symbolize_names: true)
17+
#
18+
class JSONSymbolizeNames < Base
19+
MSG = 'Use `symbolize_names` option.'
20+
21+
RESTRICT_ON_SEND = %i[deep_symbolize_keys].to_set.freeze
22+
23+
JSON_PARSING_METHOD_NAMES = %i[load_file load_file! parse parse!].to_set.freeze
24+
25+
# @!method deep_symbolize_keys?(node)
26+
def_node_matcher :deep_symbolize_keys?, <<~PATTERN
27+
(call
28+
(send (const {nil? cbase} :JSON) JSON_PARSING_METHOD_NAMES ...) :deep_symbolize_keys)
29+
PATTERN
30+
31+
def on_send(node)
32+
deep_symbolize_keys?(node) do
33+
add_offense(node)
34+
end
35+
end
36+
alias on_csend on_send
37+
end
38+
end
39+
end
40+
end

lib/rubocop/cop/rails_cops.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
require_relative 'rails/index_with'
7676
require_relative 'rails/inquiry'
7777
require_relative 'rails/inverse_of'
78+
require_relative 'rails/json_symbolize_names'
7879
require_relative 'rails/lexically_scoped_action_filter'
7980
require_relative 'rails/link_to_blank'
8081
require_relative 'rails/mailer_name'
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.describe RuboCop::Cop::Rails::JSONSymbolizeNames, :config do
4+
%i[load_file load_file! parse parse!].each do |method_name|
5+
context "with `#{method_name}` method" do
6+
it "registers an offense for `JSON.#{method_name}` followed by `deep_symbolize_keys`" do
7+
expect_offense(<<~RUBY, method_name: method_name)
8+
JSON.#{method_name}(json).deep_symbolize_keys
9+
^^^^^^{method_name}^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `symbolize_names` option.
10+
RUBY
11+
end
12+
13+
it "registers an offense for `::JSON.#{method_name}` followed by `deep_symbolize_keys`" do
14+
expect_offense(<<~RUBY, method_name: method_name)
15+
::JSON.#{method_name}(json).deep_symbolize_keys
16+
^^^^^^^^{method_name}^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `symbolize_names` option.
17+
RUBY
18+
end
19+
20+
it "registers an offense for `::JSON.#{method_name}` followed by `deep_symbolize_keys` with safe navigation" do
21+
expect_offense(<<~RUBY, method_name: method_name)
22+
::JSON.#{method_name}("null")&.deep_symbolize_keys
23+
^^^^^^^^{method_name}^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `symbolize_names` option.
24+
RUBY
25+
end
26+
27+
it "does not register for `JSON.#{method_name}` with `symbolize_names` option" do
28+
expect_no_offenses(<<~RUBY)
29+
JSON.#{method_name}(json, symbolize_names: true)
30+
RUBY
31+
end
32+
33+
it "does not register for single `JSON.#{method_name}`" do
34+
expect_no_offenses(<<~RUBY)
35+
JSON.#{method_name}(json)
36+
RUBY
37+
end
38+
end
39+
end
40+
end

0 commit comments

Comments
 (0)