• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

type-ruby / t-ruby / 20569579414

29 Dec 2025 09:31AM UTC coverage: 80.005% (+0.9%) from 79.076%
20569579414

Pull #30

github

web-flow
Merge 6db8c3a50 into a7c451da7
Pull Request #30: feat: improve error messages with tsc-style diagnostics

525 of 636 new or added lines in 14 files covered. (82.55%)

10 existing lines in 4 files now uncovered.

7110 of 8887 relevant lines covered (80.0%)

896.37 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

86.2
/lib/t_ruby/parser_combinator/token/token_declaration_parser.rb
1
# frozen_string_literal: true
2

3
module TRuby
1✔
4
  module ParserCombinator
1✔
5
    # Token Declaration Parser - Parse top-level declarations
6
    class TokenDeclarationParser
1✔
7
      include TokenDSL
1✔
8

9
      # Parse error with location info
10
      class ParseError
1✔
11
        attr_reader :message, :line, :column, :token
1✔
12

13
        def initialize(message, token: nil)
1✔
14
          @message = message
17✔
15
          @token = token
17✔
16
          @line = token&.line || 1
17✔
17
          @column = token&.column || 1
17✔
18
        end
19

20
        def to_s
1✔
NEW
21
          "Line #{@line}, Column #{@column}: #{@message}"
×
22
        end
23
      end
24

25
      attr_reader :errors
1✔
26

27
      def initialize
1✔
28
        @statement_parser = StatementParser.new
84✔
29
        @expression_parser = ExpressionParser.new
84✔
30
        @errors = []
84✔
31
      end
32

33
      def parse_declaration(tokens, position = 0)
1✔
34
        return TokenParseResult.failure("End of input", tokens, position) if position >= tokens.length
92✔
35

36
        position = skip_newlines(tokens, position)
92✔
37
        return TokenParseResult.failure("End of input", tokens, position) if position >= tokens.length
92✔
38

39
        token = tokens[position]
92✔
40

41
        case token.type
92✔
42
        when :def
43
          parse_method_def(tokens, position)
71✔
44
        when :public, :private, :protected
45
          parse_visibility_method(tokens, position)
2✔
46
        when :class
47
          parse_class(tokens, position)
5✔
48
        when :module
49
          parse_module(tokens, position)
2✔
50
        when :type
51
          parse_type_alias(tokens, position)
7✔
52
        when :interface
53
          parse_interface(tokens, position)
5✔
54
        else
55
          TokenParseResult.failure("Expected declaration, got #{token.type}", tokens, position)
×
56
        end
57
      end
58

59
      def parse_program(tokens, position = 0)
1✔
60
        declarations = []
68✔
61
        @errors = []
68✔
62

63
        loop do
68✔
64
          position = skip_newlines(tokens, position)
149✔
65
          break if position >= tokens.length
149✔
66
          break if tokens[position].type == :eof
125✔
67

68
          token = tokens[position]
81✔
69

70
          # Check if this looks like a declaration keyword
71
          unless declaration_keyword?(token.type)
81✔
72
            # Not a declaration - skip to next line (top-level expression is allowed)
73
            position = skip_to_next_line(tokens, position)
10✔
74
            next
10✔
75
          end
76

77
          result = parse_declaration(tokens, position)
71✔
78

79
          if result.failure?
71✔
80
            # Collect error and try to recover
81
            # Use result.position for accurate error location (where the error actually occurred)
82
            error_pos = result.position
17✔
83
            error_token = tokens[error_pos] if error_pos < tokens.length
17✔
84
            @errors << ParseError.new(result.error, token: error_token)
17✔
85

86
            # Try to skip to next declaration (find next 'def', 'class', etc.)
87
            position = skip_to_next_declaration(tokens, position)
17✔
88
            next
17✔
89
          end
90

91
          declarations << result.value
54✔
92
          position = result.position
54✔
93
        end
94

95
        program = IR::Program.new(declarations: declarations)
68✔
96
        TokenParseResult.success(program, tokens, position)
68✔
97
      end
98

99
      # Check if parsing encountered any errors
100
      def has_errors?
1✔
101
        !@errors.empty?
67✔
102
      end
103

104
      private
1✔
105

106
      def skip_newlines(tokens, position)
