From 65704ca993015c594ee7d47dbe3927badab5a4a6 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sun, 18 Jan 2026 11:39:15 -0500 Subject: [PATCH 1/5] Refactor type hierarchy Let's see if we can make space for a first-class intersection type --- lib/solargraph.rb | 1 + lib/solargraph/complex_type.rb | 4 +++- lib/solargraph/complex_type/unique_type.rb | 4 +++- lib/solargraph/type.rb | 4 ++++ 4 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 lib/solargraph/type.rb diff --git a/lib/solargraph.rb b/lib/solargraph.rb index 27a79e4ad..607fb19ed 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 :Type, 'solargraph/type' dir = File.dirname(__FILE__) VIEWS_PATH = File.join(dir, 'solargraph', 'views') diff --git a/lib/solargraph/complex_type.rb b/lib/solargraph/complex_type.rb index c67f9c2a4..6c91b504b 100644 --- a/lib/solargraph/complex_type.rb +++ b/lib/solargraph/complex_type.rb @@ -3,7 +3,7 @@ module Solargraph # A container for type data based on YARD type tags. # - class ComplexType + class ComplexType < Type GENERIC_TAG_NAME = 'generic'.freeze # @!parse # include TypeMethods @@ -15,6 +15,8 @@ class ComplexType # @param types [Array] def initialize types = [UniqueType::UNDEFINED] + super() + # @todo @items here should not need an annotation # @type [Array] items = types.flat_map(&:items).uniq(&:to_s) diff --git a/lib/solargraph/complex_type/unique_type.rb b/lib/solargraph/complex_type/unique_type.rb index fa0184fbf..ffd2cdd9f 100644 --- a/lib/solargraph/complex_type/unique_type.rb +++ b/lib/solargraph/complex_type/unique_type.rb @@ -5,7 +5,7 @@ class ComplexType # An individual type signature. A complex type can consist of multiple # unique types. # - class UniqueType + class UniqueType < Type include TypeMethods include Equality @@ -72,6 +72,8 @@ def self.parse name, substring = '', make_rooted: nil # @param rooted [Boolean] # @param parameters_type [Symbol, nil] def initialize(name, key_types = [], subtypes = [], rooted:, parameters_type: nil) + super() + if parameters_type.nil? raise "You must supply parameters_type if you provide parameters" unless key_types.empty? && subtypes.empty? end diff --git a/lib/solargraph/type.rb b/lib/solargraph/type.rb new file mode 100644 index 000000000..95048242d --- /dev/null +++ b/lib/solargraph/type.rb @@ -0,0 +1,4 @@ +module Solargraph + # A container for type data + class Type; end +end From ce4bc930af0a3eeb740f6f7961aa73291905e076 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 13 Feb 2026 08:25:40 -0700 Subject: [PATCH 2/5] Move UniqueType methods into Type class --- lib/solargraph/complex_type.rb | 9 +- lib/solargraph/complex_type/type_methods.rb | 239 -------------------- lib/solargraph/complex_type/unique_type.rb | 11 +- lib/solargraph/type.rb | 218 +++++++++++++++++- 4 files changed, 227 insertions(+), 250 deletions(-) delete mode 100644 lib/solargraph/complex_type/type_methods.rb diff --git a/lib/solargraph/complex_type.rb b/lib/solargraph/complex_type.rb index 8234e39d7..172f4d566 100644 --- a/lib/solargraph/complex_type.rb +++ b/lib/solargraph/complex_type.rb @@ -11,7 +11,6 @@ class ComplexType < Type include Equality autoload :Conformance, 'solargraph/complex_type/conformance' - autoload :TypeMethods, 'solargraph/complex_type/type_methods' autoload :UniqueType, 'solargraph/complex_type/unique_type' # @param types [Array] @@ -162,16 +161,10 @@ def namespaces # @param [Array] args def method_missing name, *args, &block return if @items.first.nil? - return @items.first.send(name, *args, &block) if respond_to_missing?(name) + return @items.first.send(name, *args, &block) if @items.first.respond_to?(name) super end - # @param name [Symbol] - # @param include_private [Boolean] - def respond_to_missing? name, include_private = false - TypeMethods.public_instance_methods.include?(name) || super - end - def to_s map(&:tag).join(', ') end diff --git a/lib/solargraph/complex_type/type_methods.rb b/lib/solargraph/complex_type/type_methods.rb deleted file mode 100644 index 5a1775c50..000000000 --- a/lib/solargraph/complex_type/type_methods.rb +++ /dev/null @@ -1,239 +0,0 @@ -# frozen_string_literal: true - -module Solargraph - class ComplexType - # Methods for accessing type data available from - # both ComplexType and UniqueType. - # - # @abstract This mixin relies on these - - # instance variables: - # @name: String - # @subtypes: Array - # @rooted: boolish - # methods: - # transform() - # all_params() - # rooted?() - # can_root_name?() - module TypeMethods - # @!method transform(new_name = nil, &transform_type) - # @param new_name [String, nil] - # @yieldparam t [UniqueType] - # @yieldreturn [UniqueType] - # @return [UniqueType, nil] - # @!method all_params - # @return [Array] - # @!method rooted? - # @!method can_root_name?(name_to_check = nil) - # @param name_to_check [String, nil] - - # @return [String] - attr_reader :name - - # @return [Array] - attr_reader :subtypes - - # @return [String] - def tag - @tag ||= "#{name}#{substring}" - end - - # @return [String] - def rooted_tag - @rooted_tag ||= rooted_name + rooted_substring - end - - def interface? - name.start_with?('_') - end - - # @return [Boolean] - def duck_type? - @duck_type ||= name.start_with?('#') - end - - # @return [Boolean] - def nil_type? - @nil_type ||= name.casecmp('nil').zero? - end - - def tuple? - @tuple ||= (name == 'Tuple') || (name == 'Array' && subtypes.length >= 1 && fixed_parameters?) - end - - def void? - name == 'void' - end - - def defined? - !undefined? - end - - def undefined? - name == 'undefined' - end - - # Variance of the type ignoring any type parameters - # @return [Symbol] - # @param situation [Symbol] The situation in which the variance is being considered. - def erased_variance situation = :method_call - # :nocov: - unless %i[method_call return_type assignment].include?(situation) - raise "Unknown situation: #{situation.inspect}" - end - # :nocov: - :covariant - end - - # @param generics_to_erase [Enumerable] - # @return [self] - def erase_generics generics_to_erase - transform do |type| - if type.name == ComplexType::GENERIC_TAG_NAME - if type.all_params.length == 1 && generics_to_erase.include?(type.all_params.first.to_s) - ComplexType::UNDEFINED - else - type - end - else - type - end - end - end - - # @return [Symbol, nil] - attr_reader :parameters_type - - # @type [Hash{String => Symbol}] - PARAMETERS_TYPE_BY_STARTING_TAG = { - '{' => :hash, - '(' => :fixed, - '<' => :list - }.freeze - - # @return [Boolean] - def list_parameters? - parameters_type == :list - end - - # @return [Boolean] - def fixed_parameters? - parameters_type == :fixed - end - - # @return [Boolean] - def hash_parameters? - parameters_type == :hash - end - - # @return [Array] - def value_types - @subtypes - end - - # @return [Array] - def key_types - @key_types - end - - # @return [String] - def namespace - # if priority higher than ||=, old implements cause unnecessary check - @namespace ||= lambda do - return 'Object' if duck_type? - return 'NilClass' if nil_type? - %w[Class Module].include?(name) && !subtypes.empty? ? subtypes.first.name : name - end.call - end - - # @return [self] - def namespace_type - return ComplexType.parse('::Object') if duck_type? - return ComplexType.parse('::NilClass') if nil_type? - return subtypes.first if %w[Class Module].include?(name) && !subtypes.empty? - self - end - - # @return [String] - def rooted_namespace - return namespace unless rooted? && can_root_name?(namespace) - "::#{namespace}" - end - - # @return [String] - def rooted_name - return name unless @rooted && can_root_name? - "::#{name}" - end - - # @return [String] - def substring - @substring ||= generate_substring_from(&:tags) - end - - # @return [String] - def rooted_substring - @rooted_substring = generate_substring_from(&:rooted_tags) - end - - # @return [String] - def generate_substring_from &to_str - key_types_str = key_types.map(&to_str).join(', ') - subtypes_str = subtypes.map(&to_str).join(', ') - if (key_types.none?(&:defined?) && subtypes.none?(&:defined?)) || - (key_types.empty? && subtypes.empty?) - '' - elsif hash_parameters? - "{#{key_types_str} => #{subtypes_str}}" - elsif fixed_parameters? - "(#{subtypes_str})" - elsif name == 'Hash' - "<#{key_types_str}, #{subtypes_str}>" - else - "<#{key_types_str}#{subtypes_str}>" - end - end - - # @return [::Symbol] :class or :instance - def scope - @scope ||= :instance if duck_type? || nil_type? - @scope ||= %w[Class Module].include?(name) && !subtypes.empty? ? :class : :instance - end - - # @param other [Object] - def == other - return false unless self.class == other.class - # @sg-ignore flow sensitive typing should support .class == .class - tag == other.tag - end - - # Generate a ComplexType that fully qualifies this type's namespaces. - # - # @param api_map [ApiMap] The ApiMap that performs qualification - # @param context [String] The namespace from which to resolve names - # @return [self, ComplexType, UniqueType] The generated ComplexType - def qualify api_map, context = '' - transform do |t| - next t if t.name == GENERIC_TAG_NAME - next t if t.duck_type? || t.void? || t.undefined? - recon = (t.rooted? ? '' : context) - fqns = api_map.qualify(t.name, recon) - if fqns.nil? - next UniqueType::BOOLEAN if t.tag == 'Boolean' - next UniqueType::UNDEFINED - end - t.recreate(new_name: fqns, make_rooted: true) - end - end - - # @yieldparam [UniqueType] - # @return [void] - # @overload each_unique_type() - # @return [Enumerator] - def each_unique_type &block - return enum_for(__method__) unless block_given? - yield self - end - end - end -end diff --git a/lib/solargraph/complex_type/unique_type.rb b/lib/solargraph/complex_type/unique_type.rb index 5e3540586..a86d1c8a1 100644 --- a/lib/solargraph/complex_type/unique_type.rb +++ b/lib/solargraph/complex_type/unique_type.rb @@ -6,10 +6,9 @@ class ComplexType # unique types. # class UniqueType < Type - include TypeMethods include Equality - attr_reader :all_params, :subtypes, :key_types + attr_reader :all_params, :subtypes, :key_types, :name # Create a UniqueType with the specified name and an optional substring. # The substring is the parameter section of a parametrized type, e.g., @@ -102,6 +101,11 @@ def to_s tag end + # @return [Array] + def value_types + @subtypes + end + # @return [self] def simplify_literals transform do |t| @@ -199,6 +203,9 @@ def eql? other @parameters_type == other.parameters_type end + # @return [Symbol, nil] + attr_reader :parameters_type + def == other eql?(other) end diff --git a/lib/solargraph/type.rb b/lib/solargraph/type.rb index 95048242d..b5b9824f3 100644 --- a/lib/solargraph/type.rb +++ b/lib/solargraph/type.rb @@ -1,4 +1,220 @@ module Solargraph # A container for type data - class Type; end + # + # Methods for accessing type data available from + # both ComplexType and ComplexType::UniqueType. + # + # @abstract This abstract class relies on the methods described + # below defined in its subclasses. + class Type + # @!method transform(new_name = nil, &transform_type) + # @param new_name [String, nil] + # @yieldparam t [ComplexType::UniqueType] + # @yieldreturn [ComplexType::UniqueType] + # @return [ComplexType::UniqueType, nil] + # @!method all_params + # @return [Array] + # @!method rooted? + # @!method can_root_name?(name_to_check = nil) + # @param name_to_check [String, nil] + # @!method key_types + # @return [Array] + # @!method name + # @return [String] + # @!method parameters_type + # @return [Symbol, nil] + # @!method subtypes + # @return [Array] + # @!method value_types + # @return [Array] + + # @return [String] + def tag + @tag ||= "#{name}#{substring}" + end + + # @return [String] + def rooted_tag + @rooted_tag ||= rooted_name + rooted_substring + end + + def interface? + name.start_with?('_') + end + + # @return [Boolean] + def duck_type? + @duck_type ||= name.start_with?('#') + end + + # @return [Boolean] + def nil_type? + @nil_type ||= name.casecmp('nil').zero? + end + + def tuple? + @tuple ||= (name == 'Tuple') || (name == 'Array' && subtypes.length >= 1 && fixed_parameters?) + end + + def void? + name == 'void' + end + + def defined? + !undefined? + end + + def undefined? + name == 'undefined' + end + + # Variance of the type ignoring any type parameters + # @return [Symbol] + # @param situation [Symbol] The situation in which the variance is being considered. + def erased_variance situation = :method_call + # :nocov: + unless %i[method_call return_type assignment].include?(situation) + raise "Unknown situation: #{situation.inspect}" + end + # :nocov: + :covariant + end + + # @param generics_to_erase [Enumerable] + # @return [self] + def erase_generics generics_to_erase + transform do |type| + if type.name == ComplexType::GENERIC_TAG_NAME + if type.all_params.length == 1 && generics_to_erase.include?(type.all_params.first.to_s) + ComplexType::UNDEFINED + else + type + end + else + type + end + end + end + + # @type [Hash{String => Symbol}] + PARAMETERS_TYPE_BY_STARTING_TAG = { + '{' => :hash, + '(' => :fixed, + '<' => :list + }.freeze + + # @return [Boolean] + def list_parameters? + parameters_type == :list + end + + # @return [Boolean] + def fixed_parameters? + parameters_type == :fixed + end + + # @return [Boolean] + def hash_parameters? + parameters_type == :hash + end + + # @return [String] + def namespace + # if priority higher than ||=, old implements cause unnecessary check + @namespace ||= lambda do + return 'Object' if duck_type? + return 'NilClass' if nil_type? + %w[Class Module].include?(name) && !subtypes.empty? ? subtypes.first.name : name + end.call + end + + # @return [self] + def namespace_type + return ComplexType.parse('::Object') if duck_type? + return ComplexType.parse('::NilClass') if nil_type? + return subtypes.first if %w[Class Module].include?(name) && !subtypes.empty? + self + end + + # @return [String] + def rooted_namespace + return namespace unless rooted? && can_root_name?(namespace) + "::#{namespace}" + end + + # @return [String] + def rooted_name + return name unless rooted? && can_root_name? + "::#{name}" + end + + # @return [String] + def substring + @substring ||= generate_substring_from(&:tags) + end + + # @return [String] + def rooted_substring + @rooted_substring = generate_substring_from(&:rooted_tags) + end + + # @return [String] + def generate_substring_from &to_str + key_types_str = key_types.map(&to_str).join(', ') + subtypes_str = subtypes.map(&to_str).join(', ') + if (key_types.none?(&:defined?) && subtypes.none?(&:defined?)) || + (key_types.empty? && subtypes.empty?) + '' + elsif hash_parameters? + "{#{key_types_str} => #{subtypes_str}}" + elsif fixed_parameters? + "(#{subtypes_str})" + elsif name == 'Hash' + "<#{key_types_str}, #{subtypes_str}>" + else + "<#{key_types_str}#{subtypes_str}>" + end + end + + # @return [::Symbol] :class or :instance + def scope + @scope ||= :instance if duck_type? || nil_type? + @scope ||= %w[Class Module].include?(name) && !subtypes.empty? ? :class : :instance + end + + # @param other [Object] + def == other + return false unless self.class == other.class + # @sg-ignore flow sensitive typing should support .class == .class + tag == other.tag + end + + # Generate a ComplexType that fully qualifies this type's namespaces. + # + # @param api_map [ApiMap] The ApiMap that performs qualification + # @param context [String] The namespace from which to resolve names + # @return [self, ComplexType, ComplexType::UniqueType] The generated ComplexType + def qualify api_map, context = '' + transform do |t| + next t if t.name == ComplexType::GENERIC_TAG_NAME + next t if t.duck_type? || t.void? || t.undefined? + recon = (t.rooted? ? '' : context) + fqns = api_map.qualify(t.name, recon) + if fqns.nil? + next ComplexType::UniqueType::BOOLEAN if t.tag == 'Boolean' + next ComplexType::UniqueType::UNDEFINED + end + t.recreate(new_name: fqns, make_rooted: true) + end + end + + # @yieldparam [ComplexType::UniqueType] + # @return [void] + # @overload each_unique_type() + # @return [Enumerator] + def each_unique_type &block + return enum_for(__method__) unless block_given? + yield self + end + end end From 5fac5dd3de3f1cdaabf2f370da5bc42d82d446f1 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Fri, 13 Feb 2026 14:49:49 -0700 Subject: [PATCH 3/5] Use type base class --- lib/solargraph/complex_type.rb | 15 +++++-- lib/solargraph/complex_type/unique_type.rb | 5 ++- lib/solargraph/pin/base.rb | 14 +++--- lib/solargraph/pin/base_variable.rb | 6 +-- lib/solargraph/pin/common.rb | 6 +-- lib/solargraph/pin/instance_variable.rb | 2 +- lib/solargraph/pin/local_variable.rb | 2 +- lib/solargraph/pin/method.rb | 6 +-- lib/solargraph/pin/proxy_type.rb | 8 ++-- lib/solargraph/rbs_map/conversions.rb | 8 ++-- lib/solargraph/shell.rb | 2 +- lib/solargraph/source/chain.rb | 10 ++--- lib/solargraph/source/chain/call.rb | 10 ++--- lib/solargraph/type.rb | 52 +++++++++++++++++++--- lib/solargraph/type_checker.rb | 18 ++++---- 15 files changed, 106 insertions(+), 58 deletions(-) diff --git a/lib/solargraph/complex_type.rb b/lib/solargraph/complex_type.rb index 172f4d566..24bea8335 100644 --- a/lib/solargraph/complex_type.rb +++ b/lib/solargraph/complex_type.rb @@ -46,7 +46,7 @@ def qualify api_map, *gates end # @param generics_to_resolve [Enumerable]] - # @param context_type [ComplexType, ComplexType::UniqueType, nil] + # @param context_type [Type, nil] # @param resolved_generic_values [Hash{String => ComplexType}] Added to as types are encountered or resolved # @return [self] def resolve_generics_from_context generics_to_resolve, context_type, resolved_generic_values: {} @@ -70,7 +70,7 @@ def to_rbs (@items.length > 1 ? ')' : '')) end - # @param dst [ComplexType, ComplexType::UniqueType] + # @param dst [Type] # @return [ComplexType] def self_to_type dst object_type_dst = dst.reduce_class_type @@ -165,6 +165,13 @@ def method_missing name, *args, &block super end + # @param name [Symbol] + # @param include_private [Boolean] + def respond_to_missing? name, include_private = false + return false if @items.first.nil? + @items.first.respond_to?(name) || super + end + def to_s map(&:tag).join(', ') end @@ -194,7 +201,7 @@ def desc end # @param api_map [ApiMap] - # @param expected [ComplexType, ComplexType::UniqueType] + # @param expected [Type] # @param situation [:method_call, :return_type, :assignment] # @param rules [Array<:allow_subtype_skew, :allow_empty_params, :allow_reverse_match, :allow_any_match, :allow_undefined, :allow_unresolved_generic, :allow_unmatched_interface>] # @@ -369,7 +376,7 @@ def exclude exclude_types, api_map # @see https://en.wikipedia.org/wiki/Intersection_type # - # @param intersection_type [ComplexType, ComplexType::UniqueType, nil] + # @param intersection_type [Type, nil] # @param api_map [ApiMap] # @return [self, ComplexType::UniqueType] def intersect_with intersection_type, api_map diff --git a/lib/solargraph/complex_type/unique_type.rb b/lib/solargraph/complex_type/unique_type.rb index a86d1c8a1..f04d33aa4 100644 --- a/lib/solargraph/complex_type/unique_type.rb +++ b/lib/solargraph/complex_type/unique_type.rb @@ -127,8 +127,9 @@ def exclude exclude_types, api_map # @see https://en.wikipedia.org/wiki/Intersection_type # - # @param intersection_type [ComplexType, ComplexType::UniqueType, nil] + # @param intersection_type [Type, nil] # @param api_map [ApiMap] + # # @return [self, ComplexType] def intersect_with intersection_type, api_map return self if intersection_type.nil? @@ -389,7 +390,7 @@ def downcast_to_literal_if_possible # @param generics_to_resolve [Enumerable] # @param context_type [ComplexType, UniqueType, nil] - # @param resolved_generic_values [Hash{String => ComplexType, ComplexType::UniqueType}] Added to as types are encountered or resolved + # @param resolved_generic_values [Hash{String => Type}] Added to as types are encountered or resolved # @return [UniqueType, ComplexType] def resolve_generics_from_context generics_to_resolve, context_type, resolved_generic_values: {} if name == ComplexType::GENERIC_TAG_NAME diff --git a/lib/solargraph/pin/base.rb b/lib/solargraph/pin/base.rb index 240d46b44..144258494 100644 --- a/lib/solargraph/pin/base.rb +++ b/lib/solargraph/pin/base.rb @@ -58,7 +58,7 @@ def initialize location: nil, type_location: nil, closure: nil, source: nil, nam @docstring = docstring @directives = directives @combine_priority = combine_priority - # @type [ComplexType, ComplexType::UniqueType, nil] + # @type [Type, nil] @binder = nil assert_source_provided @@ -413,7 +413,7 @@ def comments end # @param generics_to_resolve [Enumerable] - # @param return_type_context [ComplexType, ComplexType::UniqueType, nil] + # @param return_type_context [Type, nil] # @param resolved_generic_values [Hash{String => ComplexType}] # @return [self] def resolve_generics_from_context generics_to_resolve, return_type_context = nil, resolved_generic_values: {} @@ -566,7 +566,7 @@ def deprecated? # provided ApiMap. # # @param api_map [ApiMap] - # @return [ComplexType, ComplexType::UniqueType] + # @return [Type] def typify api_map return_type.qualify(api_map, *(closure&.gates || [''])) end @@ -574,14 +574,14 @@ def typify api_map # Infer the pin's return type via static code analysis. # # @param api_map [ApiMap] - # @return [ComplexType, ComplexType::UniqueType] + # @return [Type] def probe api_map typify api_map end # @deprecated Use #typify and/or #probe instead # @param api_map [ApiMap] - # @return [ComplexType, ComplexType::UniqueType] + # @return [Type] def infer api_map Solargraph.assert_or_log(:pin_infer, 'WARNING: Pin #infer methods are deprecated. Use #typify or #probe instead.') @@ -615,7 +615,7 @@ def realize api_map # the return type and the #proxied? setting, the proxy should be a clone # of the original. # - # @param return_type [ComplexType, ComplexType::UniqueType, nil] + # @param return_type [Type, nil] # @return [self] def proxy return_type result = dup @@ -706,7 +706,7 @@ def equality_fields # @return [Boolean] attr_writer :proxied - # @return [ComplexType, ComplexType::UniqueType, nil] + # @return [Type, nil] attr_writer :return_type attr_writer :docstring, :directives diff --git a/lib/solargraph/pin/base_variable.rb b/lib/solargraph/pin/base_variable.rb index c7945e599..543f85566 100644 --- a/lib/solargraph/pin/base_variable.rb +++ b/lib/solargraph/pin/base_variable.rb @@ -173,7 +173,7 @@ def return_types_from_node parent_node, api_map end # @param api_map [ApiMap] - # @return [ComplexType, ComplexType::UniqueType] + # @return [Type] def probe api_map assignment_types = assignments.flat_map { |node| return_types_from_node(node, api_map) } type_from_assignment = ComplexType.new(assignment_types.flat_map(&:items).uniq) unless assignment_types.empty? @@ -296,9 +296,9 @@ def visible_at? other_closure, other_loc private # @param api_map [ApiMap] - # @param raw_return_type [ComplexType, ComplexType::UniqueType] + # @param raw_return_type [Type] # - # @return [ComplexType, ComplexType::UniqueType] + # @return [Type] def adjust_type api_map, raw_return_type qualified_exclude = exclude_return_type&.qualify(api_map, *(closure&.gates || [''])) minus_exclusions = raw_return_type.exclude qualified_exclude, api_map diff --git a/lib/solargraph/pin/common.rb b/lib/solargraph/pin/common.rb index d52502afe..cdddfc8f4 100644 --- a/lib/solargraph/pin/common.rb +++ b/lib/solargraph/pin/common.rb @@ -10,7 +10,7 @@ module Common # @abstract # @return [void] # @type @closure [Pin::Closure, nil] - # @type @binder [ComplexType, ComplexType::UniqueType, nil] + # @type @binder [Type, nil] # @todo Missed nil violation # @return [Location, nil] @@ -44,7 +44,7 @@ def return_type @return_type ||= ComplexType::UNDEFINED end - # @return [ComplexType, ComplexType::UniqueType] + # @return [Type] def context # Get the static context from the nearest namespace @context ||= find_context @@ -56,7 +56,7 @@ def namespace context.namespace.to_s end - # @return [ComplexType, ComplexType::UniqueType] + # @return [Type] # @sg-ignore https://github.com/castwide/solargraph/pull/1100 def binder @binder || context diff --git a/lib/solargraph/pin/instance_variable.rb b/lib/solargraph/pin/instance_variable.rb index b3c69f09c..3d5a5f67f 100644 --- a/lib/solargraph/pin/instance_variable.rb +++ b/lib/solargraph/pin/instance_variable.rb @@ -4,7 +4,7 @@ module Solargraph module Pin class InstanceVariable < BaseVariable # @sg-ignore Need to add nil check here - # @return [ComplexType, ComplexType::UniqueType] + # @return [Type] def binder # @sg-ignore Need to add nil check here closure.binder diff --git a/lib/solargraph/pin/local_variable.rb b/lib/solargraph/pin/local_variable.rb index 077da21be..897bf68bb 100644 --- a/lib/solargraph/pin/local_variable.rb +++ b/lib/solargraph/pin/local_variable.rb @@ -4,7 +4,7 @@ module Solargraph module Pin class LocalVariable < BaseVariable # @param api_map [ApiMap] - # @return [ComplexType, ComplexType::UniqueType] + # @return [Type] def probe api_map if presence_certain? && return_type&.defined? # flow sensitive typing has already figured out this type diff --git a/lib/solargraph/pin/method.rb b/lib/solargraph/pin/method.rb index f06a563ed..066d62685 100644 --- a/lib/solargraph/pin/method.rb +++ b/lib/solargraph/pin/method.rb @@ -22,7 +22,7 @@ class Method < Callable # @param attribute [Boolean] # @param signatures [::Array, nil] # @param anon_splat [Boolean] - # @param context [ComplexType, ComplexType::UniqueType, nil] + # @param context [Type, nil] # @param [Hash{Symbol => Object}] splat def initialize visibility: :public, explicit: true, block: :undefined, node: nil, attribute: false, signatures: nil, anon_splat: false, context: nil, **splat @@ -580,7 +580,7 @@ def generate_complex_type end # @param api_map [ApiMap] - # @return [ComplexType, ComplexType::UniqueType, nil] + # @return [Type, nil] def see_reference api_map # This should actually be an intersection type # @param ref [YARD::Tags::Tag, YARD::Tags::RefTag] @@ -616,7 +616,7 @@ def typify_from_super api_map # @param ref [String] # @param api_map [ApiMap] - # @return [ComplexType, ComplexType::UniqueType, nil] + # @return [Type, nil] def resolve_reference ref, api_map parts = ref.split(/[.#]/) if parts.first.empty? || parts.one? diff --git a/lib/solargraph/pin/proxy_type.rb b/lib/solargraph/pin/proxy_type.rb index fd37bab85..856583eca 100644 --- a/lib/solargraph/pin/proxy_type.rb +++ b/lib/solargraph/pin/proxy_type.rb @@ -3,9 +3,9 @@ module Solargraph module Pin class ProxyType < Base - # @param return_type [ComplexType, ComplexType::UniqueType] + # @param return_type [Type] # @param gates [Array, nil] Namespaces to try while resolving non-rooted types - # @param binder [ComplexType, ComplexType::UniqueType, nil] + # @param binder [Type, nil] # @param gates [Array, nil] # @param [Hash{Symbol => Object}] splat def initialize return_type: ComplexType::UNDEFINED, binder: nil, gates: nil, **splat @@ -19,9 +19,9 @@ def context @return_type end - # @param context [ComplexType, ComplexType::UniqueType] Used as context for this pin + # @param context [Type] Used as context for this pin # @param closure [Pin::Namespace, nil] Used as the closure for this pin - # @param binder [ComplexType, ComplexType::UniqueType, nil] + # @param binder [Type, nil] # @return [ProxyType] # @param [Hash{Symbol => Object}] kwargs def self.anonymous context, closure: nil, binder: nil, **kwargs diff --git a/lib/solargraph/rbs_map/conversions.rb b/lib/solargraph/rbs_map/conversions.rb index b6295040d..4122fac78 100644 --- a/lib/solargraph/rbs_map/conversions.rb +++ b/lib/solargraph/rbs_map/conversions.rb @@ -271,7 +271,7 @@ def type_parameter_names decl # @param decl [RBS::AST::Declarations::Class] # @return [void] def class_decl_to_pin decl - # @type [Hash{String => ComplexType, ComplexType::UniqueType}] + # @type [Hash{String => Type}] 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 @@ -352,7 +352,7 @@ def module_decl_to_pin decl end # @param fqns [String] - # @param type [ComplexType, ComplexType::UniqueType] + # @param type [Type] # @param comments [String, nil] # @param decl [RBS::AST::Declarations::ClassAlias, # RBS::AST::Declarations::Constant, @@ -838,7 +838,7 @@ def alias_to_pin decl, closure end # @param type [RBS::MethodType, RBS::Types::Block] - # @return [ComplexType, ComplexType::UniqueType] + # @return [Type] def method_type_to_type type if type_aliases.key?(type.type.return_type.to_s) other_type_to_type(type_aliases[type.type.return_type.to_s].type) @@ -851,7 +851,7 @@ def method_type_to_type type # Note: Generally these extend from RBS::Types::Bases::Base, # but not all. # - # @return [ComplexType, ComplexType::UniqueType] + # @return [Type] def other_type_to_type type case type when RBS::Types::Optional diff --git a/lib/solargraph/shell.rb b/lib/solargraph/shell.rb index 8ee13eacf..1b5ad0da7 100755 --- a/lib/solargraph/shell.rb +++ b/lib/solargraph/shell.rb @@ -485,7 +485,7 @@ def pin_description pin desc end - # @param type [ComplexType, ComplexType::UniqueType] + # @param type [Type] # @return [void] def print_type type if options[:rbs] diff --git a/lib/solargraph/source/chain.rb b/lib/solargraph/source/chain.rb index 4bd1b67b6..1173e56c2 100644 --- a/lib/solargraph/source/chain.rb +++ b/lib/solargraph/source/chain.rb @@ -158,7 +158,7 @@ def infer api_map, name_pin, locals # @param api_map [ApiMap] # @param name_pin [Pin::Base] # @param locals [::Array] - # @return [ComplexType, ComplexType::UniqueType] + # @return [Type] def infer_uncached api_map, name_pin, locals pins = define(api_map, name_pin, locals) if pins.empty? @@ -220,9 +220,9 @@ def to_s # @param name_pin [Pin::Base] # @param api_map [ApiMap] # @param locals [::Enumerable] - # @return [ComplexType, ComplexType::UniqueType] + # @return [Type] def infer_from_definitions pins, name_pin, api_map, locals - # @type [::Array] + # @type [::Array] types = [] unresolved_pins = [] # @todo this param tag shouldn't be needed to probe the type @@ -283,8 +283,8 @@ def infer_from_definitions pins, name_pin, api_map, locals type.self_to_type(name_pin.context) end - # @param type [ComplexType, ComplexType::UniqueType] - # @return [ComplexType, ComplexType::UniqueType] + # @param type [Type] + # @return [Type] def maybe_nil type return type if type.undefined? || type.void? || type.nullable? return type unless nullable? diff --git a/lib/solargraph/source/chain/call.rb b/lib/solargraph/source/chain/call.rb index 7d71077c1..4a9e9e0bd 100644 --- a/lib/solargraph/source/chain/call.rb +++ b/lib/solargraph/source/chain/call.rb @@ -193,7 +193,7 @@ def inferred_pins pins, api_map, name_pin, locals # @param pin [Pin::Base] # @param api_map [ApiMap] - # @param context [ComplexType, ComplexType::UniqueType] + # @param context [Type] # @param locals [::Array] # @return [Pin::Base] def process_macro pin, api_map, context, locals @@ -212,7 +212,7 @@ def process_macro pin, api_map, context, locals # @param pin [Pin::Method] # @param api_map [ApiMap] - # @param context [ComplexType, ComplexType::UniqueType] + # @param context [Type] # @param locals [::Array] # @return [Pin::ProxyType] def process_directive pin, api_map, context, locals @@ -228,7 +228,7 @@ def process_directive pin, api_map, context, locals # @param pin [Pin::Base] # @param macro [YARD::Tags::MacroDirective] # @param api_map [ApiMap] - # @param context [ComplexType, ComplexType::UniqueType] + # @param context [Type] # @param locals [::Array] # @return [Pin::ProxyType] def inner_process_macro pin, macro, api_map, context, locals @@ -304,7 +304,7 @@ def yield_pins api_map, name_pin end # @param type [ComplexType] - # @param context [ComplexType, ComplexType::UniqueType] + # @param context [Type] # @return [ComplexType] def with_params type, context return type unless type.to_s.include?('$') @@ -318,7 +318,7 @@ def fix_block_pass end # @param api_map [ApiMap] - # @param context [ComplexType, ComplexType::UniqueType] + # @param context [Type] # @param block_parameter_types [::Array] # @param locals [::Array] # @return [ComplexType, nil] diff --git a/lib/solargraph/type.rb b/lib/solargraph/type.rb index b5b9824f3..2879743fd 100644 --- a/lib/solargraph/type.rb +++ b/lib/solargraph/type.rb @@ -18,15 +18,55 @@ class Type # @!method can_root_name?(name_to_check = nil) # @param name_to_check [String, nil] # @!method key_types - # @return [Array] + # @return [Array] # @!method name - # @return [String] + # @return [String] # @!method parameters_type - # @return [Symbol, nil] + # @return [Symbol, nil] # @!method subtypes - # @return [Array] + # @return [Array] # @!method value_types - # @return [Array] + # @return [Array] + # @!method rooted_tags + # @return [String] + # @!method reduce_class_type + # @return [ComplexType] + # @!method each &block + # @yieldparam t [self] + # @yieldreturn [self] + # @return [Enumerable] + # @!method resolve_generics_from_context generics_to_resolve, context_type, resolved_generic_values: {} + # @param generics_to_resolve [Enumerable] + # @param context_type [ComplexType, ComplexType::UniqueType, nil] + # @param resolved_generic_values [Hash{String => Type}] Added to as types are encountered or resolved + # @return [ComplexType::UniqueType, ComplexType] + # @!method downcast_to_literal_if_possible + # @return [ComplexType::UniqueType] + # @!method generic? + # @!method conforms_to? api_map, expected, situation, rules = [], variance: erased_variance(situation) + # @param api_map [ApiMap] + # @param expected [ComplexType::UniqueType, ComplexType] + # @param situation [:method_call, :assignment, :return_type] + # @param rules [Array<:allow_subtype_skew, :allow_empty_params, :allow_reverse_match, :allow_any_match, :allow_undefined, :allow_unresolved_generic>] + # @param variance [:invariant, :covariant, :contravariant] + # @!method exclude exclude_types, api_map + # @param exclude_types [ComplexType, nil] + # @param api_map [ApiMap] + # @return [ComplexType, self] + # @!method intersect_with intersection_type, api_map + # @param intersection_type [Type, nil] + # @param api_map [ApiMap] + # @return [self, ComplexType] + # @!method self_to_type dst + # @param dst [ComplexType] + # @return [self] + # @!method to_rbs + # @return [String] + # @!method tags + # @return [String] + # @!method nullable? + # @!method items + # @return [Array] # @return [String] def tag @@ -193,7 +233,7 @@ def == other # # @param api_map [ApiMap] The ApiMap that performs qualification # @param context [String] The namespace from which to resolve names - # @return [self, ComplexType, ComplexType::UniqueType] The generated ComplexType + # @return [self, Type] The generated ComplexType def qualify api_map, context = '' transform do |t| next t if t.name == ComplexType::GENERIC_TAG_NAME diff --git a/lib/solargraph/type_checker.rb b/lib/solargraph/type_checker.rb index 724340821..9766f7bef 100644 --- a/lib/solargraph/type_checker.rb +++ b/lib/solargraph/type_checker.rb @@ -50,26 +50,26 @@ def source source_map.source end - # @param inferred [ComplexType, ComplexType::UniqueType] - # @param expected [ComplexType, ComplexType::UniqueType] + # @param inferred [Type] + # @param expected [Type] def return_type_conforms_to? inferred, expected conforms_to?(inferred, expected, :return_type) end - # @param inferred [ComplexType, ComplexType::UniqueType] - # @param expected [ComplexType, ComplexType::UniqueType] + # @param inferred [Type] + # @param expected [Type] def arg_conforms_to? inferred, expected conforms_to?(inferred, expected, :method_call) end - # @param inferred [ComplexType, ComplexType::UniqueType] - # @param expected [ComplexType, ComplexType::UniqueType] + # @param inferred [Type] + # @param expected [Type] def assignment_conforms_to? inferred, expected conforms_to?(inferred, expected, :assignment) end - # @param inferred [ComplexType, ComplexType::UniqueType] - # @param expected [ComplexType, ComplexType::UniqueType] + # @param inferred [Type] + # @param expected [Type] # @param scenario [Symbol] def conforms_to? inferred, expected, scenario rules_arr = [] @@ -525,7 +525,7 @@ def kwarg_problems_for sig, argchain, api_map, closure_pin, locals, location, pi if data.nil? # @todo Some level (strong, I guess) should require the param here else - # @type [ComplexType, ComplexType::UniqueType] + # @type [Type] ptype = data[:qualified] ptype = ptype.self_to_type(pin.context) unless ptype.undefined? From 204c4e0d22b55e5c4a60528fe251021edc7f827a Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 14 Feb 2026 14:33:02 -0700 Subject: [PATCH 4/5] Add runtime assert when losing information --- lib/solargraph/complex_type.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/solargraph/complex_type.rb b/lib/solargraph/complex_type.rb index 24bea8335..65f47d51f 100644 --- a/lib/solargraph/complex_type.rb +++ b/lib/solargraph/complex_type.rb @@ -161,7 +161,13 @@ def namespaces # @param [Array] args def method_missing name, *args, &block return if @items.first.nil? - return @items.first.send(name, *args, &block) if @items.first.respond_to?(name) + if @items.first.respond_to?(name) + if @items.count > 1 + Solargraph.assert_or_log(:complex_type_method_missing, + "ComplexType being used as UniqueType: delegating #{name} to #{self.class} with items #{@items.map(&:to_s).join(', ')}") + end + return @items.first.send(name, *args, &block) + end super end From 3c5b082c5615f8b79425e9a350e589e14bf17690 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Sat, 14 Feb 2026 14:49:19 -0700 Subject: [PATCH 5/5] Add ComplexType#undefined? --- lib/solargraph/complex_type.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/solargraph/complex_type.rb b/lib/solargraph/complex_type.rb index 65f47d51f..f5cfd445a 100644 --- a/lib/solargraph/complex_type.rb +++ b/lib/solargraph/complex_type.rb @@ -171,6 +171,10 @@ def method_missing name, *args, &block super end + def undefined? + @items.all?(&:undefined?) + end + # @param name [Symbol] # @param include_private [Boolean] def respond_to_missing? name, include_private = false