class Solargraph::YardMap

The YardMap provides access to YARD documentation for the Ruby core, the stdlib, and gems.

Attributes

with_dependencies[W]

@return [Boolean]

Public Class Methods

new(required: [], directory: '', source_gems: [], with_dependencies: true) click to toggle source

@param required [Array<String>, Set<String>] @param directory [String] @param source_gems [Array<String>, Set<String>] @param with_dependencies [Boolean]

# File lib/solargraph/yard_map.rb, line 51
def initialize(required: [], directory: '', source_gems: [], with_dependencies: true)
  @with_dependencies = with_dependencies
  change required.to_set, directory, source_gems.to_set
end

Public Instance Methods

base_required() click to toggle source
# File lib/solargraph/yard_map.rb, line 157
def base_required
  @base_required ||= Set.new
end
change(new_requires, new_directory, new_source_gems) click to toggle source

@param new_requires [Set<String>] Required paths to use for loading gems @param new_directory [String] The workspace directory @param new_source_gems [Set<String>] Gems under local development (i.e., part of the workspace) @return [Boolean]

# File lib/solargraph/yard_map.rb, line 70
def change new_requires, new_directory, new_source_gems
  return false if new_requires == base_required && new_directory == @directory && new_source_gems == @source_gems
  @gem_paths = {}
  base_required.replace new_requires
  required.replace new_requires
  # HACK: Hardcoded YAML handling
  required.add 'psych' if new_requires.include?('yaml')
  @source_gems = new_source_gems
  @directory = new_directory
  process_requires
  @rebindable_method_names = nil
  @pin_class_hash = nil
  @pin_select_cache = {}
  true
end
core_pins() click to toggle source

@return [Array<Solargraph::Pin::Base>]

# File lib/solargraph/yard_map.rb, line 125
def core_pins
  # Using a class variable to reduce loads
  @@core_pins ||= load_core_pins
end
directory() click to toggle source
# File lib/solargraph/yard_map.rb, line 161
def directory
  @directory ||= ''
end
load_yardoc(y) click to toggle source

@param y [String] @return [YARD::Registry]

# File lib/solargraph/yard_map.rb, line 112
def load_yardoc y
  if y.is_a?(Array)
    YARD::Registry.load y, true
  else
    YARD::Registry.load! y
  end
rescue StandardError => e
  Solargraph::Logging.logger.warn "Error loading yardoc '#{y}' #{e.class} #{e.message}"
  yardocs.delete y
  nil
end
path_pin(path) click to toggle source

@param path [String] @return [Pin::Base]

# File lib/solargraph/yard_map.rb, line 132
def path_pin path
  pins.select { |p| p.path == path }.first
end
pins() click to toggle source

@return [Array<Solargraph::Pin::Base>]

# File lib/solargraph/yard_map.rb, line 57
def pins
  @pins ||= []
end
rebindable_method_names() click to toggle source

@return [Set<String>]

# File lib/solargraph/yard_map.rb, line 87
def rebindable_method_names
  @rebindable_method_names ||= pins_by_class(Pin::Method)
    .select { |pin| pin.comments && pin.comments.include?('@yieldself') }
    .map(&:name)
    .concat(['instance_eval', 'instance_exec', 'class_eval', 'class_exec', 'module_eval', 'module_exec', 'define_method'])
    .to_set
end
require_reference(path) click to toggle source

Get the location of a file referenced by a require path.

@param path [String] @return [Location]

# File lib/solargraph/yard_map.rb, line 140
def require_reference path
  # @type [Gem::Specification]
  spec = spec_for_require(path)
  spec.full_require_paths.each do |rp|
    file = File.join(rp, "#{path}.rb")
    next unless File.file?(file)
    return Solargraph::Location.new(file, Solargraph::Range.from_to(0, 0, 0, 0))
  end
  nil
rescue Gem::LoadError
  nil
end
required() click to toggle source

@return [Set<String>]

# File lib/solargraph/yard_map.rb, line 101
def required
  @required ||= Set.new
