DecodeSourceDecodeRBSMethod

class Method

Represents a Ruby method definition wrapper for RBS generation.

Definitions

def initialize(definition)

Initialize a new method wrapper.

Signature

parameter definition Decode::Definition

The method definition to wrap.

Implementation

def initialize(definition)
	super
	@signatures = nil
	@keyword_arguments = nil
	@return_type = nil
	@parameters = nil
end

def signatures

Extract method signatures from the method definition.

Signature

returns Array

The extracted signatures for this method.

Implementation

def signatures
	@signatures ||= extract_signatures
end

def keyword_arguments

Extract keyword arguments from the method definition.

Signature

returns Hash

Hash with :required and :optional keys.

Implementation

def keyword_arguments
	@keyword_arguments ||= extract_keyword_arguments(@definition, nil)
end

def return_type

Extract return type from the method definition.

Signature

returns ::RBS::Types::t

The RBS return type.

Implementation

def return_type
	@return_type ||= extract_return_type(@definition, nil) || ::RBS::Parser.parse_type("untyped")
end

def parameters

Extract parameters from the method definition.

Signature

returns Array

Array of RBS parameter objects.

Implementation

def parameters
	@parameters ||= extract_parameters(@definition, nil)
end

def to_rbs_ast(index = nil)

Convert the method definition to RBS AST

Implementation

def to_rbs_ast(index = nil)
	method_name = @definition.name
	comment = self.comment
	
	overloads = []
	if signatures.any?
		signatures.each do |signature_string|
			method_type = ::RBS::Parser.parse_method_type(signature_string)
			overloads << ::RBS::AST::Members::MethodDefinition::Overload.new(
				method_type: method_type,
				annotations: []
			)
		end
	else
		return_type = self.return_type
		
		# Get parameters using AST-based detection
		if ast_function = build_function_type_from_ast(@definition, index)
			method_type = ::RBS::MethodType.new(
				type_params: [],
				type: ast_function,
				block: extract_block_type(@definition, index),
				location: nil
			)
		else
			# Fall back to documentation-based approach
			parameters = self.parameters
			keywords = self.keyword_arguments
			block_type = extract_block_type(@definition, index)
			
			method_type = ::RBS::MethodType.new(
				type_params: [],
				type: ::RBS::Types::Function.new(
					required_positionals: parameters,
					optional_positionals: [],
					rest_positionals: nil,
					trailing_positionals: [],
					required_keywords: keywords[:required],
					optional_keywords: keywords[:optional],
					rest_keywords: nil,
					return_type: return_type
				),
				block: block_type,
				location: nil
			)
		end
		
		overloads << ::RBS::AST::Members::MethodDefinition::Overload.new(
			method_type: method_type,
			annotations: []
		)
	end
	
	kind = @definition.receiver ? :singleton : :instance
	
	::RBS::AST::Members::MethodDefinition.new(
	name: method_name.to_sym,
	kind: kind,
	overloads: overloads,
	annotations: [],
	location: nil,
	comment: comment,
	overloading: false,
	visibility: @definition.visibility || :public
)
end

def build_function_type_from_ast(definition, index)

Build a complete RBS function type from AST information.

Signature

parameter definition Definition

The method definition.

parameter index Index

The index for context.

returns RBS::Types::Function

The complete function type, or nil if no AST.

Implementation

