t-ruby 0.0.42 → 0.0.43

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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/lib/t_ruby/ast_type_inferrer.rb +2 -0
  3. data/lib/t_ruby/cache.rb +40 -10
  4. data/lib/t_ruby/cli.rb +13 -8
  5. data/lib/t_ruby/compiler.rb +168 -0
  6. data/lib/t_ruby/diagnostic.rb +115 -0
  7. data/lib/t_ruby/diagnostic_formatter.rb +162 -0
  8. data/lib/t_ruby/error_handler.rb +201 -35
  9. data/lib/t_ruby/error_reporter.rb +57 -0
  10. data/lib/t_ruby/ir.rb +39 -1
  11. data/lib/t_ruby/lsp_server.rb +40 -97
  12. data/lib/t_ruby/parser.rb +18 -4
  13. data/lib/t_ruby/parser_combinator/combinators/alternative.rb +20 -0
  14. data/lib/t_ruby/parser_combinator/combinators/chain_left.rb +34 -0
  15. data/lib/t_ruby/parser_combinator/combinators/choice.rb +29 -0
  16. data/lib/t_ruby/parser_combinator/combinators/flat_map.rb +21 -0
  17. data/lib/t_ruby/parser_combinator/combinators/label.rb +22 -0
  18. data/lib/t_ruby/parser_combinator/combinators/lookahead.rb +21 -0
  19. data/lib/t_ruby/parser_combinator/combinators/many.rb +29 -0
  20. data/lib/t_ruby/parser_combinator/combinators/many1.rb +32 -0
  21. data/lib/t_ruby/parser_combinator/combinators/map.rb +17 -0
  22. data/lib/t_ruby/parser_combinator/combinators/not_followed_by.rb +21 -0
  23. data/lib/t_ruby/parser_combinator/combinators/optional.rb +21 -0
  24. data/lib/t_ruby/parser_combinator/combinators/sep_by.rb +34 -0
  25. data/lib/t_ruby/parser_combinator/combinators/sep_by1.rb +34 -0
  26. data/lib/t_ruby/parser_combinator/combinators/sequence.rb +23 -0
  27. data/lib/t_ruby/parser_combinator/combinators/skip_right.rb +23 -0
  28. data/lib/t_ruby/parser_combinator/declaration_parser.rb +147 -0
  29. data/lib/t_ruby/parser_combinator/dsl.rb +115 -0
  30. data/lib/t_ruby/parser_combinator/parse_error.rb +48 -0
  31. data/lib/t_ruby/parser_combinator/parse_result.rb +46 -0
  32. data/lib/t_ruby/parser_combinator/parser.rb +84 -0
  33. data/lib/t_ruby/parser_combinator/primitives/end_of_input.rb +16 -0
  34. data/lib/t_ruby/parser_combinator/primitives/fail.rb +16 -0
  35. data/lib/t_ruby/parser_combinator/primitives/lazy.rb +18 -0
  36. data/lib/t_ruby/parser_combinator/primitives/literal.rb +21 -0
  37. data/lib/t_ruby/parser_combinator/primitives/pure.rb +16 -0
  38. data/lib/t_ruby/parser_combinator/primitives/regex.rb +25 -0
  39. data/lib/t_ruby/parser_combinator/primitives/satisfy.rb +21 -0
  40. data/lib/t_ruby/parser_combinator/token/expression_parser.rb +541 -0
  41. data/lib/t_ruby/parser_combinator/token/statement_parser.rb +644 -0
  42. data/lib/t_ruby/parser_combinator/token/token_alternative.rb +20 -0
  43. data/lib/t_ruby/parser_combinator/token/token_body_parser.rb +54 -0
  44. data/lib/t_ruby/parser_combinator/token/token_declaration_parser.rb +920 -0
  45. data/lib/t_ruby/parser_combinator/token/token_dsl.rb +16 -0
  46. data/lib/t_ruby/parser_combinator/token/token_label.rb +22 -0
  47. data/lib/t_ruby/parser_combinator/token/token_many.rb +29 -0
  48. data/lib/t_ruby/parser_combinator/token/token_many1.rb +32 -0
  49. data/lib/t_ruby/parser_combinator/token/token_map.rb +17 -0
  50. data/lib/t_ruby/parser_combinator/token/token_matcher.rb +29 -0
  51. data/lib/t_ruby/parser_combinator/token/token_optional.rb +21 -0
  52. data/lib/t_ruby/parser_combinator/token/token_parse_result.rb +40 -0
  53. data/lib/t_ruby/parser_combinator/token/token_parser.rb +62 -0
  54. data/lib/t_ruby/parser_combinator/token/token_sep_by.rb +34 -0
  55. data/lib/t_ruby/parser_combinator/token/token_sep_by1.rb +34 -0
  56. data/lib/t_ruby/parser_combinator/token/token_sequence.rb +23 -0
  57. data/lib/t_ruby/parser_combinator/token/token_skip_right.rb +23 -0
  58. data/lib/t_ruby/parser_combinator/type_parser.rb +103 -0
  59. data/lib/t_ruby/parser_combinator.rb +64 -936
  60. data/lib/t_ruby/scanner.rb +883 -0
  61. data/lib/t_ruby/version.rb +1 -1
  62. data/lib/t_ruby/watcher.rb +67 -75
  63. data/lib/t_ruby.rb +15 -1
  64. metadata +51 -2
  65. data/lib/t_ruby/body_parser.rb +0 -561
