123456789_123456789_123456789_123456789_123456789_

Module: RuboCop::Cop::ProjectIndexHelp

Overview

Common helpers for cops that consult the project-wide static-analysis index via Base#project_index.

Mixed-in cops gain the #external_dependency_checksum override that invalidates the ::RuboCop::ResultCache whenever the indexed project files change on disk. To run index-backed analysis, cops should simply check whether project_index is non-nil; the runner only exposes a non-nil index when the user opted in via AllCops/UseProjectIndex and the underlying gem is available.

Constant Summary

Class Attribute Summary

  • .cached_index_signature rw

    The signature is a property of the index, not of the cop, and computing it stats every indexed file, so all cops sharing an index share one computation.

Instance Method Summary

Class Attribute Details

.cached_index_signature (rw)

The signature is a property of the index, not of the cop, and computing it stats every indexed file, so all cops sharing an index share one computation. A single-entry cache (instead of a hash keyed by index) avoids retaining stale graphs in long-lived processes.

[ GitHub ]

  
# File 'lib/rubocop/cop/mixin/project_index_help.rb', line 25

attr_accessor :cached_index_signature

Instance Method Details

#compute_project_index_signature (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/mixin/project_index_help.rb', line 170

def compute_project_index_signature
  project_index.documents.filter_map do |doc|
    uri = doc.uri
    next if uri == BUILTIN_DOCUMENT_URI

    path = uri.delete_prefix(FILE_URI_PREFIX).sub(WINDOWS_DRIVE_PREFIX, '')
    mtime, size = begin
      stat = File.stat(path)
      [stat.mtime.to_f, stat.size]
    rescue StandardError
      [0, 0]
    end

    "#{path}:#{mtime}:#{size}"
  end.sort
end

#definitions_in_other_files(definitions) (private)

Returns the definitions among definitions that live in a file other than the one being inspected, ordered by path and line. Definitions without a file:// URI (e.g. Rubydex’s built-in declarations) are ignored.

[ GitHub ]

  
# File 'lib/rubocop/cop/mixin/project_index_help.rb', line 41

def definitions_in_other_files(definitions)
  current = processed_source.file_path

  definitions
    .select { |definition| definition.location.uri.start_with?(FILE_URI_PREFIX) }
    .reject { |definition| File.identical?(definition.location.to_file_path, current) }
    .sort_by do |definition|
    [definition.location.to_file_path,
     definition.location.start_line]
  end
end

#external_dependency_checksum

[ GitHub ]

  
# File 'lib/rubocop/cop/mixin/project_index_help.rb', line 28

def external_dependency_checksum
  return nil unless project_index

  @external_dependency_checksum ||= Digest::SHA1.hexdigest(
    project_index_signature.join("\n")
  )
end

#fully_resolved_index_ancestry?(declaration, ignore_extend: false) ⇒ Boolean (private)

Whether every link of the declaration’s ancestry is resolved in the index: no definition of any ancestor has an unresolved superclass or mixin reference. Unresolved links silently vanish from ancestors, so this is the completeness check that must pass before reasoning about what an ancestry does or does not define. extend mixins only affect the singleton class; pass ignore_extend: true when reasoning about instance-side ancestry only.

[ GitHub ]

  
# File 'lib/rubocop/cop/mixin/project_index_help.rb', line 129

def fully_resolved_index_ancestry?(declaration, ignore_extend: false)
  declaration.ancestors.all? do |ancestor|
    ancestor.definitions.all? do |definition|
      resolved_superclass_reference?(definition) &&
        resolved_mixin_references?(definition, ignore_extend: ignore_extend)
    end
  end
end

#indexed_singleton_member(namespace, member_name) (private)

A namespace without any singleton method has no singleton-class declaration of its own, so the lookup starts from the first ancestor that has one; its find_member covers the rest of the chain.

[ GitHub ]

  
# File 'lib/rubocop/cop/mixin/project_index_help.rb', line 105

def indexed_singleton_member(namespace, member_name)
  namespace.ancestors.each do |ancestor|
    singleton = indexed_singleton_of(ancestor)
    return singleton.find_member(member_name) if singleton
  end

  nil
end

#indexed_singleton_of(declaration) (private)

The declaration of `declaration’s singleton class, or nil when no singleton method is defined on it anywhere in the project.

[ GitHub ]

  
# File 'lib/rubocop/cop/mixin/project_index_help.rb', line 98

def indexed_singleton_of(declaration)
  project_index["#{declaration.name}::<#{declaration.name.split('::').last}>"]
end

#inherited_index_member?(scope, member_name) ⇒ Boolean (private)

Whether an ancestor of scope other than scope itself defines member_name.

[ GitHub ]

  
# File 'lib/rubocop/cop/mixin/project_index_help.rb', line 116

def inherited_index_member?(scope, member_name)
  scope.ancestors.any? do |ancestor|
    ancestor.name != scope.name && ancestor.member(member_name)
  end
end

#lexical_nesting_of(node) (private)

The lexical nesting the node’s constants resolve through, outermost first. Only scopes whose body contains the node count: a class or module’s identifier and superclass expression are evaluated before its scope exists, so ancestors reached through them are excluded.

[ GitHub ]

  
# File 'lib/rubocop/cop/mixin/project_index_help.rb', line 82

def lexical_nesting_of(node)
  nesting = []
  child = node

  node.each_ancestor do |ancestor|
    if ancestor.type?(:class, :module) && child.equal?(ancestor.body)
      nesting << ancestor.identifier.const_name
    end
    child = ancestor
  end

  nesting.reverse
end

#prior_definition_in_other_file(definitions) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/mixin/project_index_help.rb', line 53

def prior_definition_in_other_file(definitions)
  definitions_in_other_files(definitions).first
end

#project_index_signature (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/mixin/project_index_help.rb', line 161

def project_index_signature
  index, signature = ProjectIndexHelp.cached_index_signature
  return signature if index.equal?(project_index)

  compute_project_index_signature.tap do |computed|
    ProjectIndexHelp.cached_index_signature = [project_index, computed]
  end
end

#resolve_constant_in_index(const_node) (private)

Resolves a constant node the way Ruby does: the first segment through the lexical nesting and every following segment inside the previous one. (Qualified names cannot be passed to resolve_constant as a whole, since it only applies the nesting to the full name. Later segments use find_member — the namespace and its ancestors — since resolve_constant cannot take an already-resolved namespace as the scope, only names as they appear in source.)

[ GitHub ]

  
# File 'lib/rubocop/cop/mixin/project_index_help.rb', line 64

def resolve_constant_in_index(const_node)
  segments = const_node.const_name.split('::')
  nesting = const_node.absolute? ? [] : lexical_nesting_of(const_node)

  declaration = project_index.resolve_constant(segments.first, nesting)
  segments.drop(1).each do |segment|
    return nil unless declaration.is_a?(Rubydex::Namespace)

    declaration = declaration.find_member(segment)
  end

  declaration
end

#resolved_mixin_references?(definition, ignore_extend: false) ⇒ Boolean (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/mixin/project_index_help.rb', line 144

def resolved_mixin_references?(definition, ignore_extend: false)
  return true unless definition.respond_to?(:mixins)

  definition.mixins.none? do |mixin|
    next false if ignore_extend && mixin.is_a?(Rubydex::Extend)

    mixin.constant_reference.is_a?(Rubydex::UnresolvedConstantReference)
  end
end

#resolved_superclass_reference?(definition) ⇒ Boolean (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/mixin/project_index_help.rb', line 138

def resolved_superclass_reference?(definition)
  return true unless definition.is_a?(Rubydex::ClassDefinition)

  !definition.superclass.is_a?(Rubydex::UnresolvedConstantReference)
end

#same_file?(path, other) ⇒ Boolean (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/mixin/project_index_help.rb', line 154

def same_file?(path, other)
  return true if File.identical?(path, other)

  normalized = [path, other].map { |p| File.expand_path(p).tr('\\', '/') }
  normalized.uniq.one? || (Platform.windows? && normalized[0].casecmp?(normalized[1]))
end