1✔
107
        position += 1 while position < tokens.length && %i[newline comment].include?(tokens[position].type)
452✔
108
        position
452✔
109
      end
110

111
      # Check if token type is a declaration keyword
112
      def declaration_keyword?(type)
1✔
113
        %i[def class module type interface public private protected].include?(type)
81✔
114
      end
115

116
      # Skip to the next line (for top-level expressions)
117
      def skip_to_next_line(tokens, position)
1✔
118
        while position < tokens.length
10✔
119
          break if tokens[position].type == :newline
31✔
120

121
          position += 1
31✔
122
        end
123
        position += 1 if position < tokens.length # skip the newline itself
10✔
124
        position
10✔
125
      end
126

127
      # Skip to the next top-level declaration keyword for error recovery
128
      def skip_to_next_declaration(tokens, position)
1✔
129
        declaration_keywords = %i[def class module type interface public private protected]
17✔
130

131
        # First, skip the current token
132
        position += 1
17✔
133

134
        while position < tokens.length
17✔
135
          token = tokens[position]
182✔
136

137
          # Found a declaration keyword at start of line (or after newline)
138
          if declaration_keywords.include?(token.type)
182✔
139
            # Check if this is at start of a logical line
140
            prev_token = tokens[position - 1] if position.positive?
3✔
141
            if prev_token.nil? || prev_token.type == :newline
3✔
142
              return position
3✔
143
            end
144
          end
145

146
          # Skip to next line if we hit newline
147
          if token.type == :newline
179✔
148
            position += 1
43✔
149
            # Skip comments and blank lines
150
            position = skip_newlines(tokens, position)
43✔
151
            next
43✔
152
          end
153

154
          position += 1
136✔
155
        end
156

157
        position
14✔
158
      end
159

160
      def parse_method_def(tokens, position, visibility: :public)
1✔
161
        # Capture def token's location before consuming
162
        def_token = tokens[position]
73✔
163
        def_line = def_token.line
73✔
164
        def_column = def_token.column
73✔
165

166
        position += 1 # consume 'def'
73✔
167

168
        # Parse method name (identifier or operator)
169
        return TokenParseResult.failure("Expected method name", tokens, position) if position >= tokens.length
73✔
170

171
        method_name = tokens[position].value
73✔
172
        position += 1
73✔
173

174
        # Parse parameters
175
        params = []
73✔
176
        if position < tokens.length && tokens[position].type == :lparen
73✔
177
          position += 1 # consume (
61✔
178

179
          # Parse parameter list
180
          unless tokens[position].type == :rparen
61✔
181
            loop do
36✔
182
              param_result = parse_parameter(tokens, position)
40✔
183
              return param_result if param_result.failure?
40✔
184

185
              # Handle keyword args group which returns an array
186
              if param_result.value.is_a?(Array)
40✔
187
                params.concat(param_result.value)
3✔
188
              else
189
                params << param_result.value
37✔
190
              end
191
              position = param_result.position
40✔
192

193
              break unless tokens[position]&.type == :comma
40✔
194

195
              position += 1
4✔
196
            end
197
          end
198

199
          return TokenParseResult.failure("Expected ')'", tokens, position) unless tokens[position]&.type == :rparen
61✔
200

201
          position += 1
57✔
202
        end
203

204
        # Parse return type
205
        return_type = nil
69✔
206
        if position < tokens.length && tokens[position].type == :colon
69✔
207
          colon_token = tokens[position]
57✔
208

209
          # Check: no space allowed before colon (method name or ) must be adjacent to :)
210
          prev_token = tokens[position - 1]
57✔
211
          if prev_token && prev_token.end_pos < colon_token.start_pos
57✔
212
            return TokenParseResult.failure(
9✔
213
              "No space allowed before ':' for return type annotation",
214
              tokens,
215
              position
216
            )
217
          end
218

219
          position += 1
48✔
220

221
          # Check: space required after colon before type name
222
          if position < tokens.length
48✔
223
            type_token = tokens[position]
48✔
224
            if colon_token.end_pos == type_token.start_pos
48✔
NEW
225
              return TokenParseResult.failure(
×
226
                "Space required after ':' before return type",
227
                tokens,
228
                position
229
              )
230
            end
