diff --git a/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index 26b42ddb4..7186c4b83 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -759,7 +759,7 @@ def workspace end # @param fq_reference_tag [String] A fully qualified whose method should be pulled in - # @param namespace_pin [Pin::Base] Namespace pin for the rooted_type + # @param namespace_pin [Pin::Base, nil] Namespace pin for the rooted_type # parameter - used to pull generics information # @param type [ComplexType] The type which is having its # methods supplemented from fq_reference_tag @@ -844,7 +844,7 @@ def inner_get_methods rooted_tag, scope, visibility, deep, skip, no_core = false if deep && scope == :instance store.get_prepends(fqns).reverse.each do |im| fqim = store.constants.dereference(im) - result.concat inner_get_methods(fqim, scope, visibility, deep, skip, true) unless fqim.nil? + result.concat inner_get_methods_from_reference(fqim, namespace_pin, rooted_type, scope, visibility, deep, skip, true) unless fqim.nil? end end # Store#get_methods doesn't know about full tags, just @@ -876,7 +876,7 @@ def inner_get_methods rooted_tag, scope, visibility, deep, skip, no_core = false end store.get_extends(fqns).reverse.each do |em| fqem = dereference(em) - result.concat inner_get_methods(fqem, :instance, visibility, deep, skip, true) unless fqem.nil? + result.concat inner_get_methods_from_reference(fqem, namespace_pin, rooted_type, :instance, visibility, deep, skip, true) unless fqem.nil? end rooted_sc_tag = qualify_superclass(rooted_tag) unless rooted_sc_tag.nil? diff --git a/lib/solargraph/complex_type.rb b/lib/solargraph/complex_type.rb index 5b0e984b2..9b200ed7a 100644 --- a/lib/solargraph/complex_type.rb +++ b/lib/solargraph/complex_type.rb @@ -322,7 +322,7 @@ def force_rooted end end - # @param definitions [Pin::Namespace, Pin::Method] + # @param definitions [Pin::Namespace, Pin::Method, nil] # @param context_type [ComplexType] # @return [ComplexType] def resolve_generics definitions, context_type diff --git a/lib/solargraph/complex_type/unique_type.rb b/lib/solargraph/complex_type/unique_type.rb index 4bbdda5b2..92a402d2d 100644 --- a/lib/solargraph/complex_type/unique_type.rb +++ b/lib/solargraph/complex_type/unique_type.rb @@ -439,7 +439,7 @@ def resolve_param_generics_from_context generics_to_resolve, context_type, resol # parameters used in this type, and return a new type if # possible. # - # @param definitions [Pin::Namespace, Pin::Method] The module/class/method which uses generic types + # @param definitions [Pin::Namespace, Pin::Method, nil] The module/class/method which uses generic types # @param context_type [ComplexType] The receiver type # @return [UniqueType, ComplexType] def resolve_generics definitions, context_type diff --git a/lib/solargraph/parser/parser_gem/node_methods.rb b/lib/solargraph/parser/parser_gem/node_methods.rb index 59f2f255c..3bffbea35 100644 --- a/lib/solargraph/parser/parser_gem/node_methods.rb +++ b/lib/solargraph/parser/parser_gem/node_methods.rb @@ -2,6 +2,7 @@ require 'parser' require 'ast' +require 'rbs' # https://github.com/whitequark/parser # rubocop:disable Metrics/ModuleLength @@ -81,6 +82,44 @@ def get_node_end_position node Position.new(node.loc.last_line, node.loc.last_column) end + # A trailing inline RBS type argument list, e.g. the `#[String]` in + # `include Enumerable #[String]`. The inner group recurses so that a + # nested `Hash[String, Integer]` is not cut short at its first `]`. + TRAILING_TYPE_ARGS = /\A\s*\#(?\[(?(?:[^\[\]]++|\g)*)\])/ + + # Type arguments from an inline RBS annotation directly after +node+, + # as in `class Foo < Array #[String]` or `include Enumerable #[String]`. + # RBS requires no space between the hash mark and the bracket; with one, + # this is an ordinary comment. + # + # @param node [Parser::AST::Node] the node the annotation follows + # @param code [String] source the node was parsed from + # @return [Array] + def trailing_rbs_type_args node, code + pos = get_node_end_position(node) + offset = Position.line_char_to_offset(code, pos.line, pos.character) + eol = code.index("\n", offset) || code.length + match = code[offset...eol].to_s.match(TRAILING_TYPE_ARGS) + return [] unless match + + parse_rbs_type_args match[:body].to_s + end + + # Parse an RBS type argument list by wrapping it in a throwaway + # generic, which lets RBS split the arguments and gives each one to + # RbsTranslator for conversion to Solargraph's own type syntax. + # + # @param code [String] + # @return [Array] + def parse_rbs_type_args code + type = RBS::Parser.parse_type("Object[#{code}]") + return [] unless type.is_a?(RBS::Types::ClassInstance) + + type.args.map { |arg| RbsTranslator.to_complex_type(arg).rooted_tags } + rescue RBS::ParsingError + [] + end + # @param node [Parser::AST::Node] # @param signature [String] # 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 7cecd22c0..082cefe1c 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/namespace_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/namespace_node.rb @@ -41,26 +41,17 @@ def process private # Type arguments for a generic superclass in inline RBS syntax, e.g., - # `class Foo < Array #[String]`. Only recognized where RBS defines - # it: directly after the superclass, on the same line, with no space - # between `#` and `[`. Anything else is an ordinary comment. + # `class Foo < Array #[String]`. # # @return [String, nil] def parameters_from_inline_rbs superclass = node.children[1] return unless superclass - source = region.source.code - pos = get_node_end_position(superclass) - offset = Position.line_char_to_offset(source, pos.line, pos.character) - eol = source.index("\n", offset) || source.length - match = source[offset...eol].to_s.match(/\A\s*#\[([^\]]*)\]/) - return unless match + args = trailing_rbs_type_args(superclass, region.source.code) + return if args.empty? - code = match[1].strip - return if code.empty? - - "<#{code}>" + "<#{args.join(', ')}>" end def type_from_node diff --git a/lib/solargraph/parser/parser_gem/node_processors/send_node.rb b/lib/solargraph/parser/parser_gem/node_processors/send_node.rb index a9e60cb65..f963ab389 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/send_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/send_node.rb @@ -138,6 +138,7 @@ def process_include location: get_node_location(i), closure: cp, name: unpack_name(i), + generic_values: mixin_generic_values, source: :parser ) end @@ -153,6 +154,7 @@ def process_prepend location: get_node_location(i), closure: cp, name: unpack_name(i), + generic_values: mixin_generic_values, source: :parser ) end @@ -175,12 +177,28 @@ def process_extend location: loc, closure: region.closure, name: unpack_name(i), + generic_values: mixin_generic_values, source: :parser ) end end end + # Type arguments for a generic module mixed in with inline RBS + # syntax, e.g. `include Enumerable #[String]`. RBS recognizes this + # for one module argument only, so `include A, B` takes none. + # + # @return [Array] + def mixin_generic_values + args = node.children[2..] + return [] unless args && args.length == 1 + + arg = args.first + return [] unless arg.is_a?(AST::Node) + + trailing_rbs_type_args arg, region.source.code + end + # @return [void] def process_require return unless node.children[2].is_a?(AST::Node) && node.children[2].type == :str diff --git a/lib/solargraph/rbs_map/conversions.rb b/lib/solargraph/rbs_map/conversions.rb index ebe7a6ce0..38649f62d 100644 --- a/lib/solargraph/rbs_map/conversions.rb +++ b/lib/solargraph/rbs_map/conversions.rb @@ -164,9 +164,9 @@ def fqns type_name # @return [void] def convert_self_type_to_pins decl, closure type = build_type(decl.name, decl.args) - generic_values = type.all_params.map(&:to_s) + generic_values = type.all_params.map(&:rooted_tags) include_pin = Solargraph::Pin::Reference::Include.new( - name: decl.name.relative!.to_s, + name: type.rooted_name, # reference pins use rooted names type_location: location_decl_to_pin_location(decl.location), generic_values: generic_values, closure: closure, @@ -279,8 +279,7 @@ 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(&:to_s) - superclass_name = decl.super_class.name.to_s + generic_values = type.all_params.map(&:rooted_tags) pins.push Solargraph::Pin::Reference::Superclass.new( type_location: location_decl_to_pin_location(decl.super_class.location), closure: class_pin, @@ -299,7 +298,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: decl.name.relative!.to_s, + name: fqns(decl.name), closure: Solargraph::Pin::ROOT_PIN, comments: decl.comment&.string, generics: type_parameter_names(decl), @@ -318,7 +317,7 @@ def interface_decl_to_pin decl def module_decl_to_pin decl module_pin = Solargraph::Pin::Namespace.new( type: :module, - name: decl.name.relative!.to_s, + name: fqns(decl.name), type_location: location_decl_to_pin_location(decl.location), closure: Solargraph::Pin::ROOT_PIN, comments: decl.comment&.string, @@ -701,9 +700,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(&:to_s) + generic_values = type.all_params.map(&:rooted_tags) pins.push Solargraph::Pin::Reference::Include.new( - name: decl.name.relative!.to_s, + name: type.rooted_name, # reference pins use rooted names type_location: location_decl_to_pin_location(decl.location), generic_values: generic_values, closure: closure, @@ -718,8 +717,9 @@ 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: decl.name.relative!.to_s, + name: type.rooted_name, # reference pins use rooted names type_location: location_decl_to_pin_location(decl.location), + generic_values: generic_values, closure: closure, source: :rbs ) @@ -732,8 +732,9 @@ 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: decl.name.relative!.to_s, + name: type.rooted_name, # reference pins use rooted names type_location: location_decl_to_pin_location(decl.location), + generic_values: generic_values, closure: closure, source: :rbs ) @@ -797,9 +798,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(&:to_s) + generic_values = type.all_params.map(&:rooted_tags) pins.push klass.new( - name: mixin.name.relative!.to_s, + name: type.rooted_name, # reference pins use rooted names type_location: location_decl_to_pin_location(mixin.location), generic_values: generic_values, closure: namespace, diff --git a/spec/parser/node_processor_spec.rb b/spec/parser/node_processor_spec.rb index 54f442ee2..ce338b38a 100644 --- a/spec/parser/node_processor_spec.rb +++ b/spec/parser/node_processor_spec.rb @@ -80,6 +80,15 @@ class Foo < Array #[String] expect(map.pins.last.type.to_s).to eq('Array') end + it 'translates RBS parameter syntax on a superclass into Solargraph syntax' do + map = Solargraph::SourceMap.load_string(%( + class Foo < Array #[Hash[String, Integer]] + end + ), 'test.rb') + + expect(map.pins.last.type.to_s).to eq('Array Integer}>') + end + it 'ignores bracketed comments in the class body' do map = Solargraph::SourceMap.load_string(%( class Foo < Array @@ -98,4 +107,70 @@ class Foo < Array # [String] expect(map.pins.last.type.to_s).to eq('Array') end + + # @param code [String] + # @param klass [Class] + # @return [String] + def mixin_type_for code, klass + map = Solargraph::SourceMap.load_string(code, 'test.rb') + map.pins.find { |pin| pin.instance_of?(klass) }.type.to_s + end + + it 'parses RBS parameters for included modules' do + expect(mixin_type_for(%( + class Foo + include Bar #[String] + end + ), Solargraph::Pin::Reference::Include)).to eq('Bar') + end + + it 'parses RBS parameters for prepended modules' do + expect(mixin_type_for(%( + class Foo + prepend Bar #[String] + end + ), Solargraph::Pin::Reference::Prepend)).to eq('Bar') + end + + it 'parses RBS parameters for extended modules' do + expect(mixin_type_for(%( + class Foo + extend Bar #[String] + end + ), Solargraph::Pin::Reference::Extend)).to eq('Bar') + end + + it 'translates RBS parameter syntax on mixins into Solargraph syntax' do + expect(mixin_type_for(%( + class Foo + include Bar #[Hash[String, Integer]] + end + ), Solargraph::Pin::Reference::Include)).to eq('Bar Integer}>') + end + + it 'ignores a bracketed mixin comment separated from the hash' do + expect(mixin_type_for(%( + class Foo + include Bar # [String] + end + ), Solargraph::Pin::Reference::Include)).to eq('Bar') + end + + it 'ignores mixin parameters when one call mixes in several modules' do + # RBS rejects this too, as "Mixing multiple modules with one call is + # not supported", since the parameters could apply to either module. + expect(mixin_type_for(%( + class Foo + include Bar, Baz #[String] + end + ), Solargraph::Pin::Reference::Include)).to eq('Bar') + end + + it 'ignores mixin parameters that RBS cannot parse as a type' do + expect(mixin_type_for(%( + class Foo + include Bar #[def] + end + ), Solargraph::Pin::Reference::Include)).to eq('Bar') + end end diff --git a/spec/rbs_map/conversions_spec.rb b/spec/rbs_map/conversions_spec.rb index 50f4b0b1a..aad3f8d08 100644 --- a/spec/rbs_map/conversions_spec.rb +++ b/spec/rbs_map/conversions_spec.rb @@ -27,6 +27,41 @@ attr_reader :temp_dir + context 'with a generic module prepended and extended' do + let(:rbs) do + <<~RBS + module Wrapper[T] + end + + class Holder + prepend Wrapper[String] + extend Wrapper[Integer] + end + RBS + end + + # @param klass [Class] + # @return [Array>] + def generic_values_for klass + conversions.pins + .select { |pin| pin.instance_of?(klass) && pin.name == '::Wrapper' } + .map(&:generic_values) + .uniq + end + + it 'carries the prepended type argument' do + expect(generic_values_for(Solargraph::Pin::Reference::Prepend)).to eq([['::String']]) + end + + it 'carries the extended type argument' do + expect(generic_values_for(Solargraph::Pin::Reference::Extend)).to include(['::Integer']) + end + + it 'leaves no extend reference pin without its type argument' do + expect(generic_values_for(Solargraph::Pin::Reference::Extend)).not_to include([]) + end + end + context 'with overlapping module hierarchies and inheritance' do subject(:method_pin) { api_map.get_method_stack('A::B::C', 'foo').first } @@ -93,6 +128,51 @@ def bar: () -> untyped expect(method_pin.return_type.tag).to eq('undefined') end end + + context 'with generic modules mixed in' do + let(:rbs) do + <<~RBS + module Prependable[T] + def value: () -> T + end + module Extendable[T] + def build: () -> T + end + module Includable[T] + def fetch: () -> T + end + class Prepender + prepend Prependable[Integer] + end + class Extender + extend Extendable[String] + end + class Includer + include Includable[Symbol] + end + RBS + end + + # @param namespace [String] + # @param method [String] + # @param scope [Symbol] + # @return [String, nil] + def inferred_type namespace, method, scope + api_map.get_method_stack(namespace, method, scope: scope).first&.return_type&.tag + end + + it 'substitutes the type argument of a prepended module' do + expect(inferred_type('Prepender', 'value', :instance)).to eq('Integer') + end + + it 'substitutes the type argument of an extended module' do + expect(inferred_type('Extender', 'build', :class)).to eq('String') + end + + it 'substitutes the type argument of an included module' do + expect(inferred_type('Includer', 'fetch', :instance)).to eq('Symbol') + end + end end context 'with standard loads for solargraph project' do diff --git a/spec/rbs_map/core_map_spec.rb b/spec/rbs_map/core_map_spec.rb index 94cd8395b..6f1c48bec 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_map/clip_spec.rb b/spec/source_map/clip_spec.rb index 468612ab1..7c2ed1912 100644 --- a/spec/source_map/clip_spec.rb +++ b/spec/source_map/clip_spec.rb @@ -1917,6 +1917,25 @@ def bad_passthrough; yield; end expect(type.to_s).to eq('undefined') end + it 'resolves generics from an inline RBS parameter on a prepended module' do + source = Solargraph::Source.load_string(%( + # @generic T + module Mixin + # @return [generic] + def value; end + end + class Foo + prepend Mixin #[Integer] + end + a = Foo.new.value + a + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + clip = api_map.clip_at('test.rb', [10, 6]) + type = clip.infer + expect(type.to_s).to eq('Integer') + end + it 'uses simple return value of block to infer return value of Enumerable#map' do source = Solargraph::Source.load_string(%( a = ['a'].map { 123 }