Module: WhyClasses::Correctors::PolymorphicFunctionCorrector

Defined in:
lib/why_classes/correctors/polymorphic_function_corrector.rb

Overview

Decoupled polymorphism (Dave Thomas): an instance method that only reads a single field is more flexible as a freestanding function. This corrector extracts such a method out of the class:

class Person                        def age_in_seconds(entity)
def initialize(dob)         =>       Time.now - entity.date_of_birth
  @date_of_birth = dob            end
end
                                  # class keeps only what remains
def age_in_seconds
  Time.now - @date_of_birth
end
end

Two parameter styles:

:entity    -> `def age_in_seconds(entity)`  using `entity.date_of_birth`
            (works for anything responding to the reader)
:attribute -> `def age_in_seconds(date_of_birth)` using `date_of_birth`
            (the "even more generic" form -- just the value)

Unsafe-tier: every person.age_in_seconds call site elsewhere must become age_in_seconds(person), which this single-file tool cannot rewrite.

Constant Summary collapse

H =
AST::NodeHelpers

Class Method Summary collapse

Class Method Details

.correctable?(shape, method_node) ⇒ Boolean

Returns:

  • (Boolean)


34
35
36
37
38
39
40
41
42
43
44
# File 'lib/why_classes/correctors/polymorphic_function_corrector.rb', line 34

def correctable?(shape, method_node)
  return false if shape.uses_metaprogramming?
  return false unless no_parameters?(method_node)

  read = H.ivars_read(method_node)
  return false unless read.size == 1
  return false unless H.ivars_assigned(method_node).empty?
  return false if leans_on_self?(method_node, shape)

  true
end

.corrector(class_node, method_node, style:) ⇒ Object



46
47
48
49
50
51
52
53
# File 'lib/why_classes/correctors/polymorphic_function_corrector.rb', line 46

def corrector(class_node, method_node, style:)
  field = H.ivars_read(method_node).first.to_s.delete_prefix("@")
  lambda do |rewriter|
    buffer = class_node.location.expression.source_buffer
    rewriter.remove(removal_range(buffer, method_node))
    rewriter.insert_after(class_node.location.end, "\n\n#{freestanding(class_node, method_node, field, style)}")
  end
end

.freestanding(class_node, method_node, field, style) ⇒ Object

--- rewriting -----------------------------------------------------------



74
75
76
77
78
79
# File 'lib/why_classes/correctors/polymorphic_function_corrector.rb', line 74

def freestanding(class_node, method_node, field, style)
  param = style == :attribute ? field : "entity"
  replacement = style == :attribute ? field : "#{param}.#{field}"
  body = rewritten_source(method_node, field, param: param, replacement: replacement)
  reindent(body, class_node.location.keyword.column, method_node.location.keyword.column)
end

.leans_on_self?(method_node, shape) ⇒ Boolean

Bail when the body depends on the receiver in ways extraction would break: self, super, blocks (yield), or a bare call to a sibling method.

Returns:

  • (Boolean)


64
65
66
67
68
69
70
# File 'lib/why_classes/correctors/polymorphic_function_corrector.rb', line 64

def leans_on_self?(method_node, shape)
  siblings = shape.instance_method_names - [method_node.children[0]]
  H.collect(method_node) do |n|
    %i[self zsuper super yield].include?(n.type) ||
      (n.type == :send && n.children[0].nil? && siblings.include?(n.children[1]))
  end.any?
end

.no_parameters?(method_node) ⇒ Boolean

--- preconditions -------------------------------------------------------

Returns:

  • (Boolean)


57
58
59
60
# File 'lib/why_classes/correctors/polymorphic_function_corrector.rb', line 57

def no_parameters?(method_node)
  args = method_node.children[1]
  args.nil? || args.children.empty?
end

.reindent(source, target_col, method_col) ⇒ Object

The method source has no leading indent on line 1 and absolute indent on the rest. Shift it to sit at the class's indentation level: line 1 gets the target indent; the remaining lines are dedented by the difference between the method's indent and the target.



106
107
108
109
110
111
112
# File 'lib/why_classes/correctors/polymorphic_function_corrector.rb', line 106

def reindent(source, target_col, method_col)
  delta = [method_col - target_col, 0].max
  pad = " " * target_col
  source.lines.each_with_index.map do |line, i|
    i.zero? ? pad + line : line.sub(/\A {0,#{delta}}/, "")
  end.join
end

.removal_range(buffer, method_node) ⇒ Object

Range covering the method's whole line(s) plus its trailing newline, and one preceding blank line if present, for a tidy removal.



116
117
118
119
120
121
122
123
124
# File 'lib/why_classes/correctors/polymorphic_function_corrector.rb', line 116

def removal_range(buffer, method_node)
  expr = method_node.location.expression
  source = buffer.source
  line_start = expr.begin_pos - method_node.location.keyword.column
  stop = expr.end_pos
  stop += 1 if source[stop] == "\n"
  line_start -= 1 if source[line_start - 1] == "\n" && source[line_start - 2] == "\n"
  ::Parser::Source::Range.new(buffer, line_start, stop)
end

.rewritten_source(method_node, field, param:, replacement:) ⇒ Object

Splice the method's own source: add the parameter and rewrite each ivar read. Offsets are relative to the method's start.



83
84
85
86
87
88
89
90
91
92
93
# File 'lib/why_classes/correctors/polymorphic_function_corrector.rb', line 83

def rewritten_source(method_node, field, param:, replacement:)
  base = method_node.location.expression.begin_pos
  src = method_node.location.expression.source
  edits = []
  name_end = method_node.location.name.end_pos - base
  edits << [name_end, name_end, "(#{param})"]
  H.collect(method_node) { |n| n.type == :ivar && n.children[0] == :"@#{field}" }.each do |ivar|
    edits << [ivar.location.expression.begin_pos - base, ivar.location.expression.end_pos - base, replacement]
  end
  splice(src, edits)
end

.splice(str, edits) ⇒ Object



95
96
97
98
99
100
# File 'lib/why_classes/correctors/polymorphic_function_corrector.rb', line 95

def splice(str, edits)
  edits.sort_by { |start, _stop, _text| -start }.each do |start, stop, text|
    str = str[0...start] + text + str[stop..]
  end
  str
end