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
- .correctable?(shape, method_node) ⇒ Boolean
- .corrector(class_node, method_node, style:) ⇒ Object
-
.freestanding(class_node, method_node, field, style) ⇒ Object
--- rewriting -----------------------------------------------------------.
-
.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. -
.no_parameters?(method_node) ⇒ Boolean
--- preconditions -------------------------------------------------------.
-
.reindent(source, target_col, method_col) ⇒ Object
The method source has no leading indent on line 1 and absolute indent on the rest.
-
.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.
-
.rewritten_source(method_node, field, param:, replacement:) ⇒ Object
Splice the method's own source: add the parameter and rewrite each ivar read.
- .splice(str, edits) ⇒ Object
Class Method Details
.correctable?(shape, method_node) ⇒ 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. 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.
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 -------------------------------------------------------
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 |