forked from rubocop/rubocop
/
magic_comment.rb
272 lines (233 loc) · 7.67 KB
/
magic_comment.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
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
# frozen_string_literal: true
module RuboCop
# Parse different formats of magic comments.
#
# @abstract parent of three different magic comment handlers
class MagicComment
# @see https://git.io/vMC1C IRB's pattern for matching magic comment tokens
TOKEN = /[[:alnum:]\-_]+/.freeze
KEYWORDS = {
encoding: '(?:en)?coding',
frozen_string_literal: 'frozen[_-]string[_-]literal',
shareable_constant_value: 'shareable[_-]constant[_-]value'
}.freeze
# Detect magic comment format and pass it to the appropriate wrapper.
#
# @param comment [String]
#
# @return [RuboCop::MagicComment]
def self.parse(comment)
case comment
when EmacsComment::REGEXP then EmacsComment.new(comment)
when VimComment::REGEXP then VimComment.new(comment)
else
SimpleComment.new(comment)
end
end
def initialize(comment)
@comment = comment
end
def any?
frozen_string_literal_specified? || encoding_specified? || shareable_constant_value_specified?
end
def valid?
@comment.start_with?('#') && any?
end
# Does the magic comment enable the frozen string literal feature.
#
# Test whether the frozen string literal value is `true`. Cannot
# just return `frozen_string_literal` since an invalid magic comment
# like `# frozen_string_literal: yes` is possible and the truthy value
# `'yes'` does not actually enable the feature
#
# @return [Boolean]
def frozen_string_literal?
frozen_string_literal == true
end
def valid_literal_value?
[true, false].include?(frozen_string_literal)
end
def valid_shareable_constant_value?
%w[none literal experimental_everything experimental_copy].include?(shareable_constant_value)
end
# Was a magic comment for the frozen string literal found?
#
# @return [Boolean]
def frozen_string_literal_specified?
specified?(frozen_string_literal)
end
# Was a shareable_constant_value specified?
#
# @return [Boolean]
def shareable_constant_value_specified?
specified?(shareable_constant_value)
end
# Expose the `frozen_string_literal` value coerced to a boolean if possible.
#
# @return [Boolean] if value is `true` or `false`
# @return [nil] if frozen_string_literal comment isn't found
# @return [String] if comment is found but isn't true or false
def frozen_string_literal
return unless (setting = extract_frozen_string_literal)
case setting
when 'true' then true
when 'false' then false
else
setting
end
end
# Expose the `shareable_constant_value` value coerced to a boolean if possible.
#
# @return [String] for shareable_constant_value config
def shareable_constant_value
extract_shareable_constant_value
end
def encoding_specified?
specified?(encoding)
end
private
def specified?(value)
!value.nil?
end
# Match the entire comment string with a pattern and take the first capture.
#
# @param pattern [Regexp]
#
# @return [String] if pattern matched
# @return [nil] otherwise
def extract(pattern)
@comment[pattern, 1]
end
# Parent to Vim and Emacs magic comment handling.
#
# @abstract
class EditorComment < MagicComment
def encoding
match(self.class::KEYWORDS[:encoding])
end
# Rewrite the comment without a given token type
def without(type)
remaining = tokens.grep_v(/\A#{self.class::KEYWORDS[type.to_sym]}/)
return '' if remaining.empty?
self.class::FORMAT % remaining.join(self.class::SEPARATOR)
end
private
# Find a token starting with the provided keyword and extract its value.
#
# @param keyword [String]
#
# @return [String] extracted value if it is found
# @return [nil] otherwise
def match(keyword)
pattern = /\A#{keyword}\s*#{self.class::OPERATOR}\s*(#{TOKEN})\z/
tokens.each do |token|
next unless (value = token[pattern, 1])
return value.downcase
end
nil
end
# Individual tokens composing an editor specific comment string.
#
# @return [Array<String>]
def tokens
extract(self.class::REGEXP).split(self.class::SEPARATOR).map(&:strip)
end
end
# Wrapper for Emacs style magic comments.
#
# @example Emacs style comment
# comment = RuboCop::MagicComment.parse(
# '# -*- encoding: ASCII-8BIT -*-'
# )
#
# comment.encoding # => 'ascii-8bit'
#
# @see https://www.gnu.org/software/emacs/manual/html_node/emacs/Specify-Coding.html
# @see https://git.io/vMCXh Emacs handling in Ruby's parse.y
class EmacsComment < EditorComment
REGEXP = /-\*-(.+)-\*-/.freeze
FORMAT = '# -*- %s -*-'
SEPARATOR = ';'
OPERATOR = ':'
private
def extract_frozen_string_literal
match(KEYWORDS[:frozen_string_literal])
end
def extract_shareable_constant_value
match(KEYWORDS[:shareable_constant_value])
end
end
# Wrapper for Vim style magic comments.
#
# @example Vim style comment
# comment = RuboCop::MagicComment.parse(
# '# vim: filetype=ruby, fileencoding=ascii-8bit'
# )
#
# comment.encoding # => 'ascii-8bit'
class VimComment < EditorComment
REGEXP = /#\s*vim:\s*(.+)/.freeze
FORMAT = '# vim: %s'
SEPARATOR = ', '
OPERATOR = '='
KEYWORDS = MagicComment::KEYWORDS.merge(encoding: 'fileencoding').freeze
# For some reason the fileencoding keyword only works if there
# is at least one other token included in the string. For example
#
# # works
# # vim: foo=bar, fileencoding=ascii-8bit
#
# # does nothing
# # vim: foo=bar, fileencoding=ascii-8bit
#
def encoding
super if tokens.size > 1
end
# Vim comments cannot specify frozen string literal behavior.
def frozen_string_literal; end
# Vim comments cannot specify shareable constant values behavior.
def shareable_constant_value; end
end
# Wrapper for regular magic comments not bound to an editor.
#
# Simple comments can only specify one setting per comment.
#
# @example frozen string literal comments
# comment1 = RuboCop::MagicComment.parse('# frozen_string_literal: true')
# comment1.frozen_string_literal # => true
# comment1.encoding # => nil
#
# @example encoding comments
# comment2 = RuboCop::MagicComment.parse('# encoding: utf-8')
# comment2.frozen_string_literal # => nil
# comment2.encoding # => 'utf-8'
class SimpleComment < MagicComment
# Match `encoding` or `coding`
def encoding
extract(/\A\s*\#.*\b#{KEYWORDS[:encoding]}: (#{TOKEN})/io)
end
# Rewrite the comment without a given token type
def without(type)
if @comment.match?(/\A#\s*#{self.class::KEYWORDS[type.to_sym]}/)
''
else
@comment
end
end
private
# Extract `frozen_string_literal`.
#
# The `frozen_string_literal` magic comment only works if it
# is the only text in the comment.
#
# Case-insensitive and dashes/underscores are acceptable.
# @see https://git.io/vM7Mg
def extract_frozen_string_literal
extract(/\A\s*#\s*#{KEYWORDS[:frozen_string_literal]}:\s*(#{TOKEN})\s*\z/io)
end
def extract_shareable_constant_value
extract(/\A\s*#\s*#{KEYWORDS[:shareable_constant_value]}:\s*(#{TOKEN})\s*\z/io)
end
end
end
end