231
          end
232

233
          type_result = parse_type(tokens, position)
48✔
234
          return type_result if type_result.failure?
48✔
235

236
          return_type = type_result.value
48✔
237
          position = type_result.position
48✔
238
        elsif position < tokens.length && tokens[position].type == :symbol
12✔
239
          # Handle case where :TypeName was scanned as a symbol (no space after colon)
240
          # In method definition context, this is a syntax error
241
          symbol_token = tokens[position]
4✔
242
          type_name = symbol_token.value[1..] # Remove leading ':'
4✔
243

244
          # Only if it looks like a type name (starts with uppercase)
245
          if type_name =~ /^[A-Z]/
4✔
246
            # Check: no space allowed before colon
247
            prev_token = tokens[position - 1]
4✔
248
            if prev_token && prev_token.end_pos < symbol_token.start_pos
4✔
NEW
249
              return TokenParseResult.failure(
×
250
                "No space allowed before ':' for return type annotation",
251
                tokens,
252
                position
253
              )
254
            end
255

256
            # Error: space required after colon
257
            return TokenParseResult.failure(
4✔
258
              "Space required after ':' before return type",
259
              tokens,
260
              position
261
            )
262
          end
263
        end
264

265
        position = skip_newlines(tokens, position)
56✔
266

267
        # Parse body
268
        body_result = @statement_parser.parse_block(tokens, position)
56✔
269
        position = body_result.position
56✔
270
        position = skip_newlines(tokens, position)
56✔
271

272
        # Expect 'end'
273
        if position < tokens.length && tokens[position].type == :end
56✔
274
          position += 1
56✔
275
        end
276

277
        node = IR::MethodDef.new(
56✔
278
          name: method_name,
279
          params: params,
280
          return_type: return_type,
281
          body: body_result.value,
282
          visibility: visibility,
283
          location: "#{def_line}:#{def_column}"
284
        )
285
        TokenParseResult.success(node, tokens, position)
56✔
286
      end
287

288
      def parse_visibility_method(tokens, position)
1✔
289
        visibility = tokens[position].type
2✔
290
        position += 1
2✔
291

292
        if position < tokens.length && tokens[position].type == :def
2✔
293
          parse_method_def(tokens, position, visibility: visibility)
2✔
294
        else
295
          TokenParseResult.failure("Expected 'def' after visibility modifier", tokens, position)
×
296
        end
297
      end
298

299
      def parse_parameter(tokens, position)
1✔
300
        return TokenParseResult.failure("Expected parameter", tokens, position) if position >= tokens.length
40✔
301

302
        # Check for different parameter types
303
        case tokens[position].type
40✔
304
        when :lbrace
305
          # Keyword args group: { name: Type, age: Type = default }
306
          return parse_keyword_args_group(tokens, position)
3✔
307

308
        when :star
309
          # Splat parameter *args
310
          position += 1
×
NEW
311
          return TokenParseResult.failure("Expected parameter name after *", tokens, position) if position >= tokens.length
×
312

313
          name = tokens[position].value
×
314
          position += 1
×
315

316
          # Check for type annotation: *args: Type
NEW
317
          type_annotation = nil
×
NEW
318
          if position < tokens.length && tokens[position].type == :colon
×
NEW
319
            position += 1
×
NEW
320
            type_result = parse_type(tokens, position)
×
NEW
321
            return type_result if type_result.failure?
×
322

NEW
323
            type_annotation = type_result.value
×
NEW
324
            position = type_result.position
×
325
          end
326

NEW
327
          param = IR::Parameter.new(name: name, kind: :rest, type_annotation: type_annotation)
×
UNCOV
328
          return TokenParseResult.success(param, tokens, position)
×
329

330
        when :star_star
331
          # Double splat **opts or **opts: Type
332
          position += 1
1✔
333
          return TokenParseResult.failure("Expected parameter name after **", tokens, position) if position >= tokens.length
1✔
334

335
          name = tokens[position].value
1✔
336
          position += 1
1✔
337

338
          # Check for type annotation: **opts: Type
339
          type_annotation = nil
1✔
340
          if position < tokens.length && tokens[position].type == :colon
1✔
341
            position += 1
1✔
342
            type_result = parse_type(tokens, position)
