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

type-ruby / t-ruby / 20478001050

24 Dec 2025 04:12AM UTC coverage: 75.827% (+0.1%) from 75.685%
20478001050

Pull #12

github

web-flow
Merge ca82ff00a into 067a7f88c
Pull Request #12: fix: support Unicode method names in parser and compiler

61 of 70 new or added lines in 4 files covered. (87.14%)

26 existing lines in 1 file now uncovered.

5022 of 6623 relevant lines covered (75.83%)

1174.91 hits per line

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

90.49
/lib/t_ruby/body_parser.rb
1
# frozen_string_literal: true
2

3
module TRuby
1✔
4
  # BodyParser - T-Ruby 메서드 본문을 IR 노드로 변환
5
  # Prism은 순수 Ruby만 파싱하므로, T-Ruby 타입 어노테이션을 포함한
6
  # 메서드 본문을 파싱하기 위해 자체 구현
7
  class BodyParser
1✔
8
    # 메서드 본문을 IR::Block으로 변환
9
    # @param lines [Array<String>] 전체 소스 라인 배열
10
    # @param start_line [Integer] 메서드 본문 시작 라인 (0-indexed)
11
    # @param end_line [Integer] 메서드 본문 끝 라인 (exclusive)
12
    # @return [IR::Block] 본문을 표현하는 IR 블록
13
    def parse(lines, start_line, end_line)
1✔
14
      statements = []
3,589✔
15
      i = start_line
3,589✔
16

17
      while i < end_line
3,589✔
18
        line = lines[i]
3,596✔
19
        stripped = line.strip
3,596✔
20

21
        # 빈 줄이나 주석은 건너뛰기
22
        if stripped.empty? || stripped.start_with?("#")
3,596✔
23
          i += 1
2✔
24
          next
2✔
25
        end
26

27
        # if/unless 조건문 처리
28
        if stripped.match?(/^(if|unless)\s+/)
3,594✔
29
          node, next_i = parse_conditional(lines, i, end_line)
4✔
30
          if node
4✔
31
            statements << node
4✔
32
            i = next_i
4✔
33
            next
4✔
34
          end
35
        end
36

37
        node = parse_statement(stripped, i)
3,590✔
38
        statements << node if node
3,590✔
39
        i += 1
3,590✔
40
      end
41

42
      IR::Block.new(statements: statements)
3,589✔
43
    end
44

45
    # if/unless/elsif 조건문 파싱
46
    # @return [Array(IR::Conditional, Integer)] 조건문 노드와 다음 라인 인덱스
47
    def parse_conditional(lines, start_line, block_end)
1✔
48
      line = lines[start_line].strip
4✔
49
      match = line.match(/^(if|unless|elsif)\s+(.+)$/)
4✔
50
      return [nil, start_line] unless match
4✔
51

52
      # elsif는 내부적으로 if처럼 처리
53
      kind = match[1] == "elsif" ? :if : match[1].to_sym
4✔
54
      condition = parse_expression(match[2])
4✔
55

56
      # then/elsif/else/end 블록 찾기
57
      then_statements = []
4✔
58
      else_statements = []
4✔
59
      current_branch = :then
4✔
60
      depth = 1
4✔
61
      i = start_line + 1
4✔
62

63
      while i < block_end && depth.positive?
4✔
64
        current_line = lines[i].strip
12✔
65

66
        if current_line.match?(/^(if|unless|case|while|until|for|begin)\b/)
12✔
NEW
67
          depth += 1
×
NEW
68
          if current_branch == :then
×
NEW
69
            then_statements << IR::RawCode.new(code: current_line)
×
70
          else
NEW
71
            else_statements << IR::RawCode.new(code: current_line)
×
72
          end
73
        elsif current_line == "end"
12✔
74
          depth -= 1
4✔
75
          break if depth.zero?
4✔
76
        elsif depth == 1 && current_line.match?(/^elsif\s+/)
8✔
77
          # elsif는 중첩된 if로 처리
NEW
78
          nested_cond, next_i = parse_conditional(lines, i, block_end)
×
NEW
79
          else_statements << nested_cond if nested_cond
×
NEW
80
          i = next_i
×
NEW
81
          break
×
82
        elsif depth == 1 && current_line == "else"
8✔
83
          current_branch = :else
2✔
84
        elsif !current_line.empty? && !current_line.start_with?("#")
6✔
85
          node = parse_statement(current_line, i)
6✔
86
          next unless node
6✔
87

88
          if current_branch == :then