def build_function_type_from_ast(definition, index)
	node = definition.node
	# Only return nil if we don't have an AST node at all
	return nil unless node&.respond_to?(:parameters)
	
	doc_types = extract_documented_parameter_types(definition)
	
	required_positionals = []
	optional_positionals = []
	rest_positionals = nil
	required_keywords = {}
	optional_keywords = {}
	keyword_rest = nil
	
	# Only process parameters if the node actually has them:
	if node.parameters
		# Handle required positional parameters:
		if node.parameters.respond_to?(:requireds) && node.parameters.requireds
			node.parameters.requireds.each do |param|
				name = param.name
				type = doc_types[name.to_s] || ::RBS::Parser.parse_type("untyped")
				
				required_positionals << ::RBS::Types::Function::Param.new(
					type: type,
					name: name.to_sym
				)
			end
		end
		
		# Handle optional positional parameters (with defaults):
		if node.parameters.respond_to?(:optionals) && node.parameters.optionals
			node.parameters.optionals.each do |param|
				name = param.name
				# For optional parameters, use the documented type as-is (don't make it nullable):
				type = doc_types[name.to_s] || ::RBS::Parser.parse_type("untyped")
				
				optional_positionals << ::RBS::Types::Function::Param.new(
					type: type,
					name: name.to_sym
				)
			end
		end
		
		# Handle rest parameter (*args):
		if node.parameters.respond_to?(:rest) && node.parameters.rest
			rest_param = node.parameters.rest
			name = rest_param.respond_to?(:name) && rest_param.name ? rest_param.name : :args
			base_type = doc_types[name.to_s] || ::RBS::Parser.parse_type("untyped")
			
			rest_positionals = ::RBS::Types::Function::Param.new(
				type: base_type,
				name: name.to_sym
			)
		end
		
		# Handle keyword parameters:
		if node.parameters.respond_to?(:keywords) && node.parameters.keywords
			node.parameters.keywords.each do |param|
				name = param.name
				type = doc_types[name.to_s] || ::RBS::Parser.parse_type("untyped")
				
				if param.respond_to?(:value) && param.value
					# Has default value - optional keyword:
					optional_keywords[name.to_sym] = type
				else
					# No default value - required keyword:
					required_keywords[name.to_sym] = type
				end
			end
		end
		
		# Handle keyword rest parameter (**kwargs):
		if node.parameters.respond_to?(:keyword_rest) && node.parameters.keyword_rest
			rest_param = node.parameters.keyword_rest
			if rest_param.respond_to?(:name) && rest_param.name
				name = rest_param.name
				base_type = doc_types[name.to_s] || ::RBS::Parser.parse_type("untyped")
				
				keyword_rest = ::RBS::Types::Function::Param.new(
					type: base_type,
					name: name.to_sym
				)
			end
		end
	end
	
	return_type = extract_return_type(@definition, index) || ::RBS::Parser.parse_type("untyped")
	
	::RBS::Types::Function.new(
		required_positionals: required_positionals,
		optional_positionals: optional_positionals,
		rest_positionals: rest_positionals,
		trailing_positionals: [],
		required_keywords: required_keywords,
		optional_keywords: optional_keywords,
		rest_keywords: keyword_rest,
		return_type: return_type
	)
end

def extract_return_type(definition, index)

Extract return type from method documentation.

Implementation

def extract_return_type(definition, index)
	# Look for @returns tags in the method's documentation:
	documentation = definition.documentation
	
	# Find all @returns tags:
	returns_tags = documentation&.filter(Decode::Comment::Returns)&.to_a
	
	if returns_tags&.any?
		if returns_tags.length == 1
			# Single return type:
			type_string = returns_tags.first.type.strip
			Type.parse(type_string)
		else
			# Multiple return types - create union:
			types = returns_tags.map do |tag|
				type_string = tag.type.strip
				Type.parse(type_string)
			end
			
			::RBS::Types::Union.new(types: types, location: nil)
		end
	else
		# Infer return type based on method name patterns:
		infer_return_type(definition)
	end
end

def extract_parameters(definition, index)

Extract parameter types from method documentation.

Implementation

def extract_parameters(definition, index)
	# Try AST-based extraction first:
	if ast_params = extract_parameters_from_ast(definition)
		return ast_params unless ast_params.empty?
	end
	
	# Fall back to documentation-based extraction:
	documentation = definition.documentation
	return [] unless documentation
	
	# Find @parameter tags (but not @option tags, which are handled separately):
	param_tags = documentation.filter(Decode::Comment::Parameter).to_a
	param_tags = param_tags.reject {|tag| tag.is_a?(Decode::Comment::Option)}
	return [] if param_tags.empty?
	
	param_tags.map do |tag|
		name = tag.name
		type_string = tag.type.strip
		type = Type.parse(type_string)
		
		::RBS::Types::Function::Param.new(
			type: type,
			name: name.to_sym
		)
	end
end

def extract_parameters_from_ast(definition)

Extract parameter information from the Prism AST node.

Signature

parameter definition Definition

The method definition with AST node.

returns Array

Array of RBS parameter objects, or nil if no AST available.

Implementation

