solargraph
Advanced tools
+3
| # 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 |
@@ -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 |
+0
-1
@@ -17,2 +17,1 @@ /.gem_rbs_collection | ||
| /.rspec-local | ||
| vendor/cache |
+27
-49
@@ -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
-10
@@ -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 |
+24
-3
@@ -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 |
+105
-50
@@ -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 |
+132
-371
@@ -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] |
+477
-57
@@ -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 |
+73
-39
@@ -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 |
+158
-16
@@ -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 |
+33
-23
@@ -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']) |
+1
-0
@@ -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 |
+3
-3
@@ -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 |