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

solargraph

Package Overview
Dependencies
Maintainers
1
Versions
211
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

solargraph - rubygems Package Compare versions

Comparing version
0.56.2
to
0.57.0
+125
.github/workflows/linting.yml
---
# To debug locally:
# npm install -g act
# act pull_release -j overcommit
name: Linting
on:
workflow_dispatch: {}
pull_request:
branches: [ master ]
push:
branches:
- 'main'
tags:
- 'v*'
permissions:
pull-requests: write
contents: read
jobs:
overcommit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
# Number of commits to fetch. 0 indicates all history for all branches and tags.
with:
fetch-depth: 0
- uses: ruby/setup-ruby@v1
with:
ruby-version: 3.4
bundler: latest
bundler-cache: true
cache-version: 2025-06-06
- name: Update to best available RBS
run: |
bundle update rbs # use latest available for this Ruby version
- name: Restore cache of gem annotations
id: dot-cache-restore
uses: actions/cache/restore@v4
with:
key: |
2025-06-26-09-${{ runner.os }}-dot-cache-${{ hashFiles('Gemfile.lock') }}
restore-keys: |
2025-06-26-09-${{ runner.os }}-dot-cache
2025-06-26-09-${{ runner.os }}-dot-cache-
path: |
/home/runner/.cache/solargraph
- name: Install gem types
run: bundle exec rbs collection install
- name: Overcommit
run: |
bundle exec overcommit --sign
SOLARGRAPH_ASSERTS=on bundle exec overcommit --run --diff origin/master
rubocop:
name: rubocop
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: ruby/setup-ruby@1a615958ad9d422dd932dc1d5823942ee002799f # v1.227.0
with:
ruby-version: '3.3'
bundler-cache: true
- uses: reviewdog/action-rubocop@fcb74ba274da10b18d038d0bcddaae3518739634 # v2.21.2
with:
reporter: github-pr-check
skip_install: true
use_bundler: true
rubocop_extensions: 'rubocop-rspec:gemfile rubocop-rake:gemfile rubocop-yard:gemfile'
rubocop_flags: '-c .rubocop.yml'
fail_level: info
rubocop_version: Gemfile
level: info
rbs_validate:
name: rbs validate
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.4
bundler-cache: false
- name: Install gems
run: bundle install
- name: Run rbs validate
run: bundle exec rbs validate
rubocop_todo:
name: .rubocop_todo.yml
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.4
bundler-cache: false
- name: Install gems
run: bundle install
- name: Run RuboCop
run: bundle exec rubocop -c .rubocop.yml
- name: Run RuboCop against todo file
continue-on-error: true
run: |
bundle exec rubocop --auto-gen-config --no-exclude-limit --no-offense-counts --no-auto-gen-timestamp
if [ -n "$(git status --porcelain)" ]
then
git status --porcelain
git diff -u .
>&2 echo "Please fix deltas if bad or run 'bundle exec rubocop --auto-gen-config --no-exclude-limit --no-offense-counts --no-auto-gen-timestamp' and push up changes if good"
exit 1
fi
---
# Use this file to configure the Overcommit hooks you wish to use. This will
# extend the default configuration defined in:
# https://github.com/sds/overcommit/blob/master/config/default.yml
#
# At the topmost level of this YAML file is a key representing type of hook
# being run (e.g. pre-commit, commit-msg, etc.). Within each type you can
# customize each hook, such as whether to only run it on certain files (via
# `include`), whether to only display output if it fails (via `quiet`), etc.
#
# For a complete list of hooks, see:
# https://github.com/sds/overcommit/tree/master/lib/overcommit/hook
#
# For a complete list of options that you can use to customize hooks, see:
# https://github.com/sds/overcommit#configuration
#
# Uncomment the following lines to make the configuration take effect.
PreCommit:
RuboCop:
enabled: true
Solargraph:
enabled: true
exclude:
- 'spec/**/*'
FixMe:
enabled: true
exclude:
# don't freak out over line below
- '.overcommit.yml'
# from upstream
- 'rbs/fills/rubygems/0/basic_specification.rbs'
keywords: ['FIXME', 'TODO', 'XXX']
# creates false positives in CI
AuthorName:
enabled: false
# creates false positives in CI
AuthorEmail:
enabled: false
ALL:
# if you change the next line to 'report', and you make changes to
# a file, overcommit will have you make the entire file pass
#
problem_on_unmodified_line: warn
# on_warn: fail
exclude:
- lib/solargraph/rails/annotations/**/*
- vendor/**/*
- ".bundle/**/*"
- 'core.*'
verify_signatures: false
#
# TrailingWhitespace:
# enabled: true
# exclude:
# - '**/db/structure.sql' # Ignore trailing whitespace in generated files
#
#PostCheckout:
# ALL: # Special hook name that customizes all hooks of this type
# quiet: true # Change all post-checkout hooks to only display output on failure
#
# IndexTags:
# enabled: true # Generate a tags file with `ctags` each time HEAD changes
gemfile: Gemfile

Sorry, the diff of this file is too big to display

---
inherit_from: .rubocop_todo.yml
# The behavior of RuboCop can be controlled via the .rubocop.yml
# configuration file. It makes it possible to enable/disable
# certain cops (checks) and to alter their behavior if they accept
# any parameters. The file can be placed either in your home
# directory or in some project directory.
#
# RuboCop will start looking for the configuration file in the directory
# where the inspected file is and continue its way up to the root directory.
#
# See https://docs.rubocop.org/rubocop/configuration
AllCops:
NewCops: enable
Exclude:
- "spec/fixtures/invalid_byte.rb"
- "spec/fixtures/invalid_node_comment.rb"
- "spec/fixtures/invalid_utf8.rb"
- "vendor/**/*"
- "vendor/**/.*"
TargetRubyVersion: 3.0
RSpec/SpecFilePathFormat:
Enabled: false
Style/MethodDefParentheses:
EnforcedStyle: require_no_parentheses
Layout/EmptyLineAfterGuardClause:
Enabled: false
Lint/UnusedMethodArgument:
AllowUnusedKeywordArguments: true
Metrics/ParameterLists:
Max: 7
CountKeywordArgs: false
# we tend to use @@ and the risk doesn't seem high
Style/ClassVars:
Enabled: false
#
# Set a relaxed standard on harder-to-address item for ease of
# contribution - if you are good at refactoring, we welcome PRs to
# improve existing code!
#
Metrics/AbcSize:
Max: 65
Metrics/MethodLength:
Max: 60
Metrics/ClassLength:
Max: 500
Metrics/CyclomaticComplexity:
Max: 23
Metrics/PerceivedComplexity:
Max: 29
RSpec/ExampleLength:
Max: 17
plugins:
- rubocop-rspec
- rubocop-rake
- rubocop-yard
# frozen_string_literal: true
module Solargraph
class ApiMap
# Methods for handling constants.
#
class Constants
# @param store [Store]
def initialize store
@store = store
end
# Resolve a name to a fully qualified namespace or constant.
#
# @param name [String]
# @param gates [Array<Array<String>, String>]
# @return [String, nil]
def resolve(name, *gates)
return store.get_path_pins(name[2..]).first&.path if name.start_with?('::')
flat = gates.flatten
flat.push '' if flat.empty?
cached_resolve[[name, flat]] || resolve_and_cache(name, flat)
end
# Get a fully qualified namespace from a reference pin.
#
# @param pin [Pin::Reference]
# @return [String, nil]
def dereference pin
resolve(pin.name, pin.reference_gates)
end
# Collect a list of all constants defined in the specified gates.
#
# @param gates [Array<Array<String>, String>]
# @return [Array<Pin::Base>]
def collect(*gates)
flat = gates.flatten
cached_collect[flat] || collect_and_cache(flat)
end
# Determine fully qualified tag for a given tag used inside the
# definition of another tag ("context"). This method will start
# the search in the specified context until it finds a match for
# the tag.
#
# Does not recurse into qualifying the type parameters, but
# returns any which were passed in unchanged.
#
# @param tag [String, nil] The namespace to
# match, complete with generic parameters set to appropriate
# values if available
# @param context_tag [String] The fully qualified context in which
# the tag was referenced; start from here to resolve the name.
# Should not be prefixed with '::'.
# @return [String, nil] fully qualified tag
def qualify tag, context_tag = ''
return tag if ['Boolean', 'self', nil].include?(tag)
type = ComplexType.try_parse(tag)
return unless type.defined?
return tag if type.literal?
context_type = ComplexType.try_parse(context_tag)
return unless context_type.defined?
fqns = qualify_namespace(type.rooted_namespace, context_type.rooted_namespace)
return unless fqns
fqns + type.substring
end
# @return [void]
def clear
[cached_collect, cached_resolve].each(&:clear)
end
private
# @return [Store]
attr_reader :store
# @param name [String]
# @param gates [Array<String>]
# @return [String, nil]
def resolve_and_cache name, gates
cached_resolve[[name, gates]] = resolve_uncached(name, gates)
end
# @param name [String]
# @param gates [Array<String>]
# @return [String, nil]
def resolve_uncached name, gates
parts = name.split('::')
here = parts.shift
resolved = simple_resolve(here, gates)
return resolved if parts.empty? || resolved.nil?
final = "#{resolved}::#{parts.join('::')}".sub(/^::/, '')
final if store.namespace_exists?(final)
end
# @param name [String]
# @param gates [Array<String>]
# @return [String, nil]
def simple_resolve name, gates
gates.each do |gate|
here = "#{gate}::#{name}".sub(/^::/, '').sub(/::$/, '')
return here if store.namespace_exists?(here)
end
nil
end
# @param gates [Array<String>]
# @return [Array<Pin::Base>]
def collect_and_cache gates
skip = Set.new
cached_collect[gates] = gates.flat_map do |gate|
inner_get_constants(gate, %i[public private], skip)
end
end
# @return [Hash{Array(Name, Array<String>) => String, nil}]
def cached_resolve
@cached_resolve ||= {}
end
# @return [Hash{Array<String> => Array<Pin::Base>}]
def cached_collect
@cached_collect ||= {}
end
# Determine fully qualified namespace for a given namespace used
# inside the definition of another tag ("context"). This method
# will start the search in the specified context until it finds a
# match for the namespace.
#
# @param namespace [String, nil] The namespace to
# match
# @param context_namespace [String] The context namespace in which the
# tag was referenced; start from here to resolve the name
# @return [String, nil] fully qualified namespace
def qualify_namespace namespace, context_namespace = ''
if namespace.start_with?('::')
inner_qualify(namespace[2..], '', Set.new)
else
inner_qualify(namespace, context_namespace, Set.new)
end
end
# @param name [String] Namespace to fully qualify
# @param root [String] The context to search
# @param skip [Set<String>] Contexts already searched
# @return [String, nil] Fully qualified ("rooted") namespace
def inner_qualify name, root, skip
return name if name == ComplexType::GENERIC_TAG_NAME
return nil if name.nil?
return nil if skip.include?(root)
skip.add root
possibles = []
if name == ''
return '' if root == ''
inner_qualify(root, '', skip)
else
return name if root == '' && store.namespace_exists?(name)
roots = root.to_s.split('::')
while roots.length.positive?
fqns = "#{roots.join('::')}::#{name}"
return fqns if store.namespace_exists?(fqns)
incs = store.get_includes(roots.join('::'))
incs.each do |inc|
foundinc = inner_qualify(name, inc.parametrized_tag.to_s, skip)
possibles.push foundinc unless foundinc.nil?
end
roots.pop
end
if possibles.empty?
incs = store.get_includes('')
incs.each do |inc|
foundinc = inner_qualify(name, inc.parametrized_tag.to_s, skip)
possibles.push foundinc unless foundinc.nil?
end
end
return name if store.namespace_exists?(name)
possibles.last
end
end
# @param fqns [String]
# @param visibility [Array<Symbol>]
# @param skip [Set<String>]
# @return [Array<Pin::Base>]
def inner_get_constants fqns, visibility, skip
return [] if fqns.nil? || skip.include?(fqns)
skip.add fqns
result = []
store.get_prepends(fqns).each do |pre|
pre_fqns = resolve(pre.name, pre.closure.gates - skip.to_a)
result.concat inner_get_constants(pre_fqns, [:public], skip)
end
result.concat(store.get_constants(fqns, visibility).sort { |a, b| a.name <=> b.name })
store.get_includes(fqns).each do |pin|
inc_fqns = resolve(pin.name, pin.closure.gates - skip.to_a)
result.concat inner_get_constants(inc_fqns, [:public], skip)
end
sc_ref = store.get_superclass(fqns)
if sc_ref
fqsc = dereference(sc_ref)
result.concat inner_get_constants(fqsc, [:public], skip) unless %w[Object BasicObject].include?(fqsc)
end
result
end
end
end
end
# frozen_string_literal: true
module Solargraph
module Convention
# ActiveSupport::Concern is syntactic sugar for a common
# pattern to include class methods while mixing-in a Module
# See https://api.rubyonrails.org/classes/ActiveSupport/Concern.html
class ActiveSupportConcern < Base
include Logging
# @return [Array<Pin::Base>]
attr_reader :pins
# @param api_map [ApiMap]
# @param rooted_tag [String]
# @param scope [Symbol] :class or :instance
# @param visibility [Array<Symbol>] :public, :protected, and/or :private
# @param deep [Boolean] whether to include methods from included modules
# @param skip [Set<String>]
# @param _no_core [Boolean]n whether to skip core methods
def object api_map, rooted_tag, scope, visibility, deep, skip, _no_core
moo = ObjectProcessor.new(api_map, rooted_tag, scope, visibility, deep, skip)
moo.environ
end
# yard-activesupport-concern pulls methods inside
# 'class_methods' blocks into main class visible from YARD
#
# @param _doc_map [DocMap]
def global _doc_map
Environ.new(yard_plugins: ['activesupport-concern'])
end
# Process an object to add any class methods brought in via
# ActiveSupport::Concern
class ObjectProcessor
include Logging
attr_reader :environ
# @param api_map [ApiMap]
# @param rooted_tag [String] the tag of the class or module being processed
# @param scope [Symbol] :class or :instance
# @param visibility [Array<Symbol>] :public, :protected, and/or :private
# @param deep [Boolean] whether to include methods from included modules
# @param skip [Set<String>] a set of tags to skip
def initialize api_map, rooted_tag, scope, visibility, deep, skip
@api_map = api_map
@rooted_tag = rooted_tag
@scope = scope
@visibility = visibility
@deep = deep
@skip = skip
@environ = Environ.new
return unless scope == :class
@rooted_type = ComplexType.parse(rooted_tag).force_rooted
@fqns = rooted_type.namespace
@namespace_pin = api_map.get_path_pins(fqns).select { |p| p.is_a?(Pin::Namespace) }.first
api_map.get_includes(fqns).reverse.each do |include_tag|
process_include include_tag
end
end
private
attr_reader :api_map, :rooted_tag, :rooted_type, :scope,
:visibility, :deep, :skip, :namespace_pin,
:fqns
# @param include_tag [Pin::Reference::Include] the include reference pin
#
# @return [void]
def process_include include_tag
rooted_include_tag = api_map.dereference(include_tag)
return if rooted_include_tag.nil?
logger.debug do
"ActiveSupportConcern#object(#{fqns}, #{scope}, #{visibility}, #{deep}) - " \
"Handling class include include_tag=#{include_tag}"
end
module_extends = api_map.get_extends(rooted_include_tag).map(&:parametrized_tag).map(&:to_s)
logger.debug do
"ActiveSupportConcern#object(#{fqns}, #{scope}, #{visibility}, #{deep}) - " \
"found module extends of #{rooted_include_tag}: #{module_extends}"
end
return unless module_extends.include? 'ActiveSupport::Concern'
included_class_pins = api_map.inner_get_methods_from_reference(rooted_include_tag, namespace_pin, rooted_type,
:class, visibility, deep, skip, true)
logger.debug do
"ActiveSupportConcern#object(#{fqns}, #{scope}, #{visibility}, #{deep}) - " \
"Found #{included_class_pins.length} inluded class methods for #{rooted_include_tag}"
end
environ.pins.concat included_class_pins
# another pattern is to put class methods inside a submodule
classmethods_include_tag = "#{rooted_include_tag}::ClassMethods"
included_classmethods_pins =
api_map.inner_get_methods_from_reference(classmethods_include_tag, namespace_pin, rooted_type,
:instance, visibility, deep, skip, true)
logger.debug do
"ActiveSupportConcern#object(#{fqns}, #{scope}, #{visibility}, #{deep}) - " \
"Found #{included_classmethods_pins.length} included classmethod " \
"class methods for #{classmethods_include_tag}"
end
environ.pins.concat included_classmethods_pins
end
end
end
end
end
# frozen_string_literal: true
require 'open3'
module Solargraph
# A workspace consists of the files in a project's directory and the
# project's configuration. It provides a Source for each file to be used
# in an associated Library or ApiMap.
#
class Workspace
# Manages determining which gemspecs are available in a workspace
class RequirePaths
attr_reader :directory, :config
# @param directory [String, nil]
# @param config [Config, nil]
def initialize directory, config
@directory = directory
@config = config
end
# Generate require paths from gemspecs if they exist or assume the default
# lib directory.
#
# @return [Array<String>]
def generate
result = require_paths_from_gemspec_files
return configured_require_paths if result.empty?
result.concat(config.require_paths.map { |p| File.join(directory, p) }) if config
result
end
private
# @return [Array<String>]
def require_paths_from_gemspec_files
results = []
gemspec_file_paths.each do |gemspec_file_path|
results.concat require_path_from_gemspec_file(gemspec_file_path)
end
results
end
# Get an array of all gemspec files in the workspace.
#
# @return [Array<String>]
def gemspec_file_paths
return [] if directory.nil?
@gemspec_file_paths ||= Dir[File.join(directory, '**/*.gemspec')].select do |gs|
config.nil? || config.allow?(gs)
end
end
# Get additional require paths defined in the configuration.
#
# @return [Array<String>]
def configured_require_paths
return ['lib'] unless directory
return [File.join(directory, 'lib')] if !config || config.require_paths.empty?
config.require_paths.map { |p| File.join(directory, p) }
end
# Generate require paths from gemspecs if they exist or assume the default
# lib directory.
#
# @param gemspec_file_path [String]
# @return [Array<String>]
def require_path_from_gemspec_file gemspec_file_path
base = File.dirname(gemspec_file_path)
# HACK: Evaluating gemspec files violates the goal of not running
# workspace code, but this is how Gem::Specification.load does it
# anyway.
cmd = ['ruby', '-e',
"require 'rubygems'; " \
"require 'json'; " \
"spec = eval(File.read('#{gemspec_file_path}'), TOPLEVEL_BINDING, '#{gemspec_file_path}'); " \
'return unless Gem::Specification === spec; ' \
'puts({name: spec.name, paths: spec.require_paths}.to_json)']
# @sg-ignore Unresolved call to capture3 on Module<Open3>
o, e, s = Open3.capture3(*cmd)
if s.success?
begin
hash = o && !o.empty? ? JSON.parse(o.split("\n").last) : {}
return [] if hash.empty?
hash['paths'].map { |path| File.join(base, path) }
rescue StandardError => e
Solargraph.logger.warn "Error reading #{gemspec_file_path}: [#{e.class}] #{e.message}"
[]
end
else
Solargraph.logger.warn "Error reading #{gemspec_file_path}"
Solargraph.logger.warn e
[]
end
end
end
end
end
# frozen_string_literal: true
module Parser
##
# Default AST builder. Uses {AST::Node}s.
#
module Builders
class Default
##
# AST compatibility attribute; since `-> {}` is not semantically
# equivalent to `lambda {}`, all new code should set this attribute
# to true.
#
# If set to false (the default), `-> {}` is emitted as
# `s(:block, s(:send, nil, :lambda), s(:args), nil)`.
#
# If set to true, `-> {}` is emitted as
# `s(:block, s(:lambda), s(:args), nil)`.
#
# @return [Boolean]
attr_accessor self.emit_lambda: bool
##
# AST compatibility attribute; block arguments of `m { |a| }` are
# not semantically equivalent to block arguments of `m { |a,| }` or `m { |a, b| }`,
# all new code should set this attribute to true.
#
# If set to false (the default), arguments of `m { |a| }` are emitted as
# `s(:args, s(:arg, :a))`.
#
# If set to true, arguments of `m { |a| }` are emitted as
# `s(:args, s(:procarg0, :a)).
#
# @return [Boolean]
attr_accessor self.emit_procarg0: bool
##
# AST compatibility attribute; locations of `__ENCODING__` are not the same
# as locations of `Encoding::UTF_8` causing problems during rewriting,
# all new code should set this attribute to true.
#
# If set to false (the default), `__ENCODING__` is emitted as
# ` s(:const, s(:const, nil, :Encoding), :UTF_8)`.
#
# If set to true, `__ENCODING__` is emitted as
# `s(:__ENCODING__)`.
#
# @return [Boolean]
attr_accessor self.emit_encoding: bool
##
# AST compatibility attribute; indexed assignment, `x[] = 1`, is not
# semantically equivalent to calling the method directly, `x.[]=(1)`.
# Specifically, in the former case, the expression's value is always 1,
# and in the latter case, the expression's value is the return value
# of the `[]=` method.
#
# If set to false (the default), `self[1]` is emitted as
# `s(:send, s(:self), :[], s(:int, 1))`, and `self[1] = 2` is
# emitted as `s(:send, s(:self), :[]=, s(:int, 1), s(:int, 2))`.
#
# If set to true, `self[1]` is emitted as
# `s(:index, s(:self), s(:int, 1))`, and `self[1] = 2` is
# emitted as `s(:indexasgn, s(:self), s(:int, 1), s(:int, 2))`.
#
# @return [Boolean]
attr_accessor self.emit_index: bool
##
# AST compatibility attribute; causes a single non-mlhs
# block argument to be wrapped in s(:procarg0).
#
# If set to false (the default), block arguments `|a|` are emitted as
# `s(:args, s(:procarg0, :a))`
#
# If set to true, block arguments `|a|` are emitted as
# `s(:args, s(:procarg0, s(:arg, :a))`
#
# @return [Boolean]
attr_accessor self.emit_arg_inside_procarg0: bool
##
# AST compatibility attribute; arguments forwarding initially
# didn't have support for leading arguments
# (i.e. `def m(a, ...); end` was a syntax error). However, Ruby 3.0
# added support for any number of arguments in front of the `...`.
#
# If set to false (the default):
# 1. `def m(...) end` is emitted as
# s(:def, :m, s(:forward_args), nil)
# 2. `def m(a, b, ...) end` is emitted as
# s(:def, :m,
# s(:args, s(:arg, :a), s(:arg, :b), s(:forward_arg)))
#
# If set to true it uses a single format:
# 1. `def m(...) end` is emitted as
# s(:def, :m, s(:args, s(:forward_arg)))
# 2. `def m(a, b, ...) end` is emitted as
# s(:def, :m, s(:args, s(:arg, :a), s(:arg, :b), s(:forward_arg)))
#
# It does't matter that much on 2.7 (because there can't be any leading arguments),
# but on 3.0 it should be better enabled to use a single AST format.
#
# @return [Boolean]
attr_accessor self.emit_forward_arg: bool
##
# AST compatibility attribute; Starting from Ruby 2.7 keyword arguments
# of method calls that are passed explicitly as a hash (i.e. with curly braces)
# are treated as positional arguments and Ruby 2.7 emits a warning on such method
# call. Ruby 3.0 given an ArgumentError.
#
# If set to false (the default) the last hash argument is emitted as `hash`:
#
# ```
# (send nil :foo
# (hash
# (pair
# (sym :bar)
# (int 42))))
# ```
#
# If set to true it is emitted as `kwargs`:
#
# ```
# (send nil :foo
# (kwargs
# (pair
# (sym :bar)
# (int 42))))
# ```
#
# Note that `kwargs` node is just a replacement for `hash` argument,
# so if there's are multiple arguments (or a `kwsplat`) all of them
# are wrapped into `kwargs` instead of `hash`:
#
# ```
# (send nil :foo
# (kwargs
# (pair
# (sym :a)
# (int 42))
# (kwsplat
# (send nil :b))
# (pair
# (sym :c)
# (int 10))))
# ```
attr_accessor self.emit_kwargs: bool
##
# AST compatibility attribute; Starting from 3.0 Ruby returns
# true/false from single-line pattern matching with `in` keyword.
#
# Before 3.0 there was an exception if given value doesn't match pattern.
#
# NOTE: This attribute affects only Ruby 2.7 grammar.
# 3.0 grammar always emits `match_pattern`/`match_pattern_p`
#
# If compatibility attribute set to false `foo in bar` is emitted as `in_match`:
#
# ```
# (in-match
# (send nil :foo)
# (match-var :bar))
# ```
#
# If set to true it's emitted as `match_pattern_p`:
# ```
# (match-pattern-p
# (send nil :foo)
# (match-var :bar))
# ```
attr_accessor self.emit_match_pattern: bool
##
# If set to true (the default), `__FILE__` and `__LINE__` are transformed to
# literal nodes. For example, `s(:str, "lib/foo.rb")` and `s(:int, 10)`.
#
# If set to false, `__FILE__` and `__LINE__` are emitted as-is,
# i.e. as `s(:__FILE__)` and `s(:__LINE__)` nodes.
#
# Source maps are identical in both cases.
#
# @return [Boolean]
attr_accessor emit_file_line_as_literals: bool
def value: (untyped token) -> untyped
def string_value: (untyped token) -> String
def loc: (untyped token) -> untyped
end
end
end
---
name: thor
version: '1.2'
source:
type: git
name: ruby/gem_rbs_collection
revision: 98541aabafdf403b16ebae6fe4060d18bee75e93
remote: https://github.com/ruby/gem_rbs_collection.git
repo_dir: gems
# manifest.yaml describes dependencies which do not appear in the gemspec.
# If this gem includes such dependencies, comment-out the following lines and
# declare the dependencies.
# If all dependencies appear in the gemspec, you should remove this file.
#
# dependencies:
# - name: pathname
class Thor
class Group
end
module Actions
class CreateFile
end
def create_file: (String destination, String data, ?verbose: bool) -> String
| (String destination, ?verbose: bool) { () -> String } -> String
end
class Error
end
def self.start: (Array[String] given_args, ?Hash[Symbol, untyped] config) -> void
end
+148
-6

