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

type-ruby / t-ruby / 20573109039

29 Dec 2025 12:40PM UTC coverage: 92.341% (+13.3%) from 79.076%
20573109039

Pull #30

github

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

571 of 640 new or added lines in 14 files covered. (89.22%)

4 existing lines in 2 files now uncovered.

8210 of 8891 relevant lines covered (92.34%)

1046.45 hits per line

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

86.32
/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
25✔
15
          @token = token
25✔
16
          @line = token&.line || 1
25✔
17
          @column = token&.column || 1
25✔
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
109✔
29
        @expression_parser = ExpressionParser.new
109✔
30
        @errors = []
109✔
31
      end
32

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

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

39
        token = tokens[position]
113✔
40

41
        case token.type
113✔
42
        when :def
43
          parse_method_def(tokens, position)
88✔
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)
8✔
52
        when :interface
53
          parse_interface(tokens, position)
8✔
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 = []
93✔
61
        @errors = []
93✔
62

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

68
          token = tokens[position]
105✔
69

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

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

79
          if result.failure?
92✔
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
25✔
83
            error_token = tokens[error_pos] if error_pos < tokens.length
25✔
84
            @errors << ParseError.new(result.error, token: error_token)
25✔
85

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

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

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

99
      # Check if parsing encountered any errors
100
      def has_errors?
1✔
101
        !@errors.empty?
92✔
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)
564✔
108
        position
564✔
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)
105✔
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
13✔
119
          break if tokens[position].type == :newline
39✔
120

121
          position += 1
39✔
122
        end
123
        position += 1 if position < tokens.length # skip the newline itself
13✔
124
        position
13✔
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]
25✔
130

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

134
        while position < tokens.length
25✔
135
          token = tokens[position]
290✔
136

137
          # Found a declaration keyword at start of line (or after newline)
138
          if declaration_keywords.include?(token.type)
290✔
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
287✔
148
            position += 1
57✔
149
            # Skip comments and blank lines
150
            position = skip_newlines(tokens, position)
57✔
151
            next
57✔
152
          end
153

154
          position += 1
230✔
155
        end
156

157
        position
22✔
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]
90✔
163
        def_line = def_token.line
90✔
164
        def_column = def_token.column
90✔
165

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

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

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

174
        # Check for unexpected tokens after method name (indicates space in method name)
175
        if position < tokens.length
90✔
176
          next_token = tokens[position]
90✔
177
          # After method name, only these are valid: ( : newline end
178
          # If we see an identifier, it means there was a space in the method name
179
          if next_token.type == :identifier
90✔
180
            return TokenParseResult.failure(
3✔
181
              "Unexpected token '#{next_token.value}' after method name '#{method_name}' - method names cannot contain spaces",
182
              tokens,
183
              position
184
            )
185
          end
186
        end
187

188
        # Parse parameters
189
        params = []
87✔
190
        if position < tokens.length && tokens[position].type == :lparen
87✔
191
          position += 1 # consume (
68✔
192

193
          # Parse parameter list
194
          unless tokens[position].type == :rparen
68✔
195
            loop do
43✔
196
              param_result = parse_parameter(tokens, position)
47✔
197
              return param_result if param_result.failure?
47✔
198

199
              # Handle keyword args group which returns an array
200
              if param_result.value.is_a?(Array)
46✔
201
                params.concat(param_result.value)
3✔
202
              else
203
                params << param_result.value
43✔
204
              end
205
              position = param_result.position
46✔
206

207
              break unless tokens[position]&.type == :comma
46✔
208

209
              position += 1
4✔
210
            end
211
          end
212

213
          return TokenParseResult.failure("Expected ')'", tokens, position) unless tokens[position]&.type == :rparen
67✔
214

215
          position += 1
59✔
216
        end
217

218
        # Parse return type
219
        return_type = nil
78✔
220
        if position < tokens.length && tokens[position].type == :colon
78✔
221
          colon_token = tokens[position]
64✔
222

223
          # Check: no space allowed before colon (method name or ) must be adjacent to :)
224
          prev_token = tokens[position - 1]
64✔
225
          if prev_token && prev_token.end_pos < colon_token.start_pos
