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

type-ruby / t-ruby / 20525264502

26 Dec 2025 03:58PM UTC coverage: 76.169% (+0.1%) from 76.02%
20525264502

Pull #22

github

web-flow
Merge 8e9648dee into a9d85aa44
Pull Request #22: fix: parser improvements for v0.0.40

75 of 82 new or added lines in 6 files covered. (91.46%)

2 existing lines in 2 files now uncovered.

5082 of 6672 relevant lines covered (76.17%)

1195.08 hits per line

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

97.45
/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
  class Parser
1✔
7
    # Type names that are recognized as valid
8
    VALID_TYPES = %w[String Integer Boolean Array Hash Symbol void nil].freeze
1✔
9

10
    # Pattern for method/variable names that supports Unicode characters
11
    # \p{L} matches any Unicode letter, \p{N} matches any Unicode number
12
    IDENTIFIER_CHAR = '[\p{L}\p{N}_]'
1✔
13
    # Method names can end with ? or !
14
    METHOD_NAME_PATTERN = "#{IDENTIFIER_CHAR}+[?!]?".freeze
1✔
15
    # Visibility modifiers for method definitions
16
    VISIBILITY_PATTERN = '(?:(?:private|protected|public)\s+)?'
1✔
17

18
    attr_reader :source, :ir_program
1✔
19

20
    def initialize(source, parse_body: true)
1✔
21
      @source = source
233✔
22
      @lines = source.split("\n")
233✔
23
      @parse_body = parse_body
233✔
24
      @type_parser = ParserCombinator::TypeParser.new
233✔
25
      @body_parser = BodyParser.new if parse_body
233✔
26
      @ir_program = nil
233✔
27
    end
28

29
    def parse
1✔
30
      functions = []
233✔
31
      type_aliases = []
233✔
32
      interfaces = []
233✔
33
      classes = []
233✔
34
      i = 0
233✔
35

36
      # Pre-detect heredoc regions to skip
37
      heredoc_ranges = HeredocDetector.detect(@lines)
233✔
38

39
      while i < @lines.length
233✔
40
        # Skip lines inside heredoc content
41
        if HeredocDetector.inside_heredoc?(i, heredoc_ranges)
21,082✔
42
          i += 1
16✔
43
          next
16✔
44
        end
45

46
        line = @lines[i]
21,066✔
47

48
        # Match type alias definitions
49
        if line.match?(/^\s*type\s+\w+/)
21,066✔
50
          alias_info = parse_type_alias(line)
89✔
51
          type_aliases << alias_info if alias_info
89✔
52
        end
53

54
        # Match interface definitions
55
        if line.match?(/^\s*interface\s+\w+/)
21,066✔
56
          interface_info, next_i = parse_interface(i)
47✔
57
          if interface_info
47✔
58
            interfaces << interface_info
47✔
59
            i = next_i
47✔
60
            next
47✔
61
          end
62
        end
63

64
        # Match class definitions
65
        if line.match?(/^\s*class\s+\w+/)
21,019✔
66
          class_info, next_i = parse_class(i)
21✔
67
          if class_info
21✔
68
            classes << class_info
21✔
69
            i = next_i
21✔
70
            next
21✔
71
          end
72
        end
73

74
        # Match function definitions (top-level only, not inside class)
75
        if line.match?(/^\s*#{VISIBILITY_PATTERN}def\s+#{IDENTIFIER_CHAR}+/)
20,998✔
76
          func_info, next_i = parse_function_with_body(i)
3,612✔
77
          if func_info
3,612✔
78
            functions << func_info
3,602✔
79
            i = next_i
3,602✔
80
            next
3,602✔
81
          end
82
        end
83

84
        i += 1
17,396✔
85
      end
86

87
      result = {
88
        type: :success,
233✔
89
        functions: functions,
90
        type_aliases: type_aliases,
91
        interfaces: interfaces,
92
        classes: classes,
93
      }
94

95
      # Build IR
96
      builder = IR::Builder.new
233✔
97
      @ir_program = builder.build(result, source: @source)
233✔
98

99
      result
233✔
100
    end
101

102
    # Parse to IR directly (new API)
103
    def parse_to_ir
1✔
104
      parse unless @ir_program
×
105
      @ir_program
×
106
    end
107

108
    # Parse a type expression using combinator
109
    def parse_type(type_string)
1✔
110
      result = @type_parser.parse(type_string)
×
UNCOV
111
      result[:success] ? result[:type] : nil
×
112
    end
113

114
    private
1✔
115

116
    # 최상위 함수를 본문까지 포함하여 파싱
117
    def parse_function_with_body(start_index)
1✔
118
      line = @lines[start_index]
3,612✔
119
      func_info = parse_function_definition(line)
3,612✔
120
      return [nil, start_index] unless func_info
3,612✔
121

122
      def_indent = line.match(/^(\s*)/)[1].length
3,602✔
123
      i = start_index + 1
3,602✔
124
      body_start = i
3,602✔
125
      body_end = i
3,602✔
126

127
      # end 키워드 찾기
128
      while i < @lines.length
3,602✔
129
        current_line = @lines[i]
7,151✔
130

131
        if current_line.match?(/^\s*end\s*$/)
7,151✔
132
          end_indent = current_line.match(/^(\s*)/)[1].length
3,600✔
133
          if end_indent <= def_indent
3,600✔
134
            body_end = i
3,600✔
135
            break
3,600✔
136
          end
137
        end
138

139
        i += 1
3,551✔
140
      end
141

142
      # 본문 파싱 (parse_body 옵션이 활성화된 경우)
143
      if @parse_body && @body_parser && body_start < body_end
3,602✔
144
        func_info[:body_ir] = @body_parser.parse(@lines, body_start, body_end)
3,550✔
145
        func_info[:body_range] = { start: body_start, end: body_end }
3,550✔
146
      end
147

148
      [func_info, i]
3,602✔
149
    end
150

151
    def parse_type_alias(line)
1✔
152
      match = line.match(/^\s*type\s+(\w+)\s*=\s*(.+?)\s*$/)
89✔
153
      return nil unless match
89✔
154

155
      alias_name = match[1]
77✔
156
      definition = match[2].strip
77✔
157

158
      # Use combinator for complex type parsing
159
      type_result = @type_parser.parse(definition)
77✔
160
      if type_result[:success]
77✔
161
        return {
162
          name: alias_name,
75✔
163
          definition: definition,
164
          ir_type: type_result[:type],
165
        }
166
      end
167

168
      {
169
        name: alias_name,
2✔
170
        definition: definition,
171
      }
172
    end
173

174
    def parse_function_definition(line)
1✔
175
      # Match methods with or without parentheses
176
      # def foo(params): Type   - with params and return type
177
      # def foo(): Type         - no params but with return type
178
      # def foo(params)         - with params, no return type
179
      # def foo                  - no params, no return type
180
      # Also supports visibility modifiers: private def, protected def, public def
181
      match = line.match(/^\s*(?:(private|protected|public)\s+)?def\s+(#{METHOD_NAME_PATTERN})\s*(?:\((.*?)\))?\s*(?::\s*(.+?))?\s*$/)
3,640✔
182
      return nil unless match
3,640✔
183

184
      visibility = match[1] ? match[1].to_sym : :public
3,630✔
185
      function_name = match[2]
3,630✔
186
      params_str = match[3] || ""
3,630✔
187
      return_type_str = match[4]&.strip
3,630✔
188

189
      # Validate return type if present
190
      if return_type_str
3,630✔
191
        return_type_str = validate_and_extract_type(return_type_str)
3,605✔
192
      end
193

194
      params = parse_parameters(params_str)
3,630✔
195

196
      result = {
197
        name: function_name,
3,630✔
198
        params: params,
199
        return_type: return_type_str,
200
        visibility: visibility,
201
      }
202

203
      # Parse return type with combinator
204
      if return_type_str
3,630✔
205
        type_result = @type_parser.parse(return_type_str)
3,604✔
206
        result[:ir_return_type] = type_result[:type] if type_result[:success]
3,604✔
207
      end
208

209
      result
3,630✔
210
    end
211

212
    # Validate type string and return nil if invalid
213
    def validate_and_extract_type(type_str)
1✔
214
      return nil if type_str.nil? || type_str.empty?
3,605✔
215

216
      # Check for whitespace in simple type names that would be invalid
217
      # Pattern: Capital letter followed by lowercase, then space, then more lowercase
218
      # e.g., "Str ing", "Int eger", "Bool ean"
219
      if type_str.match?(/^[A-Z][a-z]*\s+[a-z]+/)
3,605✔
220
        return nil
1✔
221
      end
222

223
      # Check for trailing operators
224
      return nil if type_str.match?(/[|&]\s*$/)
3,604✔
225

226
      # Check for leading operators
227
      return nil if type_str.match?(/^\s*[|&]/)
3,604✔
228

229
      # Check for unbalanced brackets
230
      return nil if type_str.count("<") != type_str.count(">")
3,604✔
231
      return nil if type_str.count("[") != type_str.count("]")
3,604✔
232
      return nil if type_str.count("(") != type_str.count(")")
3,604✔
233

234
      # Check for empty generic arguments
235
      return nil if type_str.match?(/<\s*>/)
3,604✔
236

237
      type_str
3,604✔
238
    end
239

240
    def parse_parameters(params_str)
1✔
241
      return [] if params_str.empty?
3,630✔
242

243
      parameters = []
3,587✔
244
      param_list = split_params(params_str)
3,587✔
245

246
      param_list.each do |param|
3,587✔
247
        param_info = parse_single_parameter(param)
3,604✔
248
        parameters << param_info if param_info
3,604✔
249
      end
250

251
      parameters
3,587✔
252
    end
253

254
    def split_params(params_str)
1✔
255
      # Handle nested generics like Array<Map<String, Int>>
256
      result = []
3,587✔
257
      current = ""
3,587✔
258
      depth = 0
3,587✔
259

260
      params_str.each_char do |char|
3,587✔
261
        case char
39,709✔
262
        when "<", "[", "("
263
          depth += 1
3✔
264
          current += char
3✔
265
        when ">", "]", ")"
266
          depth -= 1
3✔
267
          current += char
3✔
268
        when ","
269
          if depth.zero?
17✔
270
            result << current.strip
17✔
271
            current = ""
17✔
272
          else
273
            current += char
×
274
          end
275
        else
276
          current += char
39,686✔
277
        end
278
      end
279

280
      result << current.strip unless current.empty?
3,587✔
281
      result
3,587✔
282
    end
283

284
    def parse_single_parameter(param)
1✔
285
      match = param.match(/^(\w+)(?::\s*(.+?))?$/)
3,604✔
286
      return nil unless match
3,604✔
287

288
      param_name = match[1]
3,598✔
289
      type_str = match[2]&.strip
3,598✔
290

291
      result = {
292
        name: param_name,
3,598✔
293
        type: type_str,
294
      }
295

296
      # Parse type with combinator
297
      if type_str
3,598✔
298
        type_result = @type_parser.parse(type_str)
3,591✔
299
        result[:ir_type] = type_result[:type] if type_result[:success]
3,591✔
300
      end
301

302
      result
3,598✔
303
    end
304

305
    def parse_class(start_index)
1✔
306
      line = @lines[start_index]
21✔
307
      match = line.match(/^\s*class\s+(\w+)(?:\s*<\s*(\w+))?/)
21✔
308
      return [nil, start_index] unless match
21✔
309

310
      class_name = match[1]
21✔
311
      superclass = match[2]
21✔
312
      methods = []
21✔
313
      instance_vars = []
21✔
314
      i = start_index + 1
21✔
315
      class_indent = line.match(/^(\s*)/)[1].length
21✔
316
      class_end = i
21✔
317

318
      # 먼저 클래스의 끝을 찾음
319
      temp_i = i
21✔
320
      while temp_i < @lines.length
21✔
321
        current_line = @lines[temp_i]
113✔
322
        if current_line.match?(/^\s*end\s*$/)
113✔
323
          end_indent = current_line.match(/^(\s*)/)[1].length
49✔
324
          if end_indent <= class_indent
49✔
325
            class_end = temp_i
21✔
326
            break
21✔
327
          end
328
        end
329
        temp_i += 1
92✔
330
      end
331

332
      while i < class_end
21✔
333
        current_line = @lines[i]
63✔
334

335
        # Match method definitions inside class
336
        if current_line.match?(/^\s*#{VISIBILITY_PATTERN}def\s+#{IDENTIFIER_CHAR}+/)
63✔
337
          method_info, next_i = parse_method_in_class(i, class_end)
28✔
338
          if method_info
28✔
339
            methods << method_info
28✔
340
            i = next_i
28✔
341
            next
28✔
342
          end
343
        end
344

345
        i += 1
35✔
346
      end
347

348
      # 메서드 본문에서 인스턴스 변수 추출
349
      methods.each do |method_info|
21✔
350
        extract_instance_vars_from_body(method_info[:body_ir], instance_vars)
28✔
351
      end
352

353
      # Try to infer instance variable types from initialize parameters
354
      init_method = methods.find { |m| m[:name] == "initialize" }
45✔
355
      if init_method
21✔
356
        instance_vars.each do |ivar|
5✔
357
          # Find matching parameter (e.g., @name = name)
358
          matching_param = init_method[:params]&.find { |p| p[:name] == ivar[:name] }
13✔
359
          ivar[:type] = matching_param[:type] if matching_param && matching_param[:type]
6✔
360
          ivar[:ir_type] = matching_param[:ir_type] if matching_param && matching_param[:ir_type]
6✔
361
        end
362
      end
363

364
      [{
365
        name: class_name,
21✔
366
        superclass: superclass,
367
        methods: methods,
368
        instance_vars: instance_vars,
369
      }, class_end,]
370
    end
371

372
    # 클래스 내부의 메서드를 본문까지 포함하여 파싱
373
    def parse_method_in_class(start_index, class_end)
1✔
374
      line = @lines[start_index]
28✔
375
      method_info = parse_function_definition(line)
28✔
376
      return [nil, start_index] unless method_info
28✔
377

378
      def_indent = line.match(/^(\s*)/)[1].length
28✔
379
      i = start_index + 1
28✔
380
      body_start = i
28✔
381
      body_end = i
28✔
382

383
      # 메서드의 end 키워드 찾기
384
      while i < class_end
28✔
385
        current_line = @lines[i]
57✔
386

387
        if current_line.match?(/^\s*end\s*$/)
57✔
388
          end_indent = current_line.match(/^(\s*)/)[1].length
28✔
389
          if end_indent <= def_indent
28✔
390
            body_end = i
28✔
391
            break
28✔
392
          end
393
        end
394

395
        i += 1
29✔
396
      end
397

398
      # 본문 파싱 (parse_body 옵션이 활성화된 경우)
399
      if @parse_body && @body_parser && body_start < body_end
28✔
400
        method_info[:body_ir] = @body_parser.parse(@lines, body_start, body_end)
28✔
401
        method_info[:body_range] = { start: body_start, end: body_end }
28✔
402
      end
403

404
      [method_info, i]
28✔
405
    end
406

407
    # 본문 IR에서 인스턴스 변수 추출
408
    def extract_instance_vars_from_body(body_ir, instance_vars)
1✔
409
      return unless body_ir.is_a?(IR::Block)
28✔
410

411
      body_ir.statements.each do |stmt|
28✔
412
        case stmt
29✔
413
        when IR::Assignment
414
          if stmt.target.start_with?("@") && !stmt.target.start_with?("@@")
6✔
415
            ivar_name = stmt.target[1..] # @ 제거
6✔
416
            unless instance_vars.any? { |iv| iv[:name] == ivar_name }
7✔
417
              instance_vars << { name: ivar_name }
6✔
418
            end
419
          end
420
        when IR::Block
421
          extract_instance_vars_from_body(stmt, instance_vars)
×
422
        end
423
      end
424
    end
425

426
    def parse_interface(start_index)
1✔
427
      line = @lines[start_index]
47✔
428
      match = line.match(/^\s*interface\s+([\w:]+)/)
47✔
429
      return [nil, start_index] unless match
47✔
430

431
      interface_name = match[1]
47✔
432
      members = []
47✔
433
      i = start_index + 1
47✔
434

435
      while i < @lines.length
47✔
436
        current_line = @lines[i]
139✔
437
        break if current_line.match?(/^\s*end\s*$/)
139✔
438

439
        if current_line.match?(/^\s*[\w!?]+\s*:\s*/)
92✔
440
          member_match = current_line.match(/^\s*([\w!?]+)\s*:\s*(.+?)\s*$/)
91✔
441
          if member_match
91✔
442
            member = {
443
              name: member_match[1],
91✔
444
              type: member_match[2].strip,
445
            }
446

447
            # Parse member type with combinator
448
            type_result = @type_parser.parse(member[:type])
91✔
449
            member[:ir_type] = type_result[:type] if type_result[:success]
91✔
450

451
            members << member
91✔
452
          end
453
        end
454

455
        i += 1
92✔
456
      end
457

458
      [{ name: interface_name, members: members }, i]
47✔
459
    end
460
  end
461
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