@@ -1,8 +0,13 @@

name: Plugin Backwards Compatibility Tests
---
name: Plugin
# To debug locally:
# npm install -g act
# cd /Users/broz/src/solargraph/ && act pull_request -j run_solargraph_rails_specs # e.g.
on:
push:
branches: [ master ]
branches: [master]
pull_request:
branches: [ master ]
branches: [master]

@@ -13,3 +18,3 @@ permissions:

jobs:
test:
regression:
runs-on: ubuntu-latest

@@ -23,3 +28,3 @@

ruby-version: '3.0'
bundler-cache: false
bundler-cache: true
- uses: awalsh128/cache-apt-pkgs-action@latest

@@ -34,2 +39,3 @@ with:

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

@@ -41,3 +47,3 @@ run: |

- name: Install gem types
run: bundle exec rbs collection install
run: bundle exec rbs collection update
- name: Ensure typechecking still works

@@ -47,1 +53,137 @@ run: bundle exec solargraph typecheck --level typed

run: bundle exec rake spec
rails:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.0'
bundler-cache: false
- uses: awalsh128/cache-apt-pkgs-action@latest
with:
packages: yq
version: 1.0
- name: Install gems
run: |
echo 'gem "solargraph-rails"' > .Gemfile
bundle install
bundle update rbs
- name: Configure to use plugins
run: |
bundle exec solargraph config
yq -yi '.plugins += ["solargraph-rails"]' .solargraph.yml
- name: Install gem types
run: bundle exec rbs collection update
- name: Ensure typechecking still works
run: bundle exec solargraph typecheck --level typed
- name: Ensure specs still run
run: bundle exec rake spec
rspec:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.0'
bundler-cache: false
- uses: awalsh128/cache-apt-pkgs-action@latest
with:
packages: yq
version: 1.0
- name: Install gems
run: |
echo 'gem "solargraph-rspec"' >> .Gemfile
bundle install
bundle update rbs
- name: Configure to use plugins
run: |
bundle exec solargraph config
yq -yi '.plugins += ["solargraph-rspec"]' .solargraph.yml
- name: Install gem types
run: bundle exec rbs collection update
- name: Ensure typechecking still works
run: bundle exec solargraph typecheck --level typed
- name: Ensure specs still run
run: bundle exec rake spec
# run_solargraph_rspec_specs:
# # check out solargraph-rspec as well as this project, and point the former to use the latter as a local gem
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v3
# - name: clone https://github.com/lekemula/solargraph-rspec/
# run: |
# cd ..
# git clone https://github.com/lekemula/solargraph-rspec.git
# cd solargraph-rspec
# - name: Set up Ruby
# uses: ruby/setup-ruby@v1
# with:
# ruby-version: '3.0'
# bundler-cache: false
# - name: Install gems
# run: |
# cd ../solargraph-rspec
# echo "gem 'solargraph', path: '../solargraph'" >> Gemfile
# bundle install
# - name: Run specs
# run: |
# cd ../solargraph-rspec
# bundle exec rake spec
run_solargraph_rails_specs:
# check out solargraph-rails as well as this project, and point the former to use the latter as a local gem
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: clone solargraph-rails
run: |
cd ..
git clone https://github.com/iftheshoefritz/solargraph-rails.git
cd solargraph-rails
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
# solargraph-rails supports Ruby 3.0+
ruby-version: '3.0'
bundler-cache: false
bundler: latest
env:
MATRIX_RAILS_VERSION: "7.0"
- name: Install gems
run: |
set -x
BUNDLE_PATH="${GITHUB_WORKSPACE:?}/vendor/bundle"
export BUNDLE_PATH
cd ../solargraph-rails
echo "gem 'solargraph', path: '${GITHUB_WORKSPACE:?}'" >> Gemfile
bundle install
bundle update rbs
RAILS_DIR="$(pwd)/spec/rails7"
export RAILS_DIR
cd ${RAILS_DIR}
bundle install
bundle exec --gemfile ../../Gemfile rbs --version
bundle exec --gemfile ../../Gemfile rbs collection install
cd ../../
# bundle exec rbs collection init
# bundle exec rbs collection install
env:
MATRIX_RAILS_VERSION: "7.0"
MATRIX_RAILS_MAJOR_VERSION: '7'
- name: Run specs
run: |
BUNDLE_PATH="${GITHUB_WORKSPACE:?}/vendor/bundle"
export BUNDLE_PATH
cd ../solargraph-rails
bundle exec solargraph --version
bundle info solargraph
bundle info rbs
bundle info yard
ALLOW_IMPROVEMENTS=true bundle exec rake spec
env:
MATRIX_RAILS_VERSION: "7.0"

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

jobs:
test:
rspec:
runs-on: ubuntu-latest

@@ -27,3 +26,9 @@ strategy:

ruby-version: ['3.0', '3.1', '3.2', '3.3', '3.4', 'head']
rbs-version: ['3.6.1', '3.9.4', '4.0.0.dev.4']
# Ruby 3.0 doesn't work with RBS 3.9.4 or 4.0.0.dev.4
exclude:
- ruby-version: '3.0'
rbs-version: '3.9.4'
- ruby-version: '3.0'
rbs-version: '4.0.0.dev.4'
steps:

@@ -36,5 +41,35 @@ - uses: actions/checkout@v3

bundler-cache: false
- name: Set rbs version
run: echo "gem 'rbs', '${{ matrix.rbs-version }}'" >> .Gemfile
# /home/runner/.rubies/ruby-head/lib/ruby/gems/3.5.0+2/gems/rbs-3.9.4/lib/rbs.rb:11:
# warning: tsort was loaded from the standard library,
# but will no longer be part of the default gems
# starting from Ruby 3.6.0
- name: Work around legacy rbs deprecation on ruby > 3.4
run: echo "gem 'tsort'" >> .Gemfile
- name: Install gems
run: |
bundle install
bundle update rbs # use latest available for this Ruby version
- name: Run tests
run: bundle exec rake spec
undercover:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
# fetch all history for all branches and tags so we can
# compare against origin/master
fetch-depth: 0
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.4'
bundler-cache: false
- name: Install gems
run: bundle install
- name: Run tests
run: bundle exec rspec
run: bundle exec rake spec
- name: Check PR coverage
run: bundle exec rake undercover
continue-on-error: true
+5
-2

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

jobs:
test:
solargraph_typed:
name: Solargraph / typed

@@ -33,3 +34,5 @@ runs-on: ubuntu-latest

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

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

@@ -12,1 +12,6 @@ /.gem_rbs_collection

/tmp/
/rspec-examples.txt
/.ruby-version
/Makefile
/.pryrc
/.rspec-local

@@ -0,1 +1,43 @@

