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

type-ruby / t-ruby / 20560723383

28 Dec 2025 10:58PM UTC coverage: 79.074% (+1.7%) from 77.331%
20560723383

Pull #29

github

web-flow
Merge 5876e651d into fda099366
Pull Request #29: refactor: migrate parser from regex to token-based parser combinator

1848 of 2097 new or added lines in 53 files covered. (88.13%)

6 existing lines in 2 files now uncovered.

6643 of 8401 relevant lines covered (79.07%)

908.2 hits per line

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

88.06
/lib/t_ruby/compiler.rb
1
# frozen_string_literal: true
2

3
require "fileutils"
1✔
4

5
module TRuby
1✔
6
  # Pattern for method names that supports Unicode characters
7
  # \p{L} matches any Unicode letter, \p{N} matches any Unicode number
8
  IDENTIFIER_CHAR = '[\p{L}\p{N}_]'
1✔
9
  METHOD_NAME_PATTERN = "#{IDENTIFIER_CHAR}+[?!]?".freeze
1✔
10
  # Visibility modifiers for method definitions
11
  VISIBILITY_PATTERN = '(?:(?:private|protected|public)\s+)?'
1✔
12

13
  class Compiler
1✔
14
    attr_reader :declaration_loader, :optimizer
1✔
15

16
    def initialize(config = nil, optimize: true)
1✔
17
      @config = config || Config.new
123✔
18
      @optimize = optimize
123✔
19
      @declaration_loader = DeclarationLoader.new
123✔
20
      @optimizer = IR::Optimizer.new if optimize
123✔
21
      @type_inferrer = ASTTypeInferrer.new if type_check?
123✔
22
      setup_declaration_paths if @config
123✔
23
    end
24

25
    def type_check?
1✔
26
      @config.type_check?
224✔
27
    end
28

29
    def compile(input_path)
1✔
30
      unless File.exist?(input_path)
115✔
31
        raise ArgumentError, "File not found: #{input_path}"
8✔
32
      end
33

34
      # Handle .rb files separately
35
      if input_path.end_with?(".rb")
107✔
36
        return copy_ruby_file(input_path)
3✔
37
      end
38

39
      unless input_path.end_with?(".trb")
104✔
40
        raise ArgumentError, "Expected .trb or .rb file, got: #{input_path}"
3✔
41
      end
42

43
      source = File.read(input_path)
101✔
44

45
      # Parse with IR support
46
      parser = Parser.new(source)
101✔
47
      parser.parse
101✔
48

49
      # Run type checking if enabled
50
      if type_check? && parser.ir_program
101✔
51
        check_types(parser.ir_program, input_path)
85✔
52
      end
53

54
      # Transform source to Ruby code
55
      output = transform_with_ir(source, parser)
98✔
56

57
      # Compute output path (respects preserve_structure setting)
58
      output_path = compute_output_path(input_path, @config.ruby_dir, ".rb")
98✔
59
      FileUtils.mkdir_p(File.dirname(output_path))
98✔
60

61
      File.write(output_path, output)
98✔
62

63
      # Generate .rbs file if enabled in config
64
      if @config.compiler["generate_rbs"]
98✔
65
        rbs_path = compute_output_path(input_path, @config.rbs_dir, ".rbs")
85✔
66
        FileUtils.mkdir_p(File.dirname(rbs_path))
85✔
67
        generate_rbs_from_ir_to_path(rbs_path, parser.ir_program)
85✔
68
      end
69

70
      # Generate .d.trb file if enabled in config (legacy support)
71
      # TODO: Add compiler.generate_dtrb option in future
72
      if @config.compiler.key?("generate_dtrb") && @config.compiler["generate_dtrb"]
98✔
73
        generate_dtrb_file(input_path, @config.ruby_dir)
×
74
      end
75

76
      output_path
98✔
77
    end
78

79
    # Compile T-Ruby source code from a string (useful for WASM/playground)
80
    # @param source [String] T-Ruby source code
81
    # @param options [Hash] Options for compilation