1✔
343
            return type_result if type_result.failure?
1✔
344

345
            type_annotation = type_result.value
1✔
346
            position = type_result.position
1✔
347
          end
348

349
          param = IR::Parameter.new(name: name, kind: :keyrest, type_annotation: type_annotation)
1✔
350
          return TokenParseResult.success(param, tokens, position)
1✔
351

352
        when :amp
353
          # Block parameter &block or &block: Type
354
          position += 1
×
NEW
355
          return TokenParseResult.failure("Expected parameter name after &", tokens, position) if position >= tokens.length
×
356

357
          name = tokens[position].value
×
358
          position += 1
×
359

360
          # Check for type annotation: &block: Type
NEW
361
          type_annotation = nil
×
NEW
362
          if position < tokens.length && tokens[position].type == :colon
×
NEW
363
            position += 1
×
NEW
364
            type_result = parse_type(tokens, position)
×
NEW
365
            return type_result if type_result.failure?
×
366

NEW
367
            type_annotation = type_result.value
×
NEW
368
            position = type_result.position
×
369
          end
370

NEW
371
          param = IR::Parameter.new(name: name, kind: :block, type_annotation: type_annotation)
×
UNCOV
372
          return TokenParseResult.success(param, tokens, position)
×
373
        end
374

375
        # Regular parameter: name or name: Type or name: Type = default
376
        name = tokens[position].value
36✔
377
        position += 1
36✔
378

379
        type_annotation = nil
36✔
380
        default_value = nil
36✔
381

382
        if position < tokens.length && tokens[position].type == :colon
36✔
383
          position += 1
34✔
384

385
          # Check if next token is a type (constant/identifier) or a default value
386
          if position < tokens.length
34✔
387
            type_result = parse_type(tokens, position)
34✔
388
            return type_result if type_result.failure?
34✔
389

390
            type_annotation = type_result.value
34✔
391
            position = type_result.position
34✔
392
          end
393
        end
394

395
        # Check for default value: = expression
396
        if position < tokens.length && tokens[position].type == :eq
36✔
NEW
397
          position += 1
×
398
          # Skip the default value expression (parse until comma, rparen, or newline)
NEW
399
          position = skip_default_value(tokens, position)
×
NEW
400
          default_value = true # Just mark that there's a default value
×
401
        end
402

403
        kind = default_value ? :optional : :required
36✔
404
        param = IR::Parameter.new(name: name, type_annotation: type_annotation, default_value: default_value, kind: kind)
36✔
405
        TokenParseResult.success(param, tokens, position)
36✔
406
      end
407

408
      # Parse keyword args group: { name: Type, age: Type = default } or { name:, age: default }: InterfaceName
409
      def parse_keyword_args_group(tokens, position)
1✔
410
        position += 1 # consume '{'
3✔
411

412
        params = []
3✔
413
        while position < tokens.length && tokens[position].type != :rbrace
3✔
414
          # Skip newlines inside braces
415
          position = skip_newlines(tokens, position)
6✔
416
          break if position >= tokens.length || tokens[position].type == :rbrace
6✔
417

418
          # Parse each keyword arg: name: Type or name: Type = default or name: or name: default
419
          return TokenParseResult.failure("Expected parameter name", tokens, position) unless tokens[position].type == :identifier
6✔
420

421
          name = tokens[position].value
6✔
422
          position += 1
6✔
423

424
          type_annotation = nil
6✔
425
          default_value = nil
6✔
426

427
          if position < tokens.length && tokens[position].type == :colon
6✔
428
            position += 1
6✔
429

430
            # Check what follows the colon
431
            if position < tokens.length
6✔
432
              next_token = tokens[position]
6✔
433

434
              # If it's a type (constant), parse the type
435
              if next_token.type == :constant
6✔
436
                type_result = parse_type(tokens, position)
4✔
437
                unless type_result.failure?
4✔
438
                  type_annotation = type_result.value
4✔
439
                  position = type_result.position
4✔
440
                end
441
              elsif next_token.type != :comma && next_token.type != :rbrace && next_token.type != :newline
2✔
442
                # Ruby-style default value (without =): name: default_value
443
                # e.g., { name:, limit: 10 }: InterfaceName
444
                position = skip_default_value_in_braces(tokens, position)