## 0.57.0 - September 16, 2025
- Support ActiveSupport::Concern pattern for class methods (#948)
- More CI checks (#996)
- Linting / type annotation fixes (#999)
- Avoid overlapping chdir calls in multiple threads (#1007)
- Fix kwarg generation in ApiMap::SourceToYard (#1003)
- Enable Steep typechecking of Solargraph code (#1004)
- Fix convention requires (#1008)
- Plugin Util: Add Combination Priority (#1010)
- [regression] Fix crash while typechecking files with Struct use (#1031)
- Remove yard reference from gemfile (#1033)
- Allow newer RBS gem versions, exclude incompatible ones (#995)
- Look for external requires before cataloging bench (#1021)
- Remove Library#folding_ranges (#904)
- Complain in strong type-checking if an @sg-ignore line is not needed (#1011)
- Document a log level env variable (#894)
- Fix hole in type checking evaluation (#1009)
- Improve typechecking error message (#1014)
- Internal strict type-checking fixes (#1013)
- Reproduce and fix a ||= (or-asgn) evaluation issue (#1017)
- Define closure for Pin::Symbol, for completeness (#1027)
- Fix 'all!' config to reporters (#1018)
- Fix DocMap.all_rbs_collection_gems_in_memory return type (#1037)
- Fix RuboCop linting errors in regular expressions (#1038)
- Resolve class aliases via Constant pins (#1029)
- Speed-up LSP completion response times (#1035)
- Revert "Resolve class aliases via Constant pins (#1029)" (#1041)
- Avoid stack errors when resolving method aliases (#1040)
- [regression] Refine order of object convention method pins (#1036)
- Fix crash while generating activesupport pins (#1043)
- Type annotation improvements (#1016)
- Resolve class aliases via Constant pins (#1048)
- Understand "Parser::AST::Node < AST::Node" in RBS (#1060)
- Factor out require_paths logic to its own class (#1062)
- Fix type errors found in strong typechecking (#1045)
- Run plugin specs separately for perf insights (#1046)
- Run specs from solargraph-rails configured against current code in CI (#892)
- Fix Convention/Plugin pins not being updated on file change (#1028)
- Fix solargraph-rails check (#1073)
- Flow sensitive typing handles x.is_a? without an argument (#1070)
- Refactor reference pin handling (#1058)
## 0.56.2 - July 29, 2025

@@ -2,0 +44,0 @@ - Add support for Ruby Data.define (#970)

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

CHDIR_MUTEX = Mutex.new
# @param type [Symbol] Type of assert.

@@ -58,0 +60,0 @@ def self.asserts_on?(type)

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

autoload :Index, 'solargraph/api_map/index'
autoload :Constants, 'solargraph/api_map/constants'

@@ -30,3 +31,2 @@ # @return [Array<String>]

@cache = Cache.new
@method_alias_stack = []
index pins

@@ -41,2 +41,3 @@ end

# @param other [Object]
def eql?(other)

@@ -47,2 +48,3 @@ self.class == other.class &&

# @param other [Object]
def ==(other)

@@ -71,3 +73,3 @@ self.eql?(other)

@source_map_hash = {}
implicit.clear
conventions_environ.clear
cache.clear

@@ -81,6 +83,7 @@ store.update @@core_map.pins, pins

# @param source [Source]
# @param live [Boolean] True for live source map (active editor file)
# @return [self]
def map source
def map source, live: false
map = Solargraph::SourceMap.map(source)
catalog Bench.new(source_maps: [map])
catalog Bench.new(source_maps: [map], live_map: live ? map : nil)
self

@@ -96,8 +99,8 @@ end

iced_pins = bench.icebox.flat_map(&:pins)
live_pins = bench.live_map&.pins || []
implicit.clear
live_pins = bench.live_map&.all_pins || []
conventions_environ.clear
source_map_hash.each_value do |map|
implicit.merge map.environ
conventions_environ.merge map.conventions_environ
end
unresolved_requires = (bench.external_requires + implicit.requires + bench.workspace.config.required).to_a.compact.uniq
unresolved_requires = (bench.external_requires + conventions_environ.requires + bench.workspace.config.required).to_a.compact.uniq
recreate_docmap = @unresolved_requires != unresolved_requires ||

@@ -111,3 +114,3 @@ @doc_map&.uncached_yard_gemspecs&.any? ||

end
@cache.clear if store.update(@@core_map.pins, @doc_map.pins, implicit.pins, iced_pins, live_pins)
@cache.clear if store.update(@@core_map.pins, @doc_map.pins, conventions_environ.pins, iced_pins, live_pins)
@missing_docs = [] # @todo Implement missing docs

@@ -121,5 +124,6 @@ self

protected def equality_fields
[self.class, @source_map_hash, implicit, @doc_map, @unresolved_requires]
[self.class, @source_map_hash, conventions_environ, @doc_map, @unresolved_requires]
end
# @return [DocMap]
def doc_map

@@ -144,3 +148,3 @@ @doc_map ||= DocMap.new([], [])

# @return [Array<Pin::Base>]
# @return [Enumerable<Pin::Base>]
def core_pins

@@ -162,4 +166,4 @@ @@core_map.pins

# @return [Environ]
def implicit
@implicit ||= Environ.new
def conventions_environ
@conventions_environ ||= Environ.new
end

@@ -200,2 +204,4 @@

# @param out [IO, nil]
# @return [void]
def cache_all!(out)

@@ -205,2 +211,6 @@ @doc_map.cache_all!(out)

# @param gemspec [Gem::Specification]
# @param rebuild [Boolean]
# @param out [IO, nil]
# @return [void]
def cache_gem(gemspec, rebuild: false, out: nil)

@@ -218,5 +228,2 @@ @doc_map.cache(gemspec, rebuild: rebuild, out: out)

#
# @todo IO::NULL is incorrectly inferred to be a String.
# @sg-ignore
#
# @param directory [String]

@@ -272,15 +279,9 @@ # @param out [IO] The output stream for messages

namespace ||= ''
contexts.push '' if contexts.empty?
cached = cache.get_constants(namespace, contexts)
return cached.clone unless cached.nil?
skip = Set.new
result = []
contexts.each do |context|
fqns = qualify(namespace, context)
visibility = [:public]
visibility.push :private if fqns == context
result.concat inner_get_constants(fqns, visibility, skip)
end
cache.set_constants(namespace, contexts, result)
result
gates = contexts.clone
gates.push '' if contexts.empty? && namespace.empty?
gates.push namespace unless namespace.empty?
store.constants
.collect(gates)
.select { |pin| namespace.empty? || contexts.empty? || pin.namespace == namespace }
.select { |pin| pin.visibility == :public || pin.namespace == namespace }
end

@@ -311,40 +312,23 @@

def qualify tag, context_tag = ''
return tag if ['Boolean', 'self', nil].include?(tag)
store.constants.qualify(tag, context_tag)
end
context_type = ComplexType.try_parse(context_tag).force_rooted
return unless context_type
# Get a fully qualified namespace from a reference pin.
#
# @param pin [Pin::Reference]
# @return [String, nil]
def dereference(pin)
store.constants.dereference(pin)
end
type = ComplexType.try_parse(tag)
return unless type
return tag if type.literal?
context_type = ComplexType.try_parse(context_tag)
return unless context_type
fqns = qualify_namespace(type.rooted_namespace, context_type.rooted_namespace)
return unless fqns
fqns + type.substring
# @param fqns [String]
# @return [Array<String>]
def get_extends(fqns)
store.get_extends(fqns)
end
# Determine fully qualified namespace for a given namespace used
# inside the definition of another tag ("context"). This method
# will start the search in the specified context until it finds a
# match for the namespace.
#
# @param namespace [String, nil] The namespace to
# match
# @param context_namespace [String] The context namespace in which the
# tag was referenced; start from here to resolve the name
# @return [String, nil] fully qualified namespace
def qualify_namespace(namespace, context_namespace = '')
cached = cache.get_qualified_namespace(namespace, context_namespace)
return cached.clone unless cached.nil?
result = if namespace.start_with?('::')
inner_qualify(namespace[2..-1], '', Set.new)
else
inner_qualify(namespace, context_namespace, Set.new)
end
cache.set_qualified_namespace(namespace, context_namespace, result)
result
# @param fqns [String]
# @return [Array<String>]
def get_includes(fqns)
store.get_includes(fqns)
end

@@ -362,7 +346,6 @@

result.concat store.get_instance_variables(namespace, scope)
sc = qualify_lower(store.get_superclass(namespace), namespace)
until sc.nil? || used.include?(sc)
used.push sc
result.concat store.get_instance_variables(sc, scope)
sc = qualify_lower(store.get_superclass(sc), sc)
sc_fqns = namespace
while (sc = store.get_superclass(sc_fqns))
sc_fqns = store.constants.dereference(sc)
result.concat store.get_instance_variables(sc_fqns, scope)
end

@@ -372,2 +355,3 @@ result

# @sg-ignore Missing @return tag for Solargraph::ApiMap#visible_pins
# @see Solargraph::Parser::FlowSensitiveTyping#visible_pins

@@ -424,3 +408,3 @@ def visible_pins(*args, **kwargs, &blk)

# @todo Implement domains
implicit.domains.each do |domain|
conventions_environ.domains.each do |domain|
type = ComplexType.try_parse(domain)

@@ -536,2 +520,4 @@ next if type.undefined?

# @param scope [Symbol] :instance or :class
# @param visibility [Array<Symbol>] :public, :protected, and/or :private
# @param preserve_generics [Boolean]
# @return [Array<Solargraph::Pin::Method>]

@@ -541,4 +527,14 @@ def get_method_stack rooted_tag, name, scope: :instance, visibility: [:private, :protected, :public], preserve_generics: false

fqns = rooted_type.namespace
namespace_pin = store.get_path_pins(fqns).select { |p| p.is_a?(Pin::Namespace) }.first
methods = get_methods(rooted_tag, scope: scope, visibility: visibility).select { |p| p.name == name }
namespace_pin = store.get_path_pins(fqns).first
methods = if namespace_pin.is_a?(Pin::Constant)
type = namespace_pin.infer(self)
if type.defined?
namespace_pin = store.get_path_pins(type.namespace).first
get_methods(type.namespace, scope: scope, visibility: visibility).select { |p| p.name == name }
else
[]
end
else
get_methods(rooted_tag, scope: scope, visibility: visibility).select { |p| p.name == name }
end
methods = erase_generics(namespace_pin, rooted_type, methods) unless preserve_generics

@@ -653,9 +649,18 @@ methods

def super_and_sub?(sup, sub)
fqsup = qualify(sup)
cls = qualify(sub)
tested = []
until fqsup.nil? || cls.nil? || tested.include?(cls)
return true if cls == fqsup
tested.push cls
cls = qualify_superclass(cls)
sup = ComplexType.try_parse(sup)
sub = ComplexType.try_parse(sub)
# @todo If two literals are different values of the same type, it would
# make more sense for super_and_sub? to return true, but there are a
# few callers that currently expect this to be false.
return false if sup.literal? && sub.literal? && sup.to_s != sub.to_s
sup = sup.simplify_literals.to_s
sub = sub.simplify_literals.to_s
return true if sup == sub
sc_fqns = sub
while (sc = store.get_superclass(sc_fqns))
sc_new = store.constants.dereference(sc)
# Cyclical inheritance is invalid
return false if sc_new == sc_fqns
sc_fqns = sc_new
return true if sc_fqns == sup
end

@@ -673,3 +678,3 @@ false

def type_include?(host_ns, module_ns)
store.get_includes(host_ns).map { |inc_tag| ComplexType.parse(inc_tag).name }.include?(module_ns)
store.get_includes(host_ns).map { |inc_tag| inc_tag.parametrized_tag.name }.include?(module_ns)
end

@@ -682,2 +687,3 @@

with_resolved_aliases = pins.map do |pin|
next pin unless pin.is_a?(Pin::MethodAlias)
resolved = resolve_method_alias(pin)

@@ -691,2 +697,37 @@ next nil if resolved.respond_to?(:visibility) && !visibility.include?(resolved.visibility)

# @param fq_reference_tag [String] A fully qualified whose method should be pulled in
# @param namespace_pin [Pin::Base] Namespace pin for the rooted_type
# parameter - used to pull generics information
# @param type [ComplexType] The type which is having its
# methods supplemented from fq_reference_tag
# @param scope [Symbol] :class or :instance
# @param visibility [Array<Symbol>] :public, :protected, and/or :private
# @param deep [Boolean]
# @param skip [Set<String>]
# @param no_core [Boolean] Skip core classes if true
# @return [Array<Pin::Base>]
def inner_get_methods_from_reference(fq_reference_tag, namespace_pin, type, scope, visibility, deep, skip, no_core)
logger.debug { "ApiMap#add_methods_from_reference(type=#{type}) starting" }
# Ensure the types returned by the methods in the referenced
# type are relative to the generic values passed in the
# reference. e.g., Foo<String> might include Enumerable<String>
#
# @todo perform the same translation in the other areas
# here after adding a spec and handling things correctly
# in ApiMap::Store and RbsMap::Conversions for each
resolved_reference_type = ComplexType.parse(fq_reference_tag).force_rooted.resolve_generics(namespace_pin, type)
# @todo Can inner_get_methods be cached? Lots of lookups of base types going on.
methods = inner_get_methods(resolved_reference_type.tag, scope, visibility, deep, skip, no_core)
if namespace_pin && !resolved_reference_type.all_params.empty?
reference_pin = store.get_path_pins(resolved_reference_type.name).select { |p| p.is_a?(Pin::Namespace) }.first
# logger.debug { "ApiMap#add_methods_from_reference(type=#{type}) - resolving generics with #{reference_pin.generics}, #{resolved_reference_type.rooted_tags}" }
methods = methods.map do |method_pin|
method_pin.resolve_generics(reference_pin, resolved_reference_type)
end
end
# logger.debug { "ApiMap#add_methods_from_reference(type=#{type}) - resolved_reference_type: #{resolved_reference_type} for type=#{type}: #{methods.map(&:name)}" }
methods
end
private

@@ -715,2 +756,3 @@

# @return [Array<Pin::Base>]
# rubocop:disable Metrics/CyclomaticComplexity
def inner_get_methods rooted_tag, scope, visibility, deep, skip, no_core = false

@@ -726,5 +768,12 @@ rooted_type = ComplexType.parse(rooted_tag).force_rooted

result = []
environ = Convention.for_object(self, rooted_tag, scope, visibility, deep, skip, no_core)
# ensure we start out with any immediate methods in this
# namespace so we roughly match the same ordering of get_methods
# and obey the 'deep' instruction
direct_convention_methods, convention_methods_by_reference = environ.pins.partition { |p| p.namespace == rooted_tag }
result.concat direct_convention_methods
if deep && scope == :instance
store.get_prepends(fqns).reverse.each do |im|
fqim = qualify(im, fqns)
fqim = store.constants.dereference(im)
result.concat inner_get_methods(fqim, scope, visibility, deep, skip, true) unless fqim.nil?

@@ -737,8 +786,20 @@ end

methods = store.get_methods(fqns, scope: scope, visibility: visibility).sort{ |a, b| a.name <=> b.name }
logger.info { "ApiMap#inner_get_methods(rooted_tag=#{rooted_tag.inspect}, scope=#{scope.inspect}, visibility=#{visibility.inspect}, deep=#{deep.inspect}, skip=#{skip.inspect}, fqns=#{fqns}) - added from store: #{methods}" }
result.concat methods
if deep
result.concat convention_methods_by_reference
if scope == :instance
store.get_includes(fqns).reverse.each do |include_tag|
rooted_include_tag = qualify(include_tag, rooted_tag)
result.concat inner_get_methods_from_reference(rooted_include_tag, namespace_pin, rooted_type, scope, visibility, deep, skip, true)
store.get_includes(fqns).reverse.each do |ref|
const = get_constants('', *ref.closure.gates).find { |pin| pin.path.end_with? ref.name }
if const.is_a?(Pin::Namespace)
result.concat inner_get_methods(const.path, scope, visibility, deep, skip, true)
elsif const.is_a?(Pin::Constant)
type = const.infer(self)
result.concat inner_get_methods(type.namespace, scope, visibility, deep, skip, true) if type.defined?
else
referenced_tag = ref.parametrized_tag
next unless referenced_tag.defined?
result.concat inner_get_methods_from_reference(referenced_tag.to_s, namespace_pin, rooted_type, scope, visibility, deep, skip, true)
end
end

@@ -750,4 +811,5 @@ rooted_sc_tag = qualify_superclass(rooted_tag)

else
logger.info { "ApiMap#inner_get_methods(#{fqns}, #{scope}, #{visibility}, #{deep}, #{skip}) - looking for get_extends() from #{fqns}" }
store.get_extends(fqns).reverse.each do |em|
fqem = qualify(em, fqns)
fqem = store.constants.dereference(em)
result.concat inner_get_methods(fqem, :instance, visibility, deep, skip, true) unless fqem.nil?

@@ -772,61 +834,4 @@ end

end
# rubocop:enable Metrics/CyclomaticComplexity
# @param fq_reference_tag [String] A fully qualified whose method should be pulled in
# @param namespace_pin [Pin::Base] Namespace pin for the rooted_type
# parameter - used to pull generics information
# @param type [ComplexType] The type which is having its
# methods supplemented from fq_reference_tag
# @param scope [Symbol] :class or :instance
# @param visibility [Array<Symbol>] :public, :protected, and/or :private
# @param deep [Boolean]
# @param skip [Set<String>]
# @param no_core [Boolean] Skip core classes if true
# @return [Array<Pin::Base>]
def inner_get_methods_from_reference(fq_reference_tag, namespace_pin, type, scope, visibility, deep, skip, no_core)
# logger.debug { "ApiMap#add_methods_from_reference(type=#{type}) starting" }
# Ensure the types returned by the methods in the referenced
# type are relative to the generic values passed in the
# reference. e.g., Foo<String> might include Enumerable<String>
#
# @todo perform the same translation in the other areas
# here after adding a spec and handling things correctly
# in ApiMap::Store and RbsMap::Conversions for each
resolved_reference_type = ComplexType.parse(fq_reference_tag).force_rooted.resolve_generics(namespace_pin, type)
# @todo Can inner_get_methods be cached? Lots of lookups of base types going on.
methods = inner_get_methods(resolved_reference_type.tag, scope, visibility, deep, skip, no_core)
if namespace_pin && !resolved_reference_type.all_params.empty?
reference_pin = store.get_path_pins(resolved_reference_type.name).select { |p| p.is_a?(Pin::Namespace) }.first
# logger.debug { "ApiMap#add_methods_from_reference(type=#{type}) - resolving generics with #{reference_pin.generics}, #{resolved_reference_type.rooted_tags}" }
methods = methods.map do |method_pin|
method_pin.resolve_generics(reference_pin, resolved_reference_type)
end
end
# logger.debug { "ApiMap#add_methods_from_reference(type=#{type}) - resolved_reference_type: #{resolved_reference_type} for type=#{type}: #{methods.map(&:name)}" }
methods
end
# @param fqns [String]
# @param visibility [Array<Symbol>]
# @param skip [Set<String>]
# @return [Array<Pin::Base>]
def inner_get_constants fqns, visibility, skip
return [] if fqns.nil? || skip.include?(fqns)
skip.add fqns
result = []
store.get_prepends(fqns).each do |is|
result.concat inner_get_constants(qualify(is, fqns), [:public], skip)
end
result.concat store.get_constants(fqns, visibility)
.sort { |a, b| a.name <=> b.name }
store.get_includes(fqns).each do |is|
result.concat inner_get_constants(qualify(is, fqns), [:public], skip)
end
fqsc = qualify_superclass(fqns)
unless %w[Object BasicObject].include?(fqsc)
result.concat inner_get_constants(fqsc, [:public], skip)
end
result
end
# @return [Hash]

@@ -837,65 +842,8 @@ def path_macros

# @param namespace [String]
# @param context [String]
# @param fq_sub_tag [String]
# @return [String, nil]
def qualify_lower namespace, context
qualify namespace, context.split('::')[0..-2].join('::')
end
# @param fq_tag [String]
# @return [String, nil]
def qualify_superclass fq_sub_tag
fq_sub_type = ComplexType.try_parse(fq_sub_tag)
fq_sub_ns = fq_sub_type.name
sup_tag = store.get_superclass(fq_sub_tag)
sup_type = ComplexType.try_parse(sup_tag)
sup_ns = sup_type.name
return nil if sup_tag.nil?
parts = fq_sub_ns.split('::')
last = parts.pop
parts.pop if last == sup_ns
qualify(sup_tag, parts.join('::'))
store.qualify_superclass fq_sub_tag
end
# @param name [String] Namespace to fully qualify
# @param root [String] The context to search
# @param skip [Set<String>] Contexts already searched
# @return [String, nil] Fully qualified ("rooted") namespace
def inner_qualify name, root, skip
return name if name == ComplexType::GENERIC_TAG_NAME
return nil if name.nil?
return nil if skip.include?(root)
skip.add root
possibles = []
if name == ''
if root == ''
return ''
else
return inner_qualify(root, '', skip)
end
else
return name if root == '' && store.namespace_exists?(name)
roots = root.to_s.split('::')
while roots.length > 0
fqns = roots.join('::') + '::' + name
return fqns if store.namespace_exists?(fqns)
incs = store.get_includes(roots.join('::'))
incs.each do |inc|
foundinc = inner_qualify(name, inc, skip)
possibles.push foundinc unless foundinc.nil?
end
roots.pop
end
if possibles.empty?
incs = store.get_includes('')
incs.each do |inc|
foundinc = inner_qualify(name, inc, skip)
possibles.push foundinc unless foundinc.nil?
end
end
return name if store.namespace_exists?(name)
return possibles.last
end
end
# Get the namespace's type (Class or Module).

@@ -930,46 +878,76 @@ #

# @param pin [Pin::MethodAlias, Pin::Base]
# @return [Pin::Method]
def resolve_method_alias pin
return pin unless pin.is_a?(Pin::MethodAlias)
return nil if @method_alias_stack.include?(pin.path)
@method_alias_stack.push pin.path
origin = get_method_stack(pin.full_context.tag, pin.original, scope: pin.scope, preserve_generics: true).first
@method_alias_stack.pop
return nil if origin.nil?
include Logging
private
# @param alias_pin [Pin::MethodAlias]
# @return [Pin::Method, nil]
def resolve_method_alias(alias_pin)
ancestors = store.get_ancestors(alias_pin.full_context.tag)
original = nil
# Search each ancestor for the original method
ancestors.each do |ancestor_fqns|
ancestor_fqns = ComplexType.try_parse(ancestor_fqns).force_rooted.namespace
next if ancestor_fqns.nil?
ancestor_method_path = "#{ancestor_fqns}#{alias_pin.scope == :instance ? '#' : '.'}#{alias_pin.original}"
# Search for the original method in the ancestor
original = store.get_path_pins(ancestor_method_path).find do |candidate_pin|
next if candidate_pin == alias_pin
if candidate_pin.is_a?(Pin::MethodAlias)
# recursively resolve method aliases
resolved = resolve_method_alias(candidate_pin)
break resolved if resolved
end
candidate_pin.is_a?(Pin::Method) && candidate_pin.scope == alias_pin.scope
end
break if original
end
# @sg-ignore ignore `received nil` for original
create_resolved_alias_pin(alias_pin, original) if original
end
# Fast path for creating resolved alias pins without individual method stack lookups
# @param alias_pin [Pin::MethodAlias] The alias pin to resolve
# @param original [Pin::Method] The original method pin that was already found
# @return [Pin::Method] The resolved method pin
def create_resolved_alias_pin(alias_pin, original)
# Build the resolved method pin directly (same logic as resolve_method_alias but without lookup)
args = {
location: pin.location,
type_location: origin.type_location,
closure: pin.closure,
name: pin.name,
comments: origin.comments,
scope: origin.scope,
# context: pin.context,
visibility: origin.visibility,
signatures: origin.signatures.map(&:clone).freeze,
attribute: origin.attribute?,
generics: origin.generics.clone,
return_type: origin.return_type,
location: alias_pin.location,
type_location: original.type_location,
closure: alias_pin.closure,
name: alias_pin.name,
comments: original.comments,
scope: original.scope,
visibility: original.visibility,
signatures: original.signatures.map(&:clone).freeze,
attribute: original.attribute?,
generics: original.generics.clone,
return_type: original.return_type,
source: :resolve_method_alias
}
out = Pin::Method.new **args
out.signatures.each do |sig|
resolved_pin = Pin::Method.new **args
# Clone signatures and parameters
resolved_pin.signatures.each do |sig|
sig.parameters = sig.parameters.map(&:clone).freeze
sig.source = :resolve_method_alias
sig.parameters.each do |param|
param.closure = out
param.closure = resolved_pin
param.source = :resolve_method_alias
param.reset_generated!
end
sig.closure = out
sig.closure = resolved_pin
sig.reset_generated!
end
logger.debug { "ApiMap#resolve_method_alias(pin=#{pin}) - returning #{out} from #{origin}" }
out
resolved_pin
end
include Logging
private
# @param namespace_pin [Pin::Namespace]

@@ -994,5 +972,5 @@ # @param rooted_type [ComplexType]

# @param namespace_pin [Pin::Namespace]
# @param namespace_pin [Pin::Namespace, Pin::Constant]
def has_generics?(namespace_pin)
namespace_pin && !namespace_pin.generics.empty?
namespace_pin.is_a?(Pin::Namespace) && !namespace_pin.generics.empty?
end

@@ -999,0 +977,0 @@

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

def initialize
# @type [Hash{Array => Array<Pin::Method>}]
# @type [Hash{String => Array<Pin::Method>}]
@methods = {}
# @type [Hash{(String, Array<String>) => Array<Pin::Base>}]
# @type [Hash{String, Array<String> => Array<Pin::Base>}]
@constants = {}

@@ -105,2 +105,3 @@ # @type [Hash{String => String}]

# @return [Array<Object>]
def all_caches

@@ -107,0 +108,0 @@ [@methods, @constants, @qualified_namespaces, @receiver_definitions, @clips]

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

# @return [Set<String>]
attr_reader :namespaces
# @return [Hash{String => Array<Pin::Namespace>}]

@@ -40,6 +43,7 @@ def namespace_hash

s = Set.new
# @sg-ignore need to support destructured args in blocks
@pin_select_cache[klass] ||= pin_class_hash.each_with_object(s) { |(key, o), n| n.merge(o) if key <= klass }
end
# @return [Hash{String => Array<Pin::Reference::Include>}]
# @return [Hash{String => Array<String>}]
def include_references

@@ -49,3 +53,8 @@ @include_references ||= Hash.new { |h, k| h[k] = [] }

# @return [Hash{String => Array<Pin::Reference::Extend>}]
# @return [Hash{String => Array<Pin::Reference::Include>}]
def include_reference_pins
@include_reference_pins ||= Hash.new { |h, k| h[k] = [] }
end
# @return [Hash{String => Array<String>}]
def extend_references

@@ -55,3 +64,3 @@ @extend_references ||= Hash.new { |h, k| h[k] = [] }

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

@@ -61,3 +70,3 @@ @prepend_references ||= Hash.new { |h, k| h[k] = [] }

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

@@ -67,3 +76,4 @@ @superclass_references ||= Hash.new { |h, k| h[k] = [] }

# @param pins [Array<Pin::Base>]
# @param pins [Enumerable<Pin::Base>]
# @return [self]
def merge pins

@@ -78,2 +88,3 @@ deep_clone.catalog pins

# @return [self]
def deep_clone

@@ -93,4 +104,6 @@ Index.allocate.tap do |copy|

# @param new_pins [Array<Pin::Base>]
# @param new_pins [Enumerable<Pin::Base>]
# @return [self]
def catalog new_pins
# @type [Hash{Class<generic<T>> => Set<generic<T>>}]
@pin_select_cache = {}

@@ -119,26 +132,7 @@ pins.concat new_pins

pins_by_class(klass).each do |pin|
store_parametric_reference(hash, pin)
hash[pin.namespace].push pin
end
end
# Add references to a map
#
# @param hash [Hash{String => Array<Pin::Reference>}]
# @param reference_pin [Pin::Reference]
#
# @return [void]
def store_parametric_reference(hash, reference_pin)
referenced_ns = reference_pin.name
referenced_tag_params = reference_pin.generic_values
referenced_tag = referenced_ns +
if referenced_tag_params && referenced_tag_params.length > 0
"<" + referenced_tag_params.join(', ') + ">"
else
''
end
referencing_ns = reference_pin.namespace
hash[referencing_ns].push referenced_tag
end
# @return [void]
def map_overrides

@@ -145,0 +139,0 @@ pins_by_class(Pin::Reference::Override).each do |ovr|

@@ -48,8 +48,14 @@ # frozen_string_literal: true

include_object = code_object_at(pin.path, YARD::CodeObjects::ClassObject)
include_object.instance_mixins.push code_object_map[ref] unless include_object.nil? or include_object.nil?
unless include_object.nil? || include_object.nil?
include_object.instance_mixins.push code_object_map[ref.parametrized_tag.to_s]
end
end
store.get_extends(pin.path).each do |ref|
extend_object = code_object_at(pin.path, YARD::CodeObjects::ClassObject)
extend_object.instance_mixins.push code_object_map[ref] unless extend_object.nil? or extend_object.nil?
extend_object.class_mixins.push code_object_map[ref] unless extend_object.nil? or extend_object.nil?
next unless extend_object
code_object = code_object_map[ref.parametrized_tag.to_s]
next unless code_object
extend_object.class_mixins.push code_object
# @todo add spec showing why this next line is necessary
extend_object.instance_mixins.push code_object
end

@@ -71,3 +77,3 @@ end

method_object.parameters = pin.parameters.map do |p|
[p.name, p.asgn_code]
[p.full_name, p.asgn_code]
end

@@ -74,0 +80,0 @@ end

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

def initialize *pinsets
@pinsets = pinsets
catalog pinsets

@@ -21,2 +22,7 @@ end

# @param pinsets [Array<Enumerable<Pin::Base>>]
# - pinsets[0] = core Ruby pins
# - pinsets[1] = documentation/gem pins
# - pinsets[2] = convention pins
# - pinsets[3] = workspace source pins
# - pinsets[4] = currently open file pins
# @return [Boolean] True if the index was updated

@@ -41,2 +47,4 @@ def update *pinsets

end
constants.clear
cached_qualify_superclass.clear
true

@@ -73,19 +81,29 @@ end

# @param fq_tag [String]
BOOLEAN_SUPERCLASS_PIN = Pin::Reference::Superclass.new(name: 'Boolean', closure: Pin::ROOT_PIN, source: :solargraph)
OBJECT_SUPERCLASS_PIN = Pin::Reference::Superclass.new(name: 'Object', closure: Pin::ROOT_PIN, source: :solargraph)
# @param fqns [String]
# @return [Pin::Reference::Superclass]
def get_superclass fqns
return nil if fqns.nil? || fqns.empty?
return BOOLEAN_SUPERCLASS_PIN if %w[TrueClass FalseClass].include?(fqns)
superclass_references[fqns].first || try_special_superclasses(fqns)
end
# @param fq_sub_tag [String]
# @return [String, nil]
def get_superclass fq_tag
raise "Do not prefix fully qualified tags with '::' - #{fq_tag.inspect}" if fq_tag.start_with?('::')
sub = ComplexType.parse(fq_tag)
fqns = sub.namespace
return superclass_references[fq_tag].first if superclass_references.key?(fq_tag)
return superclass_references[fqns].first if superclass_references.key?(fqns)
return 'Object' if fqns != 'BasicObject' && namespace_exists?(fqns)
return 'Object' if fqns == 'Boolean'
simplified_literal_name = ComplexType.parse("#{fqns}").simplify_literals.name
return simplified_literal_name if simplified_literal_name != fqns
nil
def qualify_superclass fq_sub_tag
cached_qualify_superclass[fq_sub_tag] || qualify_and_cache_superclass(fq_sub_tag)
type = ComplexType.try_parse(fq_sub_tag)
return type.simplify_literals.to_s if type.literal?
ref = get_superclass(fq_sub_tag)
return unless ref
res = constants.dereference(ref)
return unless res
res + type.substring
end
# @param fqns [String]
# @return [Array<String>]
# @return [Array<Pin::Reference::Include>]
def get_includes fqns

@@ -96,3 +114,3 @@ include_references[fqns] || []

# @param fqns [String]
# @return [Array<String>]
# @return [Array<Pin::Reference::Prepend>]
def get_prepends fqns

@@ -103,3 +121,3 @@ prepend_references[fqns] || []

# @param fqns [String]
# @return [Array<String>]
# @return [Array<Pin::Reference::Extend>]
def get_extends fqns

@@ -207,4 +225,49 @@ extend_references[fqns] || []

# Get all ancestors (superclasses, includes, prepends, extends) for a namespace
# @param fqns [String] The fully qualified namespace
# @return [Array<String>] Array of ancestor namespaces including the original
def get_ancestors(fqns)
return [] if fqns.nil? || fqns.empty?
ancestors = [fqns]
visited = Set.new
queue = [fqns]
until queue.empty?
current = queue.shift
next if current.nil? || current.empty? || visited.include?(current)
visited.add(current)
current = current.gsub(/^::/, '')
# Add superclass
ref = get_superclass(current)
superclass = ref && constants.dereference(ref)
if superclass && !superclass.empty? && !visited.include?(superclass)
ancestors << superclass
queue << superclass
end
# Add includes, prepends, and extends
[get_includes(current), get_prepends(current), get_extends(current)].each do |refs|
next if refs.nil?
refs.map(&:parametrized_tag).map(&:to_s).each do |ref|
next if ref.nil? || ref.empty? || visited.include?(ref)
ancestors << ref
queue << ref
end
end
end
ancestors.compact.uniq
end
# @return [Constants]
def constants
@constants ||= Constants.new(self)
end
private
# @return [Index]
def index

@@ -214,4 +277,7 @@ @indexes.last

# @param pinsets [Array<Enumerable<Pin::Base>>]
# @return [Boolean]
def catalog pinsets
@pinsets = pinsets
# @type [Array<Index>]
@indexes = []

@@ -225,2 +291,4 @@ pinsets.each do |pins|

end
constants.clear
cached_qualify_superclass.clear
true

@@ -247,3 +315,3 @@ end

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

@@ -253,3 +321,8 @@ index.include_references

# @return [Hash{String => Array<String>}]
# @return [Hash{String => Array<Solargraph::Pin::Reference::Include>}]
def include_reference_pins
index.include_reference_pins
end
# @return [Hash{String => Array<Pin::Reference::Prepend>}]
def prepend_references

@@ -259,3 +332,3 @@ index.prepend_references

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

@@ -276,4 +349,39 @@ index.extend_references

end
# @param fqns [String]
# @return [Pin::Reference::Superclass, nil]
def try_special_superclasses(fqns)
return OBJECT_SUPERCLASS_PIN if fqns == 'Boolean'
return OBJECT_SUPERCLASS_PIN if !%w[BasicObject Object].include?(fqns) && namespace_exists?(fqns)
sub = ComplexType.try_parse(fqns)
return get_superclass(sub.simplify_literals.name) if sub.literal?
get_superclass(sub.namespace) if sub.namespace != fqns
end
# @param fq_sub_tag [String]
# @return [String, nil]
def qualify_and_cache_superclass fq_sub_tag
cached_qualify_superclass[fq_sub_tag] = uncached_qualify_superclass(fq_sub_tag)
end
# @return [Hash{String => String, nil}]
def cached_qualify_superclass
@cached_qualify_superclass ||= {}
end
# @param fq_sub_tag [String]
# @return [String, nil]
def uncached_qualify_superclass fq_sub_tag
type = ComplexType.try_parse(fq_sub_tag)
return type.simplify_literals.to_s if type.literal?
ref = get_superclass(fq_sub_tag)
return unless ref
res = constants.dereference(ref)
return unless res
res + type.substring
end
end
end
end

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

# @return [Set<SourceMap>]
def icebox

@@ -42,0 +43,0 @@ @icebox ||= (source_maps - [live_map])

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

# @return [String]
def tags

@@ -163,2 +164,3 @@ map(&:tag).join(', ')

# @return [String]
def simple_tags

@@ -177,2 +179,3 @@ simplify_literals.tags

# @return [String]
def desc

@@ -182,2 +185,3 @@ rooted_tags

# @return [String]
def rooted_tags

@@ -207,2 +211,3 @@ map(&:rooted_tag).join(', ')

# @return [self]
def simplify_literals

@@ -307,3 +312,2 @@ ComplexType.new(map(&:simplify_literals))

# # @return [Array<UniqueType>]
# @sg-ignore
# @todo To be able to select the right signature above,

@@ -310,0 +314,0 @@ # Chain::Call needs to know the decl type (:arg, :optarg,

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

# @return [self]
def namespace_type

@@ -135,0 +136,0 @@ return ComplexType.parse('::Object') if duck_type?

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

raise ComplexTypeError, "Bad hash type: name=#{name}, substring=#{substring}" unless !subs.is_a?(ComplexType) and subs.length == 2 and !subs[0].is_a?(UniqueType) and !subs[1].is_a?(UniqueType)
# @todo should be able to resolve map; both types have it
# with same return type
# @sg-ignore
key_types.concat(subs[0].map { |u| ComplexType.new([u]) })
# @sg-ignore
subtypes.concat(subs[1].map { |u| ComplexType.new([u]) })

@@ -109,2 +105,3 @@ elsif parameters_type == :list && name == 'Hash'

# @return [self]
def simplify_literals

@@ -121,2 +118,3 @@ transform do |t|

# @return [String]
def non_literal_name

@@ -126,2 +124,3 @@ @non_literal_name ||= determine_non_literal_name

# @return [String]
def determine_non_literal_name

@@ -178,2 +177,3 @@ # https://github.com/ruby/rbs/blob/master/docs/syntax.md

# @return [String]
def desc

@@ -263,3 +263,3 @@ rooted_tags

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

@@ -362,5 +362,5 @@ def resolve_generics_from_context generics_to_resolve, context_type, resolved_generic_values: {}

# @param make_rooted [Boolean, nil]
# @param new_key_types [Array<UniqueType>, nil]
# @param new_key_types [Array<ComplexType>, nil]
# @param rooted [Boolean, nil]
# @param new_subtypes [Array<UniqueType>, nil]
# @param new_subtypes [Array<ComplexType>, nil]
# @return [self]

@@ -367,0 +367,0 @@ def recreate(new_name: nil, make_rooted: nil, new_key_types: nil, new_subtypes: nil)

# frozen_string_literal: true
module Solargraph

@@ -15,2 +14,3 @@ # Conventions provide a way to modify an ApiMap based on expectations about

autoload :DataDefinition, 'solargraph/convention/data_definition'
autoload :ActiveSupportConcern, 'solargraph/convention/active_support_concern'

@@ -26,2 +26,8 @@ # @type [Set<Convention::Base>]

# @param convention [Class<Convention::Base>]
# @return [void]
def self.unregister convention
@@conventions.delete_if { |c| c.is_a?(convention) }
end
# @param source_map [SourceMap]

@@ -37,3 +43,3 @@ # @return [Environ]

# @param yard_map [DocMap]
# @param doc_map [DocMap]
# @return [Environ]

@@ -48,6 +54,29 @@ def self.for_global(doc_map)

# Provides any additional method pins based on the described object.
#
# @param api_map [ApiMap]
# @param rooted_tag [String] A fully qualified namespace, with
# generic parameter values if applicable
# @param scope [Symbol] :class or :instance
# @param visibility [Array<Symbol>] :public, :protected, and/or :private
# @param deep [Boolean]
# @param skip [Set<String>]
# @param no_core [Boolean] Skip core classes if true
#
# @return [Environ]
def self.for_object api_map, rooted_tag, scope, visibility,
deep, skip, no_core
result = Environ.new
@@conventions.each do |conv|
result.merge conv.object(api_map, rooted_tag, scope, visibility,
deep, skip, no_core)
end
result
end
register Gemfile
register Gemspec
register Rakefile
register ActiveSupportConcern
end
end

@@ -31,4 +31,21 @@ # frozen_string_literal: true

end
# Provides any additional method pins based on e the described object.
#
# @param api_map [ApiMap]
# @param rooted_tag [String] A fully qualified namespace, with
# generic parameter values if applicable
# @param scope [Symbol] :class or :instance
# @param visibility [Array<Symbol>] :public, :protected, and/or :private
# @param deep [Boolean]
# @param skip [Set<String>]
# @param no_core [Boolean] Skip core classes if true
#
# @return [Environ]
def object api_map, rooted_tag, scope, visibility,
deep, skip, no_core
EMPTY_ENVIRON
end
end
end
end

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

# TODO: Support both arg and kwarg initializers for Data.define
# @todo Support both arg and kwarg initializers for Data.define
# Solargraph::SourceMap::Clip#complete_keyword_parameters does not seem to currently take into account [Pin::Method#signatures] hence we only one for :kwarg

@@ -92,2 +92,3 @@ pins.push initialize_method_pin

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

@@ -94,0 +95,0 @@ def attribute_comments(attribute_node, attribute_name)

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

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

@@ -27,0 +28,0 @@ return false unless node&.type == :casgn

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

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

@@ -50,3 +52,3 @@ return false unless node&.type == :class

# @return [Parser::AST::Node]
# @param node [Parser::AST::Node]
def initialize(node)

@@ -53,0 +55,0 @@ @node = node

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

def local source_map
return EMPTY_ENVIRON unless File.basename(source_map.filename).end_with?('.gemspec')
return Convention::Base::EMPTY_ENVIRON unless File.basename(source_map.filename).end_with?('.gemspec')
@environ ||= Environ.new(

@@ -10,0 +10,0 @@ requires: ['rubygems'],

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

visibility: :public,
gates: region.closure.gates.freeze
gates: region.closure.gates.freeze,
source: :struct_definition
)

@@ -36,3 +37,4 @@ pins.push nspin

visibility: :private,
docstring: docstring
docstring: docstring,
source: :struct_definition
)

@@ -48,3 +50,4 @@

location: get_node_location(attribute_node),
closure: initialize_method_pin
closure: initialize_method_pin,
source: :struct_definition
)

@@ -59,2 +62,6 @@ )

attribute_type = ComplexType.parse(tag_string(docs))
return_type_comment = attribute_comment(docs, false)
param_comment = attribute_comment(docs, true)
method_pin = Pin::Method.new(

@@ -66,5 +73,8 @@ name: name,

closure: nspin,
docstring: YARD::Docstring.new(return_type_comment),
# even assignments return the value
comments: attribute_comment(docs, false),
visibility: :public
comments: return_type_comment,
return_type: attribute_type,
visibility: :public,
source: :struct_definition
)

@@ -76,10 +86,14 @@

location: get_node_location(attribute_node),
closure: nspin,
comments: attribute_comment(docs, true)
closure: method_pin,
return_type: attribute_type,
comments: param_comment,
source: :struct_definition
)
pins.push Pin::InstanceVariable.new(name: "@#{attribute_name}",
closure: method_pin,
location: get_node_location(attribute_node),
comments: attribute_comment(docs, false))
closure: method_pin,
location: get_node_location(attribute_node),
return_type: attribute_type,
comments: "@type [#{attribute_type.rooted_tags}]",
source: :struct_definition)
end

@@ -97,3 +111,3 @@

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

@@ -123,3 +137,3 @@ @struct_definition_node ||= if StructDefintionNode.match?(node)

# But since we merge into the struct_comments, then we should interpret either as a param
comment = '@param ' + attr_name + comment[7..] if comment.start_with?('@return')
comment = "@param #{attr_name}#{comment[7..]}" if comment.start_with?('@return')

@@ -132,8 +146,17 @@ struct_comments += "\n#{comment}"

# @param tag [YARD::Tags::Tag, nil] The param tag for this attribute.xtract_
#
# @return [String]
def tag_string(tag)
tag&.types&.join(',') || 'undefined'
end
# @param tag [YARD::Tags::Tag, nil] The param tag for this attribute. If nil, this method is a no-op
# @param for_setter [Boolean] If true, will return a @param tag instead of a @return tag
#
# @return [String] The formatted comment for the attribute
def attribute_comment(tag, for_setter)
return "" if tag.nil?
suffix = "[#{tag.types&.join(',') || 'undefined'}] #{tag.text}"
suffix = "[#{tag_string(tag)}] #{tag.text}"

@@ -140,0 +163,0 @@ if for_setter

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

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

@@ -27,0 +28,0 @@ return false unless node&.type == :casgn

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

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

@@ -50,3 +52,3 @@ return false unless node&.type == :class

# @return [Parser::AST::Node]
# @param node [Parser::AST::Node]
def initialize(node)

@@ -53,0 +55,0 @@ @node = node

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

begin
# @type [String]
gem_path = Gem::Specification.find_by_name('rubocop', version).full_gem_path
gem_lib_path = File.join(gem_path, 'lib')
$LOAD_PATH.unshift(gem_lib_path) unless $LOAD_PATH.include?(gem_lib_path)
# @todo Gem::MissingSpecVersionError is undocumented for some reason
# @sg-ignore

@@ -24,0 +24,0 @@ rescue Gem::MissingSpecVersionError => e

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

runner = RuboCop::Runner.new(options, store)
result = redirect_stdout{ runner.run(paths) }
# Ensure only one instance of RuboCop::Runner is running at
# a time - it uses 'chdir' to read config files with ERB,
# which can conflict with other chdirs.
result = Solargraph::CHDIR_MUTEX.synchronize do
redirect_stdout{ runner.run(paths) }
end

@@ -34,0 +39,0 @@ return [] if result.empty?

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

require 'benchmark'
require 'open3'

@@ -36,4 +37,6 @@ module Solargraph

# @return [String, nil]
attr_reader :rbs_collection_path
# @return [String, nil]
attr_reader :rbs_collection_config_path

@@ -57,2 +60,3 @@

@environ = Convention.for_global(self)
@requires.concat @environ.requires if @environ
load_serialized_gem_pins

@@ -62,2 +66,4 @@ pins.concat @environ.pins

# @param out [IO]
# @return [void]
def cache_all!(out)

@@ -80,4 +86,7 @@ # if we log at debug level:

# @param gemspec [Gem::Specification]
# @param out [IO]
# @return [void]
def cache_yard_pins(gemspec, out)
pins = GemPins.build_yard_pins(gemspec)
pins = GemPins.build_yard_pins(yard_plugins, gemspec)
PinCache.serialize_yard_gem(gemspec, pins)

@@ -87,2 +96,5 @@ logger.info { "Cached #{pins.length} YARD pins for gem #{gemspec.name}:#{gemspec.version}" } unless pins.empty?

# @param gemspec [Gem::Specification]
# @param out [IO]
# @return [void]
def cache_rbs_collection_pins(gemspec, out)

@@ -99,2 +111,5 @@ rbs_map = RbsMap.from_gemspec(gemspec, rbs_collection_path, rbs_collection_config_path)

# @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)

@@ -123,2 +138,3 @@ build_yard = uncached_yard_gemspecs.include?(gemspec) || rebuild

# @return [Hash{Array(String, String) => Array<Gem::Specification>}] Indexed by gemspec name and version
def self.all_yard_gems_in_memory

@@ -128,2 +144,3 @@ @yard_gems_in_memory ||= {}

# @return [Hash{String => Hash{Array(String, String) => Array<Pin::Base>}}] stored by RBS collection path
def self.all_rbs_collection_gems_in_memory

@@ -133,2 +150,3 @@ @rbs_collection_gems_in_memory ||= {}

# @return [Hash{Array(String, String) => Array<Pin::Base>}] Indexed by gemspec name and version
def yard_pins_in_memory

@@ -138,2 +156,3 @@ self.class.all_yard_gems_in_memory

# @return [Hash{Array(String, String) => Array<Pin::Base>}] Indexed by gemspec name and version
def rbs_collection_pins_in_memory

@@ -143,2 +162,3 @@ self.class.all_rbs_collection_gems_in_memory[rbs_collection_path] ||= {}

# @return [Hash{Array(String, String) => Array<Pin::Base>}] Indexed by gemspec name and version
def self.all_combined_pins_in_memory

@@ -148,2 +168,4 @@ @combined_pins_in_memory ||= {}

# @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

@@ -153,2 +175,7 @@ self.class.all_combined_pins_in_memory

# @return [Array<String>]
def yard_plugins
@environ.yard_plugins
end
# @return [Set<Gem::Specification>]

@@ -167,3 +194,7 @@ def dependencies

with_gemspecs, without_gemspecs = required_gems_map.partition { |_, v| v }
# @sg-ignore Wrong argument type for Hash.[]: arg_0 expected _ToHash<Array(String, Array<Gem::Specification>), undefined>, received Array<Array(String, Array<Gem::Specification>)>
# @type [Array<String>]
paths = Hash[without_gemspecs].keys
# @sg-ignore Wrong argument type for Hash.[]: arg_0 expected _ToHash<Array(String, Array<Gem::Specification>), undefined>, received Array<Array(String, Array<Gem::Specification>)>
# @type [Array<Gem::Specification>]
gemspecs = Hash[with_gemspecs].values.flatten.compact + dependencies.to_a

@@ -277,2 +308,4 @@

# @param gemspec [Gem::Specification]
# @param rbs_version_cache_key [String]
# @return [Array<Pin::Base>, nil]

@@ -293,12 +326,2 @@ def deserialize_rbs_collection_cache gemspec, rbs_version_cache_key

# @param gemspec [Gem::Specification]
# @return [Boolean]
def try_gem_in_memory gemspec
gempins = DocMap.gems_in_memory[gemspec]
return false unless gempins
Solargraph.logger.debug "Found #{gemspec.name} #{gemspec.version} in memory"
@pins.concat gempins
true
end
# @param path [String]

@@ -310,2 +333,3 @@ # @return [::Array<Gem::Specification>, nil]

# @type [Gem::Specification, nil]
gemspec = Gem::Specification.find_by_path(path)

@@ -334,6 +358,8 @@ if gemspec.nil?

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[by_path.name].version
change_gemspec_version gemspec, preference_map[gemspec.name].version
# :nocov:
end

@@ -377,2 +403,3 @@

# @return [Array<Gem::Specification>]
def gemspecs_required_from_bundler

@@ -400,2 +427,3 @@ # @todo Handle projects with custom Bundler/Gemfile setups

# @return [Array<Gem::Specification>]
def gemspecs_required_from_external_bundle

@@ -402,0 +430,0 @@ logger.info 'Fetching gemspecs required from external bundle'

@@ -16,12 +16,17 @@ # frozen_string_literal: true

# @return [Array<Pin::Reference::Override>]
# @return [Array<Pin::Base>]
attr_reader :pins
# @return [Array<String>]
attr_reader :yard_plugins
# @param requires [Array<String>]
# @param domains [Array<String>]
# @param pins [Array<Pin::Base>]
def initialize requires: [], domains: [], pins: []
# @param yard_plugins[Array<String>]
def initialize requires: [], domains: [], pins: [], yard_plugins: []
@requires = requires
@domains = domains
@pins = pins
@yard_plugins = yard_plugins
end

@@ -34,2 +39,3 @@

pins.clear
yard_plugins.clear
self

@@ -44,2 +50,3 @@ end

pins.concat other.pins
yard_plugins.concat other.yard_plugins
self

@@ -46,0 +53,0 @@ end

@@ -14,13 +14,5 @@ # frozen_string_literal: true

# @param gemspec [Gem::Specification]
# @param pins [Array<Pin::Base>]
# @return [Array<Pin::Base>]
def self.build_yard_pins(gemspec)
Yardoc.cache(gemspec) unless Yardoc.cached?(gemspec)
yardoc = Yardoc.load!(gemspec)
YardMap::Mapper.new(yardoc, gemspec).map
end
# @param pins [Array<Pin::Base>]
def self.combine_method_pins_by_path(pins)
# bad_pins = pins.select { |pin| pin.is_a?(Pin::Method) && pin.path == 'StringIO.open' && pin.source == :rbs }; raise "wtf: #{bad_pins}" if bad_pins.length > 1
method_pins, alias_pins = pins.partition { |pin| pin.class == Pin::Method }

@@ -34,4 +26,8 @@ by_path = method_pins.group_by(&:path)

# @param pins [Array<Pin::Method>]
# @return [Pin::Method, nil]
def self.combine_method_pins(*pins)
out = pins.reduce(nil) do |memo, pin|
# @type [Pin::Method, nil]
combined_pin = nil
out = pins.reduce(combined_pin) do |memo, pin|
next pin if memo.nil?

@@ -50,4 +46,14 @@ if memo == pin && memo.source != :combined

# @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)
yardoc = Yardoc.load!(gemspec)
YardMap::Mapper.new(yardoc, gemspec).map
end
# @param yard_pins [Array<Pin::Base>]
# @param rbs_map [RbsMap]
# @param rbs_pins [Array<Pin::Base>]
#
# @return [Array<Pin::Base>]

@@ -54,0 +60,0 @@ def self.combine(yard_pins, rbs_pins)

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

# @return [String]
def command_path

@@ -720,3 +721,3 @@ options['commandPath'] || 'solargraph'

#
# @return [Hash{Integer => Solargraph::LanguageServer::Host}]
# @return [Hash{Integer => Solargraph::LanguageServer::Request}]
def requests

@@ -723,0 +724,0 @@ @requests ||= {}

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

# @return [Hash{String => undefined}]
def options

@@ -122,2 +123,3 @@ @options ||= {}.freeze

# @param library [Solargraph::Library]
# @param progress [Solargraph::LanguageServer::Progress, nil]
# @return [void]

@@ -124,0 +126,0 @@ def update progress

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

# @return [Hash, nil]
def next_message

@@ -80,2 +81,3 @@ cancel_message || next_priority

# @return [Hash, nil]
def cancel_message

@@ -91,2 +93,3 @@ # Handle cancellations first

# @return [Hash, nil]
def next_priority

@@ -93,0 +96,0 @@ # Prioritize updates and version-dependent messages for performance

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

# @return [Hash{String => Array<undefined>, Hash{String => undefined}, String, Integer}]
# @return [Hash{String => undefined}]
attr_reader :params

@@ -83,2 +83,3 @@

# @return [void]
def accept_or_cancel

@@ -85,0 +86,0 @@ if host.cancel?(id)

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

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

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

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

# @return [Array<Hash>]
def code_location

@@ -25,2 +26,3 @@ suggestions = host.definitions_at(params['textDocument']['uri'], @line, @column)

# @return [Array<Hash>]
def require_location

@@ -27,0 +29,0 @@ # @todo Terrible hack

@@ -22,4 +22,10 @@ # frozen_string_literal: true

options[:stdin] = original
corrections = redirect_stdout do
::RuboCop::Runner.new(options, ::RuboCop::ConfigStore.new).run(paths)
# Ensure only one instance of RuboCop::Runner is running at
# a time - it uses 'chdir' to read config files with ERB,
# which can conflict with other chdirs.
corrections = Solargraph::CHDIR_MUTEX.synchronize do
redirect_stdout do
::RuboCop::Runner.new(options, ::RuboCop::ConfigStore.new).run(paths)
end
end

@@ -38,2 +44,3 @@ result = options[:stdin]

# @param corrections [String]
# @return [void]
def log_corrections(corrections)

@@ -50,2 +57,4 @@ corrections = corrections&.strip

# @param file_uri [String]
# @return [Hash{String => undefined}]
def config_for(file_uri)

@@ -58,3 +67,5 @@ conf = host.formatter_config(file_uri)

# @param file_uri [String]
# @param config [Hash{String => String}]
# @return [Array<String>]
def cli_args file_uri, config

@@ -78,2 +89,4 @@ file = UriHelpers.uri_to_file(file_uri)

# @param config [Hash{String => String}]
# @sg-ignore
# @return [Class<RuboCop::Formatter::BaseFormatter>]
def formatter_class(config)

@@ -91,2 +104,3 @@ if self.class.const_defined?('BlankRubocopFormatter')

# @param value [Array, String]
# @return [String]
def cop_list(value)

@@ -93,0 +107,0 @@ value = value.join(',') if value.respond_to?(:join)

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

# @return [Array<Hash>]
def code_location

@@ -15,0 +16,0 @@ suggestions = host.type_definitions_at(params['textDocument']['uri'], @line, @column)

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

# @return [void]
def add_folders

@@ -18,2 +19,3 @@ return unless params['event'] && params['event']['added']

# @return [void]
def remove_folders

@@ -20,0 +22,0 @@ return unless params['event'] && params['event']['removed']

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

# @param percentage [Integer]
# @return [void]
def begin message, percentage

@@ -51,2 +52,3 @@ @kind = 'begin'

# @param percentage [Integer]
# @return [void]
def report message, percentage

@@ -59,2 +61,3 @@ @kind = 'report'

# @param message [String]
# @return [void]
def finish message

@@ -68,2 +71,3 @@ @kind = 'end'

# @param host [Solargraph::LanguageServer::Host]
# @return [void]
def send host

@@ -98,2 +102,3 @@ return unless host.client_supports_progress? && !finished?

# @return [Hash]
def build

@@ -109,2 +114,3 @@ {

# @return [Hash]
def build_value

@@ -124,2 +130,3 @@ case kind

# @param host [Host]
# @return [void]
def keep_alive host

@@ -137,2 +144,3 @@ mutex.synchronize { @last = Time.now }

# @return [Mutex]
def mutex

@@ -139,0 +147,0 @@ @mutex ||= Mutex.new

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

# @return [void]
def send_response

@@ -21,0 +22,0 @@ # noop

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

if line == 'all!'
Diagnostics.reporters.each do |reporter|
repargs[reporter] ||= []
Diagnostics.reporters.each do |reporter_name|
repargs[Diagnostics.reporter(reporter_name)] ||= []
end

@@ -441,13 +441,2 @@ else

# Get an array of foldable ranges for the specified file.
#
# @deprecated The library should not need to handle folding ranges. The
# source itself has all the information it needs.
#
# @param filename [String]
# @return [Array<Range>]
def folding_ranges filename
read(filename).folding_ranges
end
# Create a library from a directory.

@@ -516,3 +505,3 @@ #

# @return [Hash{String => Set<String>}]
# @return [Hash{String => Array<String>}]
def source_map_external_require_hash

@@ -525,2 +514,3 @@ @source_map_external_require_hash ||= {}

def find_external_requires source_map
# @type [Set<String>]
new_set = source_map.requires.map(&:name).to_set

@@ -628,2 +618,3 @@ # return if new_set == source_map_external_require_hash[source_map.filename]

# @return [Array<Gem::Specification>]
def cacheable_specs

@@ -639,2 +630,3 @@ cacheable = api_map.uncached_yard_gemspecs +

# @return [Array<Gem::Specification>]
def queued_gemspec_cache

@@ -681,2 +673,3 @@ @queued_gemspec_cache ||= []

# @return [void]
def sync_catalog

@@ -687,4 +680,4 @@ return if @sync_count == 0

logger.info "Cataloging #{workspace.directory.empty? ? 'generic workspace' : workspace.directory}"
source_map_hash.values.each { |map| find_external_requires(map) }
api_map.catalog bench
source_map_hash.values.each { |map| find_external_requires(map) }
logger.info "Catalog complete (#{api_map.source_maps.length} files, #{api_map.pins.length} pins)"

@@ -691,0 +684,0 @@ logger.info "#{api_map.uncached_yard_gemspecs.length} uncached YARD gemspecs"

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

# @param other [self]
def <=>(other)

@@ -64,2 +65,3 @@ return nil unless other.is_a?(Location)

# @param node [Parser::AST::Node, nil]
# @return [Location, nil]
def self.from_node(node)

@@ -66,0 +68,0 @@ return nil if node.nil? || node.loc.nil?

@@ -14,4 +14,13 @@ # frozen_string_literal: true

}
@@logger = Logger.new(STDERR, level: DEFAULT_LOG_LEVEL)
configured_level = ENV.fetch('SOLARGRAPH_LOG', nil)
level = if LOG_LEVELS.keys.include?(configured_level)
LOG_LEVELS.fetch(configured_level)
else
if configured_level
warn "Invalid value for SOLARGRAPH_LOG: #{configured_level.inspect} - " \
"valid values are #{LOG_LEVELS.keys}"
end
DEFAULT_LOG_LEVEL
end
@@logger = Logger.new(STDERR, level: level)
# @sg-ignore Fix cvar issue

@@ -18,0 +27,0 @@ @@logger.formatter = proc do |severity, datetime, progname, msg|

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

# @param text [String]
# @sg-ignore https://github.com/lsegal/yard/pull/1615
# @return [String]
def htmlify text
# @type [String]
YARD::Templates::Helpers::Markup::RDocMarkup.new(text).to_html

@@ -74,4 +76,6 @@ end

# @param locals [Hash]
# @sg-ignore
# @return [String]
def render template, layout: true, locals: {}
# @type [String]
@render_method.call(template, layout: layout, locals: locals)

@@ -78,0 +82,0 @@ end

@@ -6,2 +6,9 @@ require 'ripper'

class CommentRipper < Ripper::SexpBuilderPP
# @!override Ripper::SexpBuilder#on_embdoc_beg
# @return [Array(Symbol, String, Array)]
# @!override Ripper::SexpBuilder#on_embdoc
# @return [Array(Symbol, String, Array)]
# @!override Ripper::SexpBuilder#on_embdoc_end
# @return [Array(Symbol, String, Array)]
# @param src [String]

@@ -55,3 +62,3 @@ # @param filename [String]

# @return [Hash{Integer => String}]
# @return [Hash{Integer => Solargraph::Parser::Snippet}]
def parse

@@ -58,0 +65,0 @@ @comments = {}

@@ -7,2 +7,3 @@ module Solargraph

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

@@ -14,4 +15,9 @@ @locals = locals

# @param and_node [Parser::AST::Node]
# @param true_ranges [Array<Range>]
#
# @return [void]
def process_and(and_node, true_ranges = [])
# @type [Parser::AST::Node]
lhs = and_node.children[0]
# @type [Parser::AST::Node]
rhs = and_node.children[1]

@@ -28,2 +34,4 @@

# @param if_node [Parser::AST::Node]
#
# @return [void]
def process_if(if_node)

@@ -42,3 +50,5 @@ #

conditional_node = if_node.children[0]
# @type [Parser::AST::Node]
then_clause = if_node.children[1]
# @type [Parser::AST::Node]
else_clause = if_node.children[2]

@@ -79,4 +89,7 @@

# @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)

@@ -115,3 +128,6 @@ logger.debug { "FlowSensitiveTyping#visible_pins(name=#{name}, closure=#{closure}, location=#{location})" }

# @param pin [Pin::LocalVariable]
# @param if_node [Parser::AST::Node]
# @param downcast_type_name [String]
# @param presence [Range]
#
# @return [void]
def add_downcast_local(pin, downcast_type_name, presence)

@@ -135,2 +151,3 @@ # @todo Create pin#update method

# @param presences [Array<Range>]
#
# @return [void]

@@ -152,2 +169,5 @@ def process_facts(facts_by_pin, presences)

# @param conditional_node [Parser::AST::Node]
# @param true_ranges [Array<Range>]
#
# @return [void]
def process_conditional(conditional_node, true_ranges)

@@ -187,2 +207,6 @@ if conditional_node.type == :send

# @param variable_name [String]
# @param position [Position]
#
# @return [Solargraph::Pin::LocalVariable, nil]
def find_local(variable_name, position)

@@ -194,2 +218,6 @@ pins = locals.select { |pin| pin.name == variable_name && pin.presence.include?(position) }

# @param isa_node [Parser::AST::Node]
# @param true_presences [Array<Range>]
#
# @return [void]
def process_isa(isa_node, true_presences)

@@ -210,6 +238,8 @@ isa_type_name, variable_name = parse_isa(isa_node)

# @param node [Parser::AST::Node]
#
# @return [String, nil]
def type_name(node)
# e.g.,
# s(:const, nil, :Baz)
return unless node.type == :const
return unless node&.type == :const
module_node = node.children[0]

@@ -226,4 +256,2 @@ class_node = node.children[1]

# @todo "return type could not be inferred" should not trigger here
# @sg-ignore
# @param clause_node [Parser::AST::Node]

@@ -230,0 +258,0 @@ def always_breaks?(clause_node)

module Solargraph
module Parser
class NodeMethods
module NodeMethods
module_function

@@ -77,3 +77,3 @@

# @param node [Parser::AST::Node]
# @return [Hash{Parser::AST::Node => Source::Chain}]
# @return [Hash{Symbol => Source::Chain}]
def convert_hash node

@@ -80,0 +80,0 @@ raise NotImplementedError

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

class << self
# @type [Hash<Symbol, Array<Class<NodeProcessor::Base>>>]
# @type [Hash{Symbol => Array<Class<NodeProcessor::Base>>}]
@@processors ||= {}

@@ -21,3 +21,3 @@

# @param cls [Class<NodeProcessor::Base>]
# @return [Class<NodeProcessor::Base>]
# @return [Array<Class<NodeProcessor::Base>>]
def register type, cls

@@ -28,2 +28,6 @@ @@processors[type] ||= []

# @param type [Symbol]
# @param cls [Class<NodeProcessor::Base>]
#
# @return [void]
def deregister type, cls

@@ -30,0 +34,0 @@ @@processors[type].delete(cls)

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

# @return [Array<Pin::BaseVariable>]
# @return [Array<Pin::LocalVariable>]
attr_reader :locals

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

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

# @param filename [String, nil]
# @return [Array(Parser::AST::Node, Hash{Integer => String})]
# @return [Array(Parser::AST::Node, Hash{Integer => Solargraph::Parser::Snippet})]
def parse_with_comments code, filename = nil

@@ -23,0 +23,0 @@ node = parse(code, filename)

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

# @return [String]
# @sg-ignore
def string_value(token)

@@ -14,0 +15,0 @@ value(token)

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

include NodeMethods
Chain = Source::Chain

@@ -102,3 +103,4 @@

elsif n.type == :or_asgn
result.concat generate_links n.children[1]
new_node = n.updated(n.children[0].type, n.children[0].children + [n.children[1]])
result.concat generate_links new_node
elsif [:class, :module, :def, :defs].include?(n.type)

@@ -105,0 +107,0 @@ # @todo Undefined or what?

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

# @param node [Parser::AST::Node]
# @return [Hash{Parser::AST::Node => Chain}]
# @return [Hash{Symbol => Chain}]
def convert_hash node

@@ -183,2 +183,3 @@ return {} unless Parser.is_ast_node?(node)

result.push node
result.concat call_nodes_from(node.children.first)
node.children[2..-1].each { |child| result.concat call_nodes_from(child) }

@@ -237,2 +238,3 @@ elsif [:super, :zsuper].include?(node.type)

end
# @type [AST::Node, nil]
prev = nil

@@ -248,3 +250,3 @@ tree.each do |node|

else
return node if source.code[0..offset-1] =~ /\([^\(]*\z/
return node if source.code[0..offset-1] =~ /\([^(]*\z/
end

@@ -251,0 +253,0 @@ end

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

end
pins.push Solargraph::Pin::Block.new(
block_pin = Solargraph::Pin::Block.new(
location: location,

@@ -32,3 +32,4 @@ closure: parent,

)
process_children region.update(closure: pins.last)
pins.push block_pin
process_children region.update(closure: block_pin)
end

@@ -35,0 +36,0 @@

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

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

@@ -16,0 +18,0 @@ FlowSensitiveTyping.new(locals, enclosing_breakable_pin).process_if(node)

@@ -25,4 +25,7 @@ # frozen_string_literal: true

masgn = node
# @type [Parser::AST::Node]
mlhs = masgn.children.fetch(0)
# @type [Array<Parser::AST::Node>]
lhs_arr = mlhs.children
# @type [Parser::AST::Node]
mass_rhs = node.children.fetch(1)

@@ -29,0 +32,0 @@

@@ -11,28 +11,40 @@ # frozen_string_literal: true

def process
# @sg-ignore Variable type could not be inferred for method_name
# @type [Symbol]
method_name = node.children[1]
# :nocov:
unless method_name.instance_of?(Symbol)
Solargraph.assert_or_log(:parser_method_name, "Expected method name to be a Symbol, got #{method_name.class} for node #{node.inspect}")
return process_children
end
# :nocov:
if node.children[0].nil?
if [:private, :public, :protected].include?(node.children[1])
if [:private, :public, :protected].include?(method_name)
process_visibility
elsif node.children[1] == :module_function
elsif method_name == :module_function
process_module_function
elsif [:attr_reader, :attr_writer, :attr_accessor].include?(node.children[1])
elsif [:attr_reader, :attr_writer, :attr_accessor].include?(method_name)
process_attribute
elsif node.children[1] == :include
elsif method_name == :include
process_include
elsif node.children[1] == :extend
elsif method_name == :extend
process_extend
elsif node.children[1] == :prepend
elsif method_name == :prepend
process_prepend
elsif node.children[1] == :require
elsif method_name == :require
process_require
elsif node.children[1] == :autoload
elsif method_name == :autoload
process_autoload
elsif node.children[1] == :private_constant
elsif method_name == :private_constant
process_private_constant
elsif node.children[1] == :alias_method && node.children[2] && node.children[2] && node.children[2].type == :sym && node.children[3] && node.children[3].type == :sym
# @sg-ignore
elsif method_name == :alias_method && node.children[2] && node.children[2] && node.children[2].type == :sym && node.children[3] && node.children[3].type == :sym
process_alias_method
elsif node.children[1] == :private_class_method && node.children[2].is_a?(AST::Node)
# @sg-ignore
elsif method_name == :private_class_method && node.children[2].is_a?(AST::Node)
# Processing a private class can potentially handle children on its own
return if process_private_class_method
end
elsif node.children[1] == :require && node.children[0].to_s == '(const nil :Bundler)'
# @sg-ignore
elsif method_name == :require && node.children[0].to_s == '(const nil :Bundler)'
pins.push Pin::Reference::Require.new(Solargraph::Location.new(region.filename, Solargraph::Range.from_to(0, 0, 0, 0)), 'bundler/require', source: :parser)

@@ -49,2 +61,11 @@ end

node.children[2..-1].each do |child|
# @sg-ignore Variable type could not be inferred for method_name
# @type [Symbol]
visibility = node.children[1]
# :nocov:
unless visibility.instance_of?(Symbol)
Solargraph.assert_or_log(:parser_visibility, "Expected visibility name to be a Symbol, got #{visibility.class} for node #{node.inspect}")
return process_children
end
# :nocov:
if child.is_a?(AST::Node) && (child.type == :sym || child.type == :str)

@@ -55,6 +76,6 @@ name = child.children[0].to_s

# @todo Smelly instance variable access
pin.instance_variable_set(:@visibility, node.children[1])
pin.instance_variable_set(:@visibility, visibility)
end
else
process_children region.update(visibility: node.children[1])
process_children region.update(visibility: visibility)
end

@@ -61,0 +82,0 @@ end

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

# @param namespace [String]
# @param closure [Pin::Closure, nil]
# @param scope [Symbol, nil]
# @param visibility [Symbol]
# @param lvars [Array<Symbol>]
def initialize source: Solargraph::Source.load_string(''), closure: nil,

@@ -49,2 +51,3 @@ scope: nil, visibility: :public, lvars: []

# @param visibility [Symbol, nil]
# @param lvars [Array<Symbol>, nil]
# @return [Region]

@@ -51,0 +54,0 @@ def update closure: nil, scope: nil, visibility: nil, lvars: nil

@@ -9,2 +9,4 @@ module Solargraph

# @param range [Solargraph::Range]
# @param text [String]
def initialize range, text

@@ -11,0 +13,0 @@ @range = range

@@ -0,1 +1,2 @@

require 'yard-activesupport-concern'
require 'fileutils'

@@ -29,6 +30,12 @@ require 'rbs'

# @param gemspec [Gem::Specification]
# @return [String]
def yardoc_path gemspec
File.join(base_dir, "yard-#{YARD::VERSION}", "#{gemspec.name}-#{gemspec.version}.yardoc")
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

@@ -38,2 +45,4 @@ File.join(work_dir, 'stdlib')

# @param require [String]
# @return [String]
def stdlib_require_path require

@@ -43,2 +52,4 @@ File.join(stdlib_path, "#{require}.ser")

# @param require [String]
# @return [Array<Pin::Base>]
def deserialize_stdlib_require require

@@ -48,2 +59,5 @@ load(stdlib_require_path(require))

# @param require [String]
# @param pins [Array<Pin::Base>]
# @return [void]
def serialize_stdlib_require require, pins

@@ -53,2 +67,3 @@ save(stdlib_require_path(require), pins)

# @return [String]
def core_path

@@ -58,2 +73,3 @@ File.join(work_dir, 'core.ser')

# @return [Array<Pin::Base>]
def deserialize_core

@@ -63,2 +79,4 @@ load(core_path)

# @param pins [Array<Pin::Base>]
# @return [void]
def serialize_core pins

@@ -68,2 +86,4 @@ save(core_path, pins)

# @param gemspec [Gem::Specification]
# @return [String]
def yard_gem_path gemspec

@@ -73,2 +93,4 @@ File.join(work_dir, 'yard', "#{gemspec.name}-#{gemspec.version}.ser")

# @param gemspec [Gem::Specification]
# @return [Array<Pin::Base>]
def deserialize_yard_gem(gemspec)

@@ -78,2 +100,5 @@ load(yard_gem_path(gemspec))

# @param gemspec [Gem::Specification]
# @param pins [Array<Pin::Base>]
# @return [void]
def serialize_yard_gem(gemspec, pins)

@@ -83,2 +108,4 @@ save(yard_gem_path(gemspec), pins)

# @param gemspec [Gem::Specification]
# @return [Boolean]
def has_yard?(gemspec)

@@ -88,2 +115,5 @@ exist?(yard_gem_path(gemspec))

# @param gemspec [Gem::Specification]
# @param hash [String, nil]
# @return [String]
def rbs_collection_path(gemspec, hash)

@@ -93,2 +123,4 @@ File.join(work_dir, 'rbs', "#{gemspec.name}-#{gemspec.version}-#{hash || 0}.ser")

# @param gemspec [Gem::Specification]
# @return [String]
def rbs_collection_path_prefix(gemspec)

@@ -98,2 +130,5 @@ File.join(work_dir, 'rbs', "#{gemspec.name}-#{gemspec.version}-")

# @param gemspec [Gem::Specification]
# @param hash [String, nil]
# @return [Array<Pin::Base>]
def deserialize_rbs_collection_gem(gemspec, hash)

@@ -103,2 +138,6 @@ load(rbs_collection_path(gemspec, hash))

# @param gemspec [Gem::Specification]
# @param hash [String, nil]
# @param pins [Array<Pin::Base>]n
# @return [void]
def serialize_rbs_collection_gem(gemspec, hash, pins)

@@ -108,2 +147,5 @@ save(rbs_collection_path(gemspec, hash), pins)

# @param gemspec [Gem::Specification]
# @param hash [String, nil]
# @return [String]
def combined_path(gemspec, hash)

@@ -113,2 +155,4 @@ File.join(work_dir, 'combined', "#{gemspec.name}-#{gemspec.version}-#{hash || 0}.ser")

# @param gemspec [Gem::Specification]
# @return [String]
def combined_path_prefix(gemspec)

@@ -118,2 +162,6 @@ File.join(work_dir, 'combined', "#{gemspec.name}-#{gemspec.version}-")

# @param gemspec [Gem::Specification]
# @param hash [String, nil]
# @param pins [Array<Pin::Base>]
# @return [void]
def serialize_combined_gem(gemspec, hash, pins)

@@ -123,2 +171,5 @@ save(combined_path(gemspec, hash), pins)

# @param gemspec [Gem::Specification]
# @param hash [String, nil]
# @return [Array<Pin::Base>]
def deserialize_combined_gem gemspec, hash

@@ -128,2 +179,5 @@ load(combined_path(gemspec, hash))

# @param gemspec [Gem::Specification]
# @param hash [String, nil]
# @return [Boolean]
def has_rbs_collection?(gemspec, hash)

@@ -133,2 +187,3 @@ exist?(rbs_collection_path(gemspec, hash))

# @return [void]
def uncache_core

@@ -138,2 +193,3 @@ uncache(core_path)

# @return [void]
def uncache_stdlib

@@ -143,2 +199,5 @@ uncache(stdlib_path)

# @param gemspec [Gem::Specification]
# @param out [IO, nil]
# @return [void]
def uncache_gem(gemspec, out: nil)

@@ -169,7 +228,8 @@ uncache(yardoc_path(gemspec), out: out)

# @param path [String]
def exist? *path
File.file? join(*path)
File.file? File.join(*path)
end
# @param path [Array<String>]
# @param file [String]
# @param pins [Array<Pin::Base>]

@@ -185,4 +245,4 @@ # @return [void]

# @param path_segments [Array<String>]
# @return [void]
# @param path_segments [Array<String>]
def uncache *path_segments, out: nil

@@ -189,0 +249,0 @@ path = File.join(*path_segments)

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

class BaseVariable < Base
# include Solargraph::Source::NodeMethods
include Solargraph::Parser::NodeMethods
# include Solargraph::Source::NodeMethods

@@ -47,3 +47,2 @@ # @return [Parser::AST::Node, nil]

# @sg-ignore
def nil_assignment?

@@ -50,0 +49,0 @@ # this will always be false - should it be return_type ==

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

# @return [Solargraph::Location]
# @return [Solargraph::Location, nil]
attr_reader :location
# @return [Solargraph::Location]
# @return [Solargraph::Location, nil]
attr_reader :type_location

@@ -32,2 +32,7 @@

# @type [::Numeric, nil] A priority for determining if pins should be combined or not
# A nil priority is considered the be the lowest. All code, yard & rbs pins have nil priority
# Between 2 pins, the one with the higher priority gets chosen. If the priorities are equal, they are combined.
attr_reader :combine_priority
def presence_certain?

@@ -45,3 +50,4 @@ true

# @param directives [::Array<YARD::Tags::Directive>, nil]
def initialize location: nil, type_location: nil, closure: nil, source: nil, name: '', comments: '', docstring: nil, directives: nil
# @param combine_priority [::Numeric, nil] See attr_reader for combine_priority
def initialize location: nil, type_location: nil, closure: nil, source: nil, name: '', comments: '', docstring: nil, directives: nil, combine_priority: nil
@location = location

@@ -56,2 +62,4 @@ @type_location = type_location

@directives = directives
@combine_priority = combine_priority
assert_source_provided

@@ -68,4 +76,11 @@ assert_location_provided

# @return [Pin::Closure, nil]
def closure
Solargraph.assert_or_log(:closure, "Closure not set on #{self.class} #{name.inspect} from #{source.inspect}") unless @closure
# @type [Pin::Closure, nil]
@closure
end
# @param other [self]
# @param attrs [Hash{Symbol => Object}]
# @param attrs [Hash{::Symbol => Object}]
#

@@ -75,2 +90,5 @@ # @return [self]

raise "tried to combine #{other.class} with #{self.class}" unless other.class == self.class
priority_choice = choose_priority(other)
return priority_choice unless priority_choice.nil?
type_location = choose(other, :type_location)

@@ -88,2 +106,3 @@ location = choose(other, :location)

directives: combine_directives(other),
combine_priority: combine_priority
}.merge(attrs)

@@ -98,2 +117,21 @@ assert_same_macros(other)

# @param other [self]
# @return [self, nil] Returns either the pin chosen based on priority or nil
# A nil return means that the combination process must proceed
def choose_priority(other)
if combine_priority.nil? && !other.combine_priority.nil?
return other
elsif other.combine_priority.nil? && !combine_priority.nil?
return self
elsif !combine_priority.nil? && !other.combine_priority.nil?
if combine_priority > other.combine_priority
return self
elsif combine_priority < other.combine_priority
return other
end
end
nil
end
# @param other [self]
# @param attr [::Symbol]

@@ -109,3 +147,2 @@ # @sg-ignore

return val2 if val1.nil?
# @sg-ignore
val1.length > val2.length ? val1 : val2

@@ -200,3 +237,3 @@ end

# @param other [self]
# @param attr [Symbol]
# @param attr [::Symbol]
# @sg-ignore

@@ -250,3 +287,2 @@ # @return [undefined]

# @sg-ignore
# @type [undefined]

@@ -283,3 +319,4 @@ values1 = arr1.map(&block)

#
# @return [Object, nil]
# @sg-ignore
# @return [undefined]
def assert_same(other, attr)

@@ -315,2 +352,4 @@ return false if other.nil?

# @param attr [::Symbol]
#
# @sg-ignore
# @return [undefined]

@@ -323,7 +362,10 @@ def choose_pin_attr(other, attr)

if val1.class != val2.class
# :nocov:
Solargraph.assert_or_log("combine_with_#{attr}_class".to_sym,
"Inconsistent #{attr.inspect} class values between \nself =#{inspect} and \nother=#{other.inspect}:\n\n self.#{attr} = #{val1.inspect}\nother.#{attr} = #{val2.inspect}")
return val1
# :nocov:
end
# arbitrary way of choosing a pin
# @sg-ignore Need _1 support
[val1, val2].compact.min_by { _1.best_location.to_s }

@@ -330,0 +372,0 @@ end

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

# @return [String]
def method_namespace

@@ -29,2 +30,4 @@ closure.namespace

# @param other [self]
# @return [Pin::Block, nil]
def combine_blocks(other)

@@ -62,2 +65,4 @@ if block.nil?

# @param other [self]
# @return [Array<Pin::Parameter>]
def choose_parameters(other)

@@ -76,2 +81,3 @@ raise "Trying to combine two pins with different arities - \nself =#{inspect}, \nother=#{other.inspect}, \n\n self.arity=#{self.arity}, \nother.arity=#{other.arity}" if other.arity != arity

# @return [Array<Pin::Parameter>]
def blockless_parameters

@@ -85,2 +91,3 @@ if parameters.last&.block?

# @return [Array]
def arity

@@ -133,2 +140,3 @@ [generics, blockless_parameters.map(&:arity_decl), block&.arity]

# @return [String]
def method_name

@@ -206,2 +214,3 @@ raise "closure was nil in #{self.inspect}" if closure.nil?

# @return [Integer]
def mandatory_positional_param_count

@@ -208,0 +217,0 @@ parameters.count(&:arg?)

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

# @param generics [::Array<Pin::Parameter>, nil]
# @param generic_defaults [Hash{String => ComplexType}]
def initialize scope: :class, generics: nil, generic_defaults: {}, **splat

@@ -19,2 +20,3 @@ super(**splat)

# @return [Hash{String => ComplexType}]
def generic_defaults

@@ -21,0 +23,0 @@ @generic_defaults ||= {}

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

module Common
# @!method source
# @abstract
# @return [Source, nil]
# @type @closure [Pin::Closure, nil]
# @return [Location]
attr_reader :location
# @sg-ignore Solargraph::Pin::Common#closure return type could not be inferred
# @return [Pin::Closure, nil]
attr_reader :closure
def closure

@@ -14,0 +18,0 @@ Solargraph.assert_or_log(:closure, "Closure not set on #{self.class} #{name.inspect} from #{source.inspect}") unless @closure

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

# @param visibility [::Symbol] The visibility of the constant (:public, :protected, or :private)
# @param splat [Hash] Additional options supported by superclasses
def initialize visibility: :public, **splat

@@ -10,0 +12,0 @@ super(**splat)

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

define_method(method) do |api_map|
# @sg-ignore Unresolved call to resolve_method
resolve_method(api_map)

@@ -56,0 +57,0 @@ # @sg-ignore Need to set context correctly in define_method blocks

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

}.merge(attrs)
new_attrs[:presence] = assert_same(other, :presence) unless attrs.key?(:presence)
# @sg-ignore Wrong argument type for
# Solargraph::Pin::Base#assert_same: other expected
# Solargraph::Pin::Base, received self
new_attrs[:presence] = assert_same(other, :presence) unless attrs.key?(:presence)

@@ -32,0 +35,0 @@ super(other, new_attrs)

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

# @param scope [::Symbol]
# @param original [String, nil] The name of the original method
# @param splat [Hash] Additional options supported by superclasses
def initialize scope: :instance, original: nil, **splat

@@ -18,0 +21,0 @@ super(**splat)

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

# @param anon_splat [Boolean]
def initialize visibility: :public, explicit: true, block: :undefined, node: nil, attribute: false, signatures: nil, anon_splat: false, **splat
def initialize visibility: :public, explicit: true, block: :undefined, node: nil, attribute: false, signatures: nil, anon_splat: false,
**splat
super(**splat)

@@ -54,3 +55,3 @@ @visibility = visibility

# @param other [Pin::Method]
# @return [Symbol]
# @return [::Symbol]
def combine_visibility(other)

@@ -81,2 +82,5 @@ if dodgy_visibility_source? && !other.dodgy_visibility_source?

def combine_with(other, attrs = {})
priority_choice = choose_priority(other)
return priority_choice unless priority_choice.nil?
sigs = combine_signatures(other)

@@ -307,3 +311,2 @@ parameters = if sigs.length > 0

# @sg-ignore
def documentation

@@ -432,3 +435,3 @@ if @documentation.nil?

docstring.ref_tags.each do |tag|
ref = if tag.owner.to_s.start_with?(/[#\.]/)
ref = if tag.owner.to_s.start_with?(/[#.]/)
api_map.get_methods(namespace)

@@ -557,3 +560,3 @@ .select { |pin| pin.path.end_with?(tag.owner.to_s) }

def resolve_reference ref, api_map
parts = ref.split(/[\.#]/)
parts = ref.split(/[.#]/)
if parts.first.empty? || parts.one?

@@ -560,0 +563,0 @@ path = "#{namespace}#{ref}"

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

# @return [String]
def full
# @return [String] the full name of the parameter, including any
# declarative symbols such as `*` or `:` indicating type of
# parameter. This is used in method signatures.
def full_name
case decl
when :optarg
"#{name} = #{asgn_code || '?'}"
when :kwarg
when :kwarg, :kwoptarg
"#{name}:"
when :kwoptarg
"#{name}: #{asgn_code || '?'}"
when :restarg

@@ -129,2 +127,14 @@ "*#{name}"

# @return [String]
def full
full_name + case decl
when :optarg
" = #{asgn_code || '?'}"
when :kwoptarg
" #{asgn_code || '?'}"
else
''
end
end
# @return [ComplexType]

@@ -242,3 +252,3 @@ def return_type

skip.push ref
parts = ref.split(/[\.#]/)
parts = ref.split(/[.#]/)
if parts.first.empty?

@@ -245,0 +255,0 @@ path = "#{namespace}#{ref}"

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

# @param return_type [ComplexType]
# @param binder [ComplexType, ComplexType::UniqueType, nil]
def initialize return_type: ComplexType::UNDEFINED, binder: nil, **splat

@@ -9,0 +10,0 @@ super(**splat)

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

end
# @return [String]
def parameter_tag
@parameter_tag ||= if generic_values&.any?
"<#{generic_values.join(', ')}>"
else
''
end
end
# @return [ComplexType]
def parametrized_tag
@parametrized_tag ||= ComplexType.try_parse(
name +
if generic_values&.length&.> 0
"<#{generic_values.join(', ')}>"
else
''
end
)
end
# @return [Array<String>]
def reference_gates
closure.gates
end
end
end
end

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

# @param location [Location, nil]
# @param name [String]
# @param tags [::Array<YARD::Tags::Tag>]
# @param delete [::Array<Symbol>]
# @param splat [Hash]
def initialize location, name, tags, delete = [], **splat

@@ -24,6 +29,15 @@ super(location: location, name: name, **splat)

# @param name [String]
# @param tags [::Array<String>]
# @param delete [::Array<Symbol>]
# @param splat [Hash]
# @return [Solargraph::Pin::Reference::Override]
def self.method_return name, *tags, delete: [], **splat
new(nil, name, [YARD::Tags::Tag.new('return', nil, tags)], delete, **splat)
new(nil, name, [YARD::Tags::Tag.new('return', '', tags)], delete, **splat)
end
# @param name [String]
# @param comment [String]
# @param splat [Hash]
# @return [Solargraph::Pin::Reference::Override]
def self.from_comment name, comment, **splat

@@ -30,0 +44,0 @@ new(nil, name, Solargraph::Source.parse_docstring(comment).to_docstring.tags, **splat)

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

class Reference
# A Superclass reference pin.
#
class Superclass < Reference
def reference_gates
@reference_gates ||= closure.gates - [closure.path]
end
end

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

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

# @param match [Float] The match score for the pin
# @param pin [Pin::Base]
def initialize match, pin

@@ -52,3 +54,3 @@ @match = match

def fuzzy_string_match str1, str2
return (1.0 + (str2.length.to_f / str1.length.to_f)) if str1.downcase.include?(str2.downcase)
return 1.0 + (str2.length.to_f / str1.length.to_f) if str1.downcase.include?(str2.downcase)
JaroWinkler.similarity(str1, str2, ignore_case: true)

@@ -55,0 +57,0 @@ end

@@ -42,2 +42,4 @@ module Solargraph

return ComplexType::UNDEFINED unless closure.is_a?(Pin::Method)
# @sg-ignore need is_a? support
# @type [Array<Pin::Method>]
method_stack = closure.rest_of_stack api_map

@@ -44,0 +46,0 @@ logger.debug { "Signature#typify(self=#{self}) - method_stack: #{method_stack}" }

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

def closure
@closure ||= Pin::ROOT_PIN
end
def completion_item_kind

@@ -40,2 +44,3 @@ Solargraph::LanguageServer::CompletionItemKinds::KEYWORD

# @return [::Symbol]
def visibility

@@ -42,0 +47,0 @@ :public

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

# @param other [Position]
def <=>(other)

@@ -61,2 +62,3 @@ return nil unless other.is_a?(Position)

return 0 if text.empty?
# @sg-ignore Unresolved call to + on Integer
text.lines[0...position.line].sum(&:length) + position.character

@@ -63,0 +65,0 @@ end

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

# @param other [BasicObject]
def <=>(other)

@@ -29,0 +30,0 @@ return nil unless other.is_a?(Range)

# frozen_string_literal: true
require 'digest'
require 'pathname'

@@ -25,3 +26,4 @@ require 'rbs'

# @param library [String]
# @param version [String, nil]
# @param version [String, nil
# @param rbs_collection_config_path [String, Pathname, nil]
# @param rbs_collection_paths [Array<Pathname, String>]

@@ -39,2 +41,3 @@ def initialize library, version = nil, rbs_collection_config_path: nil, rbs_collection_paths: []

# @return [RBS::EnvironmentLoader]
def loader

@@ -44,3 +47,3 @@ @loader ||= RBS::EnvironmentLoader.new(core_root: nil, repository: repository)

# @return string representing the version of the RBS info fetched
# @return [String] representing the version of the RBS info fetched
# for the given library. Must change when the RBS info is

@@ -51,2 +54,3 @@ # updated upstream for the same library and version. May change

@hextdigest ||= begin
# @type [String, nil]
data = nil

@@ -75,2 +79,6 @@ if rbs_collection_config_path

# @param gemspec [Gem::Specification]
# @param rbs_collection_path [String, Pathname, nil]
# @param rbs_collection_config_path [String, Pathname, nil]
# @return [RbsMap]
def self.from_gemspec gemspec, rbs_collection_path, rbs_collection_config_path

@@ -88,2 +96,3 @@ rbs_map = RbsMap.new(gemspec.name, gemspec.version,

# @return [Array<Pin::Base>]
def pins

@@ -112,2 +121,3 @@ @pins ||= resolved? ? conversions.pins : []

# @return [RBS::Repository]
def repository

@@ -130,2 +140,3 @@ @repository ||= RBS::Repository.new(no_stdlib: false).tap do |repo|

# @return [RBS::EnvironmentLoader]
def loader

@@ -135,2 +146,3 @@ @loader ||= RBS::EnvironmentLoader.new(core_root: nil, repository: repository)

# @return [Conversions]
def conversions

@@ -142,2 +154,3 @@ @conversions ||= Conversions.new(loader: loader)

# @param library [String]
# @param version [String, nil]
# @return [Boolean] true if adding the library succeeded

@@ -144,0 +157,0 @@ def add_library loader, library, version

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

end
class_name = decl.name.relative!.to_s
class_pin = Solargraph::Pin::Namespace.new(
type: :class,
name: decl.name.relative!.to_s,
name: class_name,
closure: Solargraph::Pin::ROOT_PIN,

@@ -184,2 +185,3 @@ comments: decl.comment&.string,

generic_values = type.all_params.map(&:to_s)
superclass_name = decl.super_class.name.to_s
pins.push Solargraph::Pin::Reference::Superclass.new(

@@ -189,3 +191,3 @@ type_location: location_decl_to_pin_location(decl.super_class.location),

generic_values: generic_values,
name: decl.super_class.name.relative!.to_s,
name: superclass_name,
source: :rbs

@@ -351,3 +353,3 @@ )

# @param decl [RBS::AST::Members::MethodDefinition, RBS::AST::Members::AttrReader, RBS::AST::Members::AttrAccessor]
# @param closure [Pin::Namespace]
# @param closure [Pin::Closure]
# @param context [Context]

@@ -706,3 +708,3 @@ # @param scope [Symbol] :instance or :class

base = RBS_TO_YARD_TYPE[type_name.relative!.to_s] || type_name.relative!.to_s
params = type_args.map { |a| other_type_to_tag(a) }.reject { |t| t == 'undefined' }.map do |t|
params = type_args.map { |a| other_type_to_tag(a) }.map do |t|
ComplexType.try_parse(t).force_rooted

@@ -713,3 +715,3 @@ end

else
ComplexType::UniqueType.new(base, [], params, rooted: true, parameters_type: :list)
ComplexType::UniqueType.new(base, [], params.reject(&:undefined?), rooted: true, parameters_type: :list)
end

@@ -716,0 +718,0 @@ end

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

# @return [Enumerable<Pin::Base>]
def pins

@@ -43,2 +44,3 @@ return @pins if @pins

# @return [RBS::EnvironmentLoader]
def loader

@@ -48,2 +50,3 @@ @loader ||= RBS::EnvironmentLoader.new(repository: RBS::Repository.new(no_stdlib: false))

# @return [Conversions]
def conversions

@@ -50,0 +53,0 @@ @conversions ||= Conversions.new(loader: loader)

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

end
# @sg-ignore Wrong argument type for Backport.prepare_tcp_server: adapter expected Backport::Adapter, received Module<Solargraph::LanguageServer::Transport::Adapter>
Backport.prepare_tcp_server host: options[:host], port: port, adapter: Solargraph::LanguageServer::Transport::Adapter

@@ -56,2 +57,3 @@ STDERR.puts "Solargraph is listening PORT=#{port} PID=#{Process.pid}"

end
# @sg-ignore Wrong argument type for Backport.prepare_stdio_server: adapter expected Backport::Adapter, received Module<Solargraph::LanguageServer::Transport::Adapter>
Backport.prepare_stdio_server adapter: Solargraph::LanguageServer::Transport::Adapter

@@ -263,2 +265,3 @@ STDERR.puts "Solargraph is listening on stdio PID=#{Process.pid}"

# @param gemspec [Gem::Specification]
# @param api_map [ApiMap]
# @return [void]

@@ -265,0 +268,0 @@ def do_cache gemspec, api_map

@@ -24,2 +24,7 @@ # frozen_string_literal: true

# @return [Array<Pin::Base>]
def all_pins
pins + convention_pins
end
# @return [Array<Pin::LocalVariable>]

@@ -34,9 +39,14 @@ def locals

environ.merge Convention.for_local(self) unless filename.nil?
self.convention_pins = environ.pins
conventions_environ.merge Convention.for_local(self) unless filename.nil?
# FIXME: unmemoizing the document_symbols in case it was called and memoized from any of conventions above
# this is to ensure that the convention_pins from all conventions are used in the document_symbols.
# solargraph-rails is known to use this method to get the document symbols. It should probably be removed.
@document_symbols = nil
self.convention_pins = conventions_environ.pins
@pin_select_cache = {}
end
# @param klass [Class]
# @return [Array<Pin::Base>]
# @generic T
# @param klass [Class<generic<T>>]
# @return [Array<generic<T>>]
def pins_by_class klass

@@ -72,4 +82,4 @@ @pin_select_cache[klass] ||= pin_class_hash.select { |key, _| key <= klass }.values.flatten

# @return [Environ]
def environ
@environ ||= Environ.new
def conventions_environ
@conventions_environ ||= Environ.new
end

@@ -164,2 +174,6 @@

# @return [Hash{Class => Array<Pin::Base>}]
# @return [Array<Pin::Base>]
attr_writer :convention_pins
def pin_class_hash

@@ -169,2 +183,3 @@ @pin_class_hash ||= pins.to_set.classify(&:class).transform_values(&:to_a)

# @return [Data]
def data

@@ -179,10 +194,2 @@ @data ||= Data.new(source)

# @param pins [Array<Pin::Base>]
# @return [Array<Pin::Base>]
def convention_pins=(pins)
# unmemoizing the document_symbols in case it was called from any of conventions
@document_symbols = nil
@convention_pins = pins
end
# @param line [Integer]

@@ -189,0 +196,0 @@ # @param character [Integer]

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

#
# @return [::Array<Solargraph::Pin::Base>]
# @return [::Array<Solargraph::Pin::LocalVariable>]
def locals

@@ -71,0 +71,0 @@ @locals ||= source_map.locals_at(location)

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

class Data
# @param source [Solargraph::Source]
def initialize source

@@ -11,2 +12,3 @@ @source = source

# @return [Array<Solargraph::Pin::Base>]
def pins

@@ -17,2 +19,3 @@ generate

# @return [Array<Solargraph::LocalVariable>]
def locals

@@ -25,2 +28,3 @@ generate

# @return [void]
def generate

@@ -27,0 +31,0 @@ return if @generated

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

def process_comment source_position, comment_position, comment
# @sg-ignore Wrong argument type for String#=~: object expected String::_MatchAgainst<String, undefined>, received Regexp
return unless comment.encode('UTF-8', invalid: :replace, replace: '?') =~ DIRECTIVE_REGEXP

@@ -167,3 +168,3 @@ cmnt = remove_inline_comment_hashes(comment)

when 'visibility'
begin
kind = directive.tag.text&.to_sym

@@ -187,3 +188,3 @@ return unless [:private, :protected, :public].include?(kind)

end
end
when 'parse'

@@ -250,2 +251,3 @@ begin

def process_comment_directives
# @sg-ignore Wrong argument type for String#=~: object expected String::_MatchAgainst<String, undefined>, received Regexp
return unless @code.encode('UTF-8', invalid: :replace, replace: '?') =~ DIRECTIVE_REGEXP

@@ -252,0 +254,0 @@ code_lines = @code.lines

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

# @return [Hash{Integer => Array<String>}]
# @return [Hash{Integer => Solargraph::Parser::Snippet}]
def comments

@@ -239,2 +239,3 @@ finalize

@associated_comments ||= begin
# @type [Hash{Integer => String}]
result = {}

@@ -322,3 +323,3 @@ buffer = String.new('')

# @return [Array<Range>]
# @return [Array<Solargraph::Range>]
def comment_ranges

@@ -392,2 +393,3 @@ @comment_ranges ||= comments.values.map(&:range)

# @return [void]
def finalize

@@ -447,2 +449,3 @@ return if @finalized && changes.empty?

# @return [String]
def repaired

@@ -449,0 +452,0 @@ finalize

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

include Equality
include Logging

@@ -79,3 +80,5 @@ autoload :Link, 'solargraph/source/chain/link'

#
# @param api_map [ApiMap] @param name_pin [Pin::Base] A pin
# @param api_map [ApiMap]
#
# @param name_pin [Pin::Base] A pin
# representing the place in which expression is evaluated (e.g.,

@@ -197,2 +200,3 @@ # a Method pin, or a Module or Class pin if not run within a

# @return [String]
def desc

@@ -271,3 +275,6 @@ links.map(&:desc).to_s

end
return type if context.nil? || context.return_type.undefined?
if context.nil? || context.return_type.undefined?
# up to downstream to resolve self type
return type
end

@@ -274,0 +281,0 @@ type.self_to_type(context.return_type)

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

# @sg-ignore Fix "Not enough arguments to Module#protected"
# @sg-ignore two problems - Declared return type
# ::Solargraph::Source::Chain::Array does not match inferred
# type ::Array(::Class<::Solargraph::Source::Chain::Link>,
# ::String) for
# Solargraph::Source::Chain::Link#equality_fields
# and
# Not enough arguments to Module#protected
protected def equality_fields

@@ -43,2 +49,3 @@ [self.class, word]

# debugging description of contents; not for machine use
# @return [String]
def desc

@@ -79,2 +86,4 @@ word

# debugging description of contents; not for machine use
#
# @return [String]
def desc

@@ -81,0 +90,0 @@ word

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

def write text, nullable = false
if nullable and !range.nil? and new_text.match(/[\.\[\{\(@\$:]$/)
if nullable and !range.nil? and new_text.match(/[.\[{(@$:]$/)
[':', '@'].each do |dupable|

@@ -63,3 +63,3 @@ next unless new_text == dupable

off = Position.to_offset(text, range.start)
match = result[0, off].match(/[\.:]+\z/)
match = result[0, off].match(/[.:]+\z/)
if match

@@ -66,0 +66,0 @@ result = result[0, off].sub(/#{match[0]}\z/, ' ' * match[0].length) + result[off..-1]

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

#
# @sg-ignore Improve resolution of String#match below
# @return [String]

@@ -129,3 +128,3 @@ def start_of_word

if start_of_word.empty?
match = source.code[0, offset].match(/[\s]*(\.|:+)[\s]*$/)
match = source.code[0, offset].match(/\s*(\.|:+)\s*$/)
if match

@@ -165,3 +164,3 @@ Position.from_offset(source.code, offset - match[0].length)

def end_word_pattern
/^([a-z0-9_]|[^\u0000-\u007F])*[\?\!]?/i
/^([a-z0-9_]|[^\u0000-\u007F])*[?!]?/i
end

@@ -168,0 +167,0 @@ end

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

@end_of_phrase ||= begin
match = phrase.match(/[\s]*(\.{1}|::)[\s]*$/)
match = phrase.match(/\s*(\.{1}|::)\s*$/)
if match

@@ -103,0 +103,0 @@ match[0]

@@ -41,11 +41,16 @@ # frozen_string_literal: true

# @return [Source]
def source
@source_map.source
end
# @return [Array<Problem>]
def problems
@problems ||= begin
without_ignored(
method_tag_problems
.concat variable_type_tag_problems
.concat const_problems
.concat call_problems
)
all = method_tag_problems
.concat(variable_type_tag_problems)
.concat(const_problems)
.concat(call_problems)
unignored = without_ignored(all)
unignored.concat(unneeded_sgignore_problems)
end

@@ -144,3 +149,3 @@ end

def virtual_pin? pin
pin.location && source_map.source.comment_at?(pin.location.range.ending)
pin.location && source.comment_at?(pin.location.range.ending)
end

@@ -236,3 +241,3 @@

result = []
Solargraph::Parser::NodeMethods.const_nodes_from(source_map.source.node).each do |const|
Solargraph::Parser::NodeMethods.const_nodes_from(source.node).each do |const|
rng = Solargraph::Range.from_node(const)

@@ -255,3 +260,3 @@ chain = Solargraph::Parser.chain(const, filename)

result = []
Solargraph::Parser::NodeMethods.call_nodes_from(source_map.source.node).each do |call|
Solargraph::Parser::NodeMethods.call_nodes_from(source.node).each do |call|
rng = Solargraph::Range.from_node(call)

@@ -279,3 +284,7 @@ next if @marked_ranges.any? { |d| d.contain?(rng.start) }

unless closest.generic? || ignored_pins.include?(found)
result.push Problem.new(location, "Unresolved call to #{missing.links.last.word}")
if closest.defined?
result.push Problem.new(location, "Unresolved call to #{missing.links.last.word} on #{closest}")
else
result.push Problem.new(location, "Unresolved call to #{missing.links.last.word}")
end
@marked_ranges.push rng

@@ -292,123 +301,132 @@ end

# @param api_map [Solargraph::ApiMap]
# @param block_pin [Solargraph::Pin::Base]
# @param closure_pin [Solargraph::Pin::Closure]
# @param locals [Array<Solargraph::Pin::Base>]
# @param location [Solargraph::Location]
# @return [Array<Problem>]
def argument_problems_for chain, api_map, block_pin, locals, location
def argument_problems_for chain, api_map, closure_pin, locals, location
result = []
base = chain
until base.links.length == 1 && base.undefined?
last_base_link = base.links.last
break unless last_base_link.is_a?(Solargraph::Source::Chain::Call)
# @type last_base_link [Solargraph::Source::Chain::Call]
last_base_link = base.links.last
return [] unless last_base_link.is_a?(Solargraph::Source::Chain::Call)
arguments = last_base_link.arguments
arguments = last_base_link.arguments
pins = base.define(api_map, block_pin, locals)
pins = base.define(api_map, closure_pin, locals)
first_pin = pins.first
if first_pin.is_a?(Pin::DelegatedMethod) && !first_pin.resolvable?(api_map)
# Do nothing, as we can't find the actual method implementation
elsif first_pin.is_a?(Pin::Method)
# @type [Pin::Method]
pin = first_pin
ap = if base.links.last.is_a?(Solargraph::Source::Chain::ZSuper)
arity_problems_for(pin, fake_args_for(block_pin), location)
elsif pin.path == 'Class#new'
fqns = if base.links.one?
block_pin.namespace
first_pin = pins.first
unresolvable = first_pin.is_a?(Pin::DelegatedMethod) && !first_pin.resolvable?(api_map)
if !unresolvable && first_pin.is_a?(Pin::Method)
# @type [Pin::Method]
pin = first_pin
ap = if base.links.last.is_a?(Solargraph::Source::Chain::ZSuper)
arity_problems_for(pin, fake_args_for(closure_pin), location)
elsif pin.path == 'Class#new'
fqns = if base.links.one?
closure_pin.namespace
else
base.base.infer(api_map, closure_pin, locals).namespace
end
init = api_map.get_method_stack(fqns, 'initialize').first
init ? arity_problems_for(init, arguments, location) : []
else
arity_problems_for(pin, arguments, location)
end
return ap unless ap.empty?
return [] if !rules.validate_calls? || base.links.first.is_a?(Solargraph::Source::Chain::ZSuper)
params = first_param_hash(pins)
all_errors = []
pin.signatures.sort { |sig| sig.parameters.length }.each do |sig|
signature_errors = signature_argument_problems_for location, locals, closure_pin, params, arguments, sig, pin
if signature_errors.empty?
# we found a signature that works - meaning errors from
# other signatures don't matter.
return []
end
all_errors.concat signature_errors
end
result.concat all_errors
end
result
end
# @param location [Location]
# @param locals [Array<Pin::LocalVariable>]
# @param closure_pin [Pin::Closure]
# @param params [Hash{String => Hash{Symbol => String, Solargraph::ComplexType}}]
# @param arguments [Array<Source::Chain>]
# @param sig [Pin::Signature]
# @param pin [Pin::Method]
# @param pins [Array<Pin::Method>]
#
# @return [Array<Problem>]
def signature_argument_problems_for location, locals, closure_pin, params, arguments, sig, pin
errors = []
# @todo add logic mapping up restarg parameters with
# arguments (including restarg arguments). Use tuples
# when possible, and when not, ensure provably
# incorrect situations are detected.
sig.parameters.each_with_index do |par, idx|
return errors if par.decl == :restarg # bail out and assume the rest is valid pending better arg processing
argchain = arguments[idx]
if argchain.nil?
if par.decl == :arg
final_arg = arguments.last
if final_arg && final_arg.node.type == :splat
argchain = final_arg
return errors
else
base.base.infer(api_map, block_pin, locals).namespace
errors.push Problem.new(location, "Not enough arguments to #{pin.path}")
end
init = api_map.get_method_stack(fqns, 'initialize').first
init ? arity_problems_for(init, arguments, location) : []
else
arity_problems_for(pin, arguments, location)
final_arg = arguments.last
argchain = final_arg if final_arg && [:kwsplat, :hash].include?(final_arg.node.type)
end
unless ap.empty?
result.concat ap
break
end
break if !rules.validate_calls? || base.links.first.is_a?(Solargraph::Source::Chain::ZSuper)
end
if argchain
if par.decl != :arg
errors.concat kwarg_problems_for sig, argchain, api_map, closure_pin, locals, location, pin, params, idx
next
else
if argchain.node.type == :splat && argchain == arguments.last
final_arg = argchain
end
if (final_arg && final_arg.node.type == :splat)
# The final argument given has been seen and was a
# splat, which doesn't give us useful types or
# arities against positional parameters, so let's
# continue on in case there are any required
# kwargs we should warn about
next
end
if argchain.node.type == :splat && par != sig.parameters.last
# we have been given a splat and there are more
# arguments to come.
params = first_param_hash(pins)
all_errors = []
pin.signatures.sort { |sig| sig.parameters.length }.each do |sig|
errors = []
sig.parameters.each_with_index do |par, idx|
# @todo add logic mapping up restarg parameters with
# arguments (including restarg arguments). Use tuples
# when possible, and when not, ensure provably
# incorrect situations are detected.
break if par.decl == :restarg # bail out pending better arg processing
argchain = arguments[idx]
if argchain.nil?
if par.decl == :arg
final_arg = arguments.last
if final_arg && final_arg.node.type == :splat
argchain = final_arg
next # don't try to apply the type of the splat - unlikely to be specific enough
else
errors.push Problem.new(location, "Not enough arguments to #{pin.path}")
next
end
else
final_arg = arguments.last
argchain = final_arg if final_arg && [:kwsplat, :hash].include?(final_arg.node.type)
end
# @todo Improve this so that we can skip past the
# rest of the positional parameters here but still
# process the kwargs
return errors
end
ptype = params.key?(par.name) ? params[par.name][:qualified] : ComplexType::UNDEFINED
ptype = ptype.self_to_type(par.context)
if ptype.nil?
# @todo Some level (strong, I guess) should require the param here
else
argtype = argchain.infer(api_map, closure_pin, locals)
if argtype.defined? && ptype.defined? && !any_types_match?(api_map, ptype, argtype)
errors.push Problem.new(location, "Wrong argument type for #{pin.path}: #{par.name} expected #{ptype}, received #{argtype}")
return errors
end
if argchain
if par.decl != :arg
errors.concat kwarg_problems_for sig, argchain, api_map, block_pin, locals, location, pin, params, idx
next
else
if argchain.node.type == :splat && argchain == arguments.last
final_arg = argchain
end
if (final_arg && final_arg.node.type == :splat)
# The final argument given has been seen and was a
# splat, which doesn't give us useful types or
# arities against positional parameters, so let's
# continue on in case there are any required
# kwargs we should warn about
next
end
if argchain.node.type == :splat && par != sig.parameters.last
# we have been given a splat and there are more
# arguments to come.
# @todo Improve this so that we can skip past the
# rest of the positional parameters here but still
# process the kwargs
break
end
ptype = params.key?(par.name) ? params[par.name][:qualified] : ComplexType::UNDEFINED
ptype = ptype.self_to_type(par.context)
if ptype.nil?
# @todo Some level (strong, I guess) should require the param here
else
argtype = argchain.infer(api_map, block_pin, locals)
if argtype.defined? && ptype.defined? && !any_types_match?(api_map, ptype, argtype)
errors.push Problem.new(location, "Wrong argument type for #{pin.path}: #{par.name} expected #{ptype}, received #{argtype}")
next
end
end
end
elsif par.decl == :kwarg
errors.push Problem.new(location, "Call to #{pin.path} is missing keyword argument #{par.name}")
next
end
end
if errors.empty?
all_errors.clear
break
end
all_errors.concat errors
end
result.concat all_errors
elsif par.decl == :kwarg
errors.push Problem.new(location, "Call to #{pin.path} is missing keyword argument #{par.name}")
next
end
base = base.base
end
result
errors
end

@@ -655,3 +673,2 @@

# signature and generate Integer as return type
# @sg-ignore
# @return [Integer]

@@ -697,2 +714,31 @@ def required_param_count(parameters)

# @return [Set<Integer>]
def sg_ignore_lines_processed
@sg_ignore_lines_processed ||= Set.new
end
# @return [Set<Integer>]
def all_sg_ignore_lines
source.associated_comments.select do |_line, text|
text.include?('@sg-ignore')
end.keys.to_set
end
# @return [Array<Integer>]
def unprocessed_sg_ignore_lines
(all_sg_ignore_lines - sg_ignore_lines_processed).to_a.sort
end
# @return [Array<Problem>]
def unneeded_sgignore_problems
return [] unless rules.validate_sg_ignores?
unprocessed_sg_ignore_lines.map do |line|
Problem.new(
Location.new(filename, Range.from_to(line, 0, line, 0)),
'Unneeded @sg-ignore comment'
)
end
end
# @param problems [Array<Problem>]

@@ -702,4 +748,11 @@ # @return [Array<Problem>]

problems.reject do |problem|
node = source_map.source.node_at(problem.location.range.start.line, problem.location.range.start.column)
node && source_map.source.comments_for(node)&.include?('@sg-ignore')
node = source.node_at(problem.location.range.start.line, problem.location.range.start.column)
ignored = node && source.comments_for(node)&.include?('@sg-ignore')
unless !ignored || all_sg_ignore_lines.include?(problem.location.range.start.line)
# :nocov:
Solargraph.assert_or_log(:sg_ignore) { "@sg-ignore accounting issue - node is #{node}" }
# :nocov:
end
sg_ignore_lines_processed.add problem.location.range.start.line if ignored
ignored
end

@@ -706,0 +759,0 @@ end

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

# @param name [String]
# @param type [Symbol] The type of parameter, such as :req, :opt, :rest, etc.
def initialize name, type

@@ -17,0 +19,0 @@ @name = name

@@ -60,4 +60,12 @@ # frozen_string_literal: true

end
# We keep this at strong because if you added an @ sg-ignore to
# address a strong-level issue, then ran at a lower level, you'd
# get a false positive - we don't run stronger level checks than
# requested for performance reasons
def validate_sg_ignores?
rank >= LEVELS[:strong]
end
end
end
end
# frozen_string_literal: true
module Solargraph
VERSION = '0.56.2'
VERSION = '0.57.0'
end

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

autoload :Config, 'solargraph/workspace/config'
autoload :RequirePaths, 'solargraph/workspace/require_paths'

@@ -18,8 +19,3 @@ # @return [String]

# The require paths associated with the workspace.
#
# @return [Array<String>]
attr_reader :require_paths
# @return [Array<String>]
attr_reader :gemnames

@@ -37,6 +33,13 @@ alias source_gems gemnames

@gemnames = []
@require_paths = generate_require_paths
require_plugins
end
# The require paths associated with the workspace.
#
# @return [Array<String>]
def require_paths
# @todo are the semantics of '*' the same as '', meaning 'don't send back any require paths'?
@require_paths ||= RequirePaths.new(directory_or_nil, config).generate
end
# @return [Solargraph::Workspace::Config]

@@ -139,2 +142,3 @@ def config

# @return [String, nil]
def rbs_collection_config_path

@@ -162,2 +166,8 @@ @rbs_collection_config_path ||= begin

# @return [String, nil]
def directory_or_nil
return nil if directory.empty? || directory == '*'
directory
end
# True if the workspace has a root Gemfile.

@@ -200,44 +210,2 @@ #

# Generate require paths from gemspecs if they exist or assume the default
# lib directory.
#
# @return [Array<String>]
def generate_require_paths
return configured_require_paths unless gemspec?
result = []
gemspecs.each do |file|
base = File.dirname(file)
# HACK: Evaluating gemspec files violates the goal of not running
# workspace code, but this is how Gem::Specification.load does it
# anyway.
cmd = ['ruby', '-e', "require 'rubygems'; require 'json'; spec = eval(File.read('#{file}'), TOPLEVEL_BINDING, '#{file}'); return unless Gem::Specification === spec; puts({name: spec.name, paths: spec.require_paths}.to_json)"]
o, e, s = Open3.capture3(*cmd)
if s.success?
begin
hash = o && !o.empty? ? JSON.parse(o.split("\n").last) : {}
next if hash.empty?
@gemnames.push hash['name']
result.concat(hash['paths'].map { |path| File.join(base, path) })
rescue StandardError => e
Solargraph.logger.warn "Error reading #{file}: [#{e.class}] #{e.message}"
end
else
Solargraph.logger.warn "Error reading #{file}"
Solargraph.logger.warn e
end
end
result.concat(config.require_paths.map { |p| File.join(directory, p) })
result.push File.join(directory, 'lib') if result.empty?
result
end
# Get additional require paths defined in the configuration.
#
# @return [Array<String>]
def configured_require_paths
return ['lib'] if directory.empty?
return [File.join(directory, 'lib')] if config.require_paths.empty?
config.require_paths.map { |p| File.join(directory, p) }
end
# @return [void]

@@ -244,0 +212,0 @@ def require_plugins

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

#
# @sg-ignore pending https://github.com/castwide/solargraph/pull/905
# @return [Hash]

@@ -109,3 +108,2 @@ def formatter

#
# @sg-ignore pending https://github.com/castwide/solargraph/pull/905
# @return [Integer]

@@ -112,0 +110,0 @@ def max_files

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

final_visibility = VISIBILITY_OVERRIDE[override_key]
final_visibility ||= VISIBILITY_OVERRIDE[override_key[0..-2]]
final_visibility ||= :private if closure.path == 'Kernel' && Kernel.private_instance_methods(false).include?(name)
final_visibility ||= VISIBILITY_OVERRIDE[[closure.path, final_scope]]
final_visibility ||= :private if closure.path == 'Kernel' && Kernel.private_instance_methods(false).include?(name.to_sym)
final_visibility ||= visibility

@@ -34,0 +34,0 @@ final_visibility ||= :private if code_object.module_function? && final_scope == :instance

# frozen_string_literal: true
require 'open3'
module Solargraph

@@ -12,5 +14,6 @@ # Methods for caching and loading YARD documentation for gems.

#
# @param yard_plugins [Array<String>] The names of YARD plugins to use.
# @param gemspec [Gem::Specification]
# @return [String] The path to the cached yardoc.
def cache(gemspec)
def cache(yard_plugins, gemspec)
path = PinCache.yardoc_path gemspec

@@ -20,4 +23,13 @@ return path if cached?(gemspec)

Solargraph.logger.info "Caching yardoc for #{gemspec.name} #{gemspec.version}"
Dir.chdir gemspec.gem_dir do
`yardoc --db #{path} --no-output --plugin solargraph`
cmd = "yardoc --db #{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 RBS gem doesn't reflect that Open3.* also include
# kwopts from Process.spawn()
stdout_and_stderr_str, status = Open3.capture2e(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

@@ -37,2 +49,3 @@ path

#
# @param gemspec [Gem::Specification]
def processing?(gemspec)

@@ -39,0 +52,0 @@ yardoc = File.join(PinCache.yardoc_path(gemspec), 'processing')

+125
-13
require 'rake'
require 'rspec/core/rake_task'
require 'bundler/gem_tasks'
require 'fileutils'
require 'open3'
begin
require 'rspec/core/rake_task'
RSpec::Core::RakeTask.new(:spec)
rescue LoadError
end
desc "Open a Pry session preloaded with this library"

@@ -17,10 +12,127 @@ task :console do

desc "Run the type checker"
task :typecheck do
sh "bundle exec solargraph typecheck --level typed"
task typecheck: [:typecheck_typed]
desc "Run the type checker at typed level - return code issues provable without annotations being correct"
task :typecheck_typed do
sh "SOLARGRAPH_ASSERTS=on bundle exec solargraph typecheck --level typed"
end
desc "Run all tests"
task :test do
Rake::Task["typecheck"].invoke
Rake::Task["spec"].invoke
desc "Run the type checker at strict level - report issues using type annotations"
task :typecheck_strict do
sh "SOLARGRAPH_ASSERTS=on bundle exec solargraph typecheck --level strict"
end
desc "Run the type checker at strong level - enforce that type annotations exist"
task :typecheck_strong do
sh "SOLARGRAPH_ASSERTS=on bundle exec solargraph typecheck --level strong"
end
desc "Run the type checker at alpha level - run high-false-alarm checks"
task :typecheck_alpha do
sh "SOLARGRAPH_ASSERTS=on bundle exec solargraph typecheck --level alpha"
end
desc "Run RSpec tests, starting with the ones that failed last time"
task spec: %i[spec_failed undercover_no_fail full_spec] do
undercover
end
desc "Run all RSpec tests"
task :full_spec do
warn 'starting spec'
sh 'TEST_COVERAGE_COMMAND_NAME=full-new bundle exec rspec' # --profile'
warn 'ending spec'
# move coverage/full-new to coverage/full on success so that we
# always have the last successful run's 'coverage info
FileUtils.rm_rf('coverage/full')
FileUtils.mv('coverage/full-new', 'coverage/full')
end
# @sg-ignore #undercover return type could not be inferred
# @return [Process::Status]
def undercover
simplecov_collate
cmd = 'bundle exec undercover ' \
'--simplecov coverage/combined/coverage.json ' \
'--exclude-files "Rakefile,spec/*,spec/**/*,lib/solargraph/version.rb" ' \
'--compare origin/master'
output, status = Bundler.with_unbundled_env do
Open3.capture2e(cmd)
end
puts output
$stdout.flush
status
rescue StandardError => e
warn "hit error: #{e.message}"
warn "Backtrace:\n#{e.backtrace.join("\n")}"
warn "output: #{output}"
puts "Flushing"
$stdout.flush
raise
end
desc "Check PR coverage"
task :undercover do
raise "Undercover failed" unless undercover.success?
end
desc "Branch-focused fast-feedback quality/spec/coverage checks"
task test: %i[overcommit spec typecheck] do
# do these in order
Rake::Task['typecheck_strict'].invoke
Rake::Task['typecheck_strong'].invoke
Rake::Task['typecheck_alpha'].invoke
end
desc "Re-run failed specs. Add --fail-fast in your .rspec-local file if desired."
task :spec_failed do
# allow user to check out any persistent failures while looking for
# more in the whole test suite
sh 'TEST_COVERAGE_COMMAND_NAME=next-failure bundle exec rspec --only-failures || true'
end
desc "Run undercover and show output without failing the task if it fails"
task :undercover_no_fail do
undercover
rescue StandardError
puts "Undercover failed, but continuing with other tasks."
end
# @return [void]
def simplecov_collate
require 'simplecov'
require 'simplecov-lcov'
require 'undercover/simplecov_formatter'
SimpleCov.collate(Dir["coverage/{next-failure,full,ad-hoc}/.resultset.json"]) do
cname = 'combined'
command_name cname
new_dir = File.join('coverage', cname)
coverage_dir new_dir
formatter \
SimpleCov::Formatter::MultiFormatter
.new([
SimpleCov::Formatter::HTMLFormatter,
SimpleCov::Formatter::Undercover,
SimpleCov::Formatter::LcovFormatter
])
SimpleCov::Formatter::LcovFormatter.config.report_with_single_file = true
end
puts "Simplecov collated results into coverage/combined/.resultset.json"
rescue StandardError => e
puts "Simplecov collate failed: #{e.message}"
ensure
$stdout.flush
end
desc 'Add incremental coverage for rapid iteration with undercover'
task :simplecov_collate do
simplecov_collate
end
desc "Show quality checks on this development branch so far, including any staged files"
task :overcommit do
# OVERCOMMIT_DEBUG=1 will show more detail
sh 'SOLARGRAPH_ASSERTS=on bundle exec overcommit --run --diff origin/master'
end

@@ -48,3 +48,2 @@ # <-- liberally borrowed from

| (9 index) -> J
| (0 index) -> K
| (int index) -> nil

@@ -137,3 +136,3 @@

| [T] (9 index, T default) -> (J | T)
| [T] (int index, T default) -> (A | B | C | D | E | F |G | H | I | J | T)
| [T] (int index, T default) -> (A | B | C | D | E | F | G | H | I | J | T)
| [T] (0 index) { (int index) -> T } -> (A | T)

@@ -149,5 +148,5 @@ | [T] (1 index) { (int index) -> T } -> (B | T)

| [T] (9 index) { (int index) -> T } -> (J | T)
| [T] (int index) { (int index) -> T } -> (A | B | C | D | E | F |G | H | I | J | T)
| [T] (int index) { (int index) -> T } -> (A | B | C | D | E | F | G | H | I | J | T)
end
end
end

@@ -56,3 +56,3 @@ # Solargraph

Solargraph supports [plugins](https://solargraph.org/guides/plugins) that implements their own Solargraph features, such as diagnostics reporters and conventions to provide LSP features and type-checking, e.g. for frameworks which use metaprogramming and/or DSLs.
Solargraph supports [plugins](https://solargraph.org/guides/plugins) that implement their own Solargraph features, such as diagnostics reporters and conventions to provide LSP features and type-checking, e.g. for frameworks which use metaprogramming and/or DSLs.

@@ -69,3 +69,3 @@ For better Rails support, please consider using [solargraph-rails](https://github.com/iftheshoefritz/solargraph-rails/)

If your project automatically requires bundled gems (e.g., `require 'bundler/require'`), Solargraph will add all of the Gemfile's default dependencies to the map.
If your project automatically requires bundled gem with the `Bundler.require` statement, Solargraph will add all of the Gemfile's default dependencies to the map.

@@ -86,3 +86,3 @@ To ensure you have types for gems which contain neither RBS nor YARD

As of version 0.33.0, Solargraph includes a [type checker](https://github.com/castwide/solargraph/issues/192) that uses a combination of YARD tags and code analysis to report missing type definitions. In strict mode, it performs type inference to determine whether the tags match the types it detects from code.
As of version 0.33.0, Solargraph includes a [type checker](https://github.com/castwide/solargraph/issues/192) that uses a combination of YARD tags and code analysis to report missing type definitions. In strict mode, it performs type inference to determine whether the tags match the types it detects from code. In strong mode it will ask you to clarify your intentions by adding annotations for better validation.

@@ -107,3 +107,3 @@ ### The Documentation Cache

### Rubocop Version
### RuboCop Version

@@ -139,2 +139,6 @@ If you have multiple versions of [`rubocop`](https://rubygems.org/gems/rubocop) installed and you would like to choose a version other than the latest to use, this specific version can be configured.

To see more logging when typechecking or running specs, set the
`SOLARGRAPH_LOG` environment variable to `debug` or `info`. `warn` is
the default value.
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.

@@ -141,0 +145,0 @@

@@ -14,3 +14,6 @@ $LOAD_PATH.unshift File.dirname(__FILE__) + '/lib'

s.files = Dir.chdir(File.expand_path('..', __FILE__)) do
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
# @sg-ignore Need backtick support
# @type [String]
all_files = `git ls-files -z`
all_files.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
end

@@ -39,5 +42,5 @@ s.homepage = 'https://solargraph.org'

s.add_runtime_dependency 'prism', '~> 1.4'
s.add_runtime_dependency 'rbs', '~> 3.6.1'
s.add_runtime_dependency 'rbs', ['>= 3.6.1', '<= 4.0.0.dev.4']
s.add_runtime_dependency 'reverse_markdown', '~> 3.0'
s.add_runtime_dependency 'rubocop', '~> 1.38'
s.add_runtime_dependency 'rubocop', '~> 1.76'
s.add_runtime_dependency 'thor', '~> 1.0'

@@ -47,2 +50,3 @@ s.add_runtime_dependency 'tilt', '~> 2.0'

s.add_runtime_dependency 'yard-solargraph', '~> 0.1'
s.add_runtime_dependency 'yard-activesupport-concern', '~> 0.0'

@@ -53,3 +57,9 @@ s.add_development_dependency 'pry', '~> 0.15'

s.add_development_dependency 'rspec', '~> 3.5'
s.add_development_dependency 'simplecov', '~> 0.14'
s.add_development_dependency 'rubocop-rake', '~> 0.7'
s.add_development_dependency 'rubocop-rspec', '~> 3.6'
s.add_development_dependency 'rubocop-yard', '~> 1.0'
s.add_development_dependency 'simplecov', '~> 0.21'
s.add_development_dependency 'simplecov-lcov', '~> 0.8'
s.add_development_dependency 'undercover', '~> 0.7'
s.add_development_dependency 'overcommit', '~> 0.68.0'
s.add_development_dependency 'webmock', '~> 3.6'

@@ -56,0 +66,0 @@ # work around missing yard dependency needed as of Ruby 3.5

AllCops:
NewCops: enable
Style/MethodDefParentheses:
Enabled: false
Layout/EmptyLineAfterGuardClause:
Enabled: false
Layout/SpaceAroundMethodCallOperator:
Enabled: true
Lint/RaiseException:
Enabled: true
Lint/StructNewOverride:
Enabled: true
Metrics/MethodLength:
Max: 25
Style/ExponentialNotation:
Enabled: true
Style/HashEachMethods:
Enabled: true
Style/HashTransformKeys:
Enabled: true
Style/HashTransformValues:
Enabled: true

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet