Module: Hashira::Report::Phrases

Defined in:
lib/hashira/report/phrases.rb,
lib/hashira/report/smell_phrases.rb

Constant Summary collapse

COMPLEXITY_ADVICE =
{
  "if" => "flatten the branching — guard clauses, early returns, or polymorphism.",
  "elsif" => "replace the elsif ladder with a lookup or polymorphic dispatch.",
  "else" => "flatten the branching — guard clauses, early returns, or polymorphism.",
  "case" => "a case this size often wants polymorphism or a dispatch table.",
  "boolean" => "name the compound condition in a predicate method.",
  "rescue" => "narrow the rescue, or lift error handling to the caller.",
  "while" => "extract the loop body into its own method.",
  "until" => "extract the loop body into its own method.",
  "for" => "extract the loop body into its own method.",
  "unless" => "invert to a guard clause or a named predicate.",
  "ternary" => "extract the nested ternary into a named method."
}.freeze
DUPLICATION_ADVICE =
{
  identical: "byte-for-byte identical — extract a shared method and call it from each site.",
  literal: "differs only in literal values — extract a method, pass them as arguments.",
  message: "differs only in the receiver or message — extract a method taking the receiver.",
  constant: "differs only in a constant — extract a method and parameterize it.",
  structure: "the control flow differs — extract the common core, but verify by hand (lower confidence).",
  mixed: "extract the shared shape and pass what differs as parameters."
}.freeze

Class Method Summary collapse

Class Method Details

.addendum(parts) ⇒ Object



91
92
93
# File 'lib/hashira/report/phrases.rb', line 91

def addendum(parts)
  parts.any? { it[:shared] } ? ", keeping the shared constants as the base layer the rest builds on" : ""
end

.clause(part) ⇒ Object



84
# File 'lib/hashira/report/phrases.rb', line 84

def clause(part) = "#{part[:users].join(", ")} #{verb(part)} #{part[:constants].join(", ")}"

.footnote(detail) ⇒ Object



95
96
97
# File 'lib/hashira/report/phrases.rb', line 95

def footnote(detail)
  detail[:hot] ? " Both sites change often — fix one, miss the other." : ""
end

.message(finding) ⇒ Object



29
# File 'lib/hashira/report/phrases.rb', line 29

def message(finding) = public_send("on_#{finding.kind}", finding)

.on_complexity(finding) ⇒ Object



70
71
72
73
74
# File 'lib/hashira/report/phrases.rb', line 70

def on_complexity(finding)
  detail = finding.detail
  "#{finding.package} — cognitive #{detail[:cognitive]}, #{detail[:calls]} calls " \
    "(#{detail[:site]}). #{COMPLEXITY_ADVICE.fetch(detail[:dominant])}"
end

.on_control_parameter(finding) ⇒ Object



6
7
8
9
10
# File 'lib/hashira/report/smell_phrases.rb', line 6

def on_control_parameter(finding)
  detail = finding.detail
  "#{finding.package} is steered by #{quoted(detail[:names])} (#{detail[:site]}). " \
    "Split the method, or pass a strategy instead of a flag."
end

.on_cycle(finding) ⇒ Object



31
32
33
34
35
36
37
# File 'lib/hashira/report/phrases.rb', line 31

def on_cycle(finding)
  detail = finding.detail
  from, to = detail[:weak]
  weight = detail[:weight]
  "#{finding.package} can reach itself: #{finding.cycle.join(" -> ")} — any change may ripple back " \
    "around. The lightest edge on this cycle is #{from} -> #{to} (#{weight} ref#{"s" unless weight == 1})."
end

.on_data_clump(finding) ⇒ Object



12
13
14
15
# File 'lib/hashira/report/smell_phrases.rb', line 12

def on_data_clump(finding)
  "#{finding.package} passes the same parameters between methods (#{finding.detail[:site]}). " \
    "Introduce a parameter object."
end

.on_duplicate_method_call(finding) ⇒ Object



17
18
19
20
# File 'lib/hashira/report/smell_phrases.rb', line 17

def on_duplicate_method_call(finding)
  "#{finding.package} repeats identical calls (#{finding.detail[:site]}). " \
    "Name the result in a local variable."
end

.on_duplication(finding) ⇒ Object



76
77
78
79
80
# File 'lib/hashira/report/phrases.rb', line 76

def on_duplication(finding)
  detail = finding.detail
  "#{detail[:size]} similar fragments (mass #{detail[:mass]}) — " \
    "#{DUPLICATION_ADVICE.fetch(detail[:kind])}#{footnote(detail)}"
end

.on_feature_envy(finding) ⇒ Object



22
23
24
25
26
27
# File 'lib/hashira/report/smell_phrases.rb', line 22

def on_feature_envy(finding)
  detail = finding.detail
  names = detail[:names]
  "#{finding.package} refers to #{quoted(names)} more than to self (#{detail[:site]}). " \
    "The behavior may belong on #{names.first}."