82
    # @option options [Boolean] :rbs Whether to generate RBS output (default: true)
83
    # @return [Hash] Result with :ruby, :rbs, :errors keys
84
    def compile_string(source, options = {})
1✔
85
      generate_rbs = options.fetch(:rbs, true)
1✔
86

87
      parser = Parser.new(source)
1✔
88
      parser.parse
1✔
89

90
      # Transform source to Ruby code
91
      ruby_output = transform_with_ir(source, parser)
1✔
92

93
      # Generate RBS if requested
94
      rbs_output = ""
1✔
95
      if generate_rbs && parser.ir_program
1✔
96
        generator = IR::RBSGenerator.new
1✔
97
        rbs_output = generator.generate(parser.ir_program)
1✔
98
      end
99

100
      {
101
        ruby: ruby_output,
1✔
102
        rbs: rbs_output,
103
        errors: [],
104
      }
105
    rescue ParseError => e
106
      {
107
        ruby: "",
×
108
        rbs: "",
109
        errors: [e.message],
110
      }
111
    rescue StandardError => e
112
      {
113
        ruby: "",
×
114
        rbs: "",
115
        errors: ["Compilation error: #{e.message}"],
116
      }
117
    end
118

119
    # Compile to IR without generating output files
120
    def compile_to_ir(input_path)
1✔
121
      unless File.exist?(input_path)
4✔
122
        raise ArgumentError, "File not found: #{input_path}"
×
123
      end
124

125
      source = File.read(input_path)
4✔
126
      parser = Parser.new(source)
4✔
127
      parser.parse
4✔
128
      parser.ir_program
4✔
129
    end
130

131
    # Compile from IR program directly
132
    def compile_from_ir(ir_program, output_path)
1✔
133
      out_dir = File.dirname(output_path)
×
134
      FileUtils.mkdir_p(out_dir)
×
135

136
      # Optimize if enabled
137
      program = ir_program
×
138
      if @optimize && @optimizer
×
139
        result = @optimizer.optimize(program)
×
140
        program = result[:program]
×
141
      end
142

143
      # Generate Ruby code
144
      generator = IRCodeGenerator.new
×
145
      output = generator.generate(program)
×
146
      File.write(output_path, output)
×
147

148
      output_path
×
149
    end
150

151
    # Load external declarations from a file
152
    def load_declaration(name)
1✔
153
      @declaration_loader.load(name)
×
154
    end
155

156
    # Add a search path for declaration files
157
    def add_declaration_path(path)
1✔
158
      @declaration_loader.add_search_path(path)
×
159
    end
160

161
    # Get optimization statistics (only available after IR compilation)
162
    def optimization_stats
1✔
163
      @optimizer&.stats
×
164
    end
165

166
    # Compute output path for a source file
167
    # @param input_path [String] path to source file
168
    # @param output_dir [String] base output directory
169
    # @param new_extension [String] new file extension (e.g., ".rb", ".rbs")
170
    # @return [String] computed output path (always preserves directory structure)
171
    def compute_output_path(input_path, output_dir, new_extension)
1✔
172
      relative = compute_relative_path(input_path)
188✔
173
      base = relative.sub(/\.[^.]+$/, new_extension)
188✔
174
      File.join(output_dir, base)
188✔
175
    end
176

177
    # Compute relative path from source directory
178
    # @param input_path [String] path to source file
179
    # @return [String] relative path preserving directory structure
180
    def compute_relative_path(input_path)
1✔
181
      # Use realpath to resolve symlinks (e.g., /var vs /private/var on macOS)
182
      absolute_input = resolve_path(input_path)
188✔
183
      source_dirs = @config.source_include
188✔
184

185
      # Check if file is inside any source_include directory
186
      if source_dirs.size > 1
188✔
187
        # Multiple source directories: include the source dir name in output
188
        # src/models/user.trb → src/models/user.trb
189
        source_dirs.each do |src_dir|
4✔
190
          absolute_src = resolve_path(src_dir)
5✔
191
          next unless absolute_input.start_with?("#{absolute_src}/")
5✔
192

