vinter 0.1.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 +7 -0
- data/README.md +95 -0
- data/bin/vinter +5 -0
- data/lib/vinter/cli.rb +47 -0
- data/lib/vinter/lexer.rb +136 -0
- data/lib/vinter/linter.rb +153 -0
- data/lib/vinter/parser.rb +789 -0
- data/lib/vinter.rb +8 -0
- metadata +51 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: '0916f02e9f17998516fc27eb0d2b2ec2807d1f4ae3ab12050590556c6d78e0ba'
|
4
|
+
data.tar.gz: 21cec3329527181dde80ebe5e12d5dba094c4596c1bcb725a1ef7dc95505b43e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ce1879809bfc52ff91ad591bbd862ef347cfc8a6595a042cd7bc9efb0c9bb95824ee177844a5499b9264c9dc3c81f4e5bda1b7ccbdb9a85800ab2a87697d07d0
|
7
|
+
data.tar.gz: 10741136273e679ba2cf0c334e4ba815a9ba8fae25ba677fb0d7bd9f441bd912cff8c87e8919c27db110b6bab066102de5188f23305cbde76542793a4a4cb7c2
|
data/README.md
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
# Vinter
|
2
|
+
|
3
|
+
A Ruby gem that provides linting capabilities for Vim9 script files. This linter helps identify syntax errors and enforce best practices for the new Vim9 script language introduced in Vim 9.0.
|
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
|
+
## Installation
|
13
|
+
|
14
|
+
Install the gem:
|
15
|
+
|
16
|
+
```bash
|
17
|
+
gem install vinter
|
18
|
+
```
|
19
|
+
|
20
|
+
## Usage
|
21
|
+
|
22
|
+
### Command Line
|
23
|
+
|
24
|
+
Lint a Vim9 script file:
|
25
|
+
|
26
|
+
```bash
|
27
|
+
vim9-lint path/to/your/script.vim
|
28
|
+
```
|
29
|
+
|
30
|
+
### Ruby API
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
require 'vinter'
|
34
|
+
|
35
|
+
content = File.read('path/to/your/script.vim')
|
36
|
+
linter = Vim9Linter::Linter.new
|
37
|
+
issues = linter.lint(content)
|
38
|
+
|
39
|
+
issues.each do |issue|
|
40
|
+
puts "#{issue[:type]}: #{issue[:message]} at line #{issue[:line]}, column #{issue[:column]}"
|
41
|
+
end
|
42
|
+
```
|
43
|
+
|
44
|
+
## Supported Rules
|
45
|
+
|
46
|
+
The linter includes several built-in rules:
|
47
|
+
|
48
|
+
1. **missing-vim9script-declaration**: Checks if Vim9 script files start with the required `vim9script` declaration
|
49
|
+
2. **prefer-def-over-function**: Encourages using `def` instead of `function` in Vim9 scripts
|
50
|
+
3. **missing-type-annotation**: Identifies variable declarations without type annotations
|
51
|
+
4. **missing-return-type**: Identifies functions without return type annotations
|
52
|
+
|
53
|
+
## Adding Custom Rules
|
54
|
+
|
55
|
+
You can extend the linter with your own custom rules:
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
linter = Vinter::Linter.new
|
59
|
+
|
60
|
+
# Define a custom rule
|
61
|
+
custom_rule = Vinter::Rule.new(
|
62
|
+
"my-custom-rule",
|
63
|
+
"Description of what the rule checks"
|
64
|
+
) do |ast|
|
65
|
+
issues = []
|
66
|
+
|
67
|
+
# Analyze the AST and identify issues
|
68
|
+
# ...
|
69
|
+
|
70
|
+
issues
|
71
|
+
end
|
72
|
+
|
73
|
+
# Register the custom rule
|
74
|
+
linter.register_rule(custom_rule)
|
75
|
+
|
76
|
+
# Run the linter with your custom rule
|
77
|
+
issues = linter.lint(content)
|
78
|
+
```
|
79
|
+
|
80
|
+
## Vim9 Script Resources
|
81
|
+
|
82
|
+
- [Vim9 Script Documentation](https://vimhelp.org/vim9.txt.html)
|
83
|
+
- [Upgrading to Vim9 Script](https://www.baeldung.com/linux/vim-script-upgrade)
|
84
|
+
|
85
|
+
## Contributing
|
86
|
+
|
87
|
+
1. Fork the repository
|
88
|
+
2. Create a feature branch: `git checkout -b my-new-feature`
|
89
|
+
3. Commit your changes: `git commit -am 'Add some feature'`
|
90
|
+
4. Push to the branch: `git push origin my-new-feature`
|
91
|
+
5. Submit a pull request
|
92
|
+
|
93
|
+
## License
|
94
|
+
|
95
|
+
This project is licensed under the MIT License - see the LICENSE file for details.
|
data/bin/vinter
ADDED
data/lib/vinter/cli.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
module Vinter
|
2
|
+
class CLI
|
3
|
+
def initialize
|
4
|
+
@linter = Linter.new
|
5
|
+
end
|
6
|
+
|
7
|
+
def run(args)
|
8
|
+
if args.empty?
|
9
|
+
puts "Usage: vim9-lint [file.vim]"
|
10
|
+
return 1
|
11
|
+
end
|
12
|
+
|
13
|
+
file_path = args[0]
|
14
|
+
|
15
|
+
unless File.exist?(file_path)
|
16
|
+
puts "Error: File not found: #{file_path}"
|
17
|
+
return 1
|
18
|
+
end
|
19
|
+
|
20
|
+
content = File.read(file_path)
|
21
|
+
issues = @linter.lint(content)
|
22
|
+
|
23
|
+
if issues.empty?
|
24
|
+
puts "No issues found in #{file_path}"
|
25
|
+
return 0
|
26
|
+
else
|
27
|
+
puts "Found #{issues.length} issues in #{file_path}:"
|
28
|
+
|
29
|
+
issues.each do |issue|
|
30
|
+
type_str = case issue[:type]
|
31
|
+
when :error then "ERROR"
|
32
|
+
when :warning then "WARNING"
|
33
|
+
when :rule then "RULE(#{issue[:rule]})"
|
34
|
+
else "UNKNOWN"
|
35
|
+
end
|
36
|
+
|
37
|
+
line = issue[:line] || 1
|
38
|
+
column = issue[:column] || 1
|
39
|
+
|
40
|
+
puts "#{file_path}:#{line}:#{column}: #{type_str}: #{issue[:message]}"
|
41
|
+
end
|
42
|
+
|
43
|
+
return issues.any? { |i| i[:type] == :error } ? 1 : 0
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/lib/vinter/lexer.rb
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
module Vinter
|
2
|
+
class Lexer
|
3
|
+
TOKEN_TYPES = {
|
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/,
|
6
|
+
# Identifiers can include # and special characters
|
7
|
+
identifier: /\b[a-zA-Z_][a-zA-Z0-9_#]*\b/,
|
8
|
+
# Single-character operators
|
9
|
+
operator: /[\+\-\*\/=<>!&\|\.]/,
|
10
|
+
# Multi-character operators handled separately
|
11
|
+
number: /\b\d+(\.\d+)?\b/,
|
12
|
+
# Handle both single and double quoted strings
|
13
|
+
string: /"([^"\\]|\\.)*"|'([^'\\]|\\.)*'/,
|
14
|
+
# Vim9 comments use #
|
15
|
+
comment: /#.*/,
|
16
|
+
whitespace: /\s+/,
|
17
|
+
brace_open: /\{/,
|
18
|
+
brace_close: /\}/,
|
19
|
+
paren_open: /\(/,
|
20
|
+
paren_close: /\)/,
|
21
|
+
bracket_open: /\[/,
|
22
|
+
bracket_close: /\]/,
|
23
|
+
colon: /:/,
|
24
|
+
semicolon: /;/,
|
25
|
+
comma: /,/,
|
26
|
+
}
|
27
|
+
|
28
|
+
def initialize(input)
|
29
|
+
@input = input
|
30
|
+
@tokens = []
|
31
|
+
@position = 0
|
32
|
+
@line_num = 1
|
33
|
+
@column = 1
|
34
|
+
end
|
35
|
+
|
36
|
+
def tokenize
|
37
|
+
until @position >= @input.length
|
38
|
+
chunk = @input[@position..-1]
|
39
|
+
|
40
|
+
# Handle multi-character operators explicitly
|
41
|
+
if match = chunk.match(/\A(==|!=|=>|->|\.\.)/)
|
42
|
+
@tokens << {
|
43
|
+
type: :operator,
|
44
|
+
value: match[0],
|
45
|
+
line: @line_num,
|
46
|
+
column: @column
|
47
|
+
}
|
48
|
+
@column += match[0].length
|
49
|
+
@position += match[0].length
|
50
|
+
next
|
51
|
+
end
|
52
|
+
|
53
|
+
# Handle ellipsis for variable args
|
54
|
+
if chunk.start_with?('...')
|
55
|
+
@tokens << {
|
56
|
+
type: :ellipsis,
|
57
|
+
value: '...',
|
58
|
+
line: @line_num,
|
59
|
+
column: @column
|
60
|
+
}
|
61
|
+
@column += 3
|
62
|
+
@position += 3
|
63
|
+
next
|
64
|
+
end
|
65
|
+
|
66
|
+
# Skip whitespace but track position
|
67
|
+
if match = chunk.match(/\A(\s+)/)
|
68
|
+
whitespace = match[0]
|
69
|
+
whitespace.each_char do |c|
|
70
|
+
if c == "\n"
|
71
|
+
@line_num += 1
|
72
|
+
@column = 1
|
73
|
+
else
|
74
|
+
@column += 1
|
75
|
+
end
|
76
|
+
end
|
77
|
+
@position += whitespace.length
|
78
|
+
next
|
79
|
+
end
|
80
|
+
|
81
|
+
match_found = false
|
82
|
+
|
83
|
+
TOKEN_TYPES.each do |type, pattern|
|
84
|
+
if match = chunk.match(/\A(#{pattern})/)
|
85
|
+
value = match[0]
|
86
|
+
token = {
|
87
|
+
type: type,
|
88
|
+
value: value,
|
89
|
+
line: @line_num,
|
90
|
+
column: @column
|
91
|
+
}
|
92
|
+
@tokens << token unless type == :whitespace
|
93
|
+
|
94
|
+
# Update position
|
95
|
+
if value.include?("\n")
|
96
|
+
lines = value.split("\n")
|
97
|
+
@line_num += lines.size - 1
|
98
|
+
if lines.size > 1
|
99
|
+
@column = lines.last.length + 1
|
100
|
+
else
|
101
|
+
@column += value.length
|
102
|
+
end
|
103
|
+
else
|
104
|
+
@column += value.length
|
105
|
+
end
|
106
|
+
|
107
|
+
@position += value.length
|
108
|
+
match_found = true
|
109
|
+
break
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
unless match_found
|
114
|
+
# Try to handle unknown characters
|
115
|
+
@tokens << {
|
116
|
+
type: :unknown,
|
117
|
+
value: chunk[0],
|
118
|
+
line: @line_num,
|
119
|
+
column: @column
|
120
|
+
}
|
121
|
+
|
122
|
+
if chunk[0] == "\n"
|
123
|
+
@line_num += 1
|
124
|
+
@column = 1
|
125
|
+
else
|
126
|
+
@column += 1
|
127
|
+
end
|
128
|
+
|
129
|
+
@position += 1
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
@tokens
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
module Vinter
|
2
|
+
class Linter
|
3
|
+
def initialize
|
4
|
+
@rules = []
|
5
|
+
register_default_rules
|
6
|
+
end
|
7
|
+
|
8
|
+
def register_rule(rule)
|
9
|
+
@rules << rule
|
10
|
+
end
|
11
|
+
|
12
|
+
def register_default_rules
|
13
|
+
# Rule: Vim9 script files should start with vim9script declaration
|
14
|
+
register_rule(Rule.new("missing-vim9script-declaration", "Script does not start with vim9script declaration") do |ast|
|
15
|
+
if ast[:type] == :program && (ast[:body].empty? || ast[:body][0][:type] != :vim9script_declaration)
|
16
|
+
[{ message: "File should start with vim9script declaration", line: 1, column: 1 }]
|
17
|
+
else
|
18
|
+
[]
|
19
|
+
end
|
20
|
+
end)
|
21
|
+
|
22
|
+
# Rule: Prefer def over function in Vim9 script
|
23
|
+
register_rule(Rule.new("prefer-def-over-function", "Use def instead of function in Vim9 script") do |ast|
|
24
|
+
issues = []
|
25
|
+
|
26
|
+
traverse_ast(ast) do |node|
|
27
|
+
if node[:type] == :legacy_function
|
28
|
+
issues << { message: "Use def instead of function for #{node[:name]}", line: node[:line] || 0, column: node[:column] || 0 }
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
issues
|
33
|
+
end)
|
34
|
+
|
35
|
+
# Rule: Variables should have type annotations
|
36
|
+
register_rule(Rule.new("missing-type-annotation", "Variable declaration is missing type annotation") do |ast|
|
37
|
+
issues = []
|
38
|
+
|
39
|
+
traverse_ast(ast) do |node|
|
40
|
+
if node[:type] == :variable_declaration && node[:var_type_annotation].nil? && node[:var_type] != 'const'
|
41
|
+
issues << { message: "Variable #{node[:name]} should have a type annotation", line: node[:line] || 0, column: node[:column] || 0 }
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
issues
|
46
|
+
end)
|
47
|
+
|
48
|
+
# Rule: Functions should have return type annotations
|
49
|
+
register_rule(Rule.new("missing-return-type", "Function is missing return type annotation") do |ast|
|
50
|
+
issues = []
|
51
|
+
|
52
|
+
traverse_ast(ast) do |node|
|
53
|
+
if node[:type] == :def_function && node[:return_type].nil?
|
54
|
+
issues << { message: "Function #{node[:name]} should have a return type annotation", line: node[:line] || 0, column: node[:column] || 0 }
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
issues
|
59
|
+
end)
|
60
|
+
|
61
|
+
# Rule: Function parameters should have type annotations
|
62
|
+
register_rule(Rule.new("missing-param-type", "Function parameter is missing type annotation") do |ast|
|
63
|
+
issues = []
|
64
|
+
|
65
|
+
traverse_ast(ast) do |node|
|
66
|
+
if node[:type] == :def_function
|
67
|
+
node[:params].each do |param|
|
68
|
+
if param[:type] == :parameter && param[:param_type].nil?
|
69
|
+
issues << { message: "Parameter #{param[:name]} should have a type annotation", line: param[:line] || 0, column: param[:column] || 0 }
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
issues
|
76
|
+
end)
|
77
|
+
end
|
78
|
+
|
79
|
+
def traverse_ast(node, &block)
|
80
|
+
return unless node.is_a?(Hash)
|
81
|
+
|
82
|
+
yield node
|
83
|
+
|
84
|
+
node.each do |key, value|
|
85
|
+
if value.is_a?(Array)
|
86
|
+
value.each { |item| traverse_ast(item, &block) if item.is_a?(Hash) }
|
87
|
+
elsif value.is_a?(Hash)
|
88
|
+
traverse_ast(value, &block)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def lint(content)
|
94
|
+
lexer = Lexer.new(content)
|
95
|
+
tokens = lexer.tokenize
|
96
|
+
|
97
|
+
parser = Parser.new(tokens)
|
98
|
+
result = parser.parse
|
99
|
+
|
100
|
+
issues = []
|
101
|
+
|
102
|
+
# Add parser errors
|
103
|
+
result[:errors].each do |error|
|
104
|
+
issues << {
|
105
|
+
type: :error,
|
106
|
+
message: error[:message],
|
107
|
+
position: error[:position],
|
108
|
+
line: error[:line] || 0,
|
109
|
+
column: error[:column] || 0
|
110
|
+
}
|
111
|
+
end
|
112
|
+
|
113
|
+
# Add parser warnings
|
114
|
+
result[:warnings].each do |warning|
|
115
|
+
issues << {
|
116
|
+
type: :warning,
|
117
|
+
message: warning[:message],
|
118
|
+
position: warning[:position],
|
119
|
+
line: warning[:line] || 0,
|
120
|
+
column: warning[:column] || 0
|
121
|
+
}
|
122
|
+
end
|
123
|
+
|
124
|
+
# Apply rules
|
125
|
+
@rules.each do |rule|
|
126
|
+
rule_issues = rule.apply(result[:ast])
|
127
|
+
issues.concat(rule_issues.map { |i| {
|
128
|
+
type: :rule,
|
129
|
+
rule: rule.id,
|
130
|
+
message: i[:message],
|
131
|
+
line: i[:line] || 0,
|
132
|
+
column: i[:column] || 0
|
133
|
+
}})
|
134
|
+
end
|
135
|
+
|
136
|
+
issues
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
class Rule
|
141
|
+
attr_reader :id, :description
|
142
|
+
|
143
|
+
def initialize(id, description, &block)
|
144
|
+
@id = id
|
145
|
+
@description = description
|
146
|
+
@check = block
|
147
|
+
end
|
148
|
+
|
149
|
+
def apply(ast)
|
150
|
+
@check.call(ast)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
@@ -0,0 +1,789 @@
|
|
1
|
+
module Vinter
|
2
|
+
class Parser
|
3
|
+
def initialize(tokens)
|
4
|
+
@tokens = tokens
|
5
|
+
@position = 0
|
6
|
+
@errors = []
|
7
|
+
@warnings = []
|
8
|
+
end
|
9
|
+
|
10
|
+
def parse
|
11
|
+
result = parse_program
|
12
|
+
{
|
13
|
+
ast: result,
|
14
|
+
errors: @errors,
|
15
|
+
warnings: @warnings
|
16
|
+
}
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def current_token
|
22
|
+
@tokens[@position]
|
23
|
+
end
|
24
|
+
|
25
|
+
def peek_token(offset = 1)
|
26
|
+
@tokens[@position + offset]
|
27
|
+
end
|
28
|
+
|
29
|
+
def advance
|
30
|
+
token = current_token
|
31
|
+
@position += 1 if @position < @tokens.length
|
32
|
+
token
|
33
|
+
end
|
34
|
+
|
35
|
+
def expect(type)
|
36
|
+
if current_token && current_token[:type] == type
|
37
|
+
token = current_token
|
38
|
+
advance
|
39
|
+
return token
|
40
|
+
else
|
41
|
+
expected = type
|
42
|
+
found = current_token ? current_token[:type] : "end of input"
|
43
|
+
line = current_token ? current_token[:line] : 0
|
44
|
+
column = current_token ? current_token[:column] : 0
|
45
|
+
error = "Expected #{expected} but found #{found}"
|
46
|
+
@errors << { message: error, position: @position, line: line, column: column }
|
47
|
+
nil
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def parse_program
|
52
|
+
statements = []
|
53
|
+
|
54
|
+
# Check for vim9script declaration
|
55
|
+
if current_token && current_token[:type] == :keyword && current_token[:value] == 'vim9script'
|
56
|
+
statements << { type: :vim9script_declaration }
|
57
|
+
advance
|
58
|
+
end
|
59
|
+
|
60
|
+
while @position < @tokens.length
|
61
|
+
stmt = parse_statement
|
62
|
+
statements << stmt if stmt
|
63
|
+
end
|
64
|
+
|
65
|
+
{ type: :program, body: statements }
|
66
|
+
end
|
67
|
+
|
68
|
+
def parse_statement
|
69
|
+
if !current_token
|
70
|
+
return nil
|
71
|
+
end
|
72
|
+
|
73
|
+
if current_token[:type] == :keyword
|
74
|
+
case current_token[:value]
|
75
|
+
when 'if'
|
76
|
+
parse_if_statement
|
77
|
+
when 'while'
|
78
|
+
parse_while_statement
|
79
|
+
when 'for'
|
80
|
+
parse_for_statement
|
81
|
+
when 'def'
|
82
|
+
parse_def_function
|
83
|
+
when 'function'
|
84
|
+
parse_legacy_function
|
85
|
+
when 'return'
|
86
|
+
parse_return_statement
|
87
|
+
when 'var', 'const', 'final'
|
88
|
+
parse_variable_declaration
|
89
|
+
when 'import'
|
90
|
+
parse_import_statement
|
91
|
+
when 'export'
|
92
|
+
parse_export_statement
|
93
|
+
when 'vim9script'
|
94
|
+
token = advance # Skip 'vim9script'
|
95
|
+
{ type: :vim9script_declaration, line: token[:line], column: token[:column] }
|
96
|
+
else
|
97
|
+
@warnings << {
|
98
|
+
message: "Unexpected keyword: #{current_token[:value]}",
|
99
|
+
position: @position,
|
100
|
+
line: current_token[:line],
|
101
|
+
column: current_token[:column]
|
102
|
+
}
|
103
|
+
advance
|
104
|
+
nil
|
105
|
+
end
|
106
|
+
elsif current_token[:type] == :identifier
|
107
|
+
parse_expression_statement
|
108
|
+
elsif current_token[:type] == :comment
|
109
|
+
parse_comment
|
110
|
+
else
|
111
|
+
@warnings << {
|
112
|
+
message: "Unexpected token type: #{current_token[:type]}",
|
113
|
+
position: @position,
|
114
|
+
line: current_token[:line],
|
115
|
+
column: current_token[:column]
|
116
|
+
}
|
117
|
+
advance
|
118
|
+
nil
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def parse_comment
|
123
|
+
comment = current_token[:value]
|
124
|
+
line = current_token[:line]
|
125
|
+
column = current_token[:column]
|
126
|
+
advance
|
127
|
+
{ type: :comment, value: comment, line: line, column: column }
|
128
|
+
end
|
129
|
+
|
130
|
+
def parse_if_statement
|
131
|
+
token = advance # Skip 'if'
|
132
|
+
line = token[:line]
|
133
|
+
column = token[:column]
|
134
|
+
condition = parse_expression
|
135
|
+
|
136
|
+
then_branch = []
|
137
|
+
else_branch = []
|
138
|
+
|
139
|
+
# Parse statements until we hit 'else', 'elseif', or 'endif'
|
140
|
+
while @position < @tokens.length
|
141
|
+
if current_token[:type] == :keyword &&
|
142
|
+
['else', 'elseif', 'endif'].include?(current_token[:value])
|
143
|
+
break
|
144
|
+
end
|
145
|
+
|
146
|
+
stmt = parse_statement
|
147
|
+
then_branch << stmt if stmt
|
148
|
+
end
|
149
|
+
|
150
|
+
# Check for else/elseif
|
151
|
+
if current_token && current_token[:type] == :keyword
|
152
|
+
if current_token[:value] == 'else'
|
153
|
+
advance # Skip 'else'
|
154
|
+
|
155
|
+
# Parse statements until 'endif'
|
156
|
+
while @position < @tokens.length
|
157
|
+
if current_token[:type] == :keyword && current_token[:value] == 'endif'
|
158
|
+
break
|
159
|
+
end
|
160
|
+
|
161
|
+
stmt = parse_statement
|
162
|
+
else_branch << stmt if stmt
|
163
|
+
end
|
164
|
+
elsif current_token[:value] == 'elseif'
|
165
|
+
# This is a simplified handling - elseif should be treated as a nested if
|
166
|
+
else_branch << parse_if_statement
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
# Expect endif
|
171
|
+
expect(:keyword) # This should be 'endif'
|
172
|
+
|
173
|
+
{
|
174
|
+
type: :if_statement,
|
175
|
+
condition: condition,
|
176
|
+
then_branch: then_branch,
|
177
|
+
else_branch: else_branch,
|
178
|
+
line: line,
|
179
|
+
column: column
|
180
|
+
}
|
181
|
+
end
|
182
|
+
|
183
|
+
def parse_def_function
|
184
|
+
token = advance # Skip 'def'
|
185
|
+
line = token[:line]
|
186
|
+
column = token[:column]
|
187
|
+
|
188
|
+
name = expect(:identifier)
|
189
|
+
|
190
|
+
# Parse parameter list
|
191
|
+
expect(:paren_open)
|
192
|
+
params = parse_parameter_list
|
193
|
+
expect(:paren_close)
|
194
|
+
|
195
|
+
# Parse optional return type
|
196
|
+
return_type = nil
|
197
|
+
if current_token && current_token[:type] == :colon
|
198
|
+
advance # Skip ':'
|
199
|
+
return_type = parse_type
|
200
|
+
end
|
201
|
+
|
202
|
+
# Parse function body
|
203
|
+
body = []
|
204
|
+
while @position < @tokens.length
|
205
|
+
if current_token[:type] == :keyword && current_token[:value] == 'enddef'
|
206
|
+
break
|
207
|
+
end
|
208
|
+
|
209
|
+
stmt = parse_statement
|
210
|
+
body << stmt if stmt
|
211
|
+
end
|
212
|
+
|
213
|
+
# Expect enddef
|
214
|
+
expect(:keyword) # This should be 'enddef'
|
215
|
+
|
216
|
+
{
|
217
|
+
type: :def_function,
|
218
|
+
name: name ? name[:value] : nil,
|
219
|
+
params: params,
|
220
|
+
return_type: return_type,
|
221
|
+
body: body,
|
222
|
+
line: line,
|
223
|
+
column: column
|
224
|
+
}
|
225
|
+
end
|
226
|
+
|
227
|
+
|
228
|
+
|
229
|
+
def parse_parameter_list
|
230
|
+
params = []
|
231
|
+
|
232
|
+
# Empty parameter list
|
233
|
+
if current_token && current_token[:type] == :paren_close
|
234
|
+
return params
|
235
|
+
end
|
236
|
+
|
237
|
+
loop do
|
238
|
+
# Check for variable args
|
239
|
+
if current_token && current_token[:type] == :ellipsis
|
240
|
+
ellipsis_token = advance
|
241
|
+
|
242
|
+
# Parse type for variable args if present
|
243
|
+
param_type = nil
|
244
|
+
if current_token && current_token[:type] == :colon
|
245
|
+
advance # Skip ':'
|
246
|
+
param_type = parse_type
|
247
|
+
end
|
248
|
+
|
249
|
+
params << {
|
250
|
+
type: :var_args,
|
251
|
+
param_type: param_type,
|
252
|
+
line: ellipsis_token[:line],
|
253
|
+
column: ellipsis_token[:column]
|
254
|
+
}
|
255
|
+
break
|
256
|
+
end
|
257
|
+
|
258
|
+
# Get parameter name
|
259
|
+
if !current_token || current_token[:type] != :identifier
|
260
|
+
@errors << {
|
261
|
+
message: "Expected parameter name",
|
262
|
+
position: @position,
|
263
|
+
line: current_token ? current_token[:line] : 0,
|
264
|
+
column: current_token ? current_token[:column] : 0
|
265
|
+
}
|
266
|
+
break
|
267
|
+
end
|
268
|
+
|
269
|
+
param_name = advance
|
270
|
+
|
271
|
+
# Check for type annotation
|
272
|
+
param_type = nil
|
273
|
+
if current_token && current_token[:type] == :colon
|
274
|
+
advance # Skip ':'
|
275
|
+
param_type = parse_type
|
276
|
+
end
|
277
|
+
|
278
|
+
# Check for default value
|
279
|
+
default_value = nil
|
280
|
+
if current_token && current_token[:type] == :operator && current_token[:value] == '='
|
281
|
+
advance # Skip '='
|
282
|
+
default_value = parse_expression
|
283
|
+
end
|
284
|
+
|
285
|
+
params << {
|
286
|
+
type: :parameter,
|
287
|
+
name: param_name[:value],
|
288
|
+
param_type: param_type,
|
289
|
+
optional: false, # Set this based on default value
|
290
|
+
default_value: default_value,
|
291
|
+
line: param_name[:line],
|
292
|
+
column: param_name[:column]
|
293
|
+
}
|
294
|
+
|
295
|
+
if current_token && current_token[:type] == :comma
|
296
|
+
advance
|
297
|
+
else
|
298
|
+
break
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
params
|
303
|
+
end
|
304
|
+
|
305
|
+
def parse_type
|
306
|
+
if current_token && current_token[:type] == :identifier
|
307
|
+
type_name = advance
|
308
|
+
|
309
|
+
# Handle generic types like list<string>
|
310
|
+
if current_token && current_token[:type] == :operator && current_token[:value] == '<'
|
311
|
+
advance # Skip '<'
|
312
|
+
inner_type = parse_type
|
313
|
+
expect(:operator) # This should be '>'
|
314
|
+
|
315
|
+
return {
|
316
|
+
type: :generic_type,
|
317
|
+
base_type: type_name[:value],
|
318
|
+
inner_type: inner_type,
|
319
|
+
line: type_name[:line],
|
320
|
+
column: type_name[:column]
|
321
|
+
}
|
322
|
+
end
|
323
|
+
|
324
|
+
return type_name[:value]
|
325
|
+
else
|
326
|
+
@errors << {
|
327
|
+
message: "Expected type identifier",
|
328
|
+
position: @position,
|
329
|
+
line: current_token ? current_token[:line] : 0,
|
330
|
+
column: current_token ? current_token[:column] : 0
|
331
|
+
}
|
332
|
+
advance
|
333
|
+
return "unknown"
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
def parse_variable_declaration
|
338
|
+
var_type_token = advance # Skip 'var', 'const', or 'final'
|
339
|
+
var_type = var_type_token[:value]
|
340
|
+
line = var_type_token[:line]
|
341
|
+
column = var_type_token[:column]
|
342
|
+
|
343
|
+
if !current_token || current_token[:type] != :identifier
|
344
|
+
@errors << {
|
345
|
+
message: "Expected variable name",
|
346
|
+
position: @position,
|
347
|
+
line: current_token ? current_token[:line] : 0,
|
348
|
+
column: current_token ? current_token[:column] : 0
|
349
|
+
}
|
350
|
+
return nil
|
351
|
+
end
|
352
|
+
|
353
|
+
name_token = advance
|
354
|
+
name = name_token[:value]
|
355
|
+
|
356
|
+
# Parse optional type annotation
|
357
|
+
var_type_annotation = nil
|
358
|
+
if current_token && current_token[:type] == :colon
|
359
|
+
advance # Skip ':'
|
360
|
+
var_type_annotation = parse_type
|
361
|
+
end
|
362
|
+
|
363
|
+
# Parse initializer if present
|
364
|
+
initializer = nil
|
365
|
+
if current_token && current_token[:type] == :operator && current_token[:value] == '='
|
366
|
+
advance # Skip '='
|
367
|
+
initializer = parse_expression
|
368
|
+
end
|
369
|
+
|
370
|
+
{
|
371
|
+
type: :variable_declaration,
|
372
|
+
var_type: var_type,
|
373
|
+
name: name,
|
374
|
+
var_type_annotation: var_type_annotation,
|
375
|
+
initializer: initializer,
|
376
|
+
line: line,
|
377
|
+
column: column
|
378
|
+
}
|
379
|
+
end
|
380
|
+
|
381
|
+
def parse_return_statement
|
382
|
+
token = advance # Skip 'return'
|
383
|
+
line = token[:line]
|
384
|
+
column = token[:column]
|
385
|
+
|
386
|
+
value = nil
|
387
|
+
if @position < @tokens.length && current_token[:type] != :semicolon
|
388
|
+
value = parse_expression
|
389
|
+
end
|
390
|
+
|
391
|
+
{
|
392
|
+
type: :return_statement,
|
393
|
+
value: value,
|
394
|
+
line: line,
|
395
|
+
column: column
|
396
|
+
}
|
397
|
+
end
|
398
|
+
|
399
|
+
def parse_expression_statement
|
400
|
+
expr = parse_expression
|
401
|
+
{
|
402
|
+
type: :expression_statement,
|
403
|
+
expression: expr,
|
404
|
+
line: expr ? expr[:line] : 0,
|
405
|
+
column: expr ? expr[:column] : 0
|
406
|
+
}
|
407
|
+
end
|
408
|
+
|
409
|
+
def parse_expression
|
410
|
+
return parse_binary_expression
|
411
|
+
end
|
412
|
+
|
413
|
+
def parse_binary_expression(precedence = 0)
|
414
|
+
left = parse_primary_expression
|
415
|
+
|
416
|
+
while current_token && current_token[:type] == :operator &&
|
417
|
+
operator_precedence(current_token[:value]) >= precedence
|
418
|
+
op_token = advance
|
419
|
+
op = op_token[:value]
|
420
|
+
op_precedence = operator_precedence(op)
|
421
|
+
|
422
|
+
right = parse_binary_expression(op_precedence + 1)
|
423
|
+
|
424
|
+
left = {
|
425
|
+
type: :binary_expression,
|
426
|
+
operator: op,
|
427
|
+
left: left,
|
428
|
+
right: right,
|
429
|
+
line: op_token[:line],
|
430
|
+
column: op_token[:column]
|
431
|
+
}
|
432
|
+
end
|
433
|
+
|
434
|
+
return left
|
435
|
+
end
|
436
|
+
|
437
|
+
def operator_precedence(op)
|
438
|
+
case op
|
439
|
+
when '..' # String concatenation
|
440
|
+
1
|
441
|
+
when '||' # Logical OR
|
442
|
+
2
|
443
|
+
when '&&' # Logical AND
|
444
|
+
3
|
445
|
+
when '==', '!=', '>', '<', '>=', '<=' # Comparison
|
446
|
+
4
|
447
|
+
when '+', '-' # Addition, subtraction
|
448
|
+
5
|
449
|
+
when '*', '/', '%' # Multiplication, division, modulo
|
450
|
+
6
|
451
|
+
else
|
452
|
+
0
|
453
|
+
end
|
454
|
+
end
|
455
|
+
|
456
|
+
def parse_primary_expression
|
457
|
+
return nil unless current_token
|
458
|
+
|
459
|
+
token = current_token
|
460
|
+
line = token[:line]
|
461
|
+
column = token[:column]
|
462
|
+
|
463
|
+
case token[:type]
|
464
|
+
when :number
|
465
|
+
advance
|
466
|
+
{
|
467
|
+
type: :literal,
|
468
|
+
value: token[:value],
|
469
|
+
token_type: :number,
|
470
|
+
line: line,
|
471
|
+
column: column
|
472
|
+
}
|
473
|
+
when :string
|
474
|
+
advance
|
475
|
+
{
|
476
|
+
type: :literal,
|
477
|
+
value: token[:value],
|
478
|
+
token_type: :string,
|
479
|
+
line: line,
|
480
|
+
column: column
|
481
|
+
}
|
482
|
+
when :identifier
|
483
|
+
advance
|
484
|
+
|
485
|
+
# Check if this is a function call
|
486
|
+
if current_token && current_token[:type] == :paren_open
|
487
|
+
return parse_function_call(token[:value], line, column)
|
488
|
+
end
|
489
|
+
|
490
|
+
{
|
491
|
+
type: :identifier,
|
492
|
+
name: token[:value],
|
493
|
+
line: line,
|
494
|
+
column: column
|
495
|
+
}
|
496
|
+
when :paren_open
|
497
|
+
advance # Skip '('
|
498
|
+
expr = parse_expression
|
499
|
+
expect(:paren_close) # Expect and skip ')'
|
500
|
+
return expr
|
501
|
+
else
|
502
|
+
@errors << {
|
503
|
+
message: "Unexpected token in expression: #{token[:type]}",
|
504
|
+
position: @position,
|
505
|
+
line: line,
|
506
|
+
column: column
|
507
|
+
}
|
508
|
+
advance
|
509
|
+
return nil
|
510
|
+
end
|
511
|
+
end
|
512
|
+
|
513
|
+
def parse_function_call(name, line, column)
|
514
|
+
expect(:paren_open)
|
515
|
+
|
516
|
+
args = []
|
517
|
+
|
518
|
+
# Parse arguments
|
519
|
+
unless current_token && current_token[:type] == :paren_close
|
520
|
+
loop do
|
521
|
+
arg = parse_expression
|
522
|
+
args << arg if arg
|
523
|
+
|
524
|
+
break unless current_token && current_token[:type] == :comma
|
525
|
+
advance # Skip comma
|
526
|
+
end
|
527
|
+
end
|
528
|
+
|
529
|
+
expect(:paren_close)
|
530
|
+
|
531
|
+
{
|
532
|
+
type: :function_call,
|
533
|
+
name: name,
|
534
|
+
arguments: args,
|
535
|
+
line: line,
|
536
|
+
column: column
|
537
|
+
}
|
538
|
+
end
|
539
|
+
|
540
|
+
def parse_import_statement
|
541
|
+
token = advance # Skip 'import'
|
542
|
+
line = token[:line]
|
543
|
+
column = token[:column]
|
544
|
+
|
545
|
+
# Handle 'import autoload'
|
546
|
+
is_autoload = false
|
547
|
+
module_name = nil
|
548
|
+
path = nil
|
549
|
+
|
550
|
+
if current_token && current_token[:type] == :identifier && current_token[:value] == 'autoload'
|
551
|
+
is_autoload = true
|
552
|
+
module_name = advance[:value] # Store "autoload" as the module name
|
553
|
+
|
554
|
+
# After "autoload" keyword, expect a string path
|
555
|
+
if current_token && current_token[:type] == :string
|
556
|
+
path = current_token[:value]
|
557
|
+
advance
|
558
|
+
else
|
559
|
+
@errors << {
|
560
|
+
message: "Expected string path after 'autoload'",
|
561
|
+
position: @position,
|
562
|
+
line: current_token ? current_token[:line] : 0,
|
563
|
+
column: current_token ? current_token[:column] : 0
|
564
|
+
}
|
565
|
+
end
|
566
|
+
else
|
567
|
+
# Regular import with a string path
|
568
|
+
if current_token && current_token[:type] == :string
|
569
|
+
path = current_token[:value]
|
570
|
+
|
571
|
+
# Extract module name from the path
|
572
|
+
# This is simplified logic - you might need more complex extraction
|
573
|
+
module_name = path.gsub(/['"]/, '').split('/').last.split('.').first
|
574
|
+
|
575
|
+
advance
|
576
|
+
else
|
577
|
+
# Handle other import formats
|
578
|
+
module_expr = parse_expression()
|
579
|
+
if module_expr && module_expr[:type] == :literal && module_expr[:token_type] == :string
|
580
|
+
path = module_expr[:value]
|
581
|
+
module_name = path.gsub(/['"]/, '').split('/').last.split('.').first
|
582
|
+
end
|
583
|
+
end
|
584
|
+
end
|
585
|
+
|
586
|
+
# Handle 'as name'
|
587
|
+
as_name = nil
|
588
|
+
if current_token && current_token[:type] == :identifier && current_token[:value] == 'as'
|
589
|
+
advance # Skip 'as'
|
590
|
+
if current_token && current_token[:type] == :identifier
|
591
|
+
as_name = advance[:value]
|
592
|
+
else
|
593
|
+
@errors << {
|
594
|
+
message: "Expected identifier after 'as'",
|
595
|
+
position: @position,
|
596
|
+
line: current_token ? current_token[:line] : 0,
|
597
|
+
column: current_token ? current_token[:column] : 0
|
598
|
+
}
|
599
|
+
end
|
600
|
+
end
|
601
|
+
|
602
|
+
{
|
603
|
+
type: :import_statement,
|
604
|
+
module: module_name,
|
605
|
+
path: path,
|
606
|
+
is_autoload: is_autoload,
|
607
|
+
as_name: as_name,
|
608
|
+
line: line,
|
609
|
+
column: column
|
610
|
+
}
|
611
|
+
end
|
612
|
+
def parse_export_statement
|
613
|
+
token = advance # Skip 'export'
|
614
|
+
line = token[:line]
|
615
|
+
column = token[:column]
|
616
|
+
|
617
|
+
# Export can be followed by var/const/def/function declarations
|
618
|
+
if !current_token
|
619
|
+
@errors << {
|
620
|
+
message: "Expected declaration after export",
|
621
|
+
position: @position,
|
622
|
+
line: line,
|
623
|
+
column: column
|
624
|
+
}
|
625
|
+
return nil
|
626
|
+
end
|
627
|
+
|
628
|
+
exported_item = nil
|
629
|
+
|
630
|
+
if current_token[:type] == :keyword
|
631
|
+
case current_token[:value]
|
632
|
+
when 'def'
|
633
|
+
exported_item = parse_def_function
|
634
|
+
when 'function'
|
635
|
+
exported_item = parse_legacy_function
|
636
|
+
when 'var', 'const', 'final'
|
637
|
+
exported_item = parse_variable_declaration
|
638
|
+
when 'class'
|
639
|
+
# Handle class export when implemented
|
640
|
+
@errors << {
|
641
|
+
message: "Class export not implemented yet",
|
642
|
+
position: @position,
|
643
|
+
line: current_token[:line],
|
644
|
+
column: current_token[:column]
|
645
|
+
}
|
646
|
+
advance
|
647
|
+
return nil
|
648
|
+
else
|
649
|
+
@errors << {
|
650
|
+
message: "Unexpected keyword after export: #{current_token[:value]}",
|
651
|
+
position: @position,
|
652
|
+
line: current_token[:line],
|
653
|
+
column: current_token[:column]
|
654
|
+
}
|
655
|
+
advance
|
656
|
+
return nil
|
657
|
+
end
|
658
|
+
else
|
659
|
+
@errors << {
|
660
|
+
message: "Expected declaration after export",
|
661
|
+
position: @position,
|
662
|
+
line: current_token[:line],
|
663
|
+
column: current_token[:column]
|
664
|
+
}
|
665
|
+
advance
|
666
|
+
return nil
|
667
|
+
end
|
668
|
+
|
669
|
+
{
|
670
|
+
type: :export_statement,
|
671
|
+
export: exported_item,
|
672
|
+
line: line,
|
673
|
+
column: column
|
674
|
+
}
|
675
|
+
end
|
676
|
+
|
677
|
+
def parse_legacy_function
|
678
|
+
token = advance # Skip 'function'
|
679
|
+
line = token[:line]
|
680
|
+
column = token[:column]
|
681
|
+
|
682
|
+
# Check for bang (!) in function definition
|
683
|
+
has_bang = false
|
684
|
+
if current_token && current_token[:type] == :operator && current_token[:value] == '!'
|
685
|
+
has_bang = true
|
686
|
+
advance # Skip '!'
|
687
|
+
end
|
688
|
+
|
689
|
+
name = expect(:identifier)
|
690
|
+
|
691
|
+
# Parse parameter list
|
692
|
+
expect(:paren_open)
|
693
|
+
params = parse_parameter_list_legacy
|
694
|
+
expect(:paren_close)
|
695
|
+
|
696
|
+
# Check for optional attributes (range, dict, abort, closure)
|
697
|
+
attributes = []
|
698
|
+
while current_token && current_token[:type] == :identifier
|
699
|
+
if ['range', 'dict', 'abort', 'closure'].include?(current_token[:value])
|
700
|
+
attributes << advance[:value]
|
701
|
+
else
|
702
|
+
break
|
703
|
+
end
|
704
|
+
end
|
705
|
+
|
706
|
+
# Parse function body
|
707
|
+
body = []
|
708
|
+
while @position < @tokens.length
|
709
|
+
if current_token && current_token[:type] == :keyword &&
|
710
|
+
['endfunction', 'endfunc'].include?(current_token[:value])
|
711
|
+
break
|
712
|
+
end
|
713
|
+
|
714
|
+
stmt = parse_statement
|
715
|
+
body << stmt if stmt
|
716
|
+
end
|
717
|
+
|
718
|
+
# Expect endfunction/endfunc
|
719
|
+
end_token = advance # This should be 'endfunction' or 'endfunc'
|
720
|
+
if end_token && end_token[:type] != :keyword &&
|
721
|
+
!['endfunction', 'endfunc'].include?(end_token[:value])
|
722
|
+
@errors << {
|
723
|
+
message: "Expected 'endfunction' or 'endfunc'",
|
724
|
+
position: @position,
|
725
|
+
line: end_token ? end_token[:line] : 0,
|
726
|
+
column: end_token ? end_token[:column] : 0
|
727
|
+
}
|
728
|
+
end
|
729
|
+
|
730
|
+
{
|
731
|
+
type: :legacy_function,
|
732
|
+
name: name ? name[:value] : nil,
|
733
|
+
params: params,
|
734
|
+
has_bang: has_bang,
|
735
|
+
attributes: attributes,
|
736
|
+
body: body,
|
737
|
+
line: line,
|
738
|
+
column: column
|
739
|
+
}
|
740
|
+
end
|
741
|
+
|
742
|
+
# Legacy function parameters are different - they use a:name syntax
|
743
|
+
def parse_parameter_list_legacy
|
744
|
+
params = []
|
745
|
+
|
746
|
+
# Empty parameter list
|
747
|
+
if current_token && current_token[:type] == :paren_close
|
748
|
+
return params
|
749
|
+
end
|
750
|
+
|
751
|
+
loop do
|
752
|
+
# Check for ... (varargs) in legacy function
|
753
|
+
if current_token && current_token[:type] == :ellipsis
|
754
|
+
advance
|
755
|
+
params << { type: :var_args_legacy, name: '...' }
|
756
|
+
break
|
757
|
+
end
|
758
|
+
|
759
|
+
if !current_token || current_token[:type] != :identifier
|
760
|
+
@errors << {
|
761
|
+
message: "Expected parameter name",
|
762
|
+
position: @position,
|
763
|
+
line: current_token ? current_token[:line] : 0,
|
764
|
+
column: current_token ? current_token[:column] : 0
|
765
|
+
}
|
766
|
+
break
|
767
|
+
end
|
768
|
+
|
769
|
+
param_name = advance
|
770
|
+
|
771
|
+
params << {
|
772
|
+
type: :parameter,
|
773
|
+
name: param_name[:value],
|
774
|
+
line: param_name[:line],
|
775
|
+
column: param_name[:column]
|
776
|
+
}
|
777
|
+
|
778
|
+
if current_token && current_token[:type] == :comma
|
779
|
+
advance
|
780
|
+
else
|
781
|
+
break
|
782
|
+
end
|
783
|
+
end
|
784
|
+
|
785
|
+
params
|
786
|
+
end
|
787
|
+
|
788
|
+
end
|
789
|
+
end
|
data/lib/vinter.rb
ADDED
metadata
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: vinter
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Dan Bradbury
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2025-04-04 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: A linter for the Vim9 script language, helping to identify issues and
|
14
|
+
enforce best practices
|
15
|
+
email: dan.luckydaisy@gmail.com
|
16
|
+
executables:
|
17
|
+
- vinter
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files: []
|
20
|
+
files:
|
21
|
+
- README.md
|
22
|
+
- bin/vinter
|
23
|
+
- lib/vinter.rb
|
24
|
+
- lib/vinter/cli.rb
|
25
|
+
- lib/vinter/lexer.rb
|
26
|
+
- lib/vinter/linter.rb
|
27
|
+
- lib/vinter/parser.rb
|
28
|
+
homepage: https://github.com/DanBradbury/vinter
|
29
|
+
licenses:
|
30
|
+
- MIT
|
31
|
+
metadata: {}
|
32
|
+
post_install_message:
|
33
|
+
rdoc_options: []
|
34
|
+
require_paths:
|
35
|
+
- lib
|
36
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 2.5.0
|
41
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
42
|
+
requirements:
|
43
|
+
- - ">="
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
requirements: []
|
47
|
+
rubygems_version: 3.5.22
|
48
|
+
signing_key:
|
49
|
+
specification_version: 4
|
50
|
+
summary: A linter for Vim9 script
|
51
|
+
test_files: []
|