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

type-ruby / t-ruby / 20477536068

24 Dec 2025 03:38AM UTC coverage: 75.7% (+0.02%) from 75.685%
20477536068

Pull #12

github

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

50 of 51 new or added lines in 4 files covered. (98.04%)

43 existing lines in 2 files now uncovered.

4978 of 6576 relevant lines covered (75.7%)

1182.32 hits per line

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

92.13
/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,585✔
15
      i = start_line
3,585✔
16

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

21
        # 빈 줄이나 주석은 건너뛰기
22
        unless stripped.empty? || stripped.start_with?("#")
3,592✔
23
          node = parse_statement(stripped, i)
3,590✔
24
          statements << node if node
3,590✔
25
        end
26

27
        i += 1
3,592✔
28
      end
29

30
      IR::Block.new(statements: statements)
3,585✔
31
    end
32

33
    private
1✔
34

35
    # 단일 문장 파싱
36
    def parse_statement(line, line_num)
1✔
37
      case line
3,590✔
38
      # return 문
39
      when /^return\s+(.+)$/
40
        IR::Return.new(
1✔
41
          value: parse_expression(::Regexp.last_match(1).strip),
42
          location: line_num
43
        )
44
      when /^return\s*$/
45
        IR::Return.new(value: nil, location: line_num)
1✔
46

47
      # 인스턴스 변수 할당: @name = value (== 제외)
48
      when /^@(\w+)\s*=(?!=)\s*(.+)$/
49
        IR::Assignment.new(
7✔
50
          target: "@#{::Regexp.last_match(1)}",
51
          value: parse_expression(::Regexp.last_match(2).strip),
52
          location: line_num
53
        )
54

55
      # 지역 변수 할당: name = value (==, != 제외)
56
      when /^(\w+)\s*=(?!=)\s*(.+)$/
57
        IR::Assignment.new(
4✔
58
          target: ::Regexp.last_match(1),
59
          value: parse_expression(::Regexp.last_match(2).strip),
60
          location: line_num
61
        )
62

63
      # 그 외는 표현식 (암묵적 반환값 가능)
64
      else
65
        parse_expression(line)
3,577✔
66
      end
67
    end
68

69
    # 표현식 파싱
70
    def parse_expression(expr)
1✔
71
      return nil if expr.nil? || expr.empty?
7,145✔
72

73
      expr = expr.strip
7,145✔
74

75
      # 리터럴 파싱 시도
76
      result = parse_literal(expr)
7,145✔
77
      return result if result
7,145✔
78

79
      # 복합 표현식 파싱 시도
80
      result = parse_compound_expression(expr)
7,074✔
81
      return result if result
7,074✔
82

83
      # 연산자 파싱 시도
84
      result = parse_operators(expr)
3,577✔
85
      return result if result
3,577✔
86

87
      # 변수 참조 파싱 시도
88
      result = parse_variable_ref(expr)
3,553✔
89
      return result if result
3,553✔
90

91
      # 파싱할 수 없는 경우 RawCode로 래핑
92
      IR::RawCode.new(code: expr)
7✔
93
    end
94

95
    # 리터럴 파싱 (문자열, 숫자, 심볼, 부울, nil)
96
    def parse_literal(expr)
1✔
97
      # 문자열 리터럴 (쌍따옴표)
98
      return parse_string_literal(expr) if expr.match?(/^".*"$/)
7,145✔
99

100
      # 문자열 리터럴 (홑따옴표)
101
      if expr.match?(/^'.*'$/)
7,119✔
102
        return IR::Literal.new(value: expr[1..-2], literal_type: :string)
6✔
103
      end
104

105
      # 심볼 리터럴
106
      if (match = expr.match(/^:(\w+)$/))
7,113✔
107
        return IR::Literal.new(value: match[1].to_sym, literal_type: :symbol)
3✔
108
      end
109

110
      # nil/부울 리터럴
111
      return IR::Literal.new(value: nil, literal_type: :nil) if expr == "nil"
7,110✔
112
      return IR::Literal.new(value: true, literal_type: :boolean) if expr == "true"
7,107✔
113
      return IR::Literal.new(value: false, literal_type: :boolean) if expr == "false"
7,102✔
114

115
      # 부동소수점 리터럴
116
      if (match = expr.match(/^(-?\d+\.\d+)$/))
7,102✔
117
        return IR::Literal.new(value: match[1].to_f, literal_type: :float)
1✔
118
      end
119

120
      # 정수 리터럴
121
      if (match = expr.match(/^(-?\d+)$/))