end

.on_instance_variable_assumption(finding) ⇒ Object



29
30
31
32
# File 'lib/hashira/report/smell_phrases.rb', line 29

def on_instance_variable_assumption(finding)
  "#{finding.package} reads instance variables never set in initialize (#{finding.detail[:site]}). " \
    "Assign them in initialize, or pass the data explicitly."
end

.on_manual_dispatch(finding) ⇒ Object



34
35
36
37
# File 'lib/hashira/report/smell_phrases.rb', line 34

def on_manual_dispatch(finding)
  "#{finding.package} dispatches manually via respond_to? (#{finding.detail[:site]}). " \
    "Trust the duck type, or split the callers into two adapters."
end

.on_mixed_audience(finding) ⇒ Object



47
48
49
50
51
52
53
# File 'lib/hashira/report/phrases.rb', line 47

def on_mixed_audience(finding)
  package = finding.package
  parts = finding.detail[:parts]
  "#{package} splits #{parts.size} ways: #{parts.map { clause(it) }.join("; ")}" \
    "parts with separate client bases are separate packages in disguise. " \
    "Split #{package} along that seam#{addendum(parts)}."
end

.on_module_initialize(finding) ⇒ Object



39
40
41
42
# File 'lib/hashira/report/smell_phrases.rb', line 39

def on_module_initialize(finding)
  "#{finding.package} defines initialize in a module (#{finding.detail[:site]}). " \
    "Move construction into the including class."
end

.on_nil_check(finding) ⇒ Object



44
45
46
47
# File 'lib/hashira/report/smell_phrases.rb', line 44

def on_nil_check(finding)
  "#{finding.package} checks for nil (#{finding.detail[:site]}). " \
    "Prefer a default, a null object, or polymorphism."
end

.on_repeated_conditional(finding) ⇒ Object



49
50
51
# File 'lib/hashira/report/smell_phrases.rb', line 49

def on_repeated_conditional(finding)
  tally(finding, "branches on the same test %d times", "Replace the scattered checks with polymorphism.")
end

.on_roll_call(finding) ⇒ Object



63
64
65
66
67
68
# File 'lib/hashira/report/phrases.rb', line 63

def on_roll_call(finding)
  detail = finding.detail
  "the words #{detail[:words].join(", ")} are listed together in #{detail[:files].join(", ")}" \
    "#{detail[:packages].size} packages keep one roll-call in sync by hand. " \
    "Make the list data with a single owner."
end

.on_sdp_violation(finding) ⇒ Object



39
40
41
42
43
44
45
# File 'lib/hashira/report/phrases.rb', line 39

def on_sdp_violation(finding)
  detail = finding.detail
  from, to = detail.values_at(:from, :to)
  "#{from} (I=#{score(detail[:from_instability])}) depends on the LESS stable #{to} " \
    "(I=#{score(detail[:to_instability])}) — churn in #{to} will force churn in #{from}. " \
    "Invert the edge or extract the stable part of #{to} that #{from} needs."
end

.on_too_many_instance_variables(finding) ⇒ Object



53
54
55
# File 'lib/hashira/report/smell_phrases.rb', line 53

def on_too_many_instance_variables(finding)
  tally(finding, "holds %d instance variables", "Split the class, or gather related fields into value objects.")
end

.on_utility_function(finding) ⇒ Object



57
58
59
60
# File 'lib/hashira/report/smell_phrases.rb', line 57

def on_utility_function(finding)
  "#{finding.package} touches no instance state (#{finding.detail[:site]}). " \
    "Move it onto the object it serves, or make it a module function."
end

.on_wide_edge(finding) ⇒ Object



55
56
57
58
59
60
61
# File 'lib/hashira/report/phrases.rb', line 55

def on_wide_edge(finding)
  detail = finding.detail
  from, to = detail.values_at(:from, :to)
  names = detail[:constants]
  "#{from} -> #{to} is #{names.size} constants wide (#{names.join(", ")}) — " \
    "every one is a reason for #{from} to change. Front #{to} with one facade."
end

.quoted(names) ⇒ Object



67
# File 'lib/hashira/report/smell_phrases.rb', line 67

def quoted(names) = names.map { "'#{it}'" }.join(", ")

.score(value) ⇒ Object



82
# File 'lib/hashira/report/phrases.rb', line 82

def score(value) = format("%.2f", value)

.tally(finding, event, advice) ⇒ Object



62
63
64
65
# File 'lib/hashira/report/smell_phrases.rb', line 62

def tally(finding, event, advice)
  detail = finding.detail
  "#{finding.package} #{format(event, detail[:count])} (#{detail[:site]}). #{advice}"
end

.verb(part) ⇒ Object



86
87
88
89
# File 'lib/hashira/report/phrases.rb', line 86

def verb(part)
  return "share" if part[:shared]
  part[:users].size == 1 ? "alone uses" : "use"
end