Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/rspec.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ jobs:
with:
ruby-version: ${{ matrix.ruby-version }}
bundler-cache: false
# /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
- name: Run tests
Expand Down
1 change: 1 addition & 0 deletions .yardopts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
lib/**/*.rb
--plugin yard-solargraph
--plugin activesupport-concern
99 changes: 63 additions & 36 deletions lib/solargraph/api_map.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,13 @@ def initialize pins: []
# just caches), please also change `equality_fields` below.
#

# @param other [Object]
def eql?(other)
self.class == other.class &&
equality_fields == other.equality_fields
end

# @param other [Object]
def ==(other)
self.eql?(other)
end
Expand Down Expand Up @@ -113,6 +115,7 @@ def catalog bench
[self.class, @source_map_hash, implicit, @doc_map, @unresolved_requires]
end

# @return [DocMap]
def doc_map
@doc_map ||= DocMap.new([], [])
end
Expand Down Expand Up @@ -186,10 +189,16 @@ def self.load directory
api_map
end

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

# @param gemspec [Gem::Specification]
# @param rebuild [Boolean]
# @param out [IO, nil]
# @return [void]
def cache_gem(gemspec, rebuild: false, out: nil)
@doc_map.cache(gemspec, rebuild: rebuild, out: out)
end
Expand Down Expand Up @@ -333,6 +342,18 @@ def qualify_namespace(namespace, context_namespace = '')
result
end

# @param fqns [String]
# @return [Array<String>]
def get_extends(fqns)
store.get_extends(fqns)
end

# @param fqns [String]
# @return [Array<String>]
def get_includes(fqns)
store.get_includes(fqns)
end

# Get an array of instance variable pins defined in specified namespace
# and scope.
#
Expand Down Expand Up @@ -514,6 +535,8 @@ def get_complex_type_methods complex_type, context = '', internal = false
# @param rooted_tag [String] Parameterized namespace, fully qualified
# @param name [String] Method name to look up
# @param scope [Symbol] :instance or :class
# @param visibility [Array<Symbol>] :public, :protected, and/or :private
# @param preserve_generics [Boolean]
# @return [Array<Solargraph::Pin::Method>]
def get_method_stack rooted_tag, name, scope: :instance, visibility: [:private, :protected, :public], preserve_generics: false
rooted_type = ComplexType.parse(rooted_tag)
Expand Down Expand Up @@ -664,6 +687,41 @@ def resolve_method_aliases pins, visibility = [:public, :private, :protected]
GemPins.combine_method_pins_by_path(with_resolved_aliases)
end

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

# A hash of source maps with filename keys.
Expand Down Expand Up @@ -697,6 +755,8 @@ def inner_get_methods rooted_tag, scope, visibility, deep, skip, no_core = false
return [] if skip.include?(reqstr)
skip.add reqstr
result = []
environ = Convention.for_object(self, rooted_tag, scope, visibility, deep, skip, no_core)
result.concat environ.pins
if deep && scope == :instance
store.get_prepends(fqns).reverse.each do |im|
fqim = qualify(im, fqns)
Expand All @@ -707,6 +767,7 @@ def inner_get_methods rooted_tag, scope, visibility, deep, skip, no_core = false
# namespaces; resolving the generics in the method pins is this
# class' responsibility
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
if scope == :instance
Expand All @@ -719,6 +780,7 @@ def inner_get_methods rooted_tag, scope, visibility, deep, skip, no_core = false
result.concat inner_get_methods_from_reference(rooted_sc_tag, namespace_pin, rooted_type, scope, visibility, true, skip, no_core)
end
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)
result.concat inner_get_methods(fqem, :instance, visibility, deep, skip, true) unless fqem.nil?
Expand All @@ -741,41 +803,6 @@ def inner_get_methods rooted_tag, scope, visibility, deep, skip, no_core = false
result
end

# @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>]
Expand Down Expand Up @@ -811,7 +838,7 @@ def qualify_lower namespace, context
qualify namespace, context.split('::')[0..-2].join('::')
end

# @param fq_tag [String]
# @param fq_sub_tag [String]
# @return [String, nil]
def qualify_superclass fq_sub_tag
fq_sub_type = ComplexType.try_parse(fq_sub_tag)
Expand Down
26 changes: 25 additions & 1 deletion lib/solargraph/convention.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ module Convention
autoload :Rakefile, 'solargraph/convention/rakefile'
autoload :StructDefinition, 'solargraph/convention/struct_definition'
autoload :DataDefinition, 'solargraph/convention/data_definition'
autoload :ActiveSupportConcern, 'solargraph/convention/active_support_concern'

# @type [Set<Convention::Base>]
@@conventions = Set.new
Expand All @@ -32,7 +33,7 @@ def self.for_local(source_map)
result
end

# @param yard_map [DocMap]
# @param doc_map [DocMap]
# @return [Environ]
def self.for_global(doc_map)
result = Environ.new
Expand All @@ -42,8 +43,31 @@ def self.for_global(doc_map)
result
end

# 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
111 changes: 111 additions & 0 deletions lib/solargraph/convention/active_support_concern.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# 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 [String] the tag of the module being included
#
# @return [void]
def process_include include_tag
rooted_include_tag = api_map.qualify(include_tag, rooted_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)
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
17 changes: 17 additions & 0 deletions lib/solargraph/convention/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,23 @@ def local source_map
def global doc_map
EMPTY_ENVIRON
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
Loading