vinter 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/LICENSE +13 -0
- data/README.md +12 -12
- data/bin/vinter +0 -0
- data/lib/vinter/cli.rb +49 -21
- data/lib/vinter/lexer.rb +468 -36
- data/lib/vinter/linter.rb +28 -3
- data/lib/vinter/parser.rb +3808 -577
- data/lib/vinter.rb +1 -1
- metadata +5 -4
data/lib/vinter/lexer.rb
CHANGED
@@ -2,17 +2,18 @@ module Vinter
|
|
2
2
|
class Lexer
|
3
3
|
TOKEN_TYPES = {
|
4
4
|
# Vim9 specific keywords
|
5
|
-
keyword: /\b(if|else|elseif|endif|while|endwhile|for|endfor|def|enddef|function|endfunction|return|const|var|final|import|export|class|extends|static|enum|type|vim9script|abort)\b/,
|
5
|
+
keyword: /\b(if|else|elseif|endif|while|endwhile|for|endfor|def|enddef|function|endfunction|endfunc|return|const|var|final|import|export|class|extends|static|enum|type|vim9script|abort|autocmd|echom|echoerr|echohl|echomsg|let|unlet|execute|exec|continue|break|try|catch|finally|endtry|throw|runtime|silent|delete|command|call|set|setlocal|syntax|highlight|sleep|source|nnoremap|nmap|inoremap|imap|vnoremap|vmap|xnoremap|xmap|cnoremap|cmap|noremap|map)\b/,
|
6
6
|
# Identifiers can include # and special characters
|
7
7
|
identifier: /\b[a-zA-Z_][a-zA-Z0-9_#]*\b/,
|
8
8
|
# Single-character operators
|
9
|
-
operator: /[
|
9
|
+
operator: /[\+\-\*\/=%<>!&\|\.]/,
|
10
10
|
# Multi-character operators handled separately
|
11
|
-
number: /\b
|
11
|
+
number: /\b(0[xX][0-9A-Fa-f]+|0[oO][0-7]+|0[bB][01]+|\d+(\.\d+)?([eE][+-]?\d+)?[smh]?)\b/,
|
12
12
|
# Handle both single and double quoted strings
|
13
|
-
string: /"([^"
|
13
|
+
# string: /"(\\"|[^"])*"|'(\\'|[^'])*'/,
|
14
|
+
register_access: /@[a-zA-Z0-9":.%#=*+~_\/\-]/,
|
14
15
|
# Vim9 comments use #
|
15
|
-
comment:
|
16
|
+
comment: /(#|").*/,
|
16
17
|
whitespace: /\s+/,
|
17
18
|
brace_open: /\{/,
|
18
19
|
brace_close: /\}/,
|
@@ -23,8 +24,12 @@ module Vinter
|
|
23
24
|
colon: /:/,
|
24
25
|
semicolon: /;/,
|
25
26
|
comma: /,/,
|
27
|
+
backslash: /\\/,
|
28
|
+
question_mark: /\?/,
|
29
|
+
command_separator: /\|/,
|
26
30
|
}
|
27
31
|
|
32
|
+
CONTINUATION_OPERATORS = %w(. .. + - * / = == ==# ==? != > < >= <= && || ? : -> =>)
|
28
33
|
def initialize(input)
|
29
34
|
@input = input
|
30
35
|
@tokens = []
|
@@ -33,36 +38,428 @@ module Vinter
|
|
33
38
|
@column = 1
|
34
39
|
end
|
35
40
|
|
41
|
+
def should_parse_as_regex
|
42
|
+
# Look at recent tokens to determine if we're in a regex context
|
43
|
+
recent_tokens = @tokens.last(3)
|
44
|
+
|
45
|
+
# Check for contexts where regex is expected
|
46
|
+
return true if recent_tokens.any? { |t|
|
47
|
+
t && t[:type] == :keyword && ['syntax'].include?(t[:value])
|
48
|
+
}
|
49
|
+
|
50
|
+
return true if recent_tokens.any? { |t|
|
51
|
+
t && t[:type] == :identifier && ['match', 'region', 'keyword'].include?(t[:value])
|
52
|
+
}
|
53
|
+
|
54
|
+
# Check for comparison operators that often use regex
|
55
|
+
return true if recent_tokens.any? { |t|
|
56
|
+
t && t[:type] == :operator && ['=~', '!~', '=~#', '!~#', '=~?', '!~?'].include?(t[:value])
|
57
|
+
}
|
58
|
+
|
59
|
+
false
|
60
|
+
end
|
61
|
+
|
62
|
+
def find_unescaped_newline(chunk)
|
63
|
+
i = 0
|
64
|
+
while i < chunk.length
|
65
|
+
if chunk[i] == "\n" && (i == 0 || chunk[i - 1] != '\\')
|
66
|
+
return i
|
67
|
+
end
|
68
|
+
i += 1
|
69
|
+
end
|
70
|
+
nil # Return nil if no unescaped newline is found
|
71
|
+
end
|
72
|
+
|
36
73
|
def tokenize
|
37
74
|
until @position >= @input.length
|
38
75
|
chunk = @input[@position..-1]
|
76
|
+
|
77
|
+
# First check if the line starts with a quote (comment in Vim)
|
78
|
+
# Check if we're at the beginning of a line (optionally after whitespace)
|
79
|
+
line_start = @position == 0 || @input[@position - 1] == "\n"
|
80
|
+
if !line_start
|
81
|
+
# Check if we're after whitespace at the start of a line
|
82
|
+
temp_pos = @position - 1
|
83
|
+
while temp_pos >= 0 && @input[temp_pos] =~ /[ \t]/
|
84
|
+
temp_pos -= 1
|
85
|
+
end
|
86
|
+
line_start = temp_pos < 0 || @input[temp_pos] == "\n"
|
87
|
+
end
|
39
88
|
|
40
|
-
#
|
41
|
-
if
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
89
|
+
# If we're at the start of a line and it begins with a quote
|
90
|
+
if line_start && chunk.start_with?('"')
|
91
|
+
# Find the end of the line
|
92
|
+
line_end = find_unescaped_newline(chunk) || chunk.length
|
93
|
+
comment_text = chunk[0...line_end]
|
94
|
+
|
95
|
+
@tokens << {
|
96
|
+
type: :comment,
|
97
|
+
value: comment_text,
|
98
|
+
line: @line_num,
|
99
|
+
column: @column
|
100
|
+
}
|
101
|
+
|
102
|
+
@position += comment_text.length
|
103
|
+
@column += comment_text.length
|
104
|
+
next
|
105
|
+
end
|
106
|
+
# Handle string literals manually
|
107
|
+
if chunk.start_with?("'") || chunk.start_with?('"')
|
108
|
+
quote = chunk[0]
|
109
|
+
i = 1
|
110
|
+
escaped = false
|
111
|
+
string_value = quote
|
112
|
+
|
113
|
+
# Keep going until we find an unescaped closing quote
|
114
|
+
while i < chunk.length
|
115
|
+
char = chunk[i]
|
116
|
+
string_value += char
|
117
|
+
|
118
|
+
if char == '\\' && !escaped
|
119
|
+
escaped = true
|
120
|
+
elsif (char == "\n" or char == quote) && !escaped
|
121
|
+
# Found closing quote
|
122
|
+
break
|
123
|
+
elsif escaped
|
124
|
+
escaped = false
|
125
|
+
end
|
126
|
+
|
127
|
+
i += 1
|
128
|
+
end
|
129
|
+
|
130
|
+
# Add the string token if we found a closing quote
|
131
|
+
if i < chunk.length || (i == chunk.length && chunk[-1] == quote)
|
132
|
+
@tokens << {
|
133
|
+
type: :string,
|
134
|
+
value: string_value,
|
135
|
+
line: @line_num,
|
136
|
+
column: @column
|
137
|
+
}
|
138
|
+
|
139
|
+
@column += string_value.length
|
140
|
+
@position += string_value.length
|
141
|
+
@line_num += 1 if string_value.include?("\n")
|
142
|
+
next
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# Add special handling for command options in the tokenize method
|
147
|
+
if chunk.start_with?('<q-args>', '<f-args>', '<args>')
|
148
|
+
arg_token = chunk.match(/\A(<q-args>|<f-args>|<args>)/)[0]
|
149
|
+
@tokens << {
|
150
|
+
type: :command_arg_placeholder,
|
151
|
+
value: arg_token,
|
152
|
+
line: @line_num,
|
153
|
+
column: @column
|
154
|
+
}
|
155
|
+
@column += arg_token.length
|
156
|
+
@position += arg_token.length
|
157
|
+
next
|
158
|
+
end
|
159
|
+
|
160
|
+
# Special handling for a:000 variable arguments array
|
161
|
+
if chunk =~ /\Aa:0+/
|
162
|
+
varargs_token = chunk.match(/\Aa:0+/)[0]
|
163
|
+
@tokens << {
|
164
|
+
type: :arg_variable,
|
165
|
+
value: varargs_token,
|
166
|
+
line: @line_num,
|
167
|
+
column: @column
|
168
|
+
}
|
169
|
+
@column += varargs_token.length
|
170
|
+
@position += varargs_token.length
|
171
|
+
next
|
172
|
+
end
|
173
|
+
|
174
|
+
# Also add special handling for 'silent!' keyword
|
175
|
+
# Add this after the keyword check in tokenize method
|
176
|
+
if chunk.start_with?('silent!')
|
177
|
+
@tokens << {
|
178
|
+
type: :silent_bang,
|
179
|
+
value: 'silent!',
|
180
|
+
line: @line_num,
|
181
|
+
column: @column
|
182
|
+
}
|
183
|
+
@column += 7
|
184
|
+
@position += 7
|
185
|
+
next
|
186
|
+
end
|
187
|
+
|
188
|
+
# Check for keywords first, before other token types
|
189
|
+
if match = chunk.match(/\A\b(if|else|elseif|endif|while|endwhile|for|endfor|def|enddef|function|endfunction|endfunc|return|const|var|final|import|export|class|extends|static|enum|type|vim9script|abort|autocmd|echoerr|echohl|echomsg|let|unlet|execute|setlocal|syntax|highlight|sleep|source)\b/)
|
190
|
+
@tokens << {
|
191
|
+
type: :keyword,
|
192
|
+
value: match[0],
|
193
|
+
line: @line_num,
|
194
|
+
column: @column
|
195
|
+
}
|
196
|
+
@column += match[0].length
|
197
|
+
@position += match[0].length
|
198
|
+
next
|
199
|
+
end
|
200
|
+
|
201
|
+
# Handle Vim scoped option variables with &l: or &g: prefix
|
202
|
+
if match = chunk.match(/\A&[lg]:[a-zA-Z_][a-zA-Z0-9_]*/)
|
203
|
+
@tokens << {
|
204
|
+
type: :scoped_option_variable,
|
205
|
+
value: match[0],
|
206
|
+
line: @line_num,
|
207
|
+
column: @column
|
47
208
|
}
|
48
209
|
@column += match[0].length
|
49
210
|
@position += match[0].length
|
50
211
|
next
|
51
212
|
end
|
52
213
|
|
214
|
+
# Handle Vim option variables with & prefix
|
215
|
+
if match = chunk.match(/\A&[a-zA-Z_][a-zA-Z0-9_]*/)
|
216
|
+
@tokens << {
|
217
|
+
type: :option_variable,
|
218
|
+
value: match[0],
|
219
|
+
line: @line_num,
|
220
|
+
column: @column
|
221
|
+
}
|
222
|
+
@column += match[0].length
|
223
|
+
@position += match[0].length
|
224
|
+
next
|
225
|
+
end
|
226
|
+
|
227
|
+
# Handle Vim special variables with v: prefix
|
228
|
+
if match = chunk.match(/\Av:[a-zA-Z_][a-zA-Z0-9_]*/)
|
229
|
+
@tokens << {
|
230
|
+
type: :special_variable,
|
231
|
+
value: match[0],
|
232
|
+
line: @line_num,
|
233
|
+
column: @column
|
234
|
+
}
|
235
|
+
@column += match[0].length
|
236
|
+
@position += match[0].length
|
237
|
+
next
|
238
|
+
end
|
239
|
+
|
240
|
+
# Handle script-local identifiers with s: prefix
|
241
|
+
if match = chunk.match(/\As:[a-zA-Z_][a-zA-Z0-9_]*/)
|
242
|
+
@tokens << {
|
243
|
+
type: :script_local,
|
244
|
+
value: match[0],
|
245
|
+
line: @line_num,
|
246
|
+
column: @column
|
247
|
+
}
|
248
|
+
@column += match[0].length
|
249
|
+
@position += match[0].length
|
250
|
+
next
|
251
|
+
end
|
252
|
+
|
253
|
+
# Handle buffer-local identifiers with b: prefix
|
254
|
+
if match = chunk.match(/\Ab:[a-zA-Z_][a-zA-Z0-9_]*/)
|
255
|
+
@tokens << {
|
256
|
+
type: :buffer_local,
|
257
|
+
value: match[0],
|
258
|
+
line: @line_num,
|
259
|
+
column: @column
|
260
|
+
}
|
261
|
+
@column += match[0].length
|
262
|
+
@position += match[0].length
|
263
|
+
next
|
264
|
+
end
|
265
|
+
|
266
|
+
# Handle window-local identifiers with w: prefix
|
267
|
+
if match = chunk.match(/\Aw:[a-zA-Z_][a-zA-Z0-9_]*/)
|
268
|
+
@tokens << {
|
269
|
+
type: :window_local,
|
270
|
+
value: match[0],
|
271
|
+
line: @line_num,
|
272
|
+
column: @column
|
273
|
+
}
|
274
|
+
@column += match[0].length
|
275
|
+
@position += match[0].length
|
276
|
+
next
|
277
|
+
end
|
278
|
+
|
279
|
+
# Handle tab-local identifiers with t: prefix
|
280
|
+
if match = chunk.match(/\At:[a-zA-Z_][a-zA-Z0-9_]*/)
|
281
|
+
@tokens << {
|
282
|
+
type: :tab_local,
|
283
|
+
value: match[0],
|
284
|
+
line: @line_num,
|
285
|
+
column: @column
|
286
|
+
}
|
287
|
+
@column += match[0].length
|
288
|
+
@position += match[0].length
|
289
|
+
next
|
290
|
+
end
|
291
|
+
|
292
|
+
|
293
|
+
# Handle global variables with g: prefix
|
294
|
+
if match = chunk.match(/\Ag:[a-zA-Z_][a-zA-Z0-9_]*/)
|
295
|
+
@tokens << {
|
296
|
+
type: :global_variable,
|
297
|
+
value: match[0],
|
298
|
+
line: @line_num,
|
299
|
+
column: @column
|
300
|
+
}
|
301
|
+
@column += match[0].length
|
302
|
+
@position += match[0].length
|
303
|
+
next
|
304
|
+
end
|
305
|
+
|
306
|
+
# Handle argument variables with a: prefix
|
307
|
+
if match = chunk.match(/\Aa:[a-zA-Z_][a-zA-Z0-9_]*/) || match = chunk.match(/\Aa:[A-Z0-9]/)
|
308
|
+
@tokens << {
|
309
|
+
type: :arg_variable,
|
310
|
+
value: match[0],
|
311
|
+
line: @line_num,
|
312
|
+
column: @column
|
313
|
+
}
|
314
|
+
@column += match[0].length
|
315
|
+
@position += match[0].length
|
316
|
+
next
|
317
|
+
end
|
318
|
+
|
319
|
+
# Handle argument variables with a: prefix
|
320
|
+
if match = chunk.match(/\Al:[a-zA-Z_][a-zA-Z0-9_]*/)
|
321
|
+
@tokens << {
|
322
|
+
type: :local_variable,
|
323
|
+
value: match[0],
|
324
|
+
line: @line_num,
|
325
|
+
column: @column
|
326
|
+
}
|
327
|
+
@column += match[0].length
|
328
|
+
@position += match[0].length
|
329
|
+
next
|
330
|
+
end
|
331
|
+
|
332
|
+
# Add support for standalone namespace prefixes (like g:)
|
333
|
+
if match = chunk.match(/\A([sgbwtal]):/)
|
334
|
+
@tokens << {
|
335
|
+
type: :namespace_prefix,
|
336
|
+
value: match[0],
|
337
|
+
line: @line_num,
|
338
|
+
column: @column
|
339
|
+
}
|
340
|
+
@column += match[0].length
|
341
|
+
@position += match[0].length
|
342
|
+
next
|
343
|
+
end
|
344
|
+
|
345
|
+
# Handle compound assignment operators
|
346
|
+
if match = chunk.match(/\A(\+=|-=|\*=|\/=|\.\.=|\.=)/)
|
347
|
+
@tokens << {
|
348
|
+
type: :compound_operator,
|
349
|
+
value: match[0],
|
350
|
+
line: @line_num,
|
351
|
+
column: @column
|
352
|
+
}
|
353
|
+
@column += match[0].length
|
354
|
+
@position += match[0].length
|
355
|
+
next
|
356
|
+
end
|
357
|
+
|
53
358
|
# Handle ellipsis for variable args
|
54
359
|
if chunk.start_with?('...')
|
55
|
-
@tokens << {
|
56
|
-
type: :ellipsis,
|
57
|
-
value: '...',
|
58
|
-
line: @line_num,
|
59
|
-
column: @column
|
360
|
+
@tokens << {
|
361
|
+
type: :ellipsis,
|
362
|
+
value: '...',
|
363
|
+
line: @line_num,
|
364
|
+
column: @column
|
60
365
|
}
|
61
366
|
@column += 3
|
62
367
|
@position += 3
|
63
368
|
next
|
64
369
|
end
|
65
|
-
|
370
|
+
|
371
|
+
# Handle multi-character operators explicitly
|
372
|
+
if match = chunk.match(/\A(=~#|=~\?|=~|!~#|!~\?|!~|==#|==\?|==|!=#|!=\?|!=|=>\?|=>|>=#|>=\?|>=|<=#|<=\?|<=|->#|->\?|->|\.\.|\|\||&&)/)
|
373
|
+
@tokens << {
|
374
|
+
type: :operator,
|
375
|
+
value: match[0],
|
376
|
+
line: @line_num,
|
377
|
+
column: @column
|
378
|
+
}
|
379
|
+
@column += match[0].length
|
380
|
+
@position += match[0].length
|
381
|
+
next
|
382
|
+
end
|
383
|
+
|
384
|
+
# Handle regex patterns /pattern/ - only in specific contexts
|
385
|
+
if chunk.start_with?('/') && should_parse_as_regex
|
386
|
+
i = 1
|
387
|
+
regex_value = '/'
|
388
|
+
|
389
|
+
# Keep going until we find the closing slash
|
390
|
+
while i < chunk.length
|
391
|
+
char = chunk[i]
|
392
|
+
regex_value += char
|
393
|
+
|
394
|
+
if char == '/' && (i == 1 || chunk[i-1] != '\\')
|
395
|
+
# Found closing slash
|
396
|
+
i += 1
|
397
|
+
break
|
398
|
+
end
|
399
|
+
|
400
|
+
i += 1
|
401
|
+
end
|
402
|
+
|
403
|
+
# Add the regex token if we found a closing slash
|
404
|
+
if regex_value.end_with?('/')
|
405
|
+
@tokens << {
|
406
|
+
type: :regex,
|
407
|
+
value: regex_value,
|
408
|
+
line: @line_num,
|
409
|
+
column: @column
|
410
|
+
}
|
411
|
+
@column += regex_value.length
|
412
|
+
@position += regex_value.length
|
413
|
+
next
|
414
|
+
end
|
415
|
+
end
|
416
|
+
|
417
|
+
# Handle hex colors like #33FF33
|
418
|
+
if match = chunk.match(/\A#[0-9A-Fa-f]{6}/)
|
419
|
+
@tokens << {
|
420
|
+
type: :hex_color,
|
421
|
+
value: match[0],
|
422
|
+
line: @line_num,
|
423
|
+
column: @column
|
424
|
+
}
|
425
|
+
@column += match[0].length
|
426
|
+
@position += match[0].length
|
427
|
+
next
|
428
|
+
end
|
429
|
+
|
430
|
+
# Handle register access (@a, @", etc.)
|
431
|
+
if chunk =~ /\A@[a-zA-Z0-9":.%#=*+~_\/\-]/
|
432
|
+
register_token = chunk.match(/\A@[a-zA-Z0-9":.%#=*+~_\/\-]/)[0]
|
433
|
+
@tokens << {
|
434
|
+
type: :register_access,
|
435
|
+
value: register_token,
|
436
|
+
line: @line_num,
|
437
|
+
column: @column
|
438
|
+
}
|
439
|
+
@column += register_token.length
|
440
|
+
@position += register_token.length
|
441
|
+
next
|
442
|
+
end
|
443
|
+
|
444
|
+
# In the tokenize method, add special handling for common mapping components
|
445
|
+
if chunk.start_with?('<CR>', '<Esc>', '<Tab>', '<Space>', '<C-') ||
|
446
|
+
(chunk =~ /\A<[A-Za-z0-9\-_]+>/)
|
447
|
+
# Extract the special key notation
|
448
|
+
match = chunk.match(/\A(<[^>]+>)/)
|
449
|
+
if match
|
450
|
+
special_key = match[1]
|
451
|
+
@tokens << {
|
452
|
+
type: :special_key,
|
453
|
+
value: special_key,
|
454
|
+
line: @line_num,
|
455
|
+
column: @column
|
456
|
+
}
|
457
|
+
@position += special_key.length
|
458
|
+
@column += special_key.length
|
459
|
+
next
|
460
|
+
end
|
461
|
+
end
|
462
|
+
|
66
463
|
# Skip whitespace but track position
|
67
464
|
if match = chunk.match(/\A(\s+)/)
|
68
465
|
whitespace = match[0]
|
@@ -77,20 +474,55 @@ module Vinter
|
|
77
474
|
@position += whitespace.length
|
78
475
|
next
|
79
476
|
end
|
80
|
-
|
477
|
+
|
478
|
+
# Handle backslash for line continuation
|
479
|
+
if chunk.start_with?('\\')
|
480
|
+
@tokens << {
|
481
|
+
type: :line_continuation,
|
482
|
+
value: '\\',
|
483
|
+
line: @line_num,
|
484
|
+
column: @column
|
485
|
+
}
|
486
|
+
@column += 1
|
487
|
+
@position += 1
|
488
|
+
|
489
|
+
# If followed by a newline, advance to next line
|
490
|
+
if @position < @input.length && @input[@position] == "\n"
|
491
|
+
@line_num += 1
|
492
|
+
@column = 1
|
493
|
+
@position += 1
|
494
|
+
end
|
495
|
+
|
496
|
+
next
|
497
|
+
end
|
498
|
+
|
499
|
+
# Check for special case where 'function' is followed by '('
|
500
|
+
# which likely means it's used as a built-in function
|
501
|
+
if chunk =~ /\Afunction\s*\(/
|
502
|
+
@tokens << {
|
503
|
+
type: :identifier, # Treat as identifier, not keyword
|
504
|
+
value: 'function',
|
505
|
+
line: @line_num,
|
506
|
+
column: @column
|
507
|
+
}
|
508
|
+
@column += 'function'.length
|
509
|
+
@position += 'function'.length
|
510
|
+
next
|
511
|
+
end
|
512
|
+
|
81
513
|
match_found = false
|
82
|
-
|
514
|
+
|
83
515
|
TOKEN_TYPES.each do |type, pattern|
|
84
516
|
if match = chunk.match(/\A(#{pattern})/)
|
85
517
|
value = match[0]
|
86
|
-
token = {
|
87
|
-
type: type,
|
88
|
-
value: value,
|
89
|
-
line: @line_num,
|
90
|
-
column: @column
|
518
|
+
token = {
|
519
|
+
type: type,
|
520
|
+
value: value,
|
521
|
+
line: @line_num,
|
522
|
+
column: @column
|
91
523
|
}
|
92
524
|
@tokens << token unless type == :whitespace
|
93
|
-
|
525
|
+
|
94
526
|
# Update position
|
95
527
|
if value.include?("\n")
|
96
528
|
lines = value.split("\n")
|
@@ -103,33 +535,33 @@ module Vinter
|
|
103
535
|
else
|
104
536
|
@column += value.length
|
105
537
|
end
|
106
|
-
|
538
|
+
|
107
539
|
@position += value.length
|
108
540
|
match_found = true
|
109
541
|
break
|
110
542
|
end
|
111
543
|
end
|
112
|
-
|
544
|
+
|
113
545
|
unless match_found
|
114
546
|
# Try to handle unknown characters
|
115
|
-
@tokens << {
|
116
|
-
type: :unknown,
|
117
|
-
value: chunk[0],
|
118
|
-
line: @line_num,
|
119
|
-
column: @column
|
547
|
+
@tokens << {
|
548
|
+
type: :unknown,
|
549
|
+
value: chunk[0],
|
550
|
+
line: @line_num,
|
551
|
+
column: @column
|
120
552
|
}
|
121
|
-
|
553
|
+
|
122
554
|
if chunk[0] == "\n"
|
123
555
|
@line_num += 1
|
124
556
|
@column = 1
|
125
557
|
else
|
126
558
|
@column += 1
|
127
559
|
end
|
128
|
-
|
560
|
+
|
129
561
|
@position += 1
|
130
562
|
end
|
131
563
|
end
|
132
|
-
|
564
|
+
|
133
565
|
@tokens
|
134
566
|
end
|
135
567
|
end
|
data/lib/vinter/linter.rb
CHANGED
@@ -1,7 +1,12 @@
|
|
1
|
+
require "yaml"
|
2
|
+
|
1
3
|
module Vinter
|
2
4
|
class Linter
|
3
|
-
def initialize
|
5
|
+
def initialize(config_path: nil)
|
4
6
|
@rules = []
|
7
|
+
@ignored_rules = []
|
8
|
+
@config_path = config_path || find_config_path
|
9
|
+
load_config
|
5
10
|
register_default_rules
|
6
11
|
end
|
7
12
|
|
@@ -94,7 +99,7 @@ module Vinter
|
|
94
99
|
lexer = Lexer.new(content)
|
95
100
|
tokens = lexer.tokenize
|
96
101
|
|
97
|
-
parser = Parser.new(tokens)
|
102
|
+
parser = Parser.new(tokens, content)
|
98
103
|
result = parser.parse
|
99
104
|
|
100
105
|
issues = []
|
@@ -121,8 +126,9 @@ module Vinter
|
|
121
126
|
}
|
122
127
|
end
|
123
128
|
|
124
|
-
# Apply rules
|
129
|
+
# Apply rules, ignoring those specified in config
|
125
130
|
@rules.each do |rule|
|
131
|
+
next if @ignored_rules.include?(rule.id)
|
126
132
|
rule_issues = rule.apply(result[:ast])
|
127
133
|
issues.concat(rule_issues.map { |i| {
|
128
134
|
type: :rule,
|
@@ -135,6 +141,25 @@ module Vinter
|
|
135
141
|
|
136
142
|
issues
|
137
143
|
end
|
144
|
+
|
145
|
+
private
|
146
|
+
|
147
|
+
def find_config_path
|
148
|
+
# check for project level config
|
149
|
+
project_config = Dir.glob(".vinter{.yaml,.yml,}").first
|
150
|
+
project_config if project_config
|
151
|
+
|
152
|
+
# check for user-level config
|
153
|
+
user_config = File.expand_path("~/.vinter")
|
154
|
+
user_config if File.exist?(user_config)
|
155
|
+
end
|
156
|
+
|
157
|
+
def load_config
|
158
|
+
return unless @config_path && File.exist?(@config_path)
|
159
|
+
|
160
|
+
config = YAML.load_file(@config_path)
|
161
|
+
@ignored_rules = config["ignore_rules"] || []
|
162
|
+
end
|
138
163
|
end
|
139
164
|
|
140
165
|
class Rule
|