6✔
89
            then_statements << node
4✔
90
          else
91
            else_statements << node
2✔
92
          end
93
        end
94

95
        i += 1
8✔
96
      end
97

98
      then_block = IR::Block.new(statements: then_statements)
4✔
99
      else_block = else_statements.empty? ? nil : IR::Block.new(statements: else_statements)
4✔
100

101
      conditional = IR::Conditional.new(
4✔
102
        condition: condition,
103
        then_branch: then_block,
104
        else_branch: else_block,
105
        kind: kind,
106
        location: start_line
107
      )
108

109
      [conditional, i + 1]
4✔
110
    end
111

112
    private
1✔
113

114
    # 단일 문장 파싱
115
    def parse_statement(line, line_num)
1✔
116
      case line
3,596✔
117
      # return 문
118
      when /^return\s+(.+)$/
119
        IR::Return.new(
1✔
120
          value: parse_expression(::Regexp.last_match(1).strip),
121
          location: line_num
122
        )
123
      when /^return\s*$/
124
        IR::Return.new(value: nil, location: line_num)
1✔
125

126
      # 인스턴스 변수 할당: @name = value (== 제외)
127
      when /^@(\w+)\s*=(?!=)\s*(.+)$/
128
        IR::Assignment.new(
7✔
129
          target: "@#{::Regexp.last_match(1)}",
130
          value: parse_expression(::Regexp.last_match(2).strip),
131
          location: line_num
132
        )
133

134
      # 지역 변수 할당: name = value (==, != 제외)
135
      when /^(\w+)\s*=(?!=)\s*(.+)$/
136
        IR::Assignment.new(
4✔
137
          target: ::Regexp.last_match(1),
138
          value: parse_expression(::Regexp.last_match(2).strip),
139
          location: line_num
140
        )
141

142
      # 그 외는 표현식 (암묵적 반환값 가능)
143
      else
144
        parse_expression(line)
3,583✔
145
      end
146
    end
147

148
    # 표현식 파싱
149
    def parse_expression(expr)
1✔
150
      return nil if expr.nil? || expr.empty?
7,162✔
151

152
      expr = expr.strip
7,162✔
153

154
      # 리터럴 파싱 시도
155
      result = parse_literal(expr)
7,162✔
156
      return result if result
7,162✔
157

158
      # 복합 표현식 파싱 시도
159
      result = parse_compound_expression(expr)
7,084✔
160
      return result if result
7,084✔
161

162
      # 연산자 파싱 시도
163
      result = parse_operators(expr)
3,586✔
164
      return result if result
3,586✔
165

166
      # 변수 참조 파싱 시도
167
      result = parse_variable_ref(expr)
3,559✔
168
      return result if result
3,559✔
169

170
      # 파싱할 수 없는 경우 RawCode로 래핑
171
      IR::RawCode.new(code: expr)
7✔
172
    end
173

174
    # 리터럴 파싱 (문자열, 숫자, 심볼, 부울, nil)
175
    def parse_literal(expr)
1✔
176
      # 문자열 리터럴 (쌍따옴표)
177
      return parse_string_literal(expr) if expr.match?(/^".*"$/)
7,162✔
178

179
      # 문자열 리터럴 (홑따옴표)
180
      if expr.match?(/^'.*'$/)
7,135✔
181
        return IR::Literal.new(value: expr[1..-2], literal_type: :string)
6✔
182
      end
183

184
      # 심볼 리터럴
185
      if (match = expr.match(/^:(\w+)$/))
7,129✔
186
        return IR::Literal.new(value: match[1].to_sym, literal_type: :symbol)
3✔
187
      end
188

189
      # nil/부울 리터럴
190
      return IR::Literal.new(value: nil, literal_type: :nil) if expr == "nil"
7,126✔
191
      return IR::Literal.new(value: true, literal_type: :boolean) if expr == "true"
7,122✔
192
      return IR::Literal.new(value: false, literal_type: :boolean) if expr == "false"
7,115✔
193

194
      # 부동소수점 리터럴
195
      if (match = expr.match(/^(-?\d+\.\d+)$/))
7,114✔
196
        return IR::Literal.new(value: match[1].to_f, literal_type: :float)
1✔
197
      end
198

199
      # 정수 리터럴
200
      if (match = expr.match(/^(-?\d+)$/))
7,113✔
201
        return IR::Literal.new(value: match[1].to_i, literal_type: :integer)
29✔
202
      end
203

204
      nil
205
    end
206

