Skip to content

Commit

Permalink
Merge branch 'master' of github.com:castwide/solargraph
Browse files Browse the repository at this point in the history
  • Loading branch information
castwide committed Jan 18, 2025
2 parents 91da1d4 + ad641ad commit 9185f72
Show file tree
Hide file tree
Showing 24 changed files with 360 additions and 11 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/rspec.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
ruby-version: ['2.6', '2.7', '3.0', '3.3', '3.4', 'head']
ruby-version: ['2.6', '2.7', '3.0', '3.1', '3.2', '3.3', '3.4', 'head']

steps:
- uses: actions/checkout@v3
Expand Down
7 changes: 7 additions & 0 deletions lib/solargraph/complex_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,18 @@ def [](index)
def select &block
@items.select &block
end

# @return [String]
def namespace
# cache this attr for high frequency call
@namespace ||= method_missing(:namespace).to_s
end

# @return [Array<String>]
def namespaces
@items.map(&:namespace)
end

def method_missing name, *args, &block
return if @items.first.nil?
return @items.first.send(name, *args, &block) if respond_to_missing?(name)
Expand Down
10 changes: 10 additions & 0 deletions lib/solargraph/language_server/host.rb
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,15 @@ def definitions_at uri, line, column
library.definitions_at(uri_to_file(uri), line, column)
end

# @param uri [String]
# @param line [Integer]
# @param column [Integer]
# @return [Array<Solargraph::Pin::Base>]
def type_definitions_at uri, line, column
library = library_for(uri)
library.type_definitions_at(uri_to_file(uri), line, column)
end

# @param uri [String]
# @param line [Integer]
# @param column [Integer]
Expand Down Expand Up @@ -630,6 +639,7 @@ def default_configuration
'hover' => true,
'symbols' => true,
'definitions' => true,
'typeDefinitions' => true,
'rename' => true,
'references' => true,
'autoformat' => false,
Expand Down
1 change: 1 addition & 0 deletions lib/solargraph/language_server/message.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ def method_map
register 'textDocument/didClose', TextDocument::DidClose
register 'textDocument/hover', TextDocument::Hover
register 'textDocument/definition', TextDocument::Definition
register 'textDocument/typeDefinition', TextDocument::TypeDefinition
register 'textDocument/formatting', TextDocument::Formatting
register 'textDocument/onTypeFormatting', TextDocument::OnTypeFormatting
register 'textDocument/documentSymbol', TextDocument::DocumentSymbol
Expand Down
8 changes: 8 additions & 0 deletions lib/solargraph/language_server/message/initialize.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ def process
result[:capabilities].merge! static_document_formatting unless dynamic_registration_for?('textDocument', 'formatting')
result[:capabilities].merge! static_document_symbols unless dynamic_registration_for?('textDocument', 'documentSymbol')
result[:capabilities].merge! static_definitions unless dynamic_registration_for?('textDocument', 'definition')
result[:capabilities].merge! static_type_definitions unless dynamic_registration_for?('textDocument', 'typeDefinition')
result[:capabilities].merge! static_rename unless dynamic_registration_for?('textDocument', 'rename')
result[:capabilities].merge! static_references unless dynamic_registration_for?('textDocument', 'references')
result[:capabilities].merge! static_workspace_symbols unless dynamic_registration_for?('workspace', 'symbol')
Expand Down Expand Up @@ -121,6 +122,13 @@ def static_definitions
}
end

def static_type_definitions
return {} unless host.options['type_definitions']
{
definitionProvider: true
}
end