193
          # Return path relative to parent of source dir (includes src dir name)
194
          parent_of_src = File.dirname(absolute_src)
4✔
195
          return absolute_input.sub("#{parent_of_src}/", "")
4✔
196
        end
197
      else
198
        # Single source directory: exclude the source dir name from output
199
        # src/models/user.trb → models/user.trb
200
        src_dir = source_dirs.first
184✔
201
        if src_dir
184✔
202
          absolute_src = resolve_path(src_dir)
184✔
203
          if absolute_input.start_with?("#{absolute_src}/")
184✔
204
            return absolute_input.sub("#{absolute_src}/", "")
113✔
205
          end
206
        end
207
      end
208

209
      # File outside source directories: use path relative to current working directory
210
      # external/foo.trb → external/foo.trb
211
      cwd = resolve_path(".")
71✔
212
      if absolute_input.start_with?("#{cwd}/")
71✔
213
        return absolute_input.sub("#{cwd}/", "")
4✔
214
      end
215

216
      # Absolute path from outside cwd: use basename only
217
      File.basename(input_path)
67✔
218
    end
219

220
    private
1✔
221

222
    # Check types in IR program and raise TypeCheckError if mismatches found
223
    # @param ir_program [IR::Program] IR program to check
224
    # @param file_path [String] source file path for error messages
225
    def check_types(ir_program, file_path)
1✔
226
      ir_program.declarations.each do |decl|
85✔
227
        case decl
66✔
228
        when IR::MethodDef
229
          check_method_return_type(decl, nil, file_path)
46✔
230
        when IR::ClassDecl
231
          decl.body.each do |member|
18✔
232
            check_method_return_type(member, decl, file_path) if member.is_a?(IR::MethodDef)
25✔
233
          end
234
        end
235
      end
236
    end
237

238
    # Check if method's inferred return type matches declared return type
239
    # @param method [IR::MethodDef] method to check
240
    # @param class_def [IR::ClassDef, nil] containing class if any
241
    # @param file_path [String] source file path for error messages
242
    def check_method_return_type(method, class_def, file_path)
1✔
243
      # Skip if no explicit return type annotation
244
      return unless method.return_type
71✔
245

246
      declared_type = normalize_type(method.return_type.to_rbs)
56✔
247

248
      # Create type environment for the class context
249
      class_env = create_class_env(class_def) if class_def
56✔
250

251
      # Infer actual return type
252
      inferred_type = @type_inferrer.infer_method_return_type(method, class_env)
56✔
253
      inferred_type = normalize_type(inferred_type || "nil")
56✔
254

255
      # Check compatibility
256
      return if types_compatible?(inferred_type, declared_type)
56✔
257

258
      location = method.location ? "#{file_path}:#{method.location}" : file_path
3✔
259
      method_name = class_def ? "#{class_def.name}##{method.name}" : method.name
3✔
260

261
      raise TypeCheckError.new(
3✔
262
        message: "Return type mismatch in method '#{method_name}': " \
263
                 "declared '#{declared_type}' but inferred '#{inferred_type}'",
264
        location: location,
265
        expected: declared_type,
266
        actual: inferred_type
267
      )
268
    end
269

270
    # Create type environment for class context
271
    # @param class_def [IR::ClassDecl] class declaration
272
    # @return [TypeEnv] type environment with instance variables
273
    def create_class_env(class_def)
1✔
274
      env = TypeEnv.new
14✔
275

276
      # Register instance variables from class
277
      class_def.instance_vars&.each do |ivar|
14✔
278
        type = ivar.type_annotation&.to_rbs || "untyped"
6✔
279
        env.define_instance_var(ivar.name, type)
6✔
280
      end
281

282
      env
14✔
283
    end
284

285
    # Normalize type string for comparison
286
    # @param type [String] type string
287
    # @return [String] normalized type string
288
    def normalize_type(type)
1✔
289
      return "untyped" if type.nil?
112✔
290

291
      normalized = type.to_s.strip
112✔
292

293
      # Normalize boolean types (bool/Boolean/TrueClass/FalseClass -> bool)
