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

type-ruby / t-ruby / 20560723383

28 Dec 2025 10:58PM UTC coverage: 79.074% (+1.7%) from 77.331%
20560723383

Pull #29

github

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

1848 of 2097 new or added lines in 53 files covered. (88.13%)

6 existing lines in 2 files now uncovered.

6643 of 8401 relevant lines covered (79.07%)

908.2 hits per line

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

83.46
/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
      def initialize
1✔
10
        @statement_parser = StatementParser.new
17✔
11
        @expression_parser = ExpressionParser.new
17✔
12
      end
13

14
      def parse_declaration(tokens, position = 0)
1✔
15
        return TokenParseResult.failure("End of input", tokens, position) if position >= tokens.length
23✔
16

17
        position = skip_newlines(tokens, position)
23✔
18
        return TokenParseResult.failure("End of input", tokens, position) if position >= tokens.length
23✔
19

20
        token = tokens[position]
23✔
21

22
        case token.type
23✔
23
        when :def
24
          parse_method_def(tokens, position)
11✔
25
        when :public, :private, :protected
26
          parse_visibility_method(tokens, position)
1✔
27
        when :class
28
          parse_class(tokens, position)
5✔
29
        when :module
30
          parse_module(tokens, position)
2✔
31
        when :type
32
          parse_type_alias(tokens, position)
2✔
33
        when :interface
34
          parse_interface(tokens, position)
2✔
35
        else
NEW
36
          TokenParseResult.failure("Expected declaration, got #{token.type}", tokens, position)
×
37
        end
38
      end
39

40
      def parse_program(tokens, position = 0)
1✔
41
        declarations = []
1✔
42

43
        loop do
1✔
44
          position = skip_newlines(tokens, position)
3✔
45
          break if position >= tokens.length
3✔
46
          break if tokens[position].type == :eof
3✔
47

48
          result = parse_declaration(tokens, position)
2✔
49
          break if result.failure?
2✔
50

51
          declarations << result.value
2✔
52
          position = result.position
2✔
53
        end
54

55
        program = IR::Program.new(declarations: declarations)
1✔
56
        TokenParseResult.success(program, tokens, position)
1✔
57
      end
58

59
      private
1✔
60

61
      def skip_newlines(tokens, position)
1✔
62
        position += 1 while position < tokens.length && %i[newline comment].include?(tokens[position].type)
80✔
63
        position
80✔
64
      end
65

66
      def parse_method_def(tokens, position, visibility: :public)
1✔
67
        position += 1 # consume 'def'
12✔
68

69
        # Parse method name (identifier or operator)
70
        return TokenParseResult.failure("Expected method name", tokens, position) if position >= tokens.length
12✔
71

72
        method_name = tokens[position].value
12✔
73
        position += 1
12✔
74

75
        # Parse parameters
76
        params = []
12✔
77
        if position < tokens.length && tokens[position].type == :lparen
12✔
78
          position += 1 # consume (
5✔
79

80
          # Parse parameter list
81
          unless tokens[position].type == :rparen
5✔
82
            loop do
5✔
83
              param_result = parse_parameter(tokens, position)
6✔
84
              return param_result if param_result.failure?
6✔
85

86
              params << param_result.value
6✔
87
              position = param_result.position
6✔
88

89
              break unless tokens[position]&.type == :comma
6✔
90

91
              position += 1
1✔
92
            end
93
          end
94

95
          return TokenParseResult.failure("Expected ')'", tokens, position) unless tokens[position]&.type == :rparen
5✔
96

97
          position += 1
5✔
98
        end
99

100
        # Parse return type
101
        return_type = nil
12✔
102
        if position < tokens.length && tokens[position].type == :colon
12✔
103
          position += 1
4✔
104
          type_result = parse_type(tokens, position)
4✔
105
          return type_result if type_result.failure?
4✔
106

107
          return_type = type_result.value
4✔
108
          position = type_result.position
4✔
109
        end
110

111
        position = skip_newlines(tokens, position)
12✔
112

113
        # Parse body
114
        body_result = @statement_parser.parse_block(tokens, position)
12✔
115
        position = body_result.position
12✔
116
        position = skip_newlines(tokens, position)
12✔
117

118
        # Expect 'end'
119
        if position < tokens.length && tokens[position].type == :end
12✔
120
          position += 1
12✔
121
        end
122

123
        node = IR::MethodDef.new(
12✔
124
          name: method_name,
125
          params: params,
126
          return_type: return_type,
127
          body: body_result.value,
128
          visibility: visibility
129
        )
