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.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +12 -2
  3. data/bin/t-ruby +6 -0
  4. data/lib/t_ruby/ast_type_inferrer.rb +2 -0
  5. data/lib/t_ruby/benchmark.rb +1 -0
  6. data/lib/t_ruby/bundler_integration.rb +1 -0
  7. data/lib/t_ruby/cache.rb +40 -10
  8. data/lib/t_ruby/cli.rb +30 -8
  9. data/lib/t_ruby/compiler.rb +168 -0
  10. data/lib/t_ruby/diagnostic.rb +115 -0
  11. data/lib/t_ruby/diagnostic_formatter.rb +162 -0
  12. data/lib/t_ruby/doc_generator.rb +1 -0
  13. data/lib/t_ruby/docs_badge_generator.rb +1 -0
  14. data/lib/t_ruby/error_handler.rb +201 -35
  15. data/lib/t_ruby/error_reporter.rb +57 -0
  16. data/lib/t_ruby/ir.rb +53 -69
  17. data/lib/t_ruby/lsp_server.rb +40 -97
  18. data/lib/t_ruby/package_manager.rb +1 -0
  19. data/lib/t_ruby/parser.rb +18 -4
  20. data/lib/t_ruby/parser_combinator/combinators/alternative.rb +20 -0
  21. data/lib/t_ruby/parser_combinator/combinators/chain_left.rb +34 -0
  22. data/lib/t_ruby/parser_combinator/combinators/choice.rb +29 -0
  23. data/lib/t_ruby/parser_combinator/combinators/flat_map.rb +21 -0
  24. data/lib/t_ruby/parser_combinator/combinators/label.rb +22 -0
  25. data/lib/t_ruby/parser_combinator/combinators/lookahead.rb +21 -0
  26. data/lib/t_ruby/parser_combinator/combinators/many.rb +29 -0
  27. data/lib/t_ruby/parser_combinator/combinators/many1.rb +32 -0
  28. data/lib/t_ruby/parser_combinator/combinators/map.rb +17 -0
  29. data/lib/t_ruby/parser_combinator/combinators/not_followed_by.rb +21 -0
  30. data/lib/t_ruby/parser_combinator/combinators/optional.rb +21 -0
  31. data/lib/t_ruby/parser_combinator/combinators/sep_by.rb +34 -0
  32. data/lib/t_ruby/parser_combinator/combinators/sep_by1.rb +34 -0
  33. data/lib/t_ruby/parser_combinator/combinators/sequence.rb +23 -0
  34. data/lib/t_ruby/parser_combinator/combinators/skip_right.rb +23 -0
  35. data/lib/t_ruby/parser_combinator/declaration_parser.rb +147 -0
  36. data/lib/t_ruby/parser_combinator/dsl.rb +115 -0
  37. data/lib/t_ruby/parser_combinator/parse_error.rb +48 -0
  38. data/lib/t_ruby/parser_combinator/parse_result.rb +46 -0
  39. data/lib/t_ruby/parser_combinator/parser.rb +84 -0
  40. data/lib/t_ruby/parser_combinator/primitives/end_of_input.rb +16 -0
  41. data/lib/t_ruby/parser_combinator/primitives/fail.rb +16 -0
  42. data/lib/t_ruby/parser_combinator/primitives/lazy.rb +18 -0
  43. data/lib/t_ruby/parser_combinator/primitives/literal.rb +21 -0
  44. data/lib/t_ruby/parser_combinator/primitives/pure.rb +16 -0
  45. data/lib/t_ruby/parser_combinator/primitives/regex.rb +25 -0
  46. data/lib/t_ruby/parser_combinator/primitives/satisfy.rb +21 -0
  47. data/lib/t_ruby/parser_combinator/token/expression_parser.rb +541 -0
  48. data/lib/t_ruby/parser_combinator/token/statement_parser.rb +644 -0
  49. data/lib/t_ruby/parser_combinator/token/token_alternative.rb +20 -0
  50. data/lib/t_ruby/parser_combinator/token/token_body_parser.rb +54 -0
  51. data/lib/t_ruby/parser_combinator/token/token_declaration_parser.rb +962 -0
  52. data/lib/t_ruby/parser_combinator/token/token_dsl.rb +16 -0
  53. data/lib/t_ruby/parser_combinator/token/token_label.rb +22 -0
  54. data/lib/t_ruby/parser_combinator/token/token_many.rb +29 -0
  55. data/lib/t_ruby/parser_combinator/token/token_many1.rb +32 -0
  56. data/lib/t_ruby/parser_combinator/token/token_map.rb +17 -0
  57. data/lib/t_ruby/parser_combinator/token/token_matcher.rb +29 -0
  58. data/lib/t_ruby/parser_combinator/token/token_optional.rb +21 -0
  59. data/lib/t_ruby/parser_combinator/token/token_parse_result.rb +40 -0
  60. data/lib/t_ruby/parser_combinator/token/token_parser.rb +62 -0
  61. data/lib/t_ruby/parser_combinator/token/token_sep_by.rb +34 -0
  62. data/lib/t_ruby/parser_combinator/token/token_sep_by1.rb +34 -0
  63. data/lib/t_ruby/parser_combinator/token/token_sequence.rb +23 -0
  64. data/lib/t_ruby/parser_combinator/token/token_skip_right.rb +23 -0
  65. data/lib/t_ruby/parser_combinator/type_parser.rb +118 -0
  66. data/lib/t_ruby/parser_combinator.rb +64 -936
  67. data/lib/t_ruby/runner.rb +132 -0
  68. data/lib/t_ruby/scanner.rb +883 -0
  69. data/lib/t_ruby/version.rb +1 -1
  70. data/lib/t_ruby/watcher.rb +67 -75
  71. data/lib/t_ruby.rb +16 -1
  72. metadata +73 -4
  73. data/lib/t_ruby/body_parser.rb +0 -561
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 75019794a577853cf5a7e704febef3868a1ddd06776e43f50dc684d7dde87e3c
4
- data.tar.gz: 89a255f7e7b68e2becd6314be09470424d5f418324c40bcc77a7311e6006d9f0
3
+ metadata.gz: 509e1090df12fc29ea3109541a6e59133bf7516b31e4859bda8a6830d44f6999
4
+ data.tar.gz: 8dd1ef0eb0eeea4faccfd13372235d509997b65b9e556299542e880e6bb6ab85
5
5
  SHA512:
