vinter 0.3.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/README.md +13 -9
- data/bin/vinter +0 -0
- data/lib/vinter/cli.rb +49 -21
- data/lib/vinter/lexer.rb +358 -19
- data/lib/vinter/linter.rb +28 -3
- data/lib/vinter/parser.rb +2684 -316
- data/lib/vinter.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e6f99b1ca16b7bbca35229a549ceed21f3cdf9b8bc86a3fedff9b978b18717de
|
4
|
+
data.tar.gz: 2aa4e1df5ae4fc08e78c06cf2107c7192099326b073bf45e45a9532b3fb5d0c3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d739b01780f6ce12654b09b701f343e850bd2eb750e705ca608f793e7e9cfef26b226927157ff595dcbb3f62ddbbd4f530439074ef9fdbc9a2cfd075dd78a3da
|
7
|
+
data.tar.gz: 7ec3181349383a4b9667aab046f56099d25f830a07aaa6a6135b8640a25d59e85c2191b3ffbecd263afe703f1f17a986f40f769e458bab532e70f837f9c9a029
|
data/README.md
CHANGED
@@ -2,13 +2,6 @@
|
|
2
2
|
|
3
3
|
A Ruby gem that provides linting capabilities for Vim9 script files. This linter helps identify syntax errors and enforce best practices for for Vim9 script.
|
4
4
|
|
5
|
-
## Features
|
6
|
-
|
7
|
-
- Lexical analysis of Vim9 script syntax
|
8
|
-
- Parsing of Vim9 script constructs
|
9
|
-
- Detection of common errors and code smells
|
10
|
-
- Command-line interface for easy integration with editors
|
11
|
-
|
12
5
|
## Installation
|
13
6
|
|
14
7
|
Install the gem:
|
@@ -17,11 +10,22 @@ Install the gem:
|
|
17
10
|
gem install vinter
|
18
11
|
```
|
19
12
|
|
13
|
+
## Configure
|
14
|
+
Vinter will read config files on the following priority order
|
15
|
+
- User config (`~/.vinter`)
|
16
|
+
- Project config (`path/to/proj/.vinter`)
|
17
|
+
|
18
|
+
```yaml
|
19
|
+
ignore_rules:
|
20
|
+
- missing-vim9script-declaration
|
21
|
+
- prefer-def-over-function
|
22
|
+
```
|
23
|
+
|
20
24
|
## Usage
|
21
25
|
|
22
26
|
### Command Line
|
23
27
|
|
24
|
-
|
28
|
+
Updated vim linter for legacy and vim9script
|
25
29
|
|
26
30
|
```bash
|
27
31
|
vinter path/to/your/script.vim
|
@@ -88,4 +92,4 @@ issues = linter.lint(content)
|
|
88
92
|
2. Create a feature branch: `git checkout -b my-new-feature`
|
89
93
|
3. Commit your changes: `git commit -am 'Add some feature'`
|
90
94
|
4. Push to the branch: `git push origin my-new-feature`
|
91
|
-
5. Submit a pull request
|
95
|
+
5. Submit a pull request
|
data/bin/vinter
CHANGED
File without changes
|
data/lib/vinter/cli.rb
CHANGED
@@ -6,42 +6,70 @@ module Vinter
|
|
6
6
|
|
7
7
|
def run(args)
|
8
8
|
if args.empty?
|
9
|
-
puts "Usage:
|
9
|
+
puts "Usage: vinter [file.vim|directory]"
|
10
10
|
return 1
|
11
11
|
end
|
12
12
|
|
13
|
-
|
13
|
+
target_path = args[0]
|
14
14
|
|
15
|
-
unless File.exist?(
|
16
|
-
puts "Error: File not found: #{
|
15
|
+
unless File.exist?(target_path)
|
16
|
+
puts "Error: File or directory not found: #{target_path}"
|
17
17
|
return 1
|
18
18
|
end
|
19
19
|
|
20
|
-
|
21
|
-
|
20
|
+
vim_files = if File.directory?(target_path)
|
21
|
+
find_vim_files(target_path)
|
22
|
+
else
|
23
|
+
[target_path]
|
24
|
+
end
|
22
25
|
|
23
|
-
if
|
24
|
-
puts "No
|
26
|
+
if vim_files.empty?
|
27
|
+
puts "No .vim files found in #{target_path}"
|
25
28
|
return 0
|
26
|
-
|
27
|
-
|
29
|
+
end
|
30
|
+
|
31
|
+
total_issues = 0
|
32
|
+
error_count = 0
|
33
|
+
|
34
|
+
vim_files.each do |file_path|
|
35
|
+
content = File.read(file_path)
|
36
|
+
issues = @linter.lint(content)
|
37
|
+
total_issues += issues.length
|
38
|
+
|
39
|
+
if issues.empty?
|
40
|
+
puts "No issues found in #{file_path}" if vim_files.length == 1
|
41
|
+
else
|
42
|
+
puts "Found #{issues.length} issues in #{file_path}:" if vim_files.length > 1
|
43
|
+
|
44
|
+
issues.each do |issue|
|
45
|
+
type_str = case issue[:type]
|
46
|
+
when :error then "ERROR"
|
47
|
+
when :warning then "WARNING"
|
48
|
+
when :rule then "RULE(#{issue[:rule]})"
|
49
|
+
else "UNKNOWN"
|
50
|
+
end
|
28
51
|
|
29
|
-
|
30
|
-
|
31
|
-
when :error then "ERROR"
|
32
|
-
when :warning then "WARNING"
|
33
|
-
when :rule then "RULE(#{issue[:rule]})"
|
34
|
-
else "UNKNOWN"
|
35
|
-
end
|
52
|
+
line = issue[:line] || 1
|
53
|
+
column = issue[:column] || 1
|
36
54
|
|
37
|
-
|
38
|
-
|
55
|
+
puts "#{file_path}:#{line}:#{column}: #{type_str}: #{issue[:message]}"
|
56
|
+
end
|
39
57
|
|
40
|
-
|
58
|
+
error_count += 1 if issues.any? { |i| i[:type] == :error }
|
41
59
|
end
|
60
|
+
end
|
42
61
|
|
43
|
-
|
62
|
+
if vim_files.length > 1
|
63
|
+
puts "\nProcessed #{vim_files.length} files, found #{total_issues} total issues"
|
44
64
|
end
|
65
|
+
|
66
|
+
return error_count > 0 ? 1 : 0
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def find_vim_files(directory)
|
72
|
+
Dir.glob(File.join(directory, "**", "*.vim")).sort
|
45
73
|
end
|
46
74
|
end
|
47
75
|
end
|
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|autocmd|echohl|echomsg|let|execute)\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: /\}/,
|
@@ -24,9 +25,11 @@ module Vinter
|
|
24
25
|
semicolon: /;/,
|
25
26
|
comma: /,/,
|
26
27
|
backslash: /\\/,
|
28
|
+
question_mark: /\?/,
|
29
|
+
command_separator: /\|/,
|
27
30
|
}
|
28
31
|
|
29
|
-
CONTINUATION_OPERATORS = %w(. .. + - * / = == != > < >= <= && || ? : -> =>)
|
32
|
+
CONTINUATION_OPERATORS = %w(. .. + - * / = == ==# ==? != > < >= <= && || ? : -> =>)
|
30
33
|
def initialize(input)
|
31
34
|
@input = input
|
32
35
|
@tokens = []
|
@@ -35,9 +38,178 @@ module Vinter
|
|
35
38
|
@column = 1
|
36
39
|
end
|
37
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
|
+
|
38
73
|
def tokenize
|
39
74
|
until @position >= @input.length
|
40
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
|
88
|
+
|
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
|
208
|
+
}
|
209
|
+
@column += match[0].length
|
210
|
+
@position += match[0].length
|
211
|
+
next
|
212
|
+
end
|
41
213
|
|
42
214
|
# Handle Vim option variables with & prefix
|
43
215
|
if match = chunk.match(/\A&[a-zA-Z_][a-zA-Z0-9_]*/)
|
@@ -51,7 +223,7 @@ module Vinter
|
|
51
223
|
@position += match[0].length
|
52
224
|
next
|
53
225
|
end
|
54
|
-
|
226
|
+
|
55
227
|
# Handle Vim special variables with v: prefix
|
56
228
|
if match = chunk.match(/\Av:[a-zA-Z_][a-zA-Z0-9_]*/)
|
57
229
|
@tokens << {
|
@@ -64,7 +236,7 @@ module Vinter
|
|
64
236
|
@position += match[0].length
|
65
237
|
next
|
66
238
|
end
|
67
|
-
|
239
|
+
|
68
240
|
# Handle script-local identifiers with s: prefix
|
69
241
|
if match = chunk.match(/\As:[a-zA-Z_][a-zA-Z0-9_]*/)
|
70
242
|
@tokens << {
|
@@ -77,7 +249,47 @@ module Vinter
|
|
77
249
|
@position += match[0].length
|
78
250
|
next
|
79
251
|
end
|
80
|
-
|
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
|
+
|
81
293
|
# Handle global variables with g: prefix
|
82
294
|
if match = chunk.match(/\Ag:[a-zA-Z_][a-zA-Z0-9_]*/)
|
83
295
|
@tokens << {
|
@@ -90,9 +302,9 @@ module Vinter
|
|
90
302
|
@position += match[0].length
|
91
303
|
next
|
92
304
|
end
|
93
|
-
|
305
|
+
|
94
306
|
# Handle argument variables with a: prefix
|
95
|
-
if match = chunk.match(/\Aa:[a-zA-Z_][a-zA-Z0-9_]*/)
|
307
|
+
if match = chunk.match(/\Aa:[a-zA-Z_][a-zA-Z0-9_]*/) || match = chunk.match(/\Aa:[A-Z0-9]/)
|
96
308
|
@tokens << {
|
97
309
|
type: :arg_variable,
|
98
310
|
value: match[0],
|
@@ -103,11 +315,11 @@ module Vinter
|
|
103
315
|
@position += match[0].length
|
104
316
|
next
|
105
317
|
end
|
106
|
-
|
107
|
-
# Handle
|
108
|
-
if match = chunk.match(/\
|
318
|
+
|
319
|
+
# Handle argument variables with a: prefix
|
320
|
+
if match = chunk.match(/\Al:[a-zA-Z_][a-zA-Z0-9_]*/)
|
109
321
|
@tokens << {
|
110
|
-
type: :
|
322
|
+
type: :local_variable,
|
111
323
|
value: match[0],
|
112
324
|
line: @line_num,
|
113
325
|
column: @column
|
@@ -117,10 +329,23 @@ module Vinter
|
|
117
329
|
next
|
118
330
|
end
|
119
331
|
|
120
|
-
#
|
121
|
-
if match = chunk.match(/\A(
|
332
|
+
# Add support for standalone namespace prefixes (like g:)
|
333
|
+
if match = chunk.match(/\A([sgbwtal]):/)
|
122
334
|
@tokens << {
|
123
|
-
type: :
|
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,
|
124
349
|
value: match[0],
|
125
350
|
line: @line_num,
|
126
351
|
column: @column
|
@@ -143,6 +368,98 @@ module Vinter
|
|
143
368
|
next
|
144
369
|
end
|
145
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
|
+
|
146
463
|
# Skip whitespace but track position
|
147
464
|
if match = chunk.match(/\A(\s+)/)
|
148
465
|
whitespace = match[0]
|
@@ -161,13 +478,35 @@ module Vinter
|
|
161
478
|
# Handle backslash for line continuation
|
162
479
|
if chunk.start_with?('\\')
|
163
480
|
@tokens << {
|
164
|
-
type: :
|
481
|
+
type: :line_continuation,
|
165
482
|
value: '\\',
|
166
483
|
line: @line_num,
|
167
484
|
column: @column
|
168
485
|
}
|
169
486
|
@column += 1
|
170
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
|
171
510
|
next
|
172
511
|
end
|
173
512
|
|
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
|