207
    # 복합 표현식 파싱 (배열, 해시, 괄호, 메서드 호출)
208
    def parse_compound_expression(expr)
1✔
209
      # 배열 리터럴
210
      return parse_array_literal(expr) if expr.start_with?("[") && expr.end_with?("]")
7,084✔
211

212
      # 해시 리터럴
213
      return parse_hash_literal(expr) if expr.start_with?("{") && expr.end_with?("}")
7,081✔
214

215
      # 괄호로 감싼 표현식
216
      return parse_expression(expr[1..-2]) if expr.start_with?("(") && expr.end_with?(")")
7,077✔
217

218
      # 메서드 호출
219
      parse_method_call(expr)
7,077✔
220
    end
221

222
    # 연산자 파싱 (이항, 단항)
223
    def parse_operators(expr)
1✔
224
      # 논리 연산자 (낮은 우선순위)
225
      result = parse_binary_op(expr, ["||", "&&"])
3,586✔
226
      return result if result
3,586✔
227

228
      # 비교 연산자
229
      result = parse_binary_op(expr, ["==", "!=", "<=", ">=", "<=>", "<", ">"])
3,585✔
230
      return result if result
3,585✔
231

232
      # 산술 연산자 (낮은 우선순위부터)
233
      result = parse_binary_op(expr, ["+", "-"])
3,581✔
234
      return result if result
3,581✔
235

236
      result = parse_binary_op(expr, ["*", "/", "%"])
3,571✔
237
      return result if result
3,571✔
238

239
      result = parse_binary_op(expr, ["**"])
3,559✔
240
      return result if result
3,559✔
241

242
      # 단항 연산자
243
      parse_unary_op(expr)
3,559✔
244
    end
245

246
    # 단항 연산자 파싱
247
    def parse_unary_op(expr)
1✔
248
      if expr.start_with?("!")
3,559✔
249
        return IR::UnaryOp.new(operator: "!", operand: parse_expression(expr[1..]))
×
250
      end
251

252
      if expr.start_with?("-") && !expr.match?(/^-\d/)
3,559✔
253
        return IR::UnaryOp.new(operator: "-", operand: parse_expression(expr[1..]))
×
254
      end
255

256
      nil
257
    end
258

259
    # 변수 참조 파싱 (인스턴스, 클래스, 전역, 지역, 상수)
260
    def parse_variable_ref(expr)
1✔
261
      # 인스턴스 변수 참조
262
      if (match = expr.match(/^@(\w+)$/))
3,559✔
263
        return IR::VariableRef.new(name: "@#{match[1]}", scope: :instance)
3✔
264
      end
265

266
      # 클래스 변수 참조
267
      if (match = expr.match(/^@@(\w+)$/))
3,556✔
268
        return IR::VariableRef.new(name: "@@#{match[1]}", scope: :class)
×
269
      end
270

271
      # 전역 변수 참조
272
      if (match = expr.match(/^\$(\w+)$/))
3,556✔
273
        return IR::VariableRef.new(name: "$#{match[1]}", scope: :global)
×
274
      end
275

276
      # 지역 변수 또는 상수
277
      if (match = expr.match(/^(\w+)$/))
3,556✔
278
        name = match[1]
3,549✔
279
        scope = name.match?(/^[A-Z]/) ? :constant : :local
3,549✔
280
        return IR::VariableRef.new(name: name, scope: scope)
3,549✔
281
      end
282

283
      nil
284
    end
285

286
    # 문자열 보간 처리
287
    def parse_string_literal(expr)
1✔
288
      content = expr[1..-2] # 따옴표 제거
27✔
289

290
      # 보간이 있는지 확인
291
      if content.include?('#{')
27✔
292
        # 보간이 있으면 보간 표현식들을 추출
293
        parts = []
6✔
294
        remaining = content
6✔
295

296
        while (match = remaining.match(/#\{([^}]+)\}/))
18✔
297
          # 보간 이전의 문자열 부분
298
          unless match.pre_match.empty?
6✔
299
            parts << IR::Literal.new(value: match.pre_match, literal_type: :string)
6✔
300
          end
301

302
          # 보간 표현식
303
          interpolated_expr = parse_expression(match[1])
6✔
304
          parts << IR::MethodCall.new(
6✔
305
            receiver: interpolated_expr,
306
            method_name: "to_s",
307
            arguments: []
308
          )
309

310
          remaining = match.post_match
6✔
311
        end
312

313
        # 남은 문자열 부분
314
        unless remaining.empty?
6✔
315
          parts << IR::Literal.new(value: remaining, literal_type: :string)
3✔
316
        end
317

318
        # 여러 부분이면 + 연산으로 연결
319
        if parts.length == 1
6✔
320
          parts.first
×
321
        else
322
          result = parts.first
6✔
323
          parts[1..].each do |part|
6✔
324
            result = IR::BinaryOp.new(operator: "+", left: result, right: part)
9✔
325
          end
326
          result
6✔
327
        end
328
      else
329
        # 보간 없음
330
        IR::Literal.new(value: content, literal_type: :string)
21✔
331
      end
332
    end
333

334
    # 배열 리터럴 파싱
335
    def parse_array_literal(expr)
1✔
336
      content = expr[1..-2].strip # 괄호 제거
3✔
337
      return IR::ArrayLiteral.new(elements: []) if content.empty?
3✔
338

339
      elements = split_by_comma(content).map { |e| parse_expression(e.strip) }
8✔
340
      IR::ArrayLiteral.new(elements: elements)
2✔
341
    end
342

343
    # 해시 리터럴 파싱
344
    def parse_hash_literal(expr)
1✔
345
      content = expr[1..-2].strip # 중괄호 제거
4✔
346
      return IR::HashLiteral.new(pairs: []) if content.empty?
4✔
347

348
      pairs = []
2✔
349
      items = split_by_comma(content)
2✔
350

351
      items.each do |item|
2✔
352
        item = item.strip
6✔
353

354
        # symbol: value 형태
355
        if (match = item.match(/^(\w+):\s*(.+)$/))
6✔
356
          key = IR::Literal.new(value: match[1].to_sym, literal_type: :symbol)
5✔
357
          value = parse_expression(match[2].strip)
5✔
358
          pairs << IR::HashPair.new(key: key, value: value)
5✔
359

360
        # key => value 형태
361
        elsif (match = item.match(/^(.+?)\s*=>\s*(.+)$/))
1✔
362
          key = parse_expression(match[1].strip)
×
363
          value = parse_expression(match[2].strip)
×
364
          pairs << IR::HashPair.new(key: key, value: value)
×
365
        end
366
      end
367

368
      IR::HashLiteral.new(pairs: pairs)
2✔
369
    end
370

371
    # 이항 연산자 파싱 (우선순위 고려)
372
    def parse_binary_op(expr, operators)
1✔
373
      # 연산자를 찾되, 괄호/배열/해시/문자열 내부는 제외
374
      depth = 0
17,882✔
375
      in_string = false
17,882✔
376
      string_char = nil
17,882✔
377
      i = expr.length - 1
17,882✔
378

379
      # 오른쪽에서 왼쪽으로 검색 (왼쪽 결합)
380
      while i >= 0
17,882✔
381
        char = expr[i]
54,396✔
382

383
        # 문자열 처리
384
        if !in_string && ['"', "'"].include?(char)
54,396✔
385
          in_string = true
31✔
386
          string_char = char
31✔
387
        elsif in_string && char == string_char && (i.zero? || expr[i - 1] != "\\")
54,365✔
388
          in_string = false
31✔
389
          string_char = nil
31✔
390
        end
391

392
        unless in_string
54,396✔
393
          case char
54,054✔
394
          when ")", "]", "}"
395
            depth += 1
×
396
          when "(", "[", "{"
397
            depth -= 1
×
398
          end
399

400
          if depth.zero?
54,054✔
401
            operators.each do |op|
54,054✔
402
              op_start = i - op.length + 1
162,471✔
403
              next if op_start.negative?
162,471✔
404

405
              next unless expr[op_start, op.length] == op
130,287✔
406

407
              # 연산자 앞뒤에 피연산자가 있는지 확인
408
              left_part = expr[0...op_start].strip
27✔
409
              right_part = expr[(i + 1)..].strip
27✔
410

411
              next if left_part.empty? || right_part.empty?
27✔
412

413
              # 음수 처리: - 앞에 연산자가 있으면 단항 연산자
414
              if op == "-"
27✔
415
                prev_char = left_part[-1]
×
416
                next if prev_char && ["+", "-", "*", "/", "%", "(", ",", "=", "<", ">", "!"].include?(prev_char)
×
417
              end
418

419
              return IR::BinaryOp.new(
27✔
420
                operator: op,
421
                left: parse_expression(left_part),
422
                right: parse_expression(right_part)
423
              )
424
            end
425
          end
426
        end
427

428
        i -= 1
54,369✔
429
      end
430

431
      nil
432
    end
433

434
    # 메서드 호출 파싱
435
    def parse_method_call(expr)
1✔
436
      # receiver.method(args) 패턴
437
      # 또는 method(args) 패턴
438
      # 또는 receiver.method 패턴
439

440
      depth = 0
7,077✔
441
      in_string = false
7,077✔
442
      string_char = nil
7,077✔
443
      last_dot = nil
7,077✔
444

445
      # 마지막 점 위치 찾기 (문자열/괄호 밖에서)
446
      i = expr.length - 1
7,077✔
447
      while i >= 0
7,077✔
448
        char = expr[i]
35,419✔
449

450
        if !in_string && ['"', "'"].include?(char)
35,419✔
451
          in_string = true
11✔
452
          string_char = char
11✔
453
        elsif in_string && char == string_char && (i.zero? || expr[i - 1] != "\\")
35,408✔
454
          in_string = false
11✔
455
          string_char = nil
11✔
456
        end
457

458
        unless in_string
35,419✔
459
          case char
35,328✔
460
          when ")", "]", "}"
461
            depth += 1
3✔
462
          when "(", "[", "{"
463
            depth -= 1
3✔
464
          when "."
465
            if depth.zero?
3,489✔
466
              last_dot = i
3,489✔
467
              break
3,489✔
468
            end
469
          end
470
        end
471

472
        i -= 1
31,930✔
473
      end
474

475
      if last_dot
7,077✔
476
        receiver_str = expr[0...last_dot]
3,489✔
477
        method_part = expr[(last_dot + 1)..]
3,489✔
478

479
        # method_part에서 메서드 이름과 인자 분리
480
        if (match = method_part.match(/^([\w?!]+)\s*\((.*)?\)$/))
3,489✔
481
          method_name = match[1]
1✔
482
          args_str = match[2] || ""
1✔
483
          arguments = args_str.empty? ? [] : split_by_comma(args_str).map { |a| parse_expression(a.strip) }
3✔
484

485
          return IR::MethodCall.new(
1✔
486
            receiver: parse_expression(receiver_str),
487
            method_name: method_name,
488
            arguments: arguments
489
          )
490
        elsif (match = method_part.match(/^([\w?!]+)$/))
3,488✔
491
          # 인자 없는 메서드 호출
492
          return IR::MethodCall.new(
3,488✔
493
            receiver: parse_expression(receiver_str),
494
            method_name: match[1],
495
            arguments: []
496
          )
497
        end
498
      elsif (match = expr.match(/^([\w?!]+)\s*\((.*)?\)$/))
3,588✔
499
        # receiver 없는 메서드 호출: method(args)
500
        method_name = match[1]
2✔
501
        args_str = match[2] || ""
2✔
502

503
        # 내장 메서드가 아니면 nil
504
        # (puts, print, p 등 최상위 메서드)
505
        arguments = args_str.empty? ? [] : split_by_comma(args_str).map { |a| parse_expression(a.strip) }
3✔
506

507
        return IR::MethodCall.new(
2✔
508
          receiver: nil,
509
          method_name: method_name,
510
          arguments: arguments
511
        )
512
      end
513

514
      nil
515
    end
516

517
    # 쉼표로 분리 (괄호/배열/해시/문자열 내부는 제외)
518
    def split_by_comma(str)
1✔
519
      result = []
6✔
520
      current = ""
6✔
521
      depth = 0
6✔
522
      in_string = false
6✔
523
      string_char = nil
6✔
524

525
      str.each_char do |char|
6✔
526
        if !in_string && ['"', "'"].include?(char)
96✔
527
          in_string = true
4✔
528
          string_char = char
4✔
529
          current += char
4✔
530
        elsif in_string && char == string_char
92✔
531
          in_string = false
4✔
532
          string_char = nil
4✔
533
          current += char
4✔
534
        elsif in_string
88✔
535
          current += char
11✔
536
        else
537
          case char
77✔
538
          when "(", "[", "{"
539
            depth += 1
×
540
            current += char
×
541
          when ")", "]", "}"
542
            depth -= 1
×
543
            current += char
×
544
          when ","
545
            if depth.zero?
9✔
546
              result << current.strip
9✔
547
              current = ""
9✔
548
            else
549
              current += char
×
550
            end
551
          else
552
            current += char
68✔
553
          end
554
        end
555
      end
556

557
      result << current.strip unless current.strip.empty?
6✔
558
      result
6✔
559
    end
560
  end
561
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