1✔
445
                default_value = true
1✔
446
              end
447
              # If next_token is comma/rbrace/newline, it's shorthand `name:` with no type or default
448
            end
449
          end
450

451
          # Check for default value: = expression (T-Ruby style with equals sign)
452
          if position < tokens.length && tokens[position].type == :eq
6✔
453
            position += 1
2✔
454
            position = skip_default_value_in_braces(tokens, position)
2✔
455
            default_value = true
2✔
456
          end
457

458
          params << IR::Parameter.new(name: name, type_annotation: type_annotation, default_value: default_value, kind: :keyword)
6✔
459

460
          # Skip comma
461
          if position < tokens.length && tokens[position].type == :comma
6✔
462
            position += 1
3✔
463
          end
464

465
          position = skip_newlines(tokens, position)
6✔
466
        end
467

468
        return TokenParseResult.failure("Expected '}'", tokens, position) unless position < tokens.length && tokens[position].type == :rbrace
3✔
469

470
        position += 1 # consume '}'
3✔
471

472
        # Check for interface type annotation: { ... }: InterfaceName
473
        interface_type = nil
3✔
474
        if position < tokens.length && tokens[position].type == :colon
3✔
475
          position += 1
1✔
476
          type_result = parse_type(tokens, position)
1✔
477
          unless type_result.failure?
1✔
478
            interface_type = type_result.value
1✔
479
            position = type_result.position
1✔
480
          end
481
        end
482

483
        # If there's an interface type, set it as interface_ref for each param
484
        if interface_type
3✔
485
          params.each { |p| p.interface_ref = interface_type }
3✔
486
        end
487

488
        # Return the array of keyword params wrapped in a result
489
        # We'll handle this specially in parse_method_def
490
        TokenParseResult.success(params, tokens, position)
3✔
491
      end
492

493
      # Skip a default value expression (until comma, rparen, or newline)
494
      def skip_default_value(tokens, position)
1✔
NEW
495
        depth = 0
×
NEW
496
        while position < tokens.length
×
NEW
497
          token = tokens[position]
×
NEW
498
          case token.type
×
499
          when :lparen, :lbracket, :lbrace
NEW
500
            depth += 1
×
501
          when :rparen
NEW
502
            return position if depth.zero?
×
503

NEW
504
            depth -= 1
×
505
          when :rbracket, :rbrace
NEW
506
            depth -= 1
×
507
          when :comma
NEW
508
            return position if depth.zero?
×
509
          when :newline
NEW
510
            return position if depth.zero?
×
511
          end
NEW
512
          position += 1
×
513
        end
NEW
514
        position
×
515
      end
516

517
      # Skip a default value expression inside braces (until comma, rbrace, or newline)
518
      def skip_default_value_in_braces(tokens, position)
1✔
519
        depth = 0
4✔
520
        while position < tokens.length
4✔
521
          token = tokens[position]
8✔
522
          case token.type
8✔
523
          when :lparen, :lbracket
NEW
524
            depth += 1
×
525
          when :rparen, :rbracket
NEW
526
            depth -= 1
×
527
          when :lbrace
NEW
528
            depth += 1
×
529
          when :rbrace
530
            return position if depth.zero?
4✔
531

NEW
532
            depth -= 1
×
533
          when :comma
NEW
534
            return position if depth.zero?
×
535
          when :newline
NEW
536
            return position if depth.zero?
×
537
          end
538
          position += 1
4✔
539
        end
NEW
540
        position
×
541
      end
542

543
      def parse_class(tokens, position)
1✔
544
        position += 1 # consume 'class'
5✔
545

546
        # Parse class name
547
        return TokenParseResult.failure("Expected class name", tokens, position) if position >= tokens.length
5✔
548

549
        class_name = tokens[position].value
5✔
550
        position += 1
5✔
551

552
        # Check for superclass
553
        superclass = nil
5✔
554
        if position < tokens.length && tokens[position].type == :lt
5✔
555
          position += 1
1✔
556
          superclass = tokens[position].value
1✔
557
          position += 1
1✔
558
        end
559

560
        position = skip_newlines(tokens, position)
5✔
561

562
        # Parse class body (methods and instance variables)
563
        body = []
