diff --git a/lib/t_ruby.rb b/lib/t_ruby.rb index ce0a0b7..9b0a865 100644 --- a/lib/t_ruby.rb +++ b/lib/t_ruby.rb @@ -11,6 +11,7 @@ # Basic components require_relative "t_ruby/type_alias_registry" +require_relative "t_ruby/heredoc_detector" require_relative "t_ruby/body_parser" require_relative "t_ruby/parser" require_relative "t_ruby/union_type_parser" @@ -18,7 +19,6 @@ require_relative "t_ruby/intersection_type_parser" require_relative "t_ruby/type_erasure" require_relative "t_ruby/error_handler" -require_relative "t_ruby/rbs_generator" require_relative "t_ruby/declaration_generator" require_relative "t_ruby/compiler" require_relative "t_ruby/lsp_server" diff --git a/lib/t_ruby/compiler.rb b/lib/t_ruby/compiler.rb index 61a03d0..c514ce2 100644 --- a/lib/t_ruby/compiler.rb +++ b/lib/t_ruby/compiler.rb @@ -7,16 +7,17 @@ module TRuby # \p{L} matches any Unicode letter, \p{N} matches any Unicode number IDENTIFIER_CHAR = '[\p{L}\p{N}_]' METHOD_NAME_PATTERN = "#{IDENTIFIER_CHAR}+[?!]?".freeze + # Visibility modifiers for method definitions + VISIBILITY_PATTERN = '(?:(?:private|protected|public)\s+)?' class Compiler - attr_reader :declaration_loader, :use_ir, :optimizer + attr_reader :declaration_loader, :optimizer - def initialize(config = nil, use_ir: true, optimize: true) + def initialize(config = nil, optimize: true) @config = config || Config.new - @use_ir = use_ir @optimize = optimize @declaration_loader = DeclarationLoader.new - @optimizer = IR::Optimizer.new if use_ir && optimize + @optimizer = IR::Optimizer.new if optimize @type_inferrer = ASTTypeInferrer.new if type_check? setup_declaration_paths if @config end @@ -42,16 +43,16 @@ def compile(input_path) source = File.read(input_path) # Parse with IR support - parser = Parser.new(source, use_combinator: @use_ir) - parse_result = parser.parse + parser = Parser.new(source) + parser.parse # Run type checking if enabled - if type_check? && @use_ir && parser.ir_program + if type_check? && parser.ir_program check_types(parser.ir_program, input_path) end # Transform source to Ruby code - output = @use_ir ? transform_with_ir(source, parser) : transform_legacy(source, parse_result) + output = transform_with_ir(source, parser) # Compute output path (respects preserve_structure setting) output_path = compute_output_path(input_path, @config.ruby_dir, ".rb") @@ -63,11 +64,7 @@ def compile(input_path) if @config.compiler["generate_rbs"] rbs_path = compute_output_path(input_path, @config.rbs_dir, ".rbs") FileUtils.mkdir_p(File.dirname(rbs_path)) - if @use_ir && parser.ir_program - generate_rbs_from_ir_to_path(rbs_path, parser.ir_program) - else - generate_rbs_file_to_path(rbs_path, parse_result) - end + generate_rbs_from_ir_to_path(rbs_path, parser.ir_program) end # Generate .d.trb file if enabled in config (legacy support) @@ -87,25 +84,17 @@ def compile(input_path) def compile_string(source, options = {}) generate_rbs = options.fetch(:rbs, true) - parser = Parser.new(source, use_combinator: @use_ir) - parse_result = parser.parse + parser = Parser.new(source) + parser.parse # Transform source to Ruby code - ruby_output = @use_ir ? transform_with_ir(source, parser) : transform_legacy(source, parse_result) + ruby_output = transform_with_ir(source, parser) # Generate RBS if requested rbs_output = "" - if generate_rbs - if @use_ir && parser.ir_program - generator = IR::RBSGenerator.new - rbs_output = generator.generate(parser.ir_program) - else - generator = RBSGenerator.new - rbs_output = generator.generate( - parse_result[:functions] || [], - parse_result[:type_aliases] || [] - ) - end + if generate_rbs && parser.ir_program + generator = IR::RBSGenerator.new + rbs_output = generator.generate(parser.ir_program) end { @@ -134,7 +123,7 @@ def compile_to_ir(input_path) end source = File.read(input_path) - parser = Parser.new(source, use_combinator: true) + parser = Parser.new(source) parser.parse parser.ir_program end @@ -380,10 +369,10 @@ def setup_declaration_paths @declaration_loader.add_search_path("./lib/types") end - # Transform using IR system (new approach) + # Transform using IR system def transform_with_ir(source, parser) ir_program = parser.ir_program - return transform_legacy(source, parser.parse) unless ir_program + return source unless ir_program # Run optimization passes if enabled if @optimize && @optimizer @@ -396,33 +385,15 @@ def transform_with_ir(source, parser) generator.generate_with_source(ir_program, source) end - # Legacy transformation using TypeErasure (backward compatible) - def transform_legacy(source, parse_result) - if parse_result[:type] == :success - eraser = TypeErasure.new(source) - eraser.erase - else - source - end - end - # Generate RBS from IR to a specific path def generate_rbs_from_ir_to_path(rbs_path, ir_program) + return unless ir_program + generator = IR::RBSGenerator.new rbs_content = generator.generate(ir_program) File.write(rbs_path, rbs_content) unless rbs_content.strip.empty? end - # Legacy RBS generation to a specific path - def generate_rbs_file_to_path(rbs_path, parse_result) - generator = RBSGenerator.new - rbs_content = generator.generate( - parse_result[:functions] || [], - parse_result[:type_aliases] || [] - ) - File.write(rbs_path, rbs_content) unless rbs_content.empty? - end - def generate_dtrb_file(input_path, out_dir) dtrb_path = compute_output_path(input_path, out_dir, DeclarationGenerator::DECLARATION_EXTENSION) FileUtils.mkdir_p(File.dirname(dtrb_path)) @@ -511,7 +482,8 @@ def erase_parameter_types(source) result = source.dup # Match function definitions and remove type annotations from parameters - result.gsub!(/^(\s*def\s+#{TRuby::METHOD_NAME_PATTERN}\s*\()([^)]+)(\)\s*)(?::\s*[^\n]+)?(\s*$)/) do |_match| + # Also supports visibility modifiers: private def, protected def, public def + result.gsub!(/^(\s*#{TRuby::VISIBILITY_PATTERN}def\s+#{TRuby::METHOD_NAME_PATTERN}\s*\()([^)]+)(\)\s*)(?::\s*[^\n]+)?(\s*$)/) do |_match| indent = ::Regexp.last_match(1) params = ::Regexp.last_match(2) close_paren = ::Regexp.last_match(3) @@ -558,10 +530,13 @@ def remove_param_types(params_str) params.join(", ") end - # Clean a single parameter (remove type annotation) + # Clean a single parameter (remove type annotation, preserve default value) def clean_param(param) - # Match: name: Type or name (supports Unicode identifiers) - if (match = param.match(/^(#{TRuby::IDENTIFIER_CHAR}+)\s*:/)) + # Match: name: Type = value (with default value) + if (match = param.match(/^(#{TRuby::IDENTIFIER_CHAR}+)\s*:\s*.+?\s*(=\s*.+)$/)) + "#{match[1]} #{match[2]}" + # Match: name: Type (without default value) + elsif (match = param.match(/^(#{TRuby::IDENTIFIER_CHAR}+)\s*:/)) match[1] else param @@ -580,11 +555,4 @@ def erase_return_types(source) result end end - - # Legacy Compiler for backward compatibility (no IR) - class LegacyCompiler < Compiler - def initialize(config) - super(config, use_ir: false, optimize: false) - end - end end diff --git a/lib/t_ruby/heredoc_detector.rb b/lib/t_ruby/heredoc_detector.rb new file mode 100644 index 0000000..21bc254 --- /dev/null +++ b/lib/t_ruby/heredoc_detector.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +module TRuby + # Detects regions that should be skipped during parsing: + # - Heredoc content + # - Block comments (=begin/=end) + class HeredocDetector + # Heredoc start patterns: + # <] source lines + # @return [Array] content ranges to skip (0-indexed) + def self.detect(lines) + ranges = [] + i = 0 + + while i < lines.length + line = lines[i] + + # Check for =begin block comment + if line.strip == "=begin" + start_line = i + i += 1 + + # Find =end + while i < lines.length + break if lines[i].strip == "=end" + + i += 1 + end + + # Range covers from =begin to =end (inclusive) + ranges << (start_line..i) if i < lines.length + # Check for heredoc + elsif (match = line.match(HEREDOC_START_PATTERN)) + delimiter = match[3] + squiggly = match[1] == "~" + start_line = i + i += 1 + + # Find closing delimiter + while i < lines.length + # For squiggly heredoc or dash heredoc, delimiter can be indented + # For regular heredoc, delimiter must be at line start + if squiggly || match[1] == "-" + break if lines[i].strip == delimiter + elsif lines[i].chomp == delimiter + break + end + i += 1 + end + + # Range covers content lines (after start, up to and including end delimiter) + ranges << ((start_line + 1)..i) if i < lines.length + end + + i += 1 + end + + ranges + end + + # Check if a line index is inside any skippable region + # @param line_index [Integer] line index to check + # @param heredoc_ranges [Array] ranges from detect() + # @return [Boolean] + def self.inside_heredoc?(line_index, heredoc_ranges) + heredoc_ranges.any? { |range| range.include?(line_index) } + end + end +end diff --git a/lib/t_ruby/ir.rb b/lib/t_ruby/ir.rb index cfca19e..6d6f213 100644 --- a/lib/t_ruby/ir.rb +++ b/lib/t_ruby/ir.rb @@ -778,7 +778,8 @@ def build_method(info) name: info[:name], params: params, return_type: info[:return_type] ? parse_type(info[:return_type]) : nil, - body: body + body: body, + visibility: info[:visibility] || :public ) end @@ -1051,7 +1052,8 @@ def visit_method_def(node) end return_type ||= "untyped" - emit("def #{node.name}: (#{params}) -> #{return_type}") + visibility_prefix = format_visibility(node.visibility) + emit("#{visibility_prefix}def #{node.name}: (#{params}) -> #{return_type}") end def visit_class_decl(node) @@ -1092,6 +1094,17 @@ def visit_instance_variable(node) def emit(text) @output << ((" " * @indent) + text) end + + def format_visibility(visibility) + # RBS only supports private visibility, not protected + # See: https://github.com/ruby/rbs/issues/579 + case visibility + when :private + "private " + else + "" + end + end end #========================================================================== diff --git a/lib/t_ruby/lsp_server.rb b/lib/t_ruby/lsp_server.rb index 1d676e2..c5344e7 100644 --- a/lib/t_ruby/lsp_server.rb +++ b/lib/t_ruby/lsp_server.rb @@ -787,7 +787,7 @@ def generate_semantic_tokens(text) lines = text.split("\n") # Parse the document to get IR - parser = Parser.new(text, use_combinator: true) + parser = Parser.new(text) parse_result = parser.parse parser.ir_program diff --git a/lib/t_ruby/parser.rb b/lib/t_ruby/parser.rb index bbbbc5e..73a1c3f 100644 --- a/lib/t_ruby/parser.rb +++ b/lib/t_ruby/parser.rb @@ -12,15 +12,16 @@ class Parser IDENTIFIER_CHAR = '[\p{L}\p{N}_]' # Method names can end with ? or ! METHOD_NAME_PATTERN = "#{IDENTIFIER_CHAR}+[?!]?".freeze + # Visibility modifiers for method definitions + VISIBILITY_PATTERN = '(?:(?:private|protected|public)\s+)?' - attr_reader :source, :ir_program, :use_combinator + attr_reader :source, :ir_program - def initialize(source, use_combinator: true, parse_body: true) + def initialize(source, parse_body: true) @source = source @lines = source.split("\n") - @use_combinator = use_combinator @parse_body = parse_body - @type_parser = ParserCombinator::TypeParser.new if use_combinator + @type_parser = ParserCombinator::TypeParser.new @body_parser = BodyParser.new if parse_body @ir_program = nil end @@ -32,7 +33,16 @@ def parse classes = [] i = 0 + # Pre-detect heredoc regions to skip + heredoc_ranges = HeredocDetector.detect(@lines) + while i < @lines.length + # Skip lines inside heredoc content + if HeredocDetector.inside_heredoc?(i, heredoc_ranges) + i += 1 + next + end + line = @lines[i] # Match type alias definitions @@ -62,7 +72,7 @@ def parse end # Match function definitions (top-level only, not inside class) - if line.match?(/^\s*def\s+#{IDENTIFIER_CHAR}+/) + if line.match?(/^\s*#{VISIBILITY_PATTERN}def\s+#{IDENTIFIER_CHAR}+/) func_info, next_i = parse_function_with_body(i) if func_info functions << func_info @@ -82,11 +92,9 @@ def parse classes: classes, } - # Build IR if combinator is enabled - if @use_combinator - builder = IR::Builder.new - @ir_program = builder.build(result, source: @source) - end + # Build IR + builder = IR::Builder.new + @ir_program = builder.build(result, source: @source) result end @@ -97,10 +105,8 @@ def parse_to_ir @ir_program end - # Parse a type expression using combinator (new API) + # Parse a type expression using combinator def parse_type(type_string) - return nil unless @use_combinator - result = @type_parser.parse(type_string) result[:success] ? result[:type] : nil end @@ -149,16 +155,14 @@ def parse_type_alias(line) alias_name = match[1] definition = match[2].strip - # Use combinator for complex type parsing if available - if @use_combinator - type_result = @type_parser.parse(definition) - if type_result[:success] - return { - name: alias_name, - definition: definition, - ir_type: type_result[:type], - } - end + # Use combinator for complex type parsing + type_result = @type_parser.parse(definition) + if type_result[:success] + return { + name: alias_name, + definition: definition, + ir_type: type_result[:type], + } end { @@ -173,12 +177,14 @@ def parse_function_definition(line) # def foo(): Type - no params but with return type # def foo(params) - with params, no return type # def foo - no params, no return type - match = line.match(/^\s*def\s+(#{METHOD_NAME_PATTERN})\s*(?:\((.*?)\))?\s*(?::\s*(.+?))?\s*$/) + # Also supports visibility modifiers: private def, protected def, public def + match = line.match(/^\s*(?:(private|protected|public)\s+)?def\s+(#{METHOD_NAME_PATTERN})\s*(?:\((.*?)\))?\s*(?::\s*(.+?))?\s*$/) return nil unless match - function_name = match[1] - params_str = match[2] || "" - return_type_str = match[3]&.strip + visibility = match[1] ? match[1].to_sym : :public + function_name = match[2] + params_str = match[3] || "" + return_type_str = match[4]&.strip # Validate return type if present if return_type_str @@ -191,10 +197,11 @@ def parse_function_definition(line) name: function_name, params: params, return_type: return_type_str, + visibility: visibility, } - # Parse return type with combinator if available - if @use_combinator && return_type_str + # Parse return type with combinator + if return_type_str type_result = @type_parser.parse(return_type_str) result[:ir_return_type] = type_result[:type] if type_result[:success] end @@ -286,8 +293,8 @@ def parse_single_parameter(param) type: type_str, } - # Parse type with combinator if available - if @use_combinator && type_str + # Parse type with combinator + if type_str type_result = @type_parser.parse(type_str) result[:ir_type] = type_result[:type] if type_result[:success] end @@ -326,7 +333,7 @@ def parse_class(start_index) current_line = @lines[i] # Match method definitions inside class - if current_line.match?(/^\s*def\s+#{IDENTIFIER_CHAR}+/) + if current_line.match?(/^\s*#{VISIBILITY_PATTERN}def\s+#{IDENTIFIER_CHAR}+/) method_info, next_i = parse_method_in_class(i, class_end) if method_info methods << method_info @@ -438,10 +445,8 @@ def parse_interface(start_index) } # Parse member type with combinator - if @use_combinator - type_result = @type_parser.parse(member[:type]) - member[:ir_type] = type_result[:type] if type_result[:success] - end + type_result = @type_parser.parse(member[:type]) + member[:ir_type] = type_result[:type] if type_result[:success] members << member end @@ -453,11 +458,4 @@ def parse_interface(start_index) [{ name: interface_name, members: members }, i] end end - - # Legacy Parser for backward compatibility (regex-only) - class LegacyParser < Parser - def initialize(source) - super(source, use_combinator: false) - end - end end diff --git a/lib/t_ruby/rbs_generator.rb b/lib/t_ruby/rbs_generator.rb deleted file mode 100644 index 6a3582a..0000000 --- a/lib/t_ruby/rbs_generator.rb +++ /dev/null @@ -1,69 +0,0 @@ -# frozen_string_literal: true - -module TRuby - class RBSGenerator - def initialize - # RBS generation configuration - end - - def generate(functions, type_aliases) - # Add type aliases - lines = type_aliases.map do |type_alias| - generate_type_alias(type_alias) - end - - lines << "" if type_aliases.any? && functions.any? - - # Add function signatures - functions.each do |func| - lines << generate_function_signature(func) - end - - lines.compact.join("\n") - end - - def generate_type_aliases(aliases) - aliases.map { |alias_def| generate_type_alias(alias_def) }.join("\n") - end - - def generate_type_alias(type_alias) - name = type_alias[:name] - definition = type_alias[:definition] - - "type #{name} = #{definition}" - end - - def generate_function_signature(func) - name = func[:name] - params = func[:params] || [] - return_type = func[:return_type] - - param_str = format_parameters(params) - return_str = format_return_type(return_type) - - "def #{name}: (#{param_str}) -> #{return_str}" - end - - private - - def format_parameters(params) - return if params.empty? - - param_strs = params.map do |param| - param_name = param[:name] - param_type = param[:type] || "Object" - - "#{param_name}: #{param_type}" - end - - param_strs.join(", ") - end - - def format_return_type(return_type) - return "void" if return_type == "void" - return "nil" if return_type.nil? - - return_type - end - end -end diff --git a/spec/e2e/class_rbs_generation_spec.rb b/spec/e2e/class_rbs_generation_spec.rb index 5680bff..e369b7c 100644 --- a/spec/e2e/class_rbs_generation_spec.rb +++ b/spec/e2e/class_rbs_generation_spec.rb @@ -216,6 +216,88 @@ def untyped_method end end + describe "visibility modifier generation" do + it "generates RBS with private method visibility" do + Dir.chdir(tmpdir) do + create_config_file(<<~YAML) + source: + include: + - src + output: + ruby_dir: build + rbs_dir: sig + compiler: + generate_rbs: true + YAML + + create_trb_file("src/secret.trb", <<~TRB) + class Secret + def public_method(): String + "public" + end + + private def hidden(x: Integer): Boolean + x > 0 + end + + protected def internal(name: String): String + name.upcase + end + end + TRB + + rbs_content = compile_and_get_rbs(File.join(tmpdir, "src/secret.trb")) + + # Validate RBS syntax using official rbs gem + expect_valid_rbs(rbs_content) + + expect(rbs_content).to include("class Secret") + expect(rbs_content).to include("def public_method: () -> String") + expect(rbs_content).to include("private def hidden: (x: Integer) -> Boolean") + # RBS does not support protected visibility, treated as public + # See: https://github.com/ruby/rbs/issues/579 + expect(rbs_content).to include("def internal: (name: String) -> String") + expect(rbs_content).not_to include("protected def") + end + end + + it "preserves visibility in compiled Ruby code" do + Dir.chdir(tmpdir) do + create_config_file(<<~YAML) + source: + include: + - src + output: + ruby_dir: build + rbs_dir: sig + compiler: + generate_rbs: true + YAML + + create_trb_file("src/visible.trb", <<~TRB) + class Visible + private def secret(x: String): Integer + x.length + end + end + TRB + + trb_path = File.join(tmpdir, "src/visible.trb") + config = TRuby::Config.new + compiler = TRuby::Compiler.new(config) + compiler.compile(trb_path) + + # Check compiled Ruby preserves private keyword + ruby_path = File.join(tmpdir, "build/visible.rb") + ruby_content = File.read(ruby_path) + + expect(ruby_content).to include("private def secret") + expect(ruby_content).not_to include(": String") + expect(ruby_content).not_to include(": Integer") + end + end + end + describe "HelloWorld integration test" do it "generates correct RBS for HelloWorld sample structure" do Dir.chdir(tmpdir) do diff --git a/spec/e2e/integration_spec.rb b/spec/e2e/integration_spec.rb index 9809e0f..f168d9e 100644 --- a/spec/e2e/integration_spec.rb +++ b/spec/e2e/integration_spec.rb @@ -199,7 +199,7 @@ def process(value: String): Integer end TRB - parser = TRuby::Parser.new(content, use_combinator: true) + parser = TRuby::Parser.new(content) parser.parse ir_program = parser.ir_program expect(ir_program).not_to be_nil diff --git a/spec/t_ruby/heredoc_detector_spec.rb b/spec/t_ruby/heredoc_detector_spec.rb new file mode 100644 index 0000000..ed7ffa7 --- /dev/null +++ b/spec/t_ruby/heredoc_detector_spec.rb @@ -0,0 +1,139 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe TRuby::HeredocDetector do + describe ".detect" do + it "detects simple heredoc" do + lines = [ + "text = <", + "

Hello

", + " ", + "HTML", + "render html", + ] + ranges = described_class.detect(lines) + + expect(ranges).to eq([1..4]) + end + + it "detects multiple heredocs" do + lines = [ + "a = < 0 + end + end + RUBY + parser = TRuby::Parser.new(source) + result = parser.parse + + expect(result[:classes][0][:methods].length).to eq(1) + expect(result[:classes][0][:methods][0][:name]).to eq("internal") + expect(result[:classes][0][:methods][0][:visibility]).to eq(:protected) + end + end + + context "without visibility modifier" do + it "defaults to public visibility" do + source = "def hello(name: String): String\n name\nend" + parser = TRuby::Parser.new(source) + result = parser.parse + + expect(result[:functions][0][:visibility]).to eq(:public) + end + end + end + + describe "heredoc handling" do + it "ignores def patterns inside heredoc" do + source = <<~RUBY + text = < + def fake_method(x: Integer): void + + HTML + + def process(data: String): Boolean + true + end + RUBY + parser = TRuby::Parser.new(source) + result = parser.parse + + expect(result[:functions].length).to eq(1) + expect(result[:functions][0][:name]).to eq("process") + end + + it "handles heredoc with dash" do + source = <<~RUBY + sql = <<-SQL + SELECT def from users + WHERE def foo(x: String) + SQL + + def query(table: String): Array + [] + end + RUBY + parser = TRuby::Parser.new(source) + result = parser.parse + + expect(result[:functions].length).to eq(1) + expect(result[:functions][0][:name]).to eq("query") + end + + it "ignores def patterns inside =begin/=end block comments" do + source = <<~RUBY + =begin + def fake(x: String): Integer + x + end + =end + + def real(name: String): String + name + end + RUBY + parser = TRuby::Parser.new(source) + result = parser.parse + + expect(result[:functions].length).to eq(1) + expect(result[:functions][0][:name]).to eq("real") + end + end + describe "parsing namespaced interfaces" do it "parses namespaced interface correctly" do source = <<~RUBY diff --git a/spec/t_ruby/rbs_generator_spec.rb b/spec/t_ruby/rbs_generator_spec.rb deleted file mode 100644 index f99d9c9..0000000 --- a/spec/t_ruby/rbs_generator_spec.rb +++ /dev/null @@ -1,162 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -describe TRuby::RBSGenerator do - let(:generator) { TRuby::RBSGenerator.new } - - describe "basic function signature generation" do - it "generates RBS for simple function" do - func = { - name: "greet", - params: [{ name: "name", type: "String" }], - return_type: "String", - } - - rbs = generator.generate_function_signature(func) - expect(rbs).to include("def greet") - expect(rbs).to include("String") - end - - it "generates RBS for function with no parameters" do - func = { - name: "hello", - params: [], - return_type: "String", - } - - rbs = generator.generate_function_signature(func) - expect(rbs).to include("def hello") - end - - it "generates RBS for function with multiple parameters" do - func = { - name: "add", - params: [ - { name: "a", type: "Integer" }, - { name: "b", type: "Integer" }, - ], - return_type: "Integer", - } - - rbs = generator.generate_function_signature(func) - expect(rbs).to include("add") - expect(rbs).to include("Integer") - end - - it "handles void return type" do - func = { - name: "log", - params: [{ name: "msg", type: "String" }], - return_type: "void", - } - - rbs = generator.generate_function_signature(func) - expect(rbs).to include("void") - end - - it "handles parameters without types" do - func = { - name: "process", - params: [{ name: "value", type: nil }], - return_type: nil, - } - - rbs = generator.generate_function_signature(func) - expect(rbs).to include("process") - end - end - - describe "type alias generation" do - it "generates RBS for simple type alias" do - type_alias = { name: "UserId", definition: "String" } - - rbs = generator.generate_type_alias(type_alias) - expect(rbs).to include("type UserId") - expect(rbs).to include("String") - end - - it "generates multiple type aliases" do - aliases = [ - { name: "UserId", definition: "String" }, - { name: "Count", definition: "Integer" }, - ] - - rbs = generator.generate_type_aliases(aliases) - expect(rbs).to include("UserId") - expect(rbs).to include("Count") - end - end - - describe "complete RBS file generation" do - it "generates RBS content from parsed data" do - functions = [ - { - name: "greet", - params: [{ name: "name", type: "String" }], - return_type: "String", - }, - ] - type_aliases = [{ name: "UserId", definition: "String" }] - - rbs = generator.generate(functions, type_aliases) - expect(rbs).to include("type UserId") - expect(rbs).to include("def greet") - end - - it "returns empty string when no functions or aliases" do - rbs = generator.generate([], []) - expect(rbs).to be_a(String) - end - - it "generates valid RBS format" do - functions = [ - { - name: "create", - params: [{ name: "name", type: "String" }], - return_type: "Boolean", - }, - ] - - rbs = generator.generate(functions, []) - # Basic validation: RBS should be readable - expect(rbs).not_to include("nil:") - end - end - - describe "RBS format validation" do - it "maintains proper RBS syntax" do - func = { - name: "test_method", - params: [{ name: "x", type: "Integer" }], - return_type: "String", - } - - rbs = generator.generate_function_signature(func) - # Should have basic RBS format - expect(rbs).to match(/def\s+\w+/) - end - - it "handles array types in RBS" do - func = { - name: "process_items", - params: [{ name: "items", type: "Array" }], - return_type: "Array", - } - - rbs = generator.generate_function_signature(func) - expect(rbs).to include("Array") - end - - it "handles hash types in RBS" do - func = { - name: "get_data", - params: [{ name: "key", type: "String" }], - return_type: "Hash", - } - - rbs = generator.generate_function_signature(func) - expect(rbs).to include("Hash") - end - end -end diff --git a/spec/t_ruby/type_erasure_spec.rb b/spec/t_ruby/type_erasure_spec.rb index 8bf42e5..a825b9f 100644 --- a/spec/t_ruby/type_erasure_spec.rb +++ b/spec/t_ruby/type_erasure_spec.rb @@ -40,6 +40,41 @@ result = eraser.erase expect(result).to include("def create(name, age, active)") end + + it "preserves default value for parameter with type" do + source = "def greet(name: String, greeting: String = \"Hello\")\n " \ + "\"\#{greeting}, \#{name}!\"\nend" + eraser = TRuby::TypeErasure.new(source) + + result = eraser.erase + expect(result).to include('def greet(name, greeting = "Hello")') + expect(result).not_to include(": String") + end + + it "preserves default value with numeric value" do + source = "def add(a: Integer, b: Integer = 0)\n a + b\nend" + eraser = TRuby::TypeErasure.new(source) + + result = eraser.erase + expect(result).to include("def add(a, b = 0)") + end + + it "preserves default value with nil" do + source = "def find(id: Integer, fallback: String = nil)\n id\nend" + eraser = TRuby::TypeErasure.new(source) + + result = eraser.erase + expect(result).to include("def find(id, fallback = nil)") + end + + it "preserves multiple default values" do + source = "def config(host: String = \"localhost\", port: Integer = 8080)\n " \ + "host\nend" + eraser = TRuby::TypeErasure.new(source) + + result = eraser.erase + expect(result).to include('def config(host = "localhost", port = 8080)') + end end context "return type annotations" do