end
stdlib_paths() click to toggle source
# File lib/solargraph/yard_map.rb, line 29
def stdlib_paths
  @@stdlib_paths ||= begin
    result = {}
    YARD::Registry.load! CoreDocs.yardoc_stdlib_file
    YARD::Registry.all.each do |co|
      next if co.file.nil?
      path = co.file.sub(/^(ext|lib)\//, '').sub(/\.(rb|c)$/, '')
      base = path.split('/').first
      result[base] ||= []
      result[base].push co
    end
    result
  end
end
stdlib_pins() click to toggle source
# File lib/solargraph/yard_map.rb, line 153
def stdlib_pins
  @stdlib_pins ||= []
end
unresolved_requires() click to toggle source

@return [Array<String>]

# File lib/solargraph/yard_map.rb, line 106
def unresolved_requires
  @unresolved_requires ||= []
end
with_dependencies?() click to toggle source
# File lib/solargraph/yard_map.rb, line 61
def with_dependencies?
  @with_dependencies ||= true unless @with_dependencies == false
  @with_dependencies
end
yardocs() click to toggle source

@return [Array<String>]

# File lib/solargraph/yard_map.rb, line 96
def yardocs
  @yardocs ||= []
end

Private Instance Methods

add_gem_dependencies(spec) click to toggle source

@param spec [Gem::Specification] @return [void]

# File lib/solargraph/yard_map.rb, line 263
def add_gem_dependencies spec
  result = []
  (spec.dependencies - spec.development_dependencies).each do |dep|
    begin
      next if @source_gems.include?(dep.name) || @gem_paths.key?(dep.name)
      depspec = Gem::Specification.find_by_name(dep.name)
      next if depspec.nil?
      @gem_paths[depspec.name] = depspec.full_gem_path
      gy = yardoc_file_for_spec(depspec)
      if gy.nil?
        unresolved_requires.push dep.name
      else
        next if yardocs.include?(gy)
        yardocs.unshift gy
        result.concat process_yardoc gy, depspec
        result.concat add_gem_dependencies(depspec)
      end
    rescue Gem::LoadError
      # This error probably indicates a bug in an installed gem
      Solargraph::Logging.logger.warn "Failed to resolve #{dep.name} gem dependency for #{spec.name}"
    end
  end
  result
end
cache() click to toggle source

@return [YardMap::Cache]

# File lib/solargraph/yard_map.rb, line 168
def cache
  @cache ||= YardMap::Cache.new
end
load_core_pins() click to toggle source
# File lib/solargraph/yard_map.rb, line 362
def load_core_pins
  yd = CoreDocs.yardoc_file
  ser = File.join(File.dirname(yd), 'core.ser')
  result = if File.file?(ser)
    file = File.open(ser, 'rb')
    dump = file.read
    file.close
    begin
      Marshal.load(dump)
    rescue StandardError => e
      Solargraph.logger.warn "Error loading core pin cache: [#{e.class}] #{e.message}"
      File.unlink ser
      read_core_and_save_cache(yd, ser)
    end
  else
    read_core_and_save_cache(yd, ser)
  end
  ApiMap::Store.new(result + CoreFills::ALL).pins.reject { |pin| pin.is_a?(Pin::Reference::Override) }
end
load_stdlib_pins(base) click to toggle source
# File lib/solargraph/yard_map.rb, line 404
def load_stdlib_pins base
  ser = File.join(File.dirname(CoreDocs.yardoc_stdlib_file), "#{base}.ser")
  result = if File.file?(ser)
    Solargraph.logger.info "Loading #{base} stdlib from cache"
    file = File.open(ser, 'rb')
    dump = file.read
    file.close
    begin
      Marshal.load(dump)
    rescue StandardError => e
      Solargraph.logger.warn "Error loading #{base} stdlib pin cache: [#{e.class}] #{e.message}"
      File.unlink ser
      read_stdlib_and_save_cache(base, ser)
    end
  else
    read_stdlib_and_save_cache(base, ser)
  end
  fills = StdlibFills.get(base)
  unless fills.empty?
    result = ApiMap::Store.new(result + fills).pins.reject { |pin| pin.is_a?(Pin::Reference::Override) }
  end
  result
end
pin_class_hash() click to toggle source

@return [Hash]

# File lib/solargraph/yard_map.rb, line 173
def pin_class_hash
  @pin_class_hash ||= pins.to_set.classify(&:class).transform_values(&:to_a)
end
pins_by_class(klass) click to toggle source

@return [Array<Pin::Base>]

# File lib/solargraph/yard_map.rb, line 178
def pins_by_class klass
  @pin_select_cache[klass] ||= pin_class_hash.select { |key, _| key <= klass }.values.flatten
end
process_gemsets() click to toggle source
# File lib/solargraph/yard_map.rb, line 256
def process_gemsets
  return {} if directory.empty? || !File.file?(File.join(directory, 'Gemfile'))
  require_from_bundle(directory)
end
process_requires() click to toggle source

@return [void]

# File lib/solargraph/yard_map.rb, line 194
def process_requires
  @gemset = process_gemsets
  required.merge @gemset.keys if required.include?('bundler/require')
  pins.replace core_pins
  unresolved_requires.clear
  stdlib_pins.clear
  environ = Convention.for_global(self)
  done = []
  from_std = []
  (required + environ.requires).each do |r|
    next if r.nil? || r.empty? || done.include?(r)
    done.push r
    cached = cache.get_path_pins(r)
    unless cached.nil?
      pins.concat cached
      next
    end
    result = []
    begin
      spec = spec_for_require(r)
      if @source_gems.include?(spec.name)
        next
      end
      next if @gem_paths.key?(spec.name)
      yd = yardoc_file_for_spec(spec)
      # YARD detects gems for certain libraries that do not have a yardoc
      # but exist in the stdlib. `fileutils` is an example. Treat those
      # cases as errors and check the stdlib yardoc.
      raise Gem::LoadError if yd.nil?
      @gem_paths[spec.name] = spec.full_gem_path
      unless yardocs.include?(yd)
        yardocs.unshift yd
        result.concat process_yardoc yd, spec
        result.concat add_gem_dependencies(spec) if with_dependencies?
      end
    rescue Gem::LoadError, NoYardocError => e
      base = r.split('/').first
      next if from_std.include?(base)
      from_std.push base
      stdtmp = load_stdlib_pins(base)
      if stdtmp.empty?
        unresolved_requires.push r
      else
        stdlib_pins.concat stdtmp
        result.concat stdtmp
      end
    end
    result.delete_if(&:nil?)
    unless result.empty?
      cache.set_path_pins r, result
      pins.concat result
    end
  end
  if required.include?('yaml') && required.include?('psych')
    # HACK: Hardcoded YAML handling
    # @todo Why can't this be handled with an override or a virtual pin?
    pin = path_pin('YAML')
    pin.instance_variable_set(:@return_type, ComplexType.parse('Module<Psych>')) unless pin.nil?
  end
  pins.concat environ.pins
end
process_yardoc(y, spec = nil) click to toggle source

@param y [String, nil] @param spec [Gem::Specification, nil] @return [Array<Pin::Base>]

# File lib/solargraph/yard_map.rb, line 291
def process_yardoc y, spec = nil
  return [] if y.nil?
  if spec
    ser = File.join(CoreDocs.cache_dir, 'gems', "#{spec.name}-#{spec.version}.ser")
    if File.file?(ser)
      Solargraph.logger.info "Loading #{spec.name} #{spec.version} from cache"
      file = File.open(ser, 'rb')
      dump = file.read
      file.close
      begin
        result = Marshal.load(dump)
        return result unless result.nil? || result.empty?
        Solargraph.logger.warn "Empty cache for #{spec.name} #{spec.version}. Reloading"
        File.unlink ser
      rescue StandardError => e
        Solargraph.logger.warn "Error loading pin cache: [#{e.class}] #{e.message}"
        File.unlink ser
      end
    end
  end
  size = Dir.glob(File.join(y, '**', '*'))
    .map{ |f| File.size(f) }
    .inject(:+)
  if !size.nil? && size > 20_000_000
    Solargraph::Logging.logger.warn "Yardoc at #{y} is too large to process (#{size} bytes)"
    return []
  end
  Solargraph.logger.info "Loading #{spec.name} #{spec.version} from #{y}"
  load_yardoc y
  result = Mapper.new(YARD::Registry.all, spec).map
  raise NoYardocError, "Yardoc at #{y} is empty" if result.empty?
  if spec
    ser = File.join(CoreDocs.cache_dir, 'gems', "#{spec.name}-#{spec.version}.ser")
    file = File.open(ser, 'wb')
    file.write Marshal.dump(result)
    file.close
  end
  result
end
read_core_and_save_cache(yd, ser) click to toggle source
# File lib/solargraph/yard_map.rb, line 382
def read_core_and_save_cache yd, ser
  result = []
  load_yardoc yd
  result.concat Mapper.new(YARD::Registry.all).map
  # HACK: Assume core methods with a single `args` parameter accept restarg
  result.select { |pin| pin.is_a?(Solargraph::Pin::Method )}.each do |pin|
    if pin.parameters.length == 1 && pin.parameters.first.name == 'args' && pin.parameters.first.decl == :arg
      # @todo Smelly instance variable access
      pin.parameters.first.instance_variable_set(:@decl, :restarg)
    end
  end
  # HACK: Set missing parameters on `==` methods, e.g., `Symbol#==`
  result.select { |pin| pin.name == '==' && pin.parameters.empty? }.each do |pin|
    pin.parameters.push Pin::Parameter.new(decl: :arg, name: 'obj2')
  end
  dump = Marshal.dump(result)
  file = File.open(ser, 'wb')
  file.write dump
  file.close
  result
end
read_stdlib_and_save_cache(base, ser) click to toggle source
# File lib/solargraph/yard_map.rb, line 428
def read_stdlib_and_save_cache base, ser
  result = []
  if stdlib_paths[base]
    Solargraph.logger.info "Loading #{base} stdlib from yardoc"
    result.concat Mapper.new(stdlib_paths[base]).map
    unless result.empty?
      dump = Marshal.dump(result)
      file = File.open(ser, 'wb')
      file.write dump
      file.close
    end
  end
  result
end
recurse_namespace_object(ns) click to toggle source

@param ns [YARD::CodeObjects::NamespaceObject] @return [Array<YARD::CodeObjects::Base>]

# File lib/solargraph/yard_map.rb, line 184
def recurse_namespace_object ns
  result = []
  ns.children.each do |c|
    result.push c
    result.concat recurse_namespace_object(c) if c.respond_to?(:children)
  end
  result
end
spec_for_require(path) click to toggle source

@param path [String] @return [Gem::Specification]

# File lib/solargraph/yard_map.rb, line 345
def spec_for_require path
  name = path.split('/').first
  spec = Gem::Specification.find_by_name(name, @gemset[name])

  # Avoid loading the spec again if it's going to be skipped anyway
  return spec if @source_gems.include?(spec.name)
  # Avoid loading the spec again if it's already the correct version
  if @gemset[spec.name] && @gemset[spec.name] != spec.version
    begin
      return Gem::Specification.find_by_name(spec.name, "= #{@gemset[spec.name]}")
    rescue Gem::LoadError
      Solargraph.logger.warn "Unable to load #{spec.name} #{@gemset[spec.name]} specified by workspace, using #{spec.version} instead"
    end
  end
  spec
end
yardoc_file_for_spec(spec) click to toggle source

@param spec [Gem::Specification] @return [String]

# File lib/solargraph/yard_map.rb, line 333
def yardoc_file_for_spec spec
  cache_dir = File.join(Solargraph::YardMap::CoreDocs.cache_dir, 'gems', "#{spec.name}-#{spec.version}", 'yardoc')
  if File.exist?(cache_dir)
    Solargraph.logger.info "Using cached documentation for #{spec.name} at #{cache_dir}"
    cache_dir
  else
    YARD::Registry.yardoc_file_for_gem(spec.name, "= #{spec.version}")
  end
end