5✔
564
        instance_vars = []
5✔
565

566
        loop do
5✔
567
          position = skip_newlines(tokens, position)
13✔
568
          break if position >= tokens.length
13✔
569
          break if tokens[position].type == :end
13✔
570

571
          if tokens[position].type == :ivar && tokens[position + 1]&.type == :colon
8✔
572
            # Instance variable declaration: @name: Type
573
            ivar_result = parse_instance_var_decl(tokens, position)
4✔
574
            return ivar_result if ivar_result.failure?
4✔
575

576
            instance_vars << ivar_result.value
4✔
577
            position = ivar_result.position
4✔
578
          elsif %i[def public private protected].include?(tokens[position].type)
4✔
579
            method_result = parse_declaration(tokens, position)
4✔
580
            return method_result if method_result.failure?
4✔
581

582
            body << method_result.value
4✔
583
            position = method_result.position
4✔
584
          else
585
            break
×
586
          end
587
        end
588

589
        # Expect 'end'
590
        if position < tokens.length && tokens[position].type == :end
5✔
591
          position += 1
5✔
592
        end
593

594
        node = IR::ClassDecl.new(
5✔
595
          name: class_name,
596
          superclass: superclass,
597
          body: body,
598
          instance_vars: instance_vars
599
        )
600
        TokenParseResult.success(node, tokens, position)
5✔
601
      end
602

603
      def parse_instance_var_decl(tokens, position)
1✔
604
        # @name: Type
605
        name = tokens[position].value[1..] # remove @ prefix
4✔
606
        position += 2 # skip @name and :
4✔
607

608
        type_result = parse_type(tokens, position)
4✔
609
        return type_result if type_result.failure?
4✔
610

611
        node = IR::InstanceVariable.new(name: name, type_annotation: type_result.value)
4✔
612
        TokenParseResult.success(node, tokens, type_result.position)
4✔
613
      end
614

615
      def parse_module(tokens, position)
1✔
616
        position += 1 # consume 'module'
2✔
617

618
        # Parse module name
619
        return TokenParseResult.failure("Expected module name", tokens, position) if position >= tokens.length
2✔
620

621
        module_name = tokens[position].value
2✔
622
        position += 1
2✔
623

624
        position = skip_newlines(tokens, position)
2✔
625

626
        # Parse module body
627
        body = []
2✔
628

629
        loop do
2✔
630
          position = skip_newlines(tokens, position)
3✔
631
          break if position >= tokens.length
3✔
632
          break if tokens[position].type == :end
3✔
633

634
          break unless %i[def public private protected].include?(tokens[position].type)
1✔
635

636
          method_result = parse_declaration(tokens, position)
1✔
637
          return method_result if method_result.failure?
1✔
638

639
          body << method_result.value
1✔
640
          position = method_result.position
1✔
641
        end
642

643
        # Expect 'end'
644
        if position < tokens.length && tokens[position].type == :end
2✔
645
          position += 1
2✔
646
        end
647

648
        node = IR::ModuleDecl.new(name: module_name, body: body)
2✔
649
        TokenParseResult.success(node, tokens, position)
2✔
650
      end
651

652
      def parse_type_alias(tokens, position)
1✔
653
        position += 1 # consume 'type'
7✔
654

655
        # Parse type name
656
        return TokenParseResult.failure("Expected type name", tokens, position) if position >= tokens.length
7✔
657

658
        type_name = tokens[position].value
7✔
659
        position += 1
7✔
660

661
        # Expect '='
662
        return TokenParseResult.failure("Expected '='", tokens, position) unless tokens[position]&.type == :eq
7✔
663

664
        position += 1
7✔
665

666
        # Parse type definition
667
        type_result = parse_type(tokens, position)
7✔
668
        return type_result if type_result.failure?
7✔
669

670
        node = IR::TypeAlias.new(name: type_name, definition: type_result.value)
7✔
671
        TokenParseResult.success(node, tokens, type_result.position)
7✔
672
      end
673

674
      def parse_interface(tokens, position)
1✔
675
        position += 1 # consume 'interface'
5✔
676

677
        # Parse interface name
678
        return TokenParseResult.failure("Expected interface name", tokens, position) if position >= tokens.length
5✔
679

680
        interface_name = tokens[position].value
