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.
- checksums.yaml +4 -4
- data/lib/t_ruby/ast_type_inferrer.rb +2 -0
- data/lib/t_ruby/cache.rb +40 -10
- data/lib/t_ruby/cli.rb +13 -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/error_handler.rb +201 -35
- data/lib/t_ruby/error_reporter.rb +57 -0
- data/lib/t_ruby/ir.rb +39 -1
- data/lib/t_ruby/lsp_server.rb +40 -97
- 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 +920 -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 +103 -0
- data/lib/t_ruby/parser_combinator.rb +64 -936
- 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 +15 -1
- metadata +51 -2
- data/lib/t_ruby/body_parser.rb +0 -561
data/lib/t_ruby/error_handler.rb
CHANGED
|
@@ -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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
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
|
|
117
|
-
|
|
118
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
275
|
+
type_and_default = match[2].strip
|
|
215
276
|
|
|
216
|
-
if
|
|
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
|
-
|
|
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
|
-
|
|
326
|
+
when "("
|
|
327
|
+
paren_depth += 1
|
|
234
328
|
current += char
|
|
235
|
-
when "
|
|
236
|
-
|
|
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
|
|
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
|
-
|
|
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+(
|
|
433
|
+
func_name = line.match(/def\s+(#{IDENTIFIER_PATTERN})/)[1]
|
|
300
434
|
|
|
301
|
-
if
|
|
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
|
|
data/lib/t_ruby/lsp_server.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
#
|
|
375
|
-
|
|
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
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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
|
-
|
|
424
|
-
|
|
389
|
+
col = (diagnostic.column || 1) - 1
|
|
390
|
+
col = 0 if col.negative?
|
|
425
391
|
|
|
426
|
-
|
|
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
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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
|
-
|
|
460
|
-
|
|
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
|
-
#
|
|
463
|
-
if
|
|
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
|
-
|
|
468
|
-
|
|
469
|
-
return type_str.split("&").map(&:strip).all? { |t| valid_type?(t) }
|
|
470
|
-
end
|
|
414
|
+
lsp_diag
|
|
415
|
+
end
|
|
471
416
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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
|
-
|
|
421
|
+
uri.sub(%r{^file://}, "")
|
|
479
422
|
end
|
|
480
423
|
|
|
481
424
|
def create_diagnostic(line, message, severity)
|