vinter 0.4.0 → 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e6f99b1ca16b7bbca35229a549ceed21f3cdf9b8bc86a3fedff9b978b18717de
4
- data.tar.gz: 2aa4e1df5ae4fc08e78c06cf2107c7192099326b073bf45e45a9532b3fb5d0c3
3
+ metadata.gz: 319f22f198207f7d9d3b9e58c85327b20f7c873e905bc16a38acd6f9199d8b56
4
+ data.tar.gz: c5eeb2b1dd21094be8a901e570540b89a29619273b0c5fcb11b7edc278da9975
5
5
  SHA512:
6
- metadata.gz: d739b01780f6ce12654b09b701f343e850bd2eb750e705ca608f793e7e9cfef26b226927157ff595dcbb3f62ddbbd4f530439074ef9fdbc9a2cfd075dd78a3da
7
- data.tar.gz: 7ec3181349383a4b9667aab046f56099d25f830a07aaa6a6135b8640a25d59e85c2191b3ffbecd263afe703f1f17a986f40f769e458bab532e70f837f9c9a029
6
+ metadata.gz: b1556c23fd42b7b7d9d3163a97ecd353902f12341c203a913a4dffec05cbd6ffe867815f223dbfe8f169fd99c514b8b39ac1fab87fd26f195d0622fff9e62966
7
+ data.tar.gz: 2204f688018212f0b5ec6b13a0906da8a24078fe874dc10c2b9b326668627fb985fdcc11b9682bdbb67f2a452838b8088286260de54bf8941b6501180a7a8704
@@ -0,0 +1,14 @@
1
+ module Vinter
2
+ module ASTPrinter
3
+ def self.print(ast, indent=0)
4
+ spaces = " " * indent * 2
5
+ ast[:body].each do |node|
6
+ puts "#{spaces}(#{node[:type]}|#{node[:value]})"
7
+ if node[:type] == :export_statement
8
+ puts "(#{node[:export][:type]})"
9
+ print(node[:export], indent=1)
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
data/lib/vinter/cli.rb CHANGED
@@ -6,7 +6,7 @@ module Vinter
6
6
 
7
7
  def run(args)
8
8
  if args.empty?
9
- puts "Usage: vinter [file.vim|directory]"
9
+ puts "Usage: vinter [file.vim|directory] [--print-ast]"
10
10
  return 1
11
11
  end
12
12
 
data/lib/vinter/lexer.rb CHANGED
@@ -2,7 +2,9 @@ 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|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/,
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|scriptencoding|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|var)\b/,
6
+ encodings: /\b(latin1|iso|koi8|macroman|cp437|cp737|cp775|cp850|cp852|cp855|cp857|cp860|cp861|cp862|cp863|cp865|cp866|cp869|cp874|cp1250|cp1251|cp1253|cp1254|cp1255|cp1256|cp1257|cp1258|cp932|euc\-jp|sjis|cp949|euc\-kr|cp936|euc\-cn|cp950|big5|euc\-tw|utf\-8|ucs\-2|ucs\-21e|utf\-16|utf\-16le|ucs\-4|ucs\-4le|ansi|japan|korea|prc|chinese|taiwan|utf8|unicode|ucs2be|ucs\-2be|ucs\-4be|utf\-32|utf\-32le|default)\b/,
7
+ vimfuncs: /\b(win_execute|win_findbuf|win_getid|win_gettype|win_gotoid|win_id2tabwin|win_id2win|win_move_separator|win_move_statusline|win_screenpos|win_splitmove)\b/,
6
8
  # Identifiers can include # and special characters
7
9
  identifier: /\b[a-zA-Z_][a-zA-Z0-9_#]*\b/,
8
10
  # Single-character operators
@@ -26,7 +28,7 @@ module Vinter
26
28
  comma: /,/,
27
29
  backslash: /\\/,
28
30
  question_mark: /\?/,
29
- command_separator: /\|/,
31
+ command_separator: /\|/
30
32
  }
31
33
 
