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

type-ruby / t-ruby / 20560974963

28 Dec 2025 11:21PM UTC coverage: 79.076% (+1.7%) from 77.331%
20560974963

push

github

web-flow
refactor: migrate parser from regex to token-based parser combinator (#29)

* refactor: migrate parser from regex to token-based parser combinator

- Replace monolithic parser_combinator.rb (2833 lines) with modular architecture
- Add Scanner for tokenization with regex literal support
- Create IR::InterpolatedString for string interpolation parsing
- Fix type inference for interpolated strings (returns String)
- Add TRuby::ParseError for unified error handling
- Organize parsers into primitives/, combinators/, and token/ directories
- Each file contains exactly one class (snake_case filename matches PascalCase class)

* fix: enhance parser to support ternary, splat args, and statement expressions

- Add ternary operator (? :) parsing in ExpressionParser
- Support double splat (**opts) and single splat (*args) in method calls
- Support keyword arguments (name: value) in method calls
- Allow case/if/unless/begin as assignment right-hand side values
- Improve generic type compatibility (Array[untyped] with Array[T])

Fixes type inference errors in keyword_args samples.

* style: fix RuboCop violations and adjust metrics limits

* fix: require set for Ruby 3.1 compatibility

1849 of 2098 new or added lines in 53 files covered. (88.13%)

6 existing lines in 2 files now uncovered.

6644 of 8402 relevant lines covered (79.08%)

908.09 hits per line

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

98.13
/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
256✔
22
      @lines = source.split("\n")
256✔
23
      @parse_body = parse_body
256✔
24
      @type_parser = ParserCombinator::TypeParser.new
256✔
25
      @body_parser = ParserCombinator::TokenBodyParser.new if parse_body
256✔
26
      @ir_program = nil
256✔
27
    end
28

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

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

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

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

48
        # Match type alias definitions
49
        if line.match?(/^\s*type\s+\w+/)
21,121✔
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,121✔
56
          interface_info, next_i = parse_interface(i)
50✔
57
          if interface_info
50✔
58
            interfaces << interface_info
50✔
59
            i = next_i
50✔
60
            next
50✔
61
          end
62
        end
63

64
        # Match class definitions
65
        if line.match?(/^\s*class\s+\w+/)
21,071✔
66
          class_info, next_i = parse_class(i)
22✔
67
          if class_info
22✔
68
            classes << class_info
22✔
69
            i = next_i
22✔
70
            next
22✔
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}+/)
21,049✔
76
          func_info, next_i = parse_function_with_body(i)
3,634✔
77
          if func_info
3,634✔
78
            functions << func_info
3,624✔
79
            i = next_i
3,624✔
80
            next
3,624✔
81
          end
82
        end
83

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

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

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

99
      result
256✔
100
    rescue Scanner::ScanError => e
NEW
101
      raise ParseError.new(e.message, line: e.line, column: e.column)
×
102
    end
103

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

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

116
    private
1✔
117

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

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

129
      # end 키워드 찾기
130
      while i < @lines.length
3,624✔
131
        current_line = @lines[i]
7,196✔
132

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

141
        i += 1
3,574✔
142
      end
143

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

150
      [func_info, i]
3,624✔
151
    end
152

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

157
      alias_name = match[1]
77✔
158
      definition = match[2].strip
77✔
159

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

170
      {
171
        name: alias_name,
2✔
172
        definition: definition,
173
      }
174
    end
175

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

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

191
      # Validate return type if present
192
      if return_type_str
3,654✔
193
        return_type_str = validate_and_extract_type(return_type_str)
3,628✔
194
      end
195

196
      params = parse_parameters(params_str)
3,654✔
197

198
      result = {
199
        name: function_name,
3,654✔
200
        params: params,
201
        return_type: return_type_str,
202
        visibility: visibility,
203
      }
204

205
      # Parse return type with combinator
206
      if return_type_str
3,654✔
207
        type_result = @type_parser.parse(return_type_str)
3,627✔
208
        result[:ir_return_type] = type_result[:type] if type_result[:success]
3,627✔
209
      end
210

211
      result
3,654✔
212
    end
213

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

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

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

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

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

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

239
      type_str
3,627✔
240
    end
241

242
    def parse_parameters(params_str)
1✔
243
      return [] if params_str.empty?
3,654✔
244

245
      parameters = []
3,611✔
246
      param_list = split_params(params_str)
3,611✔
247

248
      param_list.each do |param|
3,611✔
249
        param = param.strip
3,632✔
250

251
        # 1. 더블 스플랫: **name: Type
252
        if param.start_with?("**")
3,632✔
253
          param_info = parse_double_splat_parameter(param)
1✔
254
          parameters << param_info if param_info
1✔
255
        # 2. 키워드 인자 그룹: { ... } 또는 { ... }: InterfaceName
256
        elsif param.start_with?("{")
3,631✔
257
          keyword_params = parse_keyword_args_group(param)
10✔
258
          parameters.concat(keyword_params) if keyword_params
10✔
259
        # 3. Hash 리터럴: name: { ... }
260
        elsif param.match?(/^\w+:\s*\{/)
3,621✔
261
          param_info = parse_hash_literal_parameter(param)
1✔
262
          parameters << param_info if param_info
1✔
263
        # 4. 일반 위치 인자: name: Type 또는 name: Type = default
264
        else
265
          param_info = parse_single_parameter(param)
3,620✔
266
          parameters << param_info if param_info
3,620✔
267
        end
268
      end
269

270
      parameters
3,611✔
271
    end
272

273
    def split_params(params_str)
1✔
274
      # Handle nested generics, braces, brackets
275
      result = []
3,611✔
276
      current = ""
3,611✔
277
      depth = 0
3,611✔
278
      brace_depth = 0
3,611✔
279

280
      params_str.each_char do |char|
3,611✔
281
        case char
40,368✔
282
        when "<", "[", "("
283
          depth += 1
12✔
284
          current += char
12✔
285
        when ">", "]", ")"
286
          depth -= 1
12✔
287
          current += char
12✔
288
        when "{"
289
          brace_depth += 1
12✔
290
          current += char
12✔
291
        when "}"
292
          brace_depth -= 1
12✔
293
          current += char
12✔
294
        when ","
295
          if depth.zero? && brace_depth.zero?
30✔
296
            result << current.strip
21✔
297
            current = ""
21✔
298
          else
299
            current += char
9✔
300
          end
301
        else
302
          current += char
40,290✔
303
        end
304
      end
305

306
      result << current.strip unless current.empty?
3,611✔
307
      result
3,611✔
308
    end
309

310
    # 더블 스플랫 파라미터 파싱: **opts: Type
311
    def parse_double_splat_parameter(param)
1✔
312
      # **name: Type
313
      match = param.match(/^\*\*(\w+)(?::\s*(.+?))?$/)
1✔
314
      return nil unless match
1✔
315

316
      param_name = match[1]
1✔
317
      type_str = match[2]&.strip
1✔
318

319
      result = {
320
        name: param_name,
1✔
321
        type: type_str,
322
        kind: :keyrest,
323
      }
324

325
      if type_str
1✔
326
        type_result = @type_parser.parse(type_str)
1✔
327
        result[:ir_type] = type_result[:type] if type_result[:success]
1✔
328
      end
329

330
      result
1✔
331
    end
332

333
    # 키워드 인자 그룹 파싱: { name: String, age: Integer = 0 } 또는 { name:, age: 0 }: InterfaceName
334
    def parse_keyword_args_group(param)
1✔
335
      # { ... }: InterfaceName 형태 확인
336
      # 또는 { ... } 만 있는 형태 (인라인 타입)
337
      interface_match = param.match(/^\{(.+)\}\s*:\s*(\w+)\s*$/)
10✔
338
      inline_match = param.match(/^\{(.+)\}\s*$/) unless interface_match
10✔
339

340
      if interface_match
10✔
341
        inner_content = interface_match[1]
2✔
342
        interface_name = interface_match[2]
2✔
343
        parse_keyword_args_with_interface(inner_content, interface_name)
2✔
344
      elsif inline_match
8✔
345
        inner_content = inline_match[1]
8✔
346
        parse_keyword_args_inline(inner_content)
8✔
347
      end
348
    end
349

350
    # interface 참조 키워드 인자 파싱: { name:, age: 0 }: UserParams
351
    def parse_keyword_args_with_interface(inner_content, interface_name)
1✔
352
      parameters = []
2✔
353
      parts = split_keyword_args(inner_content)
2✔
354

355
      parts.each do |part|
2✔
356
        part = part.strip
5✔
357
        next if part.empty?
5✔
358

359
        # name: default_value 또는 name: 형태
360
        next unless part.match?(/^(\w+):\s*(.*)$/)
5✔
361

362
        match = part.match(/^(\w+):\s*(.*)$/)
5✔
363
        param_name = match[1]
5✔
364
        default_value = match[2].strip
5✔
365
        default_value = nil if default_value.empty?
5✔
366

367
        parameters << {
5✔
368
          name: param_name,
369
          type: nil, # interface에서 타입을 가져옴
370
          default_value: default_value,
371
          kind: :keyword,
372
          interface_ref: interface_name,
373
        }
374
      end
375

376
      parameters
2✔
377
    end
378

379
    # 인라인 타입 키워드 인자 파싱: { name: String, age: Integer = 0 }
380
    def parse_keyword_args_inline(inner_content)
1✔
381
      parameters = []
8✔
382
      parts = split_keyword_args(inner_content)
8✔
383

384
      parts.each do |part|
8✔
385
        part = part.strip
12✔
386
        next if part.empty?
12✔
387

388
        # name: Type = default 또는 name: Type 형태
389
        next unless part.match?(/^(\w+):\s*(.+)$/)
12✔
390

391
        match = part.match(/^(\w+):\s*(.+)$/)
12✔
392
        param_name = match[1]
12✔
393
        type_and_default = match[2].strip
12✔
394

395
        # Type = default 분리
396
        type_str, default_value = split_type_and_default(type_and_default)
12✔
397

398
        result = {
399
          name: param_name,
12✔
400
          type: type_str,
401
          default_value: default_value,
402
          kind: :keyword,
403
        }
404

405
        if type_str
12✔
406
          type_result = @type_parser.parse(type_str)
12✔
407
          result[:ir_type] = type_result[:type] if type_result[:success]
12✔
408
        end
409

410
        parameters << result
12✔
411
      end
412

413
      parameters
8✔
414
    end
415

416
    # 키워드 인자 내부를 콤마로 분리 (중첩된 제네릭/배열/해시 고려)
417
    def split_keyword_args(content)
1✔
418
      StringUtils.split_by_comma(content)
10✔
419
    end
420

421
    # 타입과 기본값 분리: "String = 0" -> ["String", "0"]
422
    def split_type_and_default(type_and_default)
1✔
423
      StringUtils.split_type_and_default(type_and_default)
3,615✔
424
    end
425

426
    # Hash 리터럴 파라미터 파싱: config: { host: String, port: Integer }
427
    def parse_hash_literal_parameter(param)
1✔
428
      # name: { ... } 또는 name: { ... }: InterfaceName
429
      match = param.match(/^(\w+):\s*(\{.+\})(?::\s*(\w+))?$/)
1✔
430
      return nil unless match
1✔
431

432
      param_name = match[1]
1✔
433
      hash_type = match[2]
1✔
434
      interface_name = match[3]
1✔
435

436
      result = {
437
        name: param_name,
1✔
438
        type: interface_name || hash_type,
439
        kind: :required,
440
        hash_type_def: hash_type, # 원본 해시 타입 정의 저장
441
      }
442

443
      result[:interface_ref] = interface_name if interface_name
1✔
444

445
      result
1✔
446
    end
447

448
    def parse_single_parameter(param)
1✔
449
      # name: Type = default 또는 name: Type 또는 name
450
      # 기본값이 있는 경우 먼저 처리
451
      type_str = nil
3,620✔
452
      default_value = nil
3,620✔
453

454
      if param.include?(":")
3,620✔
455
        match = param.match(/^(\w+):\s*(.+)$/)
3,613✔
456
        return nil unless match
3,613✔
457

458
        param_name = match[1]
3,603✔
459
        type_and_default = match[2].strip
3,603✔
460
        type_str, default_value = split_type_and_default(type_and_default)
3,603✔
461
      else
462
        # 타입 없이 이름만 있는 경우
463
        param_name = param.strip
7✔
464
      end
465

466
      result = {
467
        name: param_name,
3,610✔
468
        type: type_str,
469
        default_value: default_value,
470
        kind: default_value ? :optional : :required,
3,610✔
471
      }
472

473
      # Parse type with combinator
474
      if type_str
3,610✔
475
        type_result = @type_parser.parse(type_str)
3,603✔
476
        result[:ir_type] = type_result[:type] if type_result[:success]
3,603✔
477
      end
478

479
      result
3,610✔
480
    end
481

482
    def parse_class(start_index)
1✔
483
      line = @lines[start_index]
22✔
484
      match = line.match(/^\s*class\s+(\w+)(?:\s*<\s*(\w+))?/)
22✔
485
      return [nil, start_index] unless match
22✔
486

487
      class_name = match[1]
22✔
488
      superclass = match[2]
22✔
489
      methods = []
22✔
490
      instance_vars = []
22✔
491
      i = start_index + 1
22✔
492
      class_indent = line.match(/^(\s*)/)[1].length
22✔
493
      class_end = i
22✔
494

495
      # 먼저 클래스의 끝을 찾음
496
      temp_i = i
22✔
497
      while temp_i < @lines.length
22✔
498
        current_line = @lines[temp_i]
122✔
499
        if current_line.match?(/^\s*end\s*$/)
122✔
500
          end_indent = current_line.match(/^(\s*)/)[1].length
52✔
501
          if end_indent <= class_indent
52✔
502
            class_end = temp_i
22✔
503
            break
22✔
504
          end
505
        end
506
        temp_i += 1
100✔
507
      end
508

509
      while i < class_end
22✔
510
        current_line = @lines[i]
68✔
511

512
        # Match method definitions inside class
513
        if current_line.match?(/^\s*#{VISIBILITY_PATTERN}def\s+#{IDENTIFIER_CHAR}+/)
68✔
514
          method_info, next_i = parse_method_in_class(i, class_end)
30✔
515
          if method_info
30✔
516
            methods << method_info
30✔
517
            i = next_i
30✔
518
            next
30✔
519
          end
520
        end
521

522
        i += 1
38✔
523
      end
524

525
      # 메서드 본문에서 인스턴스 변수 추출
526
      methods.each do |method_info|
22✔
527
        extract_instance_vars_from_body(method_info[:body_ir], instance_vars)
30✔
528
      end
529

530
      # Try to infer instance variable types from initialize parameters
531
      init_method = methods.find { |m| m[:name] == "initialize" }
47✔
532
      if init_method
22✔
533
        instance_vars.each do |ivar|
6✔
534
          # Find matching parameter (e.g., @name = name)
535
          matching_param = init_method[:params]&.find { |p| p[:name] == ivar[:name] }
18✔
536
          ivar[:type] = matching_param[:type] if matching_param && matching_param[:type]
8✔
537
          ivar[:ir_type] = matching_param[:ir_type] if matching_param && matching_param[:ir_type]
8✔
538
        end
539
      end
540

541
      [{
542
        name: class_name,
22✔
543
        superclass: superclass,
544
        methods: methods,
545
        instance_vars: instance_vars,
546
      }, class_end,]
547
    end
548

549
    # 클래스 내부의 메서드를 본문까지 포함하여 파싱
550
    def parse_method_in_class(start_index, class_end)
1✔
551
      line = @lines[start_index]
30✔
552
      method_info = parse_function_definition(line)
30✔
553
      return [nil, start_index] unless method_info
30✔
554

555
      def_indent = line.match(/^(\s*)/)[1].length
30✔
556
      i = start_index + 1
30✔
557
      body_start = i
30✔
558
      body_end = i
30✔
559

560
      # 메서드의 end 키워드 찾기
561
      while i < class_end
30✔
562
        current_line = @lines[i]
62✔
563

564
        if current_line.match?(/^\s*end\s*$/)
62✔
565
          end_indent = current_line.match(/^(\s*)/)[1].length
30✔
566
          if end_indent <= def_indent
30✔
567
            body_end = i
30✔
568
            break
30✔
569
          end
570
        end
571

572
        i += 1
32✔
573
      end
574

575
      # 본문 파싱 (parse_body 옵션이 활성화된 경우)
576
      if @parse_body && @body_parser && body_start < body_end
30✔
577
        method_info[:body_ir] = @body_parser.parse(@lines, body_start, body_end)
30✔
578
        method_info[:body_range] = { start: body_start, end: body_end }
30✔
579
      end
580

581
      [method_info, i]
30✔
582
    end
583

584
    # 본문 IR에서 인스턴스 변수 추출
585
    def extract_instance_vars_from_body(body_ir, instance_vars)
1✔
586
      return unless body_ir.is_a?(IR::Block)
30✔
587

588
      body_ir.statements.each do |stmt|
30✔
589
        case stmt
32✔
590
        when IR::Assignment
591
          if stmt.target.start_with?("@") && !stmt.target.start_with?("@@")
8✔
592
            ivar_name = stmt.target[1..] # @ 제거
8✔
593
            unless instance_vars.any? { |iv| iv[:name] == ivar_name }
10✔
594
              instance_vars << { name: ivar_name }
8✔
595
            end
596
          end
597
        when IR::Block
598
          extract_instance_vars_from_body(stmt, instance_vars)
×
599
        end
600
      end
601
    end
602

603
    def parse_interface(start_index)
1✔
604
      line = @lines[start_index]
50✔
605
      match = line.match(/^\s*interface\s+([\w:]+)/)
50✔
606
      return [nil, start_index] unless match
50✔
607

608
      interface_name = match[1]
50✔
609
      members = []
50✔
610
      i = start_index + 1
50✔
611

612
      while i < @lines.length
50✔
613
        current_line = @lines[i]
149✔
614
        break if current_line.match?(/^\s*end\s*$/)
149✔
615

616
        if current_line.match?(/^\s*[\w!?]+\s*:\s*/)
99✔
617
          member_match = current_line.match(/^\s*([\w!?]+)\s*:\s*(.+?)\s*$/)
98✔
618
          if member_match
98✔
619
            member = {
620
              name: member_match[1],
98✔
621
              type: member_match[2].strip,
622
            }
623

624
            # Parse member type with combinator
625
            type_result = @type_parser.parse(member[:type])
98✔
626
            member[:ir_type] = type_result[:type] if type_result[:success]
98✔
627

628
            members << member
98✔
629
          end
630
        end
631

632
        i += 1
99✔
633
      end
634

635
      [{ name: interface_name, members: members }, i]
50✔
636
    end
637
  end
638
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