You're Invited:Meet the Socket Team at RSAC and BSidesSF 2026, March 23–26.RSVP
Socket
Book a DemoSign in
Socket

solargraph

Package Overview
Dependencies
Maintainers
1
Versions
211
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

solargraph - rubygems Package Compare versions

Comparing version
0.58.3
to
0.59.0.dev.1
+3
.envrc
# current git branch
SOLARGRAPH_FORCE_VERSION=0.0.1.dev-$(git rev-parse --abbrev-ref HEAD | tr -d '\n' | tr -d '/' | tr -d '-'| tr -d '_')
export SOLARGRAPH_FORCE_VERSION
# frozen_string_literal: true
module Solargraph
class ComplexType
# Checks whether a type can be used in a given situation
class Conformance
# @param api_map [ApiMap]
# @param inferred [ComplexType::UniqueType]
# @param expected [ComplexType::UniqueType]
# @param situation [:method_call, :return_type]
# @param rules [Array<:allow_subtype_skew, :allow_empty_params, :allow_reverse_match,
# :allow_any_match, :allow_undefined, :allow_unresolved_generic,
# :allow_unmatched_interface>]
# @param variance [:invariant, :covariant, :contravariant]
def initialize api_map, inferred, expected,
situation = :method_call, rules = [],
variance: inferred.erased_variance(situation)
@api_map = api_map
@inferred = inferred
@expected = expected
@situation = situation
@rules = rules
@variance = variance
# :nocov:
unless expected.is_a?(UniqueType)
# @sg-ignore This should never happen and the typechecker is angry about it
raise "Expected type must be a UniqueType, got #{expected.class} in #{expected.inspect}"
end
# :nocov:
return if inferred.is_a?(UniqueType)
# :nocov:
# @sg-ignore This should never happen and the typechecker is angry about it
raise "Inferred type must be a UniqueType, got #{inferred.class} in #{inferred.inspect}"
# :nocov:
end
def conforms_to_unique_type?
unless expected.is_a?(UniqueType)
# :nocov:
raise "Expected type must be a UniqueType, got #{expected.class} in #{expected.inspect}"
# :nocov:
end
return true if ignore_interface?
return true if conforms_via_reverse_match?
downcast_inferred = inferred.downcast_to_literal_if_possible
downcast_expected = expected.downcast_to_literal_if_possible
if (downcast_inferred.name != inferred.name) || (downcast_expected.name != expected.name)
return with_new_types(downcast_inferred, downcast_expected).conforms_to_unique_type?
end
if rules.include?(:allow_subtype_skew) && !expected.all_params.empty?
# parameters are not considered in this case
return with_new_types(inferred, expected.erase_parameters).conforms_to_unique_type?
end
return with_new_types(inferred.erase_parameters, expected).conforms_to_unique_type? if only_inferred_parameters?
return conforms_via_stripped_expected_parameters? if can_strip_expected_parameters?
return true if inferred == expected
return false unless erased_type_conforms?
return true if inferred.all_params.empty? && rules.include?(:allow_empty_params)
# at this point we know the erased type is fine - time to look at parameters
# there's an implicit 'any' on the expectation parameters
# if there are none specified
return true if expected.all_params.empty?
return false unless key_types_conform?
subtypes_conform?
end
private
def only_inferred_parameters?
!expected.parameters? && inferred.parameters?
end
def conforms_via_stripped_expected_parameters?
with_new_types(inferred, expected.erase_parameters).conforms_to_unique_type?
end
def ignore_interface?
(expected.any?(&:interface?) && rules.include?(:allow_unmatched_interface)) ||
(inferred.interface? && rules.include?(:allow_unmatched_interface))
end
def can_strip_expected_parameters?
expected.parameters? && !inferred.parameters? && rules.include?(:allow_empty_params)
end
def conforms_via_reverse_match?
return false unless rules.include? :allow_reverse_match
expected.conforms_to?(api_map, inferred, situation,
rules - [:allow_reverse_match],
variance: variance)
end
def erased_type_conforms?
case variance
when :invariant
return false unless inferred.name == expected.name
when :covariant
# covariant: we can pass in a more specific type
# we contain the expected mix-in, or we have a more specific type
return false unless api_map.type_include?(inferred.name, expected.name) ||
api_map.super_and_sub?(expected.name, inferred.name) ||
inferred.name == expected.name
when :contravariant
# contravariant: we can pass in a more general type
# we contain the expected mix-in, or we have a more general type
return false unless api_map.type_include?(inferred.name, expected.name) ||
api_map.super_and_sub?(inferred.name, expected.name) ||
inferred.name == expected.name
else
# :nocov:
raise "Unknown variance: #{variance.inspect}"
# :nocov:
end
true
end
def key_types_conform?
return true if expected.key_types.empty?
return false if inferred.key_types.empty?
unless ComplexType.new(inferred.key_types).conforms_to?(api_map,
ComplexType.new(expected.key_types),
situation,
rules,
variance: inferred.parameter_variance(situation))
return false
end
true
end
def subtypes_conform?
return true if expected.subtypes.empty?
return true if expected.subtypes.any?(&:undefined?) && rules.include?(:allow_undefined)
return true if inferred.subtypes.any?(&:undefined?) && rules.include?(:allow_undefined)
return true if inferred.subtypes.all?(&:generic?) && rules.include?(:allow_unresolved_generic)
return true if expected.subtypes.all?(&:generic?) && rules.include?(:allow_unresolved_generic)
return false if inferred.subtypes.empty?
ComplexType.new(inferred.subtypes).conforms_to?(api_map,
ComplexType.new(expected.subtypes),
situation,
rules,
variance: inferred.parameter_variance(situation))
end
# @return [self]
# @param inferred [ComplexType::UniqueType]
# @param expected [ComplexType::UniqueType]
def with_new_types inferred, expected
self.class.new(api_map, inferred, expected, situation, rules, variance: variance)
end
attr_reader :api_map, :inferred, :expected, :situation, :rules, :variance
end
end
end
# frozen_string_literal: true
module Solargraph
module Parser
module ParserGem
module NodeProcessors
class OrNode < Parser::NodeProcessor::Base
include ParserGem::NodeMethods
def process
process_children
FlowSensitiveTyping.new(locals,
ivars,
enclosing_breakable_pin,
enclosing_compound_statement_pin).process_or(node)
end
end
end
end
end
end
# frozen_string_literal: true
module Solargraph
module Parser
module ParserGem
module NodeProcessors
class WhenNode < Parser::NodeProcessor::Base
include ParserGem::NodeMethods
def process
pins.push Solargraph::Pin::CompoundStatement.new(
location: get_node_location(node),
closure: region.closure,
node: node,
source: :parser,
)
process_children
end
end
end
end
end
end
module Solargraph
module Pin
# A series of statements where if a given statement executes, /all
# of the previous statements in the sequence must have executed as
# well/. In other words, the statements are run from the top in
# sequence, until interrupted by something like a
# return/break/next/raise/etc.
#
# This mix-in is used in flow sensitive typing to determine how
# far we can assume a given assertion about a type can be trusted
# to be true.
#
# Some examples in Ruby:
#
# * Bodies of methods and Ruby blocks
# * Branches of conditionals and loops - if/elsif/else,
# unless/else, when, until, ||=, ?:, switch/case/else
# * The body of begin-end/try/rescue/ensure statements
#
# Compare/contrast with:
#
# * Scope - a sequence where variables declared are not available
# after the end of the scope. Note that this is not necessarily
# true for a compound statement.
# * Compound statement - synonym
# * Block - in Ruby this has a special meaning (a closure passed to a method), but
# in general parlance this is also a synonym.
# * Closure - a sequence which is also a scope
# * Namespace - a named sequence which is also a scope and a
# closure
#
# See:
# https://cse.buffalo.edu/~regan/cse305/RubyBNF.pdf
# https://ruby-doc.org/docs/ruby-doc-bundle/Manual/man-1.4/syntax.html
# https://en.wikipedia.org/wiki/Block_(programming)
#
# Note:
#
# Just because statement #1 in a sequence is executed, it doesn't
# mean that future ones will. Consider the effect of
# break/next/return/raise/etc. on control flow.
class CompoundStatement < Pin::Base
attr_reader :node
# @param receiver [Parser::AST::Node, nil]
# @param node [Parser::AST::Node, nil]
# @param context [ComplexType, nil]
# @param args [::Array<Parameter>]
def initialize node: nil, **splat
super(**splat)
@node = node
end
end
end
end
# frozen_string_literal: true
require 'rubygems'
require 'bundler'
module Solargraph
class Workspace
# Manages determining which gemspecs are available in a workspace
class Gemspecs
include Logging
attr_reader :directory, :preferences
# @param directory [String, nil] If nil, assume no bundle is present
# @param preferences [Array<Gem::Specification>]
def initialize directory, preferences: []
# @todo an issue with both external bundles and the potential
# preferences feature is that bundler gives you a 'clean'
# rubygems environment with only the specified versions
# installed. Possible alternatives:
#
# *) prompt the user to run solargraph outside of bundler
# and treat all bundles as external
# *) reinstall the needed gems dynamically each time
# *) manipulate the rubygems/bundler environment
@directory = directory && File.absolute_path(directory)
# @todo implement preferences as a config-exposed feature
@preferences = preferences
end
# Take the path given to a 'require' statement in a source file
# and return the Gem::Specifications which will be brought into
# scope with it, so we can load pins for them.
#
# @param require [String] The string sent to 'require' in the code to resolve, e.g. 'rails', 'bundler/require'
# @return [::Array<Gem::Specification>, nil]
def resolve_require require
return nil if require.empty?
# This is added in the parser when it sees 'Bundler.require' -
# see https://bundler.io/guides/bundler_setup.html '
#
# @todo handle different arguments to Bundler.require
return auto_required_gemspecs_from_bundler if require == 'bundler/require'
# Determine gem name based on the require path
file = "lib/#{require}.rb"
spec_with_path = Gem::Specification.find_by_path(file)
all_gemspecs = all_gemspecs_from_bundle
gem_names_to_try = [
spec_with_path&.name,
require.tr('/', '-'),
require.split('/').first
].compact.uniq
# @param gem_name [String]
gem_names_to_try.each do |gem_name|
# @sg-ignore Unresolved call to == on Boolean
gemspec = all_gemspecs.find { |gemspec| gemspec.name == gem_name }
# @sg-ignore flow sensitive typing should be able to handle redefinition
return [gemspec_or_preference(gemspec)] if gemspec
begin
gemspec = Gem::Specification.find_by_name(gem_name)
# @sg-ignore flow sensitive typing should be able to handle redefinition
return [gemspec_or_preference(gemspec)] if gemspec
rescue Gem::MissingSpecError
logger.debug do
"Require path #{require} could not be resolved to a gem via find_by_path or guess of #{gem_name}"
end
end
# look ourselves just in case this is hanging out somewhere
# that find_by_path doesn't index
gemspec = all_gemspecs.find do |spec|
spec = to_gem_specification(spec) unless spec.respond_to?(:files)
# @sg-ignore Translate to something flow sensitive typing understands
spec&.files&.any? { |gemspec_file| file == gemspec_file }
end
# @sg-ignore flow sensitive typing should be able to handle redefinition
return [gemspec_or_preference(gemspec)] if gemspec
end
nil
end
# @param stdlib_name [String]
#
# @return [Array<String>]
def stdlib_dependencies stdlib_name
deps = RbsMap::StdlibMap.stdlib_dependencies(stdlib_name, nil) || []
deps.map { |dep| dep['name'] }.compact
end
# @param name [String]
# @param version [String, nil]
# @param out [IO, nil] output stream for logging
#
# @return [Gem::Specification, nil]
def find_gem name, version = nil, out: $stderr
# @sg-ignore flow sensitive typing should be able to handle redefinition
specish = all_gemspecs_from_bundle.find { |specish| specish.name == name && specish.version == version }
return to_gem_specification specish if specish
# @sg-ignore flow sensitive typing should be able to handle redefinition
specish = all_gemspecs_from_bundle.find { |specish| specish.name == name }
# @sg-ignore flow sensitive typing needs to create separate ranges for postfix if
return to_gem_specification specish if specish
resolve_gem_ignoring_local_bundle name, version, out: out
end
# @param gemspec [Gem::Specification]
# @param out[IO, nil] output stream for logging
#
# @return [Array<Gem::Specification>]
def fetch_dependencies gemspec, out: $stderr
gemspecs = all_gemspecs_from_bundle
# @type [Hash{String => Gem::Specification}]
deps_so_far = {}
# @param runtime_dep [Gem::Dependency]
# @param deps [Hash{String => Gem::Specification}]
gem_dep_gemspecs = only_runtime_dependencies(gemspec).each_with_object(deps_so_far) do |runtime_dep, deps|
dep = find_gem(runtime_dep.name, runtime_dep.requirement)
next unless dep
fetch_dependencies(dep, out: out).each { |sub_dep| deps[sub_dep.name] ||= sub_dep }
deps[dep.name] ||= dep
end
# RBS tracks implicit dependencies, like how the YAML standard
# library implies pulling in the psych library.
stdlib_deps = RbsMap::StdlibMap.stdlib_dependencies(gemspec.name, gemspec.version) || []
stdlib_dep_gemspecs = stdlib_deps.map { |dep| find_gem(dep['name'], dep['version']) }.compact
(gem_dep_gemspecs.values.compact + stdlib_dep_gemspecs).uniq(&:name)
end
# Returns all gemspecs directly depended on by this workspace's
# bundle (does not include transitive dependencies).
#
# @return [Array<Gem::Specification, Bundler::LazySpecification, Bundler::StubSpecification>]
def all_gemspecs_from_bundle
return [] unless directory
@all_gemspecs_from_bundle ||=
if in_this_bundle?
all_gemspecs_from_this_bundle
else
all_gemspecs_from_external_bundle
end
end
# @return [Hash{Gem::Specification, Bundler::LazySpecification, Bundler::StubSpecification => Gem::Specification}]
def self.gem_specification_cache
@gem_specification_cache ||= {}
end
private
# @param specish [Gem::Specification, Bundler::LazySpecification, Bundler::StubSpecification]
#
# @return [Gem::Specification, nil]
# @sg-ignore flow sensitive typing needs better handling of ||= on lvars
def to_gem_specification specish
# print time including milliseconds
self.class.gem_specification_cache[specish] ||= case specish
when Gem::Specification
specish
when Bundler::LazySpecification
# materializing didn't work. Let's look in the local
# rubygems without bundler's help
resolve_gem_ignoring_local_bundle specish.name,
specish.version
when Bundler::StubSpecification
# turns a Bundler::StubSpecification into a
# Gem::StubSpecification if we can
if specish.respond_to?(:stub)
# @sg-ignore flow sensitive typing ought to be able to handle 'when ClassName'
to_gem_specification specish.stub
else
# A Bundler::StubSpecification is a Bundler::
# RemoteSpecification which ought to proxy a Gem::
# Specification
specish
end
# @sg-ignore Unresolved constant Gem::StubSpecification
when Gem::StubSpecification
# @sg-ignore flow sensitive typing ought to be able to handle 'when ClassName'
specish.to_spec
else
raise "Unexpected type while resolving gem: #{specish.class}"
end
end
# @param command [String] The expression to evaluate in the external bundle
# @sg-ignore Need a JSON type
# @yield [undefined, nil]
def query_external_bundle command
Solargraph.with_clean_env do
cmd = [
'ruby', '-e',
"require 'bundler'; require 'json'; Dir.chdir('#{directory}') { puts begin; #{command}; end.to_json }"
]
o, e, s = Open3.capture3(*cmd)
if s.success?
Solargraph.logger.debug "External bundle: #{o}"
o && !o.empty? ? JSON.parse(o.split("\n").last) : nil
else
Solargraph.logger.warn e
raise BundleNotFoundError, "Failed to load gems from bundle at #{directory}"
end
end
end
# @sg-ignore need boolish support for ? methods
def in_this_bundle?
Bundler.definition&.lockfile&.to_s&.start_with?(directory)
end
# @return [Array<Gem::Specification, Bundler::LazySpecification, Bundler::StubSpecification>]
def all_gemspecs_from_this_bundle
# Find only the gems bundler is now using
specish_objects = Bundler.definition.locked_gems.specs
if specish_objects.first.respond_to?(:materialize_for_installation)
specish_objects = specish_objects.map(&:materialize_for_installation)
end
specish_objects.map do |specish|
if specish.respond_to?(:name) && specish.respond_to?(:version) && specish.respond_to?(:gem_dir)
# duck type is good enough for outside uses!
specish
else
to_gem_specification(specish)
end
end.compact
end
# @return [Array<Gem::Specification, Bundler::LazySpecification, Bundler::StubSpecification>]
def auto_required_gemspecs_from_bundler
return [] unless directory
logger.info 'Fetching gemspecs autorequired from Bundler (bundler/require)'
@auto_required_gemspecs_from_bundler ||=
if in_this_bundle?
auto_required_gemspecs_from_this_bundle
else
auto_required_gemspecs_from_external_bundle
end
end
# @return [Array<Gem::Specification, Bundler::LazySpecification, Bundler::StubSpecification>]
def auto_required_gemspecs_from_this_bundle
# Adapted from require() in lib/bundler/runtime.rb
dep_names = Bundler.definition.dependencies.select do |dep|
dep.groups.include?(:default) && dep.should_include?
end.map(&:name)
all_gemspecs_from_bundle.select { |gemspec| dep_names.include?(gemspec.name) }
end
# @return [Array<Gem::Specification, Bundler::LazySpecification, Bundler::StubSpecification>]
def auto_required_gemspecs_from_external_bundle
@auto_required_gemspecs_from_external_bundle ||=
begin
logger.info 'Fetching auto-required gemspecs from Bundler (bundler/require)'
command =
'Bundler.definition.dependencies' \
'.select { |dep| dep.groups.include?(:default) && dep.should_include? }' \
'.map(&:name)'
# @sg-ignore
# @type [Array<String>]
dep_names = query_external_bundle command
all_gemspecs_from_bundle.select { |gemspec| dep_names.include?(gemspec.name) }
end
end
# @param gemspec [Gem::Specification]
# @return [Array<Gem::Dependency>]
def only_runtime_dependencies gemspec
unless gemspec.respond_to?(:dependencies) && gemspec.respond_to?(:development_dependencies)
gemspec = to_gem_specification(gemspec)
end
return [] if gemspec.nil?
gemspec.dependencies - gemspec.development_dependencies
end
# @todo Should this be using Gem::SpecFetcher and pull them automatically?
#
# @param name [String]
# @param version_or_requirement [String, nil]
# @param out [IO, nil] output stream for logging
#
# @return [Gem::Specification, nil]
def resolve_gem_ignoring_local_bundle name, version_or_requirement = nil, out: $stderr
Gem::Specification.find_by_name(name, version_or_requirement)
rescue Gem::MissingSpecError
begin
Gem::Specification.find_by_name(name)
rescue Gem::MissingSpecError
stdlibmap = RbsMap::StdlibMap.new(name)
unless stdlibmap.resolved?
gem_desc = name
gem_desc += ":#{version_or_requirement}" if version_or_requirement
out&.puts "Please install the gem #{gem_desc} in Solargraph's Ruby environment"
end
nil # either not here or in stdlib
end
end
# @sg-ignore flow sensitive typing needs better handling of ||= on lvars
# @return [Array<Gem::Specification>]
def all_gemspecs_from_external_bundle
@all_gemspecs_from_external_bundle ||=
begin
logger.info 'Fetching gemspecs required from external bundle'
command = 'specish_objects = Bundler.definition.locked_gems&.specs; ' \
'if specish_objects.first.respond_to?(:materialize_for_installation);' \
'specish_objects = specish_objects.map(&:materialize_for_installation);' \
'end;' \
'specish_objects.map { |specish| [specish.name, specish.version] }'
# @type [Array<Gem::Specification>]
query_external_bundle(command).map do |name, version|
resolve_gem_ignoring_local_bundle(name, version)
end.compact
rescue Solargraph::BundleNotFoundError => e
Solargraph.logger.info e.message
# @sg-ignore Need to add nil check here
Solargraph.logger.debug e.backtrace.join("\n")
[]
end
end
# @return [Hash{String => Gem::Specification}]
def preference_map
@preference_map ||= preferences.to_h { |gemspec| [gemspec.name, gemspec] }
end
# @param gemspec [Gem::Specification]
#
# @return [Gem::Specification]
def gemspec_or_preference gemspec
return gemspec unless preference_map.key?(gemspec.name)
return gemspec if gemspec.version == preference_map[gemspec.name].version
change_gemspec_version gemspec, preference_map[gemspec.name].version
end
# @param gemspec [Gem::Specification]
# @param version [String]
# @return [Gem::Specification]
def change_gemspec_version gemspec, version
Gem::Specification.find_by_name(gemspec.name, "= #{version}")
rescue Gem::MissingSpecError
Solargraph.logger.info "Gem #{gemspec.name} version #{version.inspect} not found. " \
"Using #{gemspec.version} instead"
gemspec
end
end
end
end
# <!-- rdoc-file=lib/rubygems/dependency.rb -->
# The Dependency class holds a Gem name and a Gem::Requirement.
#
class Gem::Dependency
@name: untyped
@requirement: untyped
@type: untyped
@prerelease: untyped
@version_requirements: untyped
@version_requirement: untyped
# <!-- rdoc-file=lib/rubygems/dependency.rb -->
# Valid dependency types.
#
TYPES: ::Array[:development | :runtime]
# <!-- rdoc-file=lib/rubygems/dependency.rb -->
# Dependency name or regular expression.
#
attr_accessor name: untyped
# <!-- rdoc-file=lib/rubygems/dependency.rb -->
# Allows you to force this dependency to be a prerelease.
#
attr_writer prerelease: untyped
# <!--
# rdoc-file=lib/rubygems/dependency.rb
# - new(name, *requirements)
# -->
# Constructs a dependency with `name` and `requirements`. The last argument can
# optionally be the dependency type, which defaults to `:runtime`.
#
def initialize: (untyped name, *untyped requirements) -> void
def hash: () -> untyped
def inspect: () -> untyped
# <!--
# rdoc-file=lib/rubygems/dependency.rb
# - prerelease?()
# -->
# Does this dependency require a prerelease?
#
def prerelease?: () -> untyped
# <!--
# rdoc-file=lib/rubygems/dependency.rb
# - latest_version?()
# -->
# Is this dependency simply asking for the latest version of a gem?
#
def latest_version?: () -> untyped
def pretty_print: (untyped q) -> untyped
# <!--
# rdoc-file=lib/rubygems/dependency.rb
# - requirement()
# -->
# What does this dependency require?
#
def requirement: () -> untyped
# <!--
# rdoc-file=lib/rubygems/dependency.rb
# - requirements_list()
# -->
#
def requirements_list: () -> untyped
def to_s: () -> ::String
# <!--
# rdoc-file=lib/rubygems/dependency.rb
# - type()
# -->
# Dependency type.
#
def type: () -> untyped
# <!--
# rdoc-file=lib/rubygems/dependency.rb
# - runtime?()
# -->
#
def runtime?: () -> untyped
def ==: (untyped other) -> untyped
# <!--
# rdoc-file=lib/rubygems/dependency.rb
# - <=>(other)
# -->
# Dependencies are ordered by name.
#
def <=>: (untyped other) -> untyped
# <!--
# rdoc-file=lib/rubygems/dependency.rb
# - =~(other)
# -->
# Uses this dependency as a pattern to compare to `other`. This dependency will
# match if the name matches the other's name, and other has only an equal
# version requirement that satisfies this dependency.
#
def =~: (untyped other) -> (nil | false | untyped)
# <!--
# rdoc-file=lib/rubygems/dependency.rb
# - ===(other)
# -->
#
alias === =~
# <!--
# rdoc-file=lib/rubygems/dependency.rb
# - dep.match? name => true or false
# - dep.match? name, version => true or false
# - dep.match? spec => true or false
# -->
# Does this dependency match the specification described by `name` and `version`
# or match `spec`?
#
# NOTE: Unlike #matches_spec? this method does not return true when the version
# is a prerelease version unless this is a prerelease dependency.
#
def match?: (untyped obj, ?untyped? version, ?bool allow_prerelease) -> (false | true | untyped)
# <!--
# rdoc-file=lib/rubygems/dependency.rb
# - matches_spec?(spec)
# -->
# Does this dependency match `spec`?
#
# NOTE: This is not a convenience method. Unlike #match? this method returns
# true when `spec` is a prerelease version even if this dependency is not a
# prerelease dependency.
#
def matches_spec?: (untyped spec) -> (false | true | untyped)
# <!--
# rdoc-file=lib/rubygems/dependency.rb
# - merge(other)
# -->
# Merges the requirements of `other` into this dependency
#
def merge: (untyped other) -> untyped
# <!--
# rdoc-file=lib/rubygems/dependency.rb
# - matching_specs(platform_only = false)
# -->
#
def matching_specs: (?bool platform_only) -> untyped
# <!--
# rdoc-file=lib/rubygems/dependency.rb
# - specific?()
# -->
# True if the dependency will not always match the latest version.
#
def specific?: () -> untyped
# <!--
# rdoc-file=lib/rubygems/dependency.rb
# - to_specs()
# -->
#
def to_specs: () -> untyped
# <!--
# rdoc-file=lib/rubygems/dependency.rb
# - to_spec()
# -->
#
def to_spec: () -> untyped
# <!--
# rdoc-file=lib/rubygems/dependency.rb
# - identity()
# -->
#
def identity: () -> (:complete | :abs_latest | :latest | :released)
def encode_with: (untyped coder) -> untyped
end
module Diff
end
module Diff::LCS
def self.LCS: (Array[String], Array[String]) -> Array[String]
| (String, String) -> Array[String]
def self.diff: (Array[String], Array[String]) -> Array[Array[String]]
| (String, String) -> Array[Array[Diff::LCS::Change]]
def self.patch!: (Array[String], Array[String]) -> String
end
+4
-5

@@ -11,6 +11,6 @@ ---

pull_request:
branches: [ master ]
branches: ['*']
push:
branches:
- 'main'
- 'master'
tags:

@@ -35,9 +35,8 @@ - 'v*'

ruby-version: 3.4
bundler: latest
bundler-cache: true
cache-version: 2025-06-06
cache-version: 2026-01-11
- name: Update to best available RBS
run: |
bundle update rbs # use latest available for this Ruby version
bundle update --pre rbs # use latest available for this Ruby version

@@ -44,0 +43,0 @@ - name: Restore cache of gem annotations

@@ -12,3 +12,3 @@ ---

pull_request:
branches: [master]
branches: ['*']

@@ -27,3 +27,3 @@ permissions:

with:
ruby-version: 3.4
ruby-version: 3.4 # keep same as typecheck.yml
bundler-cache: true

@@ -39,3 +39,3 @@ - uses: awalsh128/cache-apt-pkgs-action@latest

bundle install
bundle update rbs
bundle update --pre rbs
- name: Configure to use plugins

@@ -49,3 +49,3 @@ run: |

- name: Ensure typechecking still works
run: bundle exec solargraph typecheck --level typed
run: bundle exec solargraph typecheck --level strong
- name: Ensure specs still run

@@ -61,3 +61,5 @@ run: bundle exec rake spec

with:
ruby-version: 3.4
ruby-version: 3.4 # keep same as typecheck.yml
# See https://github.com/castwide/solargraph/actions/runs/19000135777/job/54265647107?pr=1119
rubygems: latest
bundler-cache: false

@@ -72,3 +74,3 @@ - uses: awalsh128/cache-apt-pkgs-action@latest

bundle install
bundle update rbs
bundle update --pre rbs
- name: Configure to use plugins

@@ -81,3 +83,3 @@ run: |

- name: Ensure typechecking still works
run: bundle exec solargraph typecheck --level typed
run: bundle exec solargraph typecheck --level strong
- name: Ensure specs still run

@@ -93,3 +95,3 @@ run: bundle exec rake spec

with:
ruby-version: 3.4
ruby-version: 3.4 # keep same as typecheck.yml
bundler-cache: false

@@ -104,3 +106,3 @@ - uses: awalsh128/cache-apt-pkgs-action@latest

bundle install
bundle update rbs
bundle update --pre rbs
- name: Configure to use plugins

@@ -113,3 +115,3 @@ run: |

- name: Ensure typechecking still works
run: bundle exec solargraph typecheck --level typed
run: bundle exec solargraph typecheck --level strong
- name: Ensure specs still run

@@ -129,12 +131,9 @@ run: bundle exec rake spec

cd ..
# git clone https://github.com/lekemula/solargraph-rspec.git
git clone https://github.com/lekemula/solargraph-rspec.git
# pending https://github.com/lekemula/solargraph-rspec/pull/30
git clone https://github.com/apiology/solargraph-rspec.git
cd solargraph-rspec
git checkout reset_closures
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.2'
ruby-version: 3.4
rubygems: latest

@@ -144,21 +143,21 @@ bundler-cache: false

run: |
set -x
set -x
cd ../solargraph-rspec
echo "gem 'solargraph', path: '../solargraph'" >> Gemfile
bundle config path ${{ env.BUNDLE_PATH }}
bundle install --jobs 4 --retry 3
bundle exec appraisal install
# @todo some kind of appraisal/bundle conflict?
# https://github.com/castwide/solargraph/actions/runs/19038710934/job/54369767122?pr=1116
# /home/runner/work/solargraph/solargraph-rspec/vendor/bundle/ruby/3.1.0/gems/bundler-2.6.9/lib/bundler/runtime.rb:317:in
# `check_for_activated_spec!': You have already activated date
# 3.5.0, but your Gemfile requires date 3.4.1. Prepending
# `bundle exec` to your command may solve
# this. (Gem::LoadError)
bundle exec appraisal update date
# For some reason on ruby 3.1 it defaults to an old version: 1.3.2
# https://github.com/lekemula/solargraph-rspec/actions/runs/17814581205/job/50645370316?pr=22
# We update manually to the latest
bundle exec appraisal update rspec-rails
cd ../solargraph-rspec
echo "gem 'solargraph', path: '../solargraph'" >> Gemfile
bundle config path ${{ env.BUNDLE_PATH }}
bundle install --jobs 4 --retry 3
bundle exec appraisal install
# @todo some kind of appraisal/bundle conflict?
# https://github.com/castwide/solargraph/actions/runs/19038710934/job/54369767122?pr=1116
# /home/runner/work/solargraph/solargraph-rspec/vendor/bundle/ruby/3.1.0/gems/bundler-2.6.9/lib/bundler/runtime.rb:317:in
# `check_for_activated_spec!': You have already activated date
# 3.5.0, but your Gemfile requires date 3.4.1. Prepending
# `bundle exec` to your command may solve
# this. (Gem::LoadError)
bundle exec appraisal update date
# For some reason on ruby 3.1 it defaults to an old version: 1.3.2
# https://github.com/lekemula/solargraph-rspec/actions/runs/17814581205/job/50645370316?pr=22
# We update manually to the latest
bundle exec appraisal update rspec-rails
- name: Configure .solargraph.yml

@@ -168,6 +167,9 @@ run: |

cp .solargraph.yml.example .solargraph.yml
- name: Solargraph generate RSpec gems YARD and RBS pins
- name: Solargraph generate RSpec gems YARD pins
run: |
cd ../solargraph-rspec
bundle exec appraisal rbs collection update
# solargraph-rspec's specs don't pass a workspace, so it
# doesn't know where to look for the RBS collection - let's
# not load one so that the solargraph gems command below works
rspec_gems=$(bundle exec appraisal ruby -r './lib/solargraph-rspec' -e 'puts Solargraph::Rspec::Gems.gem_names.join(" ")' 2>/dev/null | tail -n1)

@@ -194,5 +196,6 @@ bundle exec appraisal solargraph gems $rspec_gems

# solargraph-rails supports Ruby 3.0+
# This job uses 3.2 due to a problem compiling sqlite3 in earlier versions
ruby-version: '3.2'
ruby-version: '3.0'
bundler-cache: false
# https://github.com/apiology/solargraph/actions/runs/19400815835/job/55508092473?pr=17
rubygems: latest
bundler: latest

@@ -209,3 +212,3 @@ env:

bundle install
bundle update rbs
bundle update --pre rbs
RAILS_DIR="$(pwd)/spec/rails7"

@@ -212,0 +215,0 @@ export RAILS_DIR

@@ -14,3 +14,3 @@ # This workflow uses actions that are not certified by GitHub.

pull_request:
branches: [ master ]
branches: ['*']

@@ -25,16 +25,45 @@ permissions:

matrix:
ruby-version: ['3.0', '3.1', '3.2', '3.3', '3.4', '4.0']
rbs-version: ['3.6.1', '3.9.5', '4.0.0.dev.4']
ruby-version: ['3.0', '3.1', '3.2', '3.3', '3.4', '4.0', 'head']
rbs-version: ['3.6.1', '3.8.1', '3.9.5', '3.10.0', '4.0.0.dev.5']
# Ruby 3.0 doesn't work with RBS 3.9.4 or 4.0.0.dev.4
exclude:
# only include the 3.0 variants we include later
- ruby-version: '3.0'
rbs-version: '3.9.5'
- ruby-version: '3.0'
rbs-version: '4.0.0.dev.4'
# Missing require in 'rbs collection update' - hopefully
# fixed in next RBS release
# only include the 3.1 variants we include later
- ruby-version: '3.1'
# only include the 3.2 variants we include later
- ruby-version: '3.2'
# only include the 3.3 variants we include later
- ruby-version: '3.3'
# only include the 3.4 variants we include later
- ruby-version: '3.4'
# only include the 4.0 variants we include later
- ruby-version: '4.0'
# Don't exclude 'head' - let's test all RBS versions we
# can there.
#
#
# Just exclude some odd-ball compatibility issues we can't
# work around:
#
# https://github.com/castwide/solargraph/actions/runs/20627923548/job/59241444380?pr=1102
- ruby-version: 'head'
rbs-version: '3.6.1'
- ruby-version: 'head'
rbs-version: '3.8.1'
include:
- ruby-version: '3.0'
rbs-version: '3.6.1'
- ruby-version: '3.1'
rbs-version: '3.6.1'
- ruby-version: '3.2'
rbs-version: '3.8.1'
- ruby-version: '3.3'
rbs-version: '3.9.5'
- ruby-version: '3.3'
rbs-version: '3.10.0'
- ruby-version: '3.4'
rbs-version: '4.0.0.dev.5'
- ruby-version: '4.0'
rbs-version: '4.0.0.dev.4'
rbs-version: '4.0.0.dev.5'
steps:

@@ -63,7 +92,4 @@ - uses: actions/checkout@v3

bundle update rbs # use latest available for this Ruby version
bundle list
bundle exec solargraph pin 'Bundler::Dsl#source'
- name: Update types
run: |
bundle exec rbs collection update
run: bundle exec rbs collection update
- name: Run tests

@@ -83,5 +109,11 @@ run: bundle exec rake spec

ruby-version: '3.4'
# see https://github.com/castwide/solargraph/actions/runs/19391419903/job/55485410493?pr=1119
#
# match version in Gemfile.lock and use same version below
bundler: 2.5.23
bundler-cache: false
- name: Install gems
run: bundle install
- name: Update types
run: bundle exec rbs collection update
- name: Run tests

@@ -88,0 +120,0 @@ run: bundle exec rake spec

@@ -14,3 +14,3 @@ # This workflow uses actions that are not certified by GitHub.

pull_request:
branches: [ master ]
branches: ['*']

@@ -36,3 +36,3 @@ permissions:

bundle install
bundle update rbs # use latest available for this Ruby version
bundle update --pre rbs # use latest available for this Ruby version
- name: Install gem types

@@ -39,0 +39,0 @@ run: bundle exec rbs collection install

@@ -17,2 +17,1 @@ /.gem_rbs_collection

/.rspec-local
vendor/cache

@@ -83,3 +83,2 @@ # This configuration was generated by

Exclude:
- 'lib/solargraph/doc_map.rb'
- 'lib/solargraph/language_server/message/initialize.rb'

@@ -111,3 +110,2 @@ - 'lib/solargraph/pin/delegated_method.rb'

- 'Rakefile'
- 'lib/solargraph/source/encoding_fixes.rb'
- 'solargraph.gemspec'

@@ -205,3 +203,2 @@

Exclude:
- 'lib/solargraph/api_map.rb'
- 'lib/solargraph/language_server/host/dispatch.rb'

@@ -222,7 +219,2 @@ - 'lib/solargraph/source.rb'

# This cop supports safe autocorrection (--autocorrect).
Layout/SpaceAroundKeyword:
Exclude:
- 'spec/rbs_map/conversions_spec.rb'
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: AllowForAlignment, EnforcedStyleForExponentOperator, EnforcedStyleForRationalLiterals.

@@ -369,3 +361,2 @@ # SupportedStylesForExponentOperator: space, no_space

Exclude:
- 'lib/solargraph.rb'
- 'lib/solargraph/parser/parser_gem/node_chainer.rb'

@@ -440,8 +431,2 @@ - 'spec/language_server/host_spec.rb'

# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: ContextCreatingMethods, MethodCreatingMethods.
Lint/UselessAccessModifier:
Exclude:
- 'lib/solargraph/api_map.rb'
# This cop supports safe autocorrection (--autocorrect).
Lint/UselessAssignment:

@@ -466,3 +451,3 @@ Enabled: false

Metrics/BlockLength:
Max: 56
Max: 57

@@ -478,2 +463,3 @@ # Configuration parameters: CountBlocks, CountModifierForms.

- 'lib/solargraph/language_server/host.rb'
- 'lib/solargraph/pin/method.rb'
- 'lib/solargraph/rbs_map/conversions.rb'

@@ -498,6 +484,6 @@ - 'lib/solargraph/type_checker.rb'

- 'lib/solargraph/api_map.rb'
- 'lib/solargraph/parser/node_processor.rb'
- 'lib/solargraph/pin/callable.rb'
- 'lib/solargraph/type_checker.rb'
- 'lib/solargraph/yard_map/mapper/to_method.rb'
- 'lib/solargraph/yard_map/to_method.rb'

@@ -534,3 +520,8 @@ # Configuration parameters: AllowedMethods, AllowedPatterns, Max.

Naming/MethodParameterName:
Enabled: false
Exclude:
- 'lib/solargraph/parser/parser_gem/node_chainer.rb'
- 'lib/solargraph/pin/base.rb'
- 'lib/solargraph/range.rb'
- 'lib/solargraph/source.rb'
- 'lib/solargraph/yard_map/mapper/to_method.rb'

@@ -566,3 +557,2 @@ # Configuration parameters: Mode, AllowedMethods, AllowedPatterns, AllowBangMethods, WaywardPredicates.

- 'spec/rbs_map/stdlib_map_spec.rb'
- 'spec/rbs_map_spec.rb'
- 'spec/source/source_chainer_spec.rb'

@@ -590,3 +580,2 @@

- 'spec/api_map_spec.rb'
- 'spec/doc_map_spec.rb'
- 'spec/language_server/host/dispatch_spec.rb'

@@ -638,3 +627,2 @@ - 'spec/language_server/protocol_spec.rb'

Exclude:
- 'spec/rbs_map/conversions_spec.rb'
- 'spec/source/chain/call_spec.rb'

@@ -655,8 +643,2 @@

# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: .
# SupportedStyles: is_expected, should
RSpec/ImplicitExpect:
EnforcedStyle: should
# Configuration parameters: AssignmentOnly.

@@ -666,7 +648,2 @@ RSpec/InstanceVariable:

# This cop supports safe autocorrection (--autocorrect).
RSpec/LeadingSubject:
Exclude:
- 'spec/rbs_map/conversions_spec.rb'
RSpec/LeakyConstantDeclaration:

@@ -706,3 +683,8 @@ Exclude:

RSpec/PendingWithoutReason:
Enabled: false
Exclude:
- 'spec/api_map_spec.rb'
- 'spec/doc_map_spec.rb'
- 'spec/pin/base_variable_spec.rb'
- 'spec/pin/local_variable_spec.rb'
- 'spec/type_checker/levels/strict_spec.rb'

@@ -779,3 +761,2 @@ # This cop supports unsafe autocorrection (--autocorrect-all).

Exclude:
- 'lib/solargraph/api_map.rb'
- 'lib/solargraph/complex_type.rb'

@@ -858,3 +839,2 @@

- 'lib/solargraph/language_server/message/client/register_capability.rb'
- 'lib/solargraph/pin/base.rb'
- 'spec/fixtures/formattable.rb'

@@ -963,3 +943,2 @@ - 'spec/fixtures/rdoc-lib/lib/example.rb'

- 'lib/solargraph/parser/parser_gem/node_chainer.rb'
- 'lib/solargraph/type_checker/param_def.rb'

@@ -1038,3 +1017,2 @@ # This cop supports unsafe autocorrection (--autocorrect-all).

- 'lib/solargraph/source_map/clip.rb'
- 'lib/solargraph/type_checker/checks.rb'

@@ -1135,3 +1113,2 @@ # This cop supports safe autocorrection (--autocorrect).

- 'lib/solargraph/complex_type/type_methods.rb'
- 'lib/solargraph/doc_map.rb'
- 'lib/solargraph/parser/parser_gem/node_methods.rb'

@@ -1168,3 +1145,3 @@ - 'lib/solargraph/source/chain/z_super.rb'

Exclude:
- 'lib/solargraph/doc_map.rb'
- 'lib/solargraph/workspace/gemspecs.rb'

@@ -1178,3 +1155,8 @@ # This cop supports unsafe autocorrection (--autocorrect-all).

Style/SoleNestedConditional:
Enabled: false
Exclude:
- 'lib/solargraph/complex_type/unique_type.rb'
- 'lib/solargraph/pin/parameter.rb'
- 'lib/solargraph/source.rb'
- 'lib/solargraph/source/source_chainer.rb'
- 'lib/solargraph/type_checker.rb'

@@ -1201,3 +1183,2 @@ # This cop supports safe autocorrection (--autocorrect).

Exclude:
- 'lib/solargraph/pin/base_variable.rb'
- 'lib/solargraph/pin/callable.rb'

@@ -1244,3 +1225,6 @@ - 'lib/solargraph/pin/method.rb'

Style/TrailingCommaInHashLiteral:
Enabled: false
Exclude:
- 'lib/solargraph/pin/callable.rb'
- 'lib/solargraph/pin/closure.rb'
- 'lib/solargraph/rbs_map/conversions.rb'

@@ -1253,3 +1237,2 @@ # This cop supports safe autocorrection (--autocorrect).

- 'lib/solargraph/language_server/message/extended/check_gem_version.rb'
- 'lib/solargraph/pin/keyword.rb'

@@ -1294,8 +1277,3 @@ # This cop supports safe autocorrection (--autocorrect).

YARD/TagTypeSyntax:
Exclude:
- 'lib/solargraph/api_map/constants.rb'
- 'lib/solargraph/language_server/host.rb'
- 'lib/solargraph/parser/comment_ripper.rb'
- 'lib/solargraph/pin/method.rb'
- 'lib/solargraph/type_checker.rb'
Enabled: false

@@ -1302,0 +1280,0 @@ # This cop supports safe autocorrection (--autocorrect).

@@ -1,12 +0,3 @@

## 0.58.3 - March 9, 2026
- Ignore workspace dependencies in cache processes (#1174)
## 0.58.2 - January 19, 2026
- Avoid rbs pollution (#1146)
- Fix 'solargraph pin --references ClassName' private method call (#1150)
- Improve memory efficiency of Position class (#1054)
- Raise InvalidOffsetError for offsets > text (#1155)
## 0.58.1 - January 2, 2026
- Normalize line endings to LF (#1142)
- Normalize line endings to LF (#1142)

@@ -13,0 +4,0 @@ ## 0.58.0 - January 1, 2026

@@ -58,4 +58,4 @@ # frozen_string_literal: true

# @param type [Symbol] Type of assert.
def self.asserts_on?(type)
def self.asserts_on?
# @sg-ignore Translate to something flow sensitive typing understands
if ENV['SOLARGRAPH_ASSERTS'].nil? || ENV['SOLARGRAPH_ASSERTS'].empty?

@@ -76,3 +76,24 @@ false

def self.assert_or_log(type, msg = nil, &block)
raise (msg || block.call) if asserts_on?(type) && ![:combine_with_visibility].include?(type)
if asserts_on?
# @type [String, nil]
msg ||= block.call
raise "No message given for #{type.inspect}" if msg.nil?
# conditional aliases to handle compatibility corner cases
# @sg-ignore flow sensitive typing needs to handle 'raise if'
return if type == :alias_target_missing && msg.include?('highline/compatibility.rb')
# @sg-ignore flow sensitive typing needs to handle 'raise if'
return if type == :alias_target_missing && msg.include?('lib/json/add/date.rb')
# @todo :combine_with_visibility is not ready for prime time -
# lots of disagreements found in practice that heuristics need
# to be created for and/or debugging needs to resolve in pin
# generation.
# @todo :api_map_namespace_pin_stack triggers in a badly handled
# self type case - 'keeps track of self type in method
# parameters in subclass' in call_spec.rb
return if %i[api_map_namespace_pin_stack combine_with_visibility].include?(type)
raise msg
end
logger.info msg, &block

@@ -79,0 +100,0 @@ end

@@ -27,8 +27,22 @@ # frozen_string_literal: true

# @param pins [Array<Solargraph::Pin::Base>]
def initialize pins: []
# @param loose_unions [Boolean] if true, a potential type can be
# inferred if ANY of the UniqueTypes in the base chain's
# ComplexType match it. If false, every single UniqueTypes in
# the base must be ALL able to independently provide this
# type. The former is useful during completion, but the
# latter is best for typechecking at higher levels.
#
def initialize pins: [], loose_unions: true
@source_map_hash = {}
@cache = Cache.new
@loose_unions = loose_unions
index pins
end
# @param out [StringIO, IO, nil] output stream for logging
# @return [void]
def self.reset_core out: nil
@@core_map = RbsMap::CoreMap.new
end
#

@@ -43,3 +57,3 @@ # This is a mutable object, which is cached in the Chain class -

self.class == other.class &&
# @sg-ignore Flow sensitive typing needs to handle self.class == other.class
# @sg-ignore flow sensitive typing needs to handle self.class == other.class
equality_fields == other.equality_fields

@@ -53,2 +67,3 @@ end

# @return [Integer]
def hash

@@ -58,2 +73,4 @@ equality_fields.hash

attr_reader :loose_unions
def to_s

@@ -105,7 +122,7 @@ self.class.to_s

recreate_docmap = @unresolved_requires != unresolved_requires ||
@doc_map&.uncached_yard_gemspecs&.any? ||
@doc_map&.uncached_rbs_collection_gemspecs&.any? ||
@doc_map&.rbs_collection_path != bench.workspace.rbs_collection_path
workspace.rbs_collection_path != bench.workspace.rbs_collection_path ||
@doc_map.any_uncached?
if recreate_docmap
@doc_map = DocMap.new(unresolved_requires, [], bench.workspace) # @todo Implement gem preferences
@doc_map = DocMap.new(unresolved_requires, bench.workspace, out: nil) # @todo Implement gem preferences
@unresolved_requires = @doc_map.unresolved_requires

@@ -122,3 +139,3 @@ end

protected def equality_fields
[self.class, @source_map_hash, conventions_environ, @doc_map, @unresolved_requires]
[self.class, @source_map_hash, conventions_environ, @doc_map, @unresolved_requires, @missing_docs, @loose_unions]
end

@@ -128,3 +145,3 @@

def doc_map
@doc_map ||= DocMap.new([], [])
@doc_map ||= DocMap.new([], Workspace.new('.'))
end

@@ -134,15 +151,5 @@

def uncached_gemspecs
@doc_map&.uncached_gemspecs || []
doc_map.uncached_gemspecs || []
end
# @return [::Array<Gem::Specification>]
def uncached_rbs_collection_gemspecs
@doc_map.uncached_rbs_collection_gemspecs
end
# @return [::Array<Gem::Specification>]
def uncached_yard_gemspecs
@doc_map.uncached_yard_gemspecs
end
# @return [Enumerable<Pin::Base>]

@@ -153,5 +160,6 @@ def core_pins

# @param name [String]
# @param name [String, nil]
# @return [YARD::Tags::MacroDirective, nil]
def named_macro name
# @sg-ignore Need to add nil check here
store.named_macros[name]

@@ -192,6 +200,7 @@ end

# @param directory [String]
# @param loose_unions [Boolean] See #initialize
#
# @return [ApiMap]
def self.load directory
api_map = new
def self.load directory, loose_unions: true
api_map = new(loose_unions: loose_unions)
workspace = Solargraph::Workspace.new(directory)

@@ -205,6 +214,7 @@ # api_map.catalog Bench.new(workspace: workspace)

# @param out [IO, nil]
# @param out [StringIO, IO, nil]
# @param rebuild [Boolean] whether to rebuild the pins even if they are cached
# @return [void]
def cache_all!(out)
@doc_map.cache_all!(out)
def cache_all_for_doc_map! out: $stderr, rebuild: false
doc_map.cache_doc_map_gems!(out, rebuild: rebuild)
end

@@ -214,6 +224,6 @@

# @param rebuild [Boolean]
# @param out [IO, nil]
# @param out [StringIO, IO, nil]
# @return [void]
def cache_gem(gemspec, rebuild: false, out: nil)
@doc_map.cache(gemspec, rebuild: rebuild, out: out)
doc_map.cache(gemspec, rebuild: rebuild, out: out)
end

@@ -230,7 +240,8 @@

# @param directory [String]
# @param out [IO] The output stream for messages
# @param out [IO, StringIO, nil] The output stream for messages
# @param loose_unions [Boolean] See #initialize
#
# @return [ApiMap]
def self.load_with_cache directory, out
api_map = load(directory)
def self.load_with_cache directory, out = $stderr, loose_unions: true
api_map = load(directory, loose_unions: loose_unions)
if api_map.uncached_gemspecs.empty?

@@ -241,4 +252,4 @@ logger.info { "All gems cached for #{directory}" }

api_map.cache_all!(out)
load(directory)
api_map.cache_all_for_doc_map!(out: out)
load(directory, loose_unions: loose_unions)
end

@@ -345,3 +356,3 @@

# @return [Array<Solargraph::Pin::InstanceVariable>]
def get_instance_variable_pins(namespace, scope = :instance)
def get_instance_variable_pins namespace, scope = :instance
result = []

@@ -352,2 +363,3 @@ used = [namespace]

while (sc = store.get_superclass(sc_fqns))
# @sg-ignore flow sensitive typing needs to handle "if foo = bar"
sc_fqns = store.constants.dereference(sc)

@@ -359,6 +371,23 @@ result.concat store.get_instance_variables(sc_fqns, scope)

# @sg-ignore Missing @return tag for Solargraph::ApiMap#visible_pins
# @see Solargraph::Parser::FlowSensitiveTyping#visible_pins
def visible_pins(*args, **kwargs, &blk)
Solargraph::Parser::FlowSensitiveTyping.visible_pins(*args, **kwargs, &blk)
# Find a variable pin by name and where it is used.
#
# Resolves our most specific view of this variable's type by
# preferring pins created by flow-sensitive typing when we have
# them based on the Closure and Location.
#
# @param candidates [Array<Pin::BaseVariable>]
# @param name [String]
# @param closure [Pin::Closure]
# @param location [Location]
#
# @return [Pin::BaseVariable, nil]
def var_at_location(candidates, name, closure, location)
with_correct_name = candidates.select { |pin| pin.name == name}
vars_at_location = with_correct_name.reject do |pin|
# visible_at? excludes the starting position, but we want to
# include it for this purpose
(!pin.visible_at?(closure, location) && !pin.starts_at?(location))
end
vars_at_location.inject(&:combine_with)
end

@@ -370,3 +399,3 @@

# @return [Enumerable<Solargraph::Pin::ClassVariable>]
def get_class_variable_pins(namespace)
def get_class_variable_pins namespace
prefer_non_nil_variables(store.get_class_variables(namespace))

@@ -533,3 +562,4 @@ end

methods = if namespace_pin.is_a?(Pin::Constant)
type = namespace_pin.infer(self)
type = namespace_pin.typify(self)
type = namespace_pin.probe(self) unless type.defined?
if type.defined?

@@ -611,2 +641,3 @@ namespace_pin = store.get_path_pins(type.namespace).first

def clip cursor
# @sg-ignore Need to add nil check here
raise FileNotFoundError, "ApiMap did not catalog #{cursor.filename}" unless source_map_hash.key?(cursor.filename)

@@ -659,4 +690,7 @@

# few callers that currently expect this to be false.
# @sg-ignore flow-sensitive typing should be able to handle redefinition
return false if sup.literal? && sub.literal? && sup.to_s != sub.to_s
# @sg-ignore flow sensitive typing should be able to handle redefinition
sup = sup.simplify_literals.to_s
# @sg-ignore flow sensitive typing should be able to handle redefinition
sub = sub.simplify_literals.to_s

@@ -666,2 +700,3 @@ return true if sup == sub

while (sc = store.get_superclass(sc_fqns))
# @sg-ignore flow sensitive typing needs to handle "if foo = bar"
sc_new = store.constants.dereference(sc)

@@ -694,9 +729,17 @@ # Cyclical inheritance is invalid

resolved = resolve_method_alias(pin)
# @sg-ignore Need to add nil check here
next nil if resolved.respond_to?(:visibility) && !visibility.include?(resolved.visibility)
resolved
end.compact
logger.debug { "ApiMap#resolve_method_aliases(pins=#{pins.map(&:name)}, visibility=#{visibility}) => #{with_resolved_aliases.map(&:name)}" }
logger.debug do
"ApiMap#resolve_method_aliases(pins=#{pins.map(&:name)}, visibility=#{visibility}) => #{with_resolved_aliases.map(&:name)}"
end
GemPins.combine_method_pins_by_path(with_resolved_aliases)
end
# @return [Workspace]
def workspace
doc_map.workspace
end
# @param fq_reference_tag [String] A fully qualified whose method should be pulled in

@@ -801,2 +844,3 @@ # @param namespace_pin [Pin::Base] Namespace pin for the rooted_type

in_tag = dereference(ref)
# @sg-ignore Need to add nil check here
result.concat inner_get_methods_from_reference(in_tag, namespace_pin, rooted_type, scope, visibility, deep, skip, true)

@@ -806,3 +850,4 @@ end

unless rooted_sc_tag.nil?
result.concat inner_get_methods_from_reference(rooted_sc_tag, namespace_pin, rooted_type, scope, visibility, true, skip, no_core)
result.concat inner_get_methods_from_reference(rooted_sc_tag, namespace_pin, rooted_type, scope,
visibility, true, skip, no_core)
end

@@ -817,3 +862,4 @@ else

unless rooted_sc_tag.nil?
result.concat inner_get_methods_from_reference(rooted_sc_tag, namespace_pin, rooted_type, scope, visibility, true, skip, true)
result.concat inner_get_methods_from_reference(rooted_sc_tag, namespace_pin, rooted_type, scope,
visibility, true, skip, true)
end

@@ -870,4 +916,2 @@ unless no_core || fqns.empty?

private
# @param alias_pin [Pin::MethodAlias]

@@ -877,2 +921,3 @@ # @return [Pin::Method, nil]

ancestors = store.get_ancestors(alias_pin.full_context.reduce_class_type.tag)
# @type [Pin::Method, nil]
original = nil

@@ -883,3 +928,7 @@

next if ancestor_fqns.nil?
ancestor_method_path = "#{ancestor_fqns}#{alias_pin.scope == :instance ? '#' : '.'}#{alias_pin.original}"
ancestor_method_path = if alias_pin.original == 'new' && alias_pin.scope == :class
"#{ancestor_fqns}#initialize"
else
"#{ancestor_fqns}#{alias_pin.scope == :instance ? '#' : '.'}#{alias_pin.original}"
end

@@ -896,3 +945,3 @@ # Search for the original method in the ancestor

candidate_pin.is_a?(Pin::Method) && candidate_pin.scope == alias_pin.scope
candidate_pin.is_a?(Pin::Method)
end

@@ -902,5 +951,11 @@

end
if original.nil?
# :nocov:
Solargraph.assert_or_log(:alias_target_missing) { "Rejecting alias - target is missing while looking for #{alias_pin.full_context.tag} #{alias_pin.original} in #{alias_pin.scope} scope = #{alias_pin.inspect}" }
return nil
# :nocov:
end
# @sg-ignore ignore `received nil` for original
create_resolved_alias_pin(alias_pin, original) if original
create_resolved_alias_pin(alias_pin, original)
end

@@ -950,3 +1005,3 @@

# @return [Array<Pin::Base>]
def erase_generics(namespace_pin, rooted_type, pins)
def erase_generics namespace_pin, rooted_type, pins
return pins unless should_erase_generics_when_done?(namespace_pin, rooted_type)

@@ -962,3 +1017,3 @@

# @param rooted_type [ComplexType]
def should_erase_generics_when_done?(namespace_pin, rooted_type)
def should_erase_generics_when_done? namespace_pin, rooted_type
has_generics?(namespace_pin) && !can_resolve_generics?(namespace_pin, rooted_type)

@@ -974,3 +1029,3 @@ end

# @param rooted_type [ComplexType]
def can_resolve_generics?(namespace_pin, rooted_type)
def can_resolve_generics? namespace_pin, rooted_type
has_generics?(namespace_pin) && !rooted_type.all_params.empty?

@@ -977,0 +1032,0 @@ end

@@ -11,3 +11,3 @@ # frozen_string_literal: true

@constants = {}
# @type [Hash{String => String}]
# @type [Hash{String => String, nil}]
@qualified_namespaces = {}

@@ -65,3 +65,3 @@ # @type [Hash{String => Pin::Method}]

# @param context [String]
# @return [String]
# @return [String, nil]
def get_qualified_namespace name, context

@@ -73,3 +73,3 @@ @qualified_namespaces["#{name}|#{context}"]

# @param context [String]
# @param value [String]
# @param value [String, nil]
# @return [void]

@@ -76,0 +76,0 @@ def set_qualified_namespace name, context, value

@@ -30,5 +30,7 @@ # frozen_string_literal: true

#
# @sg-ignore flow sensitive typing needs to eliminate literal from union with return if foo == :bar
# @return [String, nil] fully qualified namespace (i.e., is
# absolute, but will not start with ::)
def resolve(name, *gates)
# @sg-ignore Need to add nil check here
return store.get_path_pins(name[2..]).first&.path if name.start_with?('::')

@@ -90,2 +92,3 @@

if pin.is_a?(Pin::Constant)
# @sg-ignore Need to add nil check here
const = Solargraph::Parser::NodeMethods.unpack_name(pin.assignment)

@@ -110,2 +113,3 @@ return unless const

# @param gates [Array<String>]
# @sg-ignore flow sensitive typing should be able to handle redefinition
# @return [String, nil]

@@ -131,2 +135,3 @@ def resolve_and_cache name, gates

else
# @sg-ignore flow sensitive typing needs better handling of ||= on lvars
return resolve(name, first) unless first.empty?

@@ -145,3 +150,3 @@ end

# @param internal [Boolean] True if the name is not the last in the namespace
# @return [Array(Object, Array<String>)]
# @return [Array(String, Array<String>), Array(nil, Array<String>), String]
def complex_resolve name, gates, internal

@@ -173,2 +178,3 @@ resolved = nil

if pin.is_a?(Pin::Constant) && internal
# @sg-ignore Need to add nil check here
const = Solargraph::Parser::NodeMethods.unpack_name(pin.assignment)

@@ -206,3 +212,3 @@ return unless const

#
# @param namespace [String, nil] The namespace to
# @param namespace [String] The namespace to
# match

@@ -214,2 +220,3 @@ # @param context_namespace [String] The context namespace in which the

if namespace.start_with?('::')
# @sg-ignore Need to add nil check here
inner_qualify(namespace[2..], '', Set.new)

@@ -260,3 +267,3 @@ else

# @param fqns [String]
# @param fqns [String, nil]
# @param visibility [Array<Symbol>]

@@ -271,2 +278,3 @@ # @param skip [Set<String>]

store.get_prepends(fqns).each do |pre|
# @sg-ignore Need to add nil check here
pre_fqns = resolve(pre.name, pre.closure.gates - skip.to_a)

@@ -277,2 +285,3 @@ result.concat inner_get_constants(pre_fqns, [:public], skip)

store.get_includes(fqns).each do |pin|
# @sg-ignore Need to add nil check here
inc_fqns = resolve(pin.name, pin.closure.gates - skip.to_a)

@@ -284,2 +293,3 @@ result.concat inner_get_constants(inc_fqns, [:public], skip)

fqsc = dereference(sc_ref)
# @sg-ignore Need to add nil check here
result.concat inner_get_constants(fqsc, [:public], skip) unless %w[Object BasicObject].include?(fqsc)

@@ -286,0 +296,0 @@ end

@@ -43,3 +43,3 @@ # frozen_string_literal: true

def pins_by_class klass
# @type [Set<Solargraph::Pin::Base>]
# @type [Set<generic<T>>]
s = Set.new

@@ -50,3 +50,3 @@ # @sg-ignore need to support destructured args in blocks

# @return [Hash{String => Array<String>}]
# @return [Hash{String => Array<Pin::Reference::Include>}]
def include_references

@@ -65,3 +65,3 @@ # @param h [String]

# @return [Hash{String => Array<String>}]
# @return [Hash{String => Array<Pin::Reference::Extend>}]
def extend_references

@@ -73,3 +73,3 @@ # @param h [String]

# @return [Hash{String => Array<String>}]
# @return [Hash{String => Array<Pin::Reference::Prepend>}]
def prepend_references

@@ -81,3 +81,3 @@ # @param h [String]

# @return [Hash{String => Array<String>}]
# @return [Hash{String => Array<Pin::Reference::Superclass>}]
def superclass_references

@@ -146,3 +146,3 @@ # @param h [String]

# @param klass [Class<generic<T>>]
# @param hash [Hash{String => generic<T>}]
# @param hash [Hash{String => Array<generic<T>>}]
#

@@ -159,2 +159,3 @@ # @return [void]

def map_overrides
# @todo should complain when type for 'ovr' is not provided
# @param ovr [Pin::Reference::Override]

@@ -170,3 +171,11 @@ pins_by_class(Pin::Reference::Override).each do |ovr|

(ovr.tags.map(&:tag_name) + ovr.delete).uniq.each do |tag|
# @sg-ignore Wrong argument type for
# YARD::Docstring#delete_tags: name expected String,
# received String, Symbol - delete_tags is ok with a
# _ToS, but we should fix anyway
pin.docstring.delete_tags tag
# @sg-ignore Wrong argument type for
# YARD::Docstring#delete_tags: name expected String,
# received String, Symbol - delete_tags is ok with a
# _ToS, but we should fix anyway
new_pin.docstring.delete_tags tag if new_pin

@@ -177,6 +186,9 @@ end

redefine_return_type pin, tag
if new_pin
new_pin.docstring.add_tag(tag)
redefine_return_type new_pin, tag
end
pin.reset_generated!
next unless new_pin
new_pin.docstring.add_tag(tag)
redefine_return_type new_pin, tag
new_pin.reset_generated!
end

@@ -198,3 +210,2 @@ end

end
pin.reset_generated!
end

@@ -201,0 +212,0 @@ end

@@ -9,2 +9,5 @@ # frozen_string_literal: true

#
# @sg-ignore Declared return type generic<T>, nil does not match
# inferred type ::YARD::CodeObjects::Base, nil for
# Solargraph::ApiMap::SourceToYard#code_object_at
# @generic T

@@ -38,3 +41,5 @@ # @param path [String]

code_object_map[pin.path] ||= YARD::CodeObjects::ClassObject.new(root_code_object, pin.path) { |obj|
# @sg-ignore flow sensitive typing needs to handle attrs
next if pin.location.nil? || pin.location.filename.nil?
# @sg-ignore flow sensitive typing needs to handle attrs
obj.add_file(pin.location.filename, pin.location.range.start.line, !pin.comments.empty?)

@@ -45,3 +50,5 @@ }

code_object_map[pin.path] ||= YARD::CodeObjects::ModuleObject.new(root_code_object, pin.path) { |obj|
# @sg-ignore flow sensitive typing needs to handle attrs
next if pin.location.nil? || pin.location.filename.nil?
# @sg-ignore flow sensitive typing needs to handle attrs
obj.add_file(pin.location.filename, pin.location.range.start.line, !pin.comments.empty?)

@@ -63,3 +70,2 @@ }

extend_object.class_mixins.push code_object
# @todo add spec showing why this next line is necessary
extend_object.instance_mixins.push code_object

@@ -74,10 +80,16 @@ end

# @sg-ignore Need to add nil check here
# @param obj [YARD::CodeObjects::RootObject]
code_object_map[pin.path] ||= YARD::CodeObjects::MethodObject.new(code_object_at(pin.namespace, YARD::CodeObjects::NamespaceObject), pin.name, pin.scope) { |obj|
# @sg-ignore flow sensitive typing needs to handle attrs
next if pin.location.nil? || pin.location.filename.nil?
# @sg-ignore flow sensitive typing needs to handle attrs
obj.add_file pin.location.filename, pin.location.range.start.line
}
method_object = code_object_at(pin.path, YARD::CodeObjects::MethodObject)
# @sg-ignore Need to add nil check here
method_object.docstring = pin.docstring
# @sg-ignore Need to add nil check here
method_object.visibility = pin.visibility || :public
# @sg-ignore Need to add nil check here
method_object.parameters = pin.parameters.map do |p|

@@ -84,0 +96,0 @@ [p.full_name, p.asgn_code]

@@ -37,2 +37,3 @@ # frozen_string_literal: true

# @sg-ignore Need to add nil check here
pinsets[changed..].each_with_index do |pins, idx|

@@ -64,3 +65,3 @@ @pinsets[changed + idx] = pins

namespace_children(fqns).select { |pin|
# @sg-ignore flow-sensitive typing not smart enough to handle this case
# @sg-ignore flow sensitive typing not smart enough to handle this case
!pin.name.empty? && (pin.is_a?(Pin::Namespace) || pin.is_a?(Pin::Constant)) && visibility.include?(pin.visibility)

@@ -76,3 +77,2 @@ }

all_pins = namespace_children(fqns).select do |pin|
# @sg-ignore https://github.com/castwide/solargraph/pull/1114
pin.is_a?(Pin::Method) && pin.scope == scope && visibility.include?(pin.visibility)

@@ -86,4 +86,4 @@ end

# @param fqns [String]
# @return [Pin::Reference::Superclass]
# @param fqns [String, nil]
# @return [Pin::Reference::Superclass, nil]
def get_superclass fqns

@@ -133,3 +133,3 @@ return nil if fqns.nil? || fqns.empty?

# @param fqns [String]
# @param fqns [String, nil]
# @param scope [Symbol] :class or :instance

@@ -207,3 +207,3 @@ # @return [Enumerable<Solargraph::Pin::Base>]

# @param fqns [String]
# @param fqns [String, nil]
# @return [Array<Solargraph::Pin::Namespace>]

@@ -253,4 +253,7 @@ def fqns_pins fqns

refs.map(&:type).map(&:to_s).each do |ref|
# @sg-ignore flow sensitive typing should be able to handle redefinition
next if ref.nil? || ref.empty? || visited.include?(ref)
# @sg-ignore flow sensitive typing should be able to handle redefinition
ancestors << ref
# @sg-ignore flow sensitive typing should be able to handle redefinition
queue << ref

@@ -285,3 +288,3 @@ end

#
# @return [void]
# @return [true]
def catalog pinsets

@@ -319,3 +322,3 @@ @pinsets = pinsets

# @return [Hash{String => Array<String>}]
# @return [Hash{String => Array<Pin::Reference::Superclass>}]
def superclass_references

@@ -322,0 +325,0 @@ index.superclass_references

@@ -0,0 +0,0 @@ # frozen_string_literal: true

@@ -12,2 +12,3 @@ # frozen_string_literal: true

autoload :Conformance, 'solargraph/complex_type/conformance'
autoload :TypeMethods, 'solargraph/complex_type/type_methods'

@@ -23,9 +24,11 @@ autoload :UniqueType, 'solargraph/complex_type/unique_type'

items.delete_if { |i| i.name == 'false' || i.name == 'true' }
items.unshift(ComplexType::BOOLEAN)
items.unshift(UniqueType::BOOLEAN)
end
# @type [Array<UniqueType>]
items = [UniqueType::UNDEFINED] if items.any?(&:undefined?)
# @todo shouldn't need this cast - if statement above adds an 'Array' type
# @type [Array<UniqueType>]
@items = items
end
# @sg-ignore Fix "Not enough arguments to Module#protected"
protected def equality_fields

@@ -49,3 +52,3 @@ [self.class, items]

# @param generics_to_resolve [Enumerable<String>]]
# @param context_type [UniqueType, nil]
# @param context_type [ComplexType, ComplexType::UniqueType, nil]
# @param resolved_generic_values [Hash{String => ComplexType}] Added to as types are encountered or resolved

@@ -71,3 +74,3 @@ # @return [self]

# @param dst [ComplexType]
# @param dst [ComplexType, ComplexType::UniqueType]
# @return [ComplexType]

@@ -83,5 +86,9 @@ def self_to_type dst

# @yieldparam [UniqueType]
# @yieldreturn [UniqueType]
# @return [Array<UniqueType>]
def map &block
@items.map &block
# @sg-ignore Declared return type
# ::Array<::Solargraph::ComplexType::UniqueType> does not match
# inferred type ::Array<::Proc> for Solargraph::ComplexType#map
def map(&block)
@items.map(&block)
end

@@ -107,8 +114,2 @@

# @param atype [ComplexType] type which may be assigned to this type
# @param api_map [ApiMap] The ApiMap that performs qualification
def can_assign?(api_map, atype)
any? { |ut| ut.can_assign?(api_map, atype) }
end
# @param new_name [String, nil]

@@ -203,2 +204,56 @@ # @param make_rooted [Boolean, nil]

# @param api_map [ApiMap]
# @param expected [ComplexType, ComplexType::UniqueType]
# @param situation [:method_call, :return_type, :assignment]
# @param allow_subtype_skew [Boolean] if false, check if any
# subtypes of the expected type match the inferred type
# @param allow_reverse_match [Boolean] if true, check if any subtypes
# of the expected type match the inferred type
# @param allow_empty_params [Boolean] if true, allow a general
# inferred type without parameters to conform to a more specific
# expected type
# @param allow_any_match [Boolean] if true, any unique type
# matched in the inferred qualifies as a match
# @param allow_undefined [Boolean] if true, treat undefined as a
# wildcard that matches anything
# @param rules [Array<:allow_subtype_skew, :allow_empty_params, :allow_reverse_match, :allow_any_match, :allow_undefined, :allow_unresolved_generic, :allow_unmatched_interface>]
# @param variance [:invariant, :covariant, :contravariant]
# @return [Boolean]
def conforms_to?(api_map, expected,
situation,
rules = [],
variance: erased_variance(situation))
expected = expected.downcast_to_literal_if_possible
inferred = downcast_to_literal_if_possible
return duck_types_match?(api_map, expected, inferred) if expected.duck_type?
if rules.include? :allow_any_match
inferred.any? do |inf|
inf.conforms_to?(api_map, expected, situation, rules,
variance: variance)
end
else
inferred.all? do |inf|
inf.conforms_to?(api_map, expected, situation, rules,
variance: variance)
end
end
end
# @param api_map [ApiMap]
# @param expected [ComplexType, UniqueType]
# @param inferred [ComplexType, UniqueType]
# @return [Boolean]
def duck_types_match? api_map, expected, inferred
raise ArgumentError, 'Expected type must be duck type' unless expected.duck_type?
expected.each do |exp|
next unless exp.duck_type?
quack = exp.to_s[1..]
# @sg-ignore Need to add nil check here
return false if api_map.get_method_stack(inferred.namespace, quack, scope: inferred.scope).empty?
end
true
end
# @return [String]

@@ -262,2 +317,9 @@ def rooted_tags

# @return [ComplexType]
def without_nil
new_items = @items.reject(&:nil_type?)
return ComplexType::UNDEFINED if new_items.empty?
ComplexType.new(new_items)
end
# @return [Array<ComplexType>]

@@ -285,2 +347,9 @@ def all_params

# @param other [ComplexType, UniqueType]
def erased_version_of?(other)
return false if items.length != 1 || other.items.length != 1
@items.first.erased_version_of?(other.items.first)
end
# every top-level type has resolved to be fully qualified; see

@@ -298,2 +367,36 @@ # #all_rooted? to check their subtypes as well

# @param exclude_types [ComplexType, nil]
# @param api_map [ApiMap]
# @return [ComplexType, self]
def exclude exclude_types, api_map
return self if exclude_types.nil?
types = items - exclude_types.items
types = [ComplexType::UniqueType::UNDEFINED] if types.empty?
ComplexType.new(types)
end
# @see https://en.wikipedia.org/wiki/Intersection_type
#
# @param intersection_type [ComplexType, ComplexType::UniqueType, nil]
# @param api_map [ApiMap]
# @return [self, ComplexType::UniqueType]
def intersect_with intersection_type, api_map
return self if intersection_type.nil?
return intersection_type if undefined?
types = []
# try to find common types via conformance
items.each do |ut|
intersection_type.each do |int_type|
if int_type.conforms_to?(api_map, ut, :assignment)
types << int_type
elsif ut.conforms_to?(api_map, int_type, :assignment)
types << ut
end
end
end
types = [ComplexType::UniqueType::UNDEFINED] if types.empty?
ComplexType.new(types)
end
protected

@@ -333,3 +436,3 @@

# # @return [Array<UniqueType>]
# @todo To be able to select the right signature above,
# @sg-ignore To be able to select the right signature above,
# Chain::Call needs to know the decl type (:arg, :optarg,

@@ -339,3 +442,3 @@ # :kwarg, etc) of the arguments given, instead of just having

def parse *strings, partial: false
# @type [Hash{Array<String> => ComplexType}]
# @type [Hash{Array<String> => ComplexType, Array<ComplexType::UniqueType>}]
@cache ||= {}

@@ -346,2 +449,3 @@ unless partial

end
# @types [Array<ComplexType::UniqueType>]
types = []

@@ -367,2 +471,3 @@ key_types = nil

# types.push ComplexType.new([UniqueType.new(base[0..-2].strip)])
# @sg-ignore Need to add nil check here
types.push UniqueType.parse(base[0..-2].strip, subtype_string)

@@ -369,0 +474,0 @@ # @todo this should either expand key_type's type

@@ -76,2 +76,14 @@ # frozen_string_literal: true

# 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<String>]

@@ -198,3 +210,3 @@ # @return [self]

return false unless self.class == other.class
# @sg-ignore https://github.com/castwide/solargraph/pull/1114
# @sg-ignore flow sensitive typing should support .class == .class
tag == other.tag

@@ -223,3 +235,5 @@ end

# @yieldparam [UniqueType]
# @return [Enumerator<UniqueType>]
# @return [void]
# @overload each_unique_type()
# @return [Enumerator<UniqueType>]
def each_unique_type &block

@@ -226,0 +240,0 @@ return enum_for(__method__) unless block_given?

@@ -14,3 +14,2 @@ # frozen_string_literal: true

# @sg-ignore Fix "Not enough arguments to Module#protected"
protected def equality_fields

@@ -50,2 +49,3 @@ [@name, @all_params, @subtypes, @key_types]

subs = ComplexType.parse(substring[1..-2], partial: true)
# @sg-ignore Need to add nil check here
parameters_type = PARAMETERS_TYPE_BY_STARTING_TAG.fetch(substring[0])

@@ -67,2 +67,3 @@ if parameters_type == :hash

end
# @sg-ignore Need to add nil check here
new(name, key_types, subtypes, rooted: rooted, parameters_type: parameters_type)

@@ -115,2 +116,40 @@ end

# @param exclude_types [ComplexType, nil]
# @param api_map [ApiMap]
# @return [ComplexType, self]
def exclude exclude_types, api_map
return self if exclude_types.nil?
types = items - exclude_types.items
types = [ComplexType::UniqueType::UNDEFINED] if types.empty?
ComplexType.new(types)
end
# @see https://en.wikipedia.org/wiki/Intersection_type
#
# @param intersection_type [ComplexType, ComplexType::UniqueType, nil]
# @param api_map [ApiMap]
# @return [self, ComplexType]
def intersect_with intersection_type, api_map
return self if intersection_type.nil?
return intersection_type if undefined?
types = []
# try to find common types via conformance
items.each do |ut|
intersection_type.each do |int_type|
if ut.conforms_to?(api_map, int_type, :assignment)
types << ut
elsif int_type.conforms_to?(api_map, ut, :assignment)
types << int_type
end
end
end
types = [ComplexType::UniqueType::UNDEFINED] if types.empty?
ComplexType.new(types)
end
def simplifyable_literal?
literal? && name != 'nil'
end
def literal?

@@ -125,2 +164,9 @@ non_literal_name != name

# @return [self]
def without_nil
return UniqueType::UNDEFINED if nil_type?
self
end
# @return [String]

@@ -139,2 +185,3 @@ def determine_non_literal_name

return 'Symbol' if name[0] == ':'
# @sg-ignore Need to add nil check here
return 'String' if ['"', "'"].include?(name[0])

@@ -147,13 +194,13 @@ return 'Integer' if name.match?(/^-?\d+$/)

self.class == other.class &&
# @sg-ignore https://github.com/castwide/solargraph/pull/1114
# @sg-ignore flow sensitive typing should support .class == .class
@name == other.name &&
# @sg-ignore https://github.com/castwide/solargraph/pull/1114
# @sg-ignore flow sensitive typing should support .class == .class
@key_types == other.key_types &&
# @sg-ignore https://github.com/castwide/solargraph/pull/1114
# @sg-ignore flow sensitive typing should support .class == .class
@subtypes == other.subtypes &&
# @sg-ignore https://github.com/castwide/solargraph/pull/1114
# @sg-ignore flow sensitive typing should support .class == .class
@rooted == other.rooted? &&
# @sg-ignore https://github.com/castwide/solargraph/pull/1114
# @sg-ignore flow sensitive typing should support .class == .class
@all_params == other.all_params &&
# @sg-ignore https://github.com/castwide/solargraph/pull/1114
# @sg-ignore flow sensitive typing should support .class == .class
@parameters_type == other.parameters_type

@@ -166,2 +213,73 @@ end

# https://www.playfulpython.com/type-hinting-covariance-contra-variance/
# "[Expected] type variables that are COVARIANT can be substituted with
# a more specific [inferred] type without causing errors"
#
# "[Expected] type variables that are CONTRAVARIANT can be substituted
# with a more general [inferred] type without causing errors"
#
# "[Expected] types where neither is possible are INVARIANT"
#
# @param _situation [:method_call, :return_type]
# @param default [Symbol] The default variance to return if the type is not one of the special cases
#
# @return [:invariant, :covariant, :contravariant]
def parameter_variance _situation, default = :covariant
# @todo RBS can specify variance - maybe we can use that info
# and also let folks specify?
#
# Array/Set: ideally invariant, since we don't know if user is
# going to add new stuff into it or read it. But we don't
# have a way to specify, so we use covariant
# Enumerable: covariant: can't be changed, so we can pass
# in more specific subtypes
# Hash: read-only would be covariant, read-write would be
# invariant if we could distinguish that - should default to
# covariant
# contravariant?: Proc - can be changed, so we can pass
# in less specific super types
if ['Hash', 'Tuple', 'Array', 'Set', 'Enumerable'].include?(name) && fixed_parameters?
:covariant
else
default
end
end
# Whether this is an RBS interface like _ToAry or _Each.
def interface?
name.start_with?('_')
end
# @param other [UniqueType]
def erased_version_of?(other)
name == other.name && (all_params.empty? || all_params.all?(&:undefined?))
end
# @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]
def conforms_to?(api_map, expected, situation, rules = [],
variance: erased_variance(situation))
return true if undefined? && rules.include?(:allow_undefined)
# @todo teach this to validate duck types as inferred type
return true if duck_type?
# complex types as expectations are unions - we only need to
# match one of their unique types
expected.any? do |expected_unique_type|
# :nocov:
unless expected_unique_type.instance_of?(UniqueType)
raise "Expected type must be a UniqueType, got #{expected_unique_type.class} in #{expected.inspect}"
end
# :nocov:
conformance = Conformance.new(api_map, self, expected_unique_type, situation,
rules, variance: variance)
conformance.conforms_to_unique_type?
end
end
def hash

@@ -171,2 +289,7 @@ [self.class, @name, @key_types, @sub_types, @rooted, @all_params, @parameters_type].hash

# @return [self]
def erase_parameters
UniqueType.new(name, rooted: rooted?, parameters_type: parameters_type)
end
# @return [Array<UniqueType>]

@@ -193,2 +316,3 @@ def items

# @sg-ignore Need better if/elseanalysis
# @return [String]

@@ -203,3 +327,3 @@ def to_rbs

elsif name == GENERIC_TAG_NAME
all_params.first.name
all_params.first&.name
elsif ['Class', 'Module'].include?(name)

@@ -256,14 +380,11 @@ rbs_name

# @param api_map [ApiMap] The ApiMap that performs qualification
# @param atype [ComplexType] type which may be assigned to this type
def can_assign?(api_map, atype)
logger.debug { "UniqueType#can_assign?(self=#{rooted_tags.inspect}, atype=#{atype.rooted_tags.inspect})" }
downcasted_atype = atype.downcast_to_literal_if_possible
out = downcasted_atype.all? do |autype|
autype.name == name || api_map.super_and_sub?(name, autype.name)
end
logger.debug { "UniqueType#can_assign?(self=#{rooted_tags.inspect}, atype=#{atype.rooted_tags.inspect}) => #{out}" }
out
def nullable?
nil_type?
end
# @yieldreturn [Boolean]
def all? &block
block.yield self
end
# @return [UniqueType]

@@ -275,3 +396,3 @@ def downcast_to_literal_if_possible

# @param generics_to_resolve [Enumerable<String>]
# @param context_type [UniqueType, nil]
# @param context_type [ComplexType, UniqueType, nil]
# @param resolved_generic_values [Hash{String => ComplexType, ComplexType::UniqueType}] Added to as types are encountered or resolved

@@ -282,5 +403,8 @@ # @return [UniqueType, ComplexType]

type_param = subtypes.first&.name
# @sg-ignore flow sensitive typing needs to eliminate literal from union with [:bar].include?(foo)
return self unless generics_to_resolve.include? type_param
# @sg-ignore flow sensitive typing needs to eliminate literal from union with [:bar].include?(foo)
unless context_type.nil? || !resolved_generic_values[type_param].nil?
new_binding = true
# @sg-ignore flow sensitive typing needs to eliminate literal from union with [:bar].include?(foo)
resolved_generic_values[type_param] = context_type

@@ -293,2 +417,3 @@ end

end
# @sg-ignore flow sensitive typing needs to eliminate literal from union with [:bar].include?(foo)
return resolved_generic_values[type_param] || self

@@ -304,3 +429,3 @@ end

# @param generics_to_resolve [Enumerable<String>]
# @param context_type [UniqueType, nil]
# @param context_type [UniqueType, ComplexType, nil]
# @param resolved_generic_values [Hash{String => ComplexType}]

@@ -356,2 +481,3 @@ # @yieldreturn [Array<ComplexType>]

else
# @sg-ignore Need to add nil check here
context_type.all_params[idx] || definitions.generic_defaults[generic_name] || ComplexType::UNDEFINED

@@ -372,2 +498,9 @@ end

# @yieldparam t [self]
# @yieldreturn [self]
# @return [Enumerable<self>]
def each &block
[self].each &block
end
# @return [Array<UniqueType>]

@@ -391,2 +524,3 @@ def to_a

make_rooted = @rooted if make_rooted.nil?
# @sg-ignore flow sensitive typing needs better handling of ||= on lvars
UniqueType.new(new_name, new_key_types, new_subtypes, rooted: make_rooted, parameters_type: parameters_type)

@@ -465,2 +599,18 @@ end

# @yieldreturn [Boolean]
def any? &block
block.yield self
end
# @return [ComplexType]
def reduce_class_type
new_items = items.flat_map do |type|
next type unless ['Module', 'Class'].include?(type.name)
next type if type.all_params.empty?
type.all_params
end
ComplexType.new(new_items)
end
def all_rooted?

@@ -467,0 +617,0 @@ return true if name == GENERIC_TAG_NAME

@@ -0,0 +0,0 @@ # frozen_string_literal: true

@@ -20,2 +20,3 @@ # frozen_string_literal: true

closure: region.closure,
# @sg-ignore flow sensitive typing needs to handle attrs
name: data_definition_node.class_name,

@@ -43,2 +44,3 @@ comments: comments_for(node),

# @sg-ignore flow sensitive typing needs to handle attrs
data_definition_node.attributes.map do |attribute_node, attribute_name|

@@ -56,2 +58,3 @@ initialize_method_pin.parameters.push(

# define attribute readers and instance variables
# @sg-ignore flow sensitive typing needs to handle attrs
data_definition_node.attributes.each do |attribute_node, attribute_name|

@@ -84,3 +87,3 @@ name = attribute_name.to_s

# @return [DataDefintionNode, nil]
# @return [DataDefinition::DataDefintionNode, DataDefinition::DataAssignmentNode, nil]
def data_definition_node

@@ -87,0 +90,0 @@ @data_definition_node ||= if DataDefintionNode.match?(node)

@@ -0,0 +0,0 @@ # frozen_string_literal: true

@@ -69,3 +69,3 @@ # frozen_string_literal: true

# @return [Parser::AST::Node]
# @return [Parser::AST::Node, nil]
def body_node

@@ -85,4 +85,6 @@ node.children[2]

# @sg-ignore Need to add nil check here
# @return [Array<Parser::AST::Node>]
def data_attribute_nodes
# @sg-ignore Need to add nil check here
data_node.children[2..-1]

@@ -89,0 +91,0 @@ end

@@ -0,0 +0,0 @@ # frozen_string_literal: true

@@ -0,0 +0,0 @@ # frozen_string_literal: true

@@ -0,0 +0,0 @@ # frozen_string_literal: true

@@ -20,2 +20,3 @@ # frozen_string_literal: true

closure: region.closure,
# @sg-ignore flow sensitive typing needs to handle attrs
name: struct_definition_node.class_name,

@@ -43,2 +44,3 @@ docstring: docstring,

# @sg-ignore flow sensitive typing needs to handle attrs
struct_definition_node.attributes.map do |attribute_node, attribute_name|

@@ -57,2 +59,3 @@ initialize_method_pin.parameters.push(

# define attribute accessors and instance variables
# @sg-ignore flow sensitive typing needs to handle attrs
struct_definition_node.attributes.each do |attribute_node, attribute_name|

@@ -108,3 +111,3 @@ [attribute_name, "#{attribute_name}="].each do |name|

# @return [StructDefintionNode, StructAssignmentNode, nil]
# @return [StructDefinition::StructDefintionNode, StructDefinition::StructAssignmentNode, nil]
def struct_definition_node

@@ -128,2 +131,3 @@ @struct_definition_node ||= if StructDefintionNode.match?(node)

struct_comments = comments_for(node) || ''
# @sg-ignore Need to add nil check here
struct_definition_node.attributes.each do |attr_node, attr_name|

@@ -130,0 +134,0 @@ comment = comments_for(attr_node)

@@ -25,2 +25,3 @@ # frozen_string_literal: true

# s(:send, nil, :bar))))
#
# @param node [Parser::AST::Node]

@@ -27,0 +28,0 @@ def match?(node)

@@ -95,2 +95,3 @@ # frozen_string_literal: true

# @sg-ignore Need to add nil check here
# @return [Array<Parser::AST::Node>]

@@ -97,0 +98,0 @@ def struct_attribute_nodes

@@ -0,0 +0,0 @@ require 'nokogiri'

@@ -0,0 +0,0 @@ module ReverseMarkdown

@@ -0,0 +0,0 @@ module ReverseMarkdown

ReverseMarkdown::Converters.register :tt, ReverseMarkdown::Converters::Code.new

@@ -13,2 +13,3 @@ # frozen_string_literal: true

refs = {}
# @sg-ignore Need to add nil check here
map = api_map.source_map(source.filename)

@@ -15,0 +16,0 @@ map.requires.each { |ref| refs[ref.name] = ref }

@@ -21,2 +21,3 @@ # frozen_string_literal: true

gem_lib_path = File.join(gem_path, 'lib')
# @sg-ignore Should better support meaning of '&' in RBS
$LOAD_PATH.unshift(gem_lib_path) unless $LOAD_PATH.include?(gem_lib_path)

@@ -54,2 +55,3 @@ rescue Gem::MissingSpecVersionError => e

return path unless path.match(/^[a-z]:/)
# @sg-ignore Need to add nil check here
path[0].upcase + path[1..-1]

@@ -56,0 +58,0 @@ end

@@ -28,2 +28,3 @@ # frozen_string_literal: true

require_rubocop(rubocop_version)
# @sg-ignore Need to add nil check here
options, paths = generate_options(source.filename, source.code)

@@ -30,0 +31,0 @@ store = RuboCop::ConfigStore.new

@@ -14,2 +14,3 @@ # frozen_string_literal: true

level = (args.reverse.find { |a| ['normal', 'typed', 'strict', 'strong'].include?(a) }) || :normal
# @sg-ignore sensitive typing needs to handle || on nil types
checker = Solargraph::TypeChecker.new(source.filename, api_map: api_map, level: level.to_sym)

@@ -16,0 +17,0 @@ checker.problems

@@ -0,0 +0,0 @@ # frozen_string_literal: true

@@ -8,3 +8,6 @@ # frozen_string_literal: true

module Solargraph
# A collection of pins generated from required gems.
# A collection of pins generated from specific 'require' statements
# in code. Multiple can be created per workspace, to represent the
# pins available in different files based on their particular
# 'require' lines.
#

@@ -14,32 +17,3 @@ class DocMap

# @return [Array<String>]
attr_reader :requires
alias required requires
# @return [Array<Gem::Specification>]
attr_reader :preferences
# @return [Array<Pin::Base>]
attr_reader :pins
# @return [Array<Gem::Specification>]
def uncached_gemspecs
uncached_yard_gemspecs.concat(uncached_rbs_collection_gemspecs)
.sort
.uniq { |gemspec| "#{gemspec.name}:#{gemspec.version}" }
end
# @return [Array<Gem::Specification>]
attr_reader :uncached_yard_gemspecs
# @return [Array<Gem::Specification>]
attr_reader :uncached_rbs_collection_gemspecs
# @return [String, nil]
attr_reader :rbs_collection_path
# @return [String, nil]
attr_reader :rbs_collection_config_path
# @return [Workspace, nil]
# @return [Workspace]
attr_reader :workspace

@@ -51,392 +25,179 @@

# @param requires [Array<String>]
# @param preferences [Array<Gem::Specification>]
# @param workspace [Workspace, nil]
def initialize(requires, preferences, workspace = nil)
@requires = requires.compact
@preferences = preferences.compact
# @param out [IO, nil] output stream for logging
def initialize requires, workspace, out: $stderr
@provided_requires = requires.compact
@workspace = workspace
@rbs_collection_path = workspace&.rbs_collection_path
@rbs_collection_config_path = workspace&.rbs_collection_config_path
@environ = Convention.for_global(self)
@requires.concat @environ.requires if @environ
load_serialized_gem_pins
pins.concat @environ.pins
@out = out
end
# @param out [IO]
# @return [void]
def cache_all!(out)
# if we log at debug level:
if logger.info?
gem_desc = uncached_gemspecs.map { |gemspec| "#{gemspec.name}:#{gemspec.version}" }.join(', ')
logger.info "Caching pins for gems: #{gem_desc}" unless uncached_gemspecs.empty?
end
logger.debug { "Caching for YARD: #{uncached_yard_gemspecs.map(&:name)}" }
logger.debug { "Caching for RBS collection: #{uncached_rbs_collection_gemspecs.map(&:name)}" }
load_serialized_gem_pins
uncached_gemspecs.each do |gemspec|
cache(gemspec, out: out)
end
load_serialized_gem_pins
@uncached_rbs_collection_gemspecs = []
@uncached_yard_gemspecs = []
# @return [Array<String>]
def requires
@requires ||= @provided_requires + (workspace.global_environ&.requires || [])
end
alias required requires
# @param gemspec [Gem::Specification]
# @param out [IO]
# @return [void]
def cache_yard_pins(gemspec, out)
pins = GemPins.build_yard_pins(yard_plugins, gemspec)
PinCache.serialize_yard_gem(gemspec, pins)
logger.info { "Cached #{pins.length} YARD pins for gem #{gemspec.name}:#{gemspec.version}" } unless pins.empty?
# @sg-ignore flow sensitive typing needs to understand reassignment
# @return [Array<Gem::Specification>]
def uncached_gemspecs
if @uncached_gemspecs.nil?
@uncached_gemspecs = []
pins # force lazy-loaded pin lookup
end
@uncached_gemspecs
end
# @param gemspec [Gem::Specification]
# @param out [IO]
# @return [void]
def cache_rbs_collection_pins(gemspec, out)
rbs_map = RbsMap.from_gemspec(gemspec, rbs_collection_path, rbs_collection_config_path)
pins = rbs_map.pins
rbs_version_cache_key = rbs_map.cache_key
# cache pins even if result is zero, so we don't retry building pins
pins ||= []
PinCache.serialize_rbs_collection_gem(gemspec, rbs_version_cache_key, pins)
logger.info { "Cached #{pins.length} RBS collection pins for gem #{gemspec.name} #{gemspec.version} with cache_key #{rbs_version_cache_key.inspect}" unless pins.empty? }
# @return [Array<Pin::Base>]
def pins
@pins ||= load_serialized_gem_pins + (workspace.global_environ&.pins || [])
end
# @param gemspec [Gem::Specification]
# @param rebuild [Boolean] whether to rebuild the pins even if they are cached
# @param out [IO, nil] output stream for logging
# @return [void]
def cache(gemspec, rebuild: false, out: nil)
build_yard = uncached_yard_gemspecs.include?(gemspec) || rebuild
build_rbs_collection = uncached_rbs_collection_gemspecs.include?(gemspec) || rebuild
if build_yard || build_rbs_collection
type = []
type << 'YARD' if build_yard
type << 'RBS collection' if build_rbs_collection
out.puts("Caching #{type.join(' and ')} pins for gem #{gemspec.name}:#{gemspec.version}") if out
end
cache_yard_pins(gemspec, out) if build_yard
cache_rbs_collection_pins(gemspec, out) if build_rbs_collection
def reset_pins!
@uncached_gemspecs = nil
@pins = nil
end
# @return [Array<Gem::Specification>]
def gemspecs
@gemspecs ||= required_gems_map.values.compact.flatten
# @return [Solargraph::PinCache]
def pin_cache
@pin_cache ||= workspace.fresh_pincache
end
# @return [Array<String>]
def unresolved_requires
@unresolved_requires ||= required_gems_map.select { |_, gemspecs| gemspecs.nil? }.keys
def any_uncached?
uncached_gemspecs.any?
end
# @return [Hash{Array(String, String) => Array<Pin::Base>}] Indexed by gemspec name and version
def self.all_yard_gems_in_memory
@yard_gems_in_memory ||= {}
end
# @return [Hash{String => Hash{Array(String, String) => Array<Pin::Base>}}] stored by RBS collection path
def self.all_rbs_collection_gems_in_memory
@rbs_collection_gems_in_memory ||= {}
end
# @return [Hash{Array(String, String) => Array<Pin::Base>}] Indexed by gemspec name and version
def yard_pins_in_memory
self.class.all_yard_gems_in_memory
end
# @return [Hash{Array(String, String) => Array<Pin::Base>}] Indexed by gemspec name and version
def rbs_collection_pins_in_memory
self.class.all_rbs_collection_gems_in_memory[rbs_collection_path] ||= {}
end
# @return [Hash{Array(String, String) => Array<Pin::Base>}] Indexed by gemspec name and version
def self.all_combined_pins_in_memory
@combined_pins_in_memory ||= {}
end
# @todo this should also include an index by the hash of the RBS collection
# @return [Hash{Array(String, String) => Array<Pin::Base>}] Indexed by gemspec name and version
def combined_pins_in_memory
self.class.all_combined_pins_in_memory
end
# @return [Array<String>]
def yard_plugins
@environ.yard_plugins
end
# @return [Set<Gem::Specification>]
def dependencies
@dependencies ||= (gemspecs.flat_map { |spec| fetch_dependencies(spec) } - gemspecs).to_set
end
private
# Cache all pins needed for the sources in this doc_map
# @param out [StringIO, IO, nil] output stream for logging
# @param rebuild [Boolean] whether to rebuild the pins even if they are cached
# @return [void]
def load_serialized_gem_pins
@pins = []
@uncached_yard_gemspecs = []
@uncached_rbs_collection_gemspecs = []
with_gemspecs, without_gemspecs = required_gems_map.partition { |_, v| v }
# @sg-ignore Need support for RBS duck interfaces like _ToHash
# @type [Array<String>]
paths = Hash[without_gemspecs].keys
# @sg-ignore Need support for RBS duck interfaces like _ToHash
# @type [Array<Gem::Specification>]
gemspecs = Hash[with_gemspecs].values.flatten.compact + dependencies.to_a
paths.each do |path|
rbs_pins = deserialize_stdlib_rbs_map path
def cache_doc_map_gems! out, rebuild: false
unless uncached_gemspecs.empty?
logger.info do
gem_desc = uncached_gemspecs.map { |gemspec| "#{gemspec.name}:#{gemspec.version}" }.join(', ')
"Caching pins for gems: #{gem_desc}"
end
end
logger.debug { "DocMap#load_serialized_gem_pins: Combining pins..." }
time = Benchmark.measure do
gemspecs.each do |gemspec|
pins = deserialize_combined_pin_cache gemspec
@pins.concat pins if pins
uncached_gemspecs.each do |gemspec|
cache(gemspec, rebuild: rebuild, out: out)
end
end
logger.info { "DocMap#load_serialized_gem_pins: Loaded and processed serialized pins together in #{time.real} seconds" }
@uncached_yard_gemspecs.uniq!
@uncached_rbs_collection_gemspecs.uniq!
nil
milliseconds = (time.real * 1000).round
if (milliseconds > 500) && uncached_gemspecs.any? && out && uncached_gemspecs.any?
out.puts "Built #{uncached_gemspecs.length} gems in #{milliseconds} ms"
end
reset_pins!
end
# @return [Hash{String => Array<Gem::Specification>}]
def required_gems_map
@required_gems_map ||= requires.to_h { |path| [path, resolve_path_to_gemspecs(path)] }
# @return [Array<String>]
def unresolved_requires
@unresolved_requires ||= required_gems_map.select { |_, gemspecs| gemspecs.nil? }.keys
end
# @return [Hash{String => Gem::Specification}]
def preference_map
@preference_map ||= preferences.to_h { |gemspec| [gemspec.name, gemspec] }
# @return [Array<Gem::Specification>]
# @param out [IO, nil]
def dependencies out: $stderr
@dependencies ||=
begin
gem_deps = gemspecs
.flat_map { |spec| workspace.fetch_dependencies(spec, out: out) }
.uniq(&:name)
stdlib_deps = gemspecs
.flat_map { |spec| workspace.stdlib_dependencies(spec.name) }
.flat_map { |dep_name| workspace.resolve_require(dep_name) }
.compact
existing_gems = gemspecs.map(&:name)
(gem_deps + stdlib_deps).reject { |gemspec| existing_gems.include? gemspec.name }
end
end
# Cache gem documentation if needed for this doc_map
#
# @param gemspec [Gem::Specification]
# @return [Array<Pin::Base>, nil]
def deserialize_yard_pin_cache gemspec
if yard_pins_in_memory.key?([gemspec.name, gemspec.version])
return yard_pins_in_memory[[gemspec.name, gemspec.version]]
end
cached = PinCache.deserialize_yard_gem(gemspec)
if cached
logger.info { "Loaded #{cached.length} cached YARD pins from #{gemspec.name}:#{gemspec.version}" }
yard_pins_in_memory[[gemspec.name, gemspec.version]] = cached
cached
else
logger.debug "No YARD pin cache for #{gemspec.name}:#{gemspec.version}"
@uncached_yard_gemspecs.push gemspec
nil
end
# @param rebuild [Boolean] whether to rebuild the pins even if they are cached
# @param out [StringIO, IO, nil] output stream for logging
#
# @return [void]
def cache gemspec, rebuild: false, out: nil
pin_cache.cache_gem(gemspec: gemspec,
rebuild: rebuild,
out: out)
end
# @param gemspec [Gem::Specification]
# @return [void]
def deserialize_combined_pin_cache(gemspec)
unless combined_pins_in_memory[[gemspec.name, gemspec.version]].nil?
return combined_pins_in_memory[[gemspec.name, gemspec.version]]
end
private
rbs_map = RbsMap.from_gemspec(gemspec, rbs_collection_path, rbs_collection_config_path)
rbs_version_cache_key = rbs_map.cache_key
# @return [Array<Gem::Specification>]
def gemspecs
@gemspecs ||= required_gems_map.values.compact.flatten
end
cached = PinCache.deserialize_combined_gem(gemspec, rbs_version_cache_key)
if cached
logger.info { "Loaded #{cached.length} cached YARD pins from #{gemspec.name}:#{gemspec.version}" }
combined_pins_in_memory[[gemspec.name, gemspec.version]] = cached
return combined_pins_in_memory[[gemspec.name, gemspec.version]]
end
# @param out [IO, nil]
# @return [Array<Pin::Base>]
def load_serialized_gem_pins out: @out
serialized_pins = []
with_gemspecs, without_gemspecs = required_gems_map.partition { |_, v| v }
# @sg-ignore Need better typing for Hash[]
# @type [Array<String>]
missing_paths = Hash[without_gemspecs].keys
# @type [Array<Gem::Specification>]
gemspecs = Hash[with_gemspecs].values.flatten.compact + dependencies(out: out).to_a
rbs_collection_pins = deserialize_rbs_collection_cache gemspec, rbs_version_cache_key
# if we are type checking a gem project, we should not include
# pins from rbs or yard from that gem here - we use our own
# parser for those pins
yard_pins = deserialize_yard_pin_cache gemspec
if !rbs_collection_pins.nil? && !yard_pins.nil?
logger.debug { "Combining pins for #{gemspec.name}:#{gemspec.version}" }
combined_pins = GemPins.combine(yard_pins, rbs_collection_pins)
PinCache.serialize_combined_gem(gemspec, rbs_version_cache_key, combined_pins)
combined_pins_in_memory[[gemspec.name, gemspec.version]] = combined_pins
logger.info { "Generated #{combined_pins_in_memory[[gemspec.name, gemspec.version]].length} combined pins for #{gemspec.name} #{gemspec.version}" }
return combined_pins
# @param gemspec [Gem::Specification, Bundler::LazySpecification, Bundler::StubSpecification]
gemspecs.reject! do |gemspec|
gemspec.respond_to?(:source) &&
gemspec.source.instance_of?(Bundler::Source::Gemspec) &&
gemspec.source.respond_to?(:path) &&
gemspec.source.path == Pathname.new('.')
end
if !yard_pins.nil?
logger.debug { "Using only YARD pins for #{gemspec.name}:#{gemspec.version}" }
combined_pins_in_memory[[gemspec.name, gemspec.version]] = yard_pins
return combined_pins_in_memory[[gemspec.name, gemspec.version]]
elsif !rbs_collection_pins.nil?
logger.debug { "Using only RBS collection pins for #{gemspec.name}:#{gemspec.version}" }
combined_pins_in_memory[[gemspec.name, gemspec.version]] = rbs_collection_pins
return combined_pins_in_memory[[gemspec.name, gemspec.version]]
else
logger.debug { "Pins not yet cached for #{gemspec.name}:#{gemspec.version}" }
return nil
end
end
missing_paths.each do |path|
# this will load from disk if needed; no need to manage
# uncached_gemspecs to trigger that later
stdlib_name_guess = path.split('/').first
# @param path [String] require path that might be in the RBS stdlib collection
# @return [void]
def deserialize_stdlib_rbs_map path
map = RbsMap::StdlibMap.load(path)
if map.resolved?
logger.debug { "Loading stdlib pins for #{path}" }
@pins.concat map.pins
logger.debug { "Loaded #{map.pins.length} stdlib pins for #{path}" }
map.pins
else
# @todo Temporarily ignoring unresolved `require 'set'`
logger.debug { "Require path #{path} could not be resolved in RBS" } unless path == 'set'
nil
# try to resolve the stdlib name
# @type [Array<String>]
deps = workspace.stdlib_dependencies(stdlib_name_guess) || []
[stdlib_name_guess, *deps].compact.each do |potential_stdlib_name|
# @sg-ignore Need to support splatting in literal array
rbs_pins = pin_cache.cache_stdlib_rbs_map potential_stdlib_name
serialized_pins.concat rbs_pins if rbs_pins
end
end
end
# @param gemspec [Gem::Specification]
# @param rbs_version_cache_key [String]
# @return [Array<Pin::Base>, nil]
def deserialize_rbs_collection_cache gemspec, rbs_version_cache_key
return if rbs_collection_pins_in_memory.key?([gemspec, rbs_version_cache_key])
cached = PinCache.deserialize_rbs_collection_gem(gemspec, rbs_version_cache_key)
if cached
logger.info { "Loaded #{cached.length} pins from RBS collection cache for #{gemspec.name}:#{gemspec.version}" } unless cached.empty?
rbs_collection_pins_in_memory[[gemspec, rbs_version_cache_key]] = cached
cached
else
logger.debug "No RBS collection pin cache for #{gemspec.name} #{gemspec.version}"
@uncached_rbs_collection_gemspecs.push gemspec
nil
end
end
# @param path [String]
# @return [::Array<Gem::Specification>, nil]
def resolve_path_to_gemspecs path
return nil if path.empty?
return gemspecs_required_from_bundler if path == 'bundler/require'
# @type [Gem::Specification, nil]
gemspec = Gem::Specification.find_by_path(path)
if gemspec.nil?
gem_name_guess = path.split('/').first
begin
# this can happen when the gem is included via a local path in
# a Gemfile; Gem doesn't try to index the paths in that case.
#
# See if we can make a good guess:
potential_gemspec = Gem::Specification.find_by_name(gem_name_guess)
file = "lib/#{path}.rb"
gemspec = potential_gemspec if potential_gemspec.files.any? { |gemspec_file| file == gemspec_file }
rescue Gem::MissingSpecError
logger.debug { "Require path #{path} could not be resolved to a gem via find_by_path or guess of #{gem_name_guess}" }
[]
existing_pin_count = serialized_pins.length
time = Benchmark.measure do
gemspecs.each do |gemspec|
# only deserializes already-cached gems
gemspec_pins = pin_cache.deserialize_combined_pin_cache gemspec
if gemspec_pins
serialized_pins.concat gemspec_pins
else
uncached_gemspecs << gemspec
end
end
end
return nil if gemspec.nil?
[gemspec_or_preference(gemspec)]
pins_processed = serialized_pins.length - existing_pin_count
milliseconds = (time.real * 1000).round
if (milliseconds > 500) && out && gemspecs.any?
out.puts "Deserialized #{serialized_pins.length} gem pins from #{PinCache.base_dir} in #{milliseconds} ms"
end
uncached_gemspecs.uniq! { |gemspec| "#{gemspec.name}:#{gemspec.version}" }
serialized_pins
end
# @param gemspec [Gem::Specification]
# @return [Gem::Specification]
def gemspec_or_preference gemspec
# :nocov: dormant feature
return gemspec unless preference_map.key?(gemspec.name)
return gemspec if gemspec.version == preference_map[gemspec.name].version
change_gemspec_version gemspec, preference_map[gemspec.name].version
# :nocov:
# @return [Hash{String => Array<Gem::Specification>}]
def required_gems_map
@required_gems_map ||= requires.to_h { |path| [path, workspace.resolve_require(path)] }
end
# @param gemspec [Gem::Specification]
# @param version [Gem::Version]
# @return [Gem::Specification]
def change_gemspec_version gemspec, version
Gem::Specification.find_by_name(gemspec.name, "= #{version}")
rescue Gem::MissingSpecError
Solargraph.logger.info "Gem #{gemspec.name} version #{version} not found. Using #{gemspec.version} instead"
gemspec
end
# @param gemspec [Gem::Specification]
# @return [Array<Gem::Specification>]
def fetch_dependencies gemspec
# @param spec [Gem::Dependency]
# @param deps [Set<Gem::Specification>]
only_runtime_dependencies(gemspec).each_with_object(Set.new) do |spec, deps|
Solargraph.logger.info "Adding #{spec.name} dependency for #{gemspec.name}"
dep = Gem.loaded_specs[spec.name]
# @todo is next line necessary?
# @sg-ignore Unresolved call to requirement on Gem::Dependency
dep ||= Gem::Specification.find_by_name(spec.name, spec.requirement)
deps.merge fetch_dependencies(dep) if deps.add?(dep)
rescue Gem::MissingSpecError
# @sg-ignore Unresolved call to requirement on Gem::Dependency
Solargraph.logger.warn "Gem dependency #{spec.name} #{spec.requirement} for #{gemspec.name} not found in RubyGems."
end.to_a
end
# @param gemspec [Gem::Specification]
# @return [Array<Gem::Dependency>]
def only_runtime_dependencies gemspec
gemspec.dependencies - gemspec.development_dependencies
end
def inspect
self.class.inspect
end
# @return [Array<Gem::Specification>, nil]
def gemspecs_required_from_bundler
# @todo Handle projects with custom Bundler/Gemfile setups
return unless workspace.gemfile?
if workspace.gemfile? && Bundler.definition&.lockfile&.to_s&.start_with?(workspace.directory)
# Find only the gems bundler is now using
Bundler.definition.locked_gems.specs.flat_map do |lazy_spec|
logger.info "Handling #{lazy_spec.name}:#{lazy_spec.version}"
[Gem::Specification.find_by_name(lazy_spec.name, lazy_spec.version)]
rescue Gem::MissingSpecError => e
logger.info("Could not find #{lazy_spec.name}:#{lazy_spec.version} with find_by_name, falling back to guess")
# can happen in local filesystem references
specs = resolve_path_to_gemspecs lazy_spec.name
logger.warn "Gem #{lazy_spec.name} #{lazy_spec.version} from bundle not found: #{e}" if specs.nil?
next specs
end.compact
else
logger.info 'Fetching gemspecs required from Bundler (bundler/require)'
gemspecs_required_from_external_bundle
end
end
# @return [Array<Gem::Specification>, nil]
def gemspecs_required_from_external_bundle
logger.info 'Fetching gemspecs required from external bundle'
return [] unless workspace&.directory
Solargraph.with_clean_env do
cmd = [
'ruby', '-e',
"require 'bundler'; require 'json'; Dir.chdir('#{workspace&.directory}') { puts Bundler.definition.locked_gems.specs.map { |spec| [spec.name, spec.version] }.to_h.to_json }"
]
o, e, s = Open3.capture3(*cmd)
if s.success?
Solargraph.logger.debug "External bundle: #{o}"
hash = o && !o.empty? ? JSON.parse(o.split("\n").last) : {}
hash.flat_map do |name, version|
Gem::Specification.find_by_name(name, version)
rescue Gem::MissingSpecError => e
logger.info("Could not find #{name}:#{version} with find_by_name, falling back to guess")
# can happen in local filesystem references
specs = resolve_path_to_gemspecs name
logger.warn "Gem #{name} #{version} from bundle not found: #{e}" if specs.nil?
next specs
end.compact
else
Solargraph.logger.warn "Failed to load gems from bundle at #{workspace&.directory}: #{e}"
end
end
end
end
end

@@ -15,3 +15,3 @@ # frozen_string_literal: true

self.class.eql?(other.class) &&
# @sg-ignore https://github.com/castwide/solargraph/pull/1114
# @sg-ignore flow sensitive typing should support .class == .class
equality_fields.eql?(other.equality_fields)

@@ -18,0 +18,0 @@ end

@@ -46,12 +46,2 @@ # frozen_string_literal: true

# @param yard_plugins [Array<String>] The names of YARD plugins to use.
# @param gemspec [Gem::Specification]
# @return [Array<Pin::Base>]
def self.build_yard_pins(yard_plugins, gemspec)
Yardoc.cache(yard_plugins, gemspec) unless Yardoc.cached?(gemspec)
return [] unless Yardoc.cached?(gemspec)
yardoc = Yardoc.load!(gemspec)
YardMap::Mapper.new(yardoc, gemspec).map
end
# @param yard_pins [Array<Pin::Base>]

@@ -63,10 +53,10 @@ # @param rbs_pins [Array<Pin::Base>]

in_yard = Set.new
rbs_api_map = Solargraph::ApiMap.new(pins: rbs_pins)
rbs_store = Solargraph::ApiMap::Store.new(rbs_pins)
combined = yard_pins.map do |yard_pin|
in_yard.add yard_pin.path
rbs_pin = rbs_api_map.get_path_pins(yard_pin.path).filter { |pin| pin.is_a? Pin::Method }.first
next yard_pin unless rbs_pin && yard_pin.class == Pin::Method
rbs_pin = rbs_store.get_path_pins(yard_pin.path).filter { |pin| pin.is_a? Pin::Method }.first
next yard_pin unless rbs_pin && yard_pin.is_a?(Pin::Method)
unless rbs_pin
# @sg-ignore https://github.com/castwide/solargraph/pull/1114
logger.debug { "GemPins.combine: No rbs pin for #{yard_pin.path} - using YARD's '#{yard_pin.inspect} (return_type=#{yard_pin.return_type}; signatures=#{yard_pin.signatures})" }

@@ -76,4 +66,13 @@ next yard_pin

# at this point both yard_pins and rbs_pins are methods or
# method aliases. if not plain methods, prefer the YARD one
next yard_pin if rbs_pin.class != Pin::Method
next rbs_pin if yard_pin.class != Pin::Method
# both are method pins
out = combine_method_pins(rbs_pin, yard_pin)
logger.debug { "GemPins.combine: Combining yard.path=#{yard_pin.path} - rbs=#{rbs_pin.inspect} with yard=#{yard_pin.inspect} into #{out}" }
logger.debug do
"GemPins.combine: Combining yard.path=#{yard_pin.path} - rbs=#{rbs_pin.inspect} with yard=#{yard_pin.inspect} into #{out}"
end
out

@@ -80,0 +79,0 @@ end

@@ -0,0 +0,0 @@ # frozen_string_literal: true

@@ -108,2 +108,3 @@ # frozen_string_literal: true

logger.warn "Error processing request: [#{e.class}] #{e.message}"
# @sg-ignore Need to add nil check here
logger.warn e.backtrace.join("\n")

@@ -304,4 +305,7 @@ message.set_error Solargraph::LanguageServer::ErrorCodes::INTERNAL_ERROR, "[#{e.class}] #{e.message}"

# @sg-ignore Need to validate config
# @return [String]
# @sg-ignore Need to validate config
def command_path
# @type [String]
options['commandPath'] || 'solargraph'

@@ -734,5 +738,7 @@ end

# @param path [String]
# @sg-ignore Need to be able to choose signature on String#gsub
# @return [String]
def normalize_separators path
return path if File::ALT_SEPARATOR.nil?
# @sg-ignore flow sensitive typing needs to handle constants
path.gsub(File::ALT_SEPARATOR, File::SEPARATOR)

@@ -771,3 +777,2 @@ end

return change if diffs.length.zero? || diffs.length > 1 || diffs.first.length > 1
# @sg-ignore push this upstream
# @type [Diff::LCS::Change]

@@ -774,0 +779,0 @@ diff = diffs.first.first

@@ -0,0 +0,0 @@ # frozen_string_literal: true

@@ -36,2 +36,3 @@ # frozen_string_literal: true

src = sources.find(uri)
# @sg-ignore Need to add nil check here
using = libraries.select { |lib| lib.contain?(src.filename) }

@@ -38,0 +39,0 @@ using.push library_for(uri) if using.empty?

@@ -31,3 +31,3 @@ # frozen_string_literal: true

# pending handle messages
# @return [Array<Hash>]
# @return [Array<Hash{String => undefined}>]
def messages

@@ -70,2 +70,3 @@ @messages ||= []

end
# @sg-ignore Need to add nil check here
handler = @host.receive(message)

@@ -72,0 +73,0 @@ handler&.send_response

@@ -58,2 +58,3 @@ # frozen_string_literal: true

# @param uri [String]
# @sg-ignore flow ensitive typing should understand raise
# @return [Solargraph::Source]

@@ -60,0 +61,0 @@ def find uri

@@ -0,0 +0,0 @@ # frozen_string_literal: true

@@ -0,0 +0,0 @@ # frozen_string_literal: true

@@ -0,0 +0,0 @@ # frozen_string_literal: true

@@ -0,0 +0,0 @@ # frozen_string_literal: true

# frozen_string_literal: true
# @todo PR the RBS gem to add this
# @!parse
# module ::Gem
# class SpecFetcher; end
# end
module Solargraph

@@ -67,2 +61,3 @@ module LanguageServer

Solargraph::Logging.logger.warn error
# @sg-ignore Need to add nil check here
host.show_message(error, MessageTypes::ERROR) if params['verbose']

@@ -82,2 +77,3 @@ end

# @return [Gem::Version]
# @sg-ignore Need to add nil check here
def available

@@ -88,3 +84,2 @@ if !@available && !@fetched

@available ||= begin
# @sg-ignore Variable type could not be inferred for tuple
# @type [Gem::Dependency, nil]

@@ -91,0 +86,0 @@ tuple = CheckGemVersion.fetcher.search_for_dependency(Gem::Dependency.new('solargraph')).flatten.first

@@ -0,0 +0,0 @@ # frozen_string_literal: true

@@ -17,2 +17,3 @@ # frozen_string_literal: true

Solargraph.logger.warn "Error processing document: [#{e.class}] #{e.message}"
# @sg-ignore Need to add nil check here
Solargraph.logger.debug e.backtrace.join("\n")

@@ -19,0 +20,0 @@ end

@@ -0,0 +0,0 @@ # frozen_string_literal: true

@@ -0,0 +0,0 @@ # frozen_string_literal: true

@@ -0,0 +0,0 @@ # frozen_string_literal: true

@@ -18,2 +18,3 @@ # frozen_string_literal: true

idx = -1
# @sg-ignore Need to add nil check here
completion.pins.each do |pin|

@@ -41,2 +42,3 @@ idx += 1 if last_context != pin.context

Logging.logger.warn "[#{e.class}] #{e.message}"
# @sg-ignore Need to add nil check here
Logging.logger.warn e.backtrace.join("\n")

@@ -43,0 +45,0 @@ set_result empty_result

@@ -16,3 +16,5 @@ # frozen_string_literal: true

suggestions = host.definitions_at(params['textDocument']['uri'], @line, @column)
# @sg-ignore Need to add nil check here
return nil if suggestions.empty?
# @sg-ignore Need to add nil check here
suggestions.reject { |pin| pin.best_location.nil? || pin.best_location.filename.nil? }.map do |pin|

@@ -19,0 +21,0 @@ {

@@ -0,0 +0,0 @@ # frozen_string_literal: true

@@ -16,3 +16,5 @@ # frozen_string_literal: true

location: {
# @sg-ignore Need to add nil check here
uri: file_to_uri(pin.best_location.filename),
# @sg-ignore Need to add nil check here
range: pin.best_location.range.to_hash

@@ -19,0 +21,0 @@ },

@@ -101,5 +101,7 @@ # frozen_string_literal: true

# @param value [Array, String]
#
# @return [String, nil]
def cop_list(value)
# @type [String]
# @sg-ignore Translate to something flow sensitive typing understands
value = value.join(',') if value.respond_to?(:join)

@@ -106,0 +108,0 @@ return nil if value == '' || !value.is_a?(String)

@@ -14,2 +14,3 @@ # frozen_string_literal: true

last_link = nil
# @sg-ignore Need to add nil check here
suggestions.each do |pin|

@@ -35,2 +36,3 @@ parts = []

Logging.logger.warn "[#{e.class}] #{e.message}"
# @sg-ignore Need to add nil check here
Logging.logger.warn e.backtrace.join("\n")

@@ -37,0 +39,0 @@ set_result nil

@@ -0,0 +0,0 @@ # frozen_string_literal: true

@@ -0,0 +0,0 @@ # frozen_string_literal: true

@@ -0,0 +0,0 @@ # frozen_string_literal: true

@@ -17,2 +17,3 @@ # frozen_string_literal: true

Logging.logger.warn "[#{e.class}] #{e.message}"
# @sg-ignore Need to add nil check here
Logging.logger.warn e.backtrace.join("\n")

@@ -19,0 +20,0 @@ set_result nil

@@ -16,3 +16,5 @@ # frozen_string_literal: true

suggestions = host.type_definitions_at(params['textDocument']['uri'], @line, @column)
# @sg-ignore Need to add nil check here
return nil if suggestions.empty?
# @sg-ignore Need to add nil check here
suggestions.reject { |pin| pin.best_location.nil? || pin.best_location.filename.nil? }.map do |pin|

@@ -19,0 +21,0 @@ {

@@ -0,0 +0,0 @@ # frozen_string_literal: true

@@ -0,0 +0,0 @@ # frozen_string_literal: true

@@ -0,0 +0,0 @@ # frozen_string_literal: true

@@ -9,2 +9,3 @@ # frozen_string_literal: true

info = pins.map do |pin|
# @sg-ignore Need to add nil check here
uri = file_to_uri(pin.best_location.filename)

@@ -17,2 +18,3 @@ {

uri: uri,
# @sg-ignore Need to add nil check here
range: pin.best_location.range.to_hash

@@ -19,0 +21,0 @@ },

@@ -0,0 +0,0 @@ # frozen_string_literal: true

@@ -0,0 +0,0 @@ # frozen_string_literal: true

@@ -0,0 +0,0 @@ # frozen_string_literal: true

# frozen_string_literal: true
require 'rubygems'
require 'pathname'

@@ -7,2 +8,8 @@ require 'observer'

# @!parse
# class ::Gem::Specification
# # @return [String]
# def name; end
# end
module Solargraph

@@ -37,2 +44,3 @@ # A Library handles coordination between a Workspace and an ApiMap.

@sync_count = 0
@cache_progress = nil
end

@@ -62,4 +70,7 @@

def attach source
# @sg-ignore Need to add nil check here
if @current && (!source || @current.filename != source.filename) && source_map_hash.key?(@current.filename) && !workspace.has_file?(@current.filename)
# @sg-ignore Need to add nil check here
source_map_hash.delete @current.filename
# @sg-ignore Need to add nil check here
source_map_external_require_hash.delete @current.filename

@@ -188,5 +199,10 @@ @external_requires = nil

offset = Solargraph::Position.to_offset(source.code, Solargraph::Position.new(line, column))
# @sg-ignore Need to add nil check here
# @type [MatchData, nil]
lft = source.code[0..offset-1].match(/\[[a-z0-9_:<, ]*?([a-z0-9_:]*)\z/i)
# @sg-ignore Need to add nil check here
# @type [MatchData, nil]
rgt = source.code[offset..-1].match(/^([a-z0-9_]*)(:[a-z0-9_:]*)?[\]>, ]/i)
if lft && rgt
# @sg-ignore Need to add nil check here
tag = (lft[1] + rgt[1]).sub(/:+$/, '')

@@ -262,2 +278,4 @@ clip = mutex.synchronize { api_map.clip(cursor) }

found.select! do |loc|
# @sg-ignore Need to add nil check here
# @type [Solargraph::Pin::Base, nil]
referenced = definitions_at(loc.filename, loc.range.ending.line, loc.range.ending.character).first

@@ -267,2 +285,3 @@ referenced&.path == pin.path

if pin.path == 'Class#new'
# @todo flow sensitive typing should allow shadowing of Kernel#caller
caller = cursor.chain.base.infer(api_map, clip.send(:closure), clip.locals).first

@@ -273,2 +292,3 @@ if caller.defined?

other = clip.send(:cursor).chain.base.infer(api_map, clip.send(:closure), clip.locals).first
# @todo flow sensitive typing should allow shadowing of Kernel#caller
caller == other

@@ -283,8 +303,9 @@ end

found.map! do |loc|
Solargraph::Location.new(loc.filename, Solargraph::Range.from_to(loc.range.start.line, loc.range.start.column + match[0].length, loc.range.ending.line, loc.range.ending.column))
Solargraph::Location.new(loc.filename,
# @sg-ignore flow sensitive typing needs to handle if foo = bar
Solargraph::Range.from_to(loc.range.start.line, loc.range.start.column + match[0].length, loc.range.ending.line,
loc.range.ending.column))
end
end
result.concat(found.sort do |a, b|
a.range.start.line <=> b.range.start.line
end)
result.concat(found.sort { |a, b| a.range.start.line <=> b.range.start.line })
end

@@ -310,2 +331,3 @@ result.uniq

return if map.nil?
# @sg-ignore Need to add nil check here
pin = map.requires.select { |p| p.location.range.contain?(location.range.start) }.first

@@ -315,5 +337,3 @@ return nil if pin.nil?

return_if_match = proc do |full|
if source_map_hash.key?(full)
return Location.new(full, Solargraph::Range.from_to(0, 0, 0, 0))
end
return Location.new(full, Solargraph::Range.from_to(0, 0, 0, 0)) if source_map_hash.key?(full)
end

@@ -416,2 +436,3 @@ workspace.require_paths.each do |path|

Diagnostics.reporters.each do |reporter_name|
# @sg-ignore Need to add nil check here
repargs[Diagnostics.reporter(reporter_name)] ||= []

@@ -424,3 +445,5 @@ end

raise DiagnosticsError, "Diagnostics reporter #{name} does not exist" if reporter.nil?
# @sg-ignore flow sensitive typing needs to handle 'raise if'
repargs[reporter] ||= []
# @sg-ignore flow sensitive typing needs to handle 'raise if'
repargs[reporter].concat args

@@ -448,2 +471,3 @@ end

external_requires: external_requires,
# @sg-ignore Need to add nil check here
live_map: @current ? source_map_hash[@current.filename] : nil

@@ -485,6 +509,9 @@ )

return false if mapped?
# @sg-ignore Need to add nil check here
src = workspace.sources.find { |s| !source_map_hash.key?(s.filename) }
if src
Logging.logger.debug "Mapping #{src.filename}"
# @sg-ignore Need to add nil check here
source_map_hash[src.filename] = Solargraph::SourceMap.map(src)
# @sg-ignore Need to add nil check here
source_map_hash[src.filename]

@@ -499,3 +526,5 @@ else

workspace.sources.each do |src|
# @sg-ignore Need to add nil check here
source_map_hash[src.filename] = Solargraph::SourceMap.map(src)
# @sg-ignore Need to add nil check here
find_external_requires source_map_hash[src.filename]

@@ -518,2 +547,7 @@ end

# @return [PinCache]
def pin_cache
workspace.pin_cache
end
# @return [Hash{String => Array<String>}]

@@ -532,2 +566,3 @@ def source_map_external_require_hash

filenames = ->{ _filenames ||= workspace.filenames.to_set }
# @sg-ignore Need to add nil check here
source_map_external_require_hash[source_map.filename] = new_set.reject do |path|

@@ -582,7 +617,11 @@ workspace.require_paths.any? do |base|

return unless source
# @sg-ignore Need to add nil check here
return unless @current == source || workspace.has_file?(source.filename)
# @sg-ignore Need to add nil check here
if source_map_hash.key?(source.filename)
new_map = Solargraph::SourceMap.map(source)
# @sg-ignore Need to add nil check here
source_map_hash[source.filename] = new_map
else
# @sg-ignore Need to add nil check here
source_map_hash[source.filename] = Solargraph::SourceMap.map(source)

@@ -606,3 +645,3 @@ end

if Yardoc.processing?(spec)
if pin_cache.yardoc_processing?(spec)
logger.info "Enqueuing cache of #{spec.name} #{spec.version} (already being processed)"

@@ -618,3 +657,6 @@ queued_gemspec_cache.push(spec)

report_cache_progress spec.name, pending
_o, e, s = Open3.capture3(workspace.command_path, 'cache', spec.name, spec.version.to_s)
kwargs = {}
kwargs[:chdir] = workspace.directory.to_s if workspace.directory && !workspace.directory.empty?
_o, e, s = Open3.capture3(workspace.command_path, 'cache', spec.name, spec.version.to_s,
**kwargs)
if s.success?

@@ -636,4 +678,3 @@ logger.info "Cached #{spec.name} #{spec.version}"

def cacheable_specs
cacheable = api_map.uncached_yard_gemspecs +
api_map.uncached_rbs_collection_gemspecs -
cacheable = api_map.uncached_gemspecs +
queued_gemspec_cache -

@@ -656,7 +697,11 @@ cache_errors.to_a

@total ||= pending
# @sg-ignore flow sensitive typing needs better handling of ||= on lvars
@total = pending if pending > @total
# @sg-ignore flow sensitive typing needs better handling of ||= on lvars
finished = @total - pending
# @sg-ignore flow sensitive typing needs better handling of ||= on lvars
pct = if @total.zero?
0
else
# @sg-ignore flow sensitive typing needs better handling of ||= on lvars
((finished.to_f / @total.to_f) * 100).to_i

@@ -672,5 +717,7 @@ end

# might get stuck in the status bar forever
# @sg-ignore flow sensitive typing should be able to handle redefinition
@cache_progress.begin(message, pct)
changed
notify_observers @cache_progress
# @sg-ignore flow sensitive typing should be able to handle redefinition
@cache_progress.report(message, pct)

@@ -699,4 +746,3 @@ end

logger.info "Catalog complete (#{api_map.source_maps.length} files, #{api_map.pins.length} pins)"
logger.info "#{api_map.uncached_yard_gemspecs.length} uncached YARD gemspecs"
logger.info "#{api_map.uncached_rbs_collection_gemspecs.length} uncached RBS collection gemspecs"
logger.info "#{api_map.uncached_gemspecs.length} uncached gemspecs"
cache_next_gemspec

@@ -703,0 +749,0 @@ @sync_count = 0

@@ -9,2 +9,3 @@ # frozen_string_literal: true

include Equality
include Comparable

@@ -17,5 +18,7 @@ # @return [String]

# @param filename [String]
# @param filename [String, nil]
# @param range [Solargraph::Range]
def initialize filename, range
raise "Use nil to represent no-file" if filename&.empty?
@filename = filename

@@ -25,3 +28,2 @@ @range = range

# @sg-ignore Fix "Not enough arguments to Module#protected"
protected def equality_fields

@@ -70,4 +72,8 @@ [filename, range]

return nil if node.nil? || node.loc.nil?
filename = node.loc.expression.source_buffer.name
# @sg-ignore flow sensitive typing needs to create separate ranges for postfix if
filename = nil if filename.empty?
range = Range.from_node(node)
self.new(node.loc.expression.source_buffer.name, range)
# @sg-ignore Need to add nil check here
self.new(filename, range)
end

@@ -78,3 +84,2 @@

return false unless other.is_a?(Location)
# @sg-ignore https://github.com/castwide/solargraph/pull/1114
filename == other.filename and range == other.range

@@ -81,0 +86,0 @@ end

@@ -32,7 +32,27 @@ # frozen_string_literal: true

# override this in your class to temporarily set a custom
# filtering log level for the class (e.g., suppress any debug
# message by setting it to :info even if it is set elsewhere, or
# show existing debug messages by setting to :debug).
#
# @return [Symbol]
def log_level
:warn
end
# @return [Logger]
def logger
@@logger
if LOG_LEVELS[log_level.to_s] == DEFAULT_LOG_LEVEL
@@logger
else
new_log_level = LOG_LEVELS[log_level.to_s]
logger = Logger.new(STDERR, level: new_log_level)
# @sg-ignore Wrong argument type for Logger#formatter=: arg_0
# expected nil, received Logger::_Formatter, nil
logger.formatter = @@logger.formatter
logger
end
end
end
end

@@ -0,0 +0,0 @@ # frozen_string_literal: true

@@ -0,0 +0,0 @@ module Solargraph

@@ -26,2 +26,3 @@ require 'ripper'

result = super
# @sg-ignore Need to add nil check here
if @buffer_lines[result[2][0]][0..result[2][1]].strip =~ /^#/

@@ -44,4 +45,6 @@ chomped = result[1].chomp

# @sg-ignore @override is adding, not overriding
def on_embdoc_beg *args
result = super
# @sg-ignore @override is adding, not overriding
create_snippet(result)

@@ -51,4 +54,6 @@ result

# @sg-ignore @override is adding, not overriding
def on_embdoc *args
result = super
# @sg-ignore @override is adding, not overriding
create_snippet(result)

@@ -58,4 +63,6 @@ result

# @sg-ignore @override is adding, not overriding
def on_embdoc_end *args
result = super
# @sg-ignore @override is adding, not overriding
create_snippet(result)

@@ -62,0 +69,0 @@ result

@@ -6,7 +6,11 @@ module Solargraph

# @param locals [Array<Solargraph::Pin::LocalVariable, Solargraph::Pin::Parameter>]
# @param locals [Array<Solargraph::Pin::LocalVariable>]
# @param ivars [Array<Solargraph::Pin::InstanceVariable>]
# @param enclosing_breakable_pin [Solargraph::Pin::Breakable, nil]
def initialize(locals, enclosing_breakable_pin = nil)
# @param enclosing_compound_statement_pin [Solargraph::Pin::CompoundStatement, nil]
def initialize(locals, ivars, enclosing_breakable_pin, enclosing_compound_statement_pin)
@locals = locals
@ivars = ivars
@enclosing_breakable_pin = enclosing_breakable_pin
@enclosing_compound_statement_pin = enclosing_compound_statement_pin
end

@@ -16,5 +20,8 @@

# @param true_ranges [Array<Range>]
# @param false_ranges [Array<Range>]
#
# @return [void]
def process_and(and_node, true_ranges = [])
def process_and(and_node, true_ranges = [], false_ranges = [])
return unless and_node.type == :and
# @type [Parser::AST::Node]

@@ -30,9 +37,60 @@ lhs = and_node.children[0]

get_node_end_position(rhs))
process_isa(lhs, true_ranges + [rhs_presence])
# can't assume if an and is false that every single condition
# is false, so don't provide any false ranges to assert facts
# on
process_expression(lhs, true_ranges + [rhs_presence], [])
process_expression(rhs, true_ranges, [])
end
# @param or_node [Parser::AST::Node]
# @param true_ranges [Array<Range>]
# @param false_ranges [Array<Range>]
#
# @return [void]
def process_or(or_node, true_ranges = [], false_ranges = [])
return unless or_node.type == :or
# @type [Parser::AST::Node]
lhs = or_node.children[0]
# @type [Parser::AST::Node]
rhs = or_node.children[1]
before_rhs_loc = rhs.location.expression.adjust(begin_pos: -1)
before_rhs_pos = Position.new(before_rhs_loc.line, before_rhs_loc.column)
rhs_presence = Range.new(before_rhs_pos,
get_node_end_position(rhs))
# can assume if an or is false that every single condition is
# false, so provide false ranges to assert facts on
# can't assume if an or is true that every single condition is
# true, so don't provide true ranges to assert facts on
process_expression(lhs, [], false_ranges + [rhs_presence])
process_expression(rhs, [], false_ranges)
end
# @param node [Parser::AST::Node]
# @param true_presences [Array<Range>]
# @param false_presences [Array<Range>]
#
# @return [void]
def process_calls(node, true_presences, false_presences)
return unless node.type == :send
process_isa(node, true_presences, false_presences)
process_nilp(node, true_presences, false_presences)
process_bang(node, true_presences, false_presences)
end
# @param if_node [Parser::AST::Node]
# @param true_ranges [Array<Range>]
# @param false_ranges [Array<Range>]
#
# @return [void]
def process_if(if_node)
def process_if(if_node, true_ranges = [], false_ranges = [])
return if if_node.type != :if
#

@@ -50,12 +108,16 @@ # See if we can refine a type based on the result of 'if foo.nil?'

conditional_node = if_node.children[0]
# @type [Parser::AST::Node]
# @type [Parser::AST::Node, nil]
then_clause = if_node.children[1]
# @type [Parser::AST::Node]
# @type [Parser::AST::Node, nil]
else_clause = if_node.children[2]
true_ranges = []
if always_breaks?(else_clause)
unless enclosing_breakable_pin.nil?
rest_of_breakable_body = Range.new(get_node_end_position(if_node),
get_node_end_position(enclosing_breakable_pin.node))
unless enclosing_breakable_pin.nil?
rest_of_breakable_body = Range.new(get_node_end_position(if_node),
get_node_end_position(enclosing_breakable_pin.node))
if always_breaks?(then_clause)
false_ranges << rest_of_breakable_body
end
if always_breaks?(else_clause)
true_ranges << rest_of_breakable_body

@@ -65,5 +127,23 @@ end

unless enclosing_compound_statement_pin.node.nil?
rest_of_returnable_body = Range.new(get_node_end_position(if_node),
get_node_end_position(enclosing_compound_statement_pin.node))
#
# if one of the clauses always leaves the compound
# statement, we can assume things about the rest of the
# compound statement
#
if always_leaves_compound_statement?(then_clause)
false_ranges << rest_of_returnable_body
end
if always_leaves_compound_statement?(else_clause)
true_ranges << rest_of_returnable_body
end
end
unless then_clause.nil?
#
# Add specialized locals for the then clause range
# If the condition is true we can assume things about the then clause
#

@@ -76,47 +156,52 @@ before_then_clause_loc = then_clause.location.expression.adjust(begin_pos: -1)

process_conditional(conditional_node, true_ranges)
end
unless else_clause.nil?
#
# If the condition is true we can assume things about the else clause
#
before_else_clause_loc = else_clause.location.expression.adjust(begin_pos: -1)
before_else_clause_pos = Position.new(before_else_clause_loc.line, before_else_clause_loc.column)
false_ranges << Range.new(before_else_clause_pos,
get_node_end_position(else_clause))
end
class << self
include Logging
process_expression(conditional_node, true_ranges, false_ranges)
end
# Find a variable pin by name and where it is used.
# @param while_node [Parser::AST::Node]
# @param true_ranges [Array<Range>]
# @param false_ranges [Array<Range>]
#
# Resolves our most specific view of this variable's type by
# preferring pins created by flow-sensitive typing when we have
# them based on the Closure and Location.
#
# @param pins [Array<Pin::LocalVariable>]
# @param name [String]
# @param closure [Pin::Closure]
# @param location [Location]
#
# @return [Array<Pin::LocalVariable>]
def self.visible_pins(pins, name, closure, location)
logger.debug { "FlowSensitiveTyping#visible_pins(name=#{name}, closure=#{closure}, location=#{location})" }
pins_with_name = pins.select { |p| p.name == name }
if pins_with_name.empty?
logger.debug { "FlowSensitiveTyping#visible_pins(name=#{name}, closure=#{closure}, location=#{location}) => [] - no pins with name" }
return []
# @return [void]
def process_while(while_node, true_ranges = [], false_ranges = [])
return if while_node.type != :while
#
# See if we can refine a type based on the result of 'if foo.nil?'
#
# [3] pry(main)> Parser::CurrentRuby.parse("while a; b; c; end")
# => s(:while,
# s(:send, nil, :a),
# s(:begin,
# s(:send, nil, :b),
# s(:send, nil, :c)))
# [4] pry(main)>
conditional_node = while_node.children[0]
# @type [Parser::AST::Node, nil]
do_clause = while_node.children[1]
unless do_clause.nil?
#
# If the condition is true we can assume things about the do clause
#
before_do_clause_loc = do_clause.location.expression.adjust(begin_pos: -1)
before_do_clause_pos = Position.new(before_do_clause_loc.line, before_do_clause_loc.column)
true_ranges << Range.new(before_do_clause_pos,
get_node_end_position(do_clause))
end
pins_with_specific_visibility = pins.select { |p| p.name == name && p.presence && p.visible_at?(closure, location) }
if pins_with_specific_visibility.empty?
logger.debug { "FlowSensitiveTyping#visible_pins(name=#{name}, closure=#{closure}, location=#{location}) => #{pins_with_name} - no pins with specific visibility" }
return pins_with_name
end
visible_pins_specific_to_this_closure = pins_with_specific_visibility.select { |p| p.closure == closure }
if visible_pins_specific_to_this_closure.empty?
logger.debug { "FlowSensitiveTyping#visible_pins(name=#{name}, closure=#{closure}, location=#{location}) => #{pins_with_specific_visibility} - no visible pins specific to this closure (#{closure})}" }
return pins_with_specific_visibility
end
flow_defined_pins = pins_with_specific_visibility.select { |p| p.presence_certain? }
if flow_defined_pins.empty?
logger.debug { "FlowSensitiveTyping#visible_pins(name=#{name}, closure=#{closure}, location=#{location}) => #{visible_pins_specific_to_this_closure} - no flow-defined pins" }
return visible_pins_specific_to_this_closure
end
logger.debug { "FlowSensitiveTyping#visible_pins(name=#{name}, closure=#{closure}, location=#{location}) => #{flow_defined_pins}" }
process_expression(conditional_node, true_ranges, false_ranges)
end
flow_defined_pins
class << self
include Logging
end

@@ -128,24 +213,23 @@

# @param pin [Pin::LocalVariable]
# @param downcast_type_name [String]
# @param pin [Pin::BaseVariable]
# @param presence [Range]
# @param downcast_type [ComplexType, nil]
# @param downcast_not_type [ComplexType, nil]
#
# @return [void]
def add_downcast_local(pin, downcast_type_name, presence)
# @todo Create pin#update method
new_pin = Solargraph::Pin::LocalVariable.new(
location: pin.location,
closure: pin.closure,
name: pin.name,
assignment: pin.assignment,
comments: pin.comments,
presence: presence,
return_type: ComplexType.try_parse(downcast_type_name),
presence_certain: true,
source: :flow_sensitive_typing
)
locals.push(new_pin)
def add_downcast_var(pin, presence:, downcast_type:, downcast_not_type:)
new_pin = pin.downcast(exclude_return_type: downcast_not_type,
intersection_return_type: downcast_type,
source: :flow_sensitive_typing,
presence: presence)
if pin.is_a?(Pin::LocalVariable)
locals.push(new_pin)
elsif pin.is_a?(Pin::InstanceVariable)
ivars.push(new_pin)
else
raise "Tried to add invalid pin type #{pin.class} in FlowSensitiveTyping"
end
end
# @param facts_by_pin [Hash{Pin::LocalVariable => Array<Hash{Symbol => String}>}]
# @param facts_by_pin [Hash{Pin::BaseVariable => Array<Hash{:type, :not_type => ComplexType}>}]
# @param presences [Array<Range>]

@@ -156,9 +240,13 @@ #

#
# Add specialized locals for the rest of the block
# Add specialized vars for the rest of the block
#
facts_by_pin.each_pair do |pin, facts|
facts.each do |fact|
downcast_type_name = fact.fetch(:type)
downcast_type = fact.fetch(:type, nil)
downcast_not_type = fact.fetch(:not_type, nil)
presences.each do |presence|
add_downcast_local(pin, downcast_type_name, presence)
add_downcast_var(pin,
presence: presence,
downcast_type: downcast_type,
downcast_not_type: downcast_not_type)
end

@@ -169,18 +257,21 @@ end

# @param conditional_node [Parser::AST::Node]
# @param expression_node [Parser::AST::Node]
# @param true_ranges [Array<Range>]
# @param false_ranges [Array<Range>]
#
# @return [void]
def process_conditional(conditional_node, true_ranges)
if conditional_node.type == :send
process_isa(conditional_node, true_ranges)
elsif conditional_node.type == :and
process_and(conditional_node, true_ranges)
end
def process_expression(expression_node, true_ranges, false_ranges)
process_calls(expression_node, true_ranges, false_ranges)
process_and(expression_node, true_ranges, false_ranges)
process_or(expression_node, true_ranges, false_ranges)
process_variable(expression_node, true_ranges, false_ranges)
end
# @param isa_node [Parser::AST::Node]
# @return [Array(String, String), nil]
def parse_isa(isa_node)
return unless isa_node&.type == :send && isa_node.children[1] == :is_a?
# @param call_node [Parser::AST::Node]
# @param method_name [Symbol]
# @return [Array(String, String), nil] Tuple of rgument to
# function, then receiver of function if it's a variable,
# otherwise nil if no simple variable receiver
def parse_call(call_node, method_name)
return unless call_node&.type == :send && call_node.children[1] == method_name
# Check if conditional node follows this pattern:

@@ -190,28 +281,45 @@ # s(:send,

# s(:const, nil, :Baz)),
isa_receiver = isa_node.children[0]
isa_type_name = type_name(isa_node.children[2])
return unless isa_type_name
#
call_receiver = call_node.children[0]
call_arg = type_name(call_node.children[2])
# check if isa_receiver looks like this:
# check if call_receiver looks like this:
# s(:send, nil, :foo)
# and set variable_name to :foo
if isa_receiver&.type == :send && isa_receiver.children[0].nil? && isa_receiver.children[1].is_a?(Symbol)
variable_name = isa_receiver.children[1].to_s
if call_receiver&.type == :send && call_receiver.children[0].nil? && call_receiver.children[1].is_a?(Symbol)
variable_name = call_receiver.children[1].to_s
end
# or like this:
# (lvar :repr)
variable_name = isa_receiver.children[0].to_s if isa_receiver&.type == :lvar
# @sg-ignore Need to look at Tuple#include? handling
variable_name = call_receiver.children[0].to_s if [:lvar, :ivar].include?(call_receiver&.type)
return unless variable_name
[isa_type_name, variable_name]
[call_arg, variable_name]
end
# @param isa_node [Parser::AST::Node]
# @return [Array(String, String), nil]
def parse_isa(isa_node)
call_type_name, variable_name = parse_call(isa_node, :is_a?)
return unless call_type_name
[call_type_name, variable_name]
end
# @param variable_name [String]
# @param position [Position]
#
# @return [Solargraph::Pin::LocalVariable, nil]
def find_local(variable_name, position)
pins = locals.select { |pin| pin.name == variable_name && pin.presence.include?(position) }
return unless pins.length == 1
pins.first
# @sg-ignore Solargraph::Parser::FlowSensitiveTyping#find_var
# return type could not be inferred
# @return [Solargraph::Pin::LocalVariable, Solargraph::Pin::InstanceVariable, nil]
def find_var(variable_name, position)
if variable_name.start_with?('@')
# @sg-ignore flow sensitive typing needs to handle attrs
ivars.find { |ivar| ivar.name == variable_name && (!ivar.presence || ivar.presence.include?(position)) }
else
# @sg-ignore flow sensitive typing needs to handle attrs
locals.find { |pin| pin.name == variable_name && (!pin.presence || pin.presence.include?(position)) }
end
end

@@ -221,19 +329,127 @@

# @param true_presences [Array<Range>]
# @param false_presences [Array<Range>]
#
# @return [void]
def process_isa(isa_node, true_presences)
def process_isa(isa_node, true_presences, false_presences)
isa_type_name, variable_name = parse_isa(isa_node)
return if variable_name.nil? || variable_name.empty?
# @sg-ignore Need to add nil check here
isa_position = Range.from_node(isa_node).start
pin = find_local(variable_name, isa_position)
pin = find_var(variable_name, isa_position)
return unless pin
# @type Hash{Pin::BaseVariable => Array<Hash{Symbol => ComplexType}>}
if_true = {}
if_true[pin] ||= []
if_true[pin] << { type: isa_type_name }
if_true[pin] << { type: ComplexType.parse(isa_type_name) }
process_facts(if_true, true_presences)
# @type Hash{Pin::BaseVariable => Array<Hash{Symbol => ComplexType}>}
if_false = {}
if_false[pin] ||= []
if_false[pin] << { not_type: ComplexType.parse(isa_type_name) }
process_facts(if_false, false_presences)
end
# @param nilp_node [Parser::AST::Node]
# @return [Array(String, String), nil]
def parse_nilp(nilp_node)
parse_call(nilp_node, :nil?)
end
# @param nilp_node [Parser::AST::Node]
# @param true_presences [Array<Range>]
# @param false_presences [Array<Range>]
#
# @return [void]
def process_nilp(nilp_node, true_presences, false_presences)
nilp_arg, variable_name = parse_nilp(nilp_node)
return if variable_name.nil? || variable_name.empty?
# if .nil? got an argument, move on, this isn't the situation
# we're looking for and typechecking will cover any invalid
# ones
return unless nilp_arg.nil?
# @sg-ignore Need to add nil check here
nilp_position = Range.from_node(nilp_node).start
pin = find_var(variable_name, nilp_position)
return unless pin
# @type Hash{Pin::LocalVariable => Array<Hash{Symbol => ComplexType}>}
if_true = {}
if_true[pin] ||= []
if_true[pin] << { type: ComplexType::NIL }
process_facts(if_true, true_presences)
# @type Hash{Pin::LocalVariable => Array<Hash{Symbol => ComplexType}>}
if_false = {}
if_false[pin] ||= []
if_false[pin] << { not_type: ComplexType::NIL }
process_facts(if_false, false_presences)
end
# @param bang_node [Parser::AST::Node]
# @return [Array(String, String), nil]
def parse_bang(bang_node)
parse_call(bang_node, :!)
end
# @param bang_node [Parser::AST::Node]
# @param true_presences [Array<Range>]
# @param false_presences [Array<Range>]
#
# @return [void]
def process_bang(bang_node, true_presences, false_presences)
# pry(main)> require 'parser/current'; Parser::CurrentRuby.parse("!2")
# => s(:send,
# s(:int, 2), :!)
# end
return unless bang_node.type == :send && bang_node.children[1] == :!
receiver = bang_node.children[0]
# swap the two presences
process_expression(receiver, false_presences, true_presences)
end
# @param var_node [Parser::AST::Node]
#
# @return [String, nil] Variable name referenced
def parse_variable(var_node)
return if var_node.children.length != 1
var_node.children[0]&.to_s
end
# @return [void]
# @param node [Parser::AST::Node]
# @param true_presences [Array<Range>]
# @param false_presences [Array<Range>]
def process_variable(node, true_presences, false_presences)
return unless [:lvar, :ivar, :cvar, :gvar].include?(node.type)
variable_name = parse_variable(node)
return if variable_name.nil?
# @sg-ignore Need to add nil check here
var_position = Range.from_node(node).start
pin = find_var(variable_name, var_position)
return unless pin
# @type Hash{Pin::LocalVariable => Array<Hash{Symbol => ComplexType}>}
if_true = {}
if_true[pin] ||= []
if_true[pin] << { not_type: ComplexType::NIL }
process_facts(if_true, true_presences)
# @type Hash{Pin::LocalVariable => Array<Hash{Symbol => ComplexType}>}
if_false = {}
if_false[pin] ||= []
if_false[pin] << { type: ComplexType.parse('nil, false') }
process_facts(if_false, false_presences)
end
# @param node [Parser::AST::Node]
#

@@ -245,3 +461,5 @@ # @return [String, nil]

return unless node&.type == :const
# @type [Parser::AST::Node, nil]
module_node = node.children[0]
# @type [Parser::AST::Node, nil]
class_node = node.children[1]

@@ -257,3 +475,4 @@

# @param clause_node [Parser::AST::Node]
# @param clause_node [Parser::AST::Node, nil]
# @sg-ignore need boolish support for ? methods
def always_breaks?(clause_node)

@@ -263,7 +482,16 @@ clause_node&.type == :break

# @param clause_node [Parser::AST::Node, nil]
def always_leaves_compound_statement?(clause_node)
# https://docs.ruby-lang.org/en/2.2.0/keywords_rdoc.html
# @sg-ignore Need to look at Tuple#include? handling
[:return, :raise, :next, :redo, :retry].include?(clause_node&.type)
end
attr_reader :locals
attr_reader :enclosing_breakable_pin
attr_reader :ivars
attr_reader :enclosing_breakable_pin, :enclosing_compound_statement_pin
end
end
end

@@ -38,5 +38,6 @@ # frozen_string_literal: true

# @param pins [Array<Pin::Base>]
# @param locals [Array<Pin::BaseVariable>]
# @return [Array(Array<Pin::Base>, Array<Pin::Base>)]
def self.process node, region = Region.new, pins = [], locals = []
# @param locals [Array<Pin::LocalVariable>]
# @param ivars [Array<Pin::InstanceVariable>]
# @return [Array(Array<Pin::Base>, Array<Pin::LocalVariable>, Array<Pin::InstanceVariable>)]
def self.process node, region = Region.new, pins = [], locals = [], ivars = []
if pins.empty?

@@ -49,7 +50,7 @@ pins.push Pin::Namespace.new(

end
return [pins, locals] unless Parser.is_ast_node?(node)
return [pins, locals, ivars] unless Parser.is_ast_node?(node)
node_processor_classes = @@processors[node.type] || [NodeProcessor::Base]
node_processor_classes.each do |klass|
processor = klass.new(node, region, pins, locals)
processor = klass.new(node, region, pins, locals, ivars)
process_next = processor.process

@@ -60,3 +61,3 @@

[pins, locals]
[pins, locals, ivars]
end

@@ -63,0 +64,0 @@ end

@@ -19,2 +19,5 @@ # frozen_string_literal: true

# @return [Array<Pin::InstanceVariable>]
attr_reader :ivars
# @param node [Parser::AST::Node]

@@ -24,3 +27,4 @@ # @param region [Region]

# @param locals [Array<Pin::LocalVariable>]
def initialize node, region, pins, locals
# @param ivars [Array<Pin::InstanceVariable>]
def initialize node, region, pins, locals, ivars
@node = node

@@ -30,2 +34,3 @@ @region = region

@locals = locals
@ivars = ivars
@processed_children = false

@@ -46,2 +51,24 @@ end

# @return [Solargraph::Location]
def location
get_node_location(node)
end
# @return [Solargraph::Position]
def position
Position.new(node.loc.line, node.loc.column)
end
# @sg-ignore downcast output of Enumerable#select
# @return [Solargraph::Pin::Breakable, nil]
def enclosing_breakable_pin
pins.select{|pin| pin.is_a?(Pin::Breakable) && pin.location&.range&.contain?(position)}.last
end
# @todo downcast output of Enumerable#select
# @return [Solargraph::Pin::CompoundStatement, nil]
def enclosing_compound_statement_pin
pins.select{|pin| pin.is_a?(Pin::CompoundStatement) && pin.location&.range&.contain?(position)}.last
end
# @param subregion [Region]

@@ -54,3 +81,3 @@ # @return [void]

next unless Parser.is_ast_node?(child)
NodeProcessor.process(child, subregion, pins, locals)
NodeProcessor.process(child, subregion, pins, locals, ivars)
end

@@ -76,2 +103,3 @@ end

pins.select do |pin|
# @sg-ignore Need to add nil check here
pin.is_a?(Pin::Closure) && pin.path && !pin.path.empty? && pin.location.range.contain?(position)

@@ -86,2 +114,3 @@ end.last

# @todo determine if this can return a Pin::Block
# @sg-ignore Need to add nil check here
pins.select { |pin| pin.is_a?(Pin::Closure) && pin.location.range.contain?(position) }.last

@@ -94,2 +123,3 @@ end

def closure_pin position
# @sg-ignore Need to add nil check here
pins.select { |pin| pin.is_a?(Pin::Closure) && pin.location.range.contain?(position) }.last

@@ -96,0 +126,0 @@ end

@@ -0,0 +0,0 @@ module Solargraph

@@ -11,5 +11,8 @@ # frozen_string_literal: true

# @param filename [String, nil]
# @param starting_line [Integer] must be provided so that we
# can find relevant local variables later even if this is just
# a subset of the file in question
# @return [Array(Parser::AST::Node, Hash{Integer => Solargraph::Parser::Snippet})]
def parse_with_comments code, filename = nil
node = parse(code, filename)
def parse_with_comments code, filename = nil, starting_line = 0
node = parse(code, filename, starting_line)
comments = CommentRipper.new(code, filename, 0).parse

@@ -21,6 +24,7 @@ [node, comments]

# @param filename [String, nil]
# @param line [Integer]
# @param starting_line [Integer]
# @sg-ignore need to understand that raise does not return
# @return [Parser::AST::Node]
def parse code, filename = nil, line = 0
buffer = ::Parser::Source::Buffer.new(filename, line)
def parse code, filename = nil, starting_line = 0
buffer = ::Parser::Source::Buffer.new(filename, starting_line)
buffer.source = code

@@ -35,3 +39,5 @@ parser.parse(buffer)

@parser ||= Prism::Translation::Parser.new(FlawedBuilder.new).tap do |parser|
# @sg-ignore Unresolved call to diagnostics on Prism::Translation::Parser
parser.diagnostics.all_errors_are_fatal = true
# @sg-ignore Unresolved call to diagnostics on Prism::Translation::Parser
parser.diagnostics.ignore_warnings = true

@@ -42,5 +48,8 @@ end

# @param source [Source]
# @return [Array(Array<Pin::Base>, Array<Pin::Base>)]
# @return [Array(Array<Pin::Base>, Array<Pin::LocalVariable>)]
def map source
NodeProcessor.process(source.node, Region.new(source: source))
# @sg-ignore Need to add nil check here
pins, locals, ivars = NodeProcessor.process(source.node, Region.new(source: source))
pins.concat(ivars)
[pins, locals]
end

@@ -57,2 +66,3 @@

# @return [Array(Integer, Integer), Array(nil, nil)]
# @sg-ignore Need to add nil check here
extract_offset = ->(code, offset) { reg.match(code, offset).offset(0) }

@@ -63,2 +73,3 @@ else

# @return [Array(Integer, Integer), Array(nil, nil)]
# @sg-ignore Need to add nil check here
extract_offset = ->(code, offset) { [soff = code.index(name, offset), soff + name.length] }

@@ -68,2 +79,3 @@ end

rng = Range.from_node(n)
# @sg-ignore Need to add nil check here
offset = Position.to_offset(source.code, rng.start)

@@ -109,3 +121,3 @@ soff, eoff = extract_offset[source.code, offset]

# @param node [Parser::AST::Node]
# @param node [Parser::AST::Node, nil]
# @return [String, nil]

@@ -121,3 +133,3 @@ def infer_literal_node_type node

# @param node [BasicObject]
# @param node [BasicObject, nil]
# @return [Boolean]

@@ -136,3 +148,3 @@ def is_ast_node? node

# @param node [Parser::AST::Node]
# @param node [Parser::AST::Node, nil]
# @return [Array<Range>]

@@ -142,10 +154,16 @@ def string_ranges node

result = []
# @sg-ignore Translate to something flow sensitive typing understands
result.push Range.from_node(node) if node.type == :str
# @sg-ignore Translate to something flow sensitive typing understands
node.children.each do |child|
result.concat string_ranges(child)
end
# @sg-ignore Translate to something flow sensitive typing understands
if node.type == :dstr && node.children.last.nil?
# @sg-ignore Translate to something flow sensitive typing understands
last = node.children[-2]
# @sg-ignore Need to add nil check here
unless last.nil?
rng = Range.from_node(last)
# @sg-ignore Need to add nil check here
pos = Position.new(rng.ending.line, rng.ending.column - 1)

@@ -152,0 +170,0 @@ result.push Range.new(pos, pos)

@@ -0,0 +0,0 @@ # frozen_string_literal: true

@@ -38,5 +38,8 @@ # frozen_string_literal: true

# @param code [String]
# @param filename [String]
# @param starting_line [Integer]
#
# @return [Source::Chain]
def load_string(code)
node = Parser.parse(code.sub(/\.$/, ''))
def load_string(code, filename, starting_line)
node = Parser.parse(code.sub(/\.$/, ''), filename, starting_line)
chain = NodeChainer.new(node).chain

@@ -65,2 +68,3 @@ chain.links.push(Chain::Link.new) if code.end_with?('.')

args = []
# @sg-ignore Need to add nil check here
n.children[2..-1].each do |c|

@@ -98,3 +102,3 @@ args.push NodeChainer.chain(c, @filename, n)

elsif [:ivar, :ivasgn].include?(n.type)
result.push Chain::InstanceVariable.new(n.children[0].to_s)
result.push Chain::InstanceVariable.new(n.children[0].to_s, n, Location.from_node(n))
elsif [:cvar, :cvasgn].include?(n.type)

@@ -105,4 +109,12 @@ result.push Chain::ClassVariable.new(n.children[0].to_s)

elsif n.type == :or_asgn
new_node = n.updated(n.children[0].type, n.children[0].children + [n.children[1]])
result.concat generate_links new_node
# @bar ||= 123 translates to:
#
# s(:or_asgn,
# s(:ivasgn, :@bar),
# s(:int, 123))
lhs_chain = NodeChainer.chain n.children[0] # s(:ivasgn, :@bar)
rhs_chain = NodeChainer.chain n.children[1] # s(:int, 123)
or_link = Chain::Or.new([lhs_chain, rhs_chain])
# this is just for a call chain, so we don't need to record the assignment
result.push(or_link)
elsif [:class, :module, :def, :defs].include?(n.type)

@@ -116,3 +128,13 @@ # @todo Undefined or what?

elsif n.type == :if
result.push Chain::If.new([NodeChainer.chain(n.children[1], @filename), NodeChainer.chain(n.children[2], @filename, n)])
then_clause = if n.children[1]
NodeChainer.chain(n.children[1], @filename, n)
else
Source::Chain.new([Source::Chain::Literal.new('nil', nil)], n)
end
else_clause = if n.children[2]
NodeChainer.chain(n.children[2], @filename, n)
else
Source::Chain.new([Source::Chain::Literal.new('nil', nil)], n)
end
result.push Chain::If.new([then_clause, else_clause])
elsif [:begin, :kwbegin].include?(n.type)

@@ -158,2 +180,3 @@ result.concat generate_links(n.children.last)

# @sg-ignore Need to add nil check here
NodeChainer.chain(@parent.children[2], @filename)

@@ -163,4 +186,6 @@ end

# @param node [Parser::AST::Node]
# @sg-ignore Need to add nil check here
# @return [Array<Source::Chain>]
def node_args node
# @sg-ignore Need to add nil check here
node.children[2..-1].map do |child|

@@ -167,0 +192,0 @@ NodeChainer.chain(child, @filename, node)

@@ -40,3 +40,3 @@ # frozen_string_literal: true

# @param node [Parser::AST::Node]
# @param node [Parser::AST::Node, nil]
# @return [String, nil]

@@ -109,10 +109,14 @@ def infer_literal_node_type node

# @param node [Parser::AST::Node]
# @param node [Parser::AST::Node, nil]
# @return [Hash{Symbol => Chain}]
def convert_hash node
return {} unless Parser.is_ast_node?(node)
# @sg-ignore Translate to something flow sensitive typing understands
return convert_hash(node.children[0]) if node.type == :kwsplat
# @sg-ignore Translate to something flow sensitive typing understands
return convert_hash(node.children[0]) if Parser.is_ast_node?(node.children[0]) && node.children[0].type == :kwsplat
# @sg-ignore Translate to something flow sensitive typing understands
return {} unless node.type == :hash
result = {}
# @sg-ignore Translate to something flow sensitive typing understands
node.children.each do |pair|

@@ -124,3 +128,2 @@ result[pair.children[0].children[0]] = Solargraph::Parser.chain(pair.children[1])

# @sg-ignore Wrong argument type for AST::Node.new: type expected AST::_ToSym, received :nil
NIL_NODE = ::Parser::AST::Node.new(:nil)

@@ -167,4 +170,6 @@

if Parser.is_ast_node?(node.children[0]) && node.children[0].children.length > 2
# @sg-ignore Need to add nil check here
node.children[0].children[2..-1].each { |child| result.concat call_nodes_from(child) }
end
# @sg-ignore Need to add nil check here
node.children[1..-1].each { |child| result.concat call_nodes_from(child) }

@@ -174,2 +179,3 @@ elsif node.type == :send

result.concat call_nodes_from(node.children.first)
# @sg-ignore Need to add nil check here
node.children[2..-1].each { |child| result.concat call_nodes_from(child) }

@@ -219,4 +225,6 @@ elsif [:super, :zsuper].include?(node.type)

tree = if source.synchronized?
# @sg-ignore Need to add nil check here
match = source.code[0..offset-1].match(/,\s*\z/)
if match
# @sg-ignore Need to add nil check here
source.tree_at(position.line, position.column - match[0].length)

@@ -234,3 +242,5 @@ else

args = node.children[2..-1]
# @sg-ignore Need to add nil check here
if !args.empty?
# @sg-ignore Need to add nil check here
return node if prev && args.include?(prev)

@@ -313,3 +323,2 @@ else

CONDITIONAL_ALL_BUT_FIRST = [:if, :unless]
CONDITIONAL_ALL = [:or]
ONLY_ONE_CHILD = [:return]

@@ -345,3 +354,3 @@ FIRST_TWO_CHILDREN = [:rescue]

#
# @param node [Parser::AST::Node] Statement which is in
# @param node [AST::Node] Statement which is in
# value position for a method body

@@ -360,6 +369,5 @@ # @param include_explicit_returns [Boolean] If true,

elsif CONDITIONAL_ALL_BUT_FIRST.include?(node.type)
# @sg-ignore Need to add nil check here
result.concat reduce_to_value_nodes(node.children[1..-1])
# result.push NIL_NODE unless node.children[2]
elsif CONDITIONAL_ALL.include?(node.type)
result.concat reduce_to_value_nodes(node.children)
elsif ONLY_ONE_CHILD.include?(node.type)

@@ -377,2 +385,3 @@ result.concat reduce_to_value_nodes([node.children[0]])

elsif CASE_STATEMENT.include?(node.type)
# @sg-ignore Need to add nil check here
node.children[1..-1].each do |cc|

@@ -474,13 +483,24 @@ if cc.nil?

result.push nil
# @sg-ignore flow sensitive typing needs to narrow down type with an if is_a? check
elsif COMPOUND_STATEMENTS.include?(node.type)
result.concat from_value_position_compound_statement(node)
# @sg-ignore flow sensitive typing needs to narrow down type with an if is_a? check
elsif CONDITIONAL_ALL_BUT_FIRST.include?(node.type)
# @sg-ignore flow sensitive typing needs to narrow down type with an if is_a? check
result.concat reduce_to_value_nodes(node.children[1..-1])
# @sg-ignore flow sensitive typing needs to narrow down type with an if is_a? check
elsif node.type == :return
# @sg-ignore flow sensitive typing needs to narrow down type with an if is_a? check
result.concat reduce_to_value_nodes([node.children[0]])
# @sg-ignore flow sensitive typing needs to narrow down type with an if is_a? check
elsif node.type == :or
# @sg-ignore flow sensitive typing needs to narrow down type with an if is_a? check
result.concat reduce_to_value_nodes(node.children)
# @sg-ignore flow sensitive typing needs to narrow down type with an if is_a? check
elsif node.type == :block
# @sg-ignore flow sensitive typing needs to narrow down type with an if is_a? check
result.concat explicit_return_values_from_compound_statement(node.children[2])
# @sg-ignore flow sensitive typing needs to narrow down type with an if is_a? check
elsif node.type == :resbody
# @sg-ignore flow sensitive typing needs to narrow down type with an if is_a? check
result.concat reduce_to_value_nodes([node.children[2]])

@@ -487,0 +507,0 @@ else

@@ -30,4 +30,6 @@ # frozen_string_literal: true

autoload :UntilNode, 'solargraph/parser/parser_gem/node_processors/until_node'
autoload :WhenNode, 'solargraph/parser/parser_gem/node_processors/when_node'
autoload :WhileNode, 'solargraph/parser/parser_gem/node_processors/while_node'
autoload :AndNode, 'solargraph/parser/parser_gem/node_processors/and_node'
autoload :OrNode, 'solargraph/parser/parser_gem/node_processors/or_node'
end

@@ -67,6 +69,8 @@ end

register :until, ParserGem::NodeProcessors::UntilNode
register :when, ParserGem::NodeProcessors::WhenNode
register :while, ParserGem::NodeProcessors::WhileNode
register :and, ParserGem::NodeProcessors::AndNode
register :or, ParserGem::NodeProcessors::OrNode
end
end
end

@@ -13,6 +13,6 @@ # frozen_string_literal: true

position = get_node_start_position(node)
# @sg-ignore https://github.com/castwide/solargraph/pull/1114
enclosing_breakable_pin = pins.select{|pin| pin.is_a?(Pin::Breakable) && pin.location.range.contain?(position)}.last
FlowSensitiveTyping.new(locals, enclosing_breakable_pin).process_and(node)
FlowSensitiveTyping.new(locals,
ivars,
enclosing_breakable_pin,
enclosing_compound_statement_pin).process_and(node)
end

@@ -19,0 +19,0 @@ end

@@ -23,2 +23,3 @@ # frozen_string_literal: true

asgn_code: u.children[1] ? region.code_for(u.children[1]) : nil,
# @sg-ignore Need to add nil check here
presence: callable.location.range,

@@ -44,2 +45,3 @@ decl: get_decl(u),

closure: callable,
# @sg-ignore Need to add nil check here
presence: region.closure.location.range,

@@ -46,0 +48,0 @@ decl: get_decl(node),

@@ -9,2 +9,11 @@ # frozen_string_literal: true

def process
# We intentionally don't create a CompoundStatement pin
# here, as this is not necessarily a control flow block -
# e.g., a begin...end without rescue or ensure should be
# treated by flow sensitive typing as if the begin and end
# didn't exist at all. As such, we create the
# CompoundStatement pins around the things which actually
# result in control flow changes - like
# if/while/rescue/etc
process_children

@@ -11,0 +20,0 @@ end

@@ -12,19 +12,18 @@ # frozen_string_literal: true

location = get_node_location(node)
parent = if other_class_eval?
Solargraph::Pin::Namespace.new(
location: location,
type: :class,
name: unpack_name(node.children[0].children[0]),
source: :parser,
)
else
region.closure
scope = region.scope || region.closure.context.scope
if other_class_eval?
clazz_name = unpack_name(node.children[0].children[0])
# instance variables should come from the Class<T> type
# - i.e., treated as class instance variables
context = ComplexType.try_parse("Class<#{clazz_name}>")
scope = :class
end
block_pin = Solargraph::Pin::Block.new(
location: location,
closure: parent,
closure: region.closure,
node: node,
context: context,
receiver: node.children[0],
comments: comments_for(node),
scope: region.scope || region.closure.context.scope,
scope: scope,
source: :parser

@@ -41,2 +40,3 @@ )

node.children[0].children[1] == :class_eval &&
# @sg-ignore Need to add nil check here
[:cbase, :const].include?(node.children[0].children[0]&.type)

@@ -43,0 +43,0 @@ end

@@ -11,2 +11,6 @@ # frozen_string_literal: true

scope = region.scope || (region.closure.is_a?(Pin::Singleton) ? :class : :instance)
# specify context explicitly instead of relying on
# closure, as they may differ (e.g., defs inside
# class_eval)
method_context = scope == :instance ? region.closure.binder.namespace_type : region.closure.binder
methpin = Solargraph::Pin::Method.new(

@@ -16,2 +20,3 @@ location: get_node_location(node),

name: name,
context: method_context,
comments: comments_for(node),

@@ -28,2 +33,3 @@ scope: scope,

name: methpin.name,
context: method_context,
comments: methpin.comments,

@@ -40,2 +46,3 @@ scope: :class,

name: methpin.name,
context: method_context,
comments: methpin.comments,

@@ -42,0 +49,0 @@ scope: :instance,

@@ -0,0 +0,0 @@ # frozen_string_literal: true

@@ -11,9 +11,39 @@ # frozen_string_literal: true

def process
process_children
FlowSensitiveTyping.new(locals,
ivars,
enclosing_breakable_pin,
enclosing_compound_statement_pin).process_if(node)
condition_node = node.children[0]
if condition_node
pins.push Solargraph::Pin::CompoundStatement.new(
location: get_node_location(condition_node),
closure: region.closure,
node: condition_node,
source: :parser,
)
NodeProcessor.process(condition_node, region, pins, locals, ivars)
end
then_node = node.children[1]
if then_node
pins.push Solargraph::Pin::CompoundStatement.new(
location: get_node_location(then_node),
closure: region.closure,
node: then_node,
source: :parser,
)
NodeProcessor.process(then_node, region, pins, locals, ivars)
end
position = get_node_start_position(node)
# @sg-ignore
# @type [Solargraph::Pin::Breakable, nil]
enclosing_breakable_pin = pins.select{|pin| pin.is_a?(Pin::Breakable) && pin.location.range.contain?(position)}.last
FlowSensitiveTyping.new(locals, enclosing_breakable_pin).process_if(node)
else_node = node.children[2]
if else_node
pins.push Solargraph::Pin::CompoundStatement.new(
location: get_node_location(else_node),
closure: region.closure,
node: else_node,
source: :parser,
)
NodeProcessor.process(else_node, region, pins, locals, ivars)
end
true
end

@@ -20,0 +50,0 @@ end

@@ -12,3 +12,3 @@ # frozen_string_literal: true

loc = get_node_location(node)
pins.push Solargraph::Pin::InstanceVariable.new(
ivars.push Solargraph::Pin::InstanceVariable.new(
location: loc,

@@ -23,5 +23,6 @@ closure: region.closure,

here = get_node_start_position(node)
# @type [Pin::Closure, nil]
named_path = named_path_pin(here)
if named_path.is_a?(Pin::Method)
pins.push Solargraph::Pin::InstanceVariable.new(
ivars.push Solargraph::Pin::InstanceVariable.new(
location: loc,

@@ -28,0 +29,0 @@ closure: Pin::Namespace.new(type: :module, closure: region.closure.closure, name: region.closure.name),

@@ -12,2 +12,3 @@ # frozen_string_literal: true

here = get_node_start_position(node)
# @sg-ignore Need to add nil check here
presence = Range.new(here, region.closure.location.range.ending)

@@ -14,0 +15,0 @@ loc = get_node_location(node)

@@ -40,4 +40,6 @@ # frozen_string_literal: true

locals.find { |l| l.location == location }
elsif lhs.type == :ivasgn
# ivasgn is an instance variable assignment
ivars.find { |iv| iv.location == location }
else
# e.g., ivasgn is an instance variable, etc
pins.find { |iv| iv.location == location && iv.is_a?(Pin::BaseVariable) }

@@ -44,0 +46,0 @@ end

@@ -60,3 +60,3 @@ # frozen_string_literal: true

node.updated(:send, [call, operator, argument])])
NodeProcessor.process(new_send, region, pins, locals)
NodeProcessor.process(new_send, region, pins, locals, ivars)
end

@@ -93,3 +93,3 @@

new_asgn = node.updated(asgn.type, [variable_name, send_node])
NodeProcessor.process(new_asgn, region, pins, locals)
NodeProcessor.process(new_asgn, region, pins, locals, ivars)
end

@@ -96,0 +96,0 @@ end

@@ -11,3 +11,3 @@ # frozen_string_literal: true

new_node = node.updated(node.children[0].type, node.children[0].children + [node.children[1]])
NodeProcessor.process(new_node, region, pins, locals)
NodeProcessor.process(new_node, region, pins, locals, ivars)
end

@@ -14,0 +14,0 @@ end

@@ -14,2 +14,3 @@ # frozen_string_literal: true

here = get_node_start_position(node.children[1])
# @sg-ignore Need to add nil check here
presence = Range.new(here, region.closure.location.range.ending)

@@ -33,3 +34,3 @@ loc = get_node_location(node.children[1])

end
NodeProcessor.process(node.children[2], region, pins, locals)
NodeProcessor.process(node.children[2], region, pins, locals, ivars)
end

@@ -36,0 +37,0 @@ end

@@ -8,2 +8,3 @@ # frozen_string_literal: true

class SclassNode < Parser::NodeProcessor::Base
# @sg-ignore @override is adding, not overriding
def process

@@ -10,0 +11,0 @@ sclass = node.children[0]

@@ -10,2 +10,3 @@ # frozen_string_literal: true

# @sg-ignore @override is adding, not overriding
def process

@@ -57,2 +58,3 @@ # @sg-ignore Variable type could not be inferred for method_name

if (node.children.length > 2)
# @sg-ignore Need to add nil check here
node.children[2..-1].each do |child|

@@ -87,2 +89,3 @@ # @sg-ignore Variable type could not be inferred for method_name

def process_attribute
# @sg-ignore Need to add nil check here
node.children[2..-1].each do |a|

@@ -128,2 +131,3 @@ loc = get_node_location(node)

cp = region.closure
# @sg-ignore Need to add nil check here
node.children[2..-1].each do |i|

@@ -145,2 +149,3 @@ type = region.scope == :class ? Pin::Reference::Extend : Pin::Reference::Include

cp = region.closure
# @sg-ignore Need to add nil check here
node.children[2..-1].each do |i|

@@ -159,2 +164,3 @@ pins.push Pin::Reference::Prepend.new(

def process_extend
# @sg-ignore Need to add nil check here
node.children[2..-1].each do |i|

@@ -202,2 +208,3 @@ loc = get_node_location(node)

elsif node.children[2].type == :sym || node.children[2].type == :str
# @sg-ignore Need to add nil check here
node.children[2..-1].each do |x|

@@ -231,5 +238,5 @@ cn = x.children[0].to_s

pins.push mm, cm
pins.select{|pin| pin.is_a?(Pin::InstanceVariable) && pin.closure.path == ref.path}.each do |ivar|
pins.delete ivar
pins.push Solargraph::Pin::InstanceVariable.new(
ivars.select{|pin| pin.is_a?(Pin::InstanceVariable) && pin.closure.path == ref.path}.each do |ivar|
ivars.delete ivar
ivars.push Solargraph::Pin::InstanceVariable.new(
location: ivar.location,

@@ -239,7 +246,6 @@ closure: cm,

comments: ivar.comments,
# @sg-ignore https://github.com/castwide/solargraph/pull/1114
assignment: ivar.assignment,
source: :parser
)
pins.push Solargraph::Pin::InstanceVariable.new(
ivars.push Solargraph::Pin::InstanceVariable.new(
location: ivar.location,

@@ -249,3 +255,2 @@ closure: mm,

comments: ivar.comments,
# @sg-ignore https://github.com/castwide/solargraph/pull/1114
assignment: ivar.assignment,

@@ -258,3 +263,3 @@ source: :parser

elsif node.children[2].type == :def
NodeProcessor.process node.children[2], region.update(visibility: :module_function), pins, locals
NodeProcessor.process node.children[2], region.update(visibility: :module_function), pins, locals, ivars
end

@@ -261,0 +266,0 @@ end

@@ -0,0 +0,0 @@ # frozen_string_literal: true

@@ -11,3 +11,7 @@ # frozen_string_literal: true

def process
location = get_node_location(node)
FlowSensitiveTyping.new(locals,
ivars,
enclosing_breakable_pin,
enclosing_compound_statement_pin).process_while(node)
# Note - this should not be considered a block, as the

@@ -14,0 +18,0 @@ # while statement doesn't create a closure - e.g.,

@@ -25,3 +25,2 @@ # frozen_string_literal: true

# @param source [Source]
# @param namespace [String]
# @param closure [Pin::Closure, nil]

@@ -34,3 +33,2 @@ # @param scope [Symbol, nil]

@source = source
# @closure = closure
@closure = closure || Pin::Namespace.new(name: '', location: source.location, source: :parser)

@@ -42,3 +40,3 @@ @scope = scope

# @return [String]
# @return [String, nil]
def filename

@@ -48,2 +46,10 @@ source.filename

# @return [Pin::Namespace, nil]
def namespace_pin
ns = closure
# @sg-ignore flow sensitive typing needs to handle while
ns = ns.closure while ns && !ns.is_a?(Pin::Namespace)
ns
end
# Generate a new Region with the provided attribute changes.

@@ -50,0 +56,0 @@ #

module Solargraph
module Parser
class Snippet
# @return [Range]
# @return [Solargraph::Range]
attr_reader :range

@@ -6,0 +6,0 @@ # @return [String]

@@ -1,10 +0,432 @@

require 'yard-activesupport-concern'
require 'fileutils'
require 'rbs'
require 'rubygems'
module Solargraph
module PinCache
class PinCache
include Logging
attr_reader :directory, :rbs_collection_path, :rbs_collection_config_path, :yard_plugins
# @param rbs_collection_path [String, nil]
# @param rbs_collection_config_path [String, nil]
# @param directory [String, nil]
# @param yard_plugins [Array<String>]
def initialize rbs_collection_path:, rbs_collection_config_path:,
directory:,
yard_plugins:
@rbs_collection_path = rbs_collection_path
@rbs_collection_config_path = rbs_collection_config_path
@directory = directory
@yard_plugins = yard_plugins
end
# @param gemspec [Gem::Specification, Bundler::LazySpecification]
def cached? gemspec
rbs_version_cache_key = lookup_rbs_version_cache_key(gemspec)
combined_gem?(gemspec, rbs_version_cache_key)
end
# @param gemspec [Gem::Specification]
# @param rebuild [Boolean] whether to rebuild the cache regardless of whether it already exists
# @param out [StringIO, IO, nil] output stream for logging
# @return [void]
def cache_gem gemspec:, rebuild: false, out: nil
rbs_version_cache_key = lookup_rbs_version_cache_key(gemspec)
build_yard, build_rbs_collection, build_combined =
calculate_build_needs(gemspec,
rebuild: rebuild,
rbs_version_cache_key: rbs_version_cache_key)
return unless build_yard || build_rbs_collection || build_combined
build_combine_and_cache(gemspec,
rbs_version_cache_key,
build_yard: build_yard,
build_rbs_collection: build_rbs_collection,
build_combined: build_combined,
out: out)
end
# @param gemspec [Gem::Specification, Bundler::LazySpecification]
# @param rbs_version_cache_key [String, nil]
def suppress_yard_cache? gemspec, rbs_version_cache_key
if gemspec.name == 'parser' && rbs_version_cache_key != RbsMap::CACHE_KEY_UNRESOLVED
# parser takes forever to build YARD pins, but has excellent RBS collection pins
return true
end
false
end
# @param out [StringIO, IO, nil] output stream for logging
# @param rebuild [Boolean] build pins regardless of whether we
# have cached them already
#
# @return [void]
def cache_all_stdlibs rebuild: false, out: $stderr
possible_stdlibs.each do |stdlib|
RbsMap::StdlibMap.new(stdlib, rebuild: rebuild, out: out)
end
end
# @param path [String] require path that might be in the RBS stdlib collection
# @return [void]
def cache_stdlib_rbs_map path
# these are held in memory in RbsMap::StdlibMap
map = RbsMap::StdlibMap.load(path)
if map.resolved?
logger.debug { "Loading stdlib pins for #{path}" }
pins = map.pins
logger.debug { "Loaded #{pins.length} stdlib pins for #{path}" }
pins
else
# @todo Temporarily ignoring unresolved `require 'set'`
logger.debug { "Require path #{path} could not be resolved in RBS" } unless path == 'set'
nil
end
end
# @param gemspec [Gem::Specification, Bundler::LazySpecification]
#
# @return [String]
def lookup_rbs_version_cache_key gemspec
rbs_map = RbsMap.from_gemspec(gemspec, rbs_collection_path, rbs_collection_config_path)
rbs_map.cache_key
end
# @param gemspec [Gem::Specification, Bundler::LazySpecification]
# @param rbs_version_cache_key [String, nil]
# @param yard_pins [Array<Pin::Base>]
# @param rbs_collection_pins [Array<Pin::Base>]
# @return [void]
def cache_combined_pins gemspec, rbs_version_cache_key, yard_pins, rbs_collection_pins
combined_pins = GemPins.combine(yard_pins, rbs_collection_pins)
serialize_combined_gem(gemspec, rbs_version_cache_key, combined_pins)
end
# @param gemspec [Gem::Specification, Bundler::LazySpecification]
# @return [Array<Pin::Base>, nil]
def deserialize_combined_pin_cache gemspec
rbs_version_cache_key = lookup_rbs_version_cache_key(gemspec)
load_combined_gem(gemspec, rbs_version_cache_key)
end
# @param gemspec [Gem::Specification, Bundler::LazySpecification]
# @param out [StringIO, IO, nil]
# @return [void]
def uncache_gem gemspec, out: nil
PinCache.uncache(yardoc_path(gemspec), out: out)
PinCache.uncache(yard_gem_path(gemspec), out: out)
uncache_by_prefix(rbs_collection_pins_path_prefix(gemspec), out: out)
uncache_by_prefix(combined_path_prefix(gemspec), out: out)
rbs_version_cache_key = lookup_rbs_version_cache_key(gemspec)
combined_pins_in_memory.delete([gemspec.name, gemspec.version, rbs_version_cache_key])
end
# @param gemspec [Gem::Specification, Bundler::LazySpecification]
def yardoc_processing? gemspec
Yardoc.processing?(yardoc_path(gemspec))
end
# @return [Array<String>] a list of possible standard library names
def possible_stdlibs
# all dirs and .rb files in Gem::RUBYGEMS_DIR
Dir.glob(File.join(Gem::RUBYGEMS_DIR, '*')).map do |file_or_dir|
basename = File.basename(file_or_dir)
# remove .rb
# @sg-ignore flow sensitive typing should be able to handle redefinition
basename = basename[0..-4] if basename.end_with?('.rb')
basename
end.sort.uniq
rescue StandardError => e
logger.info { "Failed to get possible stdlibs: #{e.message}" }
# @sg-ignore Need to add nil check here
logger.debug { e.backtrace.join("\n") }
[]
end
private
# @param gemspec [Gem::Specification, Bundler::LazySpecification]
# @param rebuild [Boolean] whether to rebuild the cache regardless of whether it already exists
# @param rbs_version_cache_key [String, nil] the cache key for the gem in the RBS collection
#
# @return [Array(Boolean, Boolean, Boolean)] whether to build YARD
# pins, RBS collection pins, and combined pins
def calculate_build_needs gemspec, rebuild:, rbs_version_cache_key:
if rebuild
build_yard = true
build_rbs_collection = true
build_combined = true
else
build_yard = !yard_gem?(gemspec)
build_rbs_collection = !rbs_collection_pins?(gemspec, rbs_version_cache_key)
# @sg-ignore Need to add nil check here
build_combined = !combined_gem?(gemspec, rbs_version_cache_key) || build_yard || build_rbs_collection
end
build_yard = false if suppress_yard_cache?(gemspec, rbs_version_cache_key)
[build_yard, build_rbs_collection, build_combined]
end
# @param gemspec [Gem::Specification]
# @param rbs_version_cache_key [String, nil]
# @param build_yard [Boolean]
# @param build_rbs_collection [Boolean]
# @param build_combined [Boolean]
# @param out [StringIO, IO, nil]
#
# @return [void]
def build_combine_and_cache gemspec,
rbs_version_cache_key,
build_yard:,
build_rbs_collection:,
build_combined:,
out:
log_cache_info(gemspec, rbs_version_cache_key,
build_yard: build_yard,
build_rbs_collection: build_rbs_collection,
build_combined: build_combined,
out: out)
cache_yard_pins(gemspec, out) if build_yard
# this can be nil even if we aren't told to build it - see suppress_yard_cache?
yard_pins = deserialize_yard_pin_cache(gemspec) || []
cache_rbs_collection_pins(gemspec, out) if build_rbs_collection
rbs_collection_pins = deserialize_rbs_collection_cache(gemspec, rbs_version_cache_key) || []
cache_combined_pins(gemspec, rbs_version_cache_key, yard_pins, rbs_collection_pins) if build_combined
end
# @param gemspec [Gem::Specification, Bundler::LazySpecification]
# @param rbs_version_cache_key [String, nil]
# @param build_yard [Boolean]
# @param build_rbs_collection [Boolean]
# @param build_combined [Boolean]
# @param out [StringIO, IO, nil]
#
# @return [void]
def log_cache_info gemspec,
rbs_version_cache_key,
build_yard:,
build_rbs_collection:,
build_combined:,
out:
type = []
type << 'YARD' if build_yard
rbs_source_desc = RbsMap.rbs_source_desc(rbs_version_cache_key)
type << rbs_source_desc if build_rbs_collection && !rbs_source_desc.nil?
# we'll build it anyway, but it won't take long to build with
# only a single source
# 'combining' is awkward terminology in this case
just_yard = build_yard && rbs_source_desc.nil?
type << 'combined' if build_combined && !just_yard
out&.puts("Caching #{type.join(' and ')} pins for gem #{gemspec.name}:#{gemspec.version}")
end
# @param gemspec [Gem::Specification]
# @param out [StringIO, IO, nil]
# @return [Array<Pin::Base>]
def cache_yard_pins gemspec, out
gem_yardoc_path = yardoc_path(gemspec)
Yardoc.build_docs(gem_yardoc_path, yard_plugins, gemspec) unless Yardoc.docs_built?(gem_yardoc_path)
pins = Yardoc.build_pins(gem_yardoc_path, gemspec, out: out)
serialize_yard_gem(gemspec, pins)
logger.info { "Cached #{pins.length} YARD pins for gem #{gemspec.name}:#{gemspec.version}" } unless pins.empty?
pins
end
# @return [Hash{Array(String, String, String) => Array<Pin::Base>}]
def combined_pins_in_memory
PinCache.all_combined_pins_in_memory[yard_plugins] ||= {}
end
# @param gemspec [Gem::Specification, Bundler::LazySpecification]
# @param _out [StringIO, IO, nil]
# @return [Array<Pin::Base>]
def cache_rbs_collection_pins gemspec, _out
rbs_map = RbsMap.from_gemspec(gemspec, rbs_collection_path, rbs_collection_config_path)
pins = rbs_map.pins
rbs_version_cache_key = rbs_map.cache_key
# cache pins even if result is zero, so we don't retry building pins
pins ||= []
serialize_rbs_collection_pins(gemspec, rbs_version_cache_key, pins)
logger.info do
unless pins.empty?
"Cached #{pins.length} RBS collection pins for gem #{gemspec.name} #{gemspec.version} with " \
"cache_key #{rbs_version_cache_key.inspect}"
end
end
pins
end
# @param gemspec [Gem::Specification, Bundler::LazySpecification]
# @return [Array<Pin::Base>, nil]
def deserialize_yard_pin_cache gemspec
cached = load_yard_gem(gemspec)
if cached
cached
else
logger.debug "No YARD pin cache for #{gemspec.name}:#{gemspec.version}"
nil
end
end
# @param gemspec [Gem::Specification, Bundler::LazySpecification]
# @param rbs_version_cache_key [String, nil]
# @return [Array<Pin::Base>, nil]
def deserialize_rbs_collection_cache gemspec, rbs_version_cache_key
cached = load_rbs_collection_pins(gemspec, rbs_version_cache_key)
Solargraph.assert_or_log(:pin_cache_rbs_collection, 'Asked for non-existent rbs collection') if cached.nil?
logger.info do
"Loaded #{cached&.length} pins from RBS collection cache for #{gemspec.name}:#{gemspec.version}"
end
cached
end
# @return [Array<String>]
def yard_path_components
["yard-#{YARD::VERSION}",
yard_plugins.sort.uniq.join('-')]
end
# @param gemspec [Gem::Specification, Bundler::LazySpecification]
# @return [String]
def yardoc_path gemspec
File.join(PinCache.base_dir,
*yard_path_components,
"#{gemspec.name}-#{gemspec.version}.yardoc")
end
# @param gemspec [Gem::Specification, Bundler::LazySpecification]
# @return [String]
def yard_gem_path gemspec
File.join(PinCache.work_dir, *yard_path_components, "#{gemspec.name}-#{gemspec.version}.ser")
end
# @param gemspec [Gem::Specification, Bundler::LazySpecification]
# @return [Array<Pin::Base>, nil]
def load_yard_gem gemspec
PinCache.load(yard_gem_path(gemspec))
end
# @param gemspec [Gem::Specification, Bundler::LazySpecification]
# @param pins [Array<Pin::Base>]
# @return [void]
def serialize_yard_gem gemspec, pins
PinCache.save(yard_gem_path(gemspec), pins)
end
# @param gemspec [Gem::Specification, Bundler::LazySpecification]
# @return [Boolean]
def yard_gem? gemspec
exist?(yard_gem_path(gemspec))
end
# @param gemspec [Gem::Specification, Bundler::LazySpecification]
# @param hash [String, nil]
# @return [String]
def rbs_collection_pins_path gemspec, hash
rbs_collection_pins_path_prefix(gemspec) + "#{hash || 0}.ser"
end
# @param gemspec [Gem::Specification, Bundler::LazySpecification]
# @return [String]
def rbs_collection_pins_path_prefix gemspec
File.join(PinCache.work_dir, 'rbs', "#{gemspec.name}-#{gemspec.version}-")
end
# @param gemspec [Gem::Specification, Bundler::LazySpecification]
# @param hash [String, nil]
#
# @return [Array<Pin::Base>, nil]
def load_rbs_collection_pins gemspec, hash
PinCache.load(rbs_collection_pins_path(gemspec, hash))
end
# @param gemspec [Gem::Specification, Bundler::LazySpecification]
# @param hash [String, nil]
# @param pins [Array<Pin::Base>]
# @return [void]
def serialize_rbs_collection_pins gemspec, hash, pins
PinCache.save(rbs_collection_pins_path(gemspec, hash), pins)
end
# @param gemspec [Gem::Specification, Bundler::LazySpecification]
# @param hash [String, nil]
# @return [String]
def combined_path gemspec, hash
File.join(combined_path_prefix(gemspec) + "-#{hash || 0}.ser")
end
# @param gemspec [Gem::Specification, Bundler::LazySpecification]
# @return [String]
def combined_path_prefix gemspec
File.join(PinCache.work_dir, 'combined', yard_plugins.sort.join('-'), "#{gemspec.name}-#{gemspec.version}")
end
# @param gemspec [Gem::Specification, Bundler::LazySpecification]
# @param hash [String, nil]
# @param pins [Array<Pin::Base>]
# @return [void]
def serialize_combined_gem gemspec, hash, pins
PinCache.save(combined_path(gemspec, hash), pins)
end
# @param gemspec [Gem::Specification, Bundler::LazySpecification]
# @param hash [String]
def combined_gem? gemspec, hash
exist?(combined_path(gemspec, hash))
end
# @param gemspec [Gem::Specification, Bundler::LazySpecification]
# @param hash [String, nil]
# @return [Array<Pin::Base>, nil]
def load_combined_gem gemspec, hash
cached = combined_pins_in_memory[[gemspec.name, gemspec.version, hash]]
return cached if cached
loaded = PinCache.load(combined_path(gemspec, hash))
combined_pins_in_memory[[gemspec.name, gemspec.version, hash]] = loaded if loaded
loaded
end
# @param gemspec [Gem::Specification, Bundler::LazySpecification]
# @param hash [String, nil]
def rbs_collection_pins? gemspec, hash
exist?(rbs_collection_pins_path(gemspec, hash))
end
include Logging
# @param path [String]
def exist? *path
File.file? File.join(*path)
end
# @return [void]
# @param path_segments [Array<String>]
def uncache_by_prefix *path_segments, out: nil
path = File.join(*path_segments)
glob = "#{path}*"
out&.puts "Clearing pin cache in #{glob}"
Dir.glob(glob).each do |file|
next unless File.file?(file)
FileUtils.rm_rf file, secure: true
out&.puts "Clearing pin cache in #{file}"
end
end
class << self
include Logging
# @return [Hash{Array<String> => Hash{Array(String, String) =>
# Array<Pin::Base>}}] yard plugins, then gemspec name and
# version
def all_combined_pins_in_memory
@all_combined_pins_in_memory ||= {}
end
# The base directory where cached YARD documentation and serialized pins are serialized

@@ -21,2 +443,43 @@ #

# @param path_segments [Array<String>]
# @param out [IO, nil]
# @return [void]
def uncache *path_segments, out: nil
path = File.join(*path_segments)
if File.exist?(path)
FileUtils.rm_rf path, secure: true
out.puts "Clearing pin cache in #{path}" unless out.nil?
else
out&.puts "Pin cache file #{path} does not exist"
end
end
# @return [void]
# @param out [IO, nil]
# @param path_segments [Array<String>]
def uncache_by_prefix *path_segments, out: nil
path = File.join(*path_segments)
glob = "#{path}*"
out.puts "Clearing pin cache in #{glob}" unless out.nil?
Dir.glob(glob).each do |file|
next unless File.file?(file)
FileUtils.rm_rf file, secure: true
out.puts "Clearing pin cache in #{file}" unless out.nil?
end
end
# @param out [StringIO, IO, nil]
# @return [void]
def uncache_core out: nil
uncache(core_path, out: out)
# ApiMap keep this in memory
ApiMap.reset_core(out: out)
end
# @param out [StringIO, IO, nil]
# @return [void]
def uncache_stdlib out: nil
uncache(stdlib_path, out: out)
end
# The working directory for the current Ruby, RBS, and Solargraph versions.

@@ -31,12 +494,3 @@ #

# @param gemspec [Gem::Specification]
# @return [String]
def yardoc_path gemspec
File.join(base_dir,
"yard-#{YARD::VERSION}",
"yard-activesupport-concern-#{YARD::ActiveSupport::Concern::VERSION}",
"#{gemspec.name}-#{gemspec.version}.yardoc")
end
# @return [String]
def stdlib_path

@@ -170,22 +624,2 @@ File.join(work_dir, 'stdlib')

# @return [void]
def uncache_core
uncache(core_path)
end
# @return [void]
def uncache_stdlib
uncache(stdlib_path)
end
# @param gemspec [Gem::Specification]
# @param out [IO, nil]
# @return [void]
def uncache_gem(gemspec, out: nil)
uncache(yardoc_path(gemspec), out: out)
uncache_by_prefix(rbs_collection_path_prefix(gemspec), out: out)
uncache(yard_gem_path(gemspec), out: out)
uncache_by_prefix(combined_path_prefix(gemspec), out: out)
end
# @return [void]
def clear

@@ -195,5 +629,4 @@ FileUtils.rm_rf base_dir, secure: true

private
# @param file [String]
# @sg-ignore Marshal.load evaluates to boolean here which is wrong
# @return [Array<Solargraph::Pin::Base>, nil]

@@ -209,7 +642,2 @@ def load file

# @param path [String]
def exist? *path
File.file? File.join(*path)
end
# @param file [String]

@@ -226,26 +654,18 @@ # @param pins [Array<Pin::Base>]

# @param path_segments [Array<String>]
# @return [void]
def uncache *path_segments, out: nil
path = File.join(*path_segments)
if File.exist?(path)
FileUtils.rm_rf path, secure: true
out.puts "Clearing pin cache in #{path}" unless out.nil?
end
def core?
File.file?(core_path)
end
# @return [void]
# @param path_segments [Array<String>]
def uncache_by_prefix *path_segments, out: nil
path = File.join(*path_segments)
glob = "#{path}*"
out.puts "Clearing pin cache in #{glob}" unless out.nil?
Dir.glob(glob).each do |file|
next unless File.file?(file)
FileUtils.rm_rf file, secure: true
out.puts "Clearing pin cache in #{file}" unless out.nil?
end
# @param out [StringIO, IO, nil]
# @return [Array<Pin::Base>]
def cache_core out: $stderr
RbsMap::CoreMap.new.cache_core(out: out)
end
# @param path [String]
def exist? *path
File.file? File.join(*path)
end
end
end
end

@@ -41,2 +41,4 @@ # frozen_string_literal: true

autoload :Callable, 'solargraph/pin/callable'
autoload :CompoundStatement,
'solargraph/pin/compound_statement'

@@ -43,0 +45,0 @@ ROOT_PIN = Pin::Namespace.new(type: :class, name: '', closure: nil, source: :pin_rb)

@@ -9,23 +9,94 @@ # frozen_string_literal: true

# @return [Parser::AST::Node, nil]
attr_reader :assignment
# @return [Array<Parser::AST::Node>]
attr_reader :assignments
attr_accessor :mass_assignment
# @return [Range, nil]
attr_reader :presence
# @param return_type [ComplexType, nil]
# @param assignment [Parser::AST::Node, nil] First assignment
# that was made to this variable
# @param assignments [Array<Parser::AST::Node>] Possible
# assignments that may have been made to this variable
# @param mass_assignment [::Array(Parser::AST::Node, Integer), nil]
# @param assignment [Parser::AST::Node, nil]
def initialize assignment: nil, return_type: nil, mass_assignment: nil, **splat
# @param assignment [Parser::AST::Node, nil] First assignment
# that was made to this variable
# @param assignments [Array<Parser::AST::Node>] Possible
# assignments that may have been made to this variable
# @param exclude_return_type [ComplexType, nil] Ensure any
# return type returned will never include any of these unique
# types in the unique types of its complex type.
#
# Example: If a return type is 'Float | Integer | nil' and the
# exclude_return_type is 'Integer', the resulting return
# type will be 'Float | nil' because Integer is excluded.
# @param intersection_return_type [ComplexType, nil] Ensure each unique
# return type is compatible with at least one element of this
# complex type. If a ComplexType used as a return type is an
# union type - we can return any of these - these are
# intersection types - everything we return needs to meet at least
# one of these unique types.
#
# Example: If a return type is 'Numeric | nil' and the
# intersection_return_type is 'Float | nil', the resulting return
# type will be 'Float | nil' because Float is compatible
# with Numeric and nil is compatible with nil.
# @see https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#union-types
# @see https://en.wikipedia.org/wiki/Intersection_type#TypeScript_example
# @param presence [Range, nil]
# @param presence_certain [Boolean]
def initialize assignment: nil, assignments: [], mass_assignment: nil,
presence: nil, presence_certain: false, return_type: nil,
intersection_return_type: nil, exclude_return_type: nil,
**splat
super(**splat)
@assignment = assignment
@assignments = (assignment.nil? ? [] : [assignment]) + assignments
# @type [nil, ::Array(Parser::AST::Node, Integer)]
@mass_assignment = nil
@mass_assignment = mass_assignment
@return_type = return_type
@intersection_return_type = intersection_return_type
@exclude_return_type = exclude_return_type
@presence = presence
@presence_certain = presence_certain
end
# @param presence [Range]
# @param exclude_return_type [ComplexType, nil]
# @param intersection_return_type [ComplexType, nil]
# @param source [::Symbol]
#
# @return [self]
def downcast presence:, exclude_return_type: nil, intersection_return_type: nil,
source: self.source
result = dup
result.exclude_return_type = exclude_return_type
result.intersection_return_type = intersection_return_type
result.source = source
result.presence = presence
result.reset_generated!
result
end
def reset_generated!
@assignment = nil
super
end
def combine_with(other, attrs={})
new_assignments = combine_assignments(other)
new_attrs = attrs.merge({
assignment: assert_same(other, :assignment),
mass_assignment: assert_same(other, :mass_assignment),
# default values don't exist in RBS parameters; it just
# tells you if the arg is optional or not. Prefer a
# provided value if we have one here since we can't rely on
# it from RBS so we can infer from it and typecheck on it.
assignment: choose(other, :assignment),
assignments: new_assignments,
mass_assignment: combine_mass_assignment(other),
return_type: combine_return_type(other),
intersection_return_type: combine_types(other, :intersection_return_type),
exclude_return_type: combine_types(other, :exclude_return_type),
presence: combine_presence(other),
presence_certain: combine_presence_certain(other)
})

@@ -35,2 +106,39 @@ super(other, new_attrs)

# @param other [self]
#
# @return [Array(AST::Node, Integer), nil]
def combine_mass_assignment(other)
# @todo pick first non-nil arbitrarily - we don't yet support
# mass assignment merging
mass_assignment || other.mass_assignment
end
# If a certain pin is being combined with an uncertain pin, we
# end up with a certain result
#
# @param other [self]
#
# @return [Boolean]
def combine_presence_certain(other)
presence_certain? || other.presence_certain?
end
# @return [Parser::AST::Node, nil]
def assignment
@assignment ||= assignments.last
end
# @param other [self]
#
# @return [::Array<Parser::AST::Node>]
def combine_assignments(other)
(other.assignments + assignments).uniq
end
def inner_desc
super + ", presence=#{presence.inspect}, assignments=#{assignments}, " \
"intersection_return_type=#{intersection_return_type&.rooted_tags.inspect}, " \
"exclude_return_type=#{exclude_return_type&.rooted_tags.inspect}"
end
def completion_item_kind

@@ -45,6 +153,2 @@ Solargraph::LanguageServer::CompletionItemKinds::VARIABLE

def return_type
@return_type ||= generate_complex_type
end
def nil_assignment?

@@ -73,2 +177,3 @@ # this will always be false - should it be return_type ==

pos = rng.ending
# @sg-ignore Need to add nil check here
clip = api_map.clip_at(location.filename, pos)

@@ -78,2 +183,3 @@ # Use the return node for inference. The clip might infer from the

chain = Parser.chain(node, nil, nil)
# @sg-ignore Need to add nil check here
result = chain.infer(api_map, closure, clip.locals).self_to_type(closure.context)

@@ -87,9 +193,11 @@ types.push result unless result.undefined?

# @param api_map [ApiMap]
# @return [ComplexType]
# @return [ComplexType, ComplexType::UniqueType]
def probe api_map
unless @assignment.nil?
types = return_types_from_node(@assignment, api_map)
return ComplexType.new(types.uniq) unless types.empty?
end
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?
return adjust_type api_map, type_from_assignment unless type_from_assignment.nil?
# @todo should handle merging types from mass assignments as
# well so that we can do better flow sensitive typing with
# multiple assignments
unless @mass_assignment.nil?

@@ -105,3 +213,6 @@ mass_node, index = @mass_assignment

end.compact!
return ComplexType.new(types.uniq) unless types.empty?
return ComplexType::UNDEFINED if types.empty?
return adjust_type api_map, ComplexType.new(types.uniq).qualify(api_map, *gates)
end

@@ -123,9 +234,190 @@

# @return [ComplexType, nil]
def return_type
generate_complex_type || @return_type || intersection_return_type || ComplexType::UNDEFINED
end
def typify api_map
raw_return_type = super
adjust_type(api_map, raw_return_type)
end
# @sg-ignore need boolish support for ? methods
def presence_certain?
exclude_return_type || intersection_return_type
end
# @param other_loc [Location]
# @sg-ignore flow sensitive typing needs to handle attrs
def starts_at?(other_loc)
location&.filename == other_loc.filename &&
presence &&
# @sg-ignore flow sensitive typing needs to handle attrs
presence.start == other_loc.range.start
end
# Narrow the presence range to the intersection of both.
#
# @param other [self]
#
# @return [Range, nil]
def combine_presence(other)
return presence || other.presence if presence.nil? || other.presence.nil?
# @sg-ignore flow sensitive typing needs to handle attrs
Range.new([presence.start, other.presence.start].max, [presence.ending, other.presence.ending].min)
end
# @param other [self]
# @return [Pin::Closure, nil]
def combine_closure(other)
return closure if self.closure == other.closure
# choose first defined, as that establishes the scope of the variable
if closure.nil? || other.closure.nil?
Solargraph.assert_or_log(:varible_closure_missing) do
"One of the local variables being combined is missing a closure: " \
"#{self.inspect} vs #{other.inspect}"
end
return closure || other.closure
end
# @sg-ignore flow sensitive typing needs to handle attrs
if closure.location.nil? || other.closure.location.nil?
# @sg-ignore flow sensitive typing needs to handle attrs
return closure.location.nil? ? other.closure : closure
end
# if filenames are different, this will just pick one
# @sg-ignore flow sensitive typing needs to handle attrs
return closure if closure.location <= other.closure.location
other.closure
end
# @param other_closure [Pin::Closure]
# @param other_loc [Location]
def visible_at?(other_closure, other_loc)
# @sg-ignore flow sensitive typing needs to handle attrs
location.filename == other_loc.filename &&
# @sg-ignore flow sensitive typing needs to handle attrs
(!presence || presence.include?(other_loc.range.start)) &&
visible_in_closure?(other_closure)
end
def presence_certain?
@presence_certain
end
protected
attr_accessor :exclude_return_type, :intersection_return_type
# @return [Range]
attr_writer :presence
private
# @return [ComplexType]
# @param api_map [ApiMap]
# @param raw_return_type [ComplexType, ComplexType::UniqueType]
#
# @return [ComplexType, ComplexType::UniqueType]
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
qualified_intersection = intersection_return_type&.qualify(api_map, *(closure&.gates || ['']))
minus_exclusions.intersect_with qualified_intersection, api_map
end
# @param other [self]
# @return [Pin::Closure, nil]
def combine_closure(other)
return closure if self.closure == other.closure
# choose first defined, as that establishes the scope of the variable
if closure.nil? || other.closure.nil?
Solargraph.assert_or_log(:varible_closure_missing) do
"One of the local variables being combined is missing a closure: " \
"#{self.inspect} vs #{other.inspect}"
end
return closure || other.closure
end
# @sg-ignore Need to add nil check here
if closure.location.nil? || other.closure.location.nil?
# @sg-ignore Need to add nil check here
return closure.location.nil? ? other.closure : closure
end
# if filenames are different, this will just pick one
# @sg-ignore flow sensitive typing needs to handle attrs
return closure if closure.location <= other.closure.location
other.closure
end
# See if this variable is visible within 'viewing_closure'
#
# @param viewing_closure [Pin::Closure]
# @return [Boolean]
def visible_in_closure? viewing_closure
return false if closure.nil?
# if we're declared at top level, we can't be seen from within
# methods declared tere
# @sg-ignore Need to add nil check here
return false if viewing_closure.is_a?(Pin::Method) && closure.context.tags == 'Class<>'
# @sg-ignore Need to add nil check here
return true if viewing_closure.binder.namespace == closure.binder.namespace
# @sg-ignore Need to add nil check here
return true if viewing_closure.return_type == closure.context
# classes and modules can't see local variables declared
# in their parent closure, so stop here
return false if scope == :instance && viewing_closure.is_a?(Pin::Namespace)
parent_of_viewing_closure = viewing_closure.closure
return false if parent_of_viewing_closure.nil?
visible_in_closure?(parent_of_viewing_closure)
end
# @param other [self]
# @return [ComplexType, nil]
def combine_return_type(other)
combine_types(other, :return_type)
end
# @param other [self]
# @param attr [::Symbol]
#
# @return [ComplexType, nil]
def combine_types(other, attr)
# @type [ComplexType, nil]
type1 = send(attr)
# @type [ComplexType, nil]
type2 = other.send(attr)
if type1 && type2
types = (type1.items + type2.items).uniq
ComplexType.new(types)
else
type1 || type2
end
end
# @return [::Symbol]
def scope
:instance
end
# @return [ComplexType, nil]
def generate_complex_type
tag = docstring.tag(:type)
return ComplexType.try_parse(*tag.types) unless tag.nil? || tag.types.nil? || tag.types.empty?
ComplexType.new
nil
end

@@ -132,0 +424,0 @@ end

@@ -44,3 +44,3 @@ # frozen_string_literal: true

# @param name [String]
# @param comments [String]
# @param comments [String, nil]
# @param source [Symbol, nil]

@@ -61,3 +61,6 @@ # @param docstring [YARD::Docstring, nil]

@combine_priority = combine_priority
# @type [ComplexType, ComplexType::UniqueType, nil]
@binder = nil
assert_source_provided

@@ -77,3 +80,2 @@ assert_location_provided

Solargraph.assert_or_log(:closure, "Closure not set on #{self.class} #{name.inspect} from #{source.inspect}") unless @closure
# @type [Pin::Closure, nil]
@closure

@@ -87,3 +89,2 @@ end

def combine_with(other, attrs={})
raise "tried to combine #{other.class} with #{self.class}" unless other.class == self.class
priority_choice = choose_priority(other)

@@ -99,3 +100,3 @@ return priority_choice unless priority_choice.nil?

name: combined_name,
closure: choose_pin_attr_with_same_name(other, :closure),
closure: combine_closure(other),
comments: choose_longer(other, :comments),

@@ -148,2 +149,3 @@ source: :combined,

# @param other [self]
#
# @return [::Array<YARD::Tags::Directive>, nil]

@@ -153,6 +155,13 @@ def combine_directives(other)

return other.directives if directives.empty?
[directives + other.directives].uniq
(directives + other.directives).uniq
end
# @param other [self]
# @return [Pin::Closure, nil]
def combine_closure(other)
choose_pin_attr_with_same_name(other, :closure)
end
# @param other [self]
# @sg-ignore @type should override probed type
# @return [String]

@@ -180,2 +189,5 @@ def combine_name(other)

@deprecated = nil
@context = nil
@binder = nil
@path = nil
reset_conversions

@@ -200,2 +212,6 @@ end

return_type
elsif return_type.erased_version_of?(other.return_type)
other.return_type
elsif other.return_type.erased_version_of?(return_type)
return_type
elsif dodgy_return_type_source? && !other.dodgy_return_type_source?

@@ -215,5 +231,8 @@ other.return_type

# @sg-ignore need boolish support for ? methods
def dodgy_return_type_source?
# uses a lot of 'Object' instead of 'self'
location&.filename&.include?('core_ext/object/')
location&.filename&.include?('core_ext/object/') ||
# ditto
location&.filename&.include?('stdlib/date/0/date.rbs')
end

@@ -226,3 +245,4 @@

#
# @return [Object, nil]
# @sg-ignore
# @return [undefined, nil]
def choose(other, attr)

@@ -232,2 +252,3 @@ results = [self, other].map(&attr).compact

return true if results.any? { |r| r == true || r == false }
return results.first if results.any? { |r| r.is_a? AST::Node }
results.min

@@ -265,2 +286,3 @@ rescue

# @sg-ignore need boolish support for ? methods
def rbs_location?

@@ -325,3 +347,7 @@ type_location&.rbs?

def assert_same(other, attr)
return false if other.nil?
if other.nil?
Solargraph.assert_or_log("combine_with_#{attr}_nil".to_sym,
"Other was passed in nil in assert_same on #{self}")
return send(attr)
end
val1 = send(attr)

@@ -375,4 +401,7 @@ val2 = other.send(attr)

# depend on those gates
# @sg-ignore Need better handling of #compact
closure.gates.length,
# use basename so that results don't vary system to system
# @sg-ignore Need better handling of #compact
File.basename(closure.best_location.to_s)

@@ -394,3 +423,3 @@ ]

# @param generics_to_resolve [Enumerable<String>]
# @param return_type_context [ComplexType, nil]
# @param return_type_context [ComplexType, ComplexType::UniqueType, nil]
# @param context [ComplexType]

@@ -437,2 +466,3 @@ # @param resolved_generic_values [Hash{String => ComplexType}]

return nil if location.nil?
# @sg-ignore flow sensitive typing needs to handle attrs
location.filename

@@ -473,7 +503,12 @@ end

self.class == other.class &&
# @sg-ignore Translate to something flow sensitive typing understands
name == other.name &&
# @sg-ignore flow sensitive typing needs to handle attrs
(closure == other.closure || (closure && closure.nearly?(other.closure))) &&
# @sg-ignore Translate to something flow sensitive typing understands
(comments == other.comments ||
# @sg-ignore Translate to something flow sensitive typing understands
(((maybe_directives? == false && other.maybe_directives? == false) || compare_directives(directives, other.directives)) &&
compare_docstring_tags(docstring, other.docstring))
# @sg-ignore Translate to something flow sensitive typing understands
compare_docstring_tags(docstring, other.docstring))
)

@@ -505,2 +540,3 @@ end

# @sg-ignore parse_comments will always set @directives
# @return [::Array<YARD::Tags::Directive>]

@@ -542,3 +578,3 @@ def directives

# @param api_map [ApiMap]
# @return [ComplexType]
# @return [ComplexType, ComplexType::UniqueType]
def typify api_map

@@ -551,3 +587,3 @@ return_type.qualify(api_map, *(closure&.gates || ['']))

# @param api_map [ApiMap]
# @return [ComplexType]
# @return [ComplexType, ComplexType::UniqueType]
def probe api_map

@@ -559,5 +595,5 @@ typify api_map

# @param api_map [ApiMap]
# @return [ComplexType]
# @return [ComplexType, ComplexType::UniqueType]
def infer api_map
Solargraph::Logging.logger.warn "WARNING: Pin #infer methods are deprecated. Use #typify or #probe instead."
Solargraph.assert_or_log(:pin_infer, 'WARNING: Pin #infer methods are deprecated. Use #typify or #probe instead.')
type = typify(api_map)

@@ -593,3 +629,3 @@ return type unless type.undefined?

#
# @param return_type [ComplexType]
# @param return_type [ComplexType, ComplexType::UniqueType, nil]
# @return [self]

@@ -644,3 +680,3 @@ def proxy return_type

def inner_desc
closure_info = closure&.desc
closure_info = closure&.name.inspect
binder_info = binder&.desc

@@ -673,6 +709,2 @@ "name=#{name.inspect} return_type=#{type_desc}, context=#{context.rooted_tags}, closure=#{closure_info}, binder=#{binder_info}"

# @return [void]
def reset_generated!
end
protected

@@ -686,3 +718,3 @@

# @return [ComplexType]
# @return [ComplexType, ComplexType::UniqueType, nil]
attr_writer :return_type

@@ -689,0 +721,0 @@

@@ -24,2 +24,3 @@ # frozen_string_literal: true

@node = node
@name = '<block>'
end

@@ -34,5 +35,11 @@

def binder
@rebind&.defined? ? @rebind : closure.binder
out = @rebind if @rebind&.defined?
out ||= super
end
def context
@context = @rebind if @rebind&.defined?
super
end
# @param yield_types [::Array<ComplexType>]

@@ -55,4 +62,6 @@ # @param parameters [::Array<Parameter>]

chain = Parser.chain(receiver, filename, node)
# @sg-ignore Need to add nil check here
clip = api_map.clip_at(location.filename, location.range.start)
locals = clip.locals - [self]
# @sg-ignore Need to add nil check here
meths = chain.define(api_map, closure, locals)

@@ -64,2 +73,3 @@ # @todo Convert logic to use signatures

# @sg-ignore flow sensitive typing needs to handle attrs
yield_types = meth.block.parameters.map(&:return_type)

@@ -74,2 +84,3 @@ # 'arguments' is what the method says it will yield to the

if arg_type.generic? && param_type.defined?
# @sg-ignore Need to add nil check here
namespace_pin = api_map.get_namespace_pins(meth.namespace, closure.namespace).first

@@ -94,4 +105,7 @@ arg_type.resolve_generics(namespace_pin, param_type)

chain = Parser.chain(receiver, location.filename)
# @sg-ignore Need to add nil check here
chain = Parser.chain(receiver, location.filename, node)
# @sg-ignore Need to add nil check here
locals = api_map.source_map(location.filename).locals_at(location)
# @sg-ignore Need to add nil check here
receiver_pin = chain.define(api_map, closure, locals).first

@@ -103,4 +117,12 @@ return ComplexType::UNDEFINED unless receiver_pin

target = chain.base.infer(api_map, receiver_pin, locals)
target = full_context unless target.defined?
name_pin = self
# if we have Foo.bar { |x| ... }, and the bar method references self...
target = if chain.base.defined?
# figure out Foo
chain.base.infer(api_map, name_pin, locals)
else
# if not, any self there must be the context of our closure
# @sg-ignore Need to add nil check here
closure.full_context
end

@@ -107,0 +129,0 @@ ComplexType.try_parse(*types).qualify(api_map, *receiver_pin.gates).self_to_type(target)

module Solargraph
module Pin
# Mix-in for pins which enclose code which the 'break' statement works with-in - e.g., blocks, when, until, ...
# Mix-in for pins which enclose code which the 'break' statement
# works with-in - e.g., blocks, when, until, ...
module Breakable
# @return [Parser::AST::Node]
attr_reader :node
# @return [Location, nil]
attr_reader :location
end
end
end

@@ -24,4 +24,11 @@ # frozen_string_literal: true

def reset_generated!
parameters.each(&:reset_generated!)
super
end
# @sg-ignore Need to add nil check here
# @return [String]
def method_namespace
# @sg-ignore Need to add nil check here
closure.namespace

@@ -84,2 +91,3 @@ end

# @sg-ignore Need to add nil check here
# @return [Array<Pin::Parameter>]

@@ -94,3 +102,6 @@ def blockless_parameters

# @return [Array]
# e.g., [["T"], "", "?", "foo:"] - parameter arity declarations,
# ignoring positional names. Used to match signatures.
#
# @return [Array<Array<String>, String, nil>]
def arity

@@ -100,2 +111,21 @@ [generics, blockless_parameters.map(&:arity_decl), block&.arity]

# e.g., [["T"], "1", "?3", "foo:5"] - parameter arity
# declarations, including the number of unique types in each
# parameter. Used to determine whether combining two
# signatures has lost useful information mapping specific
# parameter types to specific return types.
#
# @return [Array<Array, String, nil>]
def type_arity
[generics, blockless_parameters.map(&:type_arity_decl), block&.type_arity]
end
# Same as type_arity, but includes return type arity at the front.
#
# @return [Array<Array, String, nil>]
def full_type_arity
# @sg-ignore flow sensitive typing needs to handle attrs
[return_type ? return_type.items.count.to_s : nil] + type_arity
end
# @param generics_to_resolve [Enumerable<String>]

@@ -108,2 +138,3 @@ # @param arg_types [Array<ComplexType>, nil]

# @param resolved_generic_values [Hash{String => ComplexType}]
#
# @return [self]

@@ -145,5 +176,7 @@ def resolve_generics_from_context(generics_to_resolve,

# @sg-ignore Need to add nil check here
# @return [String]
def method_name
raise "closure was nil in #{self.inspect}" if closure.nil?
# @sg-ignore Need to add nil check here
@method_name ||= closure.name

@@ -159,2 +192,3 @@ end

# @param resolved_generic_values [Hash{String => ComplexType}]
#
# @return [self]

@@ -194,3 +228,2 @@ def resolve_generics_from_context_until_complete(generics_to_resolve,

# @return [Array<String>]
# @yieldparam [ComplexType]

@@ -217,2 +250,5 @@ # @yieldreturn [ComplexType]

return false if block? && !with_block
# @todo this and its caller should be changed so that this can
# look at the kwargs provided and check names against what
# we acccept
return false if argcount < parcount && !(argcount == parcount - 1 && parameters.last.restarg?)

@@ -222,2 +258,7 @@ true

def reset_generated!
super
@parameters.each(&:reset_generated!)
end
# @return [Integer]

@@ -228,4 +269,10 @@ def mandatory_positional_param_count

# @return [String]
def parameters_to_rbs
# @sg-ignore Need to add nil check here
rbs_generics + '(' + parameters.map { |param| param.to_rbs }.join(', ') + ') ' + (block.nil? ? '' : '{ ' + block.to_rbs + ' } ')
end
def to_rbs
rbs_generics + '(' + parameters.map { |param| param.to_rbs }.join(', ') + ') ' + (block.nil? ? '' : '{ ' + block.to_rbs + ' } ') + '-> ' + return_type.to_rbs
parameters_to_rbs + '-> ' + (return_type&.to_rbs || 'untyped')
end

@@ -232,0 +279,0 @@

@@ -5,3 +5,3 @@ # frozen_string_literal: true

module Pin
class Closure < Base
class Closure < CompoundStatement
# @return [::Symbol] :class or :instance

@@ -11,3 +11,3 @@ attr_reader :scope

# @param scope [::Symbol] :class or :instance
# @param generics [::Array<Pin::Parameter>, nil]
# @param generics [::Array<Pin::String>, nil]
# @param generic_defaults [Hash{String => ComplexType}]

@@ -49,6 +49,2 @@ def initialize scope: :class, generics: nil, generic_defaults: {}, **splat

def binder
@binder || context
end
# @param api_map [Solargraph::ApiMap]

@@ -55,0 +51,0 @@ # @return [void]

@@ -9,8 +9,20 @@ # frozen_string_literal: true

# @return [Source, nil]
# @!method reset_generated!
# @abstract
# @return [void]
# @type @closure [Pin::Closure, nil]
# @type @binder [ComplexType, ComplexType::UniqueType, nil]
# @return [Location]
attr_reader :location
# @todo Missed nil violation
# @return [Location, nil]
attr_accessor :location
# @sg-ignore Solargraph::Pin::Common#closure return type could not be inferred
# @param value [Pin::Closure]
# @return [void]
def closure=(value)
@closure = value
# remove cached values generated from closure
reset_generated!
end
# @return [Pin::Closure, nil]

@@ -27,2 +39,3 @@ def closure

# @todo redundant with Base#return_type?
# @return [ComplexType]

@@ -33,3 +46,3 @@ def return_type

# @return [ComplexType]
# @return [ComplexType, ComplexType::UniqueType]
def context

@@ -46,3 +59,4 @@ # Get the static context from the nearest namespace

# @return [ComplexType]
# @return [ComplexType, ComplexType::UniqueType]
# @sg-ignore https://github.com/castwide/solargraph/pull/1100
def binder

@@ -77,2 +91,3 @@ @binder || context

end
# @sg-ignore Need to add nil check here
here = here.closure

@@ -79,0 +94,0 @@ end

@@ -0,0 +0,0 @@ # frozen_string_literal: true

@@ -46,2 +46,3 @@ # frozen_string_literal: true

return_type: return_type.tag,
# @sg-ignore flow sensitive typing needs to handle attrs
location: (location ? location.to_hash : nil),

@@ -84,3 +85,3 @@ deprecated: deprecated?

#
# @return [String]
# @return [String, nil]
def link_documentation

@@ -87,0 +88,0 @@ @link_documentation ||= generate_link

@@ -16,6 +16,7 @@ # frozen_string_literal: true

# @param receiver [Source::Chain, nil] the source code used to resolve the receiver for this delegated method.
# @param name [String]
# @param receiver_method_name [String] the method name that will be called on the receiver (defaults to :name).
# @param name [String, nil]
# @param receiver_method_name [String, nil] the method name that will be called on the receiver (defaults to :name).
def initialize(method: nil, receiver: nil, name: method&.name, receiver_method_name: name, **splat)
raise ArgumentError, 'either :method or :receiver is required' if (method && receiver) || (!method && !receiver)
# @sg-ignore Need to add nil check here
super(name: name, **splat)

@@ -73,26 +74,36 @@

# @return [Pin::Method, nil]
# @sg-ignore Declared return type ::Solargraph::Pin::Method, nil
# does not match inferred type nil, false for
# Solargraph::Pin::DelegatedMethod#resolve_method
def resolve_method api_map
return if @resolved_method
# @sg-ignore Need to add nil check here
resolver = @receiver_chain.define(api_map, self, []).first
unless resolver
Solargraph.logger.warn \
"Delegated receiver for #{path} was resolved to nil from `#{print_chain(@receiver_chain)}'"
# @sg-ignore Need to add nil check here
Solargraph.logger.warn "Delegated receiver for #{path} was resolved to nil from `#{print_chain(@receiver_chain)}'"
return
end
# @sg-ignore Need to add nil check here
receiver_type = resolver.return_type
# @sg-ignore Need to add nil check here
return if receiver_type.undefined?
receiver_path, method_scope =
# @sg-ignore Need to add nil check here
if @receiver_chain.constant?
# HACK: the `return_type` of a constant is Class<Whatever>, but looking up a method expects
# the arguments `"Whatever"` and `scope: :class`.
# @sg-ignore Need to add nil check here
[receiver_type.to_s.sub(/^Class<(.+)>$/, '\1'), :class]
else
# @sg-ignore Need to add nil check here
[receiver_type.to_s, :instance]
end
# @sg-ignore Need to add nil check here
method_stack = api_map.get_method_stack(receiver_path, @receiver_method_name, scope: method_scope)

@@ -99,0 +110,0 @@ @resolved_method = method_stack.first

@@ -107,2 +107,3 @@ # frozen_string_literal: true

next 0 unless match
# @sg-ignore Need to add nil check here
match[0].length

@@ -109,0 +110,0 @@ end.min

@@ -6,9 +6,13 @@ # frozen_string_literal: true

class InstanceVariable < BaseVariable
# @return [ComplexType]
# @sg-ignore Need to add nil check here
# @return [ComplexType, ComplexType::UniqueType]
def binder
# @sg-ignore Need to add nil check here
closure.binder
end
# @sg-ignore Need to add nil check here
# @return [::Symbol]
def scope
# @sg-ignore Need to add nil check here
closure.binder.scope

@@ -15,0 +19,0 @@ end

@@ -14,8 +14,4 @@ # frozen_string_literal: true

end
def name
@name
end
end
end
end

@@ -6,71 +6,27 @@ # frozen_string_literal: true

class LocalVariable < BaseVariable
# @return [Range]
attr_reader :presence
# @param api_map [ApiMap]
# @return [ComplexType, ComplexType::UniqueType]
def probe api_map
if presence_certain? && return_type && return_type&.defined?
# flow sensitive typing has already figured out this type
# has been downcast - use the type it figured out
# @sg-ignore flow sensitive typing should support ivars
return adjust_type api_map, return_type.qualify(api_map, *gates)
end
def presence_certain?
@presence_certain
super
end
# @param assignment [AST::Node, nil]
# @param presence [Range, nil]
# @param presence_certain [Boolean]
# @param splat [Hash]
def initialize assignment: nil, presence: nil, presence_certain: false, **splat
super(**splat)
@assignment = assignment
@presence = presence
@presence_certain = presence_certain
end
def combine_with(other, attrs={})
new_attrs = {
assignment: assert_same(other, :assignment),
presence_certain: assert_same(other, :presence_certain?),
}.merge(attrs)
new_attrs[:presence] = assert_same(other, :presence) unless attrs.key?(:presence)
# keep this as a parameter
return other.combine_with(self, attrs) if other.is_a?(Parameter) && !self.is_a?(Parameter)
super(other, new_attrs)
super
end
# @param other_closure [Pin::Closure]
# @param other_loc [Location]
def visible_at?(other_closure, other_loc)
location.filename == other_loc.filename &&
presence.include?(other_loc.range.start) &&
match_named_closure(other_closure, closure)
end
def to_rbs
(name || '(anon)') + ' ' + (return_type&.to_rbs || 'untyped')
end
private
# @param tag1 [String]
# @param tag2 [String]
# @return [Boolean]
def match_tags tag1, tag2
# @todo This is an unfortunate hack made necessary by a discrepancy in
# how tags indicate the root namespace. The long-term solution is to
# standardize it, whether it's `Class<>`, an empty string, or
# something else.
tag1 == tag2 ||
(['', 'Class<>'].include?(tag1) && ['', 'Class<>'].include?(tag2))
end
# @param needle [Pin::Base]
# @param haystack [Pin::Base]
# @return [Boolean]
def match_named_closure needle, haystack
return true if needle == haystack || haystack.is_a?(Pin::Block)
cursor = haystack
until cursor.nil?
return true if needle.path == cursor.path
return false if cursor.path && !cursor.path.empty?
cursor = cursor.closure
end
false
end
end
end
end

@@ -29,2 +29,10 @@ # frozen_string_literal: true

def to_rbs
if scope == :class
"alias self.#{name} self.#{original}"
else
"alias #{name} #{original}"
end
end
def path

@@ -31,0 +39,0 @@ @path ||= namespace + (scope == :instance ? '#' : '.') + name

@@ -25,4 +25,5 @@ # frozen_string_literal: true

# @param anon_splat [Boolean]
# @param context [ComplexType, ComplexType::UniqueType, nil]
def initialize visibility: :public, explicit: true, block: :undefined, node: nil, attribute: false, signatures: nil, anon_splat: false,
**splat
context: nil, **splat
super(**splat)

@@ -36,24 +37,5 @@ @visibility = visibility

@anon_splat = anon_splat
@context = context if context
end
# @param signature_pins [Array<Pin::Signature>]
# @return [Array<Pin::Signature>]
def combine_all_signature_pins(*signature_pins)
# @type [Hash{Array => Array<Pin::Signature>}]
by_arity = {}
signature_pins.each do |signature_pin|
by_arity[signature_pin.arity] ||= []
by_arity[signature_pin.arity] << signature_pin
end
by_arity.transform_values! do |same_arity_pins|
# @param memo [Pin::Signature, nil]
# @param signature [Pin::Signature]
same_arity_pins.reduce(nil) do |memo, signature|
next signature if memo.nil?
memo.combine_with(signature)
end
end
by_arity.values.flatten
end
# @param other [Pin::Method]

@@ -71,16 +53,2 @@ # @return [::Symbol]

# @param other [Pin::Method]
# @return [Array<Pin::Signature>]
def combine_signatures(other)
all_undefined = signatures.all? { |sig| sig.return_type.undefined? }
other_all_undefined = other.signatures.all? { |sig| sig.return_type.undefined? }
if all_undefined && !other_all_undefined
other.signatures
elsif other_all_undefined && !all_undefined
signatures
else
combine_all_signature_pins(*signatures, *other.signatures)
end
end
def combine_with(other, attrs = {})

@@ -98,3 +66,2 @@ priority_choice = choose_priority(other)

visibility: combine_visibility(other),
# @sg-ignore https://github.com/castwide/solargraph/pull/1050
explicit: explicit? || other.explicit?,

@@ -167,2 +134,4 @@ block: combine_blocks(other),

# @sg-ignore flow sensitive typing needs to remove literal with
# this unless block
# @return [Pin::Signature, nil]

@@ -187,5 +156,6 @@ def block

# @param parameters [::Array<Parameter>]
# @param return_type [ComplexType]
# @param return_type [ComplexType, nil]
# @return [Signature]
def generate_signature(parameters, return_type)
# @type [Pin::Signature, nil]
block = nil

@@ -211,2 +181,3 @@ yieldparam_tags = docstring.tags(:yieldparam)

decl: decl,
# @sg-ignore flow sensitive typing needs to handle attrs
presence: location ? location.range : nil,

@@ -257,2 +228,3 @@ return_type: ComplexType.try_parse(*p.types),

end.to_s
# @sg-ignore Need to add nil check here
detail += "=#{probed? ? '~' : (proxied? ? '^' : '>')} #{return_type.to_s}" unless return_type.undefined?

@@ -287,2 +259,3 @@ detail.strip!

rbs = "def #{name}: #{signatures.first.to_rbs}"
# @sg-ignore Need to add nil check here
signatures[1..].each do |sig|

@@ -306,2 +279,3 @@ rbs += "\n"

def typify api_map
# @sg-ignore Need to add nil check here
logger.debug { "Method#typify(self=#{self}, binder=#{binder}, closure=#{closure}, context=#{context.rooted_tags}, return_type=#{return_type.rooted_tags}) - starting" }

@@ -316,2 +290,3 @@ decl = super

unless type.nil?
# @sg-ignore Need to add nil check here
qualified = type.qualify(api_map, *closure.gates)

@@ -410,3 +385,3 @@ logger.debug { "Method#typify(self=#{self}) => #{qualified.rooted_tags.inspect}" }

# @return [::Array<Pin::Method>]
# @return [::Array<Pin::Signature>]
def overloads

@@ -429,2 +404,3 @@ # Ignore overload tags with nil parameters. If it's not an array, the

decl: decl,
# @sg-ignore flow sensitive typing needs to handle attrs
presence: location ? location.range : nil,

@@ -484,2 +460,3 @@ return_type: param_type_from_name(tag, src.first),

# @sg-ignore Need to add nil check here
def dodgy_visibility_source?

@@ -489,2 +466,3 @@ # as of 2025-03-12, the RBS generator used for

# inside 'class << self' blocks, but YARD did OK at it
# @sg-ignore Need to add nil check here
source == :rbs && scope == :class && type_location&.filename&.include?('generated') && return_type.undefined? ||

@@ -503,2 +481,67 @@ # YARD's RBS generator seems to miss a lot of should-be protected instance methods

# @param other [Pin::Method]
# @return [Array<Pin::Signature>]
def combine_signatures(other)
all_undefined = signatures.all? { |sig| !sig.return_type&.defined? }
other_all_undefined = other.signatures.all? { |sig| !sig.return_type&.defined? }
if all_undefined && !other_all_undefined
other.signatures
elsif other_all_undefined && !all_undefined
signatures
else
combine_signatures_by_type_arity(*signatures, *other.signatures)
end
end
# @param signature_pins [Array<Pin::Signature>]
#
# @return [Array<Pin::Signature>]
def combine_signatures_by_type_arity(*signature_pins)
# @type [Hash{Array => Array<Pin::Signature>}]
by_type_arity = {}
signature_pins.each do |signature_pin|
by_type_arity[signature_pin.type_arity] ||= []
by_type_arity[signature_pin.type_arity] << signature_pin
end
by_type_arity.transform_values! do |same_type_arity_signatures|
combine_same_type_arity_signatures same_type_arity_signatures
end
by_type_arity.values.flatten
end
# @param same_type_arity_signatures [Array<Pin::Signature>]
#
# @return [Array<Pin::Signature>]
def combine_same_type_arity_signatures(same_type_arity_signatures)
# This is an O(n^2) operation, so bail out if n is not small
return same_type_arity_signatures if same_type_arity_signatures.length > 10
# @param old_signatures [Array<Pin::Signature>]
# @param new_signature [Pin::Signature]
same_type_arity_signatures.reduce([]) do |old_signatures, new_signature|
next [new_signature] if old_signatures.empty?
found_merge = false
old_signatures.flat_map do |old_signature|
potential_new_signature = old_signature.combine_with(new_signature)
if potential_new_signature.type_arity == old_signature.type_arity
# the number of types in each parameter and return type
# match, so we found compatible signatures to merge. If
# we increased the number of types, we'd potentially
# have taken away the ability to use parameter types to
# choose the correct return type (while Ruby doesn't
# dispatch based on type, RBS does distinguish overloads
# based on types, not just arity, allowing for type
# information describing how methods behave based on
# their input types)
old_signatures - [old_signature] + [potential_new_signature]
else
old_signatures + [new_signature]
end
end
end
end
# @param name [String]

@@ -551,10 +594,10 @@ # @param asgn [Boolean]

# @param api_map [ApiMap]
# @return [ComplexType, nil]
# @return [ComplexType, ComplexType::UniqueType, nil]
def see_reference api_map
# This should actually be an intersection type
# @param ref [YARD::Tags::Tag, Solargraph::Yard::Tags::RefTag]
# @param ref [YARD::Tags::Tag, YARD::Tags::RefTag]
docstring.ref_tags.each do |ref|
# @sg-ignore ref should actually be an intersection type
next unless ref.tag_name == 'return' && ref.owner
# @sg-ignore ref should actually be an intersection type
# @sg-ignore should actually be an intersection type
result = resolve_reference(ref.owner.to_s, api_map)

@@ -565,2 +608,3 @@ return result unless result.nil?

return nil if match.nil?
# @sg-ignore Need to add nil check here
resolve_reference match[1], api_map

@@ -580,2 +624,3 @@ end

stack.each do |pin|
# @sg-ignore Need to add nil check here
return pin.return_type unless pin.return_type.undefined?

@@ -588,3 +633,3 @@ end

# @param api_map [ApiMap]
# @return [ComplexType, nil]
# @return [ComplexType, ComplexType::UniqueType, nil]
def resolve_reference ref, api_map

@@ -597,2 +642,3 @@ parts = ref.split(/[.#]/)

return ComplexType::UNDEFINED if fqns.nil?
# @sg-ignore Need to add nil check here
path = fqns + ref[parts.first.length] + parts.last

@@ -633,5 +679,7 @@ end

clip = api_map.clip_at(
# @sg-ignore Need to add nil check here
location.filename,
rng.ending
)
# @sg-ignore Need to add nil check here
chain = Solargraph::Parser.chain(n, location.filename)

@@ -638,0 +686,0 @@ type = chain.infer(api_map, self, clip.locals)

@@ -29,3 +29,2 @@ # frozen_string_literal: true

if name.start_with?('::')
# @type [String]
name = name[2..-1] || ''

@@ -43,2 +42,3 @@ @closure = Solargraph::Pin::ROOT_PIN

else
# @sg-ignore Need to add nil check here
closure.full_context.namespace + '::'

@@ -53,2 +53,8 @@ end

def reset_generated!
@return_type = nil
@full_context = nil
@path = nil
end
def to_rbs

@@ -55,0 +61,0 @@ "#{@type.to_s} #{return_type.all_params.first.to_rbs}#{rbs_generics}".strip

@@ -33,10 +33,27 @@ # frozen_string_literal: true

def combine_with(other, attrs={})
new_attrs = {
decl: assert_same(other, :decl),
presence: choose(other, :presence),
asgn_code: choose(other, :asgn_code),
}.merge(attrs)
super(other, new_attrs)
# Parameters can be combined with local variables
new_attrs = if other.is_a?(Parameter)
{
decl: assert_same(other, :decl),
asgn_code: choose(other, :asgn_code)
}
else
{
decl: decl,
asgn_code: asgn_code
}
end
super(other, new_attrs.merge(attrs))
end
def combine_return_type(other)
out = super
if out&.undefined?
# allow our return_type method to provide a better type
# using :param tag
out = nil
end
out
end
def keyword?

@@ -47,2 +64,3 @@ [:kwarg, :kwoptarg].include?(decl)

def kwrestarg?
# @sg-ignore flow sensitive typing needs to handle attrs
decl == :kwrestarg || (assignment && [:HASH, :hash].include?(assignment.type))

@@ -77,2 +95,7 @@ end

# @return [String]
def type_arity_decl
arity_decl + return_type.items.count.to_s
end
def arg?

@@ -86,2 +109,10 @@ decl == :arg

def mandatory_positional?
decl == :arg
end
def positional?
!keyword?
end
def rest?

@@ -130,2 +161,7 @@ decl == :restarg || decl == :kwrestarg

def reset_generated!
@return_type = nil if param_tag
super
end
# @return [String]

@@ -143,2 +179,3 @@ def full

# @sg-ignore super always sets @return_type to something
# @return [ComplexType]

@@ -150,2 +187,3 @@ def return_type

@return_type = ComplexType.try_parse(*found.types) unless found.nil? or found.types.nil?
# @sg-ignore flow sensitive typing should be able to handle redefinition
if @return_type.undefined?

@@ -162,3 +200,2 @@ if decl == :restarg

super
@return_type
end

@@ -168,6 +205,7 @@

#
# @sg-ignore Need to add nil check here
# @return [Integer]
def index
# @type [Method, Block]
method_pin = closure
# @sg-ignore Need to add nil check here
method_pin.parameter_names.index(name)

@@ -178,4 +216,11 @@ end

def typify api_map
return return_type.qualify(api_map, *closure.gates) unless return_type.undefined?
closure.is_a?(Pin::Block) ? typify_block_param(api_map) : typify_method_param(api_map)
new_type = super
return new_type if new_type.defined?
# sniff based on param tags
new_type = closure.is_a?(Pin::Block) ? typify_block_param(api_map) : typify_method_param(api_map)
return adjust_type api_map, new_type.self_to_type(full_context) if new_type.defined?
adjust_type api_map, super.self_to_type(full_context)
end

@@ -189,5 +234,12 @@

ptype = typify api_map
ptype.undefined? || ptype.can_assign?(api_map, atype) || ptype.generic?
return true if ptype.undefined?
return true if atype.conforms_to?(api_map,
ptype,
:method_call,
[:allow_empty_params, :allow_undefined])
ptype.generic?
end
# @sg-ignore flow sensitive typing needs to handle attrs
def documentation

@@ -201,8 +253,15 @@ tag = param_tag

def generate_complex_type
nil
end
# @return [YARD::Tags::Tag, nil]
def param_tag
# @sg-ignore Need to add nil check here
params = closure.docstring.tags(:param)
# @sg-ignore Need to add nil check here
params.each do |p|
return p if p.name == name
end
# @sg-ignore Need to add nil check here
params[index] if index && params[index] && (params[index].name.nil? || params[index].name.empty?)

@@ -215,3 +274,3 @@ end

block_pin = closure
if block_pin.is_a?(Pin::Block) && block_pin.receiver
if block_pin.is_a?(Pin::Block) && block_pin.receiver && index
return block_pin.typify_parameters(api_map)[index]

@@ -225,2 +284,3 @@ end

def typify_method_param api_map
# @sg-ignore Need to add nil check here
meths = api_map.get_method_stack(closure.full_context.tag, closure.name, scope: closure.scope)

@@ -239,2 +299,3 @@ # meths.shift # Ignore the first one

end
# @sg-ignore Need to add nil check here
return ComplexType.try_parse(*found.types).qualify(api_map, *meth.closure.gates) unless found.nil? || found.types.nil?

@@ -248,2 +309,3 @@ end

# @param skip [::Array]
#
# @return [::Array<YARD::Tags::Tag>]

@@ -256,3 +318,3 @@ def see_reference heredoc, api_map, skip = []

next unless ref.tag_name == 'param' && ref.owner
# @sg-ignore ref should actually be an intersection type
# @todo ref should actually be an intersection type
result = resolve_reference(ref.owner.to_s, api_map, skip)

@@ -277,2 +339,3 @@ return result unless result.nil?

return nil if fqns.nil?
# @sg-ignore Need to add nil check here
path = fqns + ref[parts.first.length] + parts.last

@@ -279,0 +342,0 @@ end

@@ -6,3 +6,3 @@ # frozen_string_literal: true

class ProxyType < Base
# @param return_type [ComplexType]
# @param return_type [ComplexType, ComplexType::UniqueType]
# @param gates [Array<String>, nil] Namespaces to try while resolving non-rooted types

@@ -29,2 +29,3 @@ # @param binder [ComplexType, ComplexType::UniqueType, nil]

parts = context.namespace.split('::')
# @sg-ignore Need to add nil check here
namespace = parts[0..-2].join('::').to_s

@@ -31,0 +32,0 @@ closure = Solargraph::Pin::Namespace.new(name: namespace, source: :proxy_type)

@@ -33,4 +33,6 @@ # frozen_string_literal: true

# @sg-ignore Need to add nil check here
# @return [Array<String>]
def reference_gates
# @sg-ignore Need to add nil check here
closure.gates

@@ -37,0 +39,0 @@ end

@@ -10,3 +10,3 @@ # frozen_string_literal: true

# @return [::Array<Symbol>]
# @return [::Array<::Symbol>]
attr_reader :delete

@@ -13,0 +13,0 @@

@@ -9,3 +9,5 @@ # frozen_string_literal: true

class Superclass < Reference
# @sg-ignore Need to add nil check here
def reference_gates
# @sg-ignore Need to add nil check here
@reference_gates ||= closure.gates - [closure.path]

@@ -12,0 +14,0 @@ end

@@ -54,2 +54,3 @@ # frozen_string_literal: true

# @param str2 [String]
#
# @return [Float]

@@ -56,0 +57,0 @@ def fuzzy_string_match str1, str2

@@ -13,2 +13,3 @@ module Solargraph

def generics
# @type [Array<::String, nil>]
@generics ||= [].freeze

@@ -23,2 +24,3 @@ end

# @ sg-ignore need boolish support for ? methods
def dodgy_return_type_source?

@@ -37,4 +39,7 @@ super || closure&.dodgy_return_type_source?

def typify api_map
# @sg-ignore Need to add nil check here
if return_type.defined?
# @sg-ignore Need to add nil check here
qualified = return_type.qualify(api_map, closure.namespace)
# @sg-ignore Need to add nil check here
logger.debug { "Signature#typify(self=#{self}) => #{qualified.rooted_tags.inspect}" }

@@ -52,4 +57,7 @@ return qualified

next unless sig
# @sg-ignore Need to add nil check here
unless sig.return_type.undefined?
# @sg-ignore Need to add nil check here
qualified = sig.return_type.qualify(api_map, closure.namespace)
# @sg-ignore Need to add nil check here
logger.debug { "Signature#typify(self=#{self}) => #{qualified.rooted_tags.inspect}" }

@@ -56,0 +64,0 @@ return qualified

@@ -6,3 +6,3 @@ # frozen_string_literal: true

class Symbol < Base
# @param location [Solargraph::Location]
# @param location [Solargraph::Location, nil]
# @param name [String]

@@ -9,0 +9,0 @@ def initialize(location, name, **kwargs)

@@ -5,3 +5,3 @@ # frozen_string_literal: true

module Pin
class Until < Base
class Until < CompoundStatement
include Breakable

@@ -8,0 +8,0 @@

@@ -5,3 +5,3 @@ # frozen_string_literal: true

module Pin
class While < Base
class While < CompoundStatement
include Breakable

@@ -8,0 +8,0 @@

@@ -24,3 +24,2 @@ # frozen_string_literal: true

# @sg-ignore Fix "Not enough arguments to Module#protected"
protected def equality_fields

@@ -62,17 +61,3 @@ [line, character]

return 0 if text.empty?
newline_index = -1
line = -1
last_line_index = 0
while (newline_index = text.index("\n", newline_index + 1)) && line <= position.line
line += 1
break if line == position.line
line_length = newline_index - last_line_index
last_line_index = newline_index
end
last_line_index += 1 if position.line > 0
last_line_index + position.character
text.lines[0...position.line].sum(&:length) + position.character
end

@@ -93,4 +78,2 @@

#
# @raise [InvalidOffsetError] if the offset is outside the text range
#
# @param text [String]

@@ -100,15 +83,19 @@ # @param offset [Integer]

def self.from_offset text, offset
raise InvalidOffsetError if offset > text.length
cursor = 0
line = 0
character = offset
newline_index = -1
while (newline_index = text.index("\n", newline_index + 1)) && newline_index < offset
# @type [Integer, nil]
character = nil
text.lines.each do |l|
line_length = l.length
char_length = l.chomp.length
if cursor + char_length >= offset
character = offset - cursor
break
end
cursor += line_length
line += 1
character = offset - newline_index - 1
end
character = 0 if character.nil? and (cursor - offset).between?(0, 1)
raise InvalidOffsetError if character.nil?
# @sg-ignore flow sensitive typing needs to handle 'raise if'
Position.new(line, character)

@@ -132,3 +119,2 @@ end

return false unless other.is_a?(Position)
# @sg-ignore https://github.com/castwide/solargraph/pull/1114
line == other.line and character == other.character

@@ -135,0 +121,0 @@ end

@@ -22,3 +22,2 @@ # frozen_string_literal: true

# @sg-ignore Fix "Not enough arguments to Module#protected"
protected def equality_fields

@@ -31,8 +30,5 @@ [start, ending]

return nil unless other.is_a?(Range)
# @sg-ignore https://github.com/castwide/solargraph/pull/1114
if start == other.start
# @sg-ignore https://github.com/castwide/solargraph/pull/1114
ending <=> other.ending
else
# @sg-ignore https://github.com/castwide/solargraph/pull/1114
start <=> other.start

@@ -59,4 +55,7 @@ end

position = Position.normalize(position)
# @sg-ignore flow sensitive typing should be able to handle redefinition
return false if position.line < start.line || position.line > ending.line
# @sg-ignore flow sensitive typing should be able to handle redefinition
return false if position.line == start.line && position.character < start.character
# @sg-ignore flow sensitive typing should be able to handle redefinition
return false if position.line == ending.line && position.character > ending.character

@@ -69,5 +68,7 @@ true

# @param position [Position, Array(Integer, Integer)]
# @sg-ignore flow sensitive typing should be able to handle redefinition
# @return [Boolean]
def include? position
position = Position.normalize(position)
# @sg-ignore flow sensitive typing should be able to handle redefinition
contain?(position) && !(position.line == start.line && position.character == start.character)

@@ -89,3 +90,3 @@ end

#
# @param node [Parser::AST::Node]
# @param node [::Parser::AST::Node]
# @return [Range, nil]

@@ -109,3 +110,2 @@ def self.from_node node

return false unless other.is_a?(Range)
# @sg-ignore https://github.com/castwide/solargraph/pull/1114
start == other.start && ending == other.ending

@@ -112,0 +112,0 @@ end

@@ -29,3 +29,4 @@ # frozen_string_literal: true

# @param rbs_collection_paths [Array<Pathname, String>]
def initialize library, version = nil, rbs_collection_config_path: nil, rbs_collection_paths: []
# @param out [StringIO, IO, nil] where to log messages
def initialize library, version = nil, rbs_collection_config_path: nil, rbs_collection_paths: [], out: $stderr
if rbs_collection_config_path.nil? && !rbs_collection_paths.empty?

@@ -41,2 +42,24 @@ raise 'Please provide rbs_collection_config_path if you provide rbs_collection_paths'

CACHE_KEY_GEM_EXPORT = 'gem-export'
CACHE_KEY_UNRESOLVED = 'unresolved'
CACHE_KEY_STDLIB = 'stdlib'
CACHE_KEY_LOCAL = 'local'
# @param cache_key [String, nil]
# @return [String, nil] a description of the source of the RBS info
def self.rbs_source_desc cache_key
case cache_key
when CACHE_KEY_GEM_EXPORT
'RBS gem export'
when CACHE_KEY_UNRESOLVED
nil
when CACHE_KEY_STDLIB
'RBS standard library'
when CACHE_KEY_LOCAL
'local RBS shims'
else
'RBS collection'
end
end
# @return [RBS::EnvironmentLoader]

@@ -52,6 +75,11 @@ def loader

def cache_key
return CACHE_KEY_UNRESOLVED unless resolved?
@hextdigest ||= begin
# @type [String, nil]
data = nil
# @type gem_config [nil, Hash{String => Hash{String => String}}]
gem_config = nil
if rbs_collection_config_path
# @sg-ignore flow sensitive typing needs to handle attrs
lockfile_path = RBS::Collection::Config.to_lockfile_path(Pathname.new(rbs_collection_config_path))

@@ -64,12 +92,18 @@ if lockfile_path.exist?

end
if data.nil? || data.empty?
if resolved?
# definitely came from the gem itself and not elsewhere -
# only one version per gem
'gem-export'
if gem_config.nil?
CACHE_KEY_STDLIB
else
# @type [String]
source = gem_config.dig('source', 'type')
case source
when 'rubygems'
CACHE_KEY_GEM_EXPORT
when 'local'
CACHE_KEY_LOCAL
when 'stdlib'
CACHE_KEY_STDLIB
else
'unresolved'
# @sg-ignore Need to add nil check here
Digest::SHA1.hexdigest(data)
end
else
Digest::SHA1.hexdigest(data)
end

@@ -84,2 +118,6 @@ end

def self.from_gemspec gemspec, rbs_collection_path, rbs_collection_config_path
# prefers stdlib RBS if available
rbs_map = RbsMap::StdlibMap.new(gemspec.name)
return rbs_map if rbs_map.resolved?
rbs_map = RbsMap.new(gemspec.name, gemspec.version,

@@ -96,5 +134,10 @@ rbs_collection_paths: [rbs_collection_path].compact,

# @param out [IO, nil] where to log messages
# @return [Array<Pin::Base>]
def pins
@pins ||= resolved? ? conversions.pins : []
def pins out: $stderr
@pins ||= if resolved?
conversions.pins
else
[]
end
end

@@ -105,2 +148,5 @@

# @param klass [Class<generic<T>>]
#
# @sg-ignore Need to be able to resolve generics based on a
# Class<generic<T>> param
# @return [generic<T>, nil]

@@ -150,15 +196,23 @@ def path_pin path, klass = Pin::Base

def resolve_dependencies?
# we need to resolve dependencies via gemfile.lock manually for
# YARD regardless, so use same mechanism here so we don't
# duplicate work generating pins from dependencies
false
end
# @param loader [RBS::EnvironmentLoader]
# @param library [String]
# @param version [String, nil]
# @param version [String, nil] the version of the library to load, or nil for any
# @param out [StringIO, IO, nil] where to log messages
# @return [Boolean] true if adding the library succeeded
def add_library loader, library, version
def add_library loader, library, version, out: $stderr
@resolved = if loader.has_library?(library: library, version: version)
loader.add library: library, version: version
logger.debug { "#{short_name} successfully loaded library #{library}:#{version}" }
true
else
logger.info { "#{short_name} did not find data for library #{library}:#{version}" }
false
end
loader.add library: library, version: version, resolve_dependencies: resolve_dependencies?
logger.debug { "#{short_name} successfully loaded library #{library}:#{version}" }
true
else
logger.info { "#{short_name} did not find data for library #{library}:#{version}" }
false
end
end

@@ -165,0 +219,0 @@

@@ -68,3 +68,3 @@ # frozen_string_literal: true

when RBS::AST::Declarations::TypeAlias
# @sg-ignore https://github.com/castwide/solargraph/pull/1114
# @sg-ignore flow sensitive typing should support case/when
type_aliases[decl.name.to_s] = decl

@@ -100,3 +100,3 @@ when RBS::AST::Declarations::Module

include_pin = Solargraph::Pin::Reference::Include.new(
name: decl.name.relative!.to_s,
name: type.rooted_name,
type_location: location_decl_to_pin_location(decl.location),

@@ -125,22 +125,33 @@ generic_values: generic_values,

when RBS::AST::Members::MethodDefinition
# @sg-ignore flow based typing needs to understand case when class pattern
method_def_to_pin(member, closure, context)
when RBS::AST::Members::AttrReader
# @sg-ignore flow based typing needs to understand case when class pattern
attr_reader_to_pin(member, closure, context)
when RBS::AST::Members::AttrWriter
# @sg-ignore flow based typing needs to understand case when class pattern
attr_writer_to_pin(member, closure, context)
when RBS::AST::Members::AttrAccessor
# @sg-ignore flow based typing needs to understand case when class pattern
attr_accessor_to_pin(member, closure, context)
when RBS::AST::Members::Include
# @sg-ignore flow based typing needs to understand case when class pattern
include_to_pin(member, closure)
when RBS::AST::Members::Prepend
# @sg-ignore flow based typing needs to understand case when class pattern
prepend_to_pin(member, closure)
when RBS::AST::Members::Extend
# @sg-ignore flow based typing needs to understand case when class pattern
extend_to_pin(member, closure)
when RBS::AST::Members::Alias
# @sg-ignore flow based typing needs to understand case when class pattern
alias_to_pin(member, closure)
when RBS::AST::Members::ClassInstanceVariable
# @sg-ignore flow based typing needs to understand case when class pattern
civar_to_pin(member, closure)
when RBS::AST::Members::ClassVariable
# @sg-ignore flow based typing needs to understand case when class pattern
cvar_to_pin(member, closure)
when RBS::AST::Members::InstanceVariable
# @sg-ignore flow based typing needs to understand case when class pattern
ivar_to_pin(member, closure)

@@ -152,2 +163,3 @@ when RBS::AST::Members::Public

when RBS::AST::Declarations::Base
# @sg-ignore flow based typing needs to understand case when class pattern
convert_decl_to_pin(member, closure)

@@ -239,2 +251,4 @@ else

raise "Invalid type for module declaration: #{module_pin.class}" unless module_pin.is_a?(Pin::Namespace)
add_mixins decl, module_pin.closure

@@ -245,3 +259,3 @@ end

# @param tag [String]
# @param comments [String]
# @param comments [String, nil]
# @param decl [RBS::AST::Declarations::ClassAlias, RBS::AST::Declarations::Constant, RBS::AST::Declarations::ModuleAlias]

@@ -255,2 +269,3 @@ # @param base [String, nil] Optional conversion of tag to base<tag>

name = parts.last
# @sg-ignore Need to add nil check here
closure = pins.select { |pin| pin && pin.path == parts[0..-2].join('::') }.first

@@ -357,3 +372,3 @@ else

# @param decl [RBS::AST::Members::MethodDefinition, RBS::AST::Members::AttrReader, RBS::AST::Members::AttrAccessor]
# @param decl [RBS::AST::Members::MethodDefinition, RBS::AST::Members::AttrReader, RBS::AST::Members::AttrWriter, RBS::AST::Members::AttrAccessor]
# @param closure [Pin::Closure]

@@ -363,3 +378,2 @@ # @param context [Context]

# @param name [String] The name of the method
# @sg-ignore
# @return [Symbol]

@@ -444,4 +458,5 @@ def calculate_method_visibility(decl, context, closure, scope, name)

signature_parameters, signature_return_type = parts_of_function(overload.method_type, pin)
block = if overload.method_type.block
block_parameters, block_return_type = parts_of_function(overload.method_type.block, pin)
rbs_block = overload.method_type.block
block = if rbs_block
block_parameters, block_return_type = parts_of_function(rbs_block, pin)
Pin::Signature.new(generics: generics, parameters: block_parameters, return_type: block_return_type, source: :rbs,

@@ -460,5 +475,8 @@ type_location: type_location, closure: pin)

# @sg-ignore flow sensitive typing should handle return nil if location&.name.nil?
start_pos = Position.new(location.start_line - 1, location.start_column)
# @sg-ignore flow sensitive typing should handle return nil if location&.name.nil?
end_pos = Position.new(location.end_line - 1, location.end_column)
range = Range.new(start_pos, end_pos)
# @sg-ignore flow sensitve typing should handle return nil if location&.name.nil?
Location.new(location.name.to_s, range)

@@ -719,3 +737,3 @@ end

# @param type [RBS::MethodType]
# @param type [RBS::MethodType, RBS::Types::Block]
# @return [String]

@@ -752,3 +770,5 @@ def method_type_to_tag type

# @param type [RBS::Types::Bases::Base]
# @param type [RBS::Types::Bases::Base,Object] RBS type object.
# Note: Generally these extend from RBS::Types::Bases::Base,
# but not all.
# @return [String]

@@ -810,2 +830,5 @@ def other_type_to_tag type

else
# RBS doesn't provide a common base class for its type AST nodes
#
# @sg-ignore all types should include location
Solargraph.logger.warn "Unrecognized RBS type: #{type.class} at #{type.location}"

@@ -817,3 +840,3 @@ 'undefined'

# @param decl [RBS::AST::Declarations::Class, RBS::AST::Declarations::Module]
# @param namespace [Pin::Namespace]
# @param namespace [Pin::Namespace, nil]
# @return [void]

@@ -820,0 +843,0 @@ def add_mixins decl, namespace

@@ -0,0 +0,0 @@ # frozen_string_literal: true

@@ -18,27 +18,34 @@ # frozen_string_literal: true

# @param out [IO, nil] output stream for logging
# @return [Enumerable<Pin::Base>]
def pins
def pins out: $stderr
return @pins if @pins
@pins = cache_core(out: out)
end
@pins = []
# @param out [StringIO, IO, nil] output stream for logging
# @return [Array<Pin::Base>]
def cache_core out: $stderr
new_pins = []
cache = PinCache.deserialize_core
if cache
@pins.replace cache
else
@pins.concat conversions.pins
return cache if cache
new_pins.concat conversions.pins
# Avoid RBS::DuplicatedDeclarationError by loading in a different EnvironmentLoader
fill_loader = RBS::EnvironmentLoader.new(core_root: nil, repository: RBS::Repository.new(no_stdlib: false))
fill_loader.add(path: Pathname(FILLS_DIRECTORY))
fill_conversions = Conversions.new(loader: fill_loader)
@pins.concat fill_conversions.pins
# Avoid RBS::DuplicatedDeclarationError by loading in a different EnvironmentLoader
fill_loader = RBS::EnvironmentLoader.new(core_root: nil, repository: RBS::Repository.new(no_stdlib: false))
fill_loader.add(path: Pathname(FILLS_DIRECTORY))
out&.puts 'Caching RBS pins for Ruby core'
fill_conversions = Conversions.new(loader: fill_loader)
new_pins.concat fill_conversions.pins
@pins.concat RbsMap::CoreFills::ALL
# add some overrides
new_pins.concat RbsMap::CoreFills::ALL
processed = ApiMap::Store.new(pins).pins.reject { |p| p.is_a?(Solargraph::Pin::Reference::Override) }
@pins.replace processed
# process overrides, then remove any which couldn't be resolved
processed = ApiMap::Store.new(new_pins).pins.reject { |p| p.is_a?(Solargraph::Pin::Reference::Override) }
new_pins.replace processed
PinCache.serialize_core @pins
end
@pins
PinCache.serialize_core new_pins
new_pins
end

@@ -45,0 +52,0 @@

@@ -15,6 +15,10 @@ # frozen_string_literal: true

# @param rebuild [Boolean] build pins regardless of whether we
# have cached them already
# @param library [String]
def initialize library
# @param out [StringIO, IO, nil] where to log messages
def initialize library, rebuild: false, out: $stderr
cached_pins = PinCache.deserialize_stdlib_require library
if cached_pins
if cached_pins && !rebuild
@pins = cached_pins

@@ -24,7 +28,7 @@ @resolved = true

logger.debug { "Deserialized #{cached_pins.length} cached pins for stdlib require #{library.inspect}" }
else
super
elsif self.class.source.has? library, nil
super(library, out: out)
unless resolved?
@pins = []
logger.info { "Could not resolve #{library.inspect}" }
logger.debug { "StdlibMap could not resolve #{library.inspect}" }
return

@@ -38,2 +42,27 @@ end

# @return [RBS::Collection::Sources::Stdlib]
def self.source
@source ||= RBS::Collection::Sources::Stdlib.instance
end
# @param name [String]
# @param version [String, nil]
# @return [Array<Hash{String => String}>, nil]
def self.stdlib_dependencies name, version = nil
if source.has?(name, version)
# @sg-ignore we are relying on undocumented behavior where
# passing version=nil gives the latest version it has
source.dependencies_of(name, version)
else
[]
end
end
def resolve_dependencies?
# there are 'virtual' dependencies for stdlib gems in RBS that
# aren't represented in the actual gemspecs that we'd
# otherwise use
true
end
# @param library [String]

@@ -40,0 +69,0 @@ # @return [StdlibMap]

@@ -0,0 +0,0 @@ # frozen_string_literal: true

@@ -6,2 +6,3 @@ # frozen_string_literal: true

require 'yard'
require 'yaml'

@@ -108,16 +109,4 @@ module Solargraph

def cache gem, version = nil
gemspec = Gem::Specification.find_by_name(gem, version)
if options[:rebuild] || !PinCache.has_yard?(gemspec)
pins = GemPins.build_yard_pins(['yard-activesupport-concern'], gemspec)
PinCache.serialize_yard_gem(gemspec, pins)
end
workspace = Solargraph::Workspace.new(Dir.pwd)
rbs_map = RbsMap.from_gemspec(gemspec, workspace.rbs_collection_path, workspace.rbs_collection_config_path)
if options[:rebuild] || !PinCache.has_rbs_collection?(gemspec, rbs_map.cache_key)
# cache pins even if result is zero, so we don't retry building pins
pins = rbs_map.pins || []
PinCache.serialize_rbs_collection_gem(gemspec, rbs_map.cache_key, pins)
end
gems(gem + (version ? "=#{version}" : ''))
# '
end

@@ -135,5 +124,7 @@

raise ArgumentError, 'No gems specified.' if gems.empty?
workspace = Solargraph::Workspace.new(Dir.pwd)
gems.each do |gem|
if gem == 'core'
PinCache.uncache_core
PinCache.uncache_core(out: $stdout)
next

@@ -143,12 +134,38 @@ end

if gem == 'stdlib'
PinCache.uncache_stdlib
PinCache.uncache_stdlib(out: $stdout)
next
end
spec = Gem::Specification.find_by_name(gem)
PinCache.uncache_gem(spec, out: $stdout)
spec = workspace.find_gem(gem)
raise Thor::InvocationError, "Gem '#{gem}' not found" if spec.nil?
# @sg-ignore flow sensitive typing needs to handle 'raise if'
workspace.uncache_gem(spec, out: $stdout)
end
end
desc 'gems [GEM[=VERSION]]', 'Cache documentation for installed gems'
desc 'gems [GEM[=VERSION]...] [STDLIB...] [core]', 'Cache documentation for
installed libraries'
long_desc %( This command will cache the
generated type documentation for the specified libraries. While
Solargraph will generate this on the fly when needed, it takes
time. This command will generate it in advance, which can be
useful for CI scenarios.
With no arguments, it will cache all libraries in the current
workspace. If a gem or standard library name is specified, it
will cache that library's type documentation.
An equals sign after a gem will allow a specific gem version
to be cached.
The 'core' argument can be used to cache the type
documentation for the core Ruby libraries.
If the library is already cached, it will be rebuilt if the
--rebuild option is set.
Cached documentation is stored in #{PinCache.base_dir}, which
can be stored between CI runs.
)
option :rebuild, type: :boolean, desc: 'Rebuild existing documentation', default: false

@@ -158,14 +175,30 @@ # @param names [Array<String>]

def gems *names
api_map = ApiMap.load('.')
# print time with ms
workspace = Solargraph::Workspace.new('.')
if names.empty?
Gem::Specification.to_a.each { |spec| do_cache spec, api_map }
STDERR.puts "Documentation cached for all #{Gem::Specification.count} gems."
workspace.cache_all_for_workspace!($stdout, rebuild: options[:rebuild])
else
$stderr.puts("Caching these gems: #{names}")
names.each do |name|
spec = Gem::Specification.find_by_name(*name.split('='))
do_cache spec, api_map
if name == 'core'
PinCache.cache_core(out: $stdout) if !PinCache.core? || options[:rebuild]
next
end
gemspec = workspace.find_gem(*name.split('='))
if gemspec.nil?
warn "Gem '#{name}' not found"
else
workspace.cache_gem(gemspec, rebuild: options[:rebuild], out: $stdout)
end
rescue Gem::MissingSpecError
warn "Gem '#{name}' not found"
rescue Gem::Requirement::BadRequirementError => e
warn "Gem '#{name}' failed while loading"
warn e.message
# @sg-ignore Need to add nil check here
warn e.backtrace.join("\n")
end
STDERR.puts "Documentation cached for #{names.count} gems."
$stderr.puts "Documentation cached for #{names.count} gems."
end

@@ -195,3 +228,6 @@ end

rules = workspace.rules(level)
api_map = Solargraph::ApiMap.load_with_cache(directory, $stdout)
api_map =
Solargraph::ApiMap.load_with_cache(directory, $stdout,
loose_unions:
!rules.require_all_unique_types_support_call?)
probcount = 0

@@ -204,6 +240,5 @@ if files.empty?

filecount = 0
time = Benchmark.measure {
files.each do |file|
checker = TypeChecker.new(file, api_map: api_map, level: options[:level].to_sym, workspace: workspace)
checker = TypeChecker.new(file, api_map: api_map, rules: rules, level: options[:level].to_sym, workspace: workspace)
problems = checker.problems

@@ -216,3 +251,2 @@ next if problems.empty?

end
# "
}

@@ -241,2 +275,3 @@ puts "Typecheck finished in #{time.real} seconds."

api_map = Solargraph::ApiMap.load_with_cache(directory, $stdout)
# @sg-ignore flow sensitive typing should be able to handle redefinition
api_map.pins.each do |pin|

@@ -248,4 +283,8 @@ begin

rescue StandardError => e
# @todo to add nil check here
# @todo should warn on nil dereference below
STDERR.puts "Error testing #{pin_description(pin)} #{pin.location ? "at #{pin.location.filename}:#{pin.location.range.start.line + 1}" : ''}"
STDERR.puts "[#{e.class}]: #{e.message}"
# @todo Need to add nil check here
# @todo flow sensitive typing should be able to handle redefinition
STDERR.puts e.backtrace.join("\n")

@@ -256,2 +295,3 @@ exit 1

}
# @sg-ignore Need to add nil check here
puts "Scanned #{directory} (#{api_map.pins.length} pins) in #{time.real} seconds."

@@ -304,2 +344,3 @@ end

if options[:references]
# @sg-ignore Need to add nil check here
superclass_tag = api_map.qualify_superclass(pin.return_type.tag)

@@ -335,2 +376,3 @@ superclass_pin = api_map.get_path_pins(superclass_tag).first if superclass_tag

if pin.closure
# @sg-ignore Need to add nil check here
"#{pin.closure.path} | #{pin.name}"

@@ -343,2 +385,3 @@ else

end
# @sg-ignore Need to add nil check here
desc += " (#{pin.location.filename} #{pin.location.range.start.line})" if pin.location

@@ -348,13 +391,4 @@ desc

# @param gemspec [Gem::Specification]
# @param api_map [ApiMap]
# @param type [ComplexType, ComplexType::UniqueType]
# @return [void]
def do_cache gemspec, api_map
# @todo if the rebuild: option is passed as a positional arg,
# typecheck doesn't complain on the below line
api_map.cache_gem(gemspec, rebuild: options.rebuild, out: $stdout)
end
# @param type [ComplexType]
# @return [void]
def print_type(type)

@@ -361,0 +395,0 @@ if options[:rbs]

@@ -37,2 +37,4 @@ # frozen_string_literal: true

@source = source
# @type [Array<Pin::Base>, nil]
@convention_pins = nil

@@ -67,3 +69,3 @@ conventions_environ.merge Convention.for_local(self) unless filename.nil?

# @return [String]
# @return [String, nil]
def filename

@@ -89,2 +91,3 @@ source.filename

# all pins except Solargraph::Pin::Reference::Reference
#
# @return [Array<Pin::Base>]

@@ -103,3 +106,3 @@ def document_symbols

# @param position [Position]
# @param position [Position, Array(Integer, Integer)]
# @return [Source::Cursor]

@@ -132,3 +135,3 @@ def cursor_at position

# @param character [Integer]
# @return [Pin::Namespace,Pin::Method,Pin::Block]
# @return [Pin::Closure]
def locate_closure_pin line, character

@@ -151,3 +154,3 @@ _locate_pin line, character, Pin::Closure

return [] if location.filename != filename
closure = locate_named_path_pin(location.range.start.line, location.range.start.character)
closure = locate_closure_pin(location.range.start.line, location.range.start.character)
locals.select { |pin| pin.visible_at?(closure, location) }

@@ -187,2 +190,3 @@ end

def pin_class_hash
# @todo Need to support generic resolution in classify and transform_values
@pin_class_hash ||= pins.to_set.classify(&:class).transform_values(&:to_a)

@@ -201,6 +205,8 @@ end

# @generic T
# @param line [Integer]
# @param character [Integer]
# @param klasses [Array<Class>]
# @return [Pin::Base, nil]
# @param klasses [Array<Class<generic<T>>>]
# @return [generic<T>, nil]
# @sg-ignore Need better generic inference here
def _locate_pin line, character, *klasses

@@ -213,3 +219,5 @@ position = Position.new(line, character)

next if pin.is_a?(Pin::Method) && pin.attribute?
# @sg-ignore Need to add nil check here
found = pin if (klasses.empty? || klasses.any? { |kls| pin.is_a?(kls) } ) && pin.location.range.contain?(position)
# @sg-ignore Need to add nil check here
break if pin.location.range.start.line > line

@@ -216,0 +224,0 @@ end

@@ -15,2 +15,3 @@ # frozen_string_literal: true

closure_pin = closure
# @sg-ignore Need to add nil check here
closure_pin.rebind(api_map) if closure_pin.is_a?(Pin::Block) && !Solargraph::Range.from_node(closure_pin.receiver).contain?(cursor.range.start)

@@ -24,2 +25,3 @@ end

result.concat file_global_methods
# @sg-ignore Need to add nil check here
result.concat((source_map.pins + source_map.locals).select{ |p| p.name == cursor.word && p.location.range.contain?(cursor.position) }) if result.empty?

@@ -83,3 +85,3 @@ result

def translate phrase
chain = Parser.chain(Parser.parse(phrase))
chain = Parser.chain(Parser.parse(phrase, cursor.filename, cursor.position.line))
chain.define(api_map, closure, locals)

@@ -98,2 +100,3 @@ end

def source_map
# @sg-ignore Need to add nil check here
@source_map ||= api_map.source_map(cursor.filename)

@@ -157,12 +160,19 @@ end

result = []
# @sg-ignore Need to add nil check here
match = source_map.code[0..cursor.offset-1].match(/[\[<, ]([a-z0-9_:]*)\z/i)
if match
# @sg-ignore Need to add nil check here
full = match[1]
# @sg-ignore Need to add nil check here
if full.include?('::')
# @sg-ignore Need to add nil check here
if full.end_with?('::')
# @sg-ignore Need to add nil check here
result.concat api_map.get_constants(full[0..-3], *gates)
else
# @sg-ignore Need to add nil check here
result.concat api_map.get_constants(full.split('::')[0..-2].join('::'), *gates)
end
else
# @sg-ignore Need to add nil check here
result.concat api_map.get_constants('', full.end_with?('::') ? '' : context_pin.full_context.namespace, *gates) #.select { |pin| pin.name.start_with?(full) }

@@ -184,2 +194,3 @@ end

if full.include?('::') && cursor.chain.links.length == 1
# @sg-ignore Need to add nil check here
ComplexType.try_parse(full.split('::')[0..-2].join('::'))

@@ -208,3 +219,3 @@ elsif cursor.chain.links.length > 1

elsif cursor.word.start_with?('@')
return package_completions(api_map.get_instance_variable_pins(closure.binder.namespace, closure.binder.scope))
return package_completions(api_map.get_instance_variable_pins(closure.full_context.namespace, closure.context.scope))
elsif cursor.word.start_with?('$')

@@ -211,0 +222,0 @@ return package_completions(api_map.get_global_variable_pins)

@@ -11,3 +11,5 @@ # frozen_string_literal: true

# @sg-ignore Translate to something flow sensitive typing understands
# @return [Array<Solargraph::Pin::Base>]
# @sg-ignore https://github.com/castwide/solargraph/pull/1100
def pins

@@ -18,3 +20,4 @@ generate

# @return [Array<Solargraph::LocalVariable>]
# @sg-ignore Translate to something flow sensitive typing understands
# @return [Array<Solargraph::Pin::LocalVariable>]
def locals

@@ -21,0 +24,0 @@ generate

@@ -27,2 +27,3 @@ # frozen_string_literal: true

@pins, @locals = Parser.map(source)
# @param p [Solargraph::Pin::Base]
@pins.each { |p| p.source = :code }

@@ -52,2 +53,3 @@ @locals.each { |l| l.source = :code }

def map source
# @sg-ignore Need to add nil check here
return new.unmap(source.filename, source.code) unless source.parsed?

@@ -67,2 +69,3 @@ new.map source

def closure_at(position)
# @sg-ignore Need to add nil check here
pins.select{|pin| pin.is_a?(Pin::Closure) and pin.location.range.contain?(position)}.last

@@ -96,2 +99,3 @@ end

return start unless start < comment.lines.length
# @sg-ignore Need to add nil check here
num = comment.lines[start..-1].find_index do |line|

@@ -102,2 +106,3 @@ # Legacy method directives might be `@method` instead of `@!method`

end
# @sg-ignore Need to add nil check here
num.to_i + start

@@ -111,2 +116,3 @@ end

def process_directive source_position, comment_position, directive
# @sg-ignore Need to add nil check here
docstring = Solargraph::Source.parse_docstring(directive.tag.text).to_docstring

@@ -117,2 +123,4 @@ location = Location.new(@filename, Range.new(comment_position, comment_position))

namespace = closure_at(source_position) || @pins.first
# @todo Missed nil violation
# @todo Need to add nil check here
if namespace.location.range.start.line < comment_position.line

@@ -124,2 +132,3 @@ namespace = closure_at(comment_position)

region = Parser::Region.new(source: src, closure: namespace)
# @type [Array<Pin::Method>]
method_gen_pins = Parser.process_node(src.node, region).first.select { |pin| pin.is_a?(Pin::Method) }

@@ -176,2 +185,3 @@ gen_pin = method_gen_pins.last

kind = directive.tag.text&.to_sym
# @sg-ignore Need to look at Tuple#include? handling
return unless [:private, :protected, :public].include?(kind)

@@ -181,2 +191,4 @@

closure = closure_at(source_position) || @pins.first
# @todo Missed nil violation
# @todo Need to add nil check here
if closure.location.range.start.line < comment_position.line

@@ -199,2 +211,3 @@ closure = closure_at(comment_position)

ns = closure_at(source_position)
# @sg-ignore Need to add nil check here
src = Solargraph::Source.load_string(directive.tag.text, @source.filename)

@@ -209,3 +222,7 @@ region = Parser::Region.new(source: src, closure: ns)

end
Parser.process_node(src.node, region, @pins)
locals = []
ivars = []
Parser.process_node(src.node, region, @pins, locals, ivars)
@pins.concat ivars
# @sg-ignore Need to add nil check here
@pins[index..-1].each do |p|

@@ -221,4 +238,6 @@ # @todo Smelly instance variable access

namespace = closure_at(source_position) || Pin::ROOT_PIN
# @sg-ignore flow sensitive typing should be able to handle redefinition
namespace.domains.concat directive.tag.types unless directive.tag.types.nil?
when 'override'
# @sg-ignore Need to add nil check here
pins.push Pin::Reference::Override.new(location, directive.tag.name, docstring.tags,

@@ -233,3 +252,5 @@ source: :source_map)

# @param line2 [Integer]
# @sg-ignore Need to add nil check here
def no_empty_lines?(line1, line2)
# @sg-ignore Need to add nil check here
@code.lines[line1..line2].none? { |line| line.strip.empty? }

@@ -252,2 +273,3 @@ end

cur = p.index(/[^ ]/)
# @sg-ignore Need to add nil check here
num = cur if cur < num

@@ -266,2 +288,3 @@ end

src_pos = line ? Position.new(line, code_lines[line].to_s.chomp.index(/[^\s]/) || 0) : Position.new(code_lines.length, 0)
# @sg-ignore Need to add nil check here
com_pos = Position.new(line + 1 - comments.lines.length, 0)

@@ -268,0 +291,0 @@ process_comment(src_pos, com_pos, comments)

@@ -63,2 +63,4 @@ # frozen_string_literal: true

# @param c2 [Integer]
#
# @sg-ignore Need to add nil check here
# @return [String]

@@ -85,3 +87,3 @@ def from_to l1, c1, l2, c2

# @param column [Integer]
# @return [Array<AST::Node>]
# @return [Array<Parser::AST::Node>]
def tree_at(line, column)

@@ -136,5 +138,9 @@ position = Position.new(line, column)

range = Range.from_node(node)
# @sg-ignore Need to add nil check here
next if range.ending.line < position.line
# @sg-ignore Need to add nil check here
break if range.ending.line > position.line
# @sg-ignore Need to add nil check here
return true if node.type == :str && range.include?(position) && range.start != position
# @sg-ignore Need to add nil check here
return true if [:STR, :str].include?(node.type) && range.include?(position) && range.start != position

@@ -145,8 +151,13 @@ if node.type == :dstr

inner_range = Range.from_node(inner)
# @sg-ignore Need to add nil check here
next unless range.include?(inner_range.ending)
return true if inner.type == :str
# @sg-ignore Need to add nil check here
inner_code = at(Solargraph::Range.new(inner_range.start, position))
# @sg-ignore Need to add nil check here
return true if (inner.type == :dstr && inner_range.ending.character <= position.character) && !inner_code.end_with?('}') ||
# @sg-ignore Need to add nil check here
(inner.type != :dstr && inner_range.ending.line == position.line && position.character <= inner_range.ending.character && inner_code.end_with?('}'))
end
# @sg-ignore Need to add nil check here
break if range.ending.line > position.line

@@ -188,3 +199,5 @@ end

rng = Range.from_node(node)
# @sg-ignore Need to add nil check here
b = Position.line_char_to_offset(code, rng.start.line, rng.start.column)
# @sg-ignore Need to add nil check here
e = Position.line_char_to_offset(code, rng.ending.line, rng.ending.column)

@@ -195,7 +208,10 @@ frag = code[b..e-1].to_s

# @param node [Parser::AST::Node]
# @param node [AST::Node]
#
# @return [String, nil]
def comments_for node
rng = Range.from_node(node)
# @sg-ignore Need to add nil check here
stringified_comments[rng.start.line] ||= begin
# @sg-ignore Need to add nil check here
buff = associated_comments[rng.start.line]

@@ -228,2 +244,3 @@ buff ? stringify_comment_array(buff) : nil

@folding_ranges ||= begin
# @type [Array<Range>]
result = []

@@ -242,3 +259,3 @@ inner_folding_ranges node, result

#
# @return [Hash{Integer => String}]
# @return [Hash{Integer => String, nil}]
def associated_comments

@@ -276,3 +293,3 @@ @associated_comments ||= begin

# @param top [Parser::AST::Node]
# @param top [Parser::AST::Node, nil]
# @param result [Array<Range>]

@@ -283,8 +300,13 @@ # @param parent [Symbol, nil]

return unless Parser.is_ast_node?(top)
# @sg-ignore Translate to something flow sensitive typing understands
if FOLDING_NODE_TYPES.include?(top.type)
# @sg-ignore Translate to something flow sensitive typing understands
range = Range.from_node(top)
# @sg-ignore Need to add nil check here
if result.empty? || range.start.line > result.last.start.line
# @sg-ignore Need to add nil check here
result.push range unless range.ending.line - range.start.line < 2
end
end
# @sg-ignore Translate to something flow sensitive typing understands
top.children.each do |child|

@@ -311,2 +333,3 @@ inner_folding_ranges(child, result, top.type)

here = p.index(/[^ \t]/)
# @sg-ignore flow sensitive typing should be able to handle redefinition
skip = here if skip.nil? || here < skip

@@ -322,3 +345,3 @@ ctxt.concat p[skip..-1]

#
# @return [Hash{Integer => Array<String>, nil}]
# @return [Hash{Integer => String}]
def stringified_comments

@@ -363,5 +386,7 @@ @stringified_comments ||= {}

if Parser.is_ast_node?(n)
# @sg-ignore Translate to something flow sensitive typing understands
if n.type == :str || n.type == :dstr || n.type == :STR || n.type == :DSTR
result.push n
else
# @sg-ignore Translate to something flow sensitive typing understands
n.children.each{ |c| result.concat string_nodes_in(c) }

@@ -380,2 +405,3 @@ end

here = Range.from_node(node)
# @sg-ignore Need to add nil check here
if here.contain?(position)

@@ -413,3 +439,3 @@ stack.unshift node

begin
@node, @comments = Solargraph::Parser.parse_with_comments(@code, filename)
@node, @comments = Solargraph::Parser.parse_with_comments(@code, filename, 0)
@parsed = true

@@ -430,3 +456,3 @@ @repaired = @code

begin
@node, @comments = Solargraph::Parser.parse_with_comments(@repaired, filename)
@node, @comments = Solargraph::Parser.parse_with_comments(@repaired, filename, 0)
@parsed = true

@@ -433,0 +459,0 @@ rescue Parser::SyntaxError, EncodingError => e

@@ -74,2 +74,3 @@ # frozen_string_literal: true

def base
# @sg-ignore Need to add nil check here
@base ||= Chain.new(links[0..-2])

@@ -82,7 +83,7 @@ end

#
# @param name_pin [Pin::Base] A pin
# representing the place in which expression is evaluated (e.g.,
# a Method pin, or a Module or Class pin if not run within a
# method - both in terms of the closure around the chain, as well
# as the self type used for any method calls in head position.
# @param name_pin [Pin::Base] A pin representing the closure in
# which expression is evaluated (e.g., a Method pin, or a
# Module or Class pin if not run within a method - both in
# terms of the closure around the chain, as well as the self
# type used for any method calls in head position.
#

@@ -92,5 +93,5 @@ # Requirements for name_pin:

# * name_pin.context: This should be a type representing the
# namespace where we can look up non-local variables and
# method names. If it is a Class<X>, we will look up
# :class scoped methods/variables.
# namespace where we can look up non-local variables. If
# it is a Class<X>, we will look up :class scoped
# instance variables.
#

@@ -101,4 +102,4 @@ # * name_pin.binder: Used for method call lookups only

# same as name_pin.context above. For method calls later
# in the chain (e.g., 'b' in a.b.c), it should represent
# 'a'.
# in the chain, it changes. (e.g., for 'b' in a.b.c, it
# should represent the type of 'a').
#

@@ -120,2 +121,3 @@ # @param locals [::Array<Pin::LocalVariable>] Any local

working_pin = name_pin
# @sg-ignore Need to add nil check here
links[0..-2].each do |link|

@@ -146,3 +148,4 @@ pins = link.resolve(api_map, working_pin, locals)

def infer api_map, name_pin, locals
cache_key = [node, node&.location, links, name_pin&.return_type, locals]
# includes binder as it is mutable in Pin::Block
cache_key = [node, node&.location, links, name_pin&.return_type, name_pin&.binder, locals]
if @@inference_invalidation_key == api_map.hash

@@ -163,3 +166,3 @@ cached = @@inference_cache[cache_key]

# @param locals [::Array<Pin::LocalVariable>]
# @return [ComplexType]
# @return [ComplexType, ComplexType::UniqueType]
def infer_uncached api_map, name_pin, locals

@@ -219,8 +222,8 @@ pins = define(api_map, name_pin, locals)

# @param pins [::Array<Pin::Base>]
# @param context [Pin::Base]
# @param name_pin [Pin::Base]
# @param api_map [ApiMap]
# @param locals [::Enumerable<Pin::LocalVariable>]
# @return [ComplexType]
def infer_from_definitions pins, context, api_map, locals
# @type [::Array<ComplexType>]
# @return [ComplexType, ComplexType::UniqueType]
def infer_from_definitions pins, name_pin, api_map, locals
# @type [::Array<ComplexType, ComplexType::UniqueType>]
types = []

@@ -243,3 +246,4 @@ unresolved_pins = []

# that accepts only [Pin::Namespace] as an argument
type = type.resolve_generics(pin.closure, context.binder)
# @sg-ignore Need to add nil check here
type = type.resolve_generics(pin.closure, name_pin.binder)
end

@@ -283,12 +287,11 @@ types << type

end
if context.nil? || context.return_type.undefined?
if name_pin.nil? || name_pin.context.undefined?
# up to downstream to resolve self type
return type
end
type.self_to_type(context.return_type)
type.self_to_type(name_pin.context)
end
# @param type [ComplexType]
# @return [ComplexType]
# @param type [ComplexType, ComplexType::UniqueType]
# @return [ComplexType, ComplexType::UniqueType]
def maybe_nil type

@@ -295,0 +298,0 @@ return type if type.undefined? || type.void? || type.nullable?

@@ -0,0 +0,0 @@ module Solargraph

@@ -40,2 +40,3 @@ # frozen_string_literal: true

protected def equality_fields
# @sg-ignore literal arrays in this module turn into ::Solargraph::Source::Chain::Array
super + [arguments, block]

@@ -54,9 +55,15 @@ end

return yield_pins(api_map, name_pin) if word == 'yield'
found = if head?
api_map.visible_pins(locals, word, name_pin, location)
else
[]
end
return inferred_pins(found, api_map, name_pin, locals) unless found.empty?
pins = name_pin.binder.each_unique_type.flat_map do |context|
found = api_map.var_at_location(locals, word, name_pin, location) if head?
return inferred_pins([found], api_map, name_pin, locals) unless found.nil?
binder = name_pin.binder
# this is a q_call - i.e., foo&.bar - assume result of call
# will be nil or result as if binder were not nil -
# chain.rb#maybe_nil will add the nil type later, we just
# need to worry about the not-nil case
# @sg-ignore Need to handle duck-typed method calls on union types
binder = binder.without_nil if nullable?
# @sg-ignore Need to handle duck-typed method calls on union types
pin_groups = binder.each_unique_type.map do |context|
ns_tag = context.namespace == '' ? '' : context.namespace_type.tag

@@ -66,2 +73,7 @@ stack = api_map.get_method_stack(ns_tag, word, scope: context.scope)

end
# @sg-ignore literal arrays in this module turn into ::Solargraph::Source::Chain::Array
if !api_map.loose_unions && pin_groups.any? { |pins| pins.empty? }
pin_groups = []
end
pins = pin_groups.flatten.uniq(&:path)
return [] if pins.empty?

@@ -73,3 +85,3 @@ inferred_pins(pins, api_map, name_pin, locals)

# @param pins [::Enumerable<Pin::Method>]
# @param pins [::Enumerable<Pin::Base>]
# @param api_map [ApiMap]

@@ -91,5 +103,9 @@ # @param name_pin [Pin::Base]

with_block, without_block = overloads.partition(&:block?)
# @sg-ignore flow sensitive typing should handle is_a? and next
# @type Array<Pin::Signature>
sorted_overloads = with_block + without_block
# @type [Pin::Signature, nil]
new_signature_pin = nil
# @sg-ignore flow sensitive typing should handle is_a? and next
# @param ol [Pin::Signature]
sorted_overloads.each do |ol|

@@ -107,2 +123,3 @@ next unless ol.arity_matches?(arguments, with_block?)

arg_name_pin = Pin::ProxyType.anonymous(name_pin.context,
closure: name_pin.closure,
gates: name_pin.gates,

@@ -119,2 +136,3 @@ source: :chain)

block_atypes = ol.block.parameters.map(&:return_type)
# @todo Need to add nil check here
if block.links.map(&:class) == [BlockSymbol]

@@ -150,2 +168,3 @@ # like the bar in foo(&:bar)

# the docs were written - from the method pin.
# @todo Need to add nil check here
type = with_params(new_return_type.self_to_type(self_type), self_type).qualify(api_map, *p.gates) if new_return_type.defined?

@@ -160,5 +179,7 @@ type ||= ComplexType::UNDEFINED

result = process_macro(p, api_map, name_pin.context, locals)
# @sg-ignore flow sensitive typing should be able to handle redefinition
next result unless result.return_type.undefined?
elsif !p.directives.empty?
result = process_directive(p, api_map, name_pin.context, locals)
# @sg-ignore flow sensitive typing should be able to handle redefinition
next result unless result.return_type.undefined?

@@ -174,4 +195,7 @@ end

else
# @sg-ignore Need to add nil check here
next pin if pin.return_type.undefined?
# @sg-ignore Need to add nil check here
selfy = pin.return_type.self_to_type(name_pin.binder)
# @sg-ignore Need to add nil check here
selfy == pin.return_type ? pin : pin.proxy(selfy)

@@ -184,3 +208,3 @@ end

# @param api_map [ApiMap]
# @param context [ComplexType]
# @param context [ComplexType, ComplexType::UniqueType]
# @param locals [::Array<Solargraph::Pin::LocalVariable, Solargraph::Pin::Parameter>]

@@ -204,3 +228,3 @@ # @return [Pin::Base]

# @param api_map [ApiMap]
# @param context [ComplexType]
# @param context [ComplexType, ComplexType::UniqueType]
# @param locals [::Array<Solargraph::Pin::LocalVariable, Solargraph::Pin::Parameter>]

@@ -221,3 +245,3 @@ # @return [Pin::ProxyType]

# @param api_map [ApiMap]
# @param context [ComplexType]
# @param context [ComplexType, ComplexType::UniqueType]
# @param locals [::Array<Pin::LocalVariable, Pin::Parameter>]

@@ -228,2 +252,3 @@ # @return [Pin::ProxyType]

txt = macro.tag.text.clone
# @sg-ignore Need to add nil check here
if txt.empty? && macro.tag.name

@@ -235,5 +260,7 @@ named = api_map.named_macro(macro.tag.name)

vals.each do |v|
# @sg-ignore Need to add nil check here
txt.gsub!(/\$#{i}/, v.context.namespace)
i += 1
end
# @sg-ignore Need to add nil check here
docstring = Solargraph::Source.parse_docstring(txt).to_docstring

@@ -264,2 +291,3 @@ tag = docstring.tag(:return)

until method_pin.is_a?(Pin::Method)
# @sg-ignore Need to support this in flow sensitive typing
method_pin = method_pin.closure

@@ -290,2 +318,3 @@ return if method_pin.nil?

method_pin.signatures.map(&:block).compact.map do |signature_pin|
# @sg-ignore Need to add nil check here
return_type = signature_pin.return_type.qualify(api_map, *name_pin.gates)

@@ -297,3 +326,3 @@ signature_pin.proxy(return_type)

# @param type [ComplexType]
# @param context [ComplexType]
# @param context [ComplexType, ComplexType::UniqueType]
# @return [ComplexType]

@@ -312,3 +341,3 @@ def with_params type, context

# @param api_map [ApiMap]
# @param context [ComplexType]
# @param context [ComplexType, ComplexType::UniqueType]
# @param block_parameter_types [::Array<ComplexType>]

@@ -320,2 +349,3 @@ # @param locals [::Array<Pin::LocalVariable>]

# to the first yield parameter with no arguments
# @sg-ignore Need to add nil check here
block_symbol_name = block.links.first.word

@@ -328,2 +358,3 @@ block_symbol_call_path = "#{block_parameter_types.first}##{block_symbol_name}"

# based on the generic type
# @sg-ignore Need to add nil check here
return_type ||= api_map.get_path_pins("#{context.subtypes.first}##{block.links.first.word}").first&.return_type

@@ -336,5 +367,7 @@ return_type || ComplexType::UNDEFINED

def find_block_pin(api_map)
# @sg-ignore Need to add nil check here
node_location = Solargraph::Location.from_node(block.node)
return if node_location.nil?
return if node_location.nil?
block_pins = api_map.get_block_pins
# @sg-ignore Need to add nil check here
block_pins.find { |pin| pin.location.contain?(node_location) }

@@ -351,6 +384,8 @@ end

block_context_pin = name_pin
block_pin = find_block_pin(api_map)
block_context_pin = block_pin.closure if block_pin
block.infer(api_map, block_context_pin, locals)
# We use the block pin as the closure, as the parameters
# here will only be defined inside the block itself and we
# need to be able to see them
# @sg-ignore Need to add nil check here
block.infer(api_map, block_pin, locals)
end

@@ -357,0 +392,0 @@ end

@@ -0,0 +0,0 @@ # frozen_string_literal: true

@@ -20,3 +20,5 @@ # frozen_string_literal: true

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)

@@ -23,0 +25,0 @@ end

@@ -0,0 +0,0 @@ # frozen_string_literal: true

@@ -17,2 +17,3 @@ # frozen_string_literal: true

protected def equality_fields
# @sg-ignore literal arrays in this module turn into ::Solargraph::Source::Chain::Array
super + [@splatted]

@@ -19,0 +20,0 @@ end

@@ -18,2 +18,3 @@ # frozen_string_literal: true

protected def equality_fields
# @sg-ignore literal arrays in this module turn into ::Solargraph::Source::Chain::Array
super + [@links]

@@ -20,0 +21,0 @@ end

@@ -7,5 +7,26 @@ # frozen_string_literal: true

class InstanceVariable < Link
# @param word [String]
# @param node [Parser::AST::Node, nil] The node representing the variable
# @param location [Location, nil] The location of the variable reference in the source
def initialize word, node, location
super(word)
@node = node
@location = location
end
# @sg-ignore Declared return type
# ::Array<::Solargraph::Pin::Base> does not match inferred
# type ::Array<::Solargraph::Pin::BaseVariable, ::NilClass>
# for Solargraph::Source::Chain::InstanceVariable#resolve
def resolve api_map, name_pin, locals
api_map.get_instance_variable_pins(name_pin.binder.namespace, name_pin.binder.scope).select{|p| p.name == word}
ivars = api_map.get_instance_variable_pins(name_pin.context.namespace, name_pin.context.scope).select{|p| p.name == word}
out = api_map.var_at_location(ivars, word, name_pin, location)
[out].compact
end
private
# @todo: Missed nil violation
# @return [Location]
attr_reader :location
end

@@ -12,0 +33,0 @@ end

@@ -0,0 +0,0 @@ # frozen_string_literal: true

@@ -19,7 +19,11 @@ # frozen_string_literal: true

if node.is_a?(::Parser::AST::Node)
# @sg-ignore flow sensitive typing needs to narrow down type with an if is_a? check
if node.type == :true
@value = true
# @sg-ignore flow sensitive typing needs to narrow down type with an if is_a? check
elsif node.type == :false
@value = false
# @sg-ignore flow sensitive typing needs to narrow down type with an if is_a? check
elsif [:int, :sym].include?(node.type)
# @sg-ignore flow sensitive typing needs to narrow down type with an if is_a? check
@value = node.children.first

@@ -35,2 +39,3 @@ end

protected def equality_fields
# @sg-ignore literal arrays in this module turn into ::Solargraph::Source::Chain::Array
super + [@value, @type, @literal_type, @complex_type]

@@ -37,0 +42,0 @@ end

@@ -11,2 +11,4 @@ # frozen_string_literal: true

attr_reader :links
# @param links [::Array<Chain>]

@@ -19,3 +21,9 @@ def initialize links

types = @links.map { |link| link.infer(api_map, name_pin, locals) }
[Solargraph::Pin::ProxyType.anonymous(Solargraph::ComplexType.new(types.uniq), source: :chain)]
combined_type = Solargraph::ComplexType.new(types)
unless types.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

@@ -22,0 +30,0 @@ end

@@ -0,0 +0,0 @@ module Solargraph

@@ -0,0 +0,0 @@ # frozen_string_literal: true

@@ -0,0 +0,0 @@ # frozen_string_literal: true

@@ -10,3 +10,3 @@ # frozen_string_literal: true

# @return [Range]
# @return [Range, nil]
attr_reader :range

@@ -17,3 +17,3 @@

# @param range [Range] The starting and ending positions of the change.
# @param range [Range, nil] The starting and ending positions of the change.
# If nil, the original text will be overwritten.

@@ -36,5 +36,7 @@ # @param new_text [String] The text to be changed.

next unless new_text == dupable
# @sg-ignore flow sensitive typing needs to handle attrs
offset = Position.to_offset(text, range.start)
if text[offset - 1] == dupable
p = Position.from_offset(text, offset - 1)
# @sg-ignore flow sensitive typing needs to handle attrs
r = Change.new(Range.new(p, range.start), ' ')

@@ -64,5 +66,8 @@ text = r.write(text)

result = commit text, fixed
# @sg-ignore flow sensitive typing needs to handle attrs
off = Position.to_offset(text, range.start)
# @sg-ignore Need to add nil check here
match = result[0, off].match(/[.:]+\z/)
if match
# @sg-ignore flow sensitive typing should be able to handle redefinition
result = result[0, off].sub(/#{match[0]}\z/, ' ' * match[0].length) + result[off..-1]

@@ -80,3 +85,5 @@ end

def commit text, insert
# @sg-ignore Need to add nil check here
start_offset = Position.to_offset(text, range.start)
# @sg-ignore Need to add nil check here
end_offset = Position.to_offset(text, range.ending)

@@ -83,0 +90,0 @@ (start_offset == 0 ? '' : text[0..start_offset-1].to_s) + normalize(insert) + text[end_offset..-1].to_s

@@ -22,3 +22,3 @@ # frozen_string_literal: true

# @return [String]
# @return [String, nil]
def filename

@@ -39,2 +39,3 @@ source.filename

#
# @sg-ignore Need to add nil check here
# @return [String]

@@ -46,2 +47,3 @@ def start_of_word

# Including the preceding colon if the word appears to be a symbol
# @sg-ignore Need to add nil check here
result = ":#{result}" if source.code[0..offset-result.length-1].end_with?(':') and !source.code[0..offset-result.length-1].end_with?('::')

@@ -56,2 +58,3 @@ result

# @return [String]
# @sg-ignore Need to add nil check here
def end_of_word

@@ -117,2 +120,3 @@ @end_of_word ||= begin

node = recipient_node
# @sg-ignore Need to add nil check here
node ? Cursor.new(source, Range.from_node(node).ending) : nil

@@ -132,4 +136,6 @@ end

if start_of_word.empty?
# @sg-ignore Need to add nil check here
match = source.code[0, offset].match(/\s*(\.|:+)\s*$/)
if match
# @sg-ignore Need to add nil check here
Position.from_offset(source.code, offset - match[0].length)

@@ -136,0 +142,0 @@ else

@@ -36,2 +36,3 @@ # frozen_string_literal: true

return Chain.new([Chain::Literal.new('Integer', Integer(phrase[0..-2])), Chain::UNDEFINED_CALL]) if phrase =~ /^[0-9]+\.$/
# @sg-ignore Need to add nil check here
return Chain.new([Chain::Literal.new('Symbol', phrase[1..].to_sym)]) if phrase.start_with?(':') && !phrase.start_with?('::')

@@ -41,3 +42,5 @@ return SourceChainer.chain(source, Position.new(position.line, position.character + 1)) if end_of_phrase.strip == '::' && source.code[Position.to_offset(source.code, position)].to_s.match?(/[a-z]/i)

return Chain.new([]) if phrase.end_with?('..')
# @type [::Parser::AST::Node, nil]
node = nil
# @type [::Parser::AST::Node, nil]
parent = nil

@@ -49,5 +52,6 @@ if !source.repaired? && source.parsed? && source.synchronized?

node, parent = source.tree_at(fixed_position.line, fixed_position.column)[0..2]
node = Parser.parse(fixed_phrase) if node.nil?
# provide filename and line so that we can look up local variables there later
node = Parser.parse(fixed_phrase, source.filename, fixed_position.line) if node.nil?
elsif source.repaired?
node = Parser.parse(fixed_phrase)
node = Parser.parse(fixed_phrase, source.filename, fixed_position.line)
else

@@ -57,3 +61,3 @@ node, parent = source.tree_at(fixed_position.line, fixed_position.column)[0..2] unless source.error_ranges.any?{|r| r.nil? || r.include?(fixed_position)}

node = nil unless source.synchronized? || !Parser.infer_literal_node_type(node).nil?
node = Parser.parse(fixed_phrase) if node.nil?
node = Parser.parse(fixed_phrase, source.filename, fixed_position.line) if node.nil?
end

@@ -86,2 +90,3 @@ rescue Parser::SyntaxError

# @sg-ignore Need to add nil check here
# @return [String]

@@ -92,2 +97,3 @@ def phrase

# @sg-ignore Need to add nil check here
# @return [String]

@@ -104,2 +110,3 @@ def fixed_phrase

# @return [String]
# @sg-ignore Need to add nil check here
def end_of_phrase

@@ -159,5 +166,8 @@ @end_of_phrase ||= begin

if brackets.zero? and parens.zero? and squares.zero? and in_whitespace
# @sg-ignore Need to add nil check here
unless char == '.' or @source.code[index+1..-1].strip.start_with?('.')
old = @source.code[index+1..-1]
# @sg-ignore Need to add nil check here
nxt = @source.code[index+1..-1].lstrip
# @sg-ignore Need to add nil check here
index += (@source.code[index+1..-1].length - @source.code[index+1..-1].lstrip.length)

@@ -164,0 +174,0 @@ break

@@ -32,2 +32,3 @@ # frozen_string_literal: true

# @param nullable [Boolean]
# @sg-ignore changes doesn't mutate @output, so this can never be nil
# @return [String]

@@ -41,2 +42,5 @@ def write text, nullable = false

changes.each do |ch|
# @sg-ignore Wrong argument type for
# Solargraph::Source::Change#write: text expected String,
# received String, nil
@output = ch.write(@output, can_nullify)

@@ -43,0 +47,0 @@ end

@@ -8,7 +8,6 @@ # frozen_string_literal: true

autoload :Problem, 'solargraph/type_checker/problem'
autoload :ParamDef, 'solargraph/type_checker/param_def'
autoload :Rules, 'solargraph/type_checker/rules'
autoload :Checks, 'solargraph/type_checker/checks'
include Checks
# @!parse
# include Solargraph::Parser::ParserGem::NodeMethods
include Parser::NodeMethods

@@ -30,4 +29,2 @@

# type checker rules modified by user config
# @param type_checker_rules [Hash{Symbol => Symbol}] Overrides for
# type checker rules - e.g., :report_undefined => :strong
# @param rules [Rules] Type checker rules object

@@ -41,3 +38,4 @@ def initialize filename,

# @todo Smarter directory resolution
@api_map = api_map || Solargraph::ApiMap.load(File.dirname(filename))
@api_map = api_map || Solargraph::ApiMap.load(File.dirname(filename),
loose_unions: !rules.require_all_unique_types_support_call?)
@rules = rules

@@ -55,5 +53,38 @@ # @type [Array<Range>]

def source
@source_map.source
source_map.source
end
# @param inferred [ComplexType, ComplexType::UniqueType]
# @param expected [ComplexType, ComplexType::UniqueType]
def return_type_conforms_to?(inferred, expected)
conforms_to?(inferred, expected, :return_type)
end
# @param inferred [ComplexType, ComplexType::UniqueType]
# @param expected [ComplexType, ComplexType::UniqueType]
def arg_conforms_to?(inferred, expected)
conforms_to?(inferred, expected, :method_call)
end
# @param inferred [ComplexType, ComplexType::UniqueType]
# @param expected [ComplexType, ComplexType::UniqueType]
def assignment_conforms_to?(inferred, expected)
conforms_to?(inferred, expected, :assignment)
end
# @param inferred [ComplexType, ComplexType::UniqueType]
# @param expected [ComplexType, ComplexType::UniqueType]
# @param scenario [Symbol]
def conforms_to?(inferred, expected, scenario)
rules_arr = []
rules_arr << :allow_empty_params unless rules.require_inferred_type_params?
rules_arr << :allow_any_match unless rules.require_all_unique_types_match_expected?
rules_arr << :allow_undefined unless rules.require_no_undefined_args?
rules_arr << :allow_unresolved_generic unless rules.require_generics_resolved?
rules_arr << :allow_unmatched_interface unless rules.require_interfaces_resolved?
rules_arr << :allow_reverse_match unless rules.require_downcasts?
inferred.conforms_to?(api_map, expected, scenario,
rules_arr)
end
# @return [Array<Problem>]

@@ -77,5 +108,7 @@ def problems

source = Solargraph::Source.load(filename)
api_map = Solargraph::ApiMap.new
rules = Rules.new(level, {})
api_map = Solargraph::ApiMap.new(loose_unions:
!rules.require_all_unique_types_support_call?)
api_map.map(source)
new(filename, api_map: api_map, level: level)
new(filename, api_map: api_map, level: level, rules: rules)
end

@@ -86,8 +119,12 @@

# @param level [Symbol]
# @param api_map [Solargraph::ApiMap, nil]
# @return [self]
def load_string code, filename = nil, level = :normal
def load_string code, filename = nil, level = :normal, api_map: nil
source = Solargraph::Source.load_string(code, filename)
api_map = Solargraph::ApiMap.new
rules = Rules.new(level, {})
api_map ||= Solargraph::ApiMap.new(loose_unions:
!rules.require_all_unique_types_support_call?)
# @sg-ignore flow sensitive typing needs better handling of ||= on lvars
api_map.map(source)
new(filename, api_map: api_map, level: level)
new(filename, api_map: api_map, level: level, rules: rules)
end

@@ -116,2 +153,3 @@ end

if declared.undefined?
# @sg-ignore Need to add nil check here
if pin.return_type.undefined? && rules.require_type_tags?

@@ -124,2 +162,3 @@ if pin.attribute?

end
# @sg-ignore Need to add nil check here
elsif pin.return_type.defined? && !resolved_constant?(pin)

@@ -138,3 +177,3 @@ result.push Problem.new(pin.location, "Unresolved return type #{pin.return_type} for #{pin.path}", pin: pin)

else
unless (rules.require_all_return_types_match_inferred? ? all_types_match?(api_map, inferred, declared) : any_types_match?(api_map, declared, inferred))
unless return_type_conforms_to?(inferred, declared)
result.push Problem.new(pin.location, "Declared return type #{declared.rooted_tags} does not match inferred type #{inferred.rooted_tags} for #{pin.path}", pin: pin)

@@ -166,2 +205,3 @@ end

def virtual_pin? pin
# @sg-ignore Need to add nil check here
pin.location && source.comment_at?(pin.location.range.ending)

@@ -214,2 +254,3 @@ end

all_variables.each do |pin|
# @sg-ignore Need to add nil check here
if pin.return_type.defined?

@@ -229,3 +270,3 @@ declared = pin.typify(api_map)

else
unless any_types_match?(api_map, declared, inferred)
unless assignment_conforms_to?(inferred, declared)
result.push Problem.new(pin.location, "Declared type #{declared} does not match inferred type #{inferred} for variable #{pin.name}", pin: pin)

@@ -262,4 +303,6 @@ end

chain = Solargraph::Parser.chain(const, filename)
# @sg-ignore Need to add nil check here
closure_pin = source_map.locate_closure_pin(rng.start.line, rng.start.column)
closure_pin.rebind(api_map)
# @sg-ignore Need to add nil check here
location = Location.new(filename, rng)

@@ -281,4 +324,6 @@ locals = source_map.locals_at(location)

rng = Solargraph::Range.from_node(call)
# @sg-ignore Need to add nil check here
next if @marked_ranges.any? { |d| d.contain?(rng.start) }
chain = Solargraph::Parser.chain(call, filename)
# @sg-ignore Need to add nil check here
closure_pin = source_map.locate_closure_pin(rng.start.line, rng.start.column)

@@ -290,7 +335,12 @@ namespace_pin = closure_pin

# one closure
# @todo Need to add nil check here
# @todo Should warn on nil deference here
closure_pin = closure_pin.closure
end
# @sg-ignore Need to add nil check here
closure_pin.rebind(api_map)
# @sg-ignore Need to add nil check here
location = Location.new(filename, rng)
locals = source_map.locals_at(location)
# @sg-ignore Need to add nil check here
type = chain.infer(api_map, closure_pin, locals)

@@ -300,6 +350,11 @@ if type.undefined? && !rules.ignore_all_undefined?

missing = chain
# @type [Solargraph::Pin::Base, nil]
found = nil
# @type [Array<Solargraph::Pin::Base>]
all_found = []
closest = ComplexType::UNDEFINED
until base.links.first.undefined?
found = base.define(api_map, closure_pin, locals).first
# @sg-ignore Need to add nil check here
all_found = base.define(api_map, closure_pin, locals)
found = all_found.first
break if found

@@ -309,5 +364,7 @@ missing = base

end
closest = found.typify(api_map) if found
all_closest = all_found.map { |pin| pin.typify(api_map) }
closest = ComplexType.new(all_closest.flat_map(&:items).uniq)
# @todo remove the internal_or_core? check at a higher-than-strict level
if !found || found.is_a?(Pin::BaseVariable) || (closest.defined? && internal_or_core?(found))
# @sg-ignore Need to add nil check here
unless closest.generic? || ignored_pins.include?(found)

@@ -323,2 +380,3 @@ if closest.defined?

end
# @sg-ignore Need to add nil check here
result.concat argument_problems_for(chain, api_map, closure_pin, locals, location)

@@ -332,3 +390,3 @@ end

# @param closure_pin [Solargraph::Pin::Closure]
# @param locals [Array<Solargraph::Pin::Base>]
# @param locals [Array<Solargraph::Pin::LocalVariable>]
# @param location [Solargraph::Location]

@@ -339,3 +397,2 @@ # @return [Array<Problem>]

base = chain
# @type last_base_link [Solargraph::Source::Chain::Call]
last_base_link = base.links.last

@@ -363,2 +420,4 @@ return [] unless last_base_link.is_a?(Solargraph::Source::Chain::Call)

init = api_map.get_method_stack(fqns, 'initialize').first
# @type [::Array<Solargraph::TypeChecker::Problem>]
init ? arity_problems_for(init, arguments, location) : []

@@ -392,3 +451,3 @@ else

# @param closure_pin [Pin::Closure]
# @param params [Hash{String => Hash{Symbol => String, Solargraph::ComplexType}}]
# @param params [Hash{String => undefined}]
# @param arguments [Array<Source::Chain>]

@@ -455,3 +514,3 @@ # @param sig [Pin::Signature]

argtype = argtype.self_to_type(closure_pin.context)
if argtype.defined? && ptype.defined? && !any_types_match?(api_map, ptype, argtype)
if argtype.defined? && ptype.defined? && !arg_conforms_to?(argtype, ptype)
errors.push Problem.new(location, "Wrong argument type for #{pin.path}: #{par.name} expected #{ptype}, received #{argtype}")

@@ -471,3 +530,3 @@ return errors

# @param sig [Pin::Signature]
# @param argchain [Source::Chain]
# @param argchain [Solargraph::Source::Chain]
# @param api_map [ApiMap]

@@ -478,3 +537,3 @@ # @param closure_pin [Pin::Closure]

# @param pin [Pin::Method]
# @param params [Hash{String => Hash{Symbol => String, Solargraph::ComplexType}}]
# @param params [Hash{String => Hash{Symbol => undefined}}]
# @param idx [Integer]

@@ -487,2 +546,3 @@ #

par = sig.parameters[idx]
# @type [Solargraph::Source::Chain]
argchain = kwargs[par.name.to_sym]

@@ -497,9 +557,10 @@ if par.decl == :kwrestarg || (par.decl == :optarg && idx == pin.parameters.length - 1 && par.asgn_code == '{}')

else
# @type [ComplexType, ComplexType::UniqueType]
ptype = data[:qualified]
ptype = ptype.self_to_type(pin.context)
unless ptype.undefined?
# @sg-ignore https://github.com/castwide/solargraph/pull/1127
# @type [ComplexType]
argtype = argchain.infer(api_map, closure_pin, locals).self_to_type(closure_pin.context)
# @sg-ignore Unresolved call to defined?
if argtype.defined? && ptype && !any_types_match?(api_map, ptype, argtype)
# @todo Unresolved call to defined?
if argtype.defined? && ptype && !arg_conforms_to?(argtype, ptype)
result.push Problem.new(location, "Wrong argument type for #{pin.path}: #{par.name} expected #{ptype}, received #{argtype}")

@@ -528,2 +589,3 @@ end

next unless params.key?(pname.to_s)
# @type [ComplexType]
ptype = params[pname.to_s][:qualified]

@@ -533,3 +595,3 @@ ptype = ptype.self_to_type(pin.context)

argtype = argtype.self_to_type(closure_pin.context)
if argtype.defined? && ptype && !any_types_match?(api_map, ptype, argtype)
if argtype.defined? && ptype && !arg_conforms_to?(argtype, ptype)
result.push Problem.new(location, "Wrong argument type for #{pin.path}: #{pname} expected #{ptype}, received #{argtype}")

@@ -585,2 +647,3 @@ end

tagged: tag.types.join(', '),
# @sg-ignore need to add a nil check here
qualified: Solargraph::ComplexType.try_parse(*tag.types).qualify(api_map, *pin.closure.gates)

@@ -635,2 +698,3 @@ }

return false if pin.nil?
# @sg-ignore flow sensitive typing needs to handle attrs
pin.location && api_map.bundled?(pin.location.filename)

@@ -656,4 +720,7 @@ end

chain = Solargraph::Parser.chain(pin.assignment, filename)
# @sg-ignore flow sensitive typing needs to handle attrs
rng = Solargraph::Range.from_node(pin.assignment)
# @sg-ignore Need to add nil check here
closure_pin = source_map.locate_closure_pin(rng.start.line, rng.start.column)
# @sg-ignore flow sensitive typing needs to handle "if foo.nil?"
location = Location.new(filename, Range.from_node(pin.assignment))

@@ -665,6 +732,10 @@ locals = source_map.locals_at(location)

missing = chain
# @type [Solargraph::Pin::Base, nil]
found = nil
# @type [Array<Solargraph::Pin::Base>]
all_found = []
closest = ComplexType::UNDEFINED
until base.links.first.undefined?
found = base.define(api_map, closure_pin, locals).first
all_found = base.define(api_map, closure_pin, locals)
found = all_found.first
break if found

@@ -674,3 +745,4 @@ missing = base

end
closest = found.typify(api_map) if found
all_closest = all_found.map { |pin| pin.typify(api_map) }
closest = ComplexType.new(all_closest.flat_map(&:items).uniq)
if !found || closest.defined? || internal?(found)

@@ -776,4 +848,6 @@ return false

# @param pin [Pin::Method]
# @sg-ignore need boolish support for ? methods
def abstract? pin
pin.docstring.has_tag?('abstract') ||
# @sg-ignore of low sensitive typing needs to handle ivars
(pin.closure && pin.closure.docstring.has_tag?('abstract'))

@@ -788,7 +862,11 @@ end

with_block = false
# @param pin [Pin::Parameter]
pin.parameters.each do |pin|
# @sg-ignore flow sensitive typing should be able to handle redefinition
if [:kwarg, :kwoptarg, :kwrestarg].include?(pin.decl)
with_opts = true
# @sg-ignore flow sensitive typing should be able to handle redefinition
elsif pin.decl == :block
with_block = true
# @sg-ignore flow sensitive typing should be able to handle redefinition
elsif pin.decl == :restarg

@@ -800,4 +878,6 @@ args.push Solargraph::Source::Chain.new([Solargraph::Source::Chain::Variable.new(pin.name)], nil, true)

end
args.push Solargraph::Parser.chain_string('{}') if with_opts
args.push Solargraph::Parser.chain_string('&') if with_block
pin_location = pin.location
starting_line = pin_location ? pin_location.range.start.line : 0
args.push Solargraph::Parser.chain_string('{}', filename, starting_line) if with_opts
args.push Solargraph::Parser.chain_string('&', filename, starting_line) if with_block
args

@@ -814,2 +894,3 @@ end

source.associated_comments.select do |_line, text|
# @sg-ignore Need to add nil check here
text.include?('@sg-ignore')

@@ -816,0 +897,0 @@ end.keys.to_set

@@ -8,2 +8,3 @@ # frozen_string_literal: true

class Problem
# @todo Missed nil violation
# @return [Solargraph::Location]

@@ -15,2 +16,3 @@ attr_reader :location

# @todo Missed nil violation
# @return [Pin::Base]

@@ -22,3 +24,3 @@ attr_reader :pin

# @param location [Solargraph::Location]
# @param location [Solargraph::Location, nil]
# @param message [String]

@@ -25,0 +27,0 @@ # @param pin [Solargraph::Pin::Base, nil]

@@ -63,6 +63,79 @@ # frozen_string_literal: true

def require_all_return_types_match_inferred?
report?(:require_all_return_types_match_inferred, :alpha)
def require_inferred_type_params?
report?(:require_inferred_type_params, :alpha)
end
#
# False negatives:
#
# @todo 4: Missed nil violation
#
# pending code fixes (277):
#
# @todo 281: Need to add nil check here
# @todo 22: Translate to something flow sensitive typing understands
# @todo 3: Need a downcast here
#
# flow sensitive typing could handle (96):
#
# @todo 36: flow sensitive typing needs to handle attrs
# @todo 29: flow sensitive typing should be able to handle redefinition
# @todo 19: flow sensitive typing needs to narrow down type with an if is_a? check
# @todo 13: Need to validate config
# @todo 8: flow sensitive typing should support .class == .class
# @todo 6: need boolish support for ? methods
# @todo 5: literal arrays in this module turn into ::Solargraph::Source::Chain::Array
# @todo 5: flow sensitive typing needs to handle 'raise if'
# @todo 5: flow sensitive typing needs better handling of ||= on lvars
# @todo 4: flow sensitive typing needs to eliminate literal from union with [:bar].include?(foo)
# @todo 4: nil? support in flow sensitive typing
# @todo 2: downcast output of Enumerable#select
# @todo 2: flow sensitive typing should handle return nil if location&.name.nil?
# @todo 2: flow sensitive typing should handle is_a? and next
# @todo 2: Need to look at Tuple#include? handling
# @todo 2: Should better support meaning of '&' in RBS
# @todo 2: (*) flow sensitive typing needs to handle "if foo = bar"
# @todo 2: flow sensitive typing needs to handle "if foo = bar"
# @todo 2: Need to handle duck-typed method calls on union types
# @todo 2: Need better handling of #compact
# @todo 2: flow sensitive typing should allow shadowing of Kernel#caller
# @todo 2: flow sensitive typing ought to be able to handle 'when ClassName'
# @todo 1: flow sensitive typing not smart enough to handle this case
# @todo 1: flow sensitive typing needs to handle if foo = bar
# @todo 1: flow sensitive typing needs to handle "if foo.nil?"
# @todo 1: flow sensitive typing should support case/when
# @todo 1: flow sensitive typing should support ivars
# @todo 1: Need to support this in flow sensitive typing
# @todo 1: flow sensitive typing needs to handle self.class == other.class
# @todo 1: flow sensitive typing needs to remove literal with
# @todo 1: flow sensitive typing needs to understand reassignment
# @todo 1: flow sensitive typing should be able to identify more blocks that always return
# @todo 1: should warn on nil dereference below
# @todo 1: flow sensitive typing needs to create separate ranges for postfix if
# @todo 1: flow sensitive typing needs to handle constants
# @todo 1: flow sensitive typing needs to eliminate literal from union with return if foo == :bar
def require_all_unique_types_match_expected?
report?(:require_all_unique_types_match_expected, :strong)
end
def require_all_unique_types_support_call?
report?(:require_all_unique_types_support_call, :strong)
end
def require_no_undefined_args?
report?(:require_no_undefined_args, :alpha)
end
def require_generics_resolved?
report?(:require_generics_resolved, :alpha)
end
def require_interfaces_resolved?
report?(:require_interfaces_resolved, :alpha)
end
def require_downcasts?
report?(:require_downcasts, :alpha)
end
# We keep this at strong because if you added an @ sg-ignore to

@@ -69,0 +142,0 @@ # address a strong-level issue, then ran at a lower level, you'd

# frozen_string_literal: true
module Solargraph
VERSION = '0.58.3'
VERSION = ENV.fetch('SOLARGRAPH_FORCE_VERSION', '0.59.0.dev.1')
end

@@ -5,2 +5,3 @@ # frozen_string_literal: true

require 'json'
require 'yaml'

@@ -13,3 +14,6 @@ module Solargraph

class Workspace
include Logging
autoload :Config, 'solargraph/workspace/config'
autoload :Gemspecs, 'solargraph/workspace/gemspecs'
autoload :RequirePaths, 'solargraph/workspace/require_paths'

@@ -24,3 +28,4 @@

# @param directory [String] TODO: Remove '' and '*' special cases
# @todo Remove '' and '*' special cases
# @param directory [String]
# @param config [Config, nil]

@@ -56,2 +61,72 @@ # @param server [Hash]

# @param stdlib_name [String]
#
# @return [Array<String>]
def stdlib_dependencies stdlib_name
gemspecs.stdlib_dependencies(stdlib_name)
end
# @param out [IO, nil] output stream for logging
# @param gemspec [Gem::Specification]
# @return [Array<Gem::Specification>]
def fetch_dependencies gemspec, out: $stderr
gemspecs.fetch_dependencies(gemspec, out: out)
end
# @param require [String] The string sent to 'require' in the code to resolve, e.g. 'rails', 'bundler/require'
# @return [Array<Gem::Specification>, nil]
def resolve_require require
gemspecs.resolve_require(require)
end
# @return [Solargraph::PinCache]
def pin_cache
@pin_cache ||= fresh_pincache
end
# @param stdlib_name [String]
#
# @return [Array<String>]
def stdlib_dependencies stdlib_name
deps = RbsMap::StdlibMap.stdlib_dependencies(stdlib_name, nil) || []
deps.map { |dep| dep['name'] }.compact
end
# @return [Environ]
def global_environ
# empty docmap, since the result needs to work in any possible
# context here
@global_environ ||= Convention.for_global(DocMap.new([], self, out: nil))
end
# @param gemspec [Gem::Specification]
# @param out [StringIO, IO, nil] output stream for logging
# @param rebuild [Boolean] whether to rebuild the pins even if they are cached
#
# @return [void]
def cache_gem gemspec, out: nil, rebuild: false
pin_cache.cache_gem(gemspec: gemspec, out: out, rebuild: rebuild)
end
# @param gemspec [Gem::Specification, Bundler::LazySpecification]
# @param out [StringIO, IO, nil] output stream for logging
#
# @return [void]
def uncache_gem gemspec, out: nil
pin_cache.uncache_gem(gemspec, out: out)
end
# @return [Solargraph::PinCache]
def fresh_pincache
PinCache.new(rbs_collection_path: rbs_collection_path,
rbs_collection_config_path: rbs_collection_config_path,
yard_plugins: yard_plugins,
directory: directory)
end
# @return [Array<String>]
def yard_plugins
@yard_plugins ||= global_environ.yard_plugins.sort.uniq
end
# @param level [Symbol]

@@ -70,2 +145,3 @@ # @return [TypeChecker::Rules]

def merge *sources
# @sg-ignore Need to add nil check here
unless directory == '*' || sources.all? { |source| source_hash.key?(source.filename) }

@@ -78,6 +154,8 @@ # Reload the config to determine if a new source should be included

sources.each do |source|
if directory == "*" || config.calculated.include?(source.filename)
source_hash[source.filename] = source
includes_any = true
end
# @sg-ignore Need to add nil check here
next unless directory == "*" || config.calculated.include?(source.filename)
# @sg-ignore Need to add nil check here
source_hash[source.filename] = source
includes_any = true
end

@@ -135,2 +213,19 @@

# True if the workspace contains at least one gemspec file.
#
# @return [Boolean]
def gemspec?
!gemspec_files.empty?
end
# Get an array of all gemspec files in the workspace.
#
# @return [Array<String>]
def gemspec_files
return [] if directory.empty? || directory == '*'
@gemspec_files ||= Dir[File.join(directory, '**/*.gemspec')].select do |gs|
config.allow? gs
end
end
# @return [String, nil]

@@ -143,8 +238,53 @@ def rbs_collection_path

def rbs_collection_config_path
@rbs_collection_config_path ||= begin
unless directory.empty? || directory == '*'
yaml_file = File.join(directory, 'rbs_collection.yaml')
yaml_file if File.file?(yaml_file)
@rbs_collection_config_path ||=
begin
unless directory.empty? || directory == '*'
yaml_file = File.join(directory, 'rbs_collection.yaml')
yaml_file if File.file?(yaml_file)
end
end
end
# @param name [String]
# @param version [String, nil]
# @param out [IO, nil]
#
# @return [Gem::Specification, nil]
def find_gem name, version = nil, out: nil
gemspecs.find_gem(name, version, out: out)
end
# @return [Array<Gem::Specification>]
def all_gemspecs_from_bundle
gemspecs.all_gemspecs_from_bundle
end
# @todo make this actually work against bundle instead of pulling
# all installed gemspecs -
# https://github.com/apiology/solargraph/pull/10
# @return [Array<Gem::Specification>]
def all_gemspecs_from_bundle
Gem::Specification.to_a
end
# @param out [StringIO, IO, nil] output stream for logging
# @param rebuild [Boolean] whether to rebuild the pins even if they are cached
# @return [void]
def cache_all_for_workspace! out, rebuild: false
PinCache.cache_core(out: out) unless PinCache.core? && !rebuild
gem_specs = all_gemspecs_from_bundle
# try any possible standard libraries, but be quiet about it
stdlib_specs = pin_cache.possible_stdlibs.map { |stdlib| find_gem(stdlib, out: nil) }.compact
specs = (gem_specs + stdlib_specs)
specs.each do |spec|
pin_cache.cache_gem(gemspec: spec, rebuild: rebuild, out: out) unless pin_cache.cached?(spec)
end
out&.puts "Documentation cached for all #{specs.length} gems."
# do this after so that we prefer stdlib requires from gems,
# which are likely to be newer and have more pins
pin_cache.cache_all_stdlibs(out: out, rebuild: rebuild)
out&.puts "Documentation cached for core, standard library and gems."
end

@@ -160,3 +300,5 @@

# @sg-ignore Need to validate config
# @return [String]
# @sg-ignore Need to validate config
def command_path

@@ -172,8 +314,5 @@ server['commandPath'] || 'solargraph'

# True if the workspace has a root Gemfile.
#
# @todo Handle projects with custom Bundler/Gemfile setups (see DocMap#gemspecs_required_from_bundler)
#
def gemfile?
directory && File.file?(File.join(directory, 'Gemfile'))
# @return [Solargraph::Workspace::Gemspecs]
def gemspecs
@gemspecs ||= Solargraph::Workspace::Gemspecs.new(directory_or_nil)
end

@@ -199,3 +338,6 @@

size = config.calculated.length
raise WorkspaceTooLargeError, "The workspace is too large to index (#{size} files, #{config.max_files} max)" if config.max_files > 0 and size > config.max_files
if config.max_files > 0 and size > config.max_files
raise WorkspaceTooLargeError,
"The workspace is too large to index (#{size} files, #{config.max_files} max)"
end
config.calculated.each do |filename|

@@ -202,0 +344,0 @@ begin

@@ -17,3 +17,3 @@ # frozen_string_literal: true

# @todo To make JSON strongly typed we'll need a record syntax
# @todo Need to validate config
# @return [Hash{String => undefined, nil}]

@@ -82,3 +82,5 @@ attr_reader :raw_data

#
# @sg-ignore Need to validate config
# @return [Array<String>]
# @sg-ignore Need to validate config
def require_paths

@@ -85,0 +87,0 @@ raw_data['require_paths'] || []

@@ -86,2 +86,3 @@ # frozen_string_literal: true

rescue StandardError => e
# @sg-ignore flow sensitive typing should be able to handle redefinition
Solargraph.logger.warn "Error reading #{gemspec_file_path}: [#{e.class}] #{e.message}"

@@ -88,0 +89,0 @@ []

@@ -8,3 +8,3 @@ module Solargraph

# @param spec [Gem::Specification, nil]
# @return [Solargraph::Location, nil]
# @return [Solargraph::Location]
def object_location code_object, spec

@@ -18,2 +18,3 @@ if spec.nil? || code_object.nil? || code_object.file.nil? || code_object.line.nil?

end
# @sg-ignore flow sensitive typing should be able to identify more blocks that always return
file = File.join(spec.full_gem_path, code_object.file)

@@ -20,0 +21,0 @@ Solargraph::Location.new(file, Solargraph::Range.from_to(code_object.line - 1, 0, code_object.line - 1, 0))

@@ -27,2 +27,3 @@ # frozen_string_literal: true

# ignored here. The YardMap will load dependencies separately.
# @sg-ignore Need to add nil check here
@pins.keep_if { |pin| pin.location.nil? || File.file?(pin.location.filename) } if @spec

@@ -42,2 +43,3 @@ @pins

result.push nspin
# @sg-ignore flow sensitive typing needs to narrow down type with an if is_a? check
if code_object.is_a?(YARD::CodeObjects::ClassObject) and !code_object.superclass.nil?

@@ -47,5 +49,8 @@ # This method of superclass detection is a bit of a hack. If

# yardoc and converted to a fully qualified namespace.
# @sg-ignore flow sensitive typing needs to narrow down type with an if is_a? check
superclass = if code_object.superclass.is_a?(YARD::CodeObjects::Proxy)
# @sg-ignore flow sensitive typing needs to narrow down type with an if is_a? check
"::#{code_object.superclass}"
else
# @sg-ignore flow sensitive typing needs to narrow down type with an if is_a? check
code_object.superclass.to_s

@@ -52,0 +57,0 @@ end

@@ -9,2 +9,3 @@ # frozen_string_literal: true

# @type [Hash{Array<String, Symbol, String> => Symbol}]
VISIBILITY_OVERRIDE = {

@@ -29,5 +30,8 @@ # YARD pays attention to 'private' statements prior to class methods but shouldn't

final_scope = scope || code_object.scope
# @sg-ignore Need to add nil check here
override_key = [closure.path, final_scope, name]
final_visibility = VISIBILITY_OVERRIDE[override_key]
# @sg-ignore Need to add nil check here
final_visibility ||= VISIBILITY_OVERRIDE[[closure.path, final_scope]]
# @sg-ignore Need to add nil check here
final_visibility ||= :private if closure.path == 'Kernel' && Kernel.private_instance_methods(false).include?(name.to_sym)

@@ -54,2 +58,3 @@ final_visibility ||= visibility

else
# @sg-ignore Need to add nil check here
pin = Pin::Method.new(

@@ -91,3 +96,2 @@ location: location,

# See https://github.com/castwide/solargraph/issues/345
# @sg-ignore https://github.com/castwide/solargraph/pull/1114
code_object.parameters.select { |a| a[0] && a[0] != 'self' }.map do |a|

@@ -94,0 +98,0 @@ Solargraph::Pin::Parameter.new(

@@ -24,2 +24,3 @@ # frozen_string_literal: true

closure: closure,
# @sg-ignore need to add a nil check here
gates: closure.gates,

@@ -26,0 +27,0 @@ source: :yardoc,

@@ -0,0 +0,0 @@ # frozen_string_literal: true

@@ -11,11 +11,11 @@ # frozen_string_literal: true

# Build and cache a gem's yardoc and return the path. If the cache already
# exists, do nothing and return the path.
# Build and save a gem's yardoc into a given path.
#
# @param yard_plugins [Array<String>] The names of YARD plugins to use.
# @param gem_yardoc_path [String] the path to the yardoc cache of a particular gem
# @param yard_plugins [Array<String>]
# @param gemspec [Gem::Specification]
# @return [String] The path to the cached yardoc.
def cache(yard_plugins, gemspec)
path = PinCache.yardoc_path gemspec
return path if cached?(gemspec)
#
# @return [void]
def build_docs gem_yardoc_path, yard_plugins, gemspec
return if docs_built?(gem_yardoc_path)

@@ -28,23 +28,32 @@ unless Dir.exist? gemspec.gem_dir

Solargraph.logger.info { "Bad info from gemspec - #{gemspec.gem_dir} does not exist" }
return path
return
end
Solargraph.logger.info "Caching yardoc for #{gemspec.name} #{gemspec.version}"
cmd = "yardoc --db #{path} --no-output --plugin solargraph"
cmd = "yardoc --db #{gem_yardoc_path} --no-output --plugin solargraph"
yard_plugins.each { |plugin| cmd << " --plugin #{plugin}" }
Solargraph.logger.debug { "Running: #{cmd}" }
# @todo set these up to run in parallel
# @sg-ignore Our fill won't work properly due to an issue in
# Callable#arity_matches? - see comment there
stdout_and_stderr_str, status = Open3.capture2e(current_bundle_env_tweaks, cmd, chdir: gemspec.gem_dir)
unless status.success?
Solargraph.logger.warn { "YARD failed running #{cmd.inspect} in #{gemspec.gem_dir}" }
Solargraph.logger.info stdout_and_stderr_str
end
path
return if status.success?
Solargraph.logger.warn { "YARD failed running #{cmd.inspect} in #{gemspec.gem_dir}" }
Solargraph.logger.info stdout_and_stderr_str
end
# @param gem_yardoc_path [String] the path to the yardoc cache of a particular gem
# @param gemspec [Gem::Specification, Bundler::LazySpecification]
# @param out [StringIO, IO, nil] where to log messages
# @return [Array<Pin::Base>]
def build_pins gem_yardoc_path, gemspec, out: $stderr
yardoc = load!(gem_yardoc_path)
YardMap::Mapper.new(yardoc, gemspec).map
end
# True if the gem yardoc is cached.
#
# @param gemspec [Gem::Specification]
def cached?(gemspec)
yardoc = File.join(PinCache.yardoc_path(gemspec), 'complete')
# @param gem_yardoc_path [String]
def docs_built? gem_yardoc_path
yardoc = File.join(gem_yardoc_path, 'complete')
File.exist?(yardoc)

@@ -55,5 +64,5 @@ end

#
# @param gemspec [Gem::Specification]
def processing?(gemspec)
yardoc = File.join(PinCache.yardoc_path(gemspec), 'processing')
# @param gem_yardoc_path [String] the path to the yardoc cache of a particular gem
def processing? gem_yardoc_path
yardoc = File.join(gem_yardoc_path, 'processing')
File.exist?(yardoc)

@@ -66,6 +75,6 @@ end

#
# @param gemspec [Gem::Specification]
# @param gem_yardoc_path [String] the path to the yardoc cache of a particular gem
# @return [Array<YARD::CodeObjects::Base>]
def load!(gemspec)
YARD::Registry.load! PinCache.yardoc_path gemspec
def load! gem_yardoc_path
YARD::Registry.load! gem_yardoc_path
YARD::Registry.all

@@ -85,2 +94,3 @@ end

tweaks = {}
# @sg-ignore Translate to something flow sensitive typing understands
if ENV['BUNDLE_GEMFILE'] && !ENV['BUNDLE_GEMFILE'].empty?

@@ -87,0 +97,0 @@ tweaks['BUNDLE_GEMFILE'] = File.expand_path(ENV['BUNDLE_GEMFILE'])

@@ -66,2 +66,3 @@ require 'rake'

warn "hit error: #{e.message}"
# @sg-ignore Need to add nil check here
warn "Backtrace:\n#{e.backtrace.join("\n")}"

@@ -68,0 +69,0 @@ warn "output: #{output}"

@@ -147,4 +147,32 @@ # <-- liberally borrowed from

| [T] (int index) { (int index) -> T } -> (A | B | C | D | E | F | G | H | I | J | T)
# <!--
# rdoc-file=array.rb
# - first -> object or nil
# - first(count) -> new_array
# -->
# Returns elements from `self`, or `nil`; does not modify `self`.
#
# With no argument given, returns the first element (if available):
#
# a = [:foo, 'bar', 2]
# a.first # => :foo
# a # => [:foo, "bar", 2]
#
# If `self` is empty, returns `nil`.
#
# [].first # => nil
#
# With a non-negative integer argument `count` given, returns the first `count`
# elements (as available) in a new array:
#
# a.first(0) # => []
# a.first(2) # => [:foo, "bar"]
# a.first(50) # => [:foo, "bar", 2]
#
# Related: see [Methods for Querying](rdoc-ref:Array@Methods+for+Querying).
#
def first: %a{implicitly-returns-nil} () -> A
end
end
end
module ::AST
class Node
def children: () -> [self, Integer, String, Symbol, nil]
def children: () -> Array[self | Integer | String | Symbol | nil]
end
end

@@ -54,2 +54,4 @@ # Solargraph

Use `bundle exec solargraph config` to create a configuration file.
### Plugins

@@ -136,5 +138,3 @@

To see more logging when typechecking or running specs, set the
`SOLARGRAPH_LOG` environment variable to `debug` or `info`. `warn` is
the default value.
To see more logging when typechecking or running specs, set the `SOLARGRAPH_LOG` environment variable to `debug` or `info`. `warn` is the default value.

@@ -141,0 +141,0 @@ Code contributions are always appreciated. Feel free to fork the repo and submit pull requests. Check for open issues that could use help. Start new issues to discuss changes that have a major impact on the code or require large time commitments.

@@ -0,1 +1,2 @@

# @sg-ignore Should better support meaning of '&' in RBS
$LOAD_PATH.unshift File.dirname(__FILE__) + '/lib'

@@ -45,3 +46,3 @@ require 'solargraph/version'

s.add_runtime_dependency 'prism', '~> 1.4'
s.add_runtime_dependency 'rbs', ['>= 3.6.1', '<= 4.0.0.dev.4']
s.add_runtime_dependency 'rbs', ['>= 3.6.1', '<= 4.0.0.dev.5']
s.add_runtime_dependency 'reverse_markdown', '~> 3.0'

@@ -48,0 +49,0 @@ s.add_runtime_dependency 'rubocop', '~> 1.76'

# frozen_string_literal: true
module Solargraph
class TypeChecker
# Helper methods for performing type checks
#
module Checks
module_function
# Compare an expected type with an inferred type. Common usage is to
# check if the type declared in a method's @return tag matches the type
# inferred from static analysis of the code.
#
# @param api_map [ApiMap]
# @param expected [ComplexType]
# @param inferred [ComplexType]
# @return [Boolean]
def types_match? api_map, expected, inferred
return true if expected.to_s == inferred.to_s
matches = []
expected.each do |exp|
found = false
inferred.each do |inf|
# if api_map.super_and_sub?(fuzz(inf), fuzz(exp))
if either_way?(api_map, inf, exp)
found = true
matches.push inf
break
end
end
return false unless found
end
inferred.each do |inf|
next if matches.include?(inf)
found = false
expected.each do |exp|
# if api_map.super_and_sub?(fuzz(inf), fuzz(exp))
if either_way?(api_map, inf, exp)
found = true
break
end
end
return false unless found
end
true
end
# @param api_map [ApiMap]
# @param expected [ComplexType]
# @param inferred [ComplexType]
# @return [Boolean]
def any_types_match? api_map, expected, inferred
expected = expected.downcast_to_literal_if_possible
inferred = inferred.downcast_to_literal_if_possible
return duck_types_match?(api_map, expected, inferred) if expected.duck_type?
# walk through the union expected type and see if any members
# of the union match the inferred type
expected.each do |exp|
next if exp.duck_type?
# @todo: there should be a level of typechecking where all
# unique types in the inferred must match one of the
# expected unique types
inferred.each do |inf|
# return true if exp == inf || api_map.super_and_sub?(fuzz(inf), fuzz(exp))
return true if exp == inf || either_way?(api_map, inf, exp)
end
end
false
end
# @param api_map [ApiMap]
# @param inferred [ComplexType]
# @param expected [ComplexType]
# @return [Boolean]
def all_types_match? api_map, inferred, expected
expected = expected.downcast_to_literal_if_possible
inferred = inferred.downcast_to_literal_if_possible
return duck_types_match?(api_map, expected, inferred) if expected.duck_type?
inferred.each do |inf|
next if inf.duck_type?
return false unless expected.any? { |exp| exp == inf || either_way?(api_map, inf, exp) }
end
true
end
# @param api_map [ApiMap]
# @param expected [ComplexType]
# @param inferred [ComplexType]
# @return [Boolean]
def duck_types_match? api_map, expected, inferred
raise ArgumentError, 'Expected type must be duck type' unless expected.duck_type?
expected.each do |exp|
next unless exp.duck_type?
quack = exp.to_s[1..-1]
return false if api_map.get_method_stack(inferred.namespace, quack, scope: inferred.scope).empty?
end
true
end
# @param type [ComplexType::UniqueType]
# @return [String]
def fuzz type
if type.parameters?
type.name
else
type.tag
end
end
# @param api_map [ApiMap]
# @param cls1 [ComplexType::UniqueType]
# @param cls2 [ComplexType::UniqueType]
# @return [Boolean]
def either_way?(api_map, cls1, cls2)
# @todo there should be a level of typechecking which uses the
# full tag with parameters to determine compatibility
f1 = cls1.name
f2 = cls2.name
api_map.type_include?(f1, f2) || api_map.super_and_sub?(f1, f2) || api_map.super_and_sub?(f2, f1)
# api_map.type_include?(f1, f2) || api_map.super_and_sub?(f1, f2) || api_map.super_and_sub?(f2, f1)
end
end
end
end
# frozen_string_literal: true
module Solargraph
class TypeChecker
# Data about a method parameter definition. This is the information from
# the args list in the def call, not the `@param` tags.
#
class ParamDef
# @return [String]
attr_reader :name
# @return [Symbol]
attr_reader :type
# @param name [String]
# @param type [Symbol] The type of parameter, such as :req, :opt, :rest, etc.
def initialize name, type
@name = name
@type = type
end
class << self
# Get an array of ParamDefs from a method pin.
#
# @param pin [Solargraph::Pin::Method]
# @return [Array<ParamDef>]
def from pin
result = []
pin.parameters.each do |par|
result.push ParamDef.new(par.name, par.decl)
end
result
end
end
end
end
end
# frozen_string_literal: true
module Solargraph
class YardMap
class ToMethod
module InnerMethods
module_function
# @param code_object [YARD::CodeObjects::Base]
# @param location [Solargraph::Location]
# @param comments [String]
# @return [Array<Solargraph::Pin::Parameter>]
def get_parameters code_object, location, comments
return [] unless code_object.is_a?(YARD::CodeObjects::MethodObject)
# HACK: Skip `nil` and `self` parameters that are sometimes emitted
# for methods defined in C
# See https://github.com/castwide/solargraph/issues/345
# @sg-ignore https://github.com/castwide/solargraph/pull/1114
code_object.parameters.select { |a| a[0] && a[0] != 'self' }.map do |a|
Solargraph::Pin::Parameter.new(
location: location,
closure: self,
comments: comments,
name: arg_name(a),
presence: nil,
decl: arg_type(a),
asgn_code: a[1],
source: :yard_map
)
end
end
# @param a [Array<String>]
# @return [String]
def arg_name a
a[0].gsub(/[^a-z0-9_]/i, '')
end
# @param a [Array]
# @return [::Symbol]
def arg_type a
if a[0].start_with?('**')
:kwrestarg
elsif a[0].start_with?('*')
:restarg
elsif a[0].start_with?('&')
:blockarg
elsif a[0].end_with?(':')
a[1] ? :kwoptarg : :kwarg
elsif a[1]
:optarg
else
:arg
end
end
end
private_constant :InnerMethods
include Helpers
# @param code_object [YARD::CodeObjects::MethodObject]
# @param name [String, nil]
# @param scope [Symbol, nil]
# @param visibility [Symbol, nil]
# @param closure [Solargraph::Pin::Base, nil]
# @param spec [Solargraph::Pin::Base, nil]
# @return [Solargraph::Pin::Method]
def make code_object, name = nil, scope = nil, visibility = nil, closure = nil, spec = nil
closure ||= Solargraph::Pin::Namespace.new(
name: code_object.namespace.to_s,
gates: [code_object.namespace.to_s]
)
location = object_location(code_object, spec)
comments = code_object.docstring ? code_object.docstring.all.to_s : ''
Pin::Method.new(
location: location,
closure: closure,
name: name || code_object.name.to_s,
comments: comments,
scope: scope || code_object.scope,
visibility: visibility || code_object.visibility,
parameters: InnerMethods.get_parameters(code_object, location, comments),
explicit: code_object.is_explicit?,
source: :yard_map
)
end
end
end
end