diff --git a/lib/solargraph.rb b/lib/solargraph.rb index 75c454dde..9ad6eac3c 100755 --- a/lib/solargraph.rb +++ b/lib/solargraph.rb @@ -49,6 +49,7 @@ class InvalidRubocopVersionError < RuntimeError; end autoload :RbsMap, 'solargraph/rbs_map' autoload :GemPins, 'solargraph/gem_pins' autoload :PinCache, 'solargraph/pin_cache' + autoload :RbsTranslator, 'solargraph/rbs_translator' dir = File.dirname(__FILE__) VIEWS_PATH = File.join(dir, 'solargraph', 'views') diff --git a/lib/solargraph/parser/parser_gem/node_processors/namespace_node.rb b/lib/solargraph/parser/parser_gem/node_processors/namespace_node.rb index 8051dfee8..d09539275 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/namespace_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/namespace_node.rb @@ -8,22 +8,26 @@ class NamespaceNode < Parser::NodeProcessor::Base include ParserGem::NodeMethods def process - superclass_name = nil - superclass_name = unpack_name(node.children[1]) if node.type == :class && node.children[1]&.type == :const + name = unpack_name(node.children[0]) + comments = comments_for(node) + superclass_name = if node.type == :class + "#{type_from_node}#{parameters_from_inline_rbs}" + end loc = get_node_location(node) nspin = Solargraph::Pin::Namespace.new( type: node.type, location: loc, closure: region.closure, - name: unpack_name(node.children[0]), - comments: comments_for(node), + name: name, + comments: comments, visibility: :public, gates: region.closure.gates.freeze, source: :parser ) pins.push nspin - unless superclass_name.nil? + Solargraph.logger.warn "Superclass: #{superclass_name}" if superclass_name&.start_with?('Array') + if superclass_name pins.push Pin::Reference::Superclass.new( location: loc, closure: pins.last, @@ -33,6 +37,25 @@ def process end process_children region.update(closure: nspin, visibility: :public) end + + private + + # @param comments [String] + # @return [String, nil] + def parameters_from_inline_rbs + source = region.source.code_for(node) + match = source.match(/[^\n]*?#\s?+\[([^\]]*)/) + return unless match && match[1] + + code = match[1].strip + return if code.empty? + + "<#{code}>" + end + + def type_from_node + unpack_name(node.children[1]) if node.children[1]&.type == :const + end end end end diff --git a/lib/solargraph/pin/method.rb b/lib/solargraph/pin/method.rb index 09d681c67..b028188ea 100644 --- a/lib/solargraph/pin/method.rb +++ b/lib/solargraph/pin/method.rb @@ -143,7 +143,7 @@ def symbol_kind end def return_type - @return_type ||= ComplexType.new(signatures.map(&:return_type).flat_map(&:items)) + @return_type ||= return_type_from_inline_rbs || ComplexType.new(signatures.map(&:return_type).flat_map(&:items)) end # @param parameters [::Array] @@ -188,18 +188,11 @@ def generate_signature parameters, return_type # @return [::Array] def signatures - @signatures ||= begin - top_type = generate_complex_type - result = [] - result.push generate_signature(parameters, top_type) if top_type.defined? - unless overloads.empty? - result.concat(overloads.map do |meth| - generate_signature(meth.parameters, meth.return_type) - end) - end - result.push generate_signature(parameters, @return_type || ComplexType::UNDEFINED) if result.empty? - result - end + @signatures ||= if inline_rbs.empty? + signatures_from_yard + else + signatures_from_inline_rbs + end end # @param return_type [ComplexType] @@ -451,7 +444,6 @@ def rest_of_stack api_map attr_writer :block, :signature_help, :documentation, :return_type - # @sg-ignore Need to add nil check here def dodgy_visibility_source? # as of 2025-03-12, the RBS generator used for # e.g. activesupport did not understand 'private' markings @@ -721,6 +713,41 @@ def concat_example_tags .join("\n") .concat("```\n") end + + # @return [ComplexType, nil] + def return_type_from_inline_rbs + return nil if inline_rbs.empty? + method_type = RBS::Parser.parse_method_type(inline_rbs) + RbsTranslator.to_complex_type(method_type.type.return_type) + rescue RBS::ParsingError + nil + end + + # @return [Array] + def signatures_from_inline_rbs + method_type = RBS::Parser.parse_method_type(inline_rbs) + [RbsTranslator.to_signature(method_type, self, parameter_names)] + rescue RBS::ParsingError + signatures_from_yard + end + + # @return [Array] + def signatures_from_yard + top_type = generate_complex_type + result = [] + result.push generate_signature(parameters, top_type) if top_type.defined? + result.concat(overloads.map { |meth| generate_signature(meth.parameters, meth.return_type) }) unless overloads.empty? + result.push generate_signature(parameters, @return_type || ComplexType::UNDEFINED) if result.empty? + result + end + + # @return [String] + def inline_rbs + comments.lines + .select { |line| line.start_with?(': ') } + .map { |line| line[2..].strip } + .join("\n") + end end end end diff --git a/lib/solargraph/rbs_map/conversions.rb b/lib/solargraph/rbs_map/conversions.rb index 3caf13162..377aa1b30 100644 --- a/lib/solargraph/rbs_map/conversions.rb +++ b/lib/solargraph/rbs_map/conversions.rb @@ -67,7 +67,6 @@ def convert_decl_to_pin decl, closure unless closure.name == '' || decl.name.absolute? Solargraph.assert_or_log(:rbs_closure, "Ignoring closure #{closure.inspect} on interface #{decl.inspect}") end - # STDERR.puts "Skipping interface #{decl.name.relative!}" interface_decl_to_pin decl when RBS::AST::Declarations::TypeAlias # @sg-ignore flow sensitive typing should support case/when @@ -80,7 +79,7 @@ def convert_decl_to_pin decl, closure # @sg-ignore Wrong argument type for Solargraph::Pin::Reference::TypeAlias.new: return_type expected Solargraph::ComplexType, received Solargraph::ComplexType::UniqueType, Solargraph::ComplexType Solargraph::Pin::Reference::TypeAlias.new( # @sg-ignore Unresolved calls to name, type, type_location; return_type type mismatch - name: ComplexType.try_parse(decl.name.to_s).to_s, return_type: other_type_to_type(decl.type).force_rooted, closure: closure, source: :rbs, type_location: location_decl_to_pin_location(decl.location) + name: ComplexType.try_parse(decl.name.to_s).to_s, return_type: RbsTranslator.to_complex_type(decl.type).force_rooted, closure: closure, source: :rbs, type_location: location_decl_to_pin_location(decl.location) ) ) when RBS::AST::Declarations::Module @@ -169,7 +168,8 @@ def build_type type_name, type_args = [] rbs_name = type_name.relative!.to_s base = RBS_TO_CLASS.fetch(rbs_name, rbs_name) - params = type_args.map { |a| other_type_to_type(a) } + params = type_args.map { |a| RbsTranslator.to_complex_type(a) } + # @todo Tuples are in flux # tuples have their own class and are handled in other_type_to_type if base == 'Hash' && params.length == 2 ComplexType::UniqueType.new(base, [params.first], [params.last], rooted: type_name.absolute?, @@ -185,9 +185,9 @@ def build_type type_name, type_args = [] # @return [void] def convert_self_type_to_pins decl, closure type = build_type(decl.name, decl.args) - generic_values = type.all_params.map(&:rooted_tags) + generic_values = type.all_params.map(&:to_s) include_pin = Solargraph::Pin::Reference::Include.new( - name: type.name, + name: decl.name.relative!.to_s, type_location: location_decl_to_pin_location(decl.location), generic_values: generic_values, closure: closure, @@ -274,7 +274,10 @@ def class_decl_to_pin decl # @type [Hash{String => ComplexType, ComplexType::UniqueType}] generic_defaults = {} decl.type_params.each do |param| - generic_defaults[param.name.to_s] = other_type_to_type param.default_type if param.default_type + if param.default_type + complex_type = RbsTranslator.to_complex_type(param.default_type).force_rooted + generic_defaults[param.name.to_s] = complex_type + end end class_name = fqns(decl.name) @@ -297,7 +300,8 @@ def class_decl_to_pin decl pins.push class_pin if decl.super_class type = build_type(decl.super_class.name, decl.super_class.args) - generic_values = type.all_params.map(&:rooted_tags) + generic_values = type.all_params.map(&:to_s) + superclass_name = decl.super_class.name.to_s pins.push Solargraph::Pin::Reference::Superclass.new( type_location: location_decl_to_pin_location(decl.super_class.location), closure: class_pin, @@ -316,7 +320,7 @@ def interface_decl_to_pin decl class_pin = Solargraph::Pin::Namespace.new( type: :module, type_location: location_decl_to_pin_location(decl.location), - name: fqns(decl.name), + name: decl.name.relative!.to_s, closure: Solargraph::Pin::ROOT_PIN, comments: decl.comment&.string, generics: type_parameter_names(decl), @@ -335,7 +339,7 @@ def interface_decl_to_pin decl def module_decl_to_pin decl module_pin = Solargraph::Pin::Namespace.new( type: :module, - name: fqns(decl.name), + name: decl.name.relative!.to_s, type_location: location_decl_to_pin_location(decl.location), closure: Solargraph::Pin::ROOT_PIN, comments: decl.comment&.string, @@ -380,6 +384,9 @@ def create_constant fqns, type, comments, decl, base = nil ) rooted_tag = type.rooted_tags rooted_tag = "#{base}<#{rooted_tag}>" if base + # @todo alt version + # tag = "#{base}<#{tag}>" if base + # rooted_tag = ComplexType.parse(tag).force_rooted.rooted_tags constant_pin.docstring.add_tag(YARD::Tags::Tag.new(:return, '', rooted_tag)) constant_pin end @@ -406,9 +413,8 @@ def module_alias_decl_to_pin decl # @param decl [RBS::AST::Declarations::Constant] # @return [void] def constant_decl_to_pin decl - target_type = other_type_to_type(decl.type) - constant_name = fqns(decl.name) - pins.push create_constant(constant_name, target_type, decl.comment&.string, decl) + tag = RbsTranslator.to_complex_type(decl.type) + pins.push create_constant(decl.name.relative!.to_s, tag, decl.comment&.string, decl) end # @param decl [RBS::AST::Declarations::Global] @@ -423,7 +429,7 @@ def global_decl_to_pin decl type_location: location_decl_to_pin_location(decl.location), source: :rbs ) - rooted_tag = other_type_to_type(decl.type).rooted_tags + rooted_tag = RbsTranslator.to_complex_type(decl.type).force_rooted.rooted_tags pin.docstring.add_tag(YARD::Tags::Tag.new(:type, '', rooted_tag)) pins.push pin end @@ -529,73 +535,69 @@ def method_def_to_pin decl, closure, context pin.instance_variable_set(:@return_type, ComplexType::VOID) end end - return unless decl.singleton? - final_scope = :class - name = decl.name.to_s - visibility = calculate_method_visibility(decl, context, closure, final_scope, name) - pin = Solargraph::Pin::Method.new( - name: name, - closure: closure, - comments: decl.comment&.string, - type_location: location_decl_to_pin_location(decl.location), - visibility: visibility, - scope: final_scope, - signatures: [], - generics: generics, - source: :rbs - ) - pin.signatures.concat method_def_to_sigs(decl, pin) - pins.push pin + if decl.singleton? + final_scope = :class + name = decl.name.to_s + visibility = calculate_method_visibility(decl, context, closure, final_scope, name) + pin = Solargraph::Pin::Method.new( + name: name, + closure: closure, + comments: decl.comment&.string, + type_location: location_decl_to_pin_location(decl.location), + visibility: visibility, + scope: final_scope, + signatures: [], + generics: generics, + source: :rbs + ) + pin.signatures.concat method_def_to_sigs(decl, pin) + pins.push pin + end end # @param decl [RBS::AST::Members::MethodDefinition] # @param pin [Pin::Method] # @return [void] def method_def_to_sigs decl, pin - # @param overload [RBS::AST::Members::MethodDefinition::Overload] + # rubocop:disable Style/SafeNavigationChainLength + implicit_nil = decl.overloads.first&.annotations&.map(&:string)&.include?('implicitly-returns-nil') || false + # rubocop:enable Style/SafeNavigationChainLength # @param overload [RBS::AST::Members::MethodDefinition::Overload] decl.overloads.map do |overload| - # @sg-ignore Wrong argument type for Solargraph::RbsMap::Conversions#location_decl_to_pin_location: - # location expected RBS::Location, nil, received RBS::Location<:type, :type_params>, RBS::AST::Members::Attribute::loc, nil type_location = location_decl_to_pin_location(overload.method_type.location) - generics = type_parameter_names(overload.method_type) - signature_parameters, signature_return_type = parts_of_function(overload.method_type, pin) - rbs_block = overload.method_type.block - block = if rbs_block - block_parameters, block_return_type = parts_of_function(rbs_block, pin) - Pin::Signature.new(generics: generics, parameters: block_parameters, - return_type: block_return_type, source: :rbs, + generics = overload.method_type.type_params.map(&:name).map(&:to_s) + signature_parameters, signature_return_type = parts_of_function(overload.method_type, pin, implicit_nil) + block = if overload.method_type.block + block_parameters, block_return_type = parts_of_function(overload.method_type.block, pin, implicit_nil) + Pin::Signature.new(generics: generics, parameters: block_parameters, return_type: block_return_type, source: :rbs, type_location: type_location, closure: pin) end - Pin::Signature.new(generics: generics, parameters: signature_parameters, - return_type: signature_return_type, block: block, source: :rbs, + Pin::Signature.new(generics: generics, parameters: signature_parameters, return_type: signature_return_type, block: block, source: :rbs, type_location: type_location, closure: pin) end end # @param location [RBS::Location, nil] # @return [Solargraph::Location, nil] - def location_decl_to_pin_location location + def location_decl_to_pin_location(location) return nil if location&.name.nil? - # @sg-ignore flow sensitive typing should handle return nil if location&.name.nil? start_pos = Position.new(location.start_line - 1, location.start_column) - # @sg-ignore flow sensitive typing should handle return nil if location&.name.nil? end_pos = Position.new(location.end_line - 1, location.end_column) range = Range.new(start_pos, end_pos) - # @sg-ignore flow sensitve typing should handle return nil if location&.name.nil? Location.new(location.name.to_s, range) end # @param type [RBS::MethodType, RBS::Types::Block] # @param pin [Pin::Method] + # @param implicit_nil [Boolean] # @return [Array(Array, ComplexType)] - def parts_of_function type, pin + def parts_of_function type, pin, implicit_nil type_location = pin.type_location if defined?(RBS::Types::UntypedFunction) && type.type.is_a?(RBS::Types::UntypedFunction) return [ [Solargraph::Pin::Parameter.new(decl: :restarg, name: 'arg', closure: pin, source: :rbs, type_location: type_location)], - method_type_to_type(type) + method_type_to_type(type, implicit_nil) ] end @@ -659,10 +661,21 @@ def parts_of_function type, pin source: :rbs, type_location: type_location) end - return_type = method_type_to_type(type) + return_type = method_type_to_type(type, implicit_nil) [parameters, return_type] end + # @param type [RBS::MethodType,RBS::Types::Block] + # @param pin [Pin::Method] + # @param implicit_nil [Boolean] + # @return [Array(Array, ComplexType)] + def parts_of_function type, pin, implicit_nil + [ + RbsTranslator.to_parameter_pins(type, pin, pin.parameter_names), + extract_method_type_return_type(type, implicit_nil).force_rooted + ] + end + # @param decl [RBS::AST::Members::AttrReader,RBS::AST::Members::AttrAccessor] # @param closure [Pin::Namespace] # @param context [Context] @@ -681,7 +694,7 @@ def attr_reader_to_pin decl, closure, context visibility: visibility, source: :rbs ) - rooted_tag = other_type_to_type(decl.type).rooted_tags + rooted_tag = RbsTranslator.to_complex_type(decl.type).force_rooted.rooted_tags pin.docstring.add_tag(YARD::Tags::Tag.new(:return, '', rooted_tag)) logger.debug do "Conversions#attr_reader_to_pin(name=#{name.inspect}, visibility=#{visibility.inspect}) => #{pin.inspect}" @@ -712,13 +725,13 @@ def attr_writer_to_pin decl, closure, context pin.parameters << Solargraph::Pin::Parameter.new( name: 'value', - return_type: other_type_to_type(decl.type), + return_type: RbsTranslator.to_complex_type(decl.type).force_rooted, source: :rbs, closure: pin, type_location: type_location ) - rooted_tags = other_type_to_type(decl.type).rooted_tags - pin.docstring.add_tag(YARD::Tags::Tag.new(:return, '', rooted_tags)) + rooted_tag = RbsTranslator.to_complex_type(decl.type).force_rooted.rooted_tags + pin.docstring.add_tag(YARD::Tags::Tag.new(:return, '', rooted_tag)) pins.push pin end @@ -742,7 +755,7 @@ def ivar_to_pin decl, closure comments: decl.comment&.string, source: :rbs ) - rooted_tag = other_type_to_type(decl.type).rooted_tags + rooted_tag = RbsTranslator.to_complex_type(decl.type).force_rooted.rooted_tags pin.docstring.add_tag(YARD::Tags::Tag.new(:type, '', rooted_tag)) pins.push pin end @@ -759,7 +772,7 @@ def cvar_to_pin decl, closure type_location: location_decl_to_pin_location(decl.location), source: :rbs ) - rooted_tag = other_type_to_type(decl.type).rooted_tags + rooted_tag = RbsTranslator.to_complex_type(decl.type).force_rooted.rooted_tags pin.docstring.add_tag(YARD::Tags::Tag.new(:type, '', rooted_tag)) pins.push pin end @@ -776,7 +789,7 @@ def civar_to_pin decl, closure type_location: location_decl_to_pin_location(decl.location), source: :rbs ) - rooted_tag = other_type_to_type(decl.type).rooted_tags + rooted_tag = RbsTranslator.to_complex_type(decl.type).force_rooted.rooted_tags pin.docstring.add_tag(YARD::Tags::Tag.new(:type, '', rooted_tag)) pins.push pin end @@ -786,9 +799,9 @@ def civar_to_pin decl, closure # @return [void] def include_to_pin decl, closure type = build_type(decl.name, decl.args) - generic_values = type.all_params.map(&:rooted_tags) + generic_values = type.all_params.map(&:to_s) pins.push Solargraph::Pin::Reference::Include.new( - name: type.rooted_name, # reference pins use rooted names + name: decl.name.relative!.to_s, type_location: location_decl_to_pin_location(decl.location), generic_values: generic_values, closure: closure, @@ -803,9 +816,8 @@ def prepend_to_pin decl, closure type = build_type(decl.name, decl.args) generic_values = type.all_params.map(&:rooted_tags) pins.push Solargraph::Pin::Reference::Prepend.new( - name: type.rooted_name, # reference pins use rooted names + name: decl.name.relative!.to_s, type_location: location_decl_to_pin_location(decl.location), - generic_values: generic_values, closure: closure, source: :rbs ) @@ -818,9 +830,8 @@ def extend_to_pin decl, closure type = build_type(decl.name, decl.args) generic_values = type.all_params.map(&:rooted_tags) pins.push Solargraph::Pin::Reference::Extend.new( - name: type.rooted_name, # reference pins use rooted names + name: decl.name.relative!.to_s, type_location: location_decl_to_pin_location(decl.location), - generic_values: generic_values, closure: closure, source: :rbs ) @@ -841,90 +852,37 @@ def alias_to_pin decl, closure ) end - # @param type [RBS::MethodType, RBS::Types::Block] - # @return [ComplexType, ComplexType::UniqueType] - def method_type_to_type type - other_type_to_type type.type.return_type - end + RBS_TO_YARD_TYPE = { + 'bool' => 'Boolean', + 'string' => 'String', + 'int' => 'Integer', + 'untyped' => '', + 'NilClass' => 'nil' + } + private_constant :RBS_TO_YARD_TYPE - # @param type [RBS::Types::Bases::Base,Object] RBS type object. - # Note: Generally these extend from RBS::Types::Bases::Base, - # but not all. + # Extract a ComplexType from a MethodType's return type. # - # @return [ComplexType, ComplexType::UniqueType] - def other_type_to_type type - case type - when RBS::Types::Optional - # @sg-ignore flow based typing needs to understand case when class pattern - ComplexType.new([other_type_to_type(type.type), - ComplexType::UniqueType::NIL]) - when RBS::Types::Bases::Any - ComplexType::UNDEFINED - when RBS::Types::Bases::Bool - ComplexType::BOOLEAN - when RBS::Types::Tuple - # @sg-ignore flow based typing needs to understand case when class pattern - tuple_types = type.types.map { |t| other_type_to_type(t) } - ComplexType::UniqueType.new('Array', [], tuple_types, rooted: true, parameters_type: :fixed) - when RBS::Types::Literal - # @sg-ignore flow based typing needs to understand case when class pattern - ComplexType.try_parse(type.literal.inspect).force_rooted - when RBS::Types::Union - # @sg-ignore flow based typing needs to understand case when class pattern - ComplexType.new(type.types.map { |t| other_type_to_type(t) }) - when RBS::Types::Record - # @todo Better record support - ComplexType::UniqueType.new('Hash', rooted: true) - when RBS::Types::Bases::Nil - ComplexType::NIL - when RBS::Types::Bases::Self - ComplexType::SELF - when RBS::Types::Bases::Void - ComplexType::VOID - when RBS::Types::Variable - # @sg-ignore flow based typing needs to understand case when class pattern - ComplexType.parse("generic<#{type.name}>").force_rooted - when RBS::Types::ClassInstance # && !type.args.empty? - # @sg-ignore flow based typing needs to understand case when class pattern - build_type(type.name, type.args) - when RBS::Types::Bases::Instance - ComplexType::SELF - when RBS::Types::Bases::Top - # top is the most super superclass - ComplexType::UniqueType.new('BasicObject', rooted: true) - when RBS::Types::Bases::Bottom - # bottom is used in contexts where nothing will ever return - # - e.g., it could be the return type of 'exit()' or 'raise' - # - # @todo define a specific bottom type and use it to - # determine dead code - ComplexType::UNDEFINED - when RBS::Types::Intersection - # @sg-ignore flow based typing needs to understand case when class pattern - ComplexType.new(type.types.map { |member| other_type_to_type(member) }) - when RBS::Types::Proc - ComplexType::UniqueType.new('Proc', rooted: true) - when RBS::Types::Alias - # type-level alias use - e.g., 'bool' in "type bool = true | false" - # @todo ensure these get resolved after processing all aliases - # @todo handle recursive aliases - # @sg-ignore flow based typing needs to understand case when class pattern - build_type(type.name, type.args) - when RBS::Types::Interface - # represents a mix-in module which can be considered a - # subtype of a consumer of it - # @sg-ignore flow based typing needs to understand case when class pattern - build_type(type.name, type.args) - when RBS::Types::ClassSingleton - # e.g., singleton(String) - # @sg-ignore flow based typing needs to understand case when class pattern - build_type(type.name) + # This method will convert type aliases to concrete types. + # + # @param type [RBS::MethodType] + # @return [ComplexType] + def extract_method_type_return_type type, implicit_nil + tag = RbsTranslator.to_complex_type(type.type.return_type) + return ComplexType.parse("#{tag}, nil") if tag && implicit_nil + tag + end + + # @param type_name [RBS::TypeName] + # @param type_args [Enumerable] + # @return [ComplexType::UniqueType] + def build_type(type_name, type_args = []) + base = RBS_TO_YARD_TYPE[type_name.relative!.to_s] || type_name.relative!.to_s + params = type_args.map { |arg| RbsTranslator.to_complex_type(arg).force_rooted } + if base == 'Hash' && params.length == 2 + ComplexType::UniqueType.new(base, [params.first], [params.last], rooted: true, parameters_type: :hash) else - # RBS doesn't provide a common base class for its type AST nodes - # - # @sg-ignore all types should include location - Solargraph.logger.warn "Unrecognized RBS type: #{type.class} at #{type.location}" - ComplexType::UNDEFINED + ComplexType::UniqueType.new(base, [], params.reject(&:undefined?), rooted: true, parameters_type: :list) end end @@ -937,9 +895,9 @@ def add_mixins decl, namespace # @todo are we handling prepend correctly? klass = mixin.is_a?(RBS::AST::Members::Include) ? Pin::Reference::Include : Pin::Reference::Extend type = build_type(mixin.name, mixin.args) - generic_values = type.all_params.map(&:rooted_tags) + generic_values = type.all_params.map(&:to_s) pins.push klass.new( - name: type.rooted_name, # reference pins use rooted names + name: mixin.name.relative!.to_s, type_location: location_decl_to_pin_location(mixin.location), generic_values: generic_values, closure: namespace, diff --git a/lib/solargraph/rbs_translator.rb b/lib/solargraph/rbs_translator.rb new file mode 100644 index 000000000..a070de1e1 --- /dev/null +++ b/lib/solargraph/rbs_translator.rb @@ -0,0 +1,206 @@ +# frozen_string_literal: true + +module Solargraph + # Convert RBS types to complex types and pins. + # + module RbsTranslator + RBS_TO_YARD_TYPE = { + 'bool' => 'Boolean', + 'string' => 'String', + 'int' => 'Integer', + 'untyped' => '', + 'NilClass' => 'nil' + } + + # @param type [RBS::Types::Bases::Base] + # @return [ComplexType] + def self.to_complex_type(type) + tag = type_to_tag(type) + ComplexType.try_parse(tag).force_rooted + end + + # @param param_type [RBS::Types::Function::Param] + # @param name [String] + # @param decl [Symbol] + # @param closure [Pin::Closure] + # @return [Pin::Parameter] + def self.to_parameter_pin(param_type, name, decl, closure) + return_type = if decl == :restarg + ComplexType.parse('Array') + elsif decl == :kwrestarg + ComplexType.parse('Hash{Symbol => Object}') + else + RbsTranslator.to_complex_type(param_type.type) + end + Solargraph::Pin::Parameter.new(decl: decl, name: name, closure: closure, return_type: return_type, source: :rbs, type_location: to_sg_location(param_type.location) || closure.type_location) + end + + # @param method_type [RBS::MethodType] + # @param closure [Pin::Closure] + # @param parameter_names [Array] + # @return [Array] + def self.to_parameter_pins method_type, closure, parameter_names = [] + if defined?(RBS::Types::UntypedFunction) && method_type.type.is_a?(RBS::Types::UntypedFunction) + return [ + Solargraph::Pin::Parameter.new(decl: :restarg, name: 'arg', closure: closure, source: :rbs) + ] + end + + arg_num = 0 + params = [] + method_type.type.required_positionals.each do |param| + params.push RbsTranslator.to_parameter_pin(param, param.name&.to_s || parameter_names[arg_num] || "arg_#{arg_num}", :arg, closure) + arg_num += 1 + end + method_type.type.optional_positionals.each do |param| + params.push RbsTranslator.to_parameter_pin(param, param.name&.to_s || parameter_names[arg_num] || "arg_#{arg_num}", :optarg, closure) + arg_num += 1 + end + if method_type.type.rest_positionals + params.push RbsTranslator.to_parameter_pin(method_type.type.rest_positionals, method_type.type.rest_positionals.name&.to_s || parameter_names[arg_num] || "arg_#{arg_num}", :restarg, closure) + arg_num += 1 + end + method_type.type.required_keywords.each do |param| + params.push RbsTranslator.to_parameter_pin(param.last, param.first.to_s, :kwarg, closure) + arg_num += 1 + end + method_type.type.optional_keywords.each do |param| + params.push RbsTranslator.to_parameter_pin(param.last, param.first.to_s, :kwoptarg, closure) + arg_num += 1 + end + if method_type.type.rest_keywords + params.push RbsTranslator.to_parameter_pin(method_type.type.rest_keywords, method_type.type.rest_keywords.name&.to_s || parameter_names[arg_num] || "arg_#{arg_num}", :kwrestarg, closure) + end + params + end + + # @param method_type [RBS::MethodType] + # @param closure [Pin::Closure] + # @param parameter_names [Array] + # @return [Pin::Signature] + def self.to_signature method_type, closure, parameter_names = [] + # There may be edge cases here around different signatures + # having different type params / orders - we may need to match + # this data model and have generics live in signatures to + # handle those correctly + generics = method_type.type_params.map(&:name).map(&:to_s).uniq + parameters = to_parameter_pins(method_type, closure, parameter_names) + return_type = to_complex_type(method_type.type.return_type) + block = if method_type.block + block_parameters = to_parameter_pins(method_type.block, closure) + block_return_type = to_complex_type(method_type.block.type.return_type) + Pin::Signature.new(generics: generics, parameters: block_parameters, return_type: block_return_type, source: :rbs, type_location: closure.location, closure: closure) + end + Pin::Signature.new(generics: generics, parameters: parameters, return_type: return_type, block: block, source: :rbs, type_location: closure.location, closure: closure) + end + + # @param type_name [RBS::TypeName] + # @param type_args [Enumerable] + # @return [ComplexType::UniqueType] + def self.build_unique_type(type_name, type_args = []) + base = RBS_TO_YARD_TYPE[type_name.relative!.to_s] || type_name.relative!.to_s + params = type_args.map do |a| + RbsTranslator.to_complex_type(a) + end + if base == 'Hash' && params.length == 2 + ComplexType::UniqueType.new(base, [params.first], [params.last], rooted: true, parameters_type: :hash) + else + ComplexType::UniqueType.new(base, [], params.reject(&:undefined?), rooted: true, parameters_type: :list) + end + end + + # @param location [RBS::Location, nil] + # @return [Solargraph::Location, nil] + def self.to_sg_location(location) + return nil if location&.name.nil? + + start_pos = Position.new(location.start_line - 1, location.start_column) + end_pos = Position.new(location.end_line - 1, location.end_column) + range = Range.new(start_pos, end_pos) + Location.new(location.name.to_s, range) + end + + class << self + private + + # @param type [RBS::Types::Bases::Base] + # @return [String] + def type_to_tag type + case type + when RBS::Types::Optional + "#{type_to_tag(type.type)}, nil" + when RBS::Types::Bases::Bool + 'Boolean' + when RBS::Types::Tuple + "Array(#{type.types.map { |t| type_to_tag(t) }.join(', ')})" + when RBS::Types::Literal + type.literal.inspect + when RBS::Types::Union + type.types.map { |t| type_to_tag(t) }.join(', ') + when RBS::Types::Record + # @todo Better record support + 'Hash' + when RBS::Types::Bases::Nil + 'nil' + when RBS::Types::Bases::Void + 'void' + when RBS::Types::Variable + "#{Solargraph::ComplexType::GENERIC_TAG_NAME}<#{type.name}>" + when RBS::Types::Bases::Self, RBS::Types::Bases::Instance + 'self' + when RBS::Types::Bases::Top + # `Top` is the most super superclass + 'BasicObject' + when RBS::Types::Intersection + type.types.map { |member| type_to_tag(member) }.join(', ') + when RBS::Types::Proc + 'Proc' + when RBS::Types::ClassInstance, RBS::Types::Alias, RBS::Types::Interface + # `Alias` is a top-level type alias, e.g., 'bool' in "type bool = true | false" + # @todo ensure these get resolved after processing all aliases + # @todo handle recursive aliases + # + # `Interface represents a mix-in module which can be considered a + # subtype of a consumer of it + # + type_tag(type.name, type.args) + when RBS::Types::ClassSingleton + # e.g., singleton(String) + type_tag(type.name) + when RBS::Types::Bases::Any, RBS::Types::Bases::Bottom + # `Bottom`` is used in contexts where nothing will ever return + # - e.g., it could be the return type of 'exit()' or 'raise' + # @todo define a specific bottom type and use it to + # determine dead code + # + 'undefined' + else + Solargraph.logger.warn "Unrecognized RBS type: #{type.class} at #{type.location}" + 'undefined' + end + end + + # @param type_name [RBS::TypeName] + # @param type_args [Enumerable] + # @return [String] + def type_tag(type_name, type_args = []) + build_type(type_name, type_args).tags + end + + # @param type_name [RBS::TypeName] + # @param type_args [Enumerable] + # @return [ComplexType::UniqueType] + def build_type(type_name, type_args = []) + base = RBS_TO_YARD_TYPE[type_name.relative!.to_s] || type_name.relative!.to_s + params = type_args.map { |a| type_to_tag(a) }.map do |t| + ComplexType.try_parse(t) + end + if base == 'Hash' && params.length == 2 + ComplexType::UniqueType.new(base, [params.first], [params.last], rooted: true, parameters_type: :hash) + else + ComplexType::UniqueType.new(base, [], params.reject(&:undefined?), rooted: true, parameters_type: :list) + end + end + end + end +end diff --git a/lib/solargraph/shell.rb b/lib/solargraph/shell.rb index b4ead7884..f09c0efeb 100755 --- a/lib/solargraph/shell.rb +++ b/lib/solargraph/shell.rb @@ -112,12 +112,10 @@ def cache gem, version = nil PinCache.serialize_yard_gem(gemspec, pins) end - workspace = Solargraph::Workspace.new(Dir.pwd) - rbs_map = RbsMap.from_gemspec(gemspec, workspace.rbs_collection_path, workspace.rbs_collection_config_path) + workspace = Solargraph::Workspace.new(Dir.pwd) if File.exist?('rbs_collection.yaml') + rbs_map = RbsMap.from_gemspec(gemspec, workspace&.rbs_collection_path, workspace&.rbs_collection_config_path) if options[:rebuild] || !PinCache.has_rbs_collection?(gemspec, rbs_map.cache_key) - # cache pins even if result is zero, so we don't retry building pins - pins = rbs_map.pins || [] - PinCache.serialize_rbs_collection_gem(gemspec, rbs_map.cache_key, pins) + PinCache.serialize_rbs_collection_gem(gemspec, rbs_map.cache_key, rbs_map.pins) end rescue Gem::MissingSpecError warn "Gem '#{gem}' not found" diff --git a/spec/parser/flow_sensitive_typing_spec.rb b/spec/parser/flow_sensitive_typing_spec.rb index cee6afef1..a277d69f9 100644 --- a/spec/parser/flow_sensitive_typing_spec.rb +++ b/spec/parser/flow_sensitive_typing_spec.rb @@ -954,7 +954,7 @@ def a b api_map = Solargraph::ApiMap.new.map(source) clip = api_map.clip_at('test.rb', [6, 10]) - expect(clip.infer.to_s).to eq('String') + expect(clip.infer.to_s).to eq('String, nil') clip = api_map.clip_at('test.rb', [7, 17]) expect(clip.infer.to_s).to eq('nil') diff --git a/spec/parser/node_processor_spec.rb b/spec/parser/node_processor_spec.rb index da7031779..b32371ff1 100644 --- a/spec/parser/node_processor_spec.rb +++ b/spec/parser/node_processor_spec.rb @@ -70,4 +70,13 @@ def some_method; end described_class.deregister(:def, dummy_processor1) described_class.deregister(:def, dummy_processor2) end + + it 'parses RBS parameters for classes' do + map = Solargraph::SourceMap.load_string(%( + class Foo < Array #[String] + end + ), 'test.rb') + + expect(map.pins.last.type.to_s).to eq('Array') + end end diff --git a/spec/pin/method_spec.rb b/spec/pin/method_spec.rb index c109746af..5fe116cd0 100644 --- a/spec/pin/method_spec.rb +++ b/spec/pin/method_spec.rb @@ -368,7 +368,7 @@ def bar api_map.map source pin = api_map.get_path_pins('Foo#bar').first type = pin.probe(api_map) - expect(type.to_s).to eq('String') + expect(type.to_s).to eq('String, nil') end it 'infers from multiple-assignment chains' do @@ -632,4 +632,154 @@ def bar expect(pin.overloads).to be_empty end end + + context 'with inline rbs' do + it 'sets instance return types' do + source = Solargraph::Source.load_string(%( + #: () -> String + def foo; end + )) + api_map = Solargraph::ApiMap.new + api_map.map source + pin = api_map.get_path_pins('#foo').first + expect(pin.return_type.to_s).to eq('String') + end + + it 'sets parameterized instance return types' do + source = Solargraph::Source.load_string(%( + #: () -> Array[String] + def foo; end + )) + api_map = Solargraph::ApiMap.new + api_map.map source + pin = api_map.get_path_pins('#foo').first + expect(pin.return_type.to_s).to eq('Array') + end + + it 'sets YARD conventional return types' do + source = Solargraph::Source.load_string(%( + #: () -> bool + def foo; end + )) + api_map = Solargraph::ApiMap.new + api_map.map source + pin = api_map.get_path_pins('#foo').first + expect(pin.return_type.to_s).to eq('Boolean') + end + + it 'sets required positional parameters' do + source = Solargraph::Source.load_string(%( + #: (String) -> bool + def foo(bar); end + )) + api_map = Solargraph::ApiMap.new + api_map.map source + pin = api_map.get_path_pins('#foo').first + expect(pin.signatures).to be_one + expect(pin.signatures.first.parameters).to be_one + expect(pin.signatures.first.parameters.first.name).to eq('bar') + expect(pin.signatures.first.parameters.first.decl).to eq(:arg) + expect(pin.signatures.first.parameters.first.return_type.to_s).to eq('String') + end + + it 'sets optional positional parameters' do + source = Solargraph::Source.load_string(%( + #: (?String) -> bool + def foo(bar = 'default'); end + )) + api_map = Solargraph::ApiMap.new + api_map.map source + pin = api_map.get_path_pins('#foo').first + expect(pin.signatures).to be_one + expect(pin.signatures.first.parameters).to be_one + expect(pin.signatures.first.parameters.first.name).to eq('bar') + expect(pin.signatures.first.parameters.first.decl).to eq(:optarg) + expect(pin.signatures.first.parameters.first.return_type.to_s).to eq('String') + end + + it 'sets rest positional parameters' do + source = Solargraph::Source.load_string(%( + #: (*bar) -> bool + def foo(*bar); end + )) + api_map = Solargraph::ApiMap.new + api_map.map source + pin = api_map.get_path_pins('#foo').first + expect(pin.signatures).to be_one + expect(pin.signatures.first.parameters).to be_one + expect(pin.signatures.first.parameters.first.name).to eq('bar') + expect(pin.signatures.first.parameters.first.decl).to eq(:restarg) + expect(pin.signatures.first.parameters.first.return_type.to_s).to eq('Array') + end + + it 'sets required keyword parameters' do + source = Solargraph::Source.load_string(%( + #: (bar: String) -> bool + def foo(bar:); end + )) + api_map = Solargraph::ApiMap.new + api_map.map source + pin = api_map.get_path_pins('#foo').first + expect(pin.signatures).to be_one + expect(pin.signatures.first.parameters).to be_one + expect(pin.signatures.first.parameters.first.name).to eq('bar') + expect(pin.signatures.first.parameters.first.decl).to eq(:kwarg) + expect(pin.signatures.first.parameters.first.return_type.to_s).to eq('String') + end + + it 'sets optional keyword parameters' do + source = Solargraph::Source.load_string(%( + #: (?bar: String) -> bool + def foo(bar: 'default'); end + )) + api_map = Solargraph::ApiMap.new + api_map.map source + pin = api_map.get_path_pins('#foo').first + expect(pin.signatures).to be_one + expect(pin.signatures.first.parameters).to be_one + expect(pin.signatures.first.parameters.first.name).to eq('bar') + expect(pin.signatures.first.parameters.first.decl).to eq(:kwoptarg) + expect(pin.signatures.first.parameters.first.return_type.to_s).to eq('String') + end + + it 'sets rest keyword parameters' do + source = Solargraph::Source.load_string(%( + #: (**bar) -> bool + def foo(**bar); end + )) + api_map = Solargraph::ApiMap.new + api_map.map source + pin = api_map.get_path_pins('#foo').first + expect(pin.signatures).to be_one + expect(pin.signatures.first.parameters).to be_one + expect(pin.signatures.first.parameters.first.name).to eq('bar') + expect(pin.signatures.first.parameters.first.decl).to eq(:kwrestarg) + expect(pin.signatures.first.parameters.first.return_type.to_s).to eq('Hash{Symbol => Object}') + end + + it 'sets block parameters' do + source = Solargraph::Source.load_string(%( + #: (String) { (Integer) -> void } -> bool + def foo(bar); end + )) + api_map = Solargraph::ApiMap.new + api_map.map source + pin = api_map.get_path_pins('#foo').first + expect(pin.signatures).to be_one + expect(pin.signatures.first.block.parameters).to be_one + expect(pin.signatures.first.block.parameters.first.return_type.to_s).to eq('Integer') + expect(pin.signatures.first.block.return_type.to_s).to eq('void') + end + + it 'rescues parsing errors' do + source = Solargraph::Source.load_string(%[ + #: (* -> broke + def foo(**bar); end + ]) + api_map = Solargraph::ApiMap.new + api_map.map source + pin = api_map.get_path_pins('#foo').first + expect { pin.signatures }.not_to raise_error + end + end end diff --git a/spec/rbs_map/core_map_spec.rb b/spec/rbs_map/core_map_spec.rb index 79878c572..a3769f70a 100644 --- a/spec/rbs_map/core_map_spec.rb +++ b/spec/rbs_map/core_map_spec.rb @@ -82,7 +82,7 @@ # correctly. It would be better to test RbsMap or RbsMap::Conversions # with an RBS fixture. core_map = described_class.new - pins = core_map.pins.select { |pin| pin.is_a?(Solargraph::Pin::Reference::Include) && pin.name == '::Enumerable' } + pins = core_map.pins.select { |pin| pin.is_a?(Solargraph::Pin::Reference::Include) && pin.name == 'Enumerable' } expect(pins.map(&:closure).map(&:namespace)).to include('Enumerator') end diff --git a/spec/source/chain/call_spec.rb b/spec/source/chain/call_spec.rb index 122cc2ed7..8bab98d97 100644 --- a/spec/source/chain/call_spec.rb +++ b/spec/source/chain/call_spec.rb @@ -445,7 +445,7 @@ def foo(params) foo_pin = api_map.source_map('test.rb').pins.find { |p| p.name == 'foo' } chain = Solargraph::Source::SourceChainer.chain(source, Solargraph::Position.new(4, 8)) type = chain.infer(api_map, foo_pin, api_map.source_map('test.rb').locals) - expect(type.rooted_tags).to eq('::Array, ::Hash{::String => undefined}, ::String, ::Integer') + expect(type.rooted_tags).to eq('::Array, ::Hash{::String => undefined}, ::String, ::Integer, nil') end it 'preserves undefined and underdefined tyypes in resolution' do diff --git a/spec/source_map/clip_spec.rb b/spec/source_map/clip_spec.rb index 67b3085b2..6dadad186 100644 --- a/spec/source_map/clip_spec.rb +++ b/spec/source_map/clip_spec.rb @@ -1129,7 +1129,7 @@ def bar opts = {} clip = map.clip_at('test.rb', Solargraph::Position.new(4, 15)) expect(clip.infer.to_s).to eq('Array, nil') clip = map.clip_at('test.rb', Solargraph::Position.new(5, 15)) - expect(clip.infer.to_s).to eq('String') + expect(clip.infer.to_s).to eq('String, nil') end it 'infers overloads with splats' do @@ -1779,7 +1779,7 @@ def foo; end api_map = Solargraph::ApiMap.new.map(source) clip = api_map.clip_at('test.rb', [5, 6]) type = clip.infer - expect(type.to_s).to eq('Enumerable') + expect(type.to_s).to eq('Enumerable, nil') clip = api_map.clip_at('test.rb', [7, 6]) type = clip.infer expect(type.to_s).to eq('String, nil')