def static_rename
{
renameProvider: {prepareProvider: true}
Expand Down
1 change: 1 addition & 0 deletions lib/solargraph/language_server/message/initialized.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ def process
textDocument/formatting
textDocument/documentSymbol
textDocument/definition
textDocument/typeDefinition
textDocument/references
textDocument/rename
textDocument/prepareRename
Expand Down
1 change: 1 addition & 0 deletions lib/solargraph/language_server/message/text_document.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ module TextDocument
autoload :DiagnosticsQueue, 'solargraph/language_server/message/text_document/diagnostics_queue'
autoload :OnTypeFormatting, 'solargraph/language_server/message/text_document/on_type_formatting'
autoload :Definition, 'solargraph/language_server/message/text_document/definition'
autoload :TypeDefinition, 'solargraph/language_server/message/text_document/type_definition'
autoload :DocumentSymbol, 'solargraph/language_server/message/text_document/document_symbol'
autoload :Formatting, 'solargraph/language_server/message/text_document/formatting'
autoload :References, 'solargraph/language_server/message/text_document/references'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# frozen_string_literal: true

module Solargraph::LanguageServer::Message::TextDocument
class TypeDefinition < Base
def process
@line = params['position']['line']
@column = params['position']['character']
set_result(code_location || [])
end

private

def code_location
suggestions = host.type_definitions_at(params['textDocument']['uri'], @line, @column)
return nil if suggestions.empty?
suggestions.reject { |pin| pin.location.nil? || pin.location.filename.nil? }.map do |pin|
{
uri: file_to_uri(pin.location.filename),
range: pin.location.range.to_hash
}
end
end
end
end
18 changes: 17 additions & 1 deletion lib/solargraph/library.rb
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ def contain? filename
def create filename, text
result = false
mutex.synchronize do
next unless contain?(filename) || open?(filename) || workspace.would_merge?(filename)
next unless contain?(filename) || open?(filename)
@synchronized = false
source = Solargraph::Source.load_string(text, filename)
workspace.merge(source)
Expand Down Expand Up @@ -199,6 +199,22 @@ def definitions_at filename, line, column
handle_file_not_found(filename, e)
end

# Get type definition suggestions for the expression at the specified file and
# location.
#
# @param filename [String] The file to analyze
# @param line [Integer] The zero-based line number
# @param column [Integer] The zero-based column number
# @return [Array<Solargraph::Pin::Base>]
# @todo Take filename/position instead of filename/line/column
def type_definitions_at filename, line, column
position = Position.new(line, column)
cursor = Source::Cursor.new(read(filename), position)
api_map.clip(cursor).types
rescue FileNotFoundError => e
handle_file_not_found filename, e
end

# Get signature suggestions for the method at the specified file and
# location.
#
Expand Down
1 change: 1 addition & 0 deletions lib/solargraph/pin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ module Pin
autoload :Method, 'solargraph/pin/method'
autoload :Signature, 'solargraph/pin/signature'
autoload :MethodAlias, 'solargraph/pin/method_alias'
autoload :DelegatedMethod, 'solargraph/pin/delegated_method'
autoload :BaseVariable, 'solargraph/pin/base_variable'
autoload :InstanceVariable, 'solargraph/pin/instance_variable'
autoload :ClassVariable, 'solargraph/pin/class_variable'
Expand Down
97 changes: 97 additions & 0 deletions lib/solargraph/pin/delegated_method.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# frozen_string_literal: true

module Solargraph
module Pin
# A DelegatedMethod is a more complicated version of a MethodAlias that
# allows aliasing a method from a different closure (class/module etc).
class DelegatedMethod < Pin::Method
# A DelegatedMethod can be constructed with either a :resolved_method
# pin, or a :receiver_chain. When a :receiver_chain is supplied, it
# will be used to *dynamically* resolve a receiver type within the
# given closure/scope, and the delegated method will then be resolved
# to a method pin on that type.
#
# @param resolved_method [Method] an already resolved method pin.
# @param receiver [Source::Chain] the source code used to resolve the receiver for this delegated method.
# @param receiver_method_name [String] the method name that will be called on the receiver (defaults to :name).
def initialize(method: nil, receiver: nil, name: method&.name, receiver_method_name: name, **splat)
raise ArgumentError, 'either :method or :receiver is required' if (method && receiver) || (!method && !receiver)
super(name: name, **splat)

@receiver_chain = receiver
@resolved_method = method
@receiver_method_name = receiver_method_name
end

%i[comments parameters return_type location].each do |method|
define_method(method) do
@resolved_method ? @resolved_method.send(method) : super()
end
end

%i[typify realize infer probe].each do |method|
# @param api_map [ApiMap]
define_method(method) do |api_map|
resolve_method(api_map)
@resolved_method ? @resolved_method.send(method, api_map) : super(api_map)
end
end

def resolvable?(api_map)
resolve_method(api_map)
!!@resolved_method
end

private

# Resolves the receiver chain and method name to a method pin, resetting any previously resolution.
#
# @param api_map [ApiMap]
# @return [Pin::Method, nil]
def resolve_method api_map
return if @resolved_method

resolver = @receiver_chain.define(api_map, self, []).first

unless resolver
Solargraph.logger.warn \
"Delegated receiver for #{path} was resolved to nil from `#{print_chain(@receiver_chain)}'"
return
end

receiver_type = resolver.return_type

return if receiver_type.undefined?

receiver_path, method_scope =
if @receiver_chain.constant?
# HACK: the `return_type` of a constant is Class<Whatever>, but looking up a method expects
# the arguments `"Whatever"` and `scope: :class`.
[receiver_type.to_s.sub(/^Class<(.+)>$/, '\1'), :class]
else
[receiver_type.to_s, :instance]
end

method_stack = api_map.get_method_stack(receiver_path, @receiver_method_name, scope: method_scope)
@resolved_method = method_stack.first
end

# helper to print a source chain as code, probably not 100% correct.
#
# @param chain [Source::Chain]
def print_chain(chain)
out = +''
chain.links.each_with_index do |link, index|
if index > 0
if Source::Chain::Constant
out << '::' unless link.word.start_with?('::')
else
out << '.'
end
end
out << link.word
end
end
end
end
end
5 changes: 5 additions & 0 deletions lib/solargraph/pin/method.rb
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,11 @@ def documentation
end
@documentation += "\n\n" unless @documentation.empty?
@documentation += "Visibility: #{visibility}"
example_tags = docstring.tags(:example)
unless example_tags.empty?
@documentation += "\n\nExamples:\n\n"
@documentation += example_tags.map(&:text).join("\n")
end
end
@documentation.to_s
end
Expand Down
14 changes: 14 additions & 0 deletions lib/solargraph/source_map/clip.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,16 @@ def initialize api_map, cursor
def define
return [] if cursor.comment? || cursor.chain.literal?
result = cursor.chain.define(api_map, block, locals)
result.concat file_global_methods
result.concat((source_map.pins + source_map.locals).select{ |p| p.name == cursor.word && p.location.range.contain?(cursor.position) }) if result.empty?
result
end

# @return [Array<Pin::Base>]
def types
infer.namespaces.map { |namespace| api_map.get_path_pins(namespace) }.flatten
end

# @return [Completion]
def complete
return package_completions([]) if !source_map.source.parsed? || cursor.string?
Expand Down Expand Up @@ -214,6 +220,7 @@ def code_complete
return package_completions(api_map.get_global_variable_pins)
end
result.concat locals
result.concat file_global_methods unless block.binder.namespace.empty?
result.concat api_map.get_constants(context_pin.context.namespace, *gates)
result.concat api_map.get_methods(block.binder.namespace, scope: block.binder.scope, visibility: [:public, :private, :protected])
result.concat api_map.get_methods('Kernel')
Expand All @@ -224,6 +231,13 @@ def code_complete
end
package_completions(result)
end

def file_global_methods
return [] if cursor.word.empty?
source_map.pins.select do |pin|
pin.is_a?(Pin::Method) && pin.namespace == '' && pin.name.start_with?(cursor.word)
end
end
end
end
end
7 changes: 5 additions & 2 deletions lib/solargraph/type_checker.rb
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,10 @@ def argument_problems_for chain, api_map, block_pin, locals, location
base = chain
until base.links.length == 1 && base.undefined?
pins = base.define(api_map, block_pin, locals)
if pins.first.is_a?(Pin::Method)

if pins.first.is_a?(Pin::DelegatedMethod) && !pins.first.resolvable?(api_map)
# Do nothing, as we can't find the actual method implementation
elsif pins.first.is_a?(Pin::Method)
# @type [Pin::Method]
pin = pins.first
ap = if base.links.last.is_a?(Solargraph::Source::Chain::ZSuper)
Expand Down Expand Up @@ -551,7 +554,7 @@ def fake_args_for(pin)
def without_ignored problems
problems.reject do |problem|
node = source_map.source.node_at(problem.location.range.start.line, problem.location.range.start.column)
source_map.source.comments_for(node)&.include?('@sg-ignore')
node && source_map.source.comments_for(node)&.include?('@sg-ignore')
end
end
end
Expand Down
18 changes: 13 additions & 5 deletions lib/solargraph/yard_map.rb
Original file line number Diff line number Diff line change
Expand Up @@ -220,9 +220,11 @@ def pins_for_require r, already_errored
result = []
begin
name = r.split('/').first.to_s
return [] if name.empty? || @source_gems.include?(name) || @gem_paths.key?(name)
spec = spec_for_require(name)
@gem_paths[name] = spec.full_gem_path
return [] if name.empty?

spec = spec_for_require(r)
return [] if @source_gems.include?(spec.name) || @gem_paths.key?(spec.name)
@gem_paths[spec.name] = spec.full_gem_path

yd = yardoc_file_for_spec(spec)
# YARD detects gems for certain libraries that do not have a yardoc
Expand Down Expand Up @@ -282,8 +284,14 @@ def yardoc_file_for_spec spec
# @param path [String]
# @return [Gem::Specification]
def spec_for_require path
name = path.split('/').first.to_s
spec = Gem::Specification.find_by_name(name, @gemset[name])
relatives = path.split('/')
spec = nil
while spec.nil? && !relatives.empty?
name = relatives.join('-')
spec = Gem::Specification.find_by_name(name, @gemset[name])
relatives.pop
end
raise Gem::LoadError if spec.nil?

# Avoid loading the spec again if it's going to be skipped anyway
return spec if @source_gems.include?(spec.name)
Expand Down
3 changes: 3 additions & 0 deletions spec/complex_type_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@
expect(types.length).to eq(1)
expect(types.first.namespace).to eq('Foo')
expect(types.first.scope).to eq(:class)
multiple_types = Solargraph::ComplexType.parse 'Module<Foo>, Class<Bar>, String, nil'
expect(multiple_types.length).to eq(4)
expect(multiple_types.namespaces).to eq(['Foo', 'Bar', 'String', 'NilClass'])
end

it "identifies duck types" do
Expand Down
2 changes: 2 additions & 0 deletions spec/fixtures/workspace/lib/something.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
class Something
end
4 changes: 2 additions & 2 deletions spec/fixtures/workspace/lib/thing.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
class Thing
def do_thing
end
# @return [Something]
def do_thing; end
end
Loading

0 comments on commit 9185f72

Please sign in to comment.