32
34
  CONTINUATION_OPERATORS = %w(. .. + - * / = == ==# ==? != > < >= <= && || ? : -> =>)
@@ -41,21 +43,21 @@ module Vinter
41
43
  def should_parse_as_regex
42
44
  # Look at recent tokens to determine if we're in a regex context
43
45
  recent_tokens = @tokens.last(3)
44
-
46
+
45
47
  # 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
+ return true if recent_tokens.any? { |t|
49
+ t && t[:type] == :keyword && ['syntax'].include?(t[:value])
48
50
  }
49
-
51
+
50
52
  return true if recent_tokens.any? { |t|
51
53
  t && t[:type] == :identifier && ['match', 'region', 'keyword'].include?(t[:value])
52
54
  }
53
-
55
+
54
56
  # Check for comparison operators that often use regex
55
57
  return true if recent_tokens.any? { |t|
56
58
  t && t[:type] == :operator && ['=~', '!~', '=~#', '!~#', '=~?', '!~?'].include?(t[:value])
57
59
  }
58
-
60
+
59
61
  false
60
62
  end
61
63
 
@@ -85,7 +87,7 @@ module Vinter
85
87
  end
86
88
  line_start = temp_pos < 0 || @input[temp_pos] == "\n"
87
89
  end
88
-
90
+
89
91
  # If we're at the start of a line and it begins with a quote
90
92
  if line_start && chunk.start_with?('"')
91
93
  # Find the end of the line
@@ -103,6 +105,47 @@ module Vinter
103
105
  @column += comment_text.length
104
106
  next
105
107
  end
108
+
109
+ # --- Interpolated String Handling ---
110
+ if chunk.start_with?("$'")
111
+ i = 2
112
+ string_value = "$'"
113
+ brace_depth = 0
114
+ escaped = false
115
+
116
+ while i < chunk.length
117
+ char = chunk[i]
118
+ string_value += char
119
+
120
+ if char == '\\' && !escaped
121
+ escaped = true
122
+ elsif char == "'" && !escaped && brace_depth == 0
123
+ # End of interpolated string
124
+ i += 1
125
+ break
126
+ elsif char == '{' && !escaped
127
+ brace_depth += 1
128
+ elsif char == '}' && !escaped && brace_depth > 0
129
+ brace_depth -= 1
130
+ elsif escaped
131
+ escaped = false
132
+ end
133
+
134
+ i += 1
135
+ end
136
+
137
+ @tokens << {
138
+ type: :interpolated_string,
139
+ value: string_value,
140
+ line: @line_num,
141
+ column: @column
142
+ }
143
+ @column += string_value.length
144
+ @position += string_value.length
145
+ @line_num += string_value.count("\n")
146
+ next
147
+ end
148
+
106
149
  # Handle string literals manually
107
150
  if chunk.start_with?("'") || chunk.start_with?('"')
108
151
  quote = chunk[0]
@@ -118,7 +161,7 @@ module Vinter
118
161
  if char == '\\' && !escaped
119
162
  escaped = true
120
163
  elsif (char == "\n" or char == quote) && !escaped
121
- # Found closing quote
164
+ i += 1
122
165
  break
123
166
  elsif escaped
124
167
  escaped = false
@@ -186,7 +229,7 @@ module Vinter
186
229
  end
187
230
 
188
231
  # 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/)
232
+ 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|var|execute|setlocal|syntax|highlight|sleep|source)\b/)
190
233
  @tokens << {
191
234
  type: :keyword,
192
235
  value: match[0],
@@ -210,7 +253,7 @@ module Vinter
210
253
  @position += match[0].length
211
254
  next
212
255
  end
213
-
256
+
214
257
  # Handle Vim option variables with & prefix
215
258
  if match = chunk.match(/\A&[a-zA-Z_][a-zA-Z0-9_]*/)
216
259
  @tokens << {
@@ -385,21 +428,21 @@ module Vinter
385
428
  if chunk.start_with?('/') && should_parse_as_regex
386
429
  i = 1
387
430
  regex_value = '/'
388
-
431
+
389
432
  # Keep going until we find the closing slash
390
433
  while i < chunk.length
391
434
  char = chunk[i]
392
435
  regex_value += char
393
-
436
+
394
437
  if char == '/' && (i == 1 || chunk[i-1] != '\\')
395
438
  # Found closing slash
396
439
  i += 1
397
440
  break
398
441
  end
399
-
442
+
400
443
  i += 1
401
444
  end
402
-
445
+
403
446
  # Add the regex token if we found a closing slash
404
447
  if regex_value.end_with?('/')
405
448
  @tokens << {
data/lib/vinter/parser.rb CHANGED
@@ -71,6 +71,95 @@ module Vinter
71
71
  end
72
72
  end
73
73
 
74
+ # Helper method to add errors with consistent formatting
75
+ def add_error(message, token = current_token)
76
+ @errors << {
77
+ message: message,
78
+ position: @position,
79
+ line: token ? token[:line] : 0,
80
+ column: token ? token[:column] : 0
81
+ }
82
+ end
83
+
84
+ # Helper method to expect a specific keyword value
85
+ def expect_keyword(value)
86
+ if current_token && current_token[:type] == :keyword && current_token[:value] == value
87
+ advance
88
+ true
89
+ else
90
+ add_error("Expected '#{value}' keyword")
91
+ false
92
+ end
93
+ end
94
+
95
+ # Helper method to parse a body of statements until one or more end keywords
96
+ # Returns array of parsed statements
97
+ def parse_body_until(*end_keywords)
98
+ body = []
99
+ while @position < @tokens.length
100
+ # Check if we've reached an end keyword
101
+ if current_token && current_token[:type] == :keyword &&
102
+ end_keywords.include?(current_token[:value])
103
+ break
104
+ end
105
+
106
+ stmt = parse_statement
107
+ body << stmt if stmt
108
+ end
109
+ body
110
+ end
111
+
112
+ # Helper method to expect and consume an end keyword
113
+ # Returns true if found, false and adds error if not
114
+ def expect_end_keyword(*keywords)
115
+ if current_token && current_token[:type] == :keyword &&
116
+ keywords.include?(current_token[:value])
117
+ advance
118
+ true
119
+ else
120
+ # Only add error if we haven't reached end of file
121
+ if @position < @tokens.length
122
+ add_error("Expected #{keywords.join(' or ')} to close block")
123
+ end
124
+ false
125
+ end
126
+ end
127
+
128
+ # Helper method to parse comma-separated loop variables
129
+ # Used by for loops with brackets or parentheses
130
+ def parse_loop_variables
131
+ loop_vars = []
132
+
133
+ loop do
134
+ if current_token && (current_token[:type] == :identifier ||
135
+ current_token[:type] == :local_variable ||
136
+ current_token[:type] == :global_variable ||
137
+ current_token[:type] == :script_local)
138
+ loop_vars << advance
139
+ else
140
+ add_error("Expected identifier in for loop variables")
141
+ break
142
+ end
143
+
144
+ if current_token && current_token[:type] == :comma
145
+ advance # Skip ','
146
+ else
147
+ break
148
+ end
149
+ end
150
+
151
+ loop_vars
152
+ end
153
+
154
+ # Helper method to expect 'in' keyword in for loops
155
+ def expect_in_keyword
156
+ if !current_token || (current_token[:type] != :identifier || current_token[:value] != 'in')
157
+ add_error("Expected 'in' after for loop variables")
158
+ else
159
+ advance # Skip 'in'
160
+ end
161
+ end
162
+
74
163
  def parse_program
75
164
  statements = []
76
165
 
@@ -265,6 +354,8 @@ module Vinter
265
354
  when 'vim9script'
266
355
  token = advance # Skip 'vim9script'
267
356
  { type: :vim9script_declaration, line: token[:line], column: token[:column] }
357
+ when 'scriptencoding'
358
+ parse_scriptencoding
268
359
  when 'autocmd'
269
360
  parse_autocmd_statement
270
361
  when 'execute', 'exec'
@@ -333,6 +424,24 @@ module Vinter
333
424
  parse_delete_command
334
425
  elsif current_token[:type] == :percentage
335
426
  parse_range_command
427
+ elsif current_token[:type] == :global_variable
428
+ name = current_token[:value]
429
+ #advance#skip value
430
+ #advance#operator
431
+ ##parse_expression_statement
432
+ #advance#value
433
+ while peek_token[:line] == current_token[:line]
434
+ advance
435
+ end
436
+ advance
437
+ {
438
+ type: :global_variable,
439
+ name: name
440
+ }
441
+
442
+ elsif current_token[:type] == :vimfuncs
443
+ advance
444
+ parse_function_call(current_token[:value], current_token[:line], current_token[:column])
336
445
  else
337
446
  @warnings << {
338
447
  message: "Unexpected token type: #{current_token[:type]}",
@@ -427,80 +536,7 @@ module Vinter
427
536
  }
428
537
  end
429
538
 
430
- def old_parse_filter_command
431
- token = advance # Skip 'filter' or 'filt'
432
- line = token[:line]
433
- column = token[:column]
434
-
435
- # Check for bang (!)
436
- has_bang = false
437
- if current_token && current_token[:type] == :operator && current_token[:value] == '!'
438
- has_bang = true
439
- advance # Skip '!'
440
- end
441
-
442
- # Parse the pattern
443
- pattern = nil
444
- pattern_delimiter = nil
445
-
446
- if current_token && current_token[:type] == :operator && current_token[:value] == '/'
447
- # Handle /pattern/ form
448
- pattern_delimiter = '/'
449
- advance # Skip opening delimiter
450
-
451
- # Collect all tokens until closing delimiter
452
- pattern_parts = []
453
- while @position < @tokens.length &&
454
- !(current_token[:type] == :operator && current_token[:value] == pattern_delimiter)
455
- pattern_parts << current_token[:value]
456
- advance
457
- end
458
-
459
- pattern = pattern_parts.join('')
460
-
461
- # Skip closing delimiter
462
- if current_token && current_token[:type] == :operator && current_token[:value] == pattern_delimiter
463
- advance
464
- else
465
- @errors << {
466
- message: "Expected closing pattern delimiter: #{pattern_delimiter}",
467
- position: @position,
468
- line: current_token ? current_token[:line] : 0,
469
- column: current_token ? current_token[:column] : 0
470
- }
471
- end
472
- else
473
- # Handle direct pattern form (without delimiters)
474
- # Parse until we see what appears to be the command
475
- pattern_parts = []
476
- while @position < @tokens.length
477
- # Don't consume tokens that likely belong to the command part
478
- if current_token[:type] == :keyword ||
479
- (current_token[:type] == :identifier &&
480
- ['echo', 'let', 'execute', 'exec', 'autocmd', 'au', 'oldfiles', 'clist', 'command',
481
- 'files', 'highlight', 'jumps', 'list', 'llist', 'marks', 'registers', 'set'].include?(current_token[:value]))
482
- break
483
- end
484
-
485
- pattern_parts << current_token[:value]
486
- advance
487
- end
488
539
 
489
- pattern = pattern_parts.join('').strip
490
- end
491
-
492
- # Parse the command to be filtered
493
- command = parse_statement
494
-
495
- {
496
- type: :filter_command,
497
- pattern: pattern,
498
- has_bang: has_bang,
499
- command: command,
500
- line: line,
501
- column: column
502
- }
503
- end
504
540
 
505
541
  def parse_augroup_statement
506
542
  token = advance # Skip 'augroup'
@@ -513,18 +549,11 @@ module Vinter
513
549
  name = current_token[:value]
514
550
  advance
515
551
  else
516
- @errors << {
517
- message: "Expected augroup name",
518
- position: @position,
519
- line: current_token ? current_token[:line] : 0,
520
- column: current_token ? current_token[:column] : 0
521
- }
552
+ add_error("Expected augroup name")
522
553
  end
523
554
 
524
555
  # Check for augroup END
525
- is_end_marker = false
526
- if name && (name.upcase == "END" || name == "END")
527
- is_end_marker = true
556
+ if name && name.upcase == "END"
528
557
  return {
529
558
  type: :augroup_end,
530
559
  line: line,
@@ -540,8 +569,7 @@ module Vinter
540
569
  (current_token[:type] == :identifier && current_token[:value] == 'augroup')
541
570
  # Look ahead for END
542
571
  if peek_token &&
543
- ((peek_token[:type] == :identifier &&
544
- (peek_token[:value].upcase == 'END' || peek_token[:value] == 'END')) ||
572
+ ((peek_token[:type] == :identifier && peek_token[:value].upcase == 'END') ||
545
573
  (peek_token[:type] == :keyword && peek_token[:value].upcase == 'END'))
546
574
  advance # Skip 'augroup'
547
575
  advance # Skip 'END'
@@ -619,12 +647,7 @@ module Vinter
619
647
  column: dot_token[:column]
620
648
  }
621
649
  else
622
- @errors << {
623
- message: "Expected property name after '.'",
624
- position: @position,
625
- line: current_token ? current_token[:line] : 0,
626
- column: current_token ? current_token[:column] : 0
627
- }
650
+ add_error("Expected property name after '.'")
628
651
  end
629
652
  end
630
653
 
@@ -684,12 +707,7 @@ module Vinter
684
707
  }
685
708
  advance
686
709
  else
687
- @errors << {
688
- message: "Expected variable name in destructuring assignment",
689
- position: @position,
690
- line: current_token ? current_token[:line] : 0,
691
- column: current_token ? current_token[:column] : 0
692
- }
710
+ add_error("Expected variable name in destructuring assignment")
693
711
  # Try to recover by advancing to next comma or closing bracket
694
712
  while current_token && current_token[:type] != :comma && current_token[:type] != :bracket_close
695
713
  advance
@@ -706,12 +724,7 @@ module Vinter
706
724
  column: bracket_token[:column]
707
725
  }
708
726
  else
709
- @errors << {
710
- message: "Expected variable name after let",
711
- position: @position,
712
- line: current_token ? current_token[:line] : 0,
713
- column: current_token ? current_token[:column] : 0
714
- }
727
+ add_error("Expected variable name after let")
715
728
  end
716
729
  end
717
730
 
@@ -723,12 +736,7 @@ module Vinter
723
736
  operator = current_token[:value]
724
737
  advance
725
738
  else
726
- @errors << {
727
- message: "Expected assignment operator after variable in let statement",
728
- position: @position,
729
- line: current_token ? current_token[:line] : 0,
730
- column: current_token ? current_token[:column] : 0
731
- }
739
+ add_error("Expected assignment operator after variable in let statement")
732
740
  end
733
741
 
734
742
  # Parse the value expression
@@ -748,12 +756,7 @@ module Vinter
748
756
  func_name = current_token[:value]
749
757
  advance
750
758
  else
751
- @errors << {
752
- message: "Expected string with function name in function() call",
753
- position: @position,
754
- line: current_token ? current_token[:line] : 0,
755
- column: current_token ? current_token[:column] : 0
756
- }
759
+ add_error("Expected string with function name in function() call")
757
760
  end
758
761
 
759
762
  # Expect closing parenthesis
@@ -972,32 +975,14 @@ module Vinter
972
975
  advance # Skip the pipe
973
976
 
974
977
  # Expect endif
975
- if current_token && current_token[:type] == :keyword && current_token[:value] == 'endif'
976
- advance # Skip 'endif'
977
- else
978
- @errors << {
979
- message: "Expected 'endif' after '|' in one-line if statement",
980
- position: @position,
981
- line: current_token ? current_token[:line] : 0,
982
- column: current_token ? current_token[:column] : 0
983
- }
978
+ unless expect_keyword('endif')
979
+ add_error("Expected 'endif' after '|' in one-line if statement")
984
980
  end
985
981
  end
986
982
  else
987
- # This is a regular multi-line if statement
988
- # Continue with your existing logic for parsing normal if statements
989
-
990
- # Parse statements until we hit 'else', 'elseif', or 'endif'
991
- while @position < @tokens.length
992
- # Check for the tokens that would terminate this block
993
- if current_token && current_token[:type] == :keyword &&
994
- ['else', 'elseif', 'endif'].include?(current_token[:value])
995
- break
996
- end
997
-
998
- stmt = parse_statement
999
- then_branch << stmt if stmt
1000
- end
983
+ # Parse multi-line if statement
984
+ # Parse statements until 'else', 'elseif', or 'endif'
985
+ then_branch = parse_body_until('else', 'elseif', 'endif')
1001
986
 
1002
987
  # Check for else/elseif
1003
988
  if current_token && current_token[:type] == :keyword
@@ -1005,14 +990,7 @@ module Vinter
1005
990
  advance # Skip 'else'
1006
991
 
1007
992
  # Parse statements until 'endif'
1008
- while @position < @tokens.length
1009
- if current_token[:type] == :keyword && current_token[:value] == 'endif'
1010
- break
1011
- end
1012
-
1013
- stmt = parse_statement
1014
- else_branch << stmt if stmt
1015
- end
993
+ else_branch = parse_body_until('endif')
1016
994
  elsif current_token[:value] == 'elseif'
1017
995
  elseif_stmt = parse_if_statement
1018
996
  else_branch << elseif_stmt if elseif_stmt
@@ -1029,19 +1007,7 @@ module Vinter
1029
1007
  end
1030
1008
 
1031
1009
  # Expect endif
1032
- if current_token && current_token[:type] == :keyword && current_token[:value] == 'endif'
1033
- advance # Skip 'endif'
1034
- else
1035
- # Don't add an error if we've already reached the end of the file
1036
- if @position < @tokens.length
1037
- @errors << {
1038
- message: "Expected 'endif' to close if statement",
1039
- position: @position,
1040
- line: current_token ? current_token[:line] : 0,
1041
- column: current_token ? current_token[:column] : 0
1042
- }
1043
- end
1044
- end
1010
+ expect_end_keyword('endif')
1045
1011
  end
1046
1012
 
1047
1013
  {
@@ -1061,20 +1027,11 @@ module Vinter
1061
1027
  column = token[:column]
1062
1028
  condition = parse_expression
1063
1029
 
1064
- body = []
1065
-
1066
- # Parse statements until we hit 'endwhile'
1067
- while @position < @tokens.length
1068
- if current_token[:type] == :keyword && current_token[:value] == 'endwhile'
1069
- break
1070
- end
1071
-
1072
- stmt = parse_statement
1073
- body << stmt if stmt
1074
- end
1030
+ # Parse statements until 'endwhile'
1031
+ body = parse_body_until('endwhile')
1075
1032
 
1076
1033
  # Expect endwhile
1077
- expect(:keyword) # This should be 'endwhile'
1034
+ expect_end_keyword('endwhile')
1078
1035
 
1079
1036
  {
1080
1037
  type: :while_statement,
@@ -1090,166 +1047,22 @@ module Vinter
1090
1047
  line = token[:line]
1091
1048
  column = token[:column]
1092
1049
 
1093
- # Two main patterns:
1094
- # 1. for [key, val] in dict - destructuring with bracket_open
1095
- # 2. for var in list - simple variable with identifier
1050
+ # Determine the type of for loop and parse variables accordingly
1051
+ loop_var = nil
1052
+ loop_vars = nil
1096
1053
 
1097
1054
  if current_token && current_token[:type] == :bracket_open
1098
- # Handle destructuring assignment: for [key, val] in dict
1055
+ # Handle destructuring with brackets: for [key, val] in dict
1099
1056
  advance # Skip '['
1100
-
1101
- loop_vars = []
1102
-
1103
- loop do
1104
- if current_token && (current_token[:type] == :identifier ||
1105
- current_token[:type] == :local_variable ||
1106
- current_token[:type] == :global_variable ||
1107
- current_token[:type] == :script_local)
1108
- loop_vars << advance
1109
- else
1110
- @errors << {
1111
- message: "Expected identifier in for loop variables",
1112
- position: @position,
1113
- line: current_token ? current_token[:line] : 0,
1114
- column: current_token ? current_token[:column] : 0
1115
- }
1116
- break
1117
- end
1118
-
1119
- if current_token && current_token[:type] == :comma
1120
- advance # Skip ','
1121
- else
1122
- break
1123
- end
1124
- end
1125
-
1126
- expect(:bracket_close) # Skip ']'
1127
-
1128
- if !current_token || (current_token[:type] != :identifier || current_token[:value] != 'in')
1129
- @errors << {
1130
- message: "Expected 'in' after for loop variables",
1131
- position: @position,
1132
- line: current_token ? current_token[:line] : 0,
1133
- column: current_token ? current_token[:column] : 0
1134
- }
1135
- else
1136
- advance # Skip 'in'
1137
- end
1138
-
1139
- iterable = parse_expression
1140
-
1141
- # Parse the body until 'endfor'
1142
- body = []
1143
- while @position < @tokens.length
1144
- if current_token && current_token[:type] == :keyword && current_token[:value] == 'endfor'
1145
- break
1146
- end
1147
-
1148
- stmt = parse_statement
1149
- body << stmt if stmt
1150
- end
1151
-
1152
- # Expect endfor
1153
- if current_token && current_token[:type] == :keyword && current_token[:value] == 'endfor'
1154
- advance # Skip 'endfor'
1155
- else
1156
- # Only add an error if we haven't reached the end of the file
1157
- if @position < @tokens.length
1158
- @errors << {
1159
- message: "Expected 'endfor' to close for statement",
1160
- position: @position,
1161
- line: current_token ? current_token[:line] : 0,
1162
- column: current_token ? current_token[:column] : 0
1163
- }
1164
- end
1165
- end
1166
-
1167
- return {
1168
- type: :for_statement,
1169
- loop_vars: loop_vars,
1170
- iterable: iterable,
1171
- body: body,
1172
- line: line,
1173
- column: column
1174
- }
1057
+ loop_vars = parse_loop_variables
1058
+ expect(:bracket_close)
1059
+ expect_in_keyword
1175
1060
  elsif current_token && current_token[:type] == :paren_open
1176
- # Handle multiple variables in parentheses: for (var1, var2) in list
1061
+ # Handle multiple variables with parentheses: for (var1, var2) in list
1177
1062
  advance # Skip '('
1178
-
1179
- loop_vars = []
1180
-
1181
- loop do
1182
- if current_token && (current_token[:type] == :identifier ||
1183
- current_token[:type] == :local_variable ||
1184
- current_token[:type] == :global_variable ||
1185
- current_token[:type] == :script_local)
1186
- loop_vars << advance
1187
- else
1188
- @errors << {
1189
- message: "Expected identifier in for loop variables",
1190
- position: @position,
1191
- line: current_token ? current_token[:line] : 0,
1192
- column: current_token ? current_token[:column] : 0
1193
- }
1194
- break
1195
- end
1196
-
1197
- if current_token && current_token[:type] == :comma
1198
- advance # Skip ','
1199
- else
1200
- break
1201
- end
1202
- end
1203
-
1204
- expect(:paren_close) # Skip ')'
1205
-
1206
- if !current_token || (current_token[:type] != :identifier || current_token[:value] != 'in')
1207
- @errors << {
1208
- message: "Expected 'in' after for loop variables",
1209
- position: @position,
1210
- line: current_token ? current_token[:line] : 0,
1211
- column: current_token ? current_token[:column] : 0
1212
- }
1213
- else
1214
- advance # Skip 'in'
1215
- end
1216
-
1217
- iterable = parse_expression
1218
-
1219
- # Parse the body until 'endfor'
1220
- body = []
1221
- while @position < @tokens.length
1222
- if current_token && current_token[:type] == :keyword && current_token[:value] == 'endfor'
1223
- break
1224
- end
1225
-
1226
- stmt = parse_statement
1227
- body << stmt if stmt
1228
- end
1229
-
1230
- # Expect endfor
1231
- if current_token && current_token[:type] == :keyword && current_token[:value] == 'endfor'
1232
- advance # Skip 'endfor'
1233
- else
1234
- # Only add an error if we haven't reached the end of the file
1235
- if @position < @tokens.length
1236
- @errors << {
1237
- message: "Expected 'endfor' to close for statement",
1238
- position: @position,
1239
- line: current_token ? current_token[:line] : 0,
1240
- column: current_token ? current_token[:column] : 0
1241
- }
1242
- end
1243
- end
1244
-
1245
- return {
1246
- type: :for_statement,
1247
- loop_vars: loop_vars,
1248
- iterable: iterable,
1249
- body: body,
1250
- line: line,
1251
- column: column
1252
- }
1063
+ loop_vars = parse_loop_variables
1064
+ expect(:paren_close)
1065
+ expect_in_keyword
1253
1066
  else
1254
1067
  # Handle single variable: for var in list
1255
1068
  if current_token && (current_token[:type] == :identifier ||
@@ -1258,63 +1071,36 @@ module Vinter
1258
1071
  current_token[:type] == :script_local)
1259
1072
  loop_var = advance
1260
1073
  else
1261
- @errors << {
1262
- message: "Expected identifier as for loop variable",
1263
- position: @position,
1264
- line: current_token ? current_token[:line] : 0,
1265
- column: current_token ? current_token[:column] : 0
1266
- }
1267
- loop_var = nil
1268
- end
1269
-
1270
- if !current_token || (current_token[:type] != :identifier || current_token[:value] != 'in')
1271
- @errors << {
1272
- message: "Expected 'in' after for loop variable",
1273
- position: @position,
1274
- line: current_token ? current_token[:line] : 0,
1275
- column: current_token ? current_token[:column] : 0
1276
- }
1277
- else
1278
- advance # Skip 'in'
1074
+ add_error("Expected identifier as for loop variable")
1279
1075
  end
1076
+ expect_in_keyword
1077
+ end
1280
1078
 
1281
- iterable = parse_expression
1079
+ # Parse the iterable expression
1080
+ iterable = parse_expression
1282
1081
 
1283
- # Parse the body until 'endfor'
1284
- body = []
1285
- while @position < @tokens.length
1286
- if current_token && current_token[:type] == :keyword && current_token[:value] == 'endfor'
1287
- break
1288
- end
1082
+ # Parse the body until 'endfor'
1083
+ body = parse_body_until('endfor')
1289
1084
 
1290
- stmt = parse_statement
1291
- body << stmt if stmt
1292
- end
1085
+ # Expect endfor
1086
+ expect_end_keyword('endfor')
1293
1087
 
1294
- # Expect endfor
1295
- if current_token && current_token[:type] == :keyword && current_token[:value] == 'endfor'
1296
- advance # Skip 'endfor'
1297
- else
1298
- # Only add an error if we haven't reached the end of the file
1299
- if @position < @tokens.length
1300
- @errors << {
1301
- message: "Expected 'endfor' to close for statement",
1302
- position: @position,
1303
- line: current_token ? current_token[:line] : 0,
1304
- column: current_token ? current_token[:column] : 0
1305
- }
1306
- end
1307
- end
1088
+ # Build result hash based on whether we have single or multiple vars
1089
+ result = {
1090
+ type: :for_statement,
1091
+ iterable: iterable,
1092
+ body: body,
1093
+ line: line,
1094
+ column: column
1095
+ }
1308
1096
 
1309
- return {
1310
- type: :for_statement,
1311
- loop_var: loop_var,
1312
- iterable: iterable,
1313
- body: body,
1314
- line: line,
1315
- column: column
1316
- }
1097
+ if loop_vars
1098
+ result[:loop_vars] = loop_vars
1099
+ else
1100
+ result[:loop_var] = loop_var
1317
1101
  end
1102
+
1103
+ result
1318
1104
  end
1319
1105
 
1320
1106
  def parse_def_function
@@ -1336,19 +1122,11 @@ module Vinter
1336
1122
  return_type = parse_type
1337
1123
  end
1338
1124
 
1339
- # Parse function body
1340
- body = []
1341
- while @position < @tokens.length
1342
- if current_token[:type] == :keyword && current_token[:value] == 'enddef'
1343
- break
1344
- end
1345
-
1346
- stmt = parse_statement
1347
- body << stmt if stmt
1348
- end
1125
+ # Parse function body until 'enddef'
1126
+ body = parse_body_until('enddef')
1349
1127
 
1350
1128
  # Expect enddef
1351
- expect(:keyword) # This should be 'enddef'
1129
+ expect_end_keyword('enddef')
1352
1130
 
1353
1131
  {
1354
1132
  type: :def_function,
@@ -1391,12 +1169,7 @@ module Vinter
1391
1169
 
1392
1170
  # After varargs, we expect closing paren
1393
1171
  if current_token && current_token[:type] != :paren_close
1394
- @errors << {
1395
- message: "Expected closing parenthesis after varargs",
1396
- position: @position,
1397
- line: current_token[:line],
1398
- column: current_token[:column]
1399
- }
1172
+ add_error("Expected closing parenthesis after varargs", current_token)
1400
1173
  end
1401
1174
 
1402
1175
  break
@@ -1404,12 +1177,7 @@ module Vinter
1404
1177
 
1405
1178
  # Get parameter name
1406
1179
  if !current_token || current_token[:type] != :identifier
1407
- @errors << {
1408
- message: "Expected parameter name",
1409
- position: @position,
1410
- line: current_token ? current_token[:line] : 0,
1411
- column: current_token ? current_token[:column] : 0
1412
- }
1180
+ add_error("Expected parameter name")
1413
1181
  break
1414
1182
  end
1415
1183
 
@@ -1444,12 +1212,7 @@ module Vinter
1444
1212
  advance
1445
1213
  # If we don't have a comma, we should have a closing paren
1446
1214
  elsif current_token && current_token[:type] != :paren_close
1447
- @errors << {
1448
- message: "Expected comma or closing parenthesis after parameter",
1449
- position: @position,
1450
- line: current_token[:line],
1451
- column: current_token[:column]
1452
- }
1215
+ add_error("Expected comma or closing parenthesis after parameter", current_token)
1453
1216
  break
1454
1217
  end
1455
1218
  end
@@ -1478,12 +1241,7 @@ module Vinter
1478
1241
 
1479
1242
  return type_name[:value]
1480
1243
  else
1481
- @errors << {
1482
- message: "Expected type identifier",
1483
- position: @position,
1484
- line: current_token ? current_token[:line] : 0,
1485
- column: current_token ? current_token[:column] : 0
1486
- }
1244
+ add_error("Expected type identifier")
1487
1245
  advance
1488
1246
  return "unknown"
1489
1247
  end
@@ -1496,12 +1254,7 @@ module Vinter
1496
1254
  column = var_type_token[:column]
1497
1255
 
1498
1256
  if !current_token || current_token[:type] != :identifier
1499
- @errors << {
1500
- message: "Expected variable name",
1501
- position: @position,
1502
- line: current_token ? current_token[:line] : 0,
1503
- column: current_token ? current_token[:column] : 0
1504
- }
1257
+ add_error("Expected variable name")
1505
1258
  return nil
1506
1259
  end
1507
1260
 
@@ -2064,6 +1817,18 @@ module Vinter
2064
1817
  end
2065
1818
  end
2066
1819
  end
1820
+ when :vimfuncs
1821
+ advance
1822
+ if current_token[:type] == :paren_open
1823
+ expr = parse_function_call(token[:value], line, column)
1824
+ else
1825
+ expr = {
1826
+ type: :vimfuncs,
1827
+ name: token[:value],
1828
+ line: line,
1829
+ column: column
1830
+ }
1831
+ end
2067
1832
  when :namespace_prefix
2068
1833
  advance
2069
1834
  expr = {
@@ -2099,6 +1864,12 @@ module Vinter
2099
1864
  when :line_continuation
2100
1865
  advance
2101
1866
  expr = parse_expression
1867
+ when :interpolated_string
1868
+ advance
1869
+ expr = {
1870
+ type: :interpolated_string,
1871
+ value: token[:value],
1872
+ }
2102
1873
  else
2103
1874
  @errors << {
2104
1875
  message: "Unexpected token in expression: #{token[:type]}",
@@ -2117,7 +1888,7 @@ module Vinter
2117
1888
  # Look ahead to determine if this is property access or concatenation
2118
1889
  next_token = peek_token
2119
1890
  is_property_access = next_token && (next_token[:type] == :identifier || next_token[:type] == :keyword)
2120
-
1891
+
2121
1892
  # Check if this is a property access (only when right side is identifier/keyword)
2122
1893
  if is_property_access && (expr[:type] == :identifier || expr[:type] == :global_variable ||
2123
1894
  expr[:type] == :script_local || expr[:type] == :namespace_prefix ||
@@ -2754,7 +2525,7 @@ module Vinter
2754
2525
  current_token[:type] == :whitespace)
2755
2526
  advance
2756
2527
  end
2757
-
2528
+
2758
2529
  # After skipping continuations, we should find the closing brace
2759
2530
  if current_token && current_token[:type] == :brace_close
2760
2531
  # This is fine - last entry without trailing comma
@@ -3086,6 +2857,13 @@ module Vinter
3086
2857
  column: column
3087
2858
  }
3088
2859
  end
2860
+
2861
+ def parse_scriptencoding
2862
+ token = advance
2863
+ advance
2864
+ { type: :scriptencoding, encoding: current_token[:value] }
2865
+ end
2866
+
3089
2867
  def parse_export_statement
3090
2868
  token = advance # Skip 'export'
3091
2869
  line = token[:line]
@@ -3296,33 +3074,11 @@ module Vinter
3296
3074
  end
3297
3075
  end
3298
3076
 
3299
- # Parse function body
3300
- body = []
3301
- while @position < @tokens.length
3302
- if current_token && current_token[:type] == :keyword &&
3303
- ['endfunction', 'endfunc'].include?(current_token[:value])
3304
- break
3305
- end
3306
-
3307
- stmt = parse_statement
3308
- body << stmt if stmt
3309
- end
3077
+ # Parse function body until 'endfunction' or 'endfunc'
3078
+ body = parse_body_until('endfunction', 'endfunc')
3310
3079
 
3311
3080
  # Expect endfunction/endfunc
3312
- if current_token && current_token[:type] == :keyword &&
3313
- ['endfunction', 'endfunc'].include?(current_token[:value])
3314
- advance # Skip 'endfunction' or 'endfunc'
3315
- else
3316
- # Only add an error if we haven't reached the end of the file
3317
- if @position < @tokens.length
3318
- @errors << {
3319
- message: "Expected 'endfunction' or 'endfunc'",
3320
- position: @position,
3321
- line: current_token ? current_token[:line] : 0,
3322
- column: current_token ? current_token[:column] : 0
3323
- }
3324
- end
3325
- end
3081
+ expect_end_keyword('endfunction', 'endfunc')
3326
3082
 
3327
3083
  function_name = name ? name[:value] : nil
3328
3084
  if function_scope
@@ -3508,22 +3264,12 @@ module Vinter
3508
3264
  line = token[:line]
3509
3265
  column = token[:column]
3510
3266
 
3511
- # Parse the try body
3512
- body = []
3267
+ # Parse the try body until catch/finally/endtry
3268
+ body = parse_body_until('catch', 'finally', 'endtry')
3269
+
3513
3270
  catch_clauses = []
3514
3271
  finally_clause = nil
3515
3272
 
3516
- # Parse statements in the try block
3517
- while @position < @tokens.length
3518
- if current_token && current_token[:type] == :keyword &&
3519
- ['catch', 'finally', 'endtry'].include?(current_token[:value])
3520
- break
3521
- end
3522
-
3523
- stmt = parse_statement
3524
- body << stmt if stmt
3525
- end
3526
-
3527
3273
  # Parse catch clauses
3528
3274
  while @position < @tokens.length &&
3529
3275
  current_token && current_token[:type] == :keyword &&
@@ -3549,17 +3295,8 @@ module Vinter
3549
3295
  advance
3550
3296
  end
3551
3297
 
3552
- # Parse the catch body
3553
- catch_body = []
3554
- while @position < @tokens.length
3555
- if current_token && current_token[:type] == :keyword &&
3556
- ['catch', 'finally', 'endtry'].include?(current_token[:value])
3557
- break
3558
- end
3559
-
3560
- stmt = parse_statement
3561
- catch_body << stmt if stmt
3562
- end
3298
+ # Parse the catch body until next catch/finally/endtry
3299
+ catch_body = parse_body_until('catch', 'finally', 'endtry')
3563
3300
 
3564
3301
  catch_clauses << {
3565
3302
  type: :catch_clause,
@@ -3578,16 +3315,8 @@ module Vinter
3578
3315
  finally_line = finally_token[:line]
3579
3316
  finally_column = finally_token[:column]
3580
3317
 
3581
- # Parse the finally body
3582
- finally_body = []
3583
- while @position < @tokens.length
3584
- if current_token && current_token[:type] == :keyword && current_token[:value] == 'endtry'
3585
- break
3586
- end
3587
-
3588
- stmt = parse_statement
3589
- finally_body << stmt if stmt
3590
- end
3318
+ # Parse the finally body until 'endtry'
3319
+ finally_body = parse_body_until('endtry')
3591
3320
 
3592
3321
  finally_clause = {
3593
3322
  type: :finally_clause,
@@ -3598,19 +3327,7 @@ module Vinter
3598
3327
  end
3599
3328
 
3600
3329
  # Expect endtry
3601
- if current_token && current_token[:type] == :keyword && current_token[:value] == 'endtry'
3602
- advance # Skip 'endtry'
3603
- else
3604
- # Only add an error if we haven't reached the end of the file
3605
- if @position < @tokens.length
3606
- @errors << {
3607
- message: "Expected 'endtry' to close try statement",
3608
- position: @position,
3609
- line: current_token ? current_token[:line] : 0,
3610
- column: current_token ? current_token[:column] : 0
3611
- }
3612
- end
3613
- end
3330
+ expect_end_keyword('endtry')
3614
3331
 
3615
3332
  return {
3616
3333
  type: :try_statement,
@@ -3881,14 +3598,14 @@ module Vinter
3881
3598
  def parse_brace_sequence_value
3882
3599
  # Handle special brace sequences like {{{,}}} for foldmarker
3883
3600
  value_parts = []
3884
-
3885
- while current_token && (current_token[:type] == :brace_open ||
3886
- current_token[:type] == :brace_close ||
3601
+
3602
+ while current_token && (current_token[:type] == :brace_open ||
3603
+ current_token[:type] == :brace_close ||
3887
3604
  current_token[:type] == :comma)
3888
3605
  value_parts << current_token[:value]
3889
3606
  advance
3890
3607
  end
3891
-
3608
+
3892
3609
  return {
3893
3610
  type: :brace_sequence,
3894
3611
  value: value_parts.join(''),
@@ -3900,7 +3617,7 @@ module Vinter
3900
3617
  def parse_comma_separated_value
3901
3618
  # Handle comma-separated option values like menu,menuone,noinsert,noselect
3902
3619
  values = []
3903
-
3620
+
3904
3621
  # Parse first value
3905
3622
  if current_token && current_token[:type] == :identifier
3906
3623
  values << current_token[:value]
@@ -3908,7 +3625,7 @@ module Vinter
3908
3625
  else
3909
3626
  return parse_expression # Fall back to regular expression parsing
3910
3627
  end
3911
-
3628
+
3912
3629
  # Parse additional comma-separated values
3913
3630
  while current_token && current_token[:type] == :comma
3914
3631
  advance # Skip comma
@@ -3919,7 +3636,7 @@ module Vinter
3919
3636
  break
3920
3637
  end
3921
3638
  end
3922
-
3639
+
3923
3640
  return {
3924
3641
  type: :comma_separated_value,
3925
3642
  values: values,
@@ -3971,7 +3688,7 @@ module Vinter
3971
3688
  value = nil
3972
3689
  if current_token && current_token[:type] == :operator && current_token[:value] == '='
3973
3690
  advance # Skip '='
3974
-
3691
+
3975
3692
  # Special handling for foldmarker and similar options that use brace notation
3976
3693
  if option_name == 'foldmarker' && current_token && current_token[:type] == :brace_open
3977
3694
  # Parse as special brace sequence value (e.g., {{{,}}})
@@ -4098,8 +3815,8 @@ module Vinter
4098
3815
  advance
4099
3816
 
4100
3817
  # Parse attribute value (can be identifier, number, or hex color)
4101
- if current_token && (current_token[:type] == :identifier ||
4102
- current_token[:type] == :number ||
3818
+ if current_token && (current_token[:type] == :identifier ||
3819
+ current_token[:type] == :number ||
4103
3820
  current_token[:type] == :hex_color)
4104
3821
  attributes[attr_name] = current_token[:value]
4105
3822
  advance
data/lib/vinter.rb CHANGED
@@ -2,7 +2,8 @@ require "vinter/lexer"
2
2
  require "vinter/parser"
3
3
  require "vinter/linter"
4
4
  require "vinter/cli"
5
+ require "vinter/ast_printer"
5
6
 
6
7
  module Vinter
7
- VERSION = "0.4.0"
8
+ VERSION = "0.5.0"
8
9
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vinter
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dan Bradbury
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-08-16 00:00:00.000000000 Z
11
+ date: 2025-11-26 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: A linter for the Vim9 script language, helping to identify issues and
14
14
  enforce best practices
@@ -22,6 +22,7 @@ files:
22
22
  - README.md
23
23
  - bin/vinter
24
24
  - lib/vinter.rb
25
+ - lib/vinter/ast_printer.rb
25
26
  - lib/vinter/cli.rb
26
27
  - lib/vinter/lexer.rb
27
28
  - lib/vinter/linter.rb