5✔
681
        position += 1
5✔
682

683
        position = skip_newlines(tokens, position)
5✔
684

685
        # Parse interface members
686
        members = []
5✔
687

688
        loop do
5✔
689
          position = skip_newlines(tokens, position)
12✔
690
          break if position >= tokens.length
12✔
691
          break if tokens[position].type == :end
12✔
692

693
          member_result = parse_interface_member(tokens, position)
7✔
694
          break if member_result.failure?
7✔
695

696
          members << member_result.value
7✔
697
          position = member_result.position
7✔
698
        end
699

700
        # Expect 'end'
701
        if position < tokens.length && tokens[position].type == :end
5✔
702
          position += 1
5✔
703
        end
704

705
        node = IR::Interface.new(name: interface_name, members: members)
5✔
706
        TokenParseResult.success(node, tokens, position)
5✔
707
      end
708

709
      def parse_interface_member(tokens, position)
1✔
710
        # name: Type
711
        return TokenParseResult.failure("Expected member name", tokens, position) if position >= tokens.length
7✔
712

713
        name = tokens[position].value
7✔
714
        position += 1
7✔
715

716
        return TokenParseResult.failure("Expected ':'", tokens, position) unless tokens[position]&.type == :colon
7✔
717

718
        position += 1
7✔
719

720
        type_result = parse_type(tokens, position)
7✔
721
        return type_result if type_result.failure?
7✔
722

723
        node = IR::InterfaceMember.new(name: name, type_signature: type_result.value)
7✔
724
        TokenParseResult.success(node, tokens, type_result.position)
7✔
725
      end
726

727
      def parse_type(tokens, position)
1✔
728
        return TokenParseResult.failure("Expected type", tokens, position) if position >= tokens.length
113✔
729

730
        # Parse primary type
731
        result = parse_primary_type(tokens, position)
113✔
732
        return result if result.failure?
113✔
733

734
        type = result.value
113✔
735
        position = result.position
113✔
736

737
        # Check for union type
738
        types = [type]
113✔
739
        while position < tokens.length && tokens[position].type == :pipe
113✔
740
          position += 1
1✔
741
          next_result = parse_primary_type(tokens, position)
1✔
742
          return next_result if next_result.failure?
1✔
743

744
          types << next_result.value
1✔
745
          position = next_result.position
1✔
746
        end
747

748
        if types.length > 1
113✔
749
          node = IR::UnionType.new(types: types)
1✔
750
          TokenParseResult.success(node, tokens, position)
1✔
751
        else
752
          TokenParseResult.success(type, tokens, position)
112✔
753
        end
754
      end
755

756
      def parse_primary_type(tokens, position)
1✔
757
        return TokenParseResult.failure("Expected type", tokens, position) if position >= tokens.length
117✔
758

759
        # Check for function type: -> ReturnType
760
        if tokens[position].type == :arrow
117✔
761
          position += 1
1✔
762
          return_result = parse_primary_type(tokens, position)
1✔
763
          return return_result if return_result.failure?
1✔
764

765
          node = IR::FunctionType.new(param_types: [], return_type: return_result.value)
1✔
766
          return TokenParseResult.success(node, tokens, return_result.position)
1✔
767
        end
768

769
        # Check for tuple type: (Type, Type) -> ReturnType
770
        if tokens[position].type == :lparen
116✔
771
          position += 1
2✔
772
          param_types = []
2✔
773

774
          unless tokens[position].type == :rparen
2✔
775
            loop do
2✔
776
              type_result = parse_type(tokens, position)
2✔
777
              return type_result if type_result.failure?
2✔
778

779
              param_types << type_result.value
2✔
780
              position = type_result.position
2✔
781

782
              break unless tokens[position]&.type == :comma
2✔
783

784
              position += 1
×
785
            end
786
          end
787

788
          return TokenParseResult.failure("Expected ')'", tokens, position) unless tokens[position]&.type == :rparen
2✔
789

790
          position += 1
2✔
791

792
          # Check for function arrow
793
          if position < tokens.length && tokens[position].type == :arrow
2✔
794
            position += 1
2✔
795
            return_result = parse_primary_type(tokens, position)
2✔
796
            return return_result if return_result.failure?
2✔
797

798
            node = IR::FunctionType.new(param_types: param_types, return_type: return_result.value)
2✔
799
            return TokenParseResult.success(node, tokens, return_result.position)
2✔
800
          else
801
            node = IR::TupleType.new(element_types: param_types)
×
802
            return TokenParseResult.success(node, tokens, position)
×
803
          end
804
        end
805

806
        # Check for hash literal type: { key: Type, key2: Type }
807
        if tokens[position].type == :lbrace
114✔
808
          return parse_hash_literal_type(tokens, position)
2✔
809
        end
810

811
        # Simple type or generic type
812
        type_name = tokens[position].value
112✔
813
        position += 1
112✔
814

815
        # Check for generic arguments: Type<Args>
816
        if position < tokens.length && tokens[position].type == :lt
112✔
817
          position += 1
1✔
818
          type_args = []
1✔
819

820
          loop do
1✔
821
            arg_result = parse_type(tokens, position)
1✔
822
            return arg_result if arg_result.failure?
1✔
823

824
            type_args << arg_result.value
1✔
825
            position = arg_result.position
1✔
826

827
            break unless tokens[position]&.type == :comma
1✔
828

829
            position += 1
×
830
          end
831

832
          return TokenParseResult.failure("Expected '>'", tokens, position) unless tokens[position]&.type == :gt
1✔
833

834
          position += 1
1✔
835

836
          node = IR::GenericType.new(base: type_name, type_args: type_args)
1✔
837
          TokenParseResult.success(node, tokens, position)
1✔
838
        elsif position < tokens.length && tokens[position].type == :question
111✔
839
          # Check for nullable: Type?
840
          position += 1
×
841
          inner = IR::SimpleType.new(name: type_name)
×
842
          node = IR::NullableType.new(inner_type: inner)
×
843
          TokenParseResult.success(node, tokens, position)
×
844
        else
845
          node = IR::SimpleType.new(name: type_name)
111✔
846
          TokenParseResult.success(node, tokens, position)
111✔
847
        end
848
      end
849

850
      # Parse hash literal type: { key: Type, key2: Type }
851
      # Used for typed hash parameters like: def foo(config: { host: String, port: Integer })
852
      def parse_hash_literal_type(tokens, position)
1✔
853
        return TokenParseResult.failure("Expected '{'", tokens, position) unless tokens[position]&.type == :lbrace
2✔
854

855
        position += 1 # consume '{'
2✔
856

857
        fields = []
2✔
858
        while position < tokens.length && tokens[position].type != :rbrace
2✔
859
          # Skip newlines inside braces
860
          position = skip_newlines(tokens, position)
4✔
861
          break if position >= tokens.length || tokens[position].type == :rbrace
4✔
862

863
          # Parse field: name: Type
864
          unless tokens[position].type == :identifier
4✔
NEW
865
            return TokenParseResult.failure("Expected field name", tokens, position)
×
866
          end
867

868
          field_name = tokens[position].value
4✔
869
          position += 1
4✔
870

871
          unless tokens[position]&.type == :colon
4✔
NEW
872
            return TokenParseResult.failure("Expected ':' after field name", tokens, position)
×
873
          end
874

875
          position += 1
4✔
876

877
          type_result = parse_type(tokens, position)
4✔
878
          return type_result if type_result.failure?
4✔
879

880
          fields << { name: field_name, type: type_result.value }
4✔
881
          position = type_result.position
4✔
882

883
          # Handle optional default value (skip it for type purposes)
884
          if position < tokens.length && tokens[position].type == :eq
4✔
885
            position += 1
1✔
886
            position = skip_default_value_in_braces(tokens, position)
1✔
887
          end
888

889
          # Skip comma if present
890
          if position < tokens.length && tokens[position].type == :comma
4✔
891
            position += 1
2✔
892
          end
893
        end
894

895
        unless tokens[position]&.type == :rbrace
2✔
NEW
896
          return TokenParseResult.failure("Expected '}'", tokens, position)
×
897
        end
898

899
        position += 1 # consume '}'
2✔
900

901
        node = IR::HashLiteralType.new(fields: fields)
2✔
902
        TokenParseResult.success(node, tokens, position)
2✔
903
      end
904
    end
905
  end
906
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc