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

type-ruby / t-ruby / 20477240232

24 Dec 2025 03:05AM UTC coverage: 75.685% (+1.4%) from 74.246%
20477240232

push

github

yhk1038
chore: add rbs gem and fix watcher/spec for type inference

- Add rbs gem dependency for E2E test RBS validation
- Fix watcher to handle single file paths (not just directories)
- Fix ir_spec to use keyword argument format (name: String)

9 of 9 new or added lines in 1 file covered. (100.0%)

166 existing lines in 3 files now uncovered.

4974 of 6572 relevant lines covered (75.68%)

1181.73 hits per line

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

96.6
/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
    attr_reader :source, :ir_program, :use_combinator
1✔
11

12
    def initialize(source, use_combinator: true, parse_body: true)
1✔
13
      @source = source
214✔
14
      @lines = source.split("\n")
214✔
15
      @use_combinator = use_combinator
214✔
16
      @parse_body = parse_body
214✔
17
      @type_parser = ParserCombinator::TypeParser.new if use_combinator
214✔
18
      @body_parser = BodyParser.new if parse_body
214✔
19
      @ir_program = nil
214✔
20
    end
21

22
    def parse
1✔
23
      functions = []
214✔
24
      type_aliases = []
214✔
25
      interfaces = []
214✔
26
      classes = []
214✔
27
      i = 0
214✔
28

29
      while i < @lines.length
214✔
30
        line = @lines[i]
21,021✔
31

32
        # Match type alias definitions
33
        if line.match?(/^\s*type\s+\w+/)
21,021✔
34
          alias_info = parse_type_alias(line)
89✔
35
          type_aliases << alias_info if alias_info
89✔
36
        end
37

38
        # Match interface definitions
39
        if line.match?(/^\s*interface\s+\w+/)
21,021✔
40
          interface_info, next_i = parse_interface(i)
47✔
41
          if interface_info
47✔
42
            interfaces << interface_info
47✔
43
            i = next_i
47✔
44
            next
47✔
45
          end
46
        end
47

48
        # Match class definitions
49
        if line.match?(/^\s*class\s+\w+/)
20,974✔
50
          class_info, next_i = parse_class(i)
15✔
51
          if class_info
15✔
52
            classes << class_info
15✔
53
            i = next_i
15✔
54
            next
15✔
55
          end
56
        end
57

58
        # Match function definitions (top-level only, not inside class)
59
        if line.match?(/^\s*def\s+\w+/)
20,959✔
60
          func_info, next_i = parse_function_with_body(i)
3,599✔
61
          if func_info
3,599✔
62
            functions << func_info
3,589✔
63
            i = next_i
3,589✔
64
            next
3,589✔
65
          end
66
        end
67

68
        i += 1
17,370✔
69
      end
70

71
      result = {
72
        type: :success,
214✔
73
        functions: functions,
74
        type_aliases: type_aliases,
75
        interfaces: interfaces,
76
        classes: classes,
77
      }
78

79
      # Build IR if combinator is enabled
80
      if @use_combinator
214✔
81
        builder = IR::Builder.new
214✔
82
        @ir_program = builder.build(result, source: @source)
214✔
83
      end
84

85
      result
214✔
86
    end
87

88
    # Parse to IR directly (new API)
89
    def parse_to_ir
1✔
UNCOV
90
      parse unless @ir_program
×
UNCOV
91
      @ir_program
×
92
    end
93

94
    # Parse a type expression using combinator (new API)
95
    def parse_type(type_string)
1✔
UNCOV
96
      return nil unless @use_combinator
×
97

UNCOV
98
      result = @type_parser.parse(type_string)
×
UNCOV
99
      result[:success] ? result[:type] : nil
×
100
    end
101

102
    private
1✔
103

104
    # 최상위 함수를 본문까지 포함하여 파싱
105
    def parse_function_with_body(start_index)
1✔
106
      line = @lines[start_index]
3,599✔
107
      func_info = parse_function_definition(line)
3,599✔
108
      return [nil, start_index] unless func_info
3,599✔
109

110
      def_indent = line.match(/^(\s*)/)[1].length
3,589✔
111
      i = start_index + 1
3,589✔
112
      body_start = i
3,589✔
113
      body_end = i
3,589✔
114

115
      # end 키워드 찾기
116
      while i < @lines.length
3,589✔
117
        current_line = @lines[i]
7,125✔
118

119
        if current_line.match?(/^\s*end\s*$/)
7,125✔
120
          end_indent = current_line.match(/^(\s*)/)[1].length
3,587✔
121
          if end_indent <= def_indent
3,587✔
122
            body_end = i
3,587✔
123
            break
3,587✔
124
          end
125
        end
126

127
        i += 1
3,538✔
128
      end
129

130
      # 본문 파싱 (parse_body 옵션이 활성화된 경우)
131
      if @parse_body && @body_parser && body_start < body_end
3,589✔
132
        func_info[:body_ir] = @body_parser.parse(@lines, body_start, body_end)
3,537✔
133
        func_info[:body_range] = { start: body_start, end: body_end }
3,537✔
134
      end
135

136
      [func_info, i]
3,589✔
137
    end
138

139
    def parse_type_alias(line)
1✔
140
      match = line.match(/^\s*type\s+(\w+)\s*=\s*(.+?)\s*$/)
89✔
141
      return nil unless match
89✔
142

143
      alias_name = match[1]
77✔
144
      definition = match[2].strip
77✔
145

146
      # Use combinator for complex type parsing if available
147
      if @use_combinator
77✔
148
        type_result = @type_parser.parse(definition)
77✔
149
        if type_result[:success]
77✔
150
          return {
151
            name: alias_name,
75✔
152
            definition: definition,
153
            ir_type: type_result[:type],
154
          }
155
        end
156
      end
157

158
      {
159
        name: alias_name,
2✔
160
        definition: definition,
161
      }
162
    end
163

164
    def parse_function_definition(line)
1✔
165
      # Match methods with or without parentheses
166
      # def foo(params): Type   - with params and return type
167
      # def foo(): Type         - no params but with return type
168
      # def foo(params)         - with params, no return type
169
      # def foo                  - no params, no return type
170
      match = line.match(/^\s*def\s+([\w?!]+)\s*(?:\((.*?)\))?\s*(?::\s*(.+?))?\s*$/)
3,619✔
171
      return nil unless match
3,619✔
172

173
      function_name = match[1]
3,609✔
174
      params_str = match[2] || ""
3,609✔
175
      return_type_str = match[3]&.strip
3,609✔
176

177
      # Validate return type if present
178
      if return_type_str
3,609✔
179
        return_type_str = validate_and_extract_type(return_type_str)
3,585✔
180
      end
181

182
      params = parse_parameters(params_str)
3,609✔
183

184
      result = {
185
        name: function_name,
3,609✔
186
        params: params,
187
        return_type: return_type_str,
188
      }
189

190
      # Parse return type with combinator if available
191
      if @use_combinator && return_type_str
3,609✔
192
        type_result = @type_parser.parse(return_type_str)
3,584✔
193
        result[:ir_return_type] = type_result[:type] if type_result[:success]
3,584✔
194
      end
195

196
      result
3,609✔
197
    end
198

199
    # Validate type string and return nil if invalid
200
    def validate_and_extract_type(type_str)
1✔
201
      return nil if type_str.nil? || type_str.empty?
3,585✔
202

203
      # Check for whitespace in simple type names that would be invalid
204
      # Pattern: Capital letter followed by lowercase, then space, then more lowercase
205
      # e.g., "Str ing", "Int eger", "Bool ean"
206
      if type_str.match?(/^[A-Z][a-z]*\s+[a-z]+/)
3,585✔
207
        return nil
1✔
208
      end
209

210
      # Check for trailing operators
211
      return nil if type_str.match?(/[|&]\s*$/)
3,584✔
212

213
      # Check for leading operators
214
      return nil if type_str.match?(/^\s*[|&]/)
3,584✔
215

216
      # Check for unbalanced brackets
217
      return nil if type_str.count("<") != type_str.count(">")
3,584✔
218
      return nil if type_str.count("[") != type_str.count("]")
3,584✔
219
      return nil if type_str.count("(") != type_str.count(")")
3,584✔
220

221
      # Check for empty generic arguments
222
      return nil if type_str.match?(/<\s*>/)
