diff --git a/lib/solargraph.rb b/lib/solargraph.rb index 75c454dde..75554994c 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 :Typedef, 'solargraph/typedef' dir = File.dirname(__FILE__) VIEWS_PATH = File.join(dir, 'solargraph', 'views') diff --git a/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index 58cdba509..0c0464ad2 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -403,7 +403,7 @@ def get_instance_variable_pins namespace, scope = :instance # @param candidates [Array] # @param name [String] # @param closure [Pin::Closure] - # @param location [Location] + # @param location [Location, nil] # # @return [Pin::BaseVariable, nil] def var_at_location candidates, name, closure, location @@ -520,6 +520,27 @@ def get_methods rooted_tag, scope: :instance, visibility: [:public], deep: true result end + # @param path [Typedef::Path] + # @return [Array] + def typedef_path_methods path + # @todo Can we just try to resolve the path here? I guess we'd need gates. + if path.resolved? + get_methods(path.to_s) + else + [] + end + end + + # @param type [Typedef::Type] + # @return [Array] + def typedef_type_methods type + if type.class? + get_methods(type.params.first.to_s, scope: :class) + else + get_methods(type.base.to_s, scope: :instance) + end + end + # Get an array of method pins for a complex type. # # The type's namespace and the context should be fully qualified. If the diff --git a/lib/solargraph/complex_type.rb b/lib/solargraph/complex_type.rb index 27d2ff08c..7b2ba9409 100644 --- a/lib/solargraph/complex_type.rb +++ b/lib/solargraph/complex_type.rb @@ -399,6 +399,16 @@ def intersect_with intersection_type, api_map ComplexType.new(types) end + # @return [Array] + def to_typedef_types + # @todo Quick and dirty hack + return [Typedef::Type::ROOT] if to_s == 'Class<>' + + items.map do |item| + item.to_typedef_types + end + end + protected def equality_fields diff --git a/lib/solargraph/complex_type/unique_type.rb b/lib/solargraph/complex_type/unique_type.rb index 4bbdda5b2..ea6c8aeee 100644 --- a/lib/solargraph/complex_type/unique_type.rb +++ b/lib/solargraph/complex_type/unique_type.rb @@ -614,6 +614,16 @@ def can_root_name? name_to_check = name self.class.can_root_name?(name_to_check) end + def to_typedef_types + # @todo Quick and dirty hack + return Typedef::Type.new(Typedef.tokenize(to_s)) if to_s.start_with?('generic<') + + base = name + base = "::#{base}" if rooted? && base =~ /^[A-Z]/ + params = all_params.map(&:to_typedef_types) + Typedef::Type.new(base, *params) + end + # @param name [String] def self.can_root_name? name # name is not lowercase diff --git a/lib/solargraph/pin/base.rb b/lib/solargraph/pin/base.rb index 50803c881..ff289f55a 100644 --- a/lib/solargraph/pin/base.rb +++ b/lib/solargraph/pin/base.rb @@ -33,6 +33,8 @@ class Base # Between 2 pins, the one with the higher priority gets chosen. If the priorities are equal, they are combined. attr_reader :combine_priority + attr_reader :expansions + def presence_certain? true end @@ -47,7 +49,7 @@ def presence_certain? # @param directives [::Array, nil] # @param combine_priority [::Numeric, nil] See attr_reader for combine_priority def initialize location: nil, type_location: nil, closure: nil, source: nil, name: '', comments: '', - docstring: nil, directives: nil, combine_priority: nil + docstring: nil, directives: nil, combine_priority: nil, expansions: {} @location = location @type_location = type_location @closure = closure @@ -60,11 +62,26 @@ def initialize location: nil, type_location: nil, closure: nil, source: nil, nam @combine_priority = combine_priority # @type [ComplexType, ComplexType::UniqueType, nil] @binder = nil + @expansions = expansions assert_source_provided assert_location_provided end + def typedef_path + Typedef::Path.new(path.to_s, rooted: return_type.rooted?) + end + + # @return [Array] + def typedef_return_types + [Typedef::Type.from_complex_type(return_type)].flatten + end + + + def expansions + @expansions ||= {} + end + # @return [void] def assert_location_provided return unless best_location.nil? && %i[yardoc source rbs].include?(source) @@ -711,6 +728,10 @@ def all_location_text end end + def typedef_generics + @generics ||= docstring.tags(:generic).map(&:name) + end + protected # @sg-ignore def should infer as symbol - "Not enough arguments to Module#protected" diff --git a/lib/solargraph/pin/callable.rb b/lib/solargraph/pin/callable.rb index ed87b79e4..49e3c67d9 100644 --- a/lib/solargraph/pin/callable.rb +++ b/lib/solargraph/pin/callable.rb @@ -160,6 +160,24 @@ def resolve_generics_from_context generics_to_resolve, callable end + # @param api_map [ApiMap] + # @return [Array] + def typedef_resolve_rooted api_map + typedef_return_types.map { |rt| rt.resolve_rooted(api_map, gates) } + end + + # @param arguments [Array] + # @return [Array] + def typedef_resolve_call(arguments) + # named_values = closure.generics + # .map { |name| "generic[#{name}]" } + # .zip(arguments) + # .to_h + # puts "Named values from typedef_resolve_call #{named_values.inspect}" + # puts "With arguments #{arguments.inspect}" + typedef_return_types + end + def typify api_map type = return_type return type.qualify(api_map, *gates) if type.defined? diff --git a/lib/solargraph/pin/parameter.rb b/lib/solargraph/pin/parameter.rb index ba20976ec..9be0b727a 100644 --- a/lib/solargraph/pin/parameter.rb +++ b/lib/solargraph/pin/parameter.rb @@ -22,6 +22,11 @@ def initialize decl: :arg, asgn_code: nil, **splat @decl = decl end + # @param arguments [Array] + def typedef_resolve_generics(arguments) + + end + def type_location super || closure&.type_location end diff --git a/lib/solargraph/pin/proxy_type.rb b/lib/solargraph/pin/proxy_type.rb index fd37bab85..90bcf6b08 100644 --- a/lib/solargraph/pin/proxy_type.rb +++ b/lib/solargraph/pin/proxy_type.rb @@ -3,16 +3,19 @@ module Solargraph module Pin class ProxyType < Base + attr_reader :generics + # @param return_type [ComplexType, ComplexType::UniqueType] # @param gates [Array, nil] Namespaces to try while resolving non-rooted types # @param binder [ComplexType, ComplexType::UniqueType, nil] # @param gates [Array, nil] # @param [Hash{Symbol => Object}] splat - def initialize return_type: ComplexType::UNDEFINED, binder: nil, gates: nil, **splat + def initialize return_type: ComplexType::UNDEFINED, binder: nil, gates: nil, generics: [], **splat super(**splat) @gates = gates @return_type = return_type @binder = binder if binder + @generics = generics end def context diff --git a/lib/solargraph/rbs_map/core_fills.rb b/lib/solargraph/rbs_map/core_fills.rb index 0116b15eb..34cd30c1d 100644 --- a/lib/solargraph/rbs_map/core_fills.rb +++ b/lib/solargraph/rbs_map/core_fills.rb @@ -38,7 +38,8 @@ module CoreFills source: :core_fill), # RBS does not define Class with a generic, so all calls to # generic() return an 'untyped'. We can do better: - Override.method_return('Class#allocate', 'self', source: :core_fill) + Override.method_return('Class#allocate', 'self', source: :core_fill), + Override.method_return('Class#new', 'self', source: :core_fill) ].freeze # @todo I don't see any direct link in RBS to build this from - diff --git a/lib/solargraph/typedef.rb b/lib/solargraph/typedef.rb new file mode 100644 index 000000000..8ae7f28a1 --- /dev/null +++ b/lib/solargraph/typedef.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +module Solargraph + module Typedef + autoload :Path, 'solargraph/typedef/path' + autoload :Token, 'solargraph/typedef/token' + autoload :Type, 'solargraph/typedef/type' + autoload :Linker, 'solargraph/typedef/linker' + autoload :Memos, 'solargraph/typedef/memos' + autoload :Dictionary, 'solargraph/typedef/dictionary' + autoload :Generics, 'solargraph/typedef/generics' + autoload :Typeset, 'solargraph/typedef/typeset' + + # Convert a value to a Path or Token + # @param value [String, Path, Token, Type, Array] + # @return [Path, Token, Type] + def self.tokenize value + case value + when String + convert value + when Path, Token, Type + value + when Array + Typedef::Type.new(*value) + else + raise "Invalid value #{value.inspect}" + end + end + + def self.memos + @memos ||= Memos.new + end + + class << self + private + + # @param string [String] + # @return [Path, Token] + def convert string + case string + when "" + Path::ROOT + # @todo Should interfaces (e.g, `_Each`) be paths? + # (Probably) + when /^(::)?[A-Z_][A-Za-z_(::)]*?/ + Path.new(string) + when /^generic<[A-Za-z\d_]*>$/ + Token.new(string) + when /^?[a-z\d_]*?$/ + Token.new(string) + when /^"?[a-z\d_]*?"$/ + Token.new(string) + when /^\:?[a-z\d_]*?$/ + Token.new(string) + # @todo How to handle integers? + when /^\d+$/ + Token.new(string) + else + raise "Invalid Typedef token string: #{string.inspect}" + end + end + end + end +end diff --git a/lib/solargraph/typedef/dictionary.rb b/lib/solargraph/typedef/dictionary.rb new file mode 100644 index 000000000..9a5b68bca --- /dev/null +++ b/lib/solargraph/typedef/dictionary.rb @@ -0,0 +1,169 @@ +# frozen_string_literal: true + +module Solargraph + module Typedef + # Type expansion and resolution utilities. + # + class Dictionary + include Linker + + attr_reader :api_map + + attr_reader :source_map + + attr_reader :position + + # @param api_map [ApiMap] + # @param source_map [SourceMap, String] A SourceMap object or filename + # @param position [Position, Array(Integer, Integer), nil] + def initialize api_map, source_map, position, chain: nil, closure: nil + @api_map = api_map + @source_map = source_map.is_a?(SourceMap) ? source_map : api_map.source_map(source_map) + @position = Solargraph::Position.normalize(position) if position + @chain = chain + @closure = closure + end + + def chain + @chain ||= Solargraph::Source::SourceChainer.chain(source_map.source, position) + end + + def closure + @closure ||= source_map.locate_closure_pin(position.line, position.character) + end + + def location + @location ||= Location.new(source_map.filename, Range.new(position, position)) + end + + def locals + @locals ||= source_map.locals_at(location) + end + + # @return [Array] + def define + pins, _receiver = define_from chain + pins + end + + # @return [Array] + def infer + Typedef.memos.fetch memo_key(:infer), [] do + pins, receiver = define_from chain + proxies = infer_proxies(pins, receiver) + # @todo Smelly uniqueness + proxies.flat_map(&:typedef_return_types).uniq(&:to_s) + end + end + + # @param [Source::Chain] + # @return [Array(Array, Pin::Closure)] + def define_from chain + Typedef.memos.fetch memo_key(:define), [[], nil] do + next [[closure], closure.closure] if chain.undefined? + + pins = [] + current_closure = closure + last_link = chain.links.last + chain.links.each do |link| + pins = hitch(link, current_closure).map { |pin| expand_generics(pin, current_closure) } + next pins, current_closure if link == last_link + + proxies = infer_proxies(pins, current_closure) + break [[], current_closure] if proxies.empty? + current_closure = proxies.first + break [[], nil] unless current_closure + end + [pins, current_closure] + end + end + + # @param pins [Array] + # @param receiver [Pin::Closure, nil] + # @return [Array] + def infer_proxies pins, receiver + return pins unless receiver # @todo Why is this necessary? + + pins.flat_map { |pin| pin.is_a?(Pin::Method) ? find_matching_signature(pin) : pin } + .map { |pin| root_and_infer(pin, receiver) } + .map { |pin| expand_generics(pin, receiver) } + # .map { |pin| root_and_infer(pin, receiver) } + end + + # @param pin [Pin::Base] + # @param receiver [Pin::Closure] + # @return [Pin::Base] + def expand_generics pin, receiver + types = Generics.expand(api_map, pin, receiver) + pin.proxy(ComplexType.new(types.map(&:to_complex_type))) + end + + # @param pin [Pin::Base] + # @param receiver [Pin::Closure] + # @return [Pin::ProxyType] + def root_and_infer pin, receiver + inferred = pin.typedef_return_types.flat_map do |type| + rooted = type.resolve_rooted(api_map, receiver&.closure&.gates || ['']) + if rooted.base.to_s == 'undefined' # @todo Better way to identify undefined + infer_by_pin_type pin, receiver + else + rooted + end + end + Pin::ProxyType.anonymous(ComplexType.new(inferred.map(&:to_complex_type))) + end + + def infer_by_pin_type pin, receiver + case pin + when Pin::BaseVariable, Pin::Constant + chain = Solargraph::Parser::ParserGem::NodeChainer.chain(pin.assignment) + Dictionary.new(api_map, pin.filename, pin.location.range.start, chain: chain).infer + else + next_chain = next_chain(pin) + return pin unless next_chain + Dictionary.new(api_map, pin.filename, Range.from_node(next_chain.node).start, chain: next_chain).infer + end + end + + # @param pin [Pin::Base] + # @return [Source::Chain, nil] + def next_chain(pin) + return unless pin.location + + if pin.location.range.start != position + return Parser::ParserGem::NodeChainer.chain(source_map.source.node_at(pin.location.range.start.line, pin.location.range.start.column)) + elsif pin.is_a?(Pin::Method) + node = method_body_node(pin) + Parser::ParserGem::NodeChainer.chain(node) if node + elsif pin.is_a?(Pin::BaseVariable) + Parser::ParserGem::NodeChainer.chain(pin.assignment) + else + nil + end + end + + def method_body_node(pin) + node = source_map.source.node_at(pin.location.range.start.line, pin.location.range.start.column) + return unless node + return node.children[1].children.last if node.type == :DEFN + return node.children[2].children.last if node.type == :DEFS + return node.children[2] if %i[def DEFS].include?(node.type) + return node.children[3] if node.type == :defs + end + + # @todo Either implement this or (more likely) handle it in Linker::Call + # @param pin [Pin::Method] + # @return [Pin::Signature, Pin::Method] + def find_matching_signature(pin) + pin.signatures.find do |sig| + # puts "Check against #{chain.inspect}" + false + end || pin + end + + def memo_key(action) + [source_map.filename, [api_map, position, chain, action]] + end + end + end +end diff --git a/lib/solargraph/typedef/generics.rb b/lib/solargraph/typedef/generics.rb new file mode 100644 index 000000000..45d7c8400 --- /dev/null +++ b/lib/solargraph/typedef/generics.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +module Solargraph + module Typedef + # Contextual expansion of generic tokens + # + class Generics + attr_reader :api_map + + attr_reader :pin + + attr_reader :receiver + + def initialize api_map, pin, receiver + @api_map = api_map + @pin = pin + @receiver = receiver + end + + def expand + pin.typedef_return_types + .map { |type| type.expand zip_pin_generic_values } + .map { |type| type.expand zip_receiver_generic_values } + end + + def names + generics_from(pin).concat(generics_from(receiver)) + end + + def self.expand api_map, pin, receiver + new(api_map, pin, receiver).expand + end + + private + + # @param pin [Pin::Base] + def generics_from pin + names = [] + cursor = pin + while cursor + names.concat cursor.typedef_generics + cursor = cursor.closure + end + names + end + + def expand_generic_types + types = pin.typedef_return_types + .map { |type| type.expand zip_pin_generic_values } + .map { |type| type.expand zip_receiver_generic_values } + pin.proxy(ComplexType.new(types.map(&:to_complex_type))) + end + + def zip_pin_generic_values + # @todo Figure this out. See spec/typedef/call_spec.rb:464 + # ('sends proper gates in ProxyType') + return {} + generic_names = pin.docstring.tags(:generic).map(&:name).map { |name| "generic<#{name}>"} + type = unless generic_names.empty? + pin.closure.typedef_return_types.find { |type| type.params.first.to_s == pin.binder.namespace && type.params.length == generic_names.length } + end + named_values = if type + generic_names.zip(type.params).to_h + else + {} + end + named_values.merge({'self' => receiver.binder.namespace}) + end + + def zip_receiver_generic_values + namespaces = api_map.get_path_pins(receiver.namespace).select { |pin| pin.is_a?(Pin::Namespace) } + generic_names = namespaces.flat_map(&:generics).map { |name| "generic<#{name}>"} + + type = unless generic_names.empty? + receiver.typedef_return_types.find { |type| type.base.to_s == receiver.namespace && type.params.length == generic_names.length } + end + + named_values = if type + generic_names.zip(type.params).to_h + else + {} + end + named_values.merge({'self' => receiver.binder.namespace}) + end + end + end +end diff --git a/lib/solargraph/typedef/linker.rb b/lib/solargraph/typedef/linker.rb new file mode 100644 index 000000000..aa3190195 --- /dev/null +++ b/lib/solargraph/typedef/linker.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module Solargraph + module Typedef + module Linker + autoload :Base, 'solargraph/typedef/linker/base' + autoload :Call, 'solargraph/typedef/linker/call' + autoload :ClassVariable, 'solargraph/typedef/linker/class_variable' + autoload :Or, 'solargraph/typedef/linker/or' + + def hitch link, closure + case link + when Solargraph::Source::Chain::Head + return [Pin::ProxyType.anonymous(closure.binder, source: :chain)] if link.word == 'self' + [] + when Solargraph::Source::Chain::Call + Call.resolve(self, link, closure) + when Solargraph::Source::Chain::Constant + return [Pin::ROOT_PIN] if link.word.empty? + if link.word.start_with?('::') + base = link.word[2..] + gates = [''] + else + base = link.word + gates = closure.gates + end + # @sg-ignore Need to add nil check here + fqns = api_map.resolve(base, gates) + # @sg-ignore Need to add nil check here + api_map.get_path_pins(fqns) + when Source::Chain::InstanceVariable + ivars = api_map.get_instance_variable_pins(closure.context.namespace, closure.context.scope).select do |p| + p.name == link.word + end + out = api_map.var_at_location(ivars, link.word, closure, location) + [out].compact + when Source::Chain::Literal + type_name = link.word.sub(/^$/, '') + complex_type = ComplexType.parse(type_name) + [Pin::ProxyType.anonymous(complex_type, source: :chain)] + when Source::Chain::Or + Or.resolve(self, link, closure) + when Source::Chain::ClassVariable + ClassVariable.resolve(self, link, closure) + else + raise "#{link.class} not implemented" + end + end + end + end +end diff --git a/lib/solargraph/typedef/linker/base.rb b/lib/solargraph/typedef/linker/base.rb new file mode 100644 index 000000000..d009a2488 --- /dev/null +++ b/lib/solargraph/typedef/linker/base.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module Solargraph + module Typedef + module Linker + class Base + # @return [Dictionary] + attr_reader :dictionary + + # @return [Source::Chain::Link] + attr_reader :link + + # @return [Pin::Closure] + attr_reader :closure + + # @param dictionary [Dictionary] + # @param link [Source::Chain::Link] + # @param closure [Pin::Closure] + def initialize(dictionary, link, closure) + @dictionary = dictionary + @link = link + @closure = closure + end + + def api_map + dictionary.api_map + end + + def source_map + dictionary.source_map + end + + # @return [Array] + def resolve + raise 'Not implemented' + end + + def self.resolve(dictionary, link, closure) + new(dictionary, link, closure).resolve + end + end + end + end +end diff --git a/lib/solargraph/typedef/linker/call.rb b/lib/solargraph/typedef/linker/call.rb new file mode 100644 index 000000000..b95e2f7f9 --- /dev/null +++ b/lib/solargraph/typedef/linker/call.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +module Solargraph + module Typedef + module Linker + # @!method link + # return [Source::Chain::Call] + class Call < Base + # @todo Candidate for deprecation + include Solargraph::Parser::NodeMethods + + def resolve + local_variable || method_call + end + + private + + def local_variable + found = if link.head? + source_map.locals_at(dictionary.location) + .reverse + .find { |pin| pin.name == link.word } + end + return [found] if found + end + + def method_call + types = closure.typedef_return_types + # .map { |type| type.resolve_rooted(dictionary.api_map, [closure.context.namespace]) } + # @todo Quick and dirty hack to force UniqueType to ComplexType + pins = ComplexType.new([closure.binder]).to_typedef_types + .flat_map { |type| dictionary.api_map.typedef_type_methods(type) } + .select { |pin| pin.name == link.word } + # .flat_map { |pin| overload(pin) } + return pins unless link.nullable? && closure.typedef_return_types.any?(&:nullable?) + + pins.map { |pin| pin.proxy(ComplexType.new([pin.return_type, ComplexType::NIL])) } + end + + # @param pin [Pin::Method] + def overload(pin) + pin.overloads.find { |overload| overload.arity_matches?(link.arguments, link.arguments )} || pin + end + end + end + end +end diff --git a/lib/solargraph/typedef/linker/class_variable.rb b/lib/solargraph/typedef/linker/class_variable.rb new file mode 100644 index 000000000..c9ad81065 --- /dev/null +++ b/lib/solargraph/typedef/linker/class_variable.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Solargraph + module Typedef + module Linker + class ClassVariable < Base + def resolve + found = api_map.get_class_variable_pins(closure.context.namespace).select { |p| p.name == link.word }.first + return [] unless found + + chain = Solargraph::Parser::ParserGem::NodeChainer.chain(found.assignment) + types = Dictionary.new(api_map, found.filename, found.location.range.start, chain: chain).infer + [found.proxy(ComplexType.new(types.map(&:to_complex_type)))] + end + end + end + end +end diff --git a/lib/solargraph/typedef/linker/or.rb b/lib/solargraph/typedef/linker/or.rb new file mode 100644 index 000000000..d6432497b --- /dev/null +++ b/lib/solargraph/typedef/linker/or.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Solargraph + module Typedef + module Linker + class Or < Base + def resolve + # @todo Don't call define_from here. It causes infinite recursion. + types = link.links.map do |link| + range = Solargraph::Range.from_node(link.node) + Dictionary.new(api_map, dictionary.source_map.filename, range.start, chain: link).infer + end + .flatten.map(&:to_complex_type) + combined_type = Solargraph::ComplexType.new(types) + unless types.flatten.all?(&:nullable?) + # @sg-ignore flow sensitive typing should be able to handle redefinition + combined_type = combined_type.without_nil + end + + [Solargraph::Pin::ProxyType.anonymous(combined_type, source: :chain)] + end + end + end + end +end diff --git a/lib/solargraph/typedef/memos.rb b/lib/solargraph/typedef/memos.rb new file mode 100644 index 000000000..d8313a255 --- /dev/null +++ b/lib/solargraph/typedef/memos.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Solargraph + module Typedef + # @todo Eventually it should be possible to clear memos for specific filenames + # + class Memos + def fetch key, default = nil + return cache[key] if cache.key?(key) + if pending.add?(key) + cache[key] = yield.tap { pending.delete(key) } + else + Solargraph.logger.warn "Recursive definition detected: #{key}" + default + end + ensure + pending.delete key + end + + def clear + cache.clear + end + + def cache + @cache ||= {} + end + + def pending + @processing ||= Set.new + end + end + end +end diff --git a/lib/solargraph/typedef/path.rb b/lib/solargraph/typedef/path.rb new file mode 100644 index 000000000..931535753 --- /dev/null +++ b/lib/solargraph/typedef/path.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +module Solargraph + module Typedef + class Path + attr_reader :name + + def initialize name, rooted: false + @name = name + @rooted = rooted + if name.start_with?('::') + @name = @name[2..] + @rooted = true + end + end + + def expand(named_values) + self + end + + # @param api_map [ApiMap] + # @param gates [Array] + def resolve_rooted(api_map, gates) + return self if rooted? + + new_path = api_map.qualify(name, *gates) + if new_path + # @todo Inefficient but effective + rooted = api_map.get_path_pins(new_path).any? + Path.new(new_path, rooted: rooted) if new_path + else + self + end + end + + def rooted? + @rooted + end + + def resolved? + rooted? + end + + def root? + name.empty? + end + + def expanded? + true + end + + def generic? + false + end + + def from(base) + return self if rooted? + + Path.new("#{base.name}::#{name}", rooted: base.rooted?) + end + + def to_s + name + end + + ROOT = Path.new('', rooted: true) + end + end +end diff --git a/lib/solargraph/typedef/token.rb b/lib/solargraph/typedef/token.rb new file mode 100644 index 000000000..4042c65b5 --- /dev/null +++ b/lib/solargraph/typedef/token.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module Solargraph + module Typedef + class Token + RESERVED_NAMES = %w[nil undefined] + + attr_reader :name + + attr_reader :params + + def initialize name, *params + @name = name + @params = params + end + + def expand(named_values) + return self unless named_values[name] + Typedef.tokenize(named_values[name]) + end + + def resolve_rooted(api_map, gates) + self + end + + def resolved? + RESERVED_NAMES.include?(name) + end + + def expanded? + RESERVED_NAMES.include?(name) + end + + def generic? + name.start_with?('generic<') + end + + def to_s + "#{([name] + params).join(', ')}" + end + end + end +end diff --git a/lib/solargraph/typedef/type.rb b/lib/solargraph/typedef/type.rb new file mode 100644 index 000000000..8cc74fd6c --- /dev/null +++ b/lib/solargraph/typedef/type.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +module Solargraph + module Typedef + class Type + attr_reader :base + + attr_reader :params + + # @param base [String, Path, Token] + # @param params [Array] + def initialize base, *params + @base = Typedef.tokenize(base) + @params = params.map { |par| Typedef.tokenize(par) } + end + + def expand(named_values) + new_base = base.expand(named_values) + new_params = params.map { |par| par.expand(named_values) } + Type.new(new_base, *new_params) + end + + # @param api_map [ApiMap] + # @param api_map [Array] + # @return [Type] + def resolve_rooted(api_map, gates) + new_base = base.resolve_rooted(api_map, gates) + new_params = params.map { |par| par.resolve_rooted(api_map, gates) } + Type.new(new_base, *new_params) + end + + def resolved? + base.resolved? && params.all?(&:resolved?) + end + + def scope + %w[Class Module].include?(base.to_s) ? :class : :instance + end + + def class? + scope == :class + end + + def instance? + scope == :instance + end + + def expanded? + base.expanded? && params.all?(&:expanded?) + end + + def generic? + all.any?(&:generic?) + end + + def nullable? + base.to_s == 'nil' + end + + def to_s + "#{base}#{params_to_s}" + end + + def to_complex_type + if params.empty? + ComplexType.try_parse("#{base}") + else + ComplexType.try_parse("#{base}<#{params.join(', ')}>") + end + end + + def all + [base] + params + end + + # @param [ComplexType] + # @return [Array] + def self.from_complex_type complex_type + complex_type.to_typedef_types + end + + private + + def params_to_s + return "" if @params.empty? + "[#{params.join(', ')}]" + end + + ROOT = Type.new(Path::ROOT) + end + end +end diff --git a/lib/solargraph/typedef/typeset.rb b/lib/solargraph/typedef/typeset.rb new file mode 100644 index 000000000..2d5fef69d --- /dev/null +++ b/lib/solargraph/typedef/typeset.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module Solargraph + module Typedef + class Typeset + attr_reader :types + + # @param types [Array] + def initialize types + @types = types + end + + # @param named_values [Hash] + def expand(named_values) + Typeset.new(types.map { |type| type.expand(named_values) }) + end + + def resolve_rooted(api_map, gates) + Typeset.new(types.map { |type| type.resolve_rooted(api_map, gates) }) + end + + def generic? + types.any?(&:generic?) + end + + # @return [ComplexType] + def to_complex_type + ComplexType.new(types.map(&:to_complex_type)) + end + + def to_s + types.join(', ') + end + + # @param [ComplexType] + # @return [self] + def self.from_complex_type complex_type + new(complex_type.to_typedef_types) + end + end + end +end diff --git a/spec/complex_type_spec.rb b/spec/complex_type_spec.rb index 7064b9df4..3d2e7e5a3 100644 --- a/spec/complex_type_spec.rb +++ b/spec/complex_type_spec.rb @@ -743,5 +743,10 @@ def make_bar atype = Solargraph::ComplexType.parse(':foo') expect(atype.conforms_to?(api_map, ptype, :method_call)).to be(true) end + + it 'converts generic parameters to Typdef::Type' do + complex_type = Solargraph::ComplexType.parse("Hash, self>") + expect(complex_type.to_typedef_types.map(&:to_s)).to eq(['Hash[generic, self]']) + end end end diff --git a/spec/source_map/clip_spec.rb b/spec/source_map/clip_spec.rb index b30002967..c29c7ee67 100644 --- a/spec/source_map/clip_spec.rb +++ b/spec/source_map/clip_spec.rb @@ -822,7 +822,8 @@ def self.new expect(clip.infer.tag).to eq('Class') end - it 'infers undefined from Class#new' do + it 'infers Class from Class#new' do + pending "Needs work" source = Solargraph::Source.load_string(%( cls = Class.new cls.new @@ -830,17 +831,17 @@ def self.new api_map = Solargraph::ApiMap.new api_map.map source clip = api_map.clip_at('test.rb', [2, 11]) - expect(clip.infer.tag).to eq('undefined') + expect(clip.infer.tag).to eq('Class') end - it 'infers undefined from Class.new.new' do + it 'infers BasicObject from Class.new.new' do source = Solargraph::Source.load_string(%( Class.new.new ), 'test.rb') api_map = Solargraph::ApiMap.new api_map.map source clip = api_map.clip_at('test.rb', [1, 17]) - expect(clip.infer.tag).to eq('undefined') + expect(clip.infer.tag).to eq('BasicObject') end it 'completes class instance variables in the namespace' do diff --git a/spec/typedef/call_spec.rb b/spec/typedef/call_spec.rb new file mode 100644 index 000000000..40498a2d8 --- /dev/null +++ b/spec/typedef/call_spec.rb @@ -0,0 +1,353 @@ +# frozen_string_literal: true + +# @todo describe Linker::Call +describe Solargraph::Typedef::Dictionary do + it 'handles super calls to same method' do + pending 'Returns [Integer, Integer]' + api_map = Solargraph::ApiMap.new + source = Solargraph::Source.load_string(%( + class Foo + def my_method + 123 + end + end + class Bar < Foo + def my_method + 456 + super + end + end + Bar.new.my_method), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + dictionary = described_class.new(api_map, 'test.rb', [11, 14]) + types = dictionary.infer + expect(types.map(&:to_s)).to eq(['Integer']) + end + + it 'infers return types based on yield call and @yieldreturn' do + pending 'Block suport' + api_map = Solargraph::ApiMap.new + source = Solargraph::Source.load_string(%( + class Foo + # @yieldreturn [Integer] + def my_method(&block) + yield + end + end + Foo.new.my_method), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + dictionary = described_class.new(api_map, 'test.rb', [7, 14]) + types = dictionary.infer + expect(types.map(&:to_s)).to eq(['Integer']) + end + + it 'infers return types based only on yield call and @yieldreturn' do + pending 'Block support' + api_map = Solargraph::ApiMap.new + source = Solargraph::Source.load_string(%( + class Foo + # @yieldreturn [Integer] + def my_method(&block) + yield + end + end + Foo.new.my_method { "foo" }), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + dictionary = described_class.new(api_map, 'test.rb', [7, 32]) + types = dictionary.infer + expect(types.map(&:to_s)).to eq(['Integer']) + end + + it 'adds virtual constructors for .new calls with conflicting return types' do + pending "May need to skip probes for expanded types" + api_map = Solargraph::ApiMap.new + source = Solargraph::Source.load_string(%( + class Foo + # @return [String] + def self.new; end + end + Foo.new + ), 'test.rb') + api_map.map source + # chain = Solargraph::Source::SourceChainer.chain(source, Solargraph::Position.new(4, 11)) + # type = chain.infer(api_map, Solargraph::Pin::ROOT_PIN, api_map.source_map(nil).locals) + # expect(type.tag).to eq('String') + + api_map = Solargraph::ApiMap.new.map(source) + dictionary = described_class.new(api_map, 'test.rb', [4, 11]) + types = dictionary.infer + expect(types.map(&:to_s)).to eq(['String']) + end + + it 'infers types from macros' do + pending 'WIP' + source = Solargraph::Source.load_string(%( + class Foo + # @!macro + # @return [$1] + def self.bar; end + end + Foo.bar(String) + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + dictionary = described_class.new(api_map, 'test.rb', [6, 10]) + types = dictionary.infer + expect(types.map(&:to_s)).to eq(['String']) + end + + it 'infers generic types from Array#reverse' do + source = Solargraph::Source.load_string(%( + # @type [Array] + list = array_of_strings + list.reverse + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + dictionary = described_class.new(api_map, 'test.rb', [3, 11]) + types = dictionary.infer + expect(types.map(&:to_s)).to eq(['Array[String]']) + end + + it 'infers constant return types via returns, ignoring blocks' do + pending "Block support" + source = Solargraph::Source.load_string(%( + def yielder(&blk) + "foo" + end + + yielder do + 123 + end + ), 'test.rb') + # api_map = Solargraph::ApiMap.new + # api_map.map source + # chain = Solargraph::Source::SourceChainer.chain(source, Solargraph::Position.new(7, 8)) + # type = chain.infer(api_map, Solargraph::Pin::ROOT_PIN, api_map.source_map('test.rb').locals) + # expect(type.simple_tags).to eq('String') + + api_map = Solargraph::ApiMap.new.map(source) + dictionary = described_class.new(api_map, 'test.rb', [7, 8]) + types = dictionary.infer + expect(types.map(&:to_s)).to eq(['Array[String]']) + end + + it 'infers method return types' do + source = Solargraph::Source.load_string(%( + def bar + 123 + end + + def baz + bar + end + + baz + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + dictionary = described_class.new(api_map, 'test.rb', [9, 9]) + types = dictionary.infer + expect(types.map(&:to_s)).to eq(['Integer']) + end + + it 'infers method return types with unused blocks' do + source = Solargraph::Source.load_string(%( + def bar + 123 + end + + def baz(&block) + bar + end + + baz { "foo" } + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + dictionary = described_class.new(api_map, 'test.rb', [9, 9]) + types = dictionary.infer + expect(types.map(&:to_s)).to eq(['Integer']) + end + + it 'infers generic return types from block from yield being a return node' do + pending('deeper inference support') + + source = Solargraph::Source.load_string(%( + def yielder(&blk) + yield + end + + yielder do + 123 + end + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + dictionary = described_class.new(api_map, 'test.rb', [7, 9]) + types = dictionary.infer + expect(types.map(&:to_s)).to eq(['Integer']) + end + + it 'infers types from union type' do + source = Solargraph::Source.load_string(%( + # @type [String, Float] + list = string_or_float + list.upcase + list.ceil + ), 'test.rb') + # api_map = Solargraph::ApiMap.new + # api_map.map source + + # chain = Solargraph::Source::SourceChainer.chain(source, Solargraph::Position.new(3, 11)) + # type = chain.infer(api_map, Solargraph::Pin::ROOT_PIN, api_map.source_map('test.rb').locals) + # expect(type.tag).to eq('String') + + # chain = Solargraph::Source::SourceChainer.chain(source, Solargraph::Position.new(4, 11)) + # type = chain.infer(api_map, Solargraph::Pin::ROOT_PIN, api_map.source_map('test.rb').locals) + # expect(type.tag).to eq('Integer') + + api_map = Solargraph::ApiMap.new.map(source) + + dictionary = described_class.new(api_map, 'test.rb', [3, 11]) + types = dictionary.infer + expect(types.map(&:to_s)).to eq(['String']) + + dictionary = described_class.new(api_map, 'test.rb', [4, 11]) + types = dictionary.infer + pending "[Integer, Float, Integer, Numeric]" + expect(types.map(&:to_s)).to eq(['Integer']) + end + + it 'infers generic types from union type' do + source = Solargraph::Source.load_string(%( + # @type [String, Array] + list = string_or_integer + list.upcase + list.each + ), 'test.rb') + api_map = Solargraph::ApiMap.new + api_map.map source + + api_map = Solargraph::ApiMap.new.map(source) + + dictionary = described_class.new(api_map, 'test.rb', [3, 11]) + types = dictionary.infer + expect(types.map(&:to_s)).to eq(['String']) + + dictionary = described_class.new(api_map, 'test.rb', [4, 11]) + types = dictionary.infer + pending 'Missing generic expansion' + expect(types.map(&:to_s)).to eq(['Integer']) + end + + it 'allows calls off of nilable objects by default' do + source = Solargraph::Source.load_string(%( + # @type [String, nil] + f = foo + a = f.upcase + a + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + dictionary = described_class.new(api_map, 'test.rb', [4, 6]) + types = dictionary.infer + expect(types.map(&:to_s)).to eq(['String']) + end + + it 'denies calls off of nilable objects when loose union mode is off' do + pending 'WIP' + source = Solargraph::Source.load_string(%( + # @type [String, nil] + f = foo + a = f.upcase + a + ), 'test.rb') + + api_map = Solargraph::ApiMap.new(loose_unions: false).map(source) + dictionary = described_class.new(api_map, 'test.rb', [4, 6]) + types = dictionary.infer + expect(types.map(&:to_s)).to eq(['undefined']) + end + + it 'preserves unions in value position in Hash' do + source = Solargraph::Source.load_string(%( + # @param params [Hash{String => Array, Hash{String => undefined}, String, Integer}] + def foo(params) + position = params['position'] + position + col = position['character'] + col + end + ), 'test.rb') + + api_map = Solargraph::ApiMap.new(loose_unions: false).map(source) + dictionary = described_class.new(api_map, 'test.rb', [4, 8]) + types = dictionary.infer + expect(types.map(&:to_s)).to eq(['Array', 'Hash[String, undefined]', 'String', 'Integer', 'nil']) + end + + it 'preserves undefined and underdefined types in resolution' do + source = Solargraph::Source.load_string(%( + # @param params [Hash{String => Array, Hash{String => undefined}, String, Integer}] + def foo(params) + position = params['position'] + position + col = position['character'] + col + end + ), 'test.rb') + + api_map = Solargraph::ApiMap.new(loose_unions: false).map(source) + dictionary = described_class.new(api_map, 'test.rb', [6, 8]) + types = dictionary.infer + expect(types.map(&:to_s)).to eq(['undefined']) + end + + it 'correctly looks up civars' do + source = Solargraph::Source.load_string(%( + class Foo + BAZ = /aaa/ + + # @param comment [String] + def bar(comment) + b = ("foo" =~ BAZ) + b + end + end + ), 'test.rb') + + api_map = Solargraph::ApiMap.new(loose_unions: false).map(source) + dictionary = described_class.new(api_map, 'test.rb', [7, 10]) + types = dictionary.infer + expect(types.map(&:to_s)).to eq(['Integer', 'nil']) + end + + it 'does not mis-parse generic methods with type constraints' do + source = Solargraph::Source.load_string(%( + def bl + out = (Encoding.default_external = 'UTF-8') + out + end + ), 'test.rb') + + api_map = Solargraph::ApiMap.new(loose_unions: false).map(source) + dictionary = described_class.new(api_map, 'test.rb', [3, 8]) + types = dictionary.infer + expect(types.map(&:to_s)).to eq(['String']) + end + + it 'handles this weird case' do + pending 'Generic and signature issues' + source = Solargraph::Source.load_string(%( + Encoding.default_external = 'UTF-8' + ), 'test.rb') + + api_map = Solargraph::ApiMap.new(loose_unions: false).map(source) + dictionary = described_class.new(api_map, 'test.rb', [1, 15]) + types = dictionary.infer + expect(types.map(&:to_s)).to eq(['String']) + end +end diff --git a/spec/typedef/dictionary_linker_spec.rb b/spec/typedef/dictionary_linker_spec.rb new file mode 100644 index 000000000..1190f1bb7 --- /dev/null +++ b/spec/typedef/dictionary_linker_spec.rb @@ -0,0 +1,404 @@ +# frozen_string_literal: true + +describe Solargraph::Typedef::Dictionary do + it "infers types from new subclass calls without a subclass initialize method" do + source = Solargraph::Source.load_string(%( + class Sup + def initialize; end + def meth; end + end + class Sub < Sup + def meth; end + end + + Sub.new + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + dictionary = described_class.new(api_map, 'test.rb', [9, 10]) + types = dictionary.infer + expect(types.map(&:to_s)).to match_array(['Sub']) + end + + it "follows constant chains" do + source = Solargraph::Source.load_string(%( + module Mixin; end + module Container + class Foo; end + end + Container::Foo::Mixin + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + dictionary = described_class.new(api_map, 'test.rb', [5, 23]) + pins = dictionary.define + expect(pins).to be_empty + end + + it "rebases inner constants chains" do + source = Solargraph::Source.load_string(%( + class Foo + class Bar; end + ::Foo::Bar + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + dictionary = described_class.new(api_map, 'test.rb', [3, 16]) + pins = dictionary.define + expect(pins.first.path).to eq('Foo::Bar') + end + + it "resolves relative constant paths" do + source = Solargraph::Source.load_string(%( + class Foo + class Bar + class Baz; end + end + module Other + Bar::Baz + end + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + dictionary = described_class.new(api_map, 'test.rb', [6, 16]) + pins = dictionary.define + expect(pins.first.path).to eq('Foo::Bar::Baz') + end + + it "avoids recursive variable assignments" do + source = Solargraph::Source.load_string(%( + @foo = @bar + @bar = @foo.quz + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + dictionary = described_class.new(api_map, 'test.rb', [2, 18]) + expect { + dictionary.define + }.not_to raise_error + end + + it "pulls types from multiple lines of code" do + source = Solargraph::Source.load_string(%( + 123 + 'abc' + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + dictionary = described_class.new(api_map, 'test.rb', [2, 11]) + types = dictionary.infer + expect(types.map(&:to_s)).to eq(['String']) + end + + it "uses last line of a begin expression as return type" do + source = Solargraph::Source.load_string(%( + begin + 123 + 'abc' + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + dictionary = described_class.new(api_map, 'test.rb', [4, 9]) + type = dictionary.infer + expect(type.map(&:to_s)).to eq(['String']) + end + + it "matches constants on complete symbols" do + source = Solargraph::Source.load_string(%( + class Correct; end + class NotCorrect; end + Correct + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + dictionary = described_class.new(api_map, 'test.rb', [3, 6]) + result = dictionary.define + expect(result.map(&:path)).to eq(['Correct']) + end + + it 'infers booleans from or-nodes passed to !' do + source = Solargraph::Source.load_string(%( + !([].include?('.') || [].include?('#')) + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + dictionary = described_class.new(api_map, 'test.rb', [1, 7]) + types = dictionary.infer + expect(types.map(&:to_s)).to eq(['Boolean']) + end + + it 'infers last type from and-nodes' do + source = Solargraph::Source.load_string(%( + [] && '' + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + dictionary = described_class.new(api_map, 'test.rb', [1, 14]) + types = dictionary.infer + expect(types.map(&:to_s)).to eq(['String']) + end + + it 'infers multiple types from or-nodes' do + source = Solargraph::Source.load_string(%( + [] || '' + ), 'test.rb') + # api_map = Solargraph::ApiMap.new + # chain = Solargraph::Parser.chain(source.node) + # type = chain.infer(api_map, Solargraph::Pin::ROOT_PIN, []) + api_map = Solargraph::ApiMap.new.map(source) + dictionary = described_class.new(api_map, 'test.rb', [1, 10]) + types = dictionary.infer + expect(types.map(&:to_s)).to eq(['Array', 'String']) + end + + it 'infers Procs from block-pass nodes' do + pending 'Not sure what position to define/infer' + source = Solargraph::Source.load_string(%( + x = [] + x.map(&:foo) + ), 'test.rb') + # api_map = Solargraph::ApiMap.new + # api_map.map source + # node = source.node_at(2, 12) + # chain = Solargraph::Parser.chain(node, 'test.rb') + # pin = chain.define(api_map, Solargraph::Pin::ROOT_PIN, []).first + # expect(pin.return_type.tag).to eq('Proc') + # type = chain.infer(api_map, Solargraph::Pin::ROOT_PIN, []) + # expect(type.tag).to eq('Proc') + api_map = Solargraph::ApiMap.new.map(source) + dictionary = described_class.new(api_map, 'test.rb', [0, 0]) + pins = dictionary.define + expect(pins.map(&:return_type).map(&:tag)).to eq(['Proc']) + types = dictionary.infer + expect(types.map(&:to_s)).to eq(['Proc']) + end + + it 'infers Boolean from true' do + source = Solargraph::Source.load_string(%( + @x = true + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + dictionary = described_class.new(api_map, 'test.rb', [1, 8]) + types = dictionary.infer + expect(types.map(&:to_s)).to eq(['Boolean']) + end + + it 'infers self from Array#new' do + source = Solargraph::Source.load_string(%( + Array.new + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + dictionary = described_class.new(api_map, 'test.rb', [1, 12]) + types = dictionary.infer + expect(types.map(&:to_s)).to eq(['Array']) + end + + it 'infers self from inherited Object#freeze' do + source = Solargraph::Source.load_string(%( + Array.new.freeze + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + dictionary = described_class.new(api_map, 'test.rb', [1, 16]) + types = dictionary.infer + expect(types.map(&:to_s)).to eq(['Array']) + end + + it 'infers the nearest constants first' do + source = Solargraph::Source.load_string(%( + module Outer + class String; end + end + module Outer + module Inner + def self.outer_string + String + end + end + end + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + dictionary = described_class.new(api_map, 'test.rb', [6, 19]) + types = dictionary.infer + expect(types.map(&:to_s)).to eq(['Class[Outer::String]']) + end + + it 'infers rooted constants' do + source = Solargraph::Source.load_string(%( + module Outer + class String; end + end + module Outer + module Inner + def self.core_string + ::String + end + end + end + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + dictionary = described_class.new(api_map, 'test.rb', [6, 19]) + types = dictionary.infer + expect(types.map(&:to_s)).to eq(['Class[String]']) + end + + it 'infers String from interpolated strings' do + source = Solargraph::Source.load_string('"#{Object}"', 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + dictionary = described_class.new(api_map, 'test.rb', [0, 0]) + types = dictionary.infer + expect(types.map(&:to_s)).to eq(['String']) + end + + it 'infers Symbol from symbols' do + source = Solargraph::Source.load_string(':foo', 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + dictionary = described_class.new(api_map, 'test.rb', [0, 0]) + types = dictionary.infer + expect(types.map(&:to_s)).to eq(['Symbol']) + end + + it 'infers Symbol from quoted symbols' do + source = Solargraph::Source.load_string(':"foo"', 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + dictionary = described_class.new(api_map, 'test.rb', [0, 0]) + types = dictionary.infer + expect(types.map(&:to_s)).to eq(['Symbol']) + end + + it 'infers Symbol from interpolated symbols' do + source = Solargraph::Source.load_string(':"#{Object}"', 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + dictionary = described_class.new(api_map, 'test.rb', [0, 0]) + types = dictionary.infer + expect(types.map(&:to_s)).to eq(['Symbol']) + end + + it 'infers namespaces from constant aliases' do + source = Solargraph::Source.load_string(%( + class Foo + class Bar; end + end + Alias = Foo + Alias::Bar.new + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + dictionary = described_class.new(api_map, 'test.rb', [5, 17]) + types = dictionary.infer + expect(types.map(&:to_s)).to eq(['Foo::Bar']) + end + + it 'infers instance variables from sequential assignments' do + pending('sequential assignment support') + + source = Solargraph::Source.load_string(%( + def foo + @foo = nil + @foo = 'foo' + end + )) + api_map = Solargraph::ApiMap.new + api_map.map source + pin = api_map.get_path_pins('#foo').first + type = pin.probe(api_map) + expect(type.simple_tags).to eq('String') + + api_map = Solargraph::ApiMap.new.map(source) + dictionary = described_class.new(api_map, 'test.rb', [3, 8]) + types = dictionary.infer + expect(types.map(&:to_s)).to eq(['String']) + end + + it 'recognizes nil safe navigation without upstream nil' do + source = Solargraph::Source.load_string(%( + String.new&.strip + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + dictionary = described_class.new(api_map, 'test.rb', [1, 18]) + types = dictionary.infer + expect(types.map(&:to_s)).to eq(['String']) + end + + it 'recognizes nil safe navigation with upstream nil' do + source = Solargraph::Source.load_string(%( + # @return [String, nil] + def foo; end + foo&.upcase + ), 'test.rb') + api_map = Solargraph::ApiMap.new.map(source) + dictionary = described_class.new(api_map, 'test.rb', [2, 11]) + types = dictionary.infer + expect(types.map(&:to_s)).to eq(['String', 'nil']) + end + + it 'infers Class from Object#class' do + source = Solargraph::Source.load_string(%( + String.new.class + ), 'test.rb') + # api_map = Solargraph::ApiMap.new.map(source) + # chain = Solargraph::Source::SourceChainer.chain(source, Solargraph::Position.new(1, 17)) + # tag = chain.infer(api_map, Solargraph::Pin::ROOT_PIN, []) + # expect(tag.to_s).to eq('Class') + api_map = Solargraph::ApiMap.new.map(source) + dictionary = described_class.new(api_map, 'test.rb', [1, 17]) + types = dictionary.infer + expect(types.map(&:to_s)).to eq(['Class[String]', 'Class']) + end + + it 'resolves variable and method name collisions' do + source = Solargraph::Source.load_string(%( + class Example + # @return [String] + def stringify; end + + class << self + # @return [Example] + def obj(foo); end + end + end + + obj = Example.obj + str = obj.stringify + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + dictionary = described_class.new(api_map, 'test.rb', [12, 7]) + types = dictionary.infer + expect(types.map(&:to_s)).to eq(['String']) + end + + it 'infers class variables' do + source = Solargraph::Source.load_string(%( + class Example + @@foo = 'string' + + def bar + @@foo + end + end + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + dictionary = described_class.new(api_map, 'test.rb', [4, 12]) + types = dictionary.infer + expect(types.map(&:to_s)).to eq(['String']) + end + + it 'understands &. in chains' do + source = Solargraph::Source.load_string(%( + # @param a [String, nil] + # @return [String, nil] + def foo a + b = a&.upcase + b + end + + b = foo 123 + b + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + + dictionary = described_class.new(api_map, 'test.rb', [5, 8]) + types = dictionary.infer + expect(types.map(&:to_s)).to eq(['String', 'nil']) + + dictionary = described_class.new(api_map, 'test.rb', [9, 6]) + types = dictionary.infer + expect(types.map(&:to_s)).to eq(['String', 'nil']) + end +end diff --git a/spec/typedef/dictionary_spec.rb b/spec/typedef/dictionary_spec.rb new file mode 100644 index 000000000..a088d631f --- /dev/null +++ b/spec/typedef/dictionary_spec.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +describe Solargraph::Typedef::Dictionary do + it 'resolves methods with parameters' do + source = Solargraph::Source.load_string(%( + # @return [Array] + def foo; end + + foo.first + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + dictionary = described_class.new(api_map, 'test.rb', [4, 10]) + pins = dictionary.define + expect(pins.map(&:path)).to eq(['Array#first', 'Enumerable#first']) + end + + it 'infers types' do + pending "Refine with overloads" + source = Solargraph::Source.load_string(%( + # @return [Array] + def foo; end + + foo.first + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + dictionary = described_class.new(api_map, 'test.rb', [4, 10]) + types = dictionary.infer + expect(types.map(&:to_s)).to match_array(['String', 'nil']) + end + + it 'resolves self' do + source = Solargraph::Source.load_string(%( + class Foo + def bar; end + + def baz + self.bar + end + end + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + dictionary = described_class.new(api_map, 'test.rb', [5, 17]) + pins = dictionary.define + expect(pins.map(&:path)).to eq(['Foo#bar']) + end + + it 'infers local variables' do + source = Solargraph::Source.load_string(%( + x = 0 + x + + y = 'foo' + z = [] + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + dictionary = described_class.new(api_map, 'test.rb', [2, 6]) + types = dictionary.infer + expect(types.map(&:to_s)).to eq(['Integer']) + end +end diff --git a/spec/typedef/generics_spec.rb b/spec/typedef/generics_spec.rb new file mode 100644 index 000000000..a2d4fe751 --- /dev/null +++ b/spec/typedef/generics_spec.rb @@ -0,0 +1,216 @@ +# frozen_string_literal: true + +# @todo describe Generics +describe Solargraph::Typedef::Dictionary do + it 'infers generic parameterized types through module inclusion' do + source = Solargraph::Source.load_string(%( + # @generic GenericTypeParam + module Foo + # @return [Array>] + def baz + end + end + + class Baz + # @return [Baz] + def self.bar + end + + include Foo + end + + Baz.bar.baz + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + dictionary = described_class.new(api_map, 'test.rb', [16, 15]) + types = dictionary.infer + expect(types.map(&:to_s)).to eq(['Array[String]']) + end + + it 'infers generic-class method return values with self reference' do + source = Solargraph::Source.load_string(%( + # @generic GenericTypeParam + module Foo + # @return [Hash, self>] + def baz + end + end + + class Baz + # @return [Baz] + def self.bar + end + + include Foo + end + + Baz.bar.baz + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + dictionary = described_class.new(api_map, 'test.rb', [16, 15]) + types = dictionary.infer + expect(types.map(&:to_s)).to eq(['Hash[String, Baz]']) + end + + it 'infers method return types based on method generic' do + pending('deeper inference support') + + source = Solargraph::Source.load_string(%( + class Foo + # @Generic A + # @param x [generic] + # @return [generic] + def bar(x); end + end + + foo = Foo.new + a = foo.bar("baz") + a + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + dictionary = described_class.new(api_map, 'test.rb', [10, 6]) + types = dictionary.infer + expect(types.map(&:to_s)).to eq(['String']) + end + + it 'infers generic types from @generic tag' do + pending 'Signature support' + source = Solargraph::Source.load_string(%( + # @generic GenericTypeParam + class Foo + # @return [Foo] + def self.bar + end + + # @return [Array>] + def baz + end + end + + Foo.bar.baz + Foo.bar.baz.first + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + + dictionary = described_class.new(api_map, 'test.rb', [12, 15]) + types = dictionary.infer + expect(types.map(&:to_s)).to eq(['Array[String]']) + + dictionary = described_class.new(api_map, 'test.rb', [13, 20]) + types = dictionary.infer + expect(types.map(&:to_s)).to eq(['Array']) + end + + it 'calculates class return type based on class generic' do + source = Solargraph::Source.load_string(%( + # @generic A + class Foo + # @return [generic] + def bar; end + end + + # @type [Foo] + f = Foo.new + a = f.bar + a + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + dictionary = described_class.new(api_map, 'test.rb', [10, 7]) + types = dictionary.infer + expect(types.map(&:to_s)).to eq(['String']) + end + + it 'sends proper gates in ProxyType' do + source = Solargraph::Source.load_string(%( + module Foo + module Bar + class Symbol + end + end + end + + module Foo + module Baz + class Quux + # @return [void] + def foo + s = objects_by_class(Bar::Symbol) + s + end + + # @generic T + # @param klass [Class>] + # @return [Set>] + def objects_by_class klass + # @type [Set>] + s = Set.new + s + end + end + end + end + ), 'test.rb') + + api_map = Solargraph::ApiMap.new(loose_unions: false).map(source) + dictionary = described_class.new(api_map, 'test.rb', [14, 14]) + types = dictionary.infer + expect(types.map(&:to_s)).to eq(['Set[Foo::Bar::Symbol]']) + end + + it 'gracefully handles requests for type of generic method in chain' do + source = Solargraph::Source.load_string(%( + # @generic T + # @param x [generic] + # @return [generic]} + def foo(x); x; end + foo('string') + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + dictionary = described_class.new(api_map, 'test.rb', [5, 7]) + expect { dictionary.infer }.not_to raise_error + + # @todo The original test suggested that the method call should be inferred + # as [String]. That functionality is currently possible with macros. I'm + # not sure that generics are a good fit here. + end + + describe '#names' do + let(:pin) { double(Solargraph::Pin::Base, typedef_generics: [], closure: nil) } + let(:receiver) { double(Solargraph::Pin::Base, typedef_generics: [], closure: nil) } + + it 'finds generic names from source pins' do + source = Solargraph::Source.load_string(%( + # @generic T + class Example + def foo; end + end + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + pin = api_map.get_path_pins('Example#foo').first + generics = Solargraph::Typedef::Generics.new(api_map, pin, receiver) + expect(generics.names).to eq(['T']) + end + + it 'finds generic names from receiver pins' do + source = Solargraph::Source.load_string(%( + class Example + # @return [Array] + def foo; end + end + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + # Simulating the receiver + receiver = api_map.get_path_pins('Array').first + generics = Solargraph::Typedef::Generics.new(api_map, pin, receiver) + expect(generics.names).to eq(['Elem']) + end + end +end diff --git a/spec/typedef/linker/call_spec.rb b/spec/typedef/linker/call_spec.rb new file mode 100644 index 000000000..4f1f54e10 --- /dev/null +++ b/spec/typedef/linker/call_spec.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +describe Solargraph::Typedef::Linker::Call do +end diff --git a/spec/typedef/memos_spec.rb b/spec/typedef/memos_spec.rb new file mode 100644 index 000000000..1b72f8526 --- /dev/null +++ b/spec/typedef/memos_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +describe Solargraph::Typedef::Memos do + let(:memos) { described_class.new } + + it 'saves on fetch' do + memos.fetch('key') { 'value' } + expect(memos.cache['key']).to eq('value') + end + + it 'fetches memoized values' do + memos.cache['key'] = 'value' + result = memos.fetch('key') { raise 'Should not happen' } + expect(result).to eq('value') + end + + it 'clears the cache' do + memos.cache['key'] = 'value' + memos.clear + expect(memos.cache).to be_empty + end + + it 'tracks pending memos' do + memos.cache['key'] do + expect(memos.pending).to include('key') + end + expect(memos.pending).not_to include('key') + end + + it 'returns default on recursive actions' do + result = memos.fetch('key') do + memos.fetch('key', 'safe') { 'oops' } + end + expect(result).to be('safe') + end +end diff --git a/spec/typedef/or_spec.rb b/spec/typedef/or_spec.rb new file mode 100644 index 000000000..64ceb83f4 --- /dev/null +++ b/spec/typedef/or_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +describe Solargraph::Typedef::Dictionary do + it 'handles simple nil-removal' do + source = Solargraph::Source.load_string(%( + # @param a [Integer, nil] + def foo a + b = a || 10 + b + end + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + dictionary = described_class.new(api_map, 'test.rb', [4, 8]) + types = dictionary.infer + expect(types.map(&:to_s)).to eq(['Integer']) + end + + it 'removes nil from more complex cases' do + source = Solargraph::Source.load_string(%( + def foo + out = ENV['BAR'] || + File.join(Dir.home, '.config', 'solargraph', 'config.yml') + out + end + ), 'test.rb') + + api_map = Solargraph::ApiMap.new.map(source) + dictionary = described_class.new(api_map, 'test.rb', [3, 8]) + types = dictionary.infer + expect(types.map(&:to_s)).to eq(['String']) + end +end diff --git a/spec/typedef/path_spec.rb b/spec/typedef/path_spec.rb new file mode 100644 index 000000000..e1f1160d8 --- /dev/null +++ b/spec/typedef/path_spec.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +describe Solargraph::Typedef::Path do + describe '.new' do + it 'assumes not rooted' do + path = described_class.new('Foo') + expect(path).not_to be_rooted + end + + it 'accepts rooted' do + path = described_class.new('Foo', rooted: true) + expect(path).to be_rooted + end + + it 'sets rooted from explicit root path' do + path = described_class.new('::Foo') + expect(path).to be_rooted + end + + it 'sets root' do + path = described_class.new('') + expect(path).to be_root + end + + it 'converts core pin paths' do + api_map = Solargraph::ApiMap.new + api_map.pins.each { |pin| pin.typedef_path } + end + end + + describe '#from' do + it 'combines paths' do + path1 = described_class.new('Bar') + path2 = described_class.new('Foo') + joined = path1.from(path2) + expect(joined.name).to eq('Foo::Bar') + end + + it 'sets rooted' do + path1 = described_class.new('Bar') + path2 = described_class.new('::Foo') + joined = path1.from(path2) + expect(joined.name).to eq('Foo::Bar') + expect(joined).to be_rooted + end + end + + describe '#resolve_rooted' do + it 'returns rooted paths' do + source = Solargraph::Source.load_string(%( + class Foo + class Bar + end + end + )) + api_map = Solargraph::ApiMap.new.map(source) + path = described_class.new('Bar') + resolved = path.resolve_rooted(api_map, ['Foo', '']) + expect(resolved.name).to eq('Foo::Bar') + expect(resolved).to be_rooted + end + end +end diff --git a/spec/typedef/token_spec.rb b/spec/typedef/token_spec.rb new file mode 100644 index 000000000..13d01a42f --- /dev/null +++ b/spec/typedef/token_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +describe Solargraph::Typedef::Token do + it 'does not resolve unreserved names' do + token = described_class.new('test') + expect(token).not_to be_resolved + end + + it 'resolves undefined' do + token = described_class.new('undefined') + expect(token).to be_resolved + end + + it 'resolves nil' do + token = described_class.new('nil') + expect(token).to be_resolved + end +end diff --git a/spec/typedef/type_spec.rb b/spec/typedef/type_spec.rb new file mode 100644 index 000000000..394c41b65 --- /dev/null +++ b/spec/typedef/type_spec.rb @@ -0,0 +1,101 @@ +# frozen_string_literal: true + +describe Solargraph::Typedef::Type do + describe '#from' do + it 'updates paths' do + end + end + + describe '.from_complex_type' do + it 'converts core pin return types' do + api_map = Solargraph::ApiMap.new + api_map.pins.each { |pin| Solargraph::Typedef::Type.from_complex_type(pin.return_type) } + end + + context 'with an unrooted path' do + let(:complex_type) { Solargraph::ComplexType.parse('Foo') } + let(:types) { described_class.from_complex_type(complex_type) } + + it 'converts to one type' do + expect(types).to be_one + end + + it 'converts to a path' do + expect(types.first.to_s).to eq('Foo') + expect(types.first.base).to be_a(Solargraph::Typedef::Path) + end + + it 'does not resolve' do + expect(types.first).not_to be_resolved + end + end + + context 'with a rooted path' do + let(:complex_type) { Solargraph::ComplexType.parse('::Foo') } + let(:types) { described_class.from_complex_type(complex_type) } + + it 'converts to one type' do + expect(types).to be_one + end + + it 'converts to a path' do + expect(types.first.to_s).to eq('Foo') + expect(types.first.base).to be_a(Solargraph::Typedef::Path) + end + + it 'is rooted' do + expect(types.first.base).to be_rooted + end + + it 'resolves' do + expect(types.first).to be_resolved + end + end + + context 'with a parameterized type' do + let(:complex_type) { Solargraph::ComplexType.parse('Array') } + let(:types) { described_class.from_complex_type(complex_type) } + + it 'converts to paths' do + expect(types.first.to_s).to eq('Array[String]') + end + end + end + + describe '#expand' do + it 'resolves simple named tokens to paths' do + named_values = { "foo" => "String" } + type = described_class.new('foo') + resolved = type.expand(named_values) + expect(resolved.to_s).to eq('String') + end + + it 'resolves simple named tokens to rooted paths' do + named_values = { "foo" => "::String" } + type = described_class.new('foo') + resolved = type.expand(named_values) + expect(resolved.to_s).to eq('String') + expect(resolved).to be_resolved + end + + it 'returns unresolved types' do + named_values = { "foo" => "String" } + type = described_class.new('bar') + unresolved = type.expand(named_values) + expect(unresolved.to_s).to eq('bar') + expect(unresolved).not_to be_resolved + end + end + + describe '#generic?' do + it 'is true if any parameter is generic' do + type = Solargraph::ComplexType.parse('Array>').to_typedef_types.first + expect(type).to be_generic + end + + it 'is false if no parameters are generic' do + type = Solargraph::ComplexType.parse('Array').to_typedef_types.first + expect(type).not_to be_generic + end + end +end diff --git a/spec/typedef/typeset_spec.rb b/spec/typedef/typeset_spec.rb new file mode 100644 index 000000000..269126c01 --- /dev/null +++ b/spec/typedef/typeset_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +describe Solargraph::Typedef::Typeset do + it 'accepts multiple types' do + type1 = Solargraph::Typedef::Type.new(Solargraph::Typedef.tokenize('Array')) + type2 = Solargraph::Typedef::Type.new(Solargraph::Typedef.tokenize('String')) + typeset = described_class.new([type1, type2]) + expect(typeset.to_s).to eq('Array, String') + end + + describe '#to_complex_type' do + it 'converts to complex types' do + type1 = Solargraph::Typedef::Type.new(Solargraph::Typedef.tokenize('Array')) + type2 = Solargraph::Typedef::Type.new(Solargraph::Typedef.tokenize('String')) + typeset = described_class.new([type1, type2]) + complex_type = typeset.to_complex_type + expect(complex_type).to be_a(Solargraph::ComplexType) + expect(complex_type.to_s).to eq('Array, String') + end + end + + describe '.from_complex_type' do + it 'converts from complex types' do + complex_type = Solargraph::ComplexType.parse('Array', 'String') + typeset = described_class.from_complex_type(complex_type) + expect(typeset.to_s).to eq('Array, String') + end + end + + describe '#expand' do + it 'expands all types' do + type1 = Solargraph::Typedef::Type.from_complex_type(Solargraph::ComplexType.parse('Array>')).first + type2 = Solargraph::Typedef::Type.from_complex_type(Solargraph::ComplexType.parse('Set>')).first + typeset = described_class.new([type1, type2]) + named_values = { 'generic' => 'String' } + expanded = typeset.expand(named_values) + expect(expanded.to_s).to eq('Array[String], Set[String]') + end + end +end diff --git a/spec/typedef_spec.rb b/spec/typedef_spec.rb new file mode 100644 index 000000000..6e68c17d8 --- /dev/null +++ b/spec/typedef_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +describe Solargraph::Typedef do + describe '.tokenize' do + it 'creates a path' do + token = described_class.tokenize('Foo::Bar') + expect(token).to be_a(Solargraph::Typedef::Path) + expect(token.name).to eq('Foo::Bar') + end + + it 'creates a simple token' do + token = described_class.tokenize('param') + expect(token).to be_a(Solargraph::Typedef::Token) + expect(token.name).to eq('param') + end + + it 'creates a YARD generic token' do + token = described_class.tokenize('generic') + expect(token).to be_a(Solargraph::Typedef::Token) + expect(token.name).to eq('generic') + end + end +end