64✔
226
            return TokenParseResult.failure(
9✔
227
              "No space allowed before ':' for return type annotation",
228
              tokens,
229
              position
230
            )
231
          end
232

233
          position += 1
55✔
234

235
          # Check: space required after colon before type name
236
          if position < tokens.length
55✔
237
            type_token = tokens[position]
55✔
238
            if colon_token.end_pos == type_token.start_pos
55✔
NEW
239
              return TokenParseResult.failure(
×
240
                "Space required after ':' before return type",
241
                tokens,
242
                position
243
              )
244
            end
245
          end
246

247
          type_result = parse_type(tokens, position)
55✔
248
          return type_result if type_result.failure?
55✔
249

250
          return_type = type_result.value
55✔
251
          position = type_result.position
55✔
252
        elsif position < tokens.length && tokens[position].type == :symbol
14✔
253
          # Handle case where :TypeName was scanned as a symbol (no space after colon)
254
          # In method definition context, this is a syntax error
255
          symbol_token = tokens[position]
4✔
256
          type_name = symbol_token.value[1..] # Remove leading ':'
4✔
257

258
          # Only if it looks like a type name (starts with uppercase)
259
          if type_name =~ /^[A-Z]/
4✔
260
            # Check: no space allowed before colon
261
            prev_token = tokens[position - 1]
4✔
262
            if prev_token && prev_token.end_pos < symbol_token.start_pos
4✔
NEW
263
              return TokenParseResult.failure(
×
264
                "No space allowed before ':' for return type annotation",
265
                tokens,
266
                position
267
              )
268
            end
269

270
            # Error: space required after colon
271
            return TokenParseResult.failure(
4✔
272
              "Space required after ':' before return type",
273
              tokens,
274
              position
275
            )
276
          end
277
        end
278

279
        position = skip_newlines(tokens, position)
65✔
280

281
        # Parse body
282
        body_result = @statement_parser.parse_block(tokens, position)
65✔
283
        position = body_result.position
65✔
284
        position = skip_newlines(tokens, position)
65✔
285

286
        # Expect 'end'
287
        if position < tokens.length && tokens[position].type == :end
65✔
288
          position += 1
63✔
289
        end
290

291
        node = IR::MethodDef.new(
65✔
292
          name: method_name,
293
          params: params,
294
          return_type: return_type,
295
          body: body_result.value,
296
          visibility: visibility,
297
          location: "#{def_line}:#{def_column}"
298
        )
299
        TokenParseResult.success(node, tokens, position)
65✔
300
      end
301

302
      def parse_visibility_method(tokens, position)
1✔
303
        visibility = tokens[position].type
2✔
304
        position += 1
2✔
305

306
        if position < tokens.length && tokens[position].type == :def
2✔
307
          parse_method_def(tokens, position, visibility: visibility)
2✔
308
        else
309
          TokenParseResult.failure("Expected 'def' after visibility modifier", tokens, position)
×
310
        end
311
      end
312

313
      def parse_parameter(tokens, position)
1✔
314
        return TokenParseResult.failure("Expected parameter", tokens, position) if position >= tokens.length
47✔
315

316
        # Check for different parameter types
317
        case tokens[position].type
47✔
318
        when :lbrace
319
          # Keyword args group: { name: Type, age: Type = default }
320
          return parse_keyword_args_group(tokens, position)
3✔
321

322
        when :star
323
          # Splat parameter *args
324
          position += 1
×
NEW
325
          return TokenParseResult.failure("Expected parameter name after *", tokens, position) if position >= tokens.length
×
326

327
          name = tokens[position].value
×
328
          position += 1
×
329

330
          # Check for type annotation: *args: Type
NEW
331
          type_annotation = nil
×
NEW
332
          if position < tokens.length && tokens[position].type == :colon
×
NEW
333
            position += 1
×
NEW
334
            type_result = parse_type(tokens, position)
×
NEW
335
            return type_result if type_result.failure?
×
336

NEW
337
            type_annotation = type_result.value
×
NEW
338
            position = type_result.position
×
339
          end
340

NEW
341
          param = IR::Parameter.new(name: name, kind: :rest, type_annotation: type_annotation)
×
UNCOV
342
          return TokenParseResult.success(param, tokens, position)
×
343

344
        when :star_star
345
          # Double splat **opts or **opts: Type
346
          position += 1
1✔
347
          return TokenParseResult.failure("Expected parameter name after **", tokens, position) if position >= tokens.length
1✔
348

349
          name = tokens[position].value
1✔
350
          position += 1
1✔
351

352
          # Check for type annotation: **opts: Type
353
          type_annotation = nil
1✔
354
          if position < tokens.length && tokens[position].type == :colon
1✔
355
            position += 1
1✔
356
            type_result = parse_type(tokens, position)
1✔
357
            return type_result if type_result.failure?
1✔
358

359
            type_annotation = type_result.value
1✔
360
            position = type_result.position
1✔
361
          end
362

363
          param = IR::Parameter.new(name: name, kind: :keyrest, type_annotation: type_annotation)
1✔
364
          return TokenParseResult.success(param, tokens, position)
1✔
365

366
        when :amp
367
          # Block parameter &block or &block: Type
368
          position += 1
×
NEW
369
          return TokenParseResult.failure("Expected parameter name after &", tokens, position) if position >= tokens.length
×
370

371
          name = tokens[position].value
×
372
          position += 1
×
373

374
          # Check for type annotation: &block: Type
NEW
375
          type_annotation = nil
×
NEW
376
          if position < tokens.length && tokens[position].type == :colon
×
NEW
377
            position += 1
×
NEW
378
            type_result = parse_type(tokens, position)
×
NEW
379
            return type_result if type_result.failure?
×
380

NEW
381
            type_annotation = type_result.value
×
NEW
382
            position = type_result.position
×
383
          end
384

NEW
385
          param = IR::Parameter.new(name: name, kind: :block, type_annotation: type_annotation)
×
UNCOV
386
          return TokenParseResult.success(param, tokens, position)
×
387
        end
388

389
        # Regular parameter: name or name: Type or name: Type = default
390
        name = tokens[position].value
43✔
391
        position += 1
43✔
392

393
        type_annotation = nil
43✔
394
        default_value = nil
43✔
395

396
        if position < tokens.length && tokens[position].type == :colon
43✔
397
          position += 1
40✔
398

399
          # Check if next token is a type (constant/identifier) or a default value
400
          if position < tokens.length
40✔
401
            type_result = parse_type(tokens, position)
40✔
402
            return type_result if type_result.failure?
40✔
403

404
            type_annotation = type_result.value
39✔
405
            position = type_result.position
39✔
406
          end
407
        end
408

409
        # Check for default value: = expression
410
        if position < tokens.length && tokens[position].type == :eq
42✔
NEW
411
          position += 1
×
412
          # Skip the default value expression (parse until comma, rparen, or newline)
NEW
413
          position = skip_default_value(tokens, position)
×
NEW
414
          default_value = true # Just mark that there's a default value
×
415
        end
416

417
        kind = default_value ? :optional : :required
42✔
418
        param = IR::Parameter.new(name: name, type_annotation: type_annotation, default_value: default_value, kind: kind)
42✔
419
        TokenParseResult.success(param, tokens, position)
42✔
420
      end
421

422
      # Parse keyword args group: { name: Type, age: Type = default } or { name:, age: default }: InterfaceName
423
      def parse_keyword_args_group(tokens, position)
1✔
424
        position += 1 # consume '{'
3✔
425

426
        params = []
3✔
427
        while position < tokens.length && tokens[position].type != :rbrace
3✔
428
          # Skip newlines inside braces
429
          position = skip_newlines(tokens, position)
6✔
430
          break if position >= tokens.length || tokens[position].type == :rbrace
6✔
431

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

435
          name = tokens[position].value
6✔
436
          position += 1
6✔
437

438
          type_annotation = nil
6✔
439
          default_value = nil
6✔
440

441
          if position < tokens.length && tokens[position].type == :colon
6✔
442
            position += 1
6✔
443

444
            # Check what follows the colon
445
            if position < tokens.length
6✔
446
              next_token = tokens[position]
6✔
447

448
              # If it's a type (constant), parse the type
449
              if next_token.type == :constant
6✔
450
                type_result = parse_type(tokens, position)
4✔
451
                unless type_result.failure?
4✔
452
                  type_annotation = type_result.value
4✔
453
                  position = type_result.position
4✔
454
                end
455
              elsif next_token.type != :comma && next_token.type != :rbrace && next_token.type != :newline
2✔
456
                # Ruby-style default value (without =): name: default_value
457
                # e.g., { name:, limit: 10 }: InterfaceName
458
                position = skip_default_value_in_braces(tokens, position)
1✔
459
                default_value = true
1✔
460
              end
461
              # If next_token is comma/rbrace/newline, it's shorthand `name:` with no type or default
462
            end
463
          end
464

465
          # Check for default value: = expression (T-Ruby style with equals sign)
466
          if position < tokens.length && tokens[position].type == :eq
6✔
467
            position += 1
2✔
468
            position = skip_default_value_in_braces(tokens, position)
2✔
469
            default_value = true
2✔
470
          end
471

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

474
          # Skip comma
475
          if position < tokens.length && tokens[position].type == :comma
6✔
476
            position += 1
3✔
477
          end
478

479
          position = skip_newlines(tokens, position)
6✔
480
        end
481

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

484
        position += 1 # consume '}'
3✔
485

486
        # Check for interface type annotation: { ... }: InterfaceName
487
        interface_type = nil
3✔
488
        if position < tokens.length && tokens[position].type == :colon
3✔
489
          position += 1
1✔
490
          type_result = parse_type(tokens, position)
1✔
491
          unless type_result.failure?
1✔
492
            interface_type = type_result.value
1✔
493
            position = type_result.position
1✔
494
          end
495
        end
496

497
        # If there's an interface type, set it as interface_ref for each param
498
        if interface_type
3✔
499
          params.each { |p| p.interface_ref = interface_type }
3✔
500
        end
501

502
        # Return the array of keyword params wrapped in a result
503
        # We'll handle this specially in parse_method_def
504
        TokenParseResult.success(params, tokens, position)
3✔
505
      end
506

507
      # Skip a default value expression (until comma, rparen, or newline)
508
      def skip_default_value(tokens, position)
1✔
NEW
509
        depth = 0
×
NEW
510
        while position < tokens.length
×
NEW
511
          token = tokens[position]
×
NEW
512
          case token.type
×
513
          when :lparen, :lbracket, :lbrace
NEW
514
            depth += 1
×
515
          when :rparen
NEW
516
            return position if depth.zero?
×
517

NEW
518
            depth -= 1
×
519
          when :rbracket, :rbrace
NEW
520
            depth -= 1
×
521
          when :comma
NEW
522
            return position if depth.zero?
×
523
          when :newline
NEW
524
            return position if depth.zero?
×
525
          end
NEW
526
          position += 1
×
527
        end
NEW
528
        position
×
529
      end
530

531
      # Skip a default value expression inside braces (until comma, rbrace, or newline)
532
      def skip_default_value_in_braces(tokens, position)
1✔
533
        depth = 0
4✔
534
        while position < tokens.length
4✔
535
          token = tokens[position]
8✔
536
          case token.type
8✔
537
          when :lparen, :lbracket
NEW
538
            depth += 1
×
539
          when :rparen, :rbracket
NEW
540
            depth -= 1
×
541
          when :lbrace
NEW
542
            depth += 1
×
543
          when :rbrace
544
            return position if depth.zero?
4✔
545

NEW
546
            depth -= 1
×
547
          when :comma
NEW
548
            return position if depth.zero?
×
549
          when :newline
NEW
550
            return position if depth.zero?
×
551
          end
552
          position += 1
4✔
553
        end
NEW
554
        position
×
555
      end
556

557
      def parse_class(tokens, position)
1✔
558
        position += 1 # consume 'class'
5✔
559

560
        # Parse class name
561
        return TokenParseResult.failure("Expected class name", tokens, position) if position >= tokens.length
5✔
562

563
        class_name = tokens[position].value
5✔
564
        position += 1
5✔
565

566
        # Check for superclass
567
        superclass = nil
5✔
568
        if position < tokens.length && tokens[position].type == :lt
5✔
569
          position += 1
1✔
570
          superclass = tokens[position].value
1✔
571
          position += 1
1✔
572
        end
573

574
        position = skip_newlines(tokens, position)
5✔
575

576
        # Parse class body (methods and instance variables)
577
        body = []
5✔
578
        instance_vars = []
5✔
579

580
        loop do
5✔
581
          position = skip_newlines(tokens, position)
13✔
582
          break if position >= tokens.length
13✔
583
          break if tokens[position].type == :end
13✔
584

585
          if tokens[position].type == :ivar && tokens[position + 1]&.type == :colon
8✔
586
            # Instance variable declaration: @name: Type
587
            ivar_result = parse_instance_var_decl(tokens, position)
4✔
588
            return ivar_result if ivar_result.failure?
4✔
589

590
            instance_vars << ivar_result.value
4✔
591
            position = ivar_result.position
4✔
592
          elsif %i[def public private protected].include?(tokens[position].type)
4✔
593
            method_result = parse_declaration(tokens, position)
4✔
594
            return method_result if method_result.failure?
4✔
595

596
            body << method_result.value
4✔
597
            position = method_result.position
4✔
598
          else
599
            break
×
600
          end
601
        end
602

603
        # Expect 'end'
604
        if position < tokens.length && tokens[position].type == :end
5✔
605
          position += 1
5✔
606
        end
607

608
        node = IR::ClassDecl.new(
5✔
609
          name: class_name,
610
          superclass: superclass,
611
          body: body,
612
          instance_vars: instance_vars
613
        )
614
        TokenParseResult.success(node, tokens, position)
5✔
615
      end
616

617
      def parse_instance_var_decl(tokens, position)
1✔
618
        # @name: Type
619
        name = tokens[position].value[1..] # remove @ prefix
4✔
620
        position += 2 # skip @name and :
4✔
621

622
        type_result = parse_type(tokens, position)
4✔
623
        return type_result if type_result.failure?
4✔
624

625
        node = IR::InstanceVariable.new(name: name, type_annotation: type_result.value)
4✔
626
        TokenParseResult.success(node, tokens, type_result.position)
4✔
627
      end
628

629
      def parse_module(tokens, position)
1✔
630
        position += 1 # consume 'module'
2✔
631

632
        # Parse module name
633
        return TokenParseResult.failure("Expected module name", tokens, position) if position >= tokens.length
2✔
634

635
        module_name = tokens[position].value
2✔
636
        position += 1
2✔
637

638
        position = skip_newlines(tokens, position)
2✔
639

640
        # Parse module body
641
        body = []
2✔
642

643
        loop do
2✔
644
          position = skip_newlines(tokens, position)
3✔
645
          break if position >= tokens.length
3✔
646
          break if tokens[position].type == :end
3✔
647

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

650
          method_result = parse_declaration(tokens, position)
1✔
651
          return method_result if method_result.failure?
1✔
652

653
          body << method_result.value
1✔
654
          position = method_result.position
1✔
655
        end
656

657
        # Expect 'end'
658
        if position < tokens.length && tokens[position].type == :end
2✔
659
          position += 1
2✔
660
        end
661

662
        node = IR::ModuleDecl.new(name: module_name, body: body)
2✔
663
        TokenParseResult.success(node, tokens, position)
2✔
664
      end
665

666
      def parse_type_alias(tokens, position)
1✔
667
        position += 1 # consume 'type'
8✔
668

669
        # Parse type name
670
        return TokenParseResult.failure("Expected type name", tokens, position) if position >= tokens.length
8✔
671

672
        type_name = tokens[position].value
8✔
673
        position += 1
8✔
674

675
        # Expect '='
676
        return TokenParseResult.failure("Expected '='", tokens, position) unless tokens[position]&.type == :eq
8✔
677

678
        position += 1
8✔
679

680
        # Parse type definition
681
        type_result = parse_type(tokens, position)
8✔
682
        return type_result if type_result.failure?
8✔
683

684
        node = IR::TypeAlias.new(name: type_name, definition: type_result.value)
8✔
685
        TokenParseResult.success(node, tokens, type_result.position)