def extract_parameters_from_ast(definition)
	node = definition.node
	return nil unless node&.respond_to?(:parameters) && node.parameters
	
	params = []
	doc_types = extract_documented_parameter_types(definition)
	
	# Handle required positional parameters:
	if node.parameters.respond_to?(:requireds) && node.parameters.requireds
		node.parameters.requireds.each do |param|
			name = param.name
			type = doc_types[name.to_s] || ::RBS::Parser.parse_type("untyped")
			
			params << ::RBS::Types::Function::Param.new(
				type: type,
				name: name.to_sym
			)
		end
	end
	
	# Handle optional positional parameters (with defaults):
	if node.parameters.respond_to?(:optionals) && node.parameters.optionals
		node.parameters.optionals.each do |param|
			name = param.name
			# For optional parameters, make the documented type optional if not already:
			base_type = doc_types[name.to_s] || ::RBS::Parser.parse_type("untyped")
			type = make_type_optional_if_needed(base_type)
			
			params << ::RBS::Types::Function::Param.new(
				type: type,
				name: name.to_sym
			)
		end
	end
	
	# Handle rest parameter (*args):
	if node.parameters.respond_to?(:rest) && node.parameters.rest
		rest_param = node.parameters.rest
		name = rest_param.respond_to?(:name) && rest_param.name ? rest_param.name : :args
		base_type = doc_types[name.to_s] || ::RBS::Parser.parse_type("untyped")
		# Rest parameters should be `Array[T]`:
		array_type = ::RBS::Types::ClassInstance.new(
			name: ::RBS::TypeName.new(name: :Array, namespace: ::RBS::Namespace.empty),
			args: [base_type],
			location: nil
		)
		
		params << ::RBS::Types::Function::Param.new(
			type: array_type,
			name: name.to_sym
		)
	end
	
	params
end

def extract_keyword_arguments(definition, index)

Extract keyword arguments from @option tags and AST.

Implementation

def extract_keyword_arguments(definition, index)
	# Try AST-based extraction first:
	if ast_keywords = extract_keyword_arguments_from_ast(definition)
		return ast_keywords
	end
	
	# Fall back to documentation-based extraction:
	documentation = definition.documentation
	return { required: {}, optional: {} } unless documentation
	
	# Find @option tags:
	option_tags = documentation.filter(Decode::Comment::Option).to_a
	return { required: {}, optional: {} } if option_tags.empty?
	
	keywords = { required: {}, optional: {} }
	
	option_tags.each do |tag|
		name = tag.name.to_s
		# Remove leading colon if present (e.g., ":cached" -> "cached"):
		name = name.sub(/\A:/, "")
		
		type_string = tag.type.strip
		type = Type.parse(type_string)
		
		# Determine if the keyword is optional based on the type annotation.
		# If the type is nullable (contains nil or ends with ?), make it optional:
		if Type.nullable?(type)
			keywords[:optional][name.to_sym] = type
		else
			keywords[:required][name.to_sym] = type
		end
	end
	
	keywords
end

def extract_keyword_arguments_from_ast(definition)

Extract keyword arguments from the Prism AST node.

Signature

parameter definition Definition

The method definition with AST node.

returns Hash

Hash with :required and :optional keyword arguments, or nil if no AST.

Implementation

def extract_keyword_arguments_from_ast(definition)
	node = definition.node
	return nil unless node&.respond_to?(:parameters) && node.parameters
	
	required = {}
	optional = {}
	doc_types = extract_documented_parameter_types(definition)
	
	# Handle keyword parameters:
	if node.parameters.respond_to?(:keywords) && node.parameters.keywords
		node.parameters.keywords.each do |param|
			name = param.name
			base_type = doc_types[name.to_s] || ::RBS::Parser.parse_type("untyped")
			
			if param.respond_to?(:value) && param.value
				# Has default value - optional keyword:
				type = make_type_optional_if_needed(base_type)
				optional[name.to_sym] = type
			else
				# No default value - required keyword:
				required[name.to_sym] = base_type
			end
		end
	end
	
	# Handle keyword rest parameter (**kwargs):
	if node.parameters.respond_to?(:keyword_rest) && node.parameters.keyword_rest
		rest_param = node.parameters.keyword_rest
		if rest_param.respond_to?(:name) && rest_param.name
			name = rest_param.name
			base_type = doc_types[name.to_s] || ::RBS::Parser.parse_type("untyped")
			# Keyword rest should be `Hash[Symbol, T]`:
			hash_type = ::RBS::Types::ClassInstance.new(
				name: ::RBS::TypeName.new(name: :Hash, namespace: ::RBS::Namespace.empty),
				args: [
					::RBS::Types::ClassInstance.new(name: ::RBS::TypeName.new(name: :Symbol, namespace: ::RBS::Namespace.empty), args: [], location: nil),
					base_type
				],
				location: nil
			)
			optional[name.to_sym] = hash_type
		end
	end
	
	{ required: required, optional: optional }
end

def extract_documented_parameter_types(definition)

Extract documented parameter types into a hash for lookup.

Signature

parameter definition Definition

The method definition.

returns Hash

Map of parameter name to RBS type.

Implementation

def extract_documented_parameter_types(definition)
	doc_types = {}
	documentation = definition.documentation
	return doc_types unless documentation
	
	# Extract types from @parameter tags:
	param_tags = documentation.filter(Decode::Comment::Parameter).to_a
	param_tags.each do |tag|
		doc_types[tag.name] = Type.parse(tag.type.strip)
	end
	
	# Extract types from @option tags  
	option_tags = documentation.filter(Decode::Comment::Option).to_a
	option_tags.each do |tag|
		# Remove leading colon:
		name = tag.name.sub(/\A:/, "")
		doc_types[name] = Type.parse(tag.type.strip)
	end
	
	doc_types
end

def make_type_optional_if_needed(type)

Make a type optional if it's not already nullable.

Signature

parameter type RBS::Types::t

The base type.

returns RBS::Types::t

The optionally-nullable type.

Implementation

def make_type_optional_if_needed(type)
	return type if Type.nullable?(type)
	
	# Create a union with nil to make it optional:
	::RBS::Types::Union.new(
		types: [type, ::RBS::Types::Bases::Nil.new(location: nil)],
		location: nil
	)
end

def extract_block_type(definition, index)

Extract block type from method documentation.

Implementation

def extract_block_type(definition, index)
	documentation = definition.documentation
	return nil unless documentation
	
	# Find `@yields` tags:
	yields_tag = documentation.filter(Decode::Comment::Yields).first
	return nil unless yields_tag
	
	# Extract block parameters from nested `@parameter` tags:
	block_params = yields_tag.filter(Decode::Comment::Parameter).map do |param_tag|
		name = param_tag.name
		type_string = param_tag.type.strip
		type = Type.parse(type_string)
		
		::RBS::Types::Function::Param.new(
			type: type,
			name: name.to_sym
		)
	end
	
	# Parse the block signature to determine if it's required.
	# Check both the directive name and the block signature:
	block_signature = yields_tag.block
	directive_name = yields_tag.directive
	required = !directive_name.include?("?") && !block_signature.include?("?") && !block_signature.include?("optional")
	
	# Determine block return type (default to `void` if not specified):
	block_return_type = ::RBS::Parser.parse_type("void")
	
	# Create the block function type:
	block_function = ::RBS::Types::Function.new(
		required_positionals: block_params,
		optional_positionals: [],
		rest_positionals: nil,
		trailing_positionals: [],
		required_keywords: {},
		optional_keywords: {},
		rest_keywords: nil,
		return_type: block_return_type
	)
	
	# Create and return the block type:
	::RBS::Types::Block.new(
		type: block_function,
		required: required,
		self_type: nil
	)
end

def infer_return_type(definition)

Infer return type based on method patterns and heuristics.

Implementation

def infer_return_type(definition)
	method_name = definition.name
	method_name_str = method_name.to_s
	
	# Methods ending with `?` are typically boolean:
	if method_name_str.end_with?("?")
		return ::RBS::Parser.parse_type("bool")
	end
	
	# Methods named `initialize` return `void`:
	if method_name == :initialize
		return ::RBS::Parser.parse_type("void")
	end
	
	# Methods with names that suggest they return `self`:
	if method_name_str.match?(/^(add|append|prepend|push|<<|concat|merge!|sort!|reverse!|clear|delete|remove)/)
		return ::RBS::Parser.parse_type("self")
	end
	
	# Default to `untyped`:
	::RBS::Parser.parse_type("untyped")
end