7,101✔
122
        return IR::Literal.new(value: match[1].to_i, literal_type: :integer)
27✔
123
      end
124

125
      nil
126
    end
127

128
    # 복합 표현식 파싱 (배열, 해시, 괄호, 메서드 호출)
129
    def parse_compound_expression(expr)
1✔
130
      # 배열 리터럴
131
      return parse_array_literal(expr) if expr.start_with?("[") && expr.end_with?("]")
7,074✔
132

133
      # 해시 리터럴
134
      return parse_hash_literal(expr) if expr.start_with?("{") && expr.end_with?("}")
7,071✔
135

136
      # 괄호로 감싼 표현식
137
      return parse_expression(expr[1..-2]) if expr.start_with?("(") && expr.end_with?(")")
7,067✔
138

139
      # 메서드 호출
140
      parse_method_call(expr)
7,067✔
141
    end
142

143
    # 연산자 파싱 (이항, 단항)
144
    def parse_operators(expr)
1✔
145
      # 논리 연산자 (낮은 우선순위)
146
      result = parse_binary_op(expr, ["||", "&&"])
3,577✔
147
      return result if result
3,577✔
148

149
      # 비교 연산자
150
      result = parse_binary_op(expr, ["==", "!=", "<=", ">=", "<=>", "<", ">"])
3,576✔
151
      return result if result
3,576✔
152

153
      # 산술 연산자 (낮은 우선순위부터)
154
      result = parse_binary_op(expr, ["+", "-"])
3,575✔
155
      return result if result
3,575✔
156

157
      result = parse_binary_op(expr, ["*", "/", "%"])
3,565✔
158
      return result if result
3,565✔
159

160
      result = parse_binary_op(expr, ["**"])
3,553✔
161
      return result if result
3,553✔
162

163
      # 단항 연산자
164
      parse_unary_op(expr)
3,553✔
165
    end
166

167
    # 단항 연산자 파싱
168
    def parse_unary_op(expr)
1✔
169
      if expr.start_with?("!")
3,553✔
UNCOV
170
        return IR::UnaryOp.new(operator: "!", operand: parse_expression(expr[1..]))
×
171
      end
172

173
      if expr.start_with?("-") && !expr.match?(/^-\d/)
3,553✔
UNCOV
174
        return IR::UnaryOp.new(operator: "-", operand: parse_expression(expr[1..]))
×
175
      end
176

177
      nil
178
    end
179

180
    # 변수 참조 파싱 (인스턴스, 클래스, 전역, 지역, 상수)
181
    def parse_variable_ref(expr)
1✔
182
      # 인스턴스 변수 참조
183
      if (match = expr.match(/^@(\w+)$/))
3,553✔
184
        return IR::VariableRef.new(name: "@#{match[1]}", scope: :instance)
3✔
185
      end
186

187
      # 클래스 변수 참조
188
      if (match = expr.match(/^@@(\w+)$/))
3,550✔
UNCOV
189
        return IR::VariableRef.new(name: "@@#{match[1]}", scope: :class)
×
190
      end
191

192
      # 전역 변수 참조
193
      if (match = expr.match(/^\$(\w+)$/))
3,550✔
UNCOV
194
        return IR::VariableRef.new(name: "$#{match[1]}", scope: :global)
×
195
      end
196

197
      # 지역 변수 또는 상수
198
      if (match = expr.match(/^(\w+)$/))
3,550✔
199
        name = match[1]
3,543✔
200
        scope = name.match?(/^[A-Z]/) ? :constant : :local
3,543✔
201
        return IR::VariableRef.new(name: name, scope: scope)
3,543✔
202
      end
203

204
      nil
205
    end
206

207
    # 문자열 보간 처리
208
    def parse_string_literal(expr)
1✔
209
      content = expr[1..-2] # 따옴표 제거
26✔
210

211
      # 보간이 있는지 확인
212
      if content.include?('#{')
26✔
213
        # 보간이 있으면 보간 표현식들을 추출
214
        parts = []
6✔
215
        remaining = content
6✔
216