8✔
686
      end
687

688
      def parse_interface(tokens, position)
1✔
689
        position += 1 # consume 'interface'
8✔
690

691
        # Parse interface name
692
        return TokenParseResult.failure("Expected interface name", tokens, position) if position >= tokens.length
8✔
693

694
        interface_name = tokens[position].value
8✔
695
        position += 1
8✔
696

697
        position = skip_newlines(tokens, position)
8✔
698

699
        # Parse interface members
700
        members = []
8✔
701

702
        loop do
8✔
703
          position = skip_newlines(tokens, position)
19✔
704
          break if position >= tokens.length
19✔
705
          break if tokens[position].type == :end
19✔
706

707
          member_result = parse_interface_member(tokens, position)
11✔
708
          break if member_result.failure?
11✔
709

710
          members << member_result.value
11✔
711
          position = member_result.position
11✔
712
        end
713

714
        # Expect 'end'
715
        if position < tokens.length && tokens[position].type == :end
8✔
716
          position += 1
8✔
717
        end
718

719
        node = IR::Interface.new(name: interface_name, members: members)
8✔
720
        TokenParseResult.success(node, tokens, position)
8✔
721
      end
722

723
      def parse_interface_member(tokens, position)
1✔
724
        # name: Type
725
        return TokenParseResult.failure("Expected member name", tokens, position) if position >= tokens.length
11✔
726

727
        name = tokens[position].value
11✔
728
        position += 1
11✔
729

730
        return TokenParseResult.failure("Expected ':'", tokens, position) unless tokens[position]&.type == :colon
11✔
731

732
        position += 1
11✔
733

734
        type_result = parse_type(tokens, position)
11✔
735
        return type_result if type_result.failure?
11✔
736

737
        node = IR::InterfaceMember.new(name: name, type_signature: type_result.value)
11✔
738
        TokenParseResult.success(node, tokens, type_result.position)
11✔
739
      end
740

741
      def parse_type(tokens, position)
1✔
742
        return TokenParseResult.failure("Expected type", tokens, position) if position >= tokens.length
132✔
743

744
        # Parse primary type
745
        result = parse_primary_type(tokens, position)
132✔
746
        return result if result.failure?
132✔
747

748
        type = result.value
131✔
749
        position = result.position
131✔
750

751
        # Check for union type
752
        types = [type]
131✔
753
        while position < tokens.length && tokens[position].type == :pipe
131✔
754
          position += 1
2✔
755
          next_result = parse_primary_type(tokens, position)
2✔
756
          return next_result if next_result.failure?
2✔
757

758
          types << next_result.value
2✔
759
          position = next_result.position
2✔
760
        end
761

762
        if types.length > 1
131✔
763
          node = IR::UnionType.new(types: types)
2✔
764
          TokenParseResult.success(node, tokens, position)
2✔
765
        else
766
          TokenParseResult.success(type, tokens, position)
129✔
767
        end
768
      end
769

770
      def parse_primary_type(tokens, position)
1✔
771
        return TokenParseResult.failure("Expected type", tokens, position) if position >= tokens.length
137✔
772

773
        # Check for function type: -> ReturnType
774
        if tokens[position].type == :arrow
137✔
775
          position += 1
1✔
776
          return_result = parse_primary_type(tokens, position)
1✔
777
          return return_result if return_result.failure?
1✔
778

779
          node = IR::FunctionType.new(param_types: [], return_type: return_result.value)
1✔
780
          return TokenParseResult.success(node, tokens, return_result.position)
1✔
781
        end
782

783
        # Check for tuple type: (Type, Type) -> ReturnType
784
        if tokens[position].type == :lparen
136✔
785
          position += 1
2✔
786
          param_types = []
2✔
787

788
          unless tokens[position].type == :rparen
2✔
789
            loop do
2✔
790
              type_result = parse_type(tokens, position)
2✔
791
              return type_result if type_result.failure?
2✔
792

793
              param_types << type_result.value
2✔
794
              position = type_result.position
2✔
795

796
              break unless tokens[position]&.type == :comma
2✔
797

798
              position += 1
×
799
            end
800
          end
801

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

804
          position += 1