294
      case normalized
112✔
295
      when "Boolean", "TrueClass", "FalseClass"
296
        "bool"
2✔
297
      else
298
        normalized
110✔
299
      end
300
    end
301

302
    # Check if inferred type is compatible with declared type
303
    # @param inferred [String] inferred type
304
    # @param declared [String] declared type
305
    # @return [Boolean] true if compatible
306
    def types_compatible?(inferred, declared)
1✔
307
      # Exact match
308
      return true if inferred == declared
56✔
309

310
      # untyped is compatible with anything
311
      return true if inferred == "untyped" || declared == "untyped"
41✔
312

313
      # void is compatible with anything (no return value check)
314
      return true if declared == "void"
20✔
315

316
      # nil is compatible with nullable types
317
      return true if inferred == "nil" && declared.end_with?("?")
4✔
318

319
      # Subtype relationships
320
      return true if subtype_of?(inferred, declared)
4✔
321

322
      # Handle generic types (e.g., Array[untyped] is compatible with Array[String])
323
      if inferred.include?("[") && declared.include?("[")
4✔
NEW
324
        inferred_base = inferred.split("[").first
×
NEW
325
        declared_base = declared.split("[").first
×
NEW
326
        if inferred_base == declared_base
×
327
          # Extract type arguments
NEW
328
          inferred_args = inferred[/\[(.+)\]/, 1]
×
NEW
329
          declared_args = declared[/\[(.+)\]/, 1]
×
330
          # untyped type argument is compatible with any type argument
NEW
331
          return true if inferred_args == "untyped" || declared_args == "untyped"
×
332
        end
333
      end
334

335
      # Handle union types in declared
336
      if declared.include?("|")
4✔
337
        declared_types = declared.split("|").map(&:strip)
1✔
338
        return true if declared_types.include?(inferred)
1✔
339
        return true if declared_types.any? { |t| types_compatible?(inferred, t) }
×
340
      end
341

342
      # Handle union types in inferred - all must be compatible
343
      if inferred.include?("|")
3✔
344
        inferred_types = inferred.split("|").map(&:strip)
×
345
        return inferred_types.all? { |t| types_compatible?(t, declared) }
×
346
      end
347

348
      false
3✔
349
    end
350

351
    # Check if subtype is a subtype of supertype
352
    # @param subtype [String] potential subtype
353
    # @param supertype [String] potential supertype
354
    # @return [Boolean] true if subtype
355
    def subtype_of?(subtype, supertype)
1✔
356
      # Handle nullable - X is subtype of X?
357
      return true if supertype.end_with?("?") && supertype[0..-2] == subtype
4✔
358

359
      # Numeric hierarchy
360
      return true if subtype == "Integer" && supertype == "Numeric"
4✔
361
      return true if subtype == "Float" && supertype == "Numeric"
4✔
362

363
      # Object is supertype of everything
364
      return true if supertype == "Object"
4✔
365

366
      false
4✔
367
    end
368

369
    # Resolve path to absolute path, following symlinks
370
    # Falls back to expand_path if realpath fails (e.g., file doesn't exist yet)
371
    def resolve_path(path)
1✔
372
      File.realpath(path)
448✔
373
    rescue Errno::ENOENT
374
      File.expand_path(path)
70✔
375
    end
376

377
    def setup_declaration_paths
1✔
378
      # Add default declaration paths
379
      @declaration_loader.add_search_path(@config.out_dir)
123✔
380
      @declaration_loader.add_search_path(@config.src_dir)
123✔
381
      @declaration_loader.add_search_path("./types")
123✔
382
      @declaration_loader.add_search_path("./lib/types")
123✔
383
    end
384

385
    # Transform using IR system
386
    def transform_with_ir(source, parser)
1✔
387
      ir_program = parser.ir_program
99✔
388
      return source unless ir_program
99✔
389

390
      # Run optimization passes if enabled
391
      if @optimize && @optimizer
99✔
392
        result = @optimizer.optimize(ir_program)
99✔
393
        ir_program = result[:program]