@@ -2,7 +2,9 @@
2
2
 
3
3
  module TRuby
4
4
  class ErrorHandler
5
- VALID_TYPES = %w[String Integer Boolean Array Hash Symbol void nil].freeze
5
+ VALID_TYPES = %w[String Integer Float Boolean Array Hash Symbol void nil].freeze
6
+ # Unicode-aware identifier pattern for method/variable names (supports Korean, etc.)
7
+ IDENTIFIER_PATTERN = /[\w\p{L}\p{N}]+[!?]?/
6
8
 
7
9
  def initialize(source)
8
10
  @source = source
@@ -97,34 +99,76 @@ module TRuby
97
99
  end
98
100
 
99
101
  # Pattern 2: Check for text after closing paren without colon (e.g., "def test() something")
100
- if (match = line.match(/def\s+\w+\s*\([^)]*\)\s*([^:\s].+?)\s*$/))
101
- trailing = match[1].strip
102
- # Allow if it's just end-of-line content or a valid Ruby block start
103
- unless trailing.empty? || trailing.start_with?("#") || trailing == "end"
104
- @errors << "Line #{idx + 1}: Unexpected token '#{trailing}' after method parameters - did you forget ':'?"
102
+ # Use balanced paren matching to find the correct closing paren
103
+ params_end = find_params_closing_paren(line)
104
+ if params_end
105
+ after_params = line[params_end..].strip
106
+ # Check if there's trailing content that's not a return type annotation
107
+ if (match = after_params.match(/^\)\s*([^:\s].+?)\s*$/))
108
+ trailing = match[1].strip
109
+ # Allow if it's just end-of-line content or a valid Ruby block start
110
+ unless trailing.empty? || trailing.start_with?("#") || trailing == "end"
111
+ @errors << "Line #{idx + 1}: Unexpected token '#{trailing}' after method parameters - did you forget ':'?"
112
+ end
113
+ return
105
114
  end
106
- return
107
115
  end
108
116
 
109
117
  # Pattern 3: Check for parameter with colon but no type (e.g., "def test(x:)")
110
- if line.match?(/def\s+\w+\s*\([^)]*\w+:\s*[,)]/)
118
+ # Skip this check for keyword args group { name:, age: } - they're valid
119
+ params_str = extract_params_string(line)
120
+ # Check each parameter for colon without type
121
+ # Match: "x:" at end, "x:," in middle, or "x: )" with space before closing
122
+ if params_str && !params_str.include?("{") &&
123
+ (params_str.match?(/\w+:\s*$/) || params_str.match?(/\w+:\s*,/))
111
124
  @errors << "Line #{idx + 1}: Expected type after parameter colon"
112
125
  return
113
126
  end
114
127
 
115
128
  # Pattern 4: Extract and validate return type
116
- if (match = line.match(/def\s+\w+\s*\([^)]*\)\s*:\s*(.+?)\s*$/))
117
- return_type_str = match[1].strip
118
- validate_type_expression(return_type_str, idx, "return type")
129
+ if params_end
130
+ after_params = line[params_end..]
131
+ if (match = after_params.match(/\)\s*:\s*(.+?)\s*$/))
132
+ return_type_str = match[1].strip
133
+ validate_type_expression(return_type_str, idx, "return type")
134
+ end
119
135
  end
120
136
 
121
137
  # Pattern 5: Extract and validate parameter types
122
- if (match = line.match(/def\s+\w+\s*\(([^)]+)\)/))
123
- params_str = match[1]
138
+ if params_str
124
139
  validate_parameter_types_expression(params_str, idx)
125
140
  end
126
141
  end
127
142
 
143
+ # Find the position of the closing paren for method parameters (balanced matching)
144
+ def find_params_closing_paren(line)
145
+ start_pos = line.index("(")
146
+ return nil unless start_pos
147
+
148
+ depth = 0
149
+ line[start_pos..].each_char.with_index do |char, i|
150
+ case char
151
+ when "("
152
+ depth += 1
153
+ when ")"
154
+ depth -= 1
155
+ return start_pos + i if depth.zero?
156
+ end
157
+ end
158
+ nil
159
+ end
160
+
161
+ # Extract the parameters string from a method definition line
162
+ def extract_params_string(line)
163
+ start_pos = line.index("(")
164
+ return nil unless start_pos
165
+
166
+ end_pos = find_params_closing_paren(line)
167
+ return nil unless end_pos
168
+
169
+ line[(start_pos + 1)...end_pos]
170
+ end
171
+
128
172
  def validate_type_expression(type_str, line_idx, context = "type")
129
173
  return if type_str.nil? || type_str.empty?
130
174
 
@@ -153,7 +197,9 @@ module TRuby
153
197
  end
154
198
 
155
199
  # Check for unclosed brackets
156
- if type_str.count("<") != type_str.count(">")
200
+ # Note: we need to exclude -> arrow operators when counting < and >
201
+ angle_balance = count_angle_brackets(type_str)
202
+ if angle_balance != 0
157
203
  @errors << "Line #{line_idx + 1}: Invalid #{context} '#{type_str}' - unbalanced angle brackets"
158
204
  return
159
205
  end
@@ -189,12 +235,17 @@ module TRuby
189
235
  # Check for extra tokens after valid type (e.g., "String something_else")
190
236
  # Use TypeParser to validate
191
237
  result = @type_parser.parse(type_str)
192
- if result[:success]
193
- remaining = type_str[(result[:position] || 0)..]&.strip
194
- if remaining && !remaining.empty? && result[:remaining] && !result[:remaining].strip.empty?
195
- @errors << "Line #{line_idx + 1}: Unexpected token after #{context} '#{type_str}'"
196
- end
197
- end
238
+ return unless result[:success]
239
+
240
+ remaining = result[:remaining]&.strip
241
+ return if remaining.nil? || remaining.empty?
242
+
243
+ # Allow RBS-style square bracket generics (e.g., Hash[Symbol, String])
244
+ # Allow nullable suffix (e.g., String?)
245
+ # Allow array suffix (e.g., [])
246
+ return if remaining.start_with?("[") || remaining.start_with?("?") || remaining == "[]"
247
+
248
+ @errors << "Line #{line_idx + 1}: Unexpected token after #{context} '#{type_str}'"
198
249
  end
199
250
 
200
251
  def validate_parameter_types_expression(params_str, line_idx)
@@ -207,36 +258,104 @@ module TRuby
207
258
  param = param.strip
208
259
  next if param.empty?
209
260
 
210
- # Check for param: Type pattern
261
+ # Skip keyword args group: { name: Type, age: Type }
262
+ next if param.start_with?("{")
263
+
264
+ # Skip block parameter: &block or &block: Type
265
+ next if param.start_with?("&")
266
+
267
+ # Skip rest parameter: *args or *args: Type
268
+ next if param.start_with?("*")
269
+
270
+ # Check for param: Type pattern (with optional default value)
271
+ # Match: name: Type or name: Type = default
211
272
  next unless (match = param.match(/^(\w+)\s*:\s*(.+)$/))
212
273
 
213
274
  param_name = match[1]
214
- type_str = match[2].strip
275
+ type_and_default = match[2].strip
215
276
 
216
- if type_str.empty?
277
+ if type_and_default.empty?
217
278
  @errors << "Line #{line_idx + 1}: Expected type after colon for parameter '#{param_name}'"
218
279
  next
219
280
  end
220
281
 
282
+ # Extract just the type part (before any '=' for default value)
283
+ type_str = extract_type_from_param(type_and_default)
284
+ next if type_str.nil? || type_str.empty?
285
+
221
286
  validate_type_expression(type_str, line_idx, "parameter type for '#{param_name}'")
