forked from rubocop/rubocop
/
hash_syntax.rb
214 lines (185 loc) · 6.45 KB
/
hash_syntax.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
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
# frozen_string_literal: true
module RuboCop
module Cop
module Style
# This cop checks hash literal syntax.
#
# It can enforce either the use of the class hash rocket syntax or
# the use of the newer Ruby 1.9 syntax (when applicable).
#
# A separate offense is registered for each problematic pair.
#
# The supported styles are:
#
# * ruby19 - forces use of the 1.9 syntax (e.g. `{a: 1}`) when hashes have
# all symbols for keys
# * hash_rockets - forces use of hash rockets for all hashes
# * no_mixed_keys - simply checks for hashes with mixed syntaxes
# * ruby19_no_mixed_keys - forces use of ruby 1.9 syntax and forbids mixed
# syntax hashes
#
# @example EnforcedStyle: ruby19 (default)
# # bad
# {:a => 2}
# {b: 1, :c => 2}
#
# # good
# {a: 2, b: 1}
# {:c => 2, 'd' => 2} # acceptable since 'd' isn't a symbol
# {d: 1, 'e' => 2} # technically not forbidden
#
# @example EnforcedStyle: hash_rockets
# # bad
# {a: 1, b: 2}
# {c: 1, 'd' => 5}
#
# # good
# {:a => 1, :b => 2}
#
# @example EnforcedStyle: no_mixed_keys
# # bad
# {:a => 1, b: 2}
# {c: 1, 'd' => 2}
#
# # good
# {:a => 1, :b => 2}
# {c: 1, d: 2}
#
# @example EnforcedStyle: ruby19_no_mixed_keys
# # bad
# {:a => 1, :b => 2}
# {c: 2, 'd' => 3} # should just use hash rockets
#
# # good
# {a: 1, b: 2}
# {:c => 3, 'd' => 4}
class HashSyntax < Cop
include ConfigurableEnforcedStyle
include RangeHelp
MSG_19 = 'Use the new Ruby 1.9 hash syntax.'.freeze
MSG_NO_MIXED_KEYS = "Don't mix styles in the same hash.".freeze
MSG_HASH_ROCKETS = 'Use hash rockets syntax.'.freeze
def on_hash(node)
pairs = node.pairs
return if pairs.empty?
if style == :hash_rockets || force_hash_rockets?(pairs)
hash_rockets_check(pairs)
elsif style == :ruby19_no_mixed_keys
ruby19_no_mixed_keys_check(pairs)
elsif style == :no_mixed_keys
no_mixed_keys_check(pairs)
else
ruby19_check(pairs)
end
end
def ruby19_check(pairs)
check(pairs, '=>', MSG_19) if sym_indices?(pairs)
end
def hash_rockets_check(pairs)
check(pairs, ':', MSG_HASH_ROCKETS)
end
def ruby19_no_mixed_keys_check(pairs)
if force_hash_rockets?(pairs)
check(pairs, ':', MSG_HASH_ROCKETS)
elsif sym_indices?(pairs)
check(pairs, '=>', MSG_19)
else
check(pairs, ':', MSG_NO_MIXED_KEYS)
end
end
def no_mixed_keys_check(pairs)
if !sym_indices?(pairs)
check(pairs, ':', MSG_NO_MIXED_KEYS)
else
check(pairs, pairs.first.inverse_delimiter, MSG_NO_MIXED_KEYS)
end
end
def autocorrect(node)
lambda do |corrector|
if style == :hash_rockets || force_hash_rockets?(node.parent.pairs)
autocorrect_hash_rockets(corrector, node)
elsif style == :ruby19_no_mixed_keys || style == :no_mixed_keys
autocorrect_no_mixed_keys(corrector, node)
else
autocorrect_ruby19(corrector, node)
end
end
end
def alternative_style
case style
when :hash_rockets then
:ruby19
when :ruby19, :ruby19_no_mixed_keys then
:hash_rockets
end
end
private
def sym_indices?(pairs)
pairs.all? { |p| word_symbol_pair?(p) }
end
def word_symbol_pair?(pair)
return false unless pair.key.sym_type? || pair.key.dsym_type?
acceptable_19_syntax_symbol?(pair.key.source)
end
def acceptable_19_syntax_symbol?(sym_name)
sym_name.sub!(/\A:/, '')
if cop_config['PreferHashRocketsForNonAlnumEndingSymbols']
# Prefer { :production? => false } over { production?: false } and
# similarly for other non-alnum final characters (except quotes,
# to prefer { "x y": 1 } over { :"x y" => 1 }).
return false unless sym_name =~ /[\p{Alnum}"']\z/
end
# Most hash keys can be matched against a simple regex.
return true if sym_name =~ /\A[_a-z]\w*[?!]?\z/i
# For more complicated hash keys, let the parser validate the syntax.
parse("{ #{sym_name}: :foo }").valid_syntax?
end
def check(pairs, delim, msg)
pairs.each do |pair|
if pair.delimiter == delim
location = pair.source_range.begin.join(pair.loc.operator)
add_offense(pair, location: location, message: msg) do
opposite_style_detected
end
else
correct_style_detected
end
end
end
def autocorrect_ruby19(corrector, pair_node)
key = pair_node.key
op = pair_node.loc.operator
range = range_between(key.source_range.begin_pos, op.end_pos)
range = range_with_surrounding_space(range: range, side: :right)
space = argument_without_space?(pair_node.parent) ? ' ' : ''
corrector.replace(
range,
range.source.sub(/^:(.*\S)\s*=>\s*$/, space.to_s + '\1: ')
)
end
def argument_without_space?(node)
node.argument? &&
node.loc.expression.begin_pos == node.parent.loc.selector.end_pos
end
def autocorrect_hash_rockets(corrector, pair_node)
key = pair_node.key.source_range
op = pair_node.loc.operator
corrector.insert_after(key, pair_node.inverse_delimiter(true))
corrector.insert_before(key, ':')
corrector.remove(range_with_surrounding_space(range: op))
end
def autocorrect_no_mixed_keys(corrector, pair_node)
if pair_node.colon?
autocorrect_hash_rockets(corrector, pair_node)
else
autocorrect_ruby19(corrector, pair_node)
end
end
def force_hash_rockets?(pairs)
cop_config['UseHashRocketsWithSymbolValues'] &&
pairs.map(&:value).any?(&:sym_type?)
end
end
end
end
end