3,584✔
223

224
      type_str
3,584✔
225
    end
226

227
    def parse_parameters(params_str)
1✔
228
      return [] if params_str.empty?
3,609✔
229

230
      parameters = []
3,570✔
231
      param_list = split_params(params_str)
3,570✔
232

233
      param_list.each do |param|
3,570✔
234
        param_info = parse_single_parameter(param)
3,586✔
235
        parameters << param_info if param_info
3,586✔
236
      end
237

238
      parameters
3,570✔
239
    end
240

241
    def split_params(params_str)
1✔
242
      # Handle nested generics like Array<Map<String, Int>>
243
      result = []
3,570✔
244
      current = ""
3,570✔
245
      depth = 0
3,570✔
246

247
      params_str.each_char do |char|
3,570✔
248
        case char
39,507✔
249
        when "<", "[", "("
250
          depth += 1
3✔
251
          current += char
3✔
252
        when ">", "]", ")"
253
          depth -= 1
3✔
254
          current += char
3✔
255
        when ","
256
          if depth.zero?
16✔
257
            result << current.strip
16✔
258
            current = ""
16✔
259
          else
UNCOV
260
            current += char
×
261
          end
262
        else
263
          current += char
39,485✔
264
        end
265
      end
266

267
      result << current.strip unless current.empty?
3,570✔
268
      result
3,570✔
269
    end
270

271
    def parse_single_parameter(param)
1✔
272
      match = param.match(/^(\w+)(?::\s*(.+?))?$/)
3,586✔
273
      return nil unless match
3,586✔
274

275
      param_name = match[1]
3,580✔
276
      type_str = match[2]&.strip
3,580✔
277

278
      result = {
279
        name: param_name,
3,580✔
280
        type: type_str,
281
      }
282

283
      # Parse type with combinator if available
284
      if @use_combinator && type_str
3,580✔
285
        type_result = @type_parser.parse(type_str)
3,573✔
286
        result[:ir_type] = type_result[:type] if type_result[:success]
3,573✔
287
      end
288

289
      result
3,580✔
290
    end
291

292
    def parse_class(start_index)
1✔
293
      line = @lines[start_index]
15✔
294
      match = line.match(/^\s*class\s+(\w+)(?:\s*<\s*(\w+))?/)
15✔
295
      return [nil, start_index] unless match
15✔
296

297
      class_name = match[1]
15✔
298
      superclass = match[2]
15✔
299
      methods = []
15✔
300
      instance_vars = []
15✔
301
      i = start_index + 1
15✔
302
      class_indent = line.match(/^(\s*)/)[1].length
15✔
303
      class_end = i
15✔
304

305
      # 먼저 클래스의 끝을 찾음
306
      temp_i = i
15✔
307
      while temp_i < @lines.length
15✔
308
        current_line = @lines[temp_i]
81✔
309
        if current_line.match?(/^\s*end\s*$/)
81✔
310
          end_indent = current_line.match(/^(\s*)/)[1].length
35✔
311
          if end_indent <= class_indent
35✔
312
            class_end = temp_i
15✔
313
            break
15✔
314
          end
315
        end
316
        temp_i += 1
66✔
317
      end
318

319
      while i < class_end
15✔
320
        current_line = @lines[i]
45✔
321

322
        # Match method definitions inside class
323
        if current_line.match?(/^\s*def\s+\w+/)
45✔
324
          method_info, next_i = parse_method_in_class(i, class_end)
20✔
325
          if method_info
20✔
326
            methods << method_info
20✔
327
            i = next_i
20✔
328
            next
20✔
329
          end
330
        end
331

332
        i += 1
25✔
333
      end
334

335
      # 메서드 본문에서 인스턴스 변수 추출
336
      methods.each do |method_info|
15✔
337
        extract_instance_vars_from_body(method_info[:body_ir], instance_vars)
20✔
338
      end
339

340
      # Try to infer instance variable types from initialize parameters
341
      init_method = methods.find { |m| m[:name] == "initialize" }
31✔
342
      if init_method
15✔
343
        instance_vars.each do |ivar|
5✔
344
          # Find matching parameter (e.g., @name = name)
345
          matching_param = init_method[:params]&.find { |p| p[:name] == ivar[:name] }
13✔
346
          ivar[:type] = matching_param[:type] if matching_param && matching_param[:type]
6✔
347
          ivar[:ir_type] = matching_param[:ir_type] if matching_param && matching_param[:ir_type]
6✔
348
        end
349
      end
350

351
      [{
352
        name: class_name,
15✔
353
        superclass: superclass,
354
        methods: methods,
355
        instance_vars: instance_vars,
356
      }, class_end,]
357
    end
358

359
    # 클래스 내부의 메서드를 본문까지 포함하여 파싱
360
    def parse_method_in_class(start_index, class_end)
1✔
361
      line = @lines[start_index]
20✔
362
      method_info = parse_function_definition(line)
20✔
363
      return [nil, start_index] unless method_info
20✔
364

365
      def_indent = line.match(/^(\s*)/)[1].length
20✔
366
      i = start_index + 1
20✔
367
      body_start = i
20✔
368
      body_end = i
20✔
369

370
      # 메서드의 end 키워드 찾기
371
      while i < class_end
20✔
372
        current_line = @lines[i]
41✔
373

374
        if current_line.match?(/^\s*end\s*$/)
41✔
375
          end_indent = current_line.match(/^(\s*)/)[1].length
20✔
376
          if end_indent <= def_indent
20✔
377
            body_end = i
20✔
378
            break
20✔
379
          end
380
        end
381

382
        i += 1
21✔
383
      end
384

385
      # 본문 파싱 (parse_body 옵션이 활성화된 경우)
386
      if @parse_body && @body_parser && body_start < body_end
20✔
387
        method_info[:body_ir] = @body_parser.parse(@lines, body_start, body_end)
20✔
388
        method_info[:body_range] = { start: body_start, end: body_end }
20✔
389
      end
390

391
      [method_info, i]
20✔
392
    end
393

394
    # 본문 IR에서 인스턴스 변수 추출
395
    def extract_instance_vars_from_body(body_ir, instance_vars)
1✔
396
      return unless body_ir.is_a?(IR::Block)
20✔
397

398
      body_ir.statements.each do |stmt|
20✔
399
        case stmt
21✔
400
        when IR::Assignment
401
          if stmt.target.start_with?("@") && !stmt.target.start_with?("@@")
6✔
402
            ivar_name = stmt.target[1..] # @ 제거
6✔
403
            unless instance_vars.any? { |iv| iv[:name] == ivar_name }
7✔
404
              instance_vars << { name: ivar_name }
6✔
405
            end
406
          end
407
        when IR::Block
UNCOV
408
          extract_instance_vars_from_body(stmt, instance_vars)
×
409
        end
410
      end
411
    end
412

413
    def parse_interface(start_index)
1✔
414
      line = @lines[start_index]
47✔
415
      match = line.match(/^\s*interface\s+([\w:]+)/)
47✔
416
      return [nil, start_index] unless match
47✔
417

418
      interface_name = match[1]
47✔
419
      members = []
47✔
420
      i = start_index + 1
47✔
421

422
      while i < @lines.length
47✔
423
        current_line = @lines[i]
139✔
424
        break if current_line.match?(/^\s*end\s*$/)
139✔
425

426
        if current_line.match?(/^\s*[\w!?]+\s*:\s*/)
92✔
427
          member_match = current_line.match(/^\s*([\w!?]+)\s*:\s*(.+?)\s*$/)
91✔
428
          if member_match
91✔
429
            member = {
430
              name: member_match[1],
91✔
431
              type: member_match[2].strip,
432
            }
433

434
            # Parse member type with combinator
435
            if @use_combinator
91✔
436
              type_result = @type_parser.parse(member[:type])
91✔
437
              member[:ir_type] = type_result[:type] if type_result[:success]
91✔
438
            end
439

440
            members << member
91✔
441
          end
442
        end
443

444
        i += 1
92✔
445
      end
446

447
      [{ name: interface_name, members: members }, i]
47✔
448
    end
449
  end
450

451
  # Legacy Parser for backward compatibility (regex-only)
452
  class LegacyParser < Parser
1✔
453
    def initialize(source)
1✔
UNCOV
454
      super(source, use_combinator: false)
×
455
    end
456
  end
457
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