130
        TokenParseResult.success(node, tokens, position)
12✔
131
      end
132

133
      def parse_visibility_method(tokens, position)
1✔
134
        visibility = tokens[position].type
1✔
135
        position += 1
1✔
136

137
        if position < tokens.length && tokens[position].type == :def
1✔
138
          parse_method_def(tokens, position, visibility: visibility)
1✔
139
        else
NEW
140
          TokenParseResult.failure("Expected 'def' after visibility modifier", tokens, position)
×
141
        end
142
      end
143

144
      def parse_parameter(tokens, position)
1✔
145
        return TokenParseResult.failure("Expected parameter", tokens, position) if position >= tokens.length
6✔
146

147
        # Check for different parameter types
148
        case tokens[position].type
6✔
149
        when :star
150
          # Splat parameter *args
NEW
151
          position += 1
×
NEW
152
          name = tokens[position].value
×
NEW
153
          position += 1
×
NEW
154
          param = IR::Parameter.new(name: name, kind: :rest)
×
NEW
155
          return TokenParseResult.success(param, tokens, position)
×
156

157
        when :star_star
158
          # Double splat **opts
NEW
159
          position += 1
×
NEW
160
          name = tokens[position].value
×
NEW
161
          position += 1
×
NEW
162
          param = IR::Parameter.new(name: name, kind: :keyrest)
×
NEW
163
          return TokenParseResult.success(param, tokens, position)
×
164

165
        when :amp
166
          # Block parameter &block
NEW
167
          position += 1
×
NEW
168
          name = tokens[position].value
×
NEW
169
          position += 1
×
NEW
170
          param = IR::Parameter.new(name: name, kind: :block)
×
NEW
171
          return TokenParseResult.success(param, tokens, position)
×
172
        end
173

174
        # Regular parameter: name or name: Type
175
        name = tokens[position].value
6✔
176
        position += 1
6✔
177

178
        type_annotation = nil
6✔
179
        if position < tokens.length && tokens[position].type == :colon
6✔
180
          position += 1
5✔
181
          type_result = parse_type(tokens, position)
5✔
182
          return type_result if type_result.failure?
5✔
183

184
          type_annotation = type_result.value
5✔
185
          position = type_result.position
5✔
186
        end
187

188
        param = IR::Parameter.new(name: name, type_annotation: type_annotation)
6✔
189
        TokenParseResult.success(param, tokens, position)
6✔
190
      end
191

192
      def parse_class(tokens, position)
1✔
193
        position += 1 # consume 'class'
5✔
194

195
        # Parse class name
196
        return TokenParseResult.failure("Expected class name", tokens, position) if position >= tokens.length
5✔
197

198
        class_name = tokens[position].value
5✔
199
        position += 1
5✔
200

201
        # Check for superclass
202
        superclass = nil
5✔
203
        if position < tokens.length && tokens[position].type == :lt
5✔
204
          position += 1
1✔
205
          superclass = tokens[position].value
1✔
206
          position += 1
1✔
207
        end
208

209
        position = skip_newlines(tokens, position)
5✔
210

211
        # Parse class body (methods and instance variables)
212
        body = []
5✔
213
        instance_vars = []
5✔
214

215
        loop do
5✔
216
          position = skip_newlines(tokens, position)
13✔
217
          break if position >= tokens.length
13✔
218
          break if tokens[position].type == :end
13✔
219

220
          if tokens[position].type == :ivar && tokens[position + 1]&.type == :colon
8✔
221
            # Instance variable declaration: @name: Type
222
            ivar_result = parse_instance_var_decl(tokens, position)
4✔
223
            return ivar_result if ivar_result.failure?
4✔
224

225
            instance_vars << ivar_result.value
4✔
226
            position = ivar_result.position
4✔
227
          elsif %i[def public private protected].include?(tokens[position].type)
4✔
228
            method_result = parse_declaration(tokens, position)
4✔
229
            return method_result if method_result.failure?
4✔
230

231
            body << method_result.value
4✔
232
            position = method_result.position
4✔
233
          else
NEW
234
            break
×
235
          end
236
        end
237

238
        # Expect 'end'
239
        if position < tokens.length && tokens[position].type == :end
5✔
240
          position += 1
5✔
241
        end
242

243
        node = IR::ClassDecl.new(
5✔
244
          name: class_name,
245
          superclass: superclass,
246
          body: body,
247
          instance_vars: instance_vars
248
        )
249
        TokenParseResult.success(node, tokens, position)
5✔
250
      end
251

252
      def parse_instance_var_decl(tokens, position)
1✔
253
        # @name: Type
254
        name = tokens[position].value[1..] # remove @ prefix
4✔
255
        position += 2 # skip @name and :
4✔
256

257
        type_result = parse_type(tokens, position)
4✔
258
        return type_result if type_result.failure?
4✔
259

260
        node = IR::InstanceVariable.new(name: name, type_annotation: type_result.value)
4✔
261
        TokenParseResult.success(node, tokens, type_result.position)
4✔
262
      end
263

264
      def parse_module(tokens, position)
1✔
265
        position += 1 # consume 'module'
2✔
266

267
        # Parse module name
268
        return TokenParseResult.failure("Expected module name", tokens, position) if position >= tokens.length
2✔
269

270
        module_name = tokens[position].value
2✔
271
        position += 1
2✔
272

273
        position = skip_newlines(tokens, position)
2✔
274

275
        # Parse module body
276
        body = []
2✔
277

278
        loop do
2✔
279
          position = skip_newlines(tokens, position)
3✔
280
          break if position >= tokens.length
3✔
281
          break if tokens[position].type == :end
3✔
282

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

285
          method_result = parse_declaration(tokens, position)
1✔
286
          return method_result if method_result.failure?
1✔
287

288
          body << method_result.value
1✔
289
          position = method_result.position
1✔
290
        end
291

292
        # Expect 'end'
293
        if position < tokens.length && tokens[position].type == :end
2✔
294
          position += 1
2✔
295
        end
296

297
        node = IR::ModuleDecl.new(name: module_name, body: body)
2✔
298
        TokenParseResult.success(node, tokens, position)
2✔
299
      end
300

301
      def parse_type_alias(tokens, position)
1✔
302
        position += 1 # consume 'type'
2✔
303

304
        # Parse type name
305
        return TokenParseResult.failure("Expected type name", tokens, position) if position >= tokens.length
2✔
306

307
        type_name = tokens[position].value
2✔
308
        position += 1
2✔
309

310
        # Expect '='
311
        return TokenParseResult.failure("Expected '='", tokens, position) unless tokens[position]&.type == :eq
2✔
312

313
        position += 1
2✔
314

315
        # Parse type definition
316
        type_result = parse_type(tokens, position)
2✔
317
        return type_result if type_result.failure?
2✔
318

319
        node = IR::TypeAlias.new(name: type_name, definition: type_result.value)
2✔
320
        TokenParseResult.success(node, tokens, type_result.position)
2✔
321
      end
322

323
      def parse_interface(tokens, position)
1✔
324
        position += 1 # consume 'interface'
2✔
325

326
        # Parse interface name
327
        return TokenParseResult.failure("Expected interface name", tokens, position) if position >= tokens.length
2✔
328

329
        interface_name = tokens[position].value
2✔
330
        position += 1
2✔
331

332
        position = skip_newlines(tokens, position)
2✔
333

334
        # Parse interface members
335
        members = []
2✔
336

337
        loop do
2✔
338
          position = skip_newlines(tokens, position)
5✔
339
          break if position >= tokens.length
5✔
340
          break if tokens[position].type == :end
5✔
341

342
          member_result = parse_interface_member(tokens, position)
3✔
343
          break if member_result.failure?
3✔
344

345
          members << member_result.value
3✔
346
          position = member_result.position
3✔
347
        end
348

349
        # Expect 'end'
350
        if position < tokens.length && tokens[position].type == :end
2✔
351
          position += 1
2✔
352
        end
353

354
        node = IR::Interface.new(name: interface_name, members: members)
2✔
355
        TokenParseResult.success(node, tokens, position)
2✔
356
      end
357

358
      def parse_interface_member(tokens, position)
1✔
359
        # name: Type
360
        return TokenParseResult.failure("Expected member name", tokens, position) if position >= tokens.length
3✔
361

362
        name = tokens[position].value
3✔
363
        position += 1
3✔
364

365
        return TokenParseResult.failure("Expected ':'", tokens, position) unless tokens[position]&.type == :colon
3✔
366

367
        position += 1
3✔
368

369
        type_result = parse_type(tokens, position)
3✔
370
        return type_result if type_result.failure?
3✔
371

372
        node = IR::InterfaceMember.new(name: name, type_signature: type_result.value)
3✔
373
        TokenParseResult.success(node, tokens, type_result.position)
3✔
374
      end
375

376
      def parse_type(tokens, position)