6
- metadata.gz: 4000a38df6e83591a15f453af3b4d5f759b1af8e869478737c1d7711e34c7e1d65c89df34e56a6575d41e2f120e951970fde1ac444bf24d73c1167a24de875c4
7
- data.tar.gz: 9273b4bf7d8a9af3ccbe0fa0d93914b13c9ce09f464902a4974749780871516c8fbaeb33a60d1dca754914dde09e498148ec080ff31fef3b11a0a88d96642eee
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
- [MIT](./LICENSE)
299
+ [BSD 2-Clause](./LICENSE)
data/bin/t-ruby ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/t_ruby"
5
+
6
+ TRuby::RunnerCLI.start(ARGV)
@@ -158,6 +158,8 @@ module TRuby
158
158
  type = case node
159
159
  when IR::Literal
160
160
  infer_literal(node)
161
+ when IR::InterpolatedString
162
+ "String" # Interpolated strings always produce String
161
163
  when IR::VariableRef
162
164
  infer_variable_ref(node, env)
163
165
  when IR::BinaryOp
@@ -3,6 +3,7 @@
3
3
  require "benchmark"
4
4
  require "json"
5
5
  require "fileutils"
6
+ require "time"
6
7
 
7
8
  module TRuby
8
9
  # Benchmark suite for T-Ruby performance measurement
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "fileutils"
4
+ require "time"
4
5
 
5
6
  module TRuby
6
7
  # Integrates T-Ruby type packages with Bundler/RubyGems ecosystem
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 = file_hash(file_path)
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] = file_hash(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 file_hash(file_path)
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
- errors = []
694
+ all_diagnostics = []
689
695
 
690
696
  # First pass: compile and register all files
691
697
  file_paths.each do |file_path|
692
- results[file_path] = compile_with_ir(file_path)
693
- rescue StandardError => e
694
- errors << { file: file_path, error: e.message }
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
- errors.concat(check_result[:errors])
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
- errors: errors,
706
- success: errors.empty?,
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
- output_path = compiler.compile(input_file)
218
- puts "Compiled: #{input_file} -> #{output_path}"
219
- rescue TypeCheckError => e
220
- puts "Type error: #{e.message}"
221
- exit 1
222
- rescue ArgumentError => e
223
- puts "Error: #{e.message}"
224
- exit 1
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
@@ -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