217
        while (match = remaining.match(/#\{([^}]+)\}/))
18✔
218
          # 보간 이전의 문자열 부분
219
          unless match.pre_match.empty?
6✔
220
            parts << IR::Literal.new(value: match.pre_match, literal_type: :string)
6✔
221
          end
222

223
          # 보간 표현식
224
          interpolated_expr = parse_expression(match[1])
6✔
225
          parts << IR::MethodCall.new(
6✔
226
            receiver: interpolated_expr,
227
            method_name: "to_s",
228
            arguments: []
229
          )
230

231
          remaining = match.post_match
6✔
232
        end
233

234
        # 남은 문자열 부분
235
        unless remaining.empty?
6✔
236
          parts << IR::Literal.new(value: remaining, literal_type: :string)
3✔
237
        end
238

239
        # 여러 부분이면 + 연산으로 연결
240
        if parts.length == 1
6✔
UNCOV
241
          parts.first
×
242
        else
243
          result = parts.first
6✔
244
          parts[1..].each do |part|
6✔
245
            result = IR::BinaryOp.new(operator: "+", left: result, right: part)
9✔
246
          end
247
          result
6✔
248
        end
249
      else
250
        # 보간 없음
251
        IR::Literal.new(value: content, literal_type: :string)
20✔
252
      end
253
    end
254

255
    # 배열 리터럴 파싱
256
    def parse_array_literal(expr)
1✔
257
      content = expr[1..-2].strip # 괄호 제거
3✔
258
      return IR::ArrayLiteral.new(elements: []) if content.empty?
3✔
259

260
      elements = split_by_comma(content).map { |e| parse_expression(e.strip) }
8✔
261
      IR::ArrayLiteral.new(elements: elements)
2✔
262
    end
263

264
    # 해시 리터럴 파싱
265
    def parse_hash_literal(expr)
1✔
266
      content = expr[1..-2].strip # 중괄호 제거
4✔
267
      return IR::HashLiteral.new(pairs: []) if content.empty?
4✔
268

269
      pairs = []
2✔
270
      items = split_by_comma(content)
2✔
271

272
      items.each do |item|
2✔
273
        item = item.strip
6✔
274

275
        # symbol: value 형태
276
        if (match = item.match(/^(\w+):\s*(.+)$/))
6✔
277
          key = IR::Literal.new(value: match[1].to_sym, literal_type: :symbol)
5✔
278
          value = parse_expression(match[2].strip)
5✔
279
          pairs << IR::HashPair.new(key: key, value: value)
5✔
280

281
        # key => value 형태
282
        elsif (match = item.match(/^(.+?)\s*=>\s*(.+)$/))
1✔
UNCOV
283
          key = parse_expression(match[1].strip)
×
UNCOV
284
          value = parse_expression(match[2].strip)
×
UNCOV
285
          pairs << IR::HashPair.new(key: key, value: value)
×
286
        end
287
      end
288

289
      IR::HashLiteral.new(pairs: pairs)
2✔
290
    end
291

292
    # 이항 연산자 파싱 (우선순위 고려)
293
    def parse_binary_op(expr, operators)
1✔
294
      # 연산자를 찾되, 괄호/배열/해시/문자열 내부는 제외
295
      depth = 0
17,846✔
296
      in_string = false
17,846✔
297
      string_char = nil
17,846✔
298
      i = expr.length - 1
17,846✔
299

300
      # 오른쪽에서 왼쪽으로 검색 (왼쪽 결합)
301
      while i >= 0
17,846✔
302
        char = expr[i]
54,296✔
303

304
        # 문자열 처리
305
        if !in_string && ['"', "'"].include?(char)
54,296✔
306
          in_string = true
29✔
307
          string_char = char
29✔
308
        elsif in_string && char == string_char && (i.zero? || expr[i - 1] != "\\")
54,267✔
309
          in_string = false
29✔
310
          string_char = nil
29✔
311
        end
312

313
        unless in_string
54,296✔
314
          case char
53,964✔
315
          when ")", "]", "}"
UNCOV
316
            depth += 1
×
317
          when "(", "[", "{"
UNCOV
318
            depth -= 1
×
319
          end
320

321
          if depth.zero?
53,964✔
322
            operators.each do |op|
53,964✔
323
              op_start = i - op.length + 1
162,204✔
324
              next if op_start.negative?
162,204✔
325

326
              next unless expr[op_start, op.length] == op
130,076✔
327

328
              # 연산자 앞뒤에 피연산자가 있는지 확인
329
              left_part = expr[0...op_start].strip
24✔
330
              right_part = expr[(i + 1)..].strip
24✔
331

332
              next if left_part.empty? || right_part.empty?
24✔
333

334
              # 음수 처리: - 앞에 연산자가 있으면 단항 연산자
335
              if op == "-"
24✔
UNCOV
336
                prev_char = left_part[-1]
×
UNCOV
337
                next if prev_char && ["+", "-", "*", "/", "%", "(", ",", "=", "<", ">", "!"].include?(prev_char)
×
338
              end
339

340
              return IR::BinaryOp.new(
24✔
341
                operator: op,
342
                left: parse_expression(left_part),
343
                right: parse_expression(right_part)
344
              )
345
            end
346
          end
347
        end
348

349
        i -= 1
54,272✔
350
      end
351

352
      nil
353
    end
354

355
    # 메서드 호출 파싱
356
    def parse_method_call(expr)
1✔
357
      # receiver.method(args) 패턴
358
      # 또는 method(args) 패턴
359
      # 또는 receiver.method 패턴
360

361
      depth = 0
7,067✔
362
      in_string = false
7,067✔
363
      string_char = nil
7,067✔
364
      last_dot = nil
7,067✔
365

366
      # 마지막 점 위치 찾기 (문자열/괄호 밖에서)
367
      i = expr.length - 1
7,067✔
368
      while i >= 0
7,067✔
369
        char = expr[i]
35,376✔
370

371
        if !in_string && ['"', "'"].include?(char)
35,376✔
372
          in_string = true
10✔
373
          string_char = char
10✔
374
        elsif in_string && char == string_char && (i.zero? || expr[i - 1] != "\\")
35,366✔
375
          in_string = false
10✔
376
          string_char = nil
10✔
377
        end
378

379
        unless in_string
35,376✔
380
          case char
35,290✔
381
          when ")", "]", "}"
382
            depth += 1
3✔
383
          when "(", "[", "{"
384
            depth -= 1
3✔
385
          when "."
386
            if depth.zero?
3,488✔
387
              last_dot = i
3,488✔
388
              break
3,488✔
389
            end
390
          end
391
        end
392

393
        i -= 1
31,888✔
394
      end
395

396
      if last_dot
7,067✔
397
        receiver_str = expr[0...last_dot]
3,488✔
398
        method_part = expr[(last_dot + 1)..]
3,488✔
399

400
        # method_part에서 메서드 이름과 인자 분리
401
        if (match = method_part.match(/^([\w?!]+)\s*\((.*)?\)$/))
3,488✔
402
          method_name = match[1]
1✔
403
          args_str = match[2] || ""
1✔
404
          arguments = args_str.empty? ? [] : split_by_comma(args_str).map { |a| parse_expression(a.strip) }
3✔
405

406
          return IR::MethodCall.new(
1✔
407
            receiver: parse_expression(receiver_str),
408
            method_name: method_name,
409
            arguments: arguments
410
          )
411
        elsif (match = method_part.match(/^([\w?!]+)$/))
3,487✔
412
          # 인자 없는 메서드 호출
413
          return IR::MethodCall.new(
3,487✔
414
            receiver: parse_expression(receiver_str),
415
            method_name: match[1],
416
            arguments: []
417
          )
418
        end
419
      elsif (match = expr.match(/^([\w?!]+)\s*\((.*)?\)$/))
3,579✔
420
        # receiver 없는 메서드 호출: method(args)
421
        method_name = match[1]
2✔
422
        args_str = match[2] || ""
2✔
423

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

428
        return IR::MethodCall.new(
2✔
429
          receiver: nil,
430
          method_name: method_name,
431
          arguments: arguments
432
        )
433
      end
434

435
      nil
436
    end
437

438
    # 쉼표로 분리 (괄호/배열/해시/문자열 내부는 제외)
439
    def split_by_comma(str)
1✔
440
      result = []
6✔
441
      current = ""
6✔
442
      depth = 0
6✔
443
      in_string = false
6✔
444
      string_char = nil
6✔
445

446
      str.each_char do |char|
6✔
447
        if !in_string && ['"', "'"].include?(char)
96✔
448
          in_string = true
4✔
449
          string_char = char
4✔
450
          current += char
4✔
451
        elsif in_string && char == string_char
92✔
452
          in_string = false
4✔
453
          string_char = nil
4✔
454
          current += char
4✔
455
        elsif in_string
88✔
456
          current += char
11✔
457
        else
458
          case char
77✔
459
          when "(", "[", "{"
UNCOV
460
            depth += 1
×
UNCOV
461
            current += char
×
462
          when ")", "]", "}"
UNCOV
463
            depth -= 1
×
UNCOV
464
            current += char
×
465
          when ","
466
            if depth.zero?
9✔
467
              result << current.strip
9✔
468
              current = ""
9✔
469
            else
UNCOV
470
              current += char
×
471
            end
472
          else
473
            current += char
68✔
474
          end
475
        end
476
      end
477

478
      result << current.strip unless current.strip.empty?
6✔
479
      result
6✔
480
    end
481
  end
482
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