1✔
377
        return TokenParseResult.failure("Expected type", tokens, position) if position >= tokens.length
20✔
378

379
        # Parse primary type
380
        result = parse_primary_type(tokens, position)
20✔
381
        return result if result.failure?
20✔
382

383
        type = result.value
20✔
384
        position = result.position
20✔
385

386
        # Check for union type
387
        types = [type]
20✔
388
        while position < tokens.length && tokens[position].type == :pipe
20✔
NEW
389
          position += 1
×
NEW
390
          next_result = parse_primary_type(tokens, position)
×
NEW
391
          return next_result if next_result.failure?
×
392

NEW
393
          types << next_result.value
×
NEW
394
          position = next_result.position
×
395
        end
396

397
        if types.length > 1
20✔
NEW
398
          node = IR::UnionType.new(types: types)
×
NEW
399
          TokenParseResult.success(node, tokens, position)
×
400
        else
401
          TokenParseResult.success(type, tokens, position)
20✔
402
        end
403
      end
404

405
      def parse_primary_type(tokens, position)
1✔
406
        return TokenParseResult.failure("Expected type", tokens, position) if position >= tokens.length
23✔
407

408
        # Check for function type: -> ReturnType
409
        if tokens[position].type == :arrow
23✔
410
          position += 1
1✔
411
          return_result = parse_primary_type(tokens, position)
1✔
412
          return return_result if return_result.failure?
1✔
413

414
          node = IR::FunctionType.new(param_types: [], return_type: return_result.value)
1✔
415
          return TokenParseResult.success(node, tokens, return_result.position)
1✔
416
        end
417

418
        # Check for tuple type: (Type, Type) -> ReturnType
419
        if tokens[position].type == :lparen
22✔
420
          position += 1
2✔
421
          param_types = []
2✔
422

423
          unless tokens[position].type == :rparen
2✔
424
            loop do
2✔
425
              type_result = parse_type(tokens, position)
2✔
426
              return type_result if type_result.failure?
2✔
427

428
              param_types << type_result.value
2✔
429
              position = type_result.position
2✔
430

431
              break unless tokens[position]&.type == :comma
2✔
432

NEW
433
              position += 1
×
434
            end
435
          end
436

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

439
          position += 1
2✔
440

441
          # Check for function arrow
442
          if position < tokens.length && tokens[position].type == :arrow
2✔
443
            position += 1
2✔
444
            return_result = parse_primary_type(tokens, position)
2✔
445
            return return_result if return_result.failure?
2✔
446

447
            node = IR::FunctionType.new(param_types: param_types, return_type: return_result.value)
2✔
448
            return TokenParseResult.success(node, tokens, return_result.position)
2✔
449
          else
NEW
450
            node = IR::TupleType.new(element_types: param_types)
×
NEW
451
            return TokenParseResult.success(node, tokens, position)
×
452
          end
453
        end
454

455
        # Simple type or generic type
456
        type_name = tokens[position].value
20✔
457
        position += 1
20✔
458

459
        # Check for generic arguments: Type<Args>
460
        if position < tokens.length && tokens[position].type == :lt
20✔
NEW
461
          position += 1
×
NEW
462
          type_args = []
×
463

NEW
464
          loop do
×
NEW
465
            arg_result = parse_type(tokens, position)
×
NEW
466
            return arg_result if arg_result.failure?
×
467

NEW
468
            type_args << arg_result.value
×
NEW
469
            position = arg_result.position
×
470

NEW
471
            break unless tokens[position]&.type == :comma
×
472

NEW
473
            position += 1
×
474
          end
475

NEW
476
          return TokenParseResult.failure("Expected '>'", tokens, position) unless tokens[position]&.type == :gt
×
477

NEW
478
          position += 1
×
479

NEW
480
          node = IR::GenericType.new(base: type_name, type_args: type_args)
×
NEW
481
          TokenParseResult.success(node, tokens, position)
×
482
        elsif position < tokens.length && tokens[position].type == :question
20✔
483
          # Check for nullable: Type?
NEW
484
          position += 1
×
NEW
485
          inner = IR::SimpleType.new(name: type_name)
×
NEW
486
          node = IR::NullableType.new(inner_type: inner)
×
NEW
487
          TokenParseResult.success(node, tokens, position)
×
488
        else
489
          node = IR::SimpleType.new(name: type_name)
20✔
490
          TokenParseResult.success(node, tokens, position)
20✔
491
        end
492
      end
493
    end
494
  end
495
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