222
287
  end
223
288
  end
224
289
 
290
+ # Extract type from "Type = default_value" or just "Type"
291
+ def extract_type_from_param(type_and_default)
292
+ # Find the position of '=' that's not inside parentheses/brackets
293
+ depth = 0
294
+ type_and_default.each_char.with_index do |char, i|
295
+ case char
296
+ when "(", "<", "["
297
+ depth += 1
298
+ when ")", ">", "]"
299
+ depth -= 1
300
+ when "="
301
+ # Make sure it's not part of -> operator
302
+ prev_char = i.positive? ? type_and_default[i - 1] : nil
303
+ next if %w[- ! = < >].include?(prev_char)
304
+
305
+ return type_and_default[0...i].strip if depth.zero?
306
+ end
307
+ end
308
+ type_and_default
309
+ end
310
+
225
311
  def split_parameters(params_str)
226
312
  result = []
227
313
  current = ""
228
- depth = 0
314
+ paren_depth = 0
315
+ bracket_depth = 0
316
+ angle_depth = 0
317
+ brace_depth = 0
318
+
319
+ i = 0
320
+ while i < params_str.length
321
+ char = params_str[i]
322
+ next_char = params_str[i + 1]
323
+ prev_char = i.positive? ? params_str[i - 1] : nil
229
324
 
230
- params_str.each_char do |char|
231
325
  case char
232
- when "<", "[", "("
233
- depth += 1
326
+ when "("
327
+ paren_depth += 1
234
328
  current += char
235
- when ">", "]", ")"
236
- depth -= 1
329
+ when ")"
330
+ paren_depth -= 1
331
+ current += char
332
+ when "["
333
+ bracket_depth += 1
334
+ current += char
335
+ when "]"
336
+ bracket_depth -= 1
337
+ current += char
338
+ when "<"
339
+ # Only count as generic if it's not part of operator like <=, <=>
340
+ if next_char != "=" && next_char != ">"
341
+ angle_depth += 1
342
+ end
343
+ current += char
344
+ when ">"
345
+ # Only count as closing generic if we're inside a generic (angle_depth > 0)
346
+ # and it's not part of -> operator
347
+ if angle_depth.positive? && prev_char != "-"
348
+ angle_depth -= 1
349
+ end
350
+ current += char
351
+ when "{"
352
+ brace_depth += 1
353
+ current += char
354
+ when "}"
355
+ brace_depth -= 1
237
356
  current += char
238
357
  when ","
239
- if depth.zero?
358
+ if paren_depth.zero? && bracket_depth.zero? && angle_depth.zero? && brace_depth.zero?
240
359
  result << current.strip
241
360
  current = ""
242
361
  else
@@ -245,6 +364,7 @@ module TRuby
245
364
  else
246
365
  current += char
247
366
  end
367
+ i += 1
248
368
  end
249
369
 
250
370
  result << current.strip unless current.empty?
@@ -263,7 +383,7 @@ module TRuby
263
383
  return_type = match[2]&.strip
264
384
 
265
385
  # Check return type if it's a simple type name
266
- if return_type&.match?(/^\w+$/) && !(VALID_TYPES.include?(return_type) || @type_aliases.key?(return_type))
386
+ if return_type&.match?(/^\w+$/) && !(VALID_TYPES.include?(return_type) || @type_aliases.key?(return_type) || @interfaces.key?(return_type))
267
387
  @errors << "Line #{idx + 1}: Unknown return type '#{return_type}'"
268
388
  end
269
389
 
@@ -286,24 +406,70 @@ module TRuby
286
406
 
287
407
  # Only check simple type names against VALID_TYPES
288
408
  next unless param_type.match?(/^\w+$/)
289
- next if VALID_TYPES.include?(param_type) || @type_aliases.key?(param_type)
409
+ next if VALID_TYPES.include?(param_type) || @type_aliases.key?(param_type) || @interfaces.key?(param_type)
290
410
 
291
411
  @errors << "Line #{line_idx + 1}: Unknown parameter type '#{param_type}'"
292
412
  end
293
413
  end
294
414
 
295
415
  def check_duplicate_definitions
416
+ current_class = nil
417
+ class_methods = {} # { class_name => { method_name => line_number } }
418
+
296
419
  @lines.each_with_index do |line, idx|
297
- next unless line.match?(/^\s*def\s+(\w+)/)
420
+ # Track class context
421
+ if line.match?(/^\s*class\s+(\w+)/)
422
+ current_class = line.match(/class\s+(\w+)/)[1]
423
+ class_methods[current_class] ||= {}
424
+ elsif line.match?(/^\s*end\s*$/) && current_class
425
+ # Simple heuristic: top-level 'end' closes current class
426
+ # This is imperfect but handles most cases
427
+ current_class = nil if line.match?(/^end\s*$/)
428
+ end
429
+
430
+ # Use unicode-aware pattern for function names (supports Korean, etc.)
431
+ next unless line.match?(/^\s*def\s+#{IDENTIFIER_PATTERN}/)
298
432
 
299
- func_name = line.match(/def\s+(\w+)/)[1]
433
+ func_name = line.match(/def\s+(#{IDENTIFIER_PATTERN})/)[1]
300
434
 
301
- if @functions[func_name]
435
+ if current_class
436
+ # Method inside a class - check within class scope
437
+ methods = class_methods[current_class]
438
+ if methods[func_name]
439
+ @errors << "Line #{idx + 1}: Function '#{func_name}' is already defined at line #{methods[func_name]}"
440
+ else
441
+ methods[func_name] = idx + 1
442
+ end
443
+ elsif @functions[func_name]
444
+ # Top-level function - check global scope
302
445
  @errors << "Line #{idx + 1}: Function '#{func_name}' is already defined at line #{@functions[func_name]}"
303
446
  else
304
447
  @functions[func_name] = idx + 1
305
448
  end
306
449
  end
307
450
  end
451
+
452
+ # Count angle brackets excluding those in -> arrow operators
453
+ # Returns the balance (positive if more <, negative if more >)
454
+ def count_angle_brackets(type_str)
455
+ balance = 0
456
+ i = 0
457
+ while i < type_str.length
458
+ char = type_str[i]
459
+ prev_char = i.positive? ? type_str[i - 1] : nil
460
+ next_char = type_str[i + 1]
461
+
462
+ case char
463
+ when "<"
464
+ # Skip if it's part of <= or <>
465
+ balance += 1 unless %w[= >].include?(next_char)
466
+ when ">"
467
+ # Skip if it's part of -> arrow operator
468
+ balance -= 1 unless prev_char == "-"
469
+ end
470
+ i += 1
471
+ end
472
+ balance
473
+ end
308
474
  end
309
475
  end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TRuby
4
+ class ErrorReporter
5
+ attr_reader :diagnostics
6
+
7
+ def initialize(formatter: nil)
8
+ @diagnostics = []
9
+ @formatter = formatter || DiagnosticFormatter.new
10
+ @source_cache = {}
11
+ end
12
+
13
+ def add(diagnostic)
14
+ @diagnostics << diagnostic
15
+ end
16
+
17
+ def add_type_check_error(error, file:, source: nil)
18
+ source ||= load_source(file)
19
+ add(Diagnostic.from_type_check_error(error, file: file, source: source))
20
+ end
21
+
22
+ def add_parse_error(error, file:, source: nil)
23
+ source ||= load_source(file)
24
+ add(Diagnostic.from_parse_error(error, file: file, source: source))
25
+ end
26
+
27
+ def add_scan_error(error, file:, source: nil)
28
+ source ||= load_source(file)
29
+ add(Diagnostic.from_scan_error(error, file: file, source: source))
30
+ end
31
+
32
+ def has_errors?
33
+ @diagnostics.any? { |d| d.severity == Diagnostic::SEVERITY_ERROR }
34
+ end
35
+
36
+ def error_count
37
+ @diagnostics.count { |d| d.severity == Diagnostic::SEVERITY_ERROR }
38
+ end
39
+
40
+ def report
41
+ @formatter.format_all(@diagnostics)
42
+ end
43
+
44
+ def clear
45
+ @diagnostics.clear
46
+ @source_cache.clear
47
+ end
48
+
49
+ private
50
+
51
+ def load_source(file)
52
+ return nil unless file && File.exist?(file)
53
+
54
+ @source_cache[file] ||= File.read(file)
55
+ end
56
+ end
57
+ end
data/lib/t_ruby/ir.rb CHANGED
@@ -234,6 +234,20 @@ module TRuby
234
234
  end
235
235
  end
236
236
 
237
+ # Interpolated string (string with #{...} expressions)
238
+ class InterpolatedString < Node
239
+ attr_accessor :parts
240
+
241
+ def initialize(parts: [], **opts)
242
+ super(**opts)
243
+ @parts = parts
244
+ end
245
+
246
+ def children
247
+ @parts
248
+ end
249
+ end
250
+
237
251
  # Array literal
238
252
  class ArrayLiteral < Node
239
253
  attr_accessor :elements, :element_type
@@ -658,6 +672,26 @@ module TRuby
658
672
  end
659
673
  end
660
674
 
675
+ # Hash literal type: { key: Type, key2: Type }
676
+ class HashLiteralType < TypeNode
677
+ attr_accessor :fields # Array of { name: String, type: TypeNode }
678
+
679
+ def initialize(fields:, **opts)
680
+ super(**opts)
681
+ @fields = fields
682
+ end
683
+
684
+ def to_rbs
685
+ # Hash literal types in RBS are represented as Hash[Symbol, untyped] or specific record types
686
+ "Hash[Symbol, untyped]"
687
+ end
688
+
689
+ def to_trb
690
+ field_strs = @fields.map { |f| "#{f[:name]}: #{f[:type].to_trb}" }
691
+ "{ #{field_strs.join(", ")} }"
692
+ end
693
+ end
694
+
661
695
  #==========================================================================
662
696
  # Visitor Pattern
663
697
  #==========================================================================
@@ -778,12 +812,16 @@ module TRuby
778
812
  # 본문 IR이 있으면 사용 (BodyParser에서 파싱됨)
779
813
  body = info[:body_ir]
780
814
 
815
+ # Build location string from line/column info
816
+ location = info[:line] && info[:column] ? "#{info[:line]}:#{info[:column]}" : nil
817
+
781
818
  MethodDef.new(
782
819
  name: info[:name],
783
820
  params: params,
784
821
  return_type: info[:return_type] ? parse_type(info[:return_type]) : nil,
785
822
  body: body,
786
- visibility: info[:visibility] || :public
823
+ visibility: info[:visibility] || :public,
824
+ location: location
787
825
  )
788
826
  end
789
827
 
@@ -122,6 +122,8 @@ module TRuby
122
122
  @initialized = false
123
123
  @shutdown_requested = false
124
124
  @type_alias_registry = TypeAliasRegistry.new
125
+ # Use Compiler for unified diagnostics (same as CLI)
126
+ @compiler = Compiler.new
125
127
  end
126
128
 
127
129
  # Main run loop for the LSP server
@@ -368,114 +370,55 @@ module TRuby
368
370
  })
369
371
  end
370
372
 
371
- def analyze_document(text)
372
- diagnostics = []
373
+ def analyze_document(text, uri: nil)
374
+ # Use unified Compiler.analyze for diagnostics
375
+ # This ensures CLI and LSP show the same errors
376
+ file_path = uri ? uri_to_path(uri) : "<source>"
377
+ compiler_diagnostics = @compiler.analyze(text, file: file_path)
373
378
 
374
- # Use ErrorHandler to check for errors
375
- error_handler = ErrorHandler.new(text)
376
- errors = error_handler.check
377
-
378
- errors.each do |error|
379
- # Parse line number from error message
380
- next unless error =~ /^Line (\d+):\s*(.+)$/
381
-
382
- line_num = Regexp.last_match(1).to_i - 1 # LSP uses 0-based line numbers
383
- message = Regexp.last_match(2)
384
-
385
- diagnostics << create_diagnostic(line_num, message, DiagnosticSeverity::ERROR)
386
- end
387
-
388
- # Additional validation using Parser
389
- begin
390
- parser = Parser.new(text)
391
- result = parser.parse
392
-
393
- # Validate type aliases
394
- validate_type_aliases(result[:type_aliases] || [], diagnostics, text)
395
-
396
- # Validate function types
397
- validate_functions(result[:functions] || [], diagnostics, text)
398
- rescue StandardError => e
399
- diagnostics << create_diagnostic(0, "Parse error: #{e.message}", DiagnosticSeverity::ERROR)
400
- end
401
-
402
- diagnostics
379
+ # Convert TRuby::Diagnostic objects to LSP diagnostic format
380
+ compiler_diagnostics.map { |d| diagnostic_to_lsp(d) }
403
381
  end
404
382
 
405
- def validate_type_aliases(type_aliases, diagnostics, text)
406
- lines = text.split("\n")
407
- registry = TypeAliasRegistry.new
408
-
409
- type_aliases.each do |alias_info|
410
- line_num = find_line_number(lines, /^\s*type\s+#{Regexp.escape(alias_info[:name])}\s*=/)
411
- next unless line_num
412
-
413
- begin
414
- registry.register(alias_info[:name], alias_info[:definition])
415
- rescue DuplicateTypeAliasError => e
416
- diagnostics << create_diagnostic(line_num, e.message, DiagnosticSeverity::ERROR)
417
- rescue CircularTypeAliasError => e
418
- diagnostics << create_diagnostic(line_num, e.message, DiagnosticSeverity::ERROR)
419
- end
420
- end
421
- end
383
+ # Convert TRuby::Diagnostic to LSP diagnostic format
384
+ def diagnostic_to_lsp(diagnostic)
385
+ # LSP uses 0-based line numbers
386
+ line = (diagnostic.line || 1) - 1
387
+ line = 0 if line.negative?
422
388
 
423
- def validate_functions(functions, diagnostics, text)
424
- lines = text.split("\n")
389
+ col = (diagnostic.column || 1) - 1
390
+ col = 0 if col.negative?
425
391
 
426
- functions.each do |func|
427
- line_num = find_line_number(lines, /^\s*def\s+#{Regexp.escape(func[:name])}\s*\(/)
428
- next unless line_num
429
-
430
- # Validate return type
431
- if func[:return_type] && !valid_type?(func[:return_type])
432
- diagnostics << create_diagnostic(
433
- line_num,
434
- "Unknown return type '#{func[:return_type]}'",
435
- DiagnosticSeverity::WARNING
436
- )
437
- end
438
-
439
- # Validate parameter types
440
- func[:params]&.each do |param|
441
- next unless param[:type] && !valid_type?(param[:type])
442
-
443
- diagnostics << create_diagnostic(
444
- line_num,
445
- "Unknown parameter type '#{param[:type]}' for '#{param[:name]}'",
446
- DiagnosticSeverity::WARNING
447
- )
448
- end
449
- end
450
- end
392
+ end_col = diagnostic.end_column ? diagnostic.end_column - 1 : col + 1
451
393
 
452
- def find_line_number(lines, pattern)
453
- lines.each_with_index do |line, idx|
454
- return idx if line.match?(pattern)
455
- end
456
- nil
457
- end
394
+ severity = case diagnostic.severity
395
+ when :error then DiagnosticSeverity::ERROR
396
+ when :warning then DiagnosticSeverity::WARNING
397
+ when :info then DiagnosticSeverity::INFORMATION
398
+ else DiagnosticSeverity::ERROR
399
+ end
458
400
 
459
- def valid_type?(type_str)
460
- return true if type_str.nil?
401
+ lsp_diag = {
402
+ "range" => {
403
+ "start" => { "line" => line, "character" => col },
404
+ "end" => { "line" => line, "character" => end_col },
405
+ },
406
+ "severity" => severity,
407
+ "source" => "t-ruby",
408
+ "message" => diagnostic.message,
409
+ }
461
410
 
462
- # Handle union types
463
- if type_str.include?("|")
464
- return type_str.split("|").map(&:strip).all? { |t| valid_type?(t) }
465
- end
411
+ # Add error code if available
412
+ lsp_diag["code"] = diagnostic.code if diagnostic.code
466
413
 
467
- # Handle intersection types
468
- if type_str.include?("&")
469
- return type_str.split("&").map(&:strip).all? { |t| valid_type?(t) }
470
- end
414
+ lsp_diag
415
+ end
471
416
 
472
- # Handle generic types
473
- if type_str.include?("<")
474
- base_type = type_str.split("<").first
475
- return BUILT_IN_TYPES.include?(base_type) || @type_alias_registry.valid_type?(base_type)
476
- end
417
+ def uri_to_path(uri)
418
+ # Convert file:// URI to filesystem path
419
+ return uri unless uri.start_with?("file://")
477
420
 
478
- BUILT_IN_TYPES.include?(type_str) || @type_alias_registry.valid_type?(type_str)
421
+ uri.sub(%r{^file://}, "")
479
422
  end
480
423
 
481
424
  def create_diagnostic(line, message, severity)