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

type-ruby / t-ruby / 21242754680

22 Jan 2026 09:15AM UTC coverage: 92.628% (+0.2%) from 92.432%
21242754680

push

github

web-flow
Parser architecture improvement with TypeSlot (#48)

* Add TypeSlot infrastructure for type position tracking

Introduce TypeSlot class that represents positions where type
annotations are expected. This enables explicit tracking of
explicit, inferred, and resolved types for each position.

- Add IR::TypeSlot class with kind, location, and context
- Add type_slot attribute to IR::Parameter
- Add return_type_slot attribute to IR::MethodDef
- Maintain backward compatibility with existing code

Refs #47

* Integrate TypeSlot generation in TokenDeclarationParser

- Add TypeSlot creation during parameter parsing in parse_parameter
- Add return_type_slot creation in parse_method_def
- Pass method_name context to parameter parsers for TypeSlot context
- Support all parameter types (regular, rest, keyrest, block, keyword)
- Add comprehensive tests for TypeSlot integration
- Maintain backward compatibility with existing type_annotation/return_type

Refs #47

* Add TypeSlotError for context-aware error messages

- Add TRuby::Errors::TypeSlotError class for type-related errors
- Support location info (line, column) from TypeSlot
- Include context description (parameter/return/variable info)
- Support suggestion field for helpful error hints
- Provide to_lsp_diagnostic for LSP integration
- Add comprehensive tests

Refs #47

* Add SlotResolver for TypeSlot resolution

- Add TRuby::TypeResolution::SlotResolver class
- Collect unresolved TypeSlots from IR program
- Support resolve_to_untyped for gradual typing fallback
- Provide slot_summary for type coverage statistics
- Handle method parameters and return types
- Traverse ClassDef and ModuleDef for nested methods

Refs #47

* Add facade pattern for parser with token parser option

- Add use_token_parser option to Parser.new for opt-in new parser
- Support TRUBY_NEW_PARSER=1 environment variable
- Implement parse_with_token_parser using TokenDeclarationParser
- Add backward-compatible legacy format conversion
- Ma... (continued)

173 of 189 new or added lines in 7 files covered. (91.53%)

8444 of 9116 relevant lines covered (92.63%)

1767.1 hits per line

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

96.75
/lib/t_ruby/parser.rb
1
# frozen_string_literal: true
2

3
module TRuby
1✔
4
  # Enhanced Parser using Parser Combinator for complex type expressions
5
  # Maintains backward compatibility with original Parser interface
6
  #
7
  # This class serves as a facade that can delegate to either:
8
  # 1. Legacy regex-based parsing (default)
9
  # 2. New TokenDeclarationParser with TypeSlot support (opt-in)
10
  #
11
  # To use the new parser, set use_token_parser: true or
12
  # set TRUBY_NEW_PARSER=1 environment variable.
13
  class Parser
1✔
14
    # Type names that are recognized as valid
15
    VALID_TYPES = %w[String Integer Boolean Array Hash Symbol void nil].freeze
1✔
16

17
    # Pattern for method/variable names that supports Unicode characters
18
    # \p{L} matches any Unicode letter, \p{N} matches any Unicode number
19
    IDENTIFIER_CHAR = '[\p{L}\p{N}_]'
1✔
20
    # Method names can end with ? or !
21
    METHOD_NAME_PATTERN = "#{IDENTIFIER_CHAR}+[?!]?".freeze
1✔
22
    # Visibility modifiers for method definitions
23
    VISIBILITY_PATTERN = '(?:(?:private|protected|public)\s+)?'
1✔
24

25
    # @deprecated The regex-based parsing will be replaced by TokenDeclarationParser.
26
    # See: lib/t_ruby/parser_combinator/token/token_declaration_parser.rb
27

28
    attr_reader :source, :ir_program
1✔
29

30
    def initialize(source, parse_body: true, use_token_parser: nil)
1✔
31
      @source = source
427✔
32
      @lines = source.split("\n")
427✔
33
      @parse_body = parse_body
427✔
34
      @use_token_parser = use_token_parser
427✔
35
      @type_parser = ParserCombinator::TypeParser.new
427✔
36
      @body_parser = ParserCombinator::TokenBodyParser.new if parse_body
427✔
37
      @ir_program = nil
427✔
38
    end
39

40
    def parse
1✔
41
      if use_token_parser?
425✔
42
        parse_with_token_parser
4✔
43
      else
44
        parse_with_legacy_parser
421✔
45
      end
46
    rescue Scanner::ScanError => e
47
      raise ParseError.new(e.message, line: e.line, column: e.column)
2✔
48
    end
49

50
    private
1✔
51

52
    # Check if token parser should be used
53
    def use_token_parser?
1✔
54
      return @use_token_parser unless @use_token_parser.nil?
427✔
55

56
      ENV["TRUBY_NEW_PARSER"] == "1"
422✔
57
    end
58

59
    # Parse using the new TokenDeclarationParser with TypeSlot support
60
    def parse_with_token_parser
1✔
61
      scanner = Scanner.new(@source)
4✔
62
      tokens = scanner.scan_all
4✔
63
      token_parser = ParserCombinator::TokenDeclarationParser.new
4✔
64

65
      program_result = token_parser.parse_program(tokens)
4✔
66

67
      if program_result.success?
4✔
68
        @ir_program = program_result.value
4✔
69
        convert_ir_to_legacy_format(@ir_program)
4✔
70
      else
NEW
71
        raise ParseError.new(
×
72
          program_result.error,
73
          line: token_parser.errors.first&.line,
74
          column: token_parser.errors.first&.column
75
        )
76
      end
77
    end
78

79
    # Convert IR::Program to legacy hash format for backward compatibility
80
    def convert_ir_to_legacy_format(program)
1✔
81
      functions = []
4✔
82
      type_aliases = []
4✔
83
      interfaces = []
4✔
84
      classes = []
4✔
85

86
      program.declarations.each do |decl|
4✔
87
        case decl
4✔
88
        when IR::MethodDef
89
          functions << convert_method_to_legacy(decl)
3✔
90
        when IR::TypeAlias
NEW
91
          type_aliases << convert_type_alias_to_legacy(decl)
×
92
        when IR::Interface
NEW
93
          interfaces << convert_interface_to_legacy(decl)
×
94
        when IR::ClassDecl
95
          classes << convert_class_to_legacy(decl)
1✔
96
        end
97
      end
98

99
      {
100
        type: :success,
4✔
101
        functions: functions,
102
        type_aliases: type_aliases,
103
        interfaces: interfaces,
104
        classes: classes,
105
      }
106
    end
107

108
    def convert_method_to_legacy(method_def)
1✔
109
      params = method_def.params.map do |param|
4✔
110
        {
111
          name: param.name,
4✔
112
          type: param.type_annotation&.to_s,
113
          ir_type: param.type_annotation,
114
          kind: param.kind || :required,
115
        }
116
      end
117

118
      {
119
        name: method_def.name,
4✔
120
        params: params,
121
        return_type: method_def.return_type&.to_s,
122
        ir_return_type: method_def.return_type,
123
        visibility: method_def.visibility || :public,
124
        body_ir: method_def.body,
125
      }
126
    end
127

128
    def convert_type_alias_to_legacy(type_alias)
1✔
129
      {
NEW
130
        name: type_alias.name,
×
131
        definition: type_alias.definition&.to_s,
132
        ir_type: type_alias.definition,
133
      }
134
    end
135

136
    def convert_interface_to_legacy(interface_def)
1✔
NEW
137
      members = interface_def.members.map do |member|
×
138
        {
NEW
139
          name: member[:name] || member.name,
×
140
          type: member[:type]&.to_s || member.type&.to_s,
141
          ir_type: member[:type] || member.type,
142
        }
143
      end
144

NEW
145
      { name: interface_def.name, members: members }
×
146
    end
147

148
    def convert_class_to_legacy(class_def)
1✔
149
      methods = (class_def.body || []).select { |d| d.is_a?(IR::MethodDef) }.map do |m|
2✔
150
        convert_method_to_legacy(m)
1✔
151
      end
152

153
      {
154
        name: class_def.name,
1✔
155
        superclass: class_def.superclass,
156
        methods: methods,
157
        instance_vars: [],
158
      }
159
    end
160

161
    # @deprecated Legacy regex-based parser. Will be removed in future version.
162
    def parse_with_legacy_parser
1✔
163
      functions = []
421✔
164
      type_aliases = []
421✔
165
      interfaces = []
421✔
166
      classes = []
421✔
167
      i = 0
421✔
168

169
      # Pre-detect heredoc regions to skip
170
      heredoc_ranges = HeredocDetector.detect(@lines)
421✔
171

172
      while i < @lines.length
421✔
173
        # Skip lines inside heredoc content
174
        if HeredocDetector.inside_heredoc?(i, heredoc_ranges)
23,246✔
175
          i += 1
16✔
176
          next
16✔
177
        end
178

179
        line = @lines[i]
23,230✔
180

181
        # Match type alias definitions
182
        if line.match?(/^\s*type\s+\w+/)
23,230✔
183
          alias_info = parse_type_alias(line)
100✔
184
          type_aliases << alias_info if alias_info
100✔
185
        end
186

187
        # Match interface definitions
188
        if line.match?(/^\s*interface\s+\w+/)
23,230✔
189
          interface_info, next_i = parse_interface(i)
64✔
190
          if interface_info
64✔
191
            interfaces << interface_info
64✔
192
            i = next_i
64✔
193
            next
64✔
194
          end
195
        end
196

197
        # Match class definitions
198
        if line.match?(/^\s*class\s+\w+/)
23,166✔
199
          class_info, next_i = parse_class(i)
25✔
200
          if class_info
25✔
201
            classes << class_info
25✔
202
            i = next_i
25✔
203
            next
25✔
204
          end
205
        end
206

207
        # Match function definitions (top-level only, not inside class)
208
        if line.match?(/^\s*#{VISIBILITY_PATTERN}def\s+#{IDENTIFIER_CHAR}+/)
23,141✔
209
          func_info, next_i = parse_function_with_body(i)
4,349✔
210
          if func_info
4,347✔
211
            functions << func_info
4,329✔
212
            i = next_i
4,329✔
213
            next
4,329✔
214
          end
215
        end
216

217
        i += 1
18,810✔
218
      end
219

220
      result = {
221
        type: :success,
419✔
222
        functions: functions,
223
        type_aliases: type_aliases,
224
        interfaces: interfaces,
225
        classes: classes,
226
      }
227

228
      # Build IR
229
      builder = IR::Builder.new
419✔
230
      @ir_program = builder.build(result, source: @source)
419✔
231

232
      result
419✔
233
    end
234

235
    public
1✔
236

237
    # Parse to IR directly (new API)
238
    def parse_to_ir
1✔
239
      parse unless @ir_program
×
240
      @ir_program
×
241
    end
242

243
    # Parse a type expression using combinator
244
    def parse_type(type_string)
1✔
245
      result = @type_parser.parse(type_string)
×
246
      result[:success] ? result[:type] : nil
×
247
    end
248

249
    private
1✔
250

251
    # 최상위 함수를 본문까지 포함하여 파싱
252
    def parse_function_with_body(start_index)
1✔
253
      line = @lines[start_index]
4,349✔
254
      func_info = parse_function_definition(line, line_number: start_index + 1)
4,349✔
255
      return [nil, start_index] unless func_info
4,349✔
256

257
      # Add location info (1-based line number, column is 1 + indentation)
258
      def_indent = line.match(/^(\s*)/)[1].length
4,331✔
259
      func_info[:line] = start_index + 1
4,331✔
260
      func_info[:column] = def_indent + 1
4,331✔
261

262
      i = start_index + 1
4,331✔
263
      body_start = i
4,331✔
264
      body_end = i
4,331✔
265

266
      # end 키워드 찾기
267
      while i < @lines.length
4,331✔
268
        current_line = @lines[i]
8,577✔
269

270
        if current_line.match?(/^\s*end\s*$/)
8,577✔
271
          end_indent = current_line.match(/^(\s*)/)[1].length
4,322✔
272
          if end_indent <= def_indent
4,322✔
273
            body_end = i
4,322✔
274
            break
4,322✔
275
          end
276
        end
277

278
        i += 1
4,255✔
279
      end
280

281
      # 본문 파싱 (parse_body 옵션이 활성화된 경우)
282
      if @parse_body && @body_parser && body_start < body_end
4,331✔
283
        func_info[:body_ir] = @body_parser.parse(@lines, body_start, body_end)
4,253✔
284
        func_info[:body_range] = { start: body_start, end: body_end }
4,251✔
285
      end
286

287
      [func_info, i]
4,329✔
288
    end
289

290
    def parse_type_alias(line)
1✔
291
      match = line.match(/^\s*type\s+(\w+)\s*=\s*(.+?)\s*$/)
100✔
292
      return nil unless match
100✔
293

294
      alias_name = match[1]
86✔
295
      definition = match[2].strip
86✔
296

297
      # Use combinator for complex type parsing
298
      type_result = @type_parser.parse(definition)
86✔
299
      if type_result[:success]
86✔
300
        return {
301
          name: alias_name,
84✔
302
          definition: definition,
303
          ir_type: type_result[:type],
304
        }
305
      end
306

307
      {
308
        name: alias_name,
2✔
309
        definition: definition,
310
      }
311
    end
312

313
    def parse_function_definition(line, line_number: 1) # rubocop:disable Lint/UnusedMethodArgument
1✔
314
      # Match methods with or without parentheses
315
      # def foo(params): Type   - with params and return type
316
      # def foo(): Type         - no params but with return type
317
      # def foo(params)         - with params, no return type
318
      # def foo                  - no params, no return type
319
      # Also supports visibility modifiers: private def, protected def, public def
320

321
      match = line.match(/^\s*(?:(private|protected|public)\s+)?def\s+(#{METHOD_NAME_PATTERN})\s*(?:\((.*?)\))?\s*(?::\s*(.+?))?\s*$/)
4,388✔
322
      return nil unless match
4,388✔
323

324
      visibility = match[1] ? match[1].to_sym : :public
4,370✔
325
      function_name = match[2]
4,370✔
326
      params_str = match[3] || ""
4,370✔
327
      return_type_str = match[4]&.strip
4,370✔
328

329
      # Validate return type if present
330
      if return_type_str
4,370✔
331
        return_type_str = validate_and_extract_type(return_type_str)
4,339✔
332
      end
333

334
      params = parse_parameters(params_str)
4,370✔
335

336
      result = {
337
        name: function_name,
4,370✔
338
        params: params,
339
        return_type: return_type_str,
340
        visibility: visibility,
341
      }
342

343
      # Parse return type with combinator
344
      if return_type_str
4,370✔
345
        type_result = @type_parser.parse(return_type_str)
4,338✔
346
        result[:ir_return_type] = type_result[:type] if type_result[:success]
4,338✔
347
      end
348

349
      result
4,370✔
350
    end
351

352
    # Validate type string and return nil if invalid
353
    def validate_and_extract_type(type_str)
1✔
354
      return nil if type_str.nil? || type_str.empty?
4,339✔
355

356
      # Check for whitespace in simple type names that would be invalid
357
      # Pattern: Capital letter followed by lowercase, then space, then more lowercase
358
      # e.g., "Str ing", "Int eger", "Bool ean"
359
      if type_str.match?(/^[A-Z][a-z]*\s+[a-z]+/)
4,339✔
360
        return nil
1✔
361
      end
362

363
      # Check for trailing operators
364
      return nil if type_str.match?(/[|&]\s*$/)
4,338✔
365

366
      # Check for leading operators
367
      return nil if type_str.match?(/^\s*[|&]/)
4,338✔
368

369
      # Check for unbalanced brackets
370
      return nil if type_str.count("<") != type_str.count(">")
4,338✔
371
      return nil if type_str.count("[") != type_str.count("]")
4,338✔
372
      return nil if type_str.count("(") != type_str.count(")")
4,338✔
373

374
      # Check for empty generic arguments
375
      return nil if type_str.match?(/<\s*>/)
4,338✔
376

377
      type_str
4,338✔
378
    end
379

380
    def parse_parameters(params_str)
1✔
381
      return [] if params_str.empty?
4,370✔
382

383
      parameters = []
4,247✔
384
      param_list = split_params(params_str)
4,247✔
385

386
      param_list.each do |param|
4,247✔
387
        param = param.strip
4,278✔
388

389
        # 1. 더블 스플랫: **name: Type
390
        if param.start_with?("**")
4,278✔
391
          param_info = parse_double_splat_parameter(param)
3✔
392
          parameters << param_info if param_info
3✔
393
        # 2. 키워드 인자 그룹: { ... } 또는 { ... }: InterfaceName
394
        elsif param.start_with?("{")
4,275✔
395
          keyword_params = parse_keyword_args_group(param)
16✔
396
          parameters.concat(keyword_params) if keyword_params
16✔
397
        # 3. Hash 리터럴: name: { ... }
398
        elsif param.match?(/^\w+:\s*\{/)
4,259✔
399
          param_info = parse_hash_literal_parameter(param)
5✔
400
          parameters << param_info if param_info
5✔
401
        # 4. 일반 위치 인자: name: Type 또는 name: Type = default
402
        else
403
          param_info = parse_single_parameter(param)
4,254✔
404
          parameters << param_info if param_info
4,254✔
405
        end
406
      end
407

408
      parameters
4,247✔
409
    end
410

411
    def split_params(params_str)
1✔
412
      # Handle nested generics, braces, brackets
413
      result = []
4,247✔
414
      current = ""
4,247✔
415
      depth = 0
4,247✔
416
      brace_depth = 0
4,247✔
417

418
      params_str.each_char do |char|
4,247✔
419
        case char
47,766✔
420
        when "<", "[", "("
421
          depth += 1
18✔
422
          current += char
18✔
423
        when ">", "]", ")"
424
          depth -= 1
16✔
425
          current += char
16✔
426
        when "{"
427
          brace_depth += 1
22✔
428
          current += char
22✔
429
        when "}"
430
          brace_depth -= 1
22✔
431
          current += char
22✔
432
        when ","
433
          if depth.zero? && brace_depth.zero?
50✔
434
            result << current.strip
31✔
435
            current = ""
31✔
436
          else
437
            current += char
19✔
438
          end
439
        else
440
          current += char
47,638✔
441
        end
442
      end
443

444
      result << current.strip unless current.empty?
4,247✔
445
      result
4,247✔
446
    end
447

448
    # 더블 스플랫 파라미터 파싱: **opts: Type
449
    def parse_double_splat_parameter(param)
1✔
450
      # **name: Type
451
      match = param.match(/^\*\*(\w+)(?::\s*(.+?))?$/)
3✔
452
      return nil unless match
3✔
453

454
      param_name = match[1]
3✔
455
      type_str = match[2]&.strip
3✔
456

457
      result = {
458
        name: param_name,
3✔
459
        type: type_str,
460
        kind: :keyrest,
461
      }
462

463
      if type_str
3✔
464
        type_result = @type_parser.parse(type_str)
3✔
465
        result[:ir_type] = type_result[:type] if type_result[:success]
3✔
466
      end
467

468
      result
3✔
469
    end
470

471
    # 키워드 인자 그룹 파싱: { name: String, age: Integer = 0 } 또는 { name:, age: 0 }: InterfaceName
472
    def parse_keyword_args_group(param)
1✔
473
      # { ... }: InterfaceName 형태 확인
474
      # 또는 { ... } 만 있는 형태 (인라인 타입)
475
      interface_match = param.match(/^\{(.+)\}\s*:\s*(\w+)\s*$/)
16✔
476
      inline_match = param.match(/^\{(.+)\}\s*$/) unless interface_match
16✔
477

478
      if interface_match
16✔
479
        inner_content = interface_match[1]
4✔
480
        interface_name = interface_match[2]
4✔
481
        parse_keyword_args_with_interface(inner_content, interface_name)
4✔
482
      elsif inline_match
12✔
483
        inner_content = inline_match[1]
12✔
484
        parse_keyword_args_inline(inner_content)
12✔
485
      end
486
    end
487

488
    # interface 참조 키워드 인자 파싱: { name:, age: 0 }: UserParams
489
    def parse_keyword_args_with_interface(inner_content, interface_name)
1✔
490
      parameters = []
4✔
491
      parts = split_keyword_args(inner_content)
4✔
492

493
      parts.each do |part|
4✔
494
        part = part.strip
9✔
495
        next if part.empty?
9✔
496

497
        # name: default_value 또는 name: 형태
498
        next unless part.match?(/^(\w+):\s*(.*)$/)
9✔
499

500
        match = part.match(/^(\w+):\s*(.*)$/)
9✔
501
        param_name = match[1]
9✔
502
        default_value = match[2].strip
9✔
503
        default_value = nil if default_value.empty?
9✔
504

505
        parameters << {
9✔
506
          name: param_name,
507
          type: nil, # interface에서 타입을 가져옴
508
          default_value: default_value,
509
          kind: :keyword,
510
          interface_ref: interface_name,
511
        }
512
      end
513

514
      parameters
4✔
515
    end
516

517
    # 인라인 타입 키워드 인자 파싱: { name: String, age: Integer = 0 }
518
    def parse_keyword_args_inline(inner_content)
1✔
519
      parameters = []
12✔
520
      parts = split_keyword_args(inner_content)
12✔
521

522
      parts.each do |part|
12✔
523
        part = part.strip
20✔
524
        next if part.empty?
20✔
525

526
        # name: Type = default 또는 name: Type 형태
527
        next unless part.match?(/^(\w+):\s*(.+)$/)
20✔
528

529
        match = part.match(/^(\w+):\s*(.+)$/)
20✔
530
        param_name = match[1]
20✔
531
        type_and_default = match[2].strip
20✔
532

533
        # Type = default 분리
534
        type_str, default_value = split_type_and_default(type_and_default)
20✔
535

536
        result = {
537
          name: param_name,
20✔
538
          type: type_str,
539
          default_value: default_value,
540
          kind: :keyword,
541
        }
542

543
        if type_str
20✔
544
          type_result = @type_parser.parse(type_str)
20✔
545
          result[:ir_type] = type_result[:type] if type_result[:success]
20✔
546
        end
547

548
        parameters << result
20✔
549
      end
550

551
      parameters
12✔
552
    end
553

554
    # 키워드 인자 내부를 콤마로 분리 (중첩된 제네릭/배열/해시 고려)
555
    def split_keyword_args(content)
1✔
556
      StringUtils.split_by_comma(content)
16✔
557
    end
558

559
    # 타입과 기본값 분리: "String = 0" -> ["String", "0"]
560
    def split_type_and_default(type_and_default)
1✔
561
      StringUtils.split_type_and_default(type_and_default)
4,250✔
562
    end
563

564
    # Hash 리터럴 파라미터 파싱: config: { host: String, port: Integer }
565
    def parse_hash_literal_parameter(param)
1✔
566
      # name: { ... } 또는 name: { ... }: InterfaceName
567
      match = param.match(/^(\w+):\s*(\{.+\})(?::\s*(\w+))?$/)
5✔
568
      return nil unless match
5✔
569

570
      param_name = match[1]
5✔
571
      hash_type = match[2]
5✔
572
      interface_name = match[3]
5✔
573

574
      result = {
575
        name: param_name,
5✔
576
        type: interface_name || hash_type,
577
        kind: :required,
578
        hash_type_def: hash_type, # 원본 해시 타입 정의 저장
579
      }
580

581
      result[:interface_ref] = interface_name if interface_name
5✔
582

583
      result
5✔
584
    end
585

586
    def parse_single_parameter(param)
1✔
587
      # name: Type = default 또는 name: Type 또는 name
588
      # 기본값이 있는 경우 먼저 처리
589
      type_str = nil
4,254✔
590
      default_value = nil
4,254✔
591

592
      if param.include?(":")
4,254✔
593
        match = param.match(/^(\w+):\s*(.+)$/)
4,247✔
594
        return nil unless match
4,247✔
595

596
        param_name = match[1]
4,230✔
597
        type_and_default = match[2].strip
4,230✔
598
        type_str, default_value = split_type_and_default(type_and_default)
4,230✔
599
      else
600
        # 타입 없이 이름만 있는 경우
601
        param_name = param.strip
7✔
602
      end
603

604
      result = {
605
        name: param_name,
4,237✔
606
        type: type_str,
607
        default_value: default_value,
608
        kind: default_value ? :optional : :required,
4,237✔
609
      }
610

611
      # Parse type with combinator
612
      if type_str
4,237✔
613
        type_result = @type_parser.parse(type_str)
4,230✔
614
        result[:ir_type] = type_result[:type] if type_result[:success]
4,230✔
615
      end
616

617
      result
4,237✔
618
    end
619

620
    def parse_class(start_index)
1✔
621
      line = @lines[start_index]
25✔
622
      match = line.match(/^\s*class\s+(\w+)(?:\s*<\s*(\w+))?/)
25✔
623
      return [nil, start_index] unless match
25✔
624

625
      class_name = match[1]
25✔
626
      superclass = match[2]
25✔
627
      methods = []
25✔
628
      instance_vars = []
25✔
629
      i = start_index + 1
25✔
630
      class_indent = line.match(/^(\s*)/)[1].length
25✔
631
      class_end = i
25✔
632

633
      # 먼저 클래스의 끝을 찾음
634
      temp_i = i
25✔
635
      while temp_i < @lines.length
25✔
636
        current_line = @lines[temp_i]
172✔
637
        if current_line.match?(/^\s*end\s*$/)
172✔
638
          end_indent = current_line.match(/^(\s*)/)[1].length
66✔
639
          if end_indent <= class_indent
66✔
640
            class_end = temp_i
25✔
641
            break
25✔
642
          end
643
        end
644
        temp_i += 1
147✔
645
      end
646

647
      while i < class_end
25✔
648
        current_line = @lines[i]
97✔
649

650
        # Match method definitions inside class
651
        if current_line.match?(/^\s*#{VISIBILITY_PATTERN}def\s+#{IDENTIFIER_CHAR}+/)
97✔
652
          method_info, next_i = parse_method_in_class(i, class_end)
39✔
653
          if method_info
39✔
654
            methods << method_info
39✔
655
            i = next_i
39✔
656
            next
39✔
657
          end
658
        end
659

660
        i += 1
58✔
661
      end
662

663
      # 메서드 본문에서 인스턴스 변수 추출
664
      methods.each do |method_info|
25✔
665
        extract_instance_vars_from_body(method_info[:body_ir], instance_vars)
39✔
666
      end
667

668
      # Try to infer instance variable types from initialize parameters
669
      init_method = methods.find { |m| m[:name] == "initialize" }
53✔
670
      if init_method
25✔
671
        instance_vars.each do |ivar|
8✔
672
          # Find matching parameter (e.g., @name = name)
673
          matching_param = init_method[:params]&.find { |p| p[:name] == ivar[:name] }
21✔
674
          ivar[:type] = matching_param[:type] if matching_param && matching_param[:type]
11✔
675
          ivar[:ir_type] = matching_param[:ir_type] if matching_param && matching_param[:ir_type]
11✔
676
        end
677
      end
678

679
      [{
680
        name: class_name,
25✔
681
        superclass: superclass,
682
        methods: methods,
683
        instance_vars: instance_vars,
684
      }, class_end,]
685
    end
686

687
    # 클래스 내부의 메서드를 본문까지 포함하여 파싱
688
    def parse_method_in_class(start_index, class_end)
1✔
689
      line = @lines[start_index]
39✔
690
      method_info = parse_function_definition(line, line_number: start_index + 1)
39✔
691
      return [nil, start_index] unless method_info
39✔
692

693
      # Add location info (1-based line number, column is 1 + indentation)
694
      def_indent = line.match(/^(\s*)/)[1].length
39✔
695
      method_info[:line] = start_index + 1
39✔
696
      method_info[:column] = def_indent + 1
39✔
697

698
      i = start_index + 1
39✔
699
      body_start = i
39✔
700
      body_end = i
39✔
701

702
      # 메서드의 end 키워드 찾기
703
      while i < class_end
39✔
704
        current_line = @lines[i]
89✔
705

706
        if current_line.match?(/^\s*end\s*$/)
89✔
707
          end_indent = current_line.match(/^(\s*)/)[1].length
41✔
708
          if end_indent <= def_indent
41✔
709
            body_end = i
39✔
710
            break
39✔
711
          end
712
        end
713

714
        i += 1
50✔
715
      end
716

717
      # 본문 파싱 (parse_body 옵션이 활성화된 경우)
718
      if @parse_body && @body_parser && body_start < body_end
39✔
719
        method_info[:body_ir] = @body_parser.parse(@lines, body_start, body_end)
39✔
720
        method_info[:body_range] = { start: body_start, end: body_end }
39✔
721
      end
722

723
      [method_info, i]
39✔
724
    end
725

726
    # 본문 IR에서 인스턴스 변수 추출
727
    def extract_instance_vars_from_body(body_ir, instance_vars)
1✔
728
      return unless body_ir.is_a?(IR::Block)
39✔
729

730
      body_ir.statements.each do |stmt|
39✔
731
        case stmt
40✔
732
        when IR::Assignment
733
          if stmt.target.start_with?("@") && !stmt.target.start_with?("@@")
12✔
734
            ivar_name = stmt.target[1..] # @ 제거
11✔
735
            unless instance_vars.any? { |iv| iv[:name] == ivar_name }
14✔
736
              instance_vars << { name: ivar_name }
11✔
737
            end
738
          end
739
        when IR::Block
740
          extract_instance_vars_from_body(stmt, instance_vars)
×
741
        end
742
      end
743
    end
744

745
    def parse_interface(start_index)
1✔
746
      line = @lines[start_index]
64✔
747
      match = line.match(/^\s*interface\s+([\w:]+)/)
64✔
748
      return [nil, start_index] unless match
64✔
749

750
      interface_name = match[1]
64✔
751
      members = []
64✔
752
      i = start_index + 1
64✔
753

754
      while i < @lines.length
64✔
755
        current_line = @lines[i]
188✔
756
        break if current_line.match?(/^\s*end\s*$/)
188✔
757

758
        if current_line.match?(/^\s*[\w!?]+\s*:\s*/)
124✔
759
          member_match = current_line.match(/^\s*([\w!?]+)\s*:\s*(.+?)\s*$/)
123✔
760
          if member_match
123✔
761
            member = {
762
              name: member_match[1],
123✔
763
              type: member_match[2].strip,
764
            }
765

766
            # Parse member type with combinator
767
            type_result = @type_parser.parse(member[:type])
123✔
768
            member[:ir_type] = type_result[:type] if type_result[:success]
123✔
769

770
            members << member
123✔
771
          end
772
        end
773

774
        i += 1
124✔
775
      end
776

777
      [{ name: interface_name, members: members }, i]
64✔
778
    end
779
  end
780
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

© 2026 Coveralls, Inc