2✔
805

806
          # Check for function arrow
807
          if position < tokens.length && tokens[position].type == :arrow
2✔
808
            position += 1
2✔
809
            return_result = parse_primary_type(tokens, position)
2✔
810
            return return_result if return_result.failure?
2✔
811

812
            node = IR::FunctionType.new(param_types: param_types, return_type: return_result.value)
2✔
813
            return TokenParseResult.success(node, tokens, return_result.position)
2✔
814
          else
815
            node = IR::TupleType.new(element_types: param_types)
×
816
            return TokenParseResult.success(node, tokens, position)
×
817
          end
818
        end
819

820
        # Check for hash literal type: { key: Type, key2: Type }
821
        if tokens[position].type == :lbrace
134✔
822
          return parse_hash_literal_type(tokens, position)
2✔
823
        end
824

825
        # Simple type or generic type
826
        type_name = tokens[position].value
132✔
827
        position += 1
132✔
828

829
        # Check for generic arguments: Type<Args>
830
        if position < tokens.length && tokens[position].type == :lt
132✔
831
          position += 1
2✔
832
          type_args = []
2✔
833

834
          loop do
2✔
835
            arg_result = parse_type(tokens, position)
2✔
836
            return arg_result if arg_result.failure?
2✔
837

838
            type_args << arg_result.value
2✔
839
            position = arg_result.position
2✔
840

841
            break unless tokens[position]&.type == :comma
2✔
842

843
            position += 1
×
844
          end
845

846
          return TokenParseResult.failure("Expected '>'", tokens, position) unless tokens[position]&.type == :gt
2✔
847

848
          position += 1
1✔
849

850
          node = IR::GenericType.new(base: type_name, type_args: type_args)
1✔
851
          TokenParseResult.success(node, tokens, position)
1✔
852
        elsif position < tokens.length && tokens[position].type == :question
130✔
853
          # Check for nullable: Type?
854
          position += 1
×
855
          inner = IR::SimpleType.new(name: type_name)
×
856
          node = IR::NullableType.new(inner_type: inner)
×
857
          TokenParseResult.success(node, tokens, position)
×
858
        else
859
          node = IR::SimpleType.new(name: type_name)
130✔
860
          TokenParseResult.success(node, tokens, position)
130✔
861
        end
862
      end
863

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

869
        position += 1 # consume '{'
2✔
870

871
        fields = []
2✔
872
        while position < tokens.length && tokens[position].type != :rbrace
2✔
873
          # Skip newlines inside braces
874
          position = skip_newlines(tokens, position)
4✔
875
          break if position >= tokens.length || tokens[position].type == :rbrace
4✔
876

877
          # Parse field: name: Type
878
          unless tokens[position].type == :identifier
4✔
NEW
879
            return TokenParseResult.failure("Expected field name", tokens, position)
×
880
          end
881

882
          field_name = tokens[position].value
4✔
883
          position += 1
4✔
884

885
          unless tokens[position]&.type == :colon
4✔
NEW
886
            return TokenParseResult.failure("Expected ':' after field name", tokens, position)
×
887
          end
888

889
          position += 1
4✔
890

891
          type_result = parse_type(tokens, position)
4✔
892
          return type_result if type_result.failure?
4✔
893

894
          fields << { name: field_name, type: type_result.value }
4✔
895
          position = type_result.position
4✔
896

897
          # Handle optional default value (skip it for type purposes)
898
          if position < tokens.length && tokens[position].type == :eq
4✔
899
            position += 1
1✔
900
            position = skip_default_value_in_braces(tokens, position)
1✔
901
          end
902

903
          # Skip comma if present
904
          if position < tokens.length && tokens[position].type == :comma
4✔
905
            position += 1
2✔
906
          end
907
        end
908

909
        unless tokens[position]&.type == :rbrace
2✔
NEW
910
          return TokenParseResult.failure("Expected '}'", tokens, position)
×
911
        end
912

913
        position += 1 # consume '}'
2✔
914

915
        node = IR::HashLiteralType.new(fields: fields)
2✔
916
        TokenParseResult.success(node, tokens, position)
2✔
917
      end
918
    end
919
  end
920
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