t-ruby 0.0.42 → 0.0.46
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 +12 -2
- data/bin/t-ruby +6 -0
- data/lib/t_ruby/ast_type_inferrer.rb +2 -0
- data/lib/t_ruby/benchmark.rb +1 -0
- data/lib/t_ruby/bundler_integration.rb +1 -0
- data/lib/t_ruby/cache.rb +40 -10
- data/lib/t_ruby/cli.rb +30 -8
- data/lib/t_ruby/compiler.rb +168 -0
- data/lib/t_ruby/diagnostic.rb +115 -0
- data/lib/t_ruby/diagnostic_formatter.rb +162 -0
- data/lib/t_ruby/doc_generator.rb +1 -0
- data/lib/t_ruby/docs_badge_generator.rb +1 -0
- data/lib/t_ruby/error_handler.rb +201 -35
- data/lib/t_ruby/error_reporter.rb +57 -0
- data/lib/t_ruby/ir.rb +53 -69
- data/lib/t_ruby/lsp_server.rb +40 -97
- data/lib/t_ruby/package_manager.rb +1 -0
- data/lib/t_ruby/parser.rb +18 -4
- data/lib/t_ruby/parser_combinator/combinators/alternative.rb +20 -0
- data/lib/t_ruby/parser_combinator/combinators/chain_left.rb +34 -0
- data/lib/t_ruby/parser_combinator/combinators/choice.rb +29 -0
- data/lib/t_ruby/parser_combinator/combinators/flat_map.rb +21 -0
- data/lib/t_ruby/parser_combinator/combinators/label.rb +22 -0
- data/lib/t_ruby/parser_combinator/combinators/lookahead.rb +21 -0
- data/lib/t_ruby/parser_combinator/combinators/many.rb +29 -0
- data/lib/t_ruby/parser_combinator/combinators/many1.rb +32 -0
- data/lib/t_ruby/parser_combinator/combinators/map.rb +17 -0
- data/lib/t_ruby/parser_combinator/combinators/not_followed_by.rb +21 -0
- data/lib/t_ruby/parser_combinator/combinators/optional.rb +21 -0
- data/lib/t_ruby/parser_combinator/combinators/sep_by.rb +34 -0
- data/lib/t_ruby/parser_combinator/combinators/sep_by1.rb +34 -0
- data/lib/t_ruby/parser_combinator/combinators/sequence.rb +23 -0
- data/lib/t_ruby/parser_combinator/combinators/skip_right.rb +23 -0
- data/lib/t_ruby/parser_combinator/declaration_parser.rb +147 -0
- data/lib/t_ruby/parser_combinator/dsl.rb +115 -0
- data/lib/t_ruby/parser_combinator/parse_error.rb +48 -0
- data/lib/t_ruby/parser_combinator/parse_result.rb +46 -0
- data/lib/t_ruby/parser_combinator/parser.rb +84 -0
- data/lib/t_ruby/parser_combinator/primitives/end_of_input.rb +16 -0
- data/lib/t_ruby/parser_combinator/primitives/fail.rb +16 -0
- data/lib/t_ruby/parser_combinator/primitives/lazy.rb +18 -0
- data/lib/t_ruby/parser_combinator/primitives/literal.rb +21 -0
- data/lib/t_ruby/parser_combinator/primitives/pure.rb +16 -0
- data/lib/t_ruby/parser_combinator/primitives/regex.rb +25 -0
- data/lib/t_ruby/parser_combinator/primitives/satisfy.rb +21 -0
- data/lib/t_ruby/parser_combinator/token/expression_parser.rb +541 -0
- data/lib/t_ruby/parser_combinator/token/statement_parser.rb +644 -0
- data/lib/t_ruby/parser_combinator/token/token_alternative.rb +20 -0
- data/lib/t_ruby/parser_combinator/token/token_body_parser.rb +54 -0
- data/lib/t_ruby/parser_combinator/token/token_declaration_parser.rb +962 -0
- data/lib/t_ruby/parser_combinator/token/token_dsl.rb +16 -0
- data/lib/t_ruby/parser_combinator/token/token_label.rb +22 -0
- data/lib/t_ruby/parser_combinator/token/token_many.rb +29 -0
- data/lib/t_ruby/parser_combinator/token/token_many1.rb +32 -0
- data/lib/t_ruby/parser_combinator/token/token_map.rb +17 -0
- data/lib/t_ruby/parser_combinator/token/token_matcher.rb +29 -0
- data/lib/t_ruby/parser_combinator/token/token_optional.rb +21 -0
- data/lib/t_ruby/parser_combinator/token/token_parse_result.rb +40 -0
- data/lib/t_ruby/parser_combinator/token/token_parser.rb +62 -0
- data/lib/t_ruby/parser_combinator/token/token_sep_by.rb +34 -0
- data/lib/t_ruby/parser_combinator/token/token_sep_by1.rb +34 -0
- data/lib/t_ruby/parser_combinator/token/token_sequence.rb +23 -0
- data/lib/t_ruby/parser_combinator/token/token_skip_right.rb +23 -0
- data/lib/t_ruby/parser_combinator/type_parser.rb +118 -0
- data/lib/t_ruby/parser_combinator.rb +64 -936
- data/lib/t_ruby/runner.rb +132 -0
- data/lib/t_ruby/scanner.rb +883 -0
- data/lib/t_ruby/version.rb +1 -1
- data/lib/t_ruby/watcher.rb +67 -75
- data/lib/t_ruby.rb +16 -1
- metadata +73 -4
- data/lib/t_ruby/body_parser.rb +0 -561
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 509e1090df12fc29ea3109541a6e59133bf7516b31e4859bda8a6830d44f6999
|
|
4
|
+
data.tar.gz: 8dd1ef0eb0eeea4faccfd13372235d509997b65b9e556299542e880e6bb6ab85
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d97698f9bf39ac32ef8635e5f6a0e8ca1819490c5907bc3997563b6082c88d13bd5ad29e3c6a88aba808fcaf1802a15a43b0a4a0faea67b02f9cd6d2dec526c0
|
|
7
|
+
data.tar.gz: 5de7ceb419cf6667ff43c1662676d3222ef6c589fb9b9831c8e7e695dd7b4e5b07e3e70039818018ff8997328423b96e63f40a588a005985eda35b836ff52892
|
data/README.md
CHANGED
|
@@ -243,7 +243,7 @@ watch:
|
|
|
243
243
|
|
|
244
244
|
- **Type annotations** — Parameter and return types, erased at compile time
|
|
245
245
|
- **Union types** — `String | Integer | nil`
|
|
246
|
-
- **Generics** — `Array<User>`, `Hash<String, Integer>`
|
|
246
|
+
- **Generics** — `User[]`, `Array<User>`, `Hash<String, Integer>`
|
|
247
247
|
- **Interfaces** — Define contracts between objects
|
|
248
248
|
- **Type aliases** — `type UserID = Integer`
|
|
249
249
|
- **RBS generation** — Works with Steep, Ruby LSP, Sorbet
|
|
@@ -284,6 +284,16 @@ See [ROADMAP.md](./ROADMAP.md) for details.
|
|
|
284
284
|
|
|
285
285
|
Contributions are welcome! Please feel free to submit issues and pull requests.
|
|
286
286
|
|
|
287
|
+
## ❤️ Support T-Ruby
|
|
288
|
+
|
|
289
|
+
If you find T-Ruby useful, consider sponsoring the project to support ongoing development and experimentation.
|
|
290
|
+
|
|
291
|
+
<p align="center">
|
|
292
|
+
<a href="https://github.com/sponsors/type-ruby">
|
|
293
|
+
<img src="https://img.shields.io/badge/Sponsor-❤️-ea4aaa" alt="Sponsor" />
|
|
294
|
+
</a>
|
|
295
|
+
</p>
|
|
296
|
+
|
|
287
297
|
## License
|
|
288
298
|
|
|
289
|
-
[
|
|
299
|
+
[BSD 2-Clause](./LICENSE)
|
data/bin/t-ruby
ADDED
data/lib/t_ruby/benchmark.rb
CHANGED
data/lib/t_ruby/cache.rb
CHANGED
|
@@ -318,7 +318,7 @@ module TRuby
|
|
|
318
318
|
def needs_compile?(file_path)
|
|
319
319
|
return true unless File.exist?(file_path)
|
|
320
320
|
|
|
321
|
-
current_hash =
|
|
321
|
+
current_hash = compute_file_hash(file_path)
|
|
322
322
|
stored_hash = @file_hashes[file_path]
|
|
323
323
|
|
|
324
324
|
return true if stored_hash.nil? || stored_hash != current_hash
|
|
@@ -333,7 +333,7 @@ module TRuby
|
|
|
333
333
|
return @compiled_files[file_path] unless needs_compile?(file_path)
|
|
334
334
|
|
|
335
335
|
result = @compiler.compile(file_path)
|
|
336
|
-
@file_hashes[file_path] =
|
|
336
|
+
@file_hashes[file_path] = compute_file_hash(file_path)
|
|
337
337
|
@compiled_files[file_path] = result
|
|
338
338
|
|
|
339
339
|
result
|
|
@@ -365,9 +365,14 @@ module TRuby
|
|
|
365
365
|
@cache.stats # Just accessing for potential cleanup
|
|
366
366
|
end
|
|
367
367
|
|
|
368
|
+
# Update file hash after external compile (for watcher integration)
|
|
369
|
+
def update_file_hash(file_path)
|
|
370
|
+
@file_hashes[file_path] = compute_file_hash(file_path)
|
|
371
|
+
end
|
|
372
|
+
|
|
368
373
|
private
|
|
369
374
|
|
|
370
|
-
def
|
|
375
|
+
def compute_file_hash(file_path)
|
|
371
376
|
return nil unless File.exist?(file_path)
|
|
372
377
|
|
|
373
378
|
Digest::SHA256.hexdigest(File.read(file_path))
|
|
@@ -683,27 +688,52 @@ module TRuby
|
|
|
683
688
|
end
|
|
684
689
|
|
|
685
690
|
# Compile all with cross-file checking
|
|
691
|
+
# Returns diagnostics using unified Diagnostic format
|
|
686
692
|
def compile_all_with_checking(file_paths)
|
|
687
693
|
results = {}
|
|
688
|
-
|
|
694
|
+
all_diagnostics = []
|
|
689
695
|
|
|
690
696
|
# First pass: compile and register all files
|
|
691
697
|
file_paths.each do |file_path|
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
698
|
+
source = File.exist?(file_path) ? File.read(file_path) : nil
|
|
699
|
+
|
|
700
|
+
begin
|
|
701
|
+
results[file_path] = compile_with_ir(file_path)
|
|
702
|
+
rescue TypeCheckError => e
|
|
703
|
+
all_diagnostics << Diagnostic.from_type_check_error(e, file: file_path, source: source)
|
|
704
|
+
rescue ParseError => e
|
|
705
|
+
all_diagnostics << Diagnostic.from_parse_error(e, file: file_path, source: source)
|
|
706
|
+
rescue Scanner::ScanError => e
|
|
707
|
+
all_diagnostics << Diagnostic.from_scan_error(e, file: file_path, source: source)
|
|
708
|
+
rescue StandardError => e
|
|
709
|
+
all_diagnostics << Diagnostic.new(
|
|
710
|
+
code: "TR0001",
|
|
711
|
+
message: e.message,
|
|
712
|
+
file: file_path,
|
|
713
|
+
line: 1,
|
|
714
|
+
column: 1
|
|
715
|
+
)
|
|
716
|
+
end
|
|
695
717
|
end
|
|
696
718
|
|
|
697
719
|
# Second pass: cross-file type checking
|
|
698
720
|
if @cross_file_checker
|
|
699
721
|
check_result = @cross_file_checker.check_all
|
|
700
|
-
|
|
722
|
+
check_result[:errors].each do |e|
|
|
723
|
+
all_diagnostics << Diagnostic.new(
|
|
724
|
+
code: "TR2002",
|
|
725
|
+
message: e[:message],
|
|
726
|
+
file: e[:file],
|
|
727
|
+
line: 1,
|
|
728
|
+
column: 1
|
|
729
|
+
)
|
|
730
|
+
end
|
|
701
731
|
end
|
|
702
732
|
|
|
703
733
|
{
|
|
704
734
|
results: results,
|
|
705
|
-
|
|
706
|
-
success:
|
|
735
|
+
diagnostics: all_diagnostics,
|
|
736
|
+
success: all_diagnostics.empty?,
|
|
707
737
|
}
|
|
708
738
|
end
|
|
709
739
|
|
data/lib/t_ruby/cli.rb
CHANGED
|
@@ -13,6 +13,7 @@ module TRuby
|
|
|
13
13
|
trc --watch, -w Watch input files and recompile on change
|
|
14
14
|
trc --decl <file.trb> Generate .d.trb declaration file
|
|
15
15
|
trc --lsp Start LSP server (for IDE integration)
|
|
16
|
+
trc run <file.trb> Run a .trb file directly (delegates to t-ruby)
|
|
16
17
|
trc update Update t-ruby to the latest version
|
|
17
18
|
trc --version, -v Show version (and check for updates)
|
|
18
19
|
trc --help, -h Show this help
|
|
@@ -27,6 +28,7 @@ module TRuby
|
|
|
27
28
|
trc --watch hello.trb Watch specific file for changes
|
|
28
29
|
trc --decl hello.trb Generate hello.d.trb declaration file
|
|
29
30
|
trc --lsp Start language server for VS Code
|
|
31
|
+
trc run hello.trb Run hello.trb directly without compilation
|
|
30
32
|
HELP
|
|
31
33
|
|
|
32
34
|
def self.run(args)
|
|
@@ -64,6 +66,11 @@ module TRuby
|
|
|
64
66
|
return
|
|
65
67
|
end
|
|
66
68
|
|
|
69
|
+
if @args.first == "run"
|
|
70
|
+
run_direct
|
|
71
|
+
return
|
|
72
|
+
end
|
|
73
|
+
|
|
67
74
|
if @args.include?("--watch") || @args.include?("-w")
|
|
68
75
|
start_watch_mode
|
|
69
76
|
return
|
|
@@ -186,6 +193,16 @@ module TRuby
|
|
|
186
193
|
server.run
|
|
187
194
|
end
|
|
188
195
|
|
|
196
|
+
def run_direct
|
|
197
|
+
remaining_args = @args[1..] || []
|
|
198
|
+
|
|
199
|
+
# Find t-ruby executable path
|
|
200
|
+
t_ruby_bin = File.expand_path("../../bin/t-ruby", __dir__)
|
|
201
|
+
|
|
202
|
+
# Execute t-ruby (replaces current process)
|
|
203
|
+
exec(t_ruby_bin, *remaining_args)
|
|
204
|
+
end
|
|
205
|
+
|
|
189
206
|
def start_watch_mode
|
|
190
207
|
# Get paths to watch (everything after --watch or -w flag)
|
|
191
208
|
watch_index = @args.index("--watch") || @args.index("-w")
|
|
@@ -214,14 +231,19 @@ module TRuby
|
|
|
214
231
|
config = Config.new(config_path)
|
|
215
232
|
compiler = Compiler.new(config)
|
|
216
233
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
234
|
+
result = compiler.compile_with_diagnostics(input_file)
|
|
235
|
+
|
|
236
|
+
if result[:success]
|
|
237
|
+
puts "Compiled: #{input_file} -> #{result[:output_path]}"
|
|
238
|
+
else
|
|
239
|
+
formatter = DiagnosticFormatter.new(use_colors: $stdout.tty?)
|
|
240
|
+
result[:diagnostics].each do |diagnostic|
|
|
241
|
+
puts formatter.format(diagnostic)
|
|
242
|
+
end
|
|
243
|
+
puts
|
|
244
|
+
puts formatter.send(:format_summary, result[:diagnostics])
|
|
245
|
+
exit 1
|
|
246
|
+
end
|
|
225
247
|
end
|
|
226
248
|
|
|
227
249
|
# Extract config path from --config or -c flag
|
data/lib/t_ruby/compiler.rb
CHANGED
|
@@ -76,6 +76,161 @@ module TRuby
|
|
|
76
76
|
output_path
|
|
77
77
|
end
|
|
78
78
|
|
|
79
|
+
# Compile a file and return result with diagnostics
|
|
80
|
+
# This is the unified compilation interface for CLI and Watcher
|
|
81
|
+
# @param input_path [String] Path to the input file
|
|
82
|
+
# @return [Hash] Result with :success, :output_path, :diagnostics keys
|
|
83
|
+
def compile_with_diagnostics(input_path)
|
|
84
|
+
source = File.exist?(input_path) ? File.read(input_path) : nil
|
|
85
|
+
all_diagnostics = []
|
|
86
|
+
|
|
87
|
+
# Run analyze first to get all diagnostics (colon spacing, etc.)
|
|
88
|
+
if source
|
|
89
|
+
all_diagnostics = analyze(source, file: input_path)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
begin
|
|
93
|
+
output_path = compile(input_path)
|
|
94
|
+
# Compilation succeeded, but we may still have diagnostics from analyze
|
|
95
|
+
{
|
|
96
|
+
success: all_diagnostics.empty?,
|
|
97
|
+
output_path: all_diagnostics.empty? ? output_path : nil,
|
|
98
|
+
diagnostics: all_diagnostics,
|
|
99
|
+
}
|
|
100
|
+
rescue TypeCheckError => e
|
|
101
|
+
# Skip if already reported by analyze (same message and location)
|
|
102
|
+
new_diag = Diagnostic.from_type_check_error(e, file: input_path, source: source)
|
|
103
|
+
unless all_diagnostics.any? { |d| d.message == new_diag.message && d.line == new_diag.line }
|
|
104
|
+
all_diagnostics << new_diag
|
|
105
|
+
end
|
|
106
|
+
{
|
|
107
|
+
success: false,
|
|
108
|
+
output_path: nil,
|
|
109
|
+
diagnostics: all_diagnostics,
|
|
110
|
+
}
|
|
111
|
+
rescue ParseError => e
|
|
112
|
+
new_diag = Diagnostic.from_parse_error(e, file: input_path, source: source)
|
|
113
|
+
unless all_diagnostics.any? { |d| d.message == new_diag.message && d.line == new_diag.line }
|
|
114
|
+
all_diagnostics << new_diag
|
|
115
|
+
end
|
|
116
|
+
{
|
|
117
|
+
success: false,
|
|
118
|
+
output_path: nil,
|
|
119
|
+
diagnostics: all_diagnostics,
|
|
120
|
+
}
|
|
121
|
+
rescue Scanner::ScanError => e
|
|
122
|
+
new_diag = Diagnostic.from_scan_error(e, file: input_path, source: source)
|
|
123
|
+
unless all_diagnostics.any? { |d| d.message == new_diag.message && d.line == new_diag.line }
|
|
124
|
+
all_diagnostics << new_diag
|
|
125
|
+
end
|
|
126
|
+
{
|
|
127
|
+
success: false,
|
|
128
|
+
output_path: nil,
|
|
129
|
+
diagnostics: all_diagnostics,
|
|
130
|
+
}
|
|
131
|
+
rescue ArgumentError => e
|
|
132
|
+
all_diagnostics << Diagnostic.new(
|
|
133
|
+
code: "TR0001",
|
|
134
|
+
message: e.message,
|
|
135
|
+
file: input_path,
|
|
136
|
+
severity: Diagnostic::SEVERITY_ERROR
|
|
137
|
+
)
|
|
138
|
+
{
|
|
139
|
+
success: false,
|
|
140
|
+
output_path: nil,
|
|
141
|
+
diagnostics: all_diagnostics,
|
|
142
|
+
}
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Analyze source code without compiling - returns diagnostics only
|
|
147
|
+
# This is the unified analysis interface for LSP and other tools
|
|
148
|
+
# @param source [String] T-Ruby source code
|
|
149
|
+
# @param file [String] File path for error reporting (optional)
|
|
150
|
+
# @return [Array<Diagnostic>] Array of diagnostic objects
|
|
151
|
+
def analyze(source, file: "<source>")
|
|
152
|
+
diagnostics = []
|
|
153
|
+
source_lines = source.split("\n")
|
|
154
|
+
|
|
155
|
+
# Run ErrorHandler checks (syntax validation, duplicate definitions, etc.)
|
|
156
|
+
error_handler = ErrorHandler.new(source)
|
|
157
|
+
errors = error_handler.check
|
|
158
|
+
errors.each do |error|
|
|
159
|
+
# Parse line number from "Line N: message" format
|
|
160
|
+
next unless error =~ /^Line (\d+):\s*(.+)$/
|
|
161
|
+
|
|
162
|
+
line_num = Regexp.last_match(1).to_i
|
|
163
|
+
message = Regexp.last_match(2)
|
|
164
|
+
source_line = source_lines[line_num - 1] if line_num.positive?
|
|
165
|
+
diagnostics << Diagnostic.new(
|
|
166
|
+
code: "TR1002",
|
|
167
|
+
message: message,
|
|
168
|
+
file: file,
|
|
169
|
+
line: line_num,
|
|
170
|
+
column: 1,
|
|
171
|
+
source_line: source_line,
|
|
172
|
+
severity: Diagnostic::SEVERITY_ERROR
|
|
173
|
+
)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Run TokenDeclarationParser for colon spacing and declaration syntax validation
|
|
177
|
+
begin
|
|
178
|
+
scanner = Scanner.new(source)
|
|
179
|
+
tokens = scanner.scan_all
|
|
180
|
+
decl_parser = ParserCombinator::TokenDeclarationParser.new
|
|
181
|
+
decl_parser.parse_program(tokens)
|
|
182
|
+
|
|
183
|
+
if decl_parser.has_errors?
|
|
184
|
+
decl_parser.errors.each do |err|
|
|
185
|
+
source_line = source_lines[err.line - 1] if err.line.positive? && err.line <= source_lines.length
|
|
186
|
+
diagnostics << Diagnostic.new(
|
|
187
|
+
code: "TR1003",
|
|
188
|
+
message: err.message,
|
|
189
|
+
file: file,
|
|
190
|
+
line: err.line,
|
|
191
|
+
column: err.column,
|
|
192
|
+
source_line: source_line,
|
|
193
|
+
severity: Diagnostic::SEVERITY_ERROR
|
|
194
|
+
)
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
rescue Scanner::ScanError
|
|
198
|
+
# Scanner errors will be caught below in the main parse section
|
|
199
|
+
rescue StandardError
|
|
200
|
+
# Ignore TokenDeclarationParser errors for now - regex parser is authoritative
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
begin
|
|
204
|
+
# Parse source with regex-based parser for IR generation
|
|
205
|
+
parser = Parser.new(source)
|
|
206
|
+
parser.parse
|
|
207
|
+
|
|
208
|
+
# Run type checking if enabled and IR is available
|
|
209
|
+
if type_check? && parser.ir_program
|
|
210
|
+
begin
|
|
211
|
+
check_types(parser.ir_program, file)
|
|
212
|
+
rescue TypeCheckError => e
|
|
213
|
+
diagnostics << Diagnostic.from_type_check_error(e, file: file, source: source)
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
rescue ParseError => e
|
|
217
|
+
diagnostics << Diagnostic.from_parse_error(e, file: file, source: source)
|
|
218
|
+
rescue Scanner::ScanError => e
|
|
219
|
+
diagnostics << Diagnostic.from_scan_error(e, file: file, source: source)
|
|
220
|
+
rescue StandardError => e
|
|
221
|
+
diagnostics << Diagnostic.new(
|
|
222
|
+
code: "TR0001",
|
|
223
|
+
message: e.message,
|
|
224
|
+
file: file,
|
|
225
|
+
line: 1,
|
|
226
|
+
column: 1,
|
|
227
|
+
severity: Diagnostic::SEVERITY_ERROR
|
|
228
|
+
)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
diagnostics
|
|
232
|
+
end
|
|
233
|
+
|
|
79
234
|
# Compile T-Ruby source code from a string (useful for WASM/playground)
|
|
80
235
|
# @param source [String] T-Ruby source code
|
|
81
236
|
# @param options [Hash] Options for compilation
|
|
@@ -319,6 +474,19 @@ module TRuby
|
|
|
319
474
|
# Subtype relationships
|
|
320
475
|
return true if subtype_of?(inferred, declared)
|
|
321
476
|
|
|
477
|
+
# Handle generic types (e.g., Array[untyped] is compatible with Array[String])
|
|
478
|
+
if inferred.include?("[") && declared.include?("[")
|
|
479
|
+
inferred_base = inferred.split("[").first
|
|
480
|
+
declared_base = declared.split("[").first
|
|
481
|
+
if inferred_base == declared_base
|
|
482
|
+
# Extract type arguments
|
|
483
|
+
inferred_args = inferred[/\[(.+)\]/, 1]
|
|
484
|
+
declared_args = declared[/\[(.+)\]/, 1]
|
|
485
|
+
# untyped type argument is compatible with any type argument
|
|
486
|
+
return true if inferred_args == "untyped" || declared_args == "untyped"
|
|
487
|
+
end
|
|
488
|
+
end
|
|
489
|
+
|
|
322
490
|
# Handle union types in declared
|
|
323
491
|
if declared.include?("|")
|
|
324
492
|
declared_types = declared.split("|").map(&:strip)
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TRuby
|
|
4
|
+
class Diagnostic
|
|
5
|
+
SEVERITY_ERROR = :error
|
|
6
|
+
SEVERITY_WARNING = :warning
|
|
7
|
+
SEVERITY_INFO = :info
|
|
8
|
+
SEVERITY_HINT = :hint
|
|
9
|
+
|
|
10
|
+
attr_reader :code, :message, :file, :line, :column, :end_column,
|
|
11
|
+
:severity, :expected, :actual, :suggestion, :source_line
|
|
12
|
+
|
|
13
|
+
# rubocop:disable Metrics/ParameterLists
|
|
14
|
+
def initialize(
|
|
15
|
+
code:,
|
|
16
|
+
message:,
|
|
17
|
+
file: nil,
|
|
18
|
+
line: nil,
|
|
19
|
+
column: nil,
|
|
20
|
+
end_column: nil,
|
|
21
|
+
severity: SEVERITY_ERROR,
|
|
22
|
+
expected: nil,
|
|
23
|
+
actual: nil,
|
|
24
|
+
suggestion: nil,
|
|
25
|
+
source_line: nil
|
|
26
|
+
)
|
|
27
|
+
# rubocop:enable Metrics/ParameterLists
|
|
28
|
+
@code = code
|
|
29
|
+
@message = message
|
|
30
|
+
@file = file
|
|
31
|
+
@line = line
|
|
32
|
+
@column = column || 1
|
|
33
|
+
@end_column = end_column || (@column + 1)
|
|
34
|
+
@severity = severity
|
|
35
|
+
@expected = expected
|
|
36
|
+
@actual = actual
|
|
37
|
+
@suggestion = suggestion
|
|
38
|
+
@source_line = source_line
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.from_type_check_error(error, file: nil, source: nil)
|
|
42
|
+
line, col = parse_location(error.location)
|
|
43
|
+
source_line = extract_source_line(source, line) if source && line
|
|
44
|
+
|
|
45
|
+
new(
|
|
46
|
+
code: "TR2001",
|
|
47
|
+
message: error.error_message,
|
|
48
|
+
file: file,
|
|
49
|
+
line: line,
|
|
50
|
+
column: col,
|
|
51
|
+
severity: error.severity || SEVERITY_ERROR,
|
|
52
|
+
expected: error.expected,
|
|
53
|
+
actual: error.actual,
|
|
54
|
+
suggestion: error.suggestion,
|
|
55
|
+
source_line: source_line
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def self.from_parse_error(error, file: nil, source: nil)
|
|
60
|
+
source_line = extract_source_line(source, error.line) if source && error.line
|
|
61
|
+
|
|
62
|
+
new(
|
|
63
|
+
code: "TR1001",
|
|
64
|
+
message: error.message,
|
|
65
|
+
file: file,
|
|
66
|
+
line: error.line,
|
|
67
|
+
column: error.column,
|
|
68
|
+
source_line: source_line
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def self.from_scan_error(error, file: nil, source: nil)
|
|
73
|
+
source_line = extract_source_line(source, error.line) if source && error.line
|
|
74
|
+
# ScanError adds " at line X, column Y" to the message in its constructor
|
|
75
|
+
message = error.message.sub(/ at line \d+, column \d+\z/, "")
|
|
76
|
+
|
|
77
|
+
new(
|
|
78
|
+
code: "TR1001",
|
|
79
|
+
message: message,
|
|
80
|
+
file: file,
|
|
81
|
+
line: error.line,
|
|
82
|
+
column: error.column,
|
|
83
|
+
source_line: source_line
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def error?
|
|
88
|
+
@severity == SEVERITY_ERROR
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def self.parse_location(location_str)
|
|
92
|
+
return [nil, 1] unless location_str
|
|
93
|
+
|
|
94
|
+
case location_str
|
|
95
|
+
when /:(\d+):(\d+)$/
|
|
96
|
+
[::Regexp.last_match(1).to_i, ::Regexp.last_match(2).to_i]
|
|
97
|
+
when /:(\d+)$/
|
|
98
|
+
[::Regexp.last_match(1).to_i, 1]
|
|
99
|
+
when /line (\d+)/i
|
|
100
|
+
[::Regexp.last_match(1).to_i, 1]
|
|
101
|
+
else
|
|
102
|
+
[nil, 1]
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def self.extract_source_line(source, line_num)
|
|
107
|
+
return nil unless source && line_num
|
|
108
|
+
|
|
109
|
+
lines = source.split("\n")
|
|
110
|
+
lines[line_num - 1] if line_num.positive? && line_num <= lines.length
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
private_class_method :parse_location, :extract_source_line
|
|
114
|
+
end
|
|
115
|
+
end
|