99✔
394
      end
395

396
      # Generate Ruby code using IR-aware generator with target Ruby version
397
      generator = IRCodeGenerator.new(target_ruby: @config.target_ruby)
99✔
398
      generator.generate_with_source(ir_program, source)
99✔
399
    end
400

401
    # Generate RBS from IR to a specific path
402
    def generate_rbs_from_ir_to_path(rbs_path, ir_program)
1✔
403
      return unless ir_program
85✔
404

405
      generator = IR::RBSGenerator.new
85✔
406
      rbs_content = generator.generate(ir_program)
85✔
407
      File.write(rbs_path, rbs_content) unless rbs_content.strip.empty?
85✔
408
    end
409

410
    def generate_dtrb_file(input_path, out_dir)
1✔
411
      dtrb_path = compute_output_path(input_path, out_dir, DeclarationGenerator::DECLARATION_EXTENSION)
×
412
      FileUtils.mkdir_p(File.dirname(dtrb_path))
×
413

414
      generator = DeclarationGenerator.new
×
415
      generator.generate_file_to_path(input_path, dtrb_path)
×
416
    end
417

418
    # Copy .rb file to output directory and generate .rbs signature
419
    def copy_ruby_file(input_path)
1✔
420
      unless File.exist?(input_path)
3✔
421
        raise ArgumentError, "File not found: #{input_path}"
×
422
      end
423

424
      # Compute output path (respects preserve_structure setting)
425
      output_path = compute_output_path(input_path, @config.ruby_dir, ".rb")
3✔
426
      FileUtils.mkdir_p(File.dirname(output_path))
3✔
427

428
      # Copy the .rb file to output directory
429
      FileUtils.cp(input_path, output_path)
3✔
430

431
      # Generate .rbs file if enabled in config
432
      if @config.compiler["generate_rbs"]
3✔
433
        rbs_path = compute_output_path(input_path, @config.rbs_dir, ".rbs")
2✔
434
        FileUtils.mkdir_p(File.dirname(rbs_path))
2✔
435
        generate_rbs_from_ruby_to_path(rbs_path, input_path)
2✔
436
      end
437

438
      output_path
3✔
439
    end
440

441
    # Generate RBS from Ruby file using rbs prototype to a specific path
442
    def generate_rbs_from_ruby_to_path(rbs_path, input_path)
1✔
443
      result = `rbs prototype rb #{input_path} 2>/dev/null`
2✔
444
      File.write(rbs_path, result) unless result.strip.empty?
2✔
445
    end
446
  end
447

448
  # IR-aware code generator for source-preserving transformation
449
  class IRCodeGenerator
1✔
450
    attr_reader :emitter
1✔
451

452
    # @param target_ruby [String] target Ruby version (e.g., "3.0", "4.0")
453
    def initialize(target_ruby: "3.0")
1✔
454
      @output = []
99✔
455
      @emitter = CodeEmitter.for_version(target_ruby)
99✔
456
    end
457

458
    # Generate Ruby code from IR program
459
    def generate(program)
1✔
460
      generator = IR::CodeGenerator.new
×
461
      generator.generate(program)
×
462
    end
463

464
    # Generate Ruby code while preserving source structure
465
    def generate_with_source(program, source)
1✔
466
      result = source.dup
99✔
467

468
      # Collect type alias names to remove
469
      program.declarations
99✔
470
             .select { |d| d.is_a?(IR::TypeAlias) }
90✔
471
             .map(&:name)
472

473
      # Collect interface names to remove
474
      program.declarations
99✔
475
             .select { |d| d.is_a?(IR::Interface) }
90✔
476
             .map(&:name)
477

478
      # Remove type alias definitions
479
      result = result.gsub(/^\s*type\s+\w+\s*=\s*.+?$\n?/, "")
99✔
480

481
      # Remove interface definitions (multi-line)
482
      result = result.gsub(/^\s*interface\s+\w+.*?^\s*end\s*$/m, "")
99✔
483

484
      # Remove parameter type annotations using IR info
485
      # Enhanced: Handle complex types (generics, unions, etc.)
486
      result = erase_parameter_types(result)
99✔
487

488
      # Remove return type annotations
489
      result = erase_return_types(result)
99✔
490

491
      # Apply version-specific transformations
492
      result = @emitter.transform(result)
99✔
493

494
      # Clean up extra blank lines
495
      result.gsub(/\n{3,}/, "\n\n")
99✔
496
    end
497

498
    private
1✔
499

500
    # Erase parameter type annotations
501
    def erase_parameter_types(source)
1✔
502
      result = source.dup
99✔
503

504
      # Match function definitions and remove type annotations from parameters
505
      # Also supports visibility modifiers: private def, protected def, public def
506
      result.gsub!(/^(\s*#{TRuby::VISIBILITY_PATTERN}def\s+#{TRuby::METHOD_NAME_PATTERN}\s*\()([^)]+)(\)\s*)(?::\s*[^\n]+)?(\s*$)/) do |_match|
99✔
507
        indent = ::Regexp.last_match(1)
66✔
508
        params = ::Regexp.last_match(2)
66✔
509
        close_paren = ::Regexp.last_match(3)
66✔
510
        ending = ::Regexp.last_match(4)
66✔
511

512
        # Remove type annotations from each parameter
513
        cleaned_params = remove_param_types(params)
66✔
514

515
        "#{indent}#{cleaned_params}#{close_paren.rstrip}#{ending}"
66✔
516
      end
517

518
      result
99✔
519
    end
520

521
    # Remove type annotations from parameter list
522
    def remove_param_types(params_str)
1✔
523
      return params_str if params_str.strip.empty?
66✔
524

525
      params = []
66✔
526
      current = ""
66✔
527
      depth = 0
66✔
528
      brace_depth = 0
66✔
529

530
      params_str.each_char do |char|
66✔
531
        case char
1,210✔
532
        when "<", "[", "("
533
          depth += 1
10✔
534
          current += char
10✔
535
        when ">", "]", ")"
536
          depth -= 1
10✔
537
          current += char
10✔
538
        when "{"
539
          brace_depth += 1
12✔
540
          current += char
12✔
541
        when "}"
542
          brace_depth -= 1
12✔
543
          current += char
12✔
544
        when ","
545
          if depth.zero? && brace_depth.zero?
18✔
546
            cleaned = clean_param(current.strip)
9✔
547
            params.concat(Array(cleaned)) if cleaned
9✔
548
            current = ""
9✔
549
          else
550
            current += char
9✔
551
          end
552
        else
553
          current += char
1,148✔
554
        end
555
      end
556

557
      cleaned = clean_param(current.strip) unless current.empty?
66✔
558
      params.concat(Array(cleaned)) if cleaned
66✔
559
      params.join(", ")
66✔
560
    end
561

562
    # Clean a single parameter (remove type annotation, preserve default value)
563
    # Returns String or Array of Strings (for keyword args group)
564
    def clean_param(param)
1✔
565
      param = param.strip
75✔
566
      return nil if param.empty?
75✔
567

568
      # 0. 블록 파라미터: &name: Type -> &name
569
      if param.start_with?("&")
75✔
570
        match = param.match(/^&(\w+)(?::\s*.+)?$/)
4✔
571
        return "&#{match[1]}" if match
4✔
572

573
        return param
×
574
      end
575

576
      # 1. 더블 스플랫: **name: Type -> **name
577
      if param.start_with?("**")
71✔
578
        match = param.match(/^\*\*(\w+)(?::\s*.+)?$/)
1✔
579
        return "**#{match[1]}" if match
1✔
580

581
        return param
×
582
      end
583

584
      # 2. 키워드 인자 그룹: { ... } 또는 { ... }: InterfaceName
585
      if param.start_with?("{")
70✔
586
        return clean_keyword_args_group(param)
10✔
587
      end
588

589
      # 3. Hash 리터럴: name: { ... } -> name
590
      if param.match?(/^\w+:\s*\{/)
60✔
591
        match = param.match(/^(\w+):\s*\{.+\}(?::\s*\w+)?$/)
1✔
592
        return match[1] if match
1✔
593

594
        return param
×
595
      end
596

597
      # 4. 일반 파라미터: name: Type = value -> name = value 또는 name: Type -> name
598
      # Match: name: Type = value (with default value)
599
      if (match = param.match(/^(#{TRuby::IDENTIFIER_CHAR}+)\s*:\s*.+?\s*(=\s*.+)$/))
59✔
600
        "#{match[1]} #{match[2]}"
1✔
601
      # Match: name: Type (without default value)
602
      elsif (match = param.match(/^(#{TRuby::IDENTIFIER_CHAR}+)\s*:/))
58✔
603
        match[1]
55✔
604
      else
605
        param
3✔
606
      end
607
    end
608

609
    # 키워드 인자 그룹을 Ruby 키워드 인자로 변환
610
    # { name: String, age: Integer = 0 } -> name:, age: 0
611
    # { name:, age: 0 }: UserParams -> name:, age: 0
612
    def clean_keyword_args_group(param)
1✔
613
      # { ... }: InterfaceName 또는 { ... } 형태 파싱
614
      interface_match = param.match(/^\{(.+)\}\s*:\s*\w+\s*$/)
10✔
615
      inline_match = param.match(/^\{(.+)\}\s*$/) unless interface_match
10✔
616

617
      inner_content = if interface_match
10✔
618
                        interface_match[1]
2✔
619
                      elsif inline_match
8✔
620
                        inline_match[1]
8✔
621
                      else
622
                        return param
×
623
                      end
624

625
      # 내부 파라미터 분리
626
      parts = split_nested_content(inner_content)
10✔
627
      keyword_params = []
10✔
628

629
      parts.each do |part|
10✔
630
        part = part.strip
17✔
631
        next if part.empty?
17✔
632

633
        if interface_match
17✔
634
          # interface 참조: name: default_value 또는 name:
635
          if (match = part.match(/^(\w+):\s*(.*)$/))
5✔
636
            name = match[1]
5✔
637
            default_value = match[2].strip
5✔
638
            keyword_params << if default_value.empty?
5✔
639
                                "#{name}:"
3✔
640
                              else
641
                                "#{name}: #{default_value}"
2✔
642
                              end
643
          end
644
        elsif (match = part.match(/^(\w+):\s*(.+)$/))
12✔
645
          # 인라인 타입: name: Type = default 또는 name: Type
646
          name = match[1]
12✔
647
          type_and_default = match[2].strip
12✔
648

649
          # Type = default 분리
650
          default_value = extract_default_value(type_and_default)
12✔
651
          keyword_params << if default_value
12✔
652
                              "#{name}: #{default_value}"
6✔
653
                            else
654
                              "#{name}:"
6✔
655
                            end
656
        end
657
      end
658

659
      keyword_params
10✔
660
    end
661

662
    # 중첩된 내용을 콤마로 분리
663
    def split_nested_content(content)
1✔
664
      StringUtils.split_by_comma(content)
10✔
665
    end
666

667
    # 타입과 기본값에서 기본값만 추출
668
    def extract_default_value(type_and_default)
1✔
669
      StringUtils.extract_default_value(type_and_default)
12✔
670
    end
671

672
    # Erase return type annotations
673
    def erase_return_types(source)
1✔
674
      result = source.dup
99✔
675

676
      # Remove return type after parentheses: ): Type or ): Type<Foo> etc.
677
      result.gsub!(/\)\s*:\s*[^\n]+?(?=\s*$)/m) do |_match|
99✔
678
        ")"
5✔
679
      end
680

681
      # Remove return type for methods without parentheses: def method_name: Type
682
      result.gsub!(/^(\s*#{TRuby::VISIBILITY_PATTERN}def\s+#{TRuby::METHOD_NAME_PATTERN})\s*:\s*[^\n]+?(?=\s*$)/m) do |_match|
99✔
683
        ::Regexp.last_match(1)
9✔
684
      end
685

686
      result
99✔
687
    end
688
  end
689
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