forked from rubocop/rubocop
/
symbol_conversion.rb
179 lines (154 loc) · 5.46 KB
/
symbol_conversion.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
# frozen_string_literal: true
module RuboCop
module Cop
module Lint
# This cop checks for uses of literal strings converted to
# a symbol where a literal symbol could be used instead.
#
# There are two possible styles for this cop.
# `strict` (default) will register an offense for any incorrect usage.
# `consistent` additionally requires hashes to use the same style for
# every symbol key (ie. if any symbol key needs to be quoted it requires
# all keys to be quoted).
#
# @example
# # bad
# 'string'.to_sym
# :symbol.to_sym
# 'underscored_string'.to_sym
# :'underscored_symbol'
# 'hyphenated-string'.to_sym
#
# # good
# :string
# :symbol
# :underscored_string
# :underscored_symbol
# :'hyphenated-string'
#
# @example EnforcedStyle: strict (default)
#
# # bad
# {
# 'a': 1,
# "b": 2,
# 'c-d': 3
# }
#
# # good (don't quote keys that don't require quoting)
# {
# a: 1,
# b: 2,
# 'c-d': 3
# }
#
# @example EnforcedStyle: consistent
#
# # bad
# {
# a: 1,
# 'b-c': 2
# }
#
# # good (quote all keys if any need quoting)
# {
# 'a': 1,
# 'b-c': 2
# }
#
# # good (no quoting required)
# {
# a: 1,
# b: 2
# }
#
class SymbolConversion < Base
extend AutoCorrector
include ConfigurableEnforcedStyle
include SymbolHelp
MSG = 'Unnecessary symbol conversion; use `%<correction>s` instead.'
MSG_CONSISTENCY = 'Symbol hash key should be quoted for consistency; ' \
'use `%<correction>s` instead.'
RESTRICT_ON_SEND = %i[to_sym intern].freeze
def on_send(node)
return unless node.receiver
return unless node.receiver.str_type? || node.receiver.sym_type?
register_offense(node, correction: node.receiver.value.to_sym.inspect)
end
def on_sym(node)
return if ignored_node?(node) || properly_quoted?(node.source, node.value.inspect)
# `alias` arguments are symbols but since a symbol that requires
# being quoted is not a valid method identifier, it can be ignored
return if in_alias?(node)
# The `%I[]` and `%i[]` macros are parsed as normal arrays of symbols
# so they need to be ignored.
return if in_percent_literal_array?(node)
# Symbol hash keys have a different format and need to be handled separately
return correct_hash_key(node) if hash_key?(node)
register_offense(node, correction: node.value.inspect)
end
def on_hash(node)
# For `EnforcedStyle: strict`, hash keys are evaluated in `on_sym`
return unless style == :consistent
keys = node.keys.select(&:sym_type?)
if keys.any? { |key| requires_quotes?(key) }
correct_inconsistent_hash_keys(keys)
else
# If there are no symbol keys requiring quoting,
# treat the hash like `EnforcedStyle: strict`.
keys.each { |key| correct_hash_key(key) }
end
end
private
def register_offense(node, correction:, message: format(MSG, correction: correction))
add_offense(node, message: message) { |corrector| corrector.replace(node, correction) }
end
def properly_quoted?(source, value)
return true if style == :strict && (!source.match?(/['"]/) || value.end_with?('='))
source == value ||
# `Symbol#inspect` uses double quotes, but allow single-quoted
# symbols to work as well.
source.tr("'", '"') == value
end
def requires_quotes?(sym_node)
sym_node.value.inspect.match?(/^:".*?"|=$/)
end
def in_alias?(node)
node.parent&.alias_type?
end
def in_percent_literal_array?(node)
node.parent&.array_type? && node.parent&.percent_literal?
end
def correct_hash_key(node)
# Although some operators can be converted to symbols normally
# (ie. `:==`), these are not accepted as hash keys and will
# raise a syntax error (eg. `{ ==: ... }`). Therefore, if the
# symbol does not start with an alphanumeric or underscore, it
# will be ignored.
return unless node.value.to_s.match?(/\A[a-z0-9_]/i)
correction = node.value.inspect
correction = correction.delete_prefix(':') if node.parent.colon?
return if properly_quoted?(node.source, correction)
register_offense(
node,
correction: correction,
message: format(MSG, correction: node.parent.colon? ? "#{correction}:" : correction)
)
end
def correct_inconsistent_hash_keys(keys)
keys.each do |key|
ignore_node(key)
next if requires_quotes?(key)
next if properly_quoted?(key.source, %("#{key.value}"))
correction = %("#{key.value}")
register_offense(
key,
correction: correction,
message: format(MSG_CONSISTENCY, correction: "#{correction}:")
)
end
end
end
end
end
end