Class: Pinspec::Analyzer::Discovery

Inherits:
Object
  • Object
show all
Defined in:
lib/pinspec/analyzer/discovery.rb

Defined Under Namespace

Classes: Choice

Constant Summary collapse

CONVENTIONAL =
%w[call perform run execute process].freeze
NON_TARGETS =

Never a target: these are object protocol, not behaviour anyone pins.

%w[initialize to_s to_str inspect hash eql? == <=> to_proc].freeze
LAST_RESORT =

Conversion methods, which are USUALLY protocol but are sometimes the whole public surface of a service object - OFN's AvailablePaymentMethodsService exposes exactly to_a and nothing else. Excluding them outright meant such a class had no candidates at all and was refused as ambiguous. They are ranked last instead, so they win only when nothing else is offered.

%w[to_a to_h each].freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(file_path) ⇒ Discovery

Returns a new instance of Discovery.



51
52
53
# File 'lib/pinspec/analyzer/discovery.rb', line 51

def initialize(file_path)
  @file_path = file_path
end

Class Method Details

.convention_for(files) ⇒ Object

convention is the method name this application uses most, counted once over the directory being pinned. Hardcoding call reads 2% of a codebase whose services are named perform.



37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/pinspec/analyzer/discovery.rb', line 37

def self.convention_for(files)
  counts = Hash.new(0)

  files.each do |file|
    surface = new(file).surface
    surface[:instance].each { |name| counts[name] += 1 }
  end

  best = counts.max_by { |name, count| [count, CONVENTIONAL.index(name) ? 1 : 0] }
  return nil if best.nil? || best.last < 2

  best.first
end

Instance Method Details

#choose(convention: nil) ⇒ Object



59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/pinspec/analyzer/discovery.rb', line 59

def choose(convention: nil)
  instance = surface[:instance]
  singleton = surface[:singleton]

  if instance.empty? && singleton.empty?
    return Choice.new(method_name: nil, reason: :no_public_methods, candidates: [], owner: nil)
  end

  preferred = [convention].compact + CONVENTIONAL

  preferred.each do |name|
    return chosen(name, reason_for(name, convention), instance) if instance.include?(name)
  end

  preferred.each do |name|
    return Choice.new(method_name: name, reason: :class_method, candidates: singleton, owner: nil) if singleton.include?(name)
  end

  # A class with exactly one public method has only one thing it can mean.
  return chosen(instance.first, :sole_method, instance) if instance.size == 1

  # Nothing conventional, and what remains is a single conversion method: that is
  # this class's whole public surface, so it is the target.
  # Conversion methods rank last, so a real entry point wins - but a class whose
  # only ordinary method is one of them still has something to pin.
  ordinary = instance - LAST_RESORT
  return chosen(ordinary.first, :sole_method, instance) if ordinary.size == 1
  return Choice.new(method_name: singleton.first, reason: :sole_method, candidates: singleton, owner: nil) if instance.empty? && singleton.size == 1

  Choice.new(method_name: nil, reason: :ambiguous, candidates: (instance + singleton).first(8), owner: nil)
end

#surfaceObject



55
56
57
# File 'lib/pinspec/analyzer/discovery.rb', line 55

def surface
  @surface ||= read_surface
end