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
- .addendum(parts) ⇒ Object
- .clause(part) ⇒ Object
- .footnote(detail) ⇒ Object
- .message(finding) ⇒ Object
- .on_complexity(finding) ⇒ Object
- .on_control_parameter(finding) ⇒ Object
- .on_cycle(finding) ⇒ Object
- .on_data_clump(finding) ⇒ Object
- .on_duplicate_method_call(finding) ⇒ Object
- .on_duplication(finding) ⇒ Object
- .on_feature_envy(finding) ⇒ Object
- .on_instance_variable_assumption(finding) ⇒ Object
- .on_manual_dispatch(finding) ⇒ Object
- .on_mixed_audience(finding) ⇒ Object
- .on_module_initialize(finding) ⇒ Object
- .on_nil_check(finding) ⇒ Object
- .on_repeated_conditional(finding) ⇒ Object
- .on_roll_call(finding) ⇒ Object
- .on_sdp_violation(finding) ⇒ Object
- .on_too_many_instance_variables(finding) ⇒ Object
- .on_utility_function(finding) ⇒ Object
- .on_wide_edge(finding) ⇒ Object
- .quoted(names) ⇒ Object
- .score(value) ⇒ Object
- .tally(finding, event, advice) ⇒ Object
- .verb(part) ⇒ Object
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 (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 |