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

type-ruby / t-ruby / 20560733162

28 Dec 2025 10:59PM UTC coverage: 79.076% (+1.7%) from 77.331%
20560733162

Pull #29

github

web-flow
Merge 5e12f0648 into fda099366
Pull Request #29: refactor: migrate parser from regex to token-based parser combinator

1849 of 2098 new or added lines in 53 files covered. (88.13%)

6 existing lines in 2 files now uncovered.

6644 of 8402 relevant lines covered (79.08%)

908.09 hits per line

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

90.57
/lib/t_ruby/parser_combinator/token/statement_parser.rb
1
# frozen_string_literal: true
2

3
module TRuby
1✔
4
  module ParserCombinator
1✔
5
    # Statement Parser - Parse statements into IR nodes
6
    class StatementParser
1✔
7
      include TokenDSL
1✔
8

9
      def initialize
1✔
10
        @expression_parser = ExpressionParser.new
324✔
11
      end
12

13
      def parse_statement(tokens, position = 0)
1✔
14
        return TokenParseResult.failure("End of input", tokens, position) if position >= tokens.length
3,713✔
15

16
        # Skip newlines
17
        position = skip_newlines(tokens, position)
3,713✔
18
        return TokenParseResult.failure("End of input", tokens, position) if position >= tokens.length
3,713✔
19

20
        token = tokens[position]
3,713✔
21

22
        case token.type
3,713✔
23
        when :return
24
          parse_return(tokens, position)
8✔
25
        when :if
26
          parse_if(tokens, position)
6✔
27
        when :unless
28
          parse_unless(tokens, position)
2✔
29
        when :while
30
          parse_while(tokens, position)
1✔
31
        when :until
32
          parse_until(tokens, position)
1✔
33
        when :case
34
          parse_case(tokens, position)
1✔
35
        when :begin
36
          parse_begin(tokens, position)
3✔
37
        else
38
          # Could be assignment or expression
39
          parse_assignment_or_expression(tokens, position)
3,691✔
40
        end
41
      end
42

43
      def parse_block(tokens, position = 0)
1✔
44
        statements = []
3,667✔
45

46
        loop do
3,667✔
47
          position = skip_newlines(tokens, position)
7,347✔
48
          break if position >= tokens.length
7,347✔
49

50
          token = tokens[position]
7,347✔
51
          break if token.type == :eof
7,347✔
52
          break if %i[end else elsif when rescue ensure].include?(token.type)
3,727✔
53

54
          result = parse_statement(tokens, position)
3,691✔
55
          break if result.failure?
3,691✔
56

57
          statements << result.value
3,680✔
58
          position = result.position
3,680✔
59
        end
60

61
        node = IR::Block.new(statements: statements)
3,667✔
62
        TokenParseResult.success(node, tokens, position)
3,667✔
63
      end
64

65
      private
1✔
66

67
      def skip_newlines(tokens, position)
1✔
68
        position += 1 while position < tokens.length && %i[newline comment].include?(tokens[position].type)
11,118✔
69
        position
11,118✔
70
      end
71

72
      def parse_return(tokens, position)
1✔
73
        position += 1 # consume 'return'
8✔
74

75
        # Check if there's a return value
76
        position = skip_newlines_if_not_modifier(tokens, position)
8✔
77

78
        if position >= tokens.length ||
8✔
79
           tokens[position].type == :eof ||
80
           tokens[position].type == :newline ||
81
           end_of_statement?(tokens, position)
82
          node = IR::Return.new(value: nil)
4✔
83
          return TokenParseResult.success(node, tokens, position)
4✔
84
        end
85

86
        # Parse return value expression
87
        expr_result = @expression_parser.parse_expression(tokens, position)
4✔
88
        return expr_result if expr_result.failure?
4✔
89

90
        # Check for modifier
91
        modifier_result = parse_modifier(tokens, expr_result.position, IR::Return.new(value: expr_result.value))
4✔
92
        return modifier_result if modifier_result.success? && modifier_result.value.is_a?(IR::Conditional)
4✔
93

94
        node = IR::Return.new(value: expr_result.value)
3✔
95
        TokenParseResult.success(node, tokens, expr_result.position)
3✔
96
      end
97

98
      def parse_if(tokens, position)
1✔
99
        position += 1 # consume 'if'
7✔
100

101
        # Parse condition
102
        cond_result = @expression_parser.parse_expression(tokens, position)
7✔
103
        return cond_result if cond_result.failure?
7✔
104

105
        position = cond_result.position
7✔
106

107
        # Skip newline after condition
108
        position = skip_newlines(tokens, position)
7✔
109

110
        # Parse then branch
111
        then_result = parse_block(tokens, position)
7✔
112
        position = then_result.position
7✔
113
        position = skip_newlines(tokens, position)
7✔
114

115
        # Check for elsif or else
116
        else_branch = nil
7✔
117
        if position < tokens.length && tokens[position].type == :elsif
7✔
118
          elsif_result = parse_if(tokens, position) # Reuse if parsing for elsif
1✔
119
          return elsif_result if elsif_result.failure?
1✔
120

121
          else_branch = elsif_result.value
1✔
122
          position = elsif_result.position
1✔
123
        elsif position < tokens.length && tokens[position].type == :else
6✔
124
          position += 1 # consume 'else'
4✔
125
          position = skip_newlines(tokens, position)
4✔
126
          else_result = parse_block(tokens, position)
4✔
127
          else_branch = else_result.value
4✔
128
          position = else_result.position
4✔
129
          position = skip_newlines(tokens, position)
4✔
130
        end
131

132
        # Expect 'end' (unless it was an elsif chain)
133
        if position < tokens.length && tokens[position].type == :end
7✔
134
          position += 1
6✔
135
        end
136

137
        node = IR::Conditional.new(
7✔
138
          kind: :if,
139
          condition: cond_result.value,
140
          then_branch: then_result.value,
141
          else_branch: else_branch
142
        )
143
        TokenParseResult.success(node, tokens, position)
7✔
144
      end
145

146
      def parse_unless(tokens, position)
1✔
147
        position += 1 # consume 'unless'
2✔
148

149
        # Parse condition
150
        cond_result = @expression_parser.parse_expression(tokens, position)
2✔
151
        return cond_result if cond_result.failure?
2✔
152

153
        position = cond_result.position
2✔
154

155
        # Skip newline
156
        position = skip_newlines(tokens, position)
2✔
157

158
        # Parse then branch
159
        then_result = parse_block(tokens, position)
2✔
160
        position = then_result.position
2✔
161
        position = skip_newlines(tokens, position)
2✔
162

163
        # Check for else
164
        else_branch = nil
2✔
165
        if position < tokens.length && tokens[position].type == :else
2✔
NEW
166
          position += 1
×
NEW
167
          position = skip_newlines(tokens, position)
×
NEW
168
          else_result = parse_block(tokens, position)
×
NEW
169
          else_branch = else_result.value
×
NEW
170
          position = else_result.position
×
NEW
171
          position = skip_newlines(tokens, position)
×
172
        end
173

174
        # Expect 'end'
175
        if position < tokens.length && tokens[position].type == :end
2✔
176
          position += 1
2✔
177
        end
178

179
        node = IR::Conditional.new(
2✔
180
          kind: :unless,
181
          condition: cond_result.value,
182
          then_branch: then_result.value,
183
          else_branch: else_branch
184
        )
185
        TokenParseResult.success(node, tokens, position)
2✔
186
      end
187

188
      def parse_while(tokens, position)
1✔
189
        position += 1 # consume 'while'
1✔
190

191
        # Parse condition
192
        cond_result = @expression_parser.parse_expression(tokens, position)
1✔
193
        return cond_result if cond_result.failure?
1✔
194

195
        position = cond_result.position
1✔
196

197
        # Skip newline
198
        position = skip_newlines(tokens, position)
1✔
199

200
        # Parse body
201
        body_result = parse_block(tokens, position)
1✔
202
        position = body_result.position
1✔
203
        position = skip_newlines(tokens, position)
1✔
204

205
        # Expect 'end'
206
        if position < tokens.length && tokens[position].type == :end
1✔
207
          position += 1
1✔
208
        end
209

210
        node = IR::Loop.new(
1✔
211
          kind: :while,
212
          condition: cond_result.value,
213
          body: body_result.value
214
        )
215
        TokenParseResult.success(node, tokens, position)
1✔
216
      end
217

218
      def parse_until(tokens, position)
1✔
219
        position += 1 # consume 'until'
1✔
220

221
        # Parse condition
222
        cond_result = @expression_parser.parse_expression(tokens, position)
1✔
223
        return cond_result if cond_result.failure?
1✔
224

225
        position = cond_result.position
1✔
226

227
        # Skip newline
228
        position = skip_newlines(tokens, position)
1✔
229

230
        # Parse body
231
        body_result = parse_block(tokens, position)
1✔
232
        position = body_result.position
1✔
233
        position = skip_newlines(tokens, position)
1✔
234

235
        # Expect 'end'
236
        if position < tokens.length && tokens[position].type == :end
1✔
237
          position += 1
1✔
238
        end
239

240
        node = IR::Loop.new(
1✔
241
          kind: :until,
242
          condition: cond_result.value,
243
          body: body_result.value
244
        )
245
        TokenParseResult.success(node, tokens, position)
1✔
246
      end
247

248
      def parse_case(tokens, position)
1✔
249
        position += 1 # consume 'case'
1✔
250

251
        # Parse subject (optional)
252
        subject = nil
1✔
253
        position = skip_newlines(tokens, position)
1✔
254

255
        if position < tokens.length && tokens[position].type != :when
1✔
256
          subj_result = @expression_parser.parse_expression(tokens, position)
1✔
257
          if subj_result.success?
1✔
258
            subject = subj_result.value
1✔
259
            position = subj_result.position
1✔
260
          end
261
        end
262

263
        position = skip_newlines(tokens, position)
1✔
264

265
        # Parse when clauses
266
        when_clauses = []
1✔
267
        while position < tokens.length && tokens[position].type == :when
1✔
268
          when_result = parse_when_clause(tokens, position)
2✔
269
          return when_result if when_result.failure?
2✔
270

271
          when_clauses << when_result.value
2✔
272
          position = when_result.position
2✔
273
          position = skip_newlines(tokens, position)
2✔
274
        end
275

276
        # Parse else clause
277
        else_clause = nil
1✔
278
        if position < tokens.length && tokens[position].type == :else
1✔
279
          position += 1
1✔
280
          position = skip_newlines(tokens, position)
1✔
281
          else_result = parse_block(tokens, position)
1✔
282
          else_clause = else_result.value
1✔
283
          position = else_result.position
1✔
284
          position = skip_newlines(tokens, position)
1✔
285
        end
286

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

292
        node = IR::CaseExpr.new(
1✔
293
          subject: subject,
294
          when_clauses: when_clauses,
295
          else_clause: else_clause
296
        )
297
        TokenParseResult.success(node, tokens, position)
1✔
298
      end
299

300
      def parse_when_clause(tokens, position)
1✔
301
        position += 1 # consume 'when'
2✔
302

303
        # Parse patterns (comma-separated)
304
        patterns = []
2✔
305
        loop do
2✔
306
          pattern_result = @expression_parser.parse_expression(tokens, position)
2✔
307
          return pattern_result if pattern_result.failure?
2✔
308

309
          patterns << pattern_result.value
2✔
310
          position = pattern_result.position
2✔
311

312
          break unless tokens[position]&.type == :comma
2✔
313

NEW
314
          position += 1
×
315
        end
316

317
        position = skip_newlines(tokens, position)
2✔
318

319
        # Parse body
320
        body_result = parse_block(tokens, position)
2✔
321
        position = body_result.position
2✔
322

323
        node = IR::WhenClause.new(patterns: patterns, body: body_result.value)
2✔
324
        TokenParseResult.success(node, tokens, position)
2✔
325
      end
326

327
      def parse_begin(tokens, position)
1✔
328
        position += 1 # consume 'begin'
3✔
329
        position = skip_newlines(tokens, position)
3✔
330

331
        # Parse body
332
        body_result = parse_block(tokens, position)
3✔
333
        position = body_result.position
3✔
334
        position = skip_newlines(tokens, position)
3✔
335

336
        # Parse rescue clauses
337
        rescue_clauses = []
3✔
338
        while position < tokens.length && tokens[position].type == :rescue
3✔
339
          rescue_result = parse_rescue_clause(tokens, position)
2✔
340
          return rescue_result if rescue_result.failure?
2✔
341

342
          rescue_clauses << rescue_result.value
2✔
343
          position = rescue_result.position
2✔
344
          position = skip_newlines(tokens, position)
2✔
345
        end
346

347
        # Parse else clause (runs if no exception)
348
        else_clause = nil
3✔
349
        if position < tokens.length && tokens[position].type == :else
3✔
NEW
350
          position += 1
×
NEW
351
          position = skip_newlines(tokens, position)
×
NEW
352
          else_result = parse_block(tokens, position)
×
NEW
353
          else_clause = else_result.value
×
NEW
354
          position = else_result.position
×
NEW
355
          position = skip_newlines(tokens, position)
×
356
        end
357

358
        # Parse ensure clause
359
        ensure_clause = nil
3✔
360
        if position < tokens.length && tokens[position].type == :ensure
3✔
361
          position += 1
1✔
362
          position = skip_newlines(tokens, position)
1✔
363
          ensure_result = parse_block(tokens, position)
1✔
364
          ensure_clause = ensure_result.value
1✔
365
          position = ensure_result.position
1✔
366
          position = skip_newlines(tokens, position)
1✔
367
        end
368

369
        # Expect 'end'
370
        if position < tokens.length && tokens[position].type == :end
3✔
371
          position += 1
3✔
372
        end
373

374
        node = IR::BeginBlock.new(
3✔
375
          body: body_result.value,
376
          rescue_clauses: rescue_clauses,
377
          else_clause: else_clause,
378
          ensure_clause: ensure_clause
379
        )
380
        TokenParseResult.success(node, tokens, position)
3✔
381
      end
382

383
      def parse_rescue_clause(tokens, position)
1✔
384
        position += 1 # consume 'rescue'
2✔
385

386
        exception_types = []
2✔
387
        variable = nil
2✔
388

389
        # Check for exception types and variable binding
390
        # Format: rescue ExType, ExType2 => var or rescue => var
391
        # Parse exception types
392
        if position < tokens.length && !%i[newline hash_rocket].include?(tokens[position].type) && (tokens[position].type == :constant)
2✔
393
          loop do
1✔
394
            if tokens[position].type == :constant
1✔
395
              exception_types << tokens[position].value
1✔
396
              position += 1
1✔
397
            end
398
            break unless tokens[position]&.type == :comma
1✔
399

NEW
400
            position += 1
×
401
          end
402
        end
403

404
        # Check for => var binding
405
        if position < tokens.length && tokens[position].type == :hash_rocket
2✔
406
          position += 1
1✔
407
          if tokens[position]&.type == :identifier
1✔
408
            variable = tokens[position].value
1✔
409
            position += 1
1✔
410
          end
411
        end
412

413
        position = skip_newlines(tokens, position)
2✔
414

415
        # Parse body
416
        body_result = parse_block(tokens, position)
2✔
417
        position = body_result.position
2✔
418

419
        node = IR::RescueClause.new(
2✔
420
          exception_types: exception_types,
421
          variable: variable,
422
          body: body_result.value
423
        )
424
        TokenParseResult.success(node, tokens, position)
2✔
425
      end
426

427
      def parse_assignment_or_expression(tokens, position)
1✔
428
        # Check for typed assignment: name: Type = value
429
        if tokens[position].type == :identifier &&
3,691✔
430
           tokens[position + 1]&.type == :colon &&
431
           tokens[position + 2]&.type == :constant
432
          return parse_typed_assignment(tokens, position)
1✔
433
        end
434

435
        # Check for simple assignment patterns
436
        if assignable_token?(tokens[position])
3,690✔
437
          next_pos = position + 1
3,605✔
438

439
          # Simple assignment: x = value
440
          if tokens[next_pos]&.type == :eq
3,605✔
441
            return parse_simple_assignment(tokens, position)
20✔
442
          end
443

444
          # Compound assignment: x += value, x -= value, etc.
445
          if compound_assignment_token?(tokens[next_pos])
3,585✔
446
            return parse_compound_assignment(tokens, position)
2✔
447
          end
448
        end
449

450
        # Parse as expression
451
        expr_result = @expression_parser.parse_expression(tokens, position)
3,668✔
452
        return expr_result if expr_result.failure?
3,668✔
453

454
        # Check for statement modifiers
455
        parse_modifier(tokens, expr_result.position, expr_result.value)
3,657✔
456
      end
457

458
      def parse_typed_assignment(tokens, position)
1✔
459
        target = tokens[position].value
1✔
460
        position += 2 # skip identifier and colon
1✔
461

462
        # Parse type annotation (simple constant for now)
463
        type_annotation = IR::SimpleType.new(name: tokens[position].value)
1✔
464
        position += 1
1✔
465

466
        # Expect '='
467
        return TokenParseResult.failure("Expected '='", tokens, position) unless tokens[position]&.type == :eq
1✔
468

469
        position += 1
1✔
470

471
        # Parse value
472
        value_result = @expression_parser.parse_expression(tokens, position)
1✔
473
        return value_result if value_result.failure?
1✔
474

475
        node = IR::Assignment.new(
1✔
476
          target: target,
477
          value: value_result.value,
478
          type_annotation: type_annotation
479
        )
480
        TokenParseResult.success(node, tokens, value_result.position)
1✔
481
      end
482

483
      def parse_simple_assignment(tokens, position)
1✔
484
        target = tokens[position].value
20✔
485
        position += 2 # skip variable and '='
20✔
486

487
        # Check for statement expressions (case, if, begin, etc.) as value
488
        value_result = case tokens[position]&.type
20✔
489
                       when :case
NEW
490
                         parse_case(tokens, position)
×
491
                       when :if
NEW
492
                         parse_if(tokens, position)
×
493
                       when :unless
NEW
494
                         parse_unless(tokens, position)
×
495
                       when :begin
NEW
496
                         parse_begin(tokens, position)
×
497
                       else
498
                         @expression_parser.parse_expression(tokens, position)
20✔
499
                       end
500
        return value_result if value_result.failure?
20✔
501

502
        node = IR::Assignment.new(target: target, value: value_result.value)
20✔
503
        TokenParseResult.success(node, tokens, value_result.position)
20✔
504
      end
505

506
      def parse_compound_assignment(tokens, position)
1✔
507
        target = tokens[position].value
2✔
508
        op_token = tokens[position + 1]
2✔
509
        position += 2 # skip variable and operator
2✔
510

511
        # Map compound operator to binary operator
512
        op_map = {
2✔
513
          plus_eq: :+,
514
          minus_eq: :-,
515
          star_eq: :*,
516
          slash_eq: :/,
517
          percent_eq: :%,
518
        }
519
        binary_op = op_map[op_token.type]
2✔
520

521
        # Parse right-hand side
522
        rhs_result = @expression_parser.parse_expression(tokens, position)
2✔
523
        return rhs_result if rhs_result.failure?
2✔
524

525
        # Create expanded form: x = x + value
526
        target_ref = IR::VariableRef.new(name: target, scope: infer_scope(target))
2✔
527
        binary_expr = IR::BinaryOp.new(
2✔
528
          operator: binary_op,
529
          left: target_ref,
530
          right: rhs_result.value
531
        )
532

533
        node = IR::Assignment.new(target: target, value: binary_expr)
2✔
534

535
        # Check for statement modifiers
536
        parse_modifier(tokens, rhs_result.position, node)
2✔
537
      end
538

539
      def parse_modifier(tokens, position, statement)
1✔
540
        return TokenParseResult.success(statement, tokens, position) if position >= tokens.length
3,663✔
541

542
        token = tokens[position]
3,663✔
543
        case token.type
3,663✔
544
        when :if
545
          position += 1
1✔
546
          cond_result = @expression_parser.parse_expression(tokens, position)
1✔
547
          return cond_result if cond_result.failure?
1✔
548

549
          then_branch = statement.is_a?(IR::Block) ? statement : IR::Block.new(statements: [statement])
1✔
550
          node = IR::Conditional.new(
1✔
551
            kind: :if,
552
            condition: cond_result.value,
553
            then_branch: then_branch
554
          )
555
          TokenParseResult.success(node, tokens, cond_result.position)
1✔
556

557
        when :unless
558
          position += 1
1✔
559
          cond_result = @expression_parser.parse_expression(tokens, position)
1✔
560
          return cond_result if cond_result.failure?
1✔
561

562
          then_branch = statement.is_a?(IR::Block) ? statement : IR::Block.new(statements: [statement])
1✔
563
          node = IR::Conditional.new(
1✔
564
            kind: :unless,
565
            condition: cond_result.value,
566
            then_branch: then_branch
567
          )
568
          TokenParseResult.success(node, tokens, cond_result.position)
1✔
569

570
        when :while
571
          position += 1
1✔
572
          cond_result = @expression_parser.parse_expression(tokens, position)
1✔
573
          return cond_result if cond_result.failure?
1✔
574

575
          body = statement.is_a?(IR::Block) ? statement : IR::Block.new(statements: [statement])
1✔
576
          node = IR::Loop.new(
1✔
577
            kind: :while,
578
            condition: cond_result.value,
579
            body: body
580
          )
581
          TokenParseResult.success(node, tokens, cond_result.position)
1✔
582

583
        when :until
NEW
584
          position += 1
×
NEW
585
          cond_result = @expression_parser.parse_expression(tokens, position)
×
NEW
586
          return cond_result if cond_result.failure?
×
587

NEW
588
          body = statement.is_a?(IR::Block) ? statement : IR::Block.new(statements: [statement])
×
NEW
589
          node = IR::Loop.new(
×
590
            kind: :until,
591
            condition: cond_result.value,
592
            body: body
593
          )
NEW
594
          TokenParseResult.success(node, tokens, cond_result.position)
×
595

596
        else
597
          TokenParseResult.success(statement, tokens, position)
3,660✔
598
        end
599
      end
600

601
      def assignable_token?(token)
1✔
602
        return false unless token
3,690✔
603

604
        %i[identifier ivar cvar gvar].include?(token.type)
3,690✔
605
      end
606

607
      def compound_assignment_token?(token)
1✔
608
        return false unless token
3,585✔
609

610
        %i[plus_eq minus_eq star_eq slash_eq percent_eq].include?(token.type)
3,585✔
611
      end
612

613
      def end_of_statement?(tokens, position)
1✔
614
        return true if position >= tokens.length
4✔
615

616
        %i[newline eof end else elsif when rescue ensure].include?(tokens[position].type)
4✔
617
      end
618

619
      def skip_newlines_if_not_modifier(tokens, position)
1✔
620
        # Don't skip newlines if next token after newline is a modifier
621
        if tokens[position]&.type == :newline
8✔
NEW
622
          next_pos = position + 1
×
NEW
623
          next_pos += 1 while next_pos < tokens.length && tokens[next_pos].type == :newline
×
624
          # If next meaningful token is a modifier, return original position
NEW
625
          if next_pos < tokens.length && %i[if unless while until].include?(tokens[next_pos].type)
×
NEW
626
            return position
×
627
          end
628
        end
629
        skip_newlines(tokens, position)
8✔
630
      end
631

632
      def infer_scope(name)
1✔
633
        case name[0]
2✔
634
        when "@"
NEW
635
          name[1] == "@" ? :class : :instance
×
636
        when "$"
NEW
637
          :global
×
638
        else
639
          :local
2✔
640
        end
641
      end
642
    end
643
  end
644
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