hashira 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +106 -0
  3. data/README.md +148 -53
  4. data/exe/hashira +1 -1
  5. data/lib/hashira/analysis/finding.rb +5 -1
  6. data/lib/hashira/analysis/syntax.rb +6 -6
  7. data/lib/hashira/churn.rb +3 -4
  8. data/lib/hashira/ci/accepted.rb +9 -10
  9. data/lib/hashira/ci/baseline.rb +39 -12
  10. data/lib/hashira/ci/comparison.rb +24 -0
  11. data/lib/hashira/ci/diff.rb +4 -8
  12. data/lib/hashira/ci/finding_diff_report.rb +10 -0
  13. data/lib/hashira/ci/gate.rb +10 -2
  14. data/lib/hashira/ci/improvement.rb +1 -1
  15. data/lib/hashira/ci/ratchet.rb +41 -10
  16. data/lib/hashira/ci/ratchet_report.rb +8 -6
  17. data/lib/hashira/ci/scope.rb +10 -0
  18. data/lib/hashira/ci/slice.rb +17 -0
  19. data/lib/hashira/ci/status.rb +7 -0
  20. data/lib/hashira/ci/sweep.rb +13 -0
  21. data/lib/hashira/cli/arguments.rb +17 -8
  22. data/lib/hashira/cli/choice.rb +9 -0
  23. data/lib/hashira/cli/command_line.rb +8 -5
  24. data/lib/hashira/cli/fail_on.rb +17 -7
  25. data/lib/hashira/cli/flag.rb +3 -1
  26. data/lib/hashira/cli/flags.rb +23 -0
  27. data/lib/hashira/cli/format.rb +1 -1
  28. data/lib/hashira/cli/needs.rb +45 -0
  29. data/lib/hashira/cli/only.rb +16 -0
  30. data/lib/hashira/cli/options.rb +12 -3
  31. data/lib/hashira/cli/package_by.rb +1 -1
  32. data/lib/hashira/cli/run.rb +36 -8
  33. data/lib/hashira/cli/top.rb +13 -0
  34. data/lib/hashira/cli/usage.rb +5 -1
  35. data/lib/hashira/cli.rb +31 -11
  36. data/lib/hashira/complexity/cognitive_score.rb +44 -33
  37. data/lib/hashira/complexity/method_finding.rb +7 -5
  38. data/lib/hashira/complexity/method_score.rb +1 -1
  39. data/lib/hashira/complexity/scores.rb +11 -9
  40. data/lib/hashira/coupling/audiences.rb +3 -3
  41. data/lib/hashira/coupling/catalog.rb +8 -6
  42. data/lib/hashira/coupling/census.rb +42 -22
  43. data/lib/hashira/coupling/cycle_search.rb +11 -7
  44. data/lib/hashira/coupling/definitions.rb +1 -1
  45. data/lib/hashira/coupling/edge_map.rb +9 -8
  46. data/lib/hashira/coupling/folding.rb +5 -5
  47. data/lib/hashira/coupling/graph.rb +16 -9
  48. data/lib/hashira/coupling/metric.rb +9 -1
  49. data/lib/hashira/coupling/naming.rb +4 -4
  50. data/lib/hashira/coupling/references.rb +12 -12
  51. data/lib/hashira/coupling/report.rb +7 -9
  52. data/lib/hashira/coupling/roll_call.rb +1 -1
  53. data/lib/hashira/coupling/roll_call_findings.rb +1 -1
  54. data/lib/hashira/coupling/roster.rb +37 -12
  55. data/lib/hashira/coupling/rule.rb +1 -1
  56. data/lib/hashira/coupling/sdp_violation_findings.rb +3 -1
  57. data/lib/hashira/diagram/dot.rb +9 -6
  58. data/lib/hashira/diagram/mermaid.rb +11 -8
  59. data/lib/hashira/diagram/source.rb +5 -2
  60. data/lib/hashira/duplication/clones.rb +1 -1
  61. data/lib/hashira/duplication/clusters.rb +10 -7
  62. data/lib/hashira/duplication/delta.rb +1 -4
  63. data/lib/hashira/duplication/duplication_finding.rb +4 -2
  64. data/lib/hashira/duplication/fragment.rb +2 -2
  65. data/lib/hashira/duplication/harvest.rb +7 -5
  66. data/lib/hashira/duplication/index.rb +3 -2
  67. data/lib/hashira/duplication/similarity.rb +2 -2
  68. data/lib/hashira/duplication/variance.rb +1 -1
  69. data/lib/hashira/error.rb +0 -3
  70. data/lib/hashira/focus.rb +29 -0
  71. data/lib/hashira/git_log.rb +19 -0
  72. data/lib/hashira/hotspots/file_cost.rb +1 -1
  73. data/lib/hashira/hotspots/rollup.rb +2 -2
  74. data/lib/hashira/pipeline.rb +26 -24
  75. data/lib/hashira/project.rb +47 -32
  76. data/lib/hashira/report/columns.rb +54 -0
  77. data/lib/hashira/report/complexity_table.rb +12 -16
  78. data/lib/hashira/report/dependency_map.rb +1 -0
  79. data/lib/hashira/report/graph_payload.rb +1 -1
  80. data/lib/hashira/report/hotspot_table.rb +6 -14
  81. data/lib/hashira/report/json.rb +10 -2
  82. data/lib/hashira/report/metrics_table.rb +19 -20
  83. data/lib/hashira/report/notices.rb +37 -0
  84. data/lib/hashira/report/phrases.rb +25 -18
  85. data/lib/hashira/report/text.rb +24 -9
  86. data/lib/hashira/report/view.rb +4 -1
  87. data/lib/hashira/smells/census.rb +3 -4
  88. data/lib/hashira/smells/check.rb +1 -5
  89. data/lib/hashira/smells/contexts.rb +4 -4
  90. data/lib/hashira/smells/control_parameter.rb +1 -1
  91. data/lib/hashira/smells/data_clump.rb +2 -4
  92. data/lib/hashira/smells/duplicate_method_call.rb +3 -3
  93. data/lib/hashira/smells/feature_envy.rb +3 -3
  94. data/lib/hashira/smells/foreign.rb +14 -14
  95. data/lib/hashira/smells/instance_variable_assumption.rb +3 -3
  96. data/lib/hashira/smells/kind.rb +9 -0
  97. data/lib/hashira/smells/manual_dispatch.rb +1 -1
  98. data/lib/hashira/smells/module_initialize.rb +0 -2
  99. data/lib/hashira/smells/nil_check.rb +1 -1
  100. data/lib/hashira/smells/ownership.rb +19 -7
  101. data/lib/hashira/smells/param_check.rb +2 -2
  102. data/lib/hashira/smells/refs.rb +13 -7
  103. data/lib/hashira/smells/repeated_conditional.rb +1 -3
  104. data/lib/hashira/smells/report.rb +16 -7
  105. data/lib/hashira/smells/too_many_instance_variables.rb +3 -3
  106. data/lib/hashira/smells/visibility.rb +18 -11
  107. data/lib/hashira/trees.rb +26 -0
  108. data/lib/hashira/version.rb +1 -1
  109. data/lib/hashira.rb +14 -0
  110. metadata +16 -1
data/lib/hashira/cli.rb CHANGED
@@ -1,24 +1,44 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Hashira::CLI
4
- def self.run(argv)
5
- options = Options.parse(argv)
6
- usage?(options) ? Usage.public_send(options.mode) : new(options).run
4
+ MISUSE = 2
5
+ BROKEN = 70
6
+
7
+ def initialize(argv)
8
+ @argv = argv
9
+ end
10
+
11
+ def status
12
+ dispatch(Options.parse(@argv))
7
13
  rescue Hashira::Error => error
8
14
  failure(error)
15
+ rescue StandardError => error
16
+ crash(error)
9
17
  end
10
18
 
11
- def self.usage?(options) = Usage::PAGES.include?(options.mode)
19
+ private
20
+
21
+ def dispatch(options) = usage?(options) ? Usage.public_send(options.mode) : timed(options)
12
22
 
13
- def self.failure(error)
14
- warn("hashira: #{error.message}")
15
- 1
23
+ def usage?(options) = Usage::PAGES.include?(options.mode)
24
+
25
+ def timed(options)
26
+ started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
27
+ pipeline = options.pipeline
28
+ Run.new(pipeline, options).status.tap do
29
+ Hashira::Report::Notices.new.finished(pipeline.project.files.size, elapsed(started))
30
+ end
16
31
  end
17
32
 
18
- def initialize(options)
19
- @options = options
20
- @pipeline = options.pipeline
33
+ def elapsed(started) = format("%.1f", Process.clock_gettime(Process::CLOCK_MONOTONIC) - started)
34
+
35
+ def failure(error)
36
+ Kernel.warn("hashira: #{error.message}")
37
+ MISUSE
21
38
  end
22
39
 
23
- def run = Run.new(@pipeline, @options).status
40
+ def crash(error)
41
+ Hashira::Report::Notices.new.crashed(error)
42
+ BROKEN
43
+ end
24
44
  end
@@ -3,37 +3,35 @@
3
3
  require "prism"
4
4
 
5
5
  class Hashira::Complexity::CognitiveScore
6
- HANDLERS = {
7
- Prism::IfNode => :on_if,
8
- Prism::UnlessNode => :on_nester,
9
- Prism::WhileNode => :on_nester,
10
- Prism::UntilNode => :on_nester,
11
- Prism::ForNode => :on_nester,
12
- Prism::CaseNode => :on_nester,
13
- Prism::CaseMatchNode => :on_nester,
14
- Prism::BeginNode => :on_begin,
15
- Prism::AndNode => :on_boolean,
16
- Prism::OrNode => :on_boolean,
17
- Prism::BlockNode => :on_block,
18
- Prism::CallNode => :on_call
19
- }.freeze
20
-
21
- LABELS = {
22
- Prism::UnlessNode => "unless", Prism::WhileNode => "while",
23
- Prism::UntilNode => "until", Prism::ForNode => "for",
24
- Prism::CaseNode => "case", Prism::CaseMatchNode => "case"
25
- }.freeze
26
-
27
- def initialize(def_node)
28
- @increments = []
29
- @calls = 0
30
- @nesting = 0
31
- visit(def_node.body)
6
+ HANDLERS = { Prism::IfNode => :on_if, Prism::BeginNode => :on_begin, Prism::BlockNode => :on_block }
7
+ .merge(Prism::AndNode => :on_boolean, Prism::OrNode => :on_boolean, Prism::CallNode => :on_call)
8
+ .merge(Prism::UnlessNode => :on_nester, Prism::WhileNode => :on_nester, Prism::UntilNode => :on_nester)
9
+ .merge(Prism::ForNode => :on_nester, Prism::CaseNode => :on_nester, Prism::CaseMatchNode => :on_nester)
10
+ .freeze
11
+
12
+ LABELS = { Prism::UnlessNode => "unless", Prism::WhileNode => "while", Prism::UntilNode => "until" }
13
+ .merge(Prism::ForNode => "for", Prism::CaseNode => "case", Prism::CaseMatchNode => "case").freeze
14
+
15
+ def initialize(node)
16
+ @node = node
17
+ end
18
+
19
+ def increments
20
+ walked
21
+ @_increments
32
22
  end
33
23
 
34
- attr_reader :increments, :calls, :nesting
24
+ def calls
25
+ walked
26
+ @_calls
27
+ end
35
28
 
36
- def total = @increments.sum(&:cost)
29
+ def nesting
30
+ walked
31
+ @_nesting
32
+ end
33
+
34
+ def total = increments.sum(&:cost)
37
35
 
38
36
  def visit(node)
39
37
  return unless node
@@ -41,28 +39,41 @@ class Hashira::Complexity::CognitiveScore
41
39
  end
42
40
 
43
41
  def add(node, cost, label)
44
- @increments << Hashira::Complexity::Increment.new(line: node.location.start_line, cost:, label:)
42
+ @_increments << Hashira::Complexity::Increment.new(line: node.location.start_line, cost:, label:)
45
43
  end
46
44
 
47
45
  def deeper
48
- @nesting += 1
46
+ @_nesting += 1
49
47
  yield
50
- @nesting -= 1
48
+ @_nesting -= 1
51
49
  end
52
50
 
53
51
  private
54
52
 
53
+ def walked
54
+ return if @_walked
55
+ @_walked = true
56
+ blank
57
+ visit(@node.body)
58
+ end
59
+
60
+ def blank
61
+ @_increments = []
62
+ @_calls = 0
63
+ @_nesting = 0
64
+ end
65
+
55
66
  def descend(node) = node.compact_child_nodes.each { visit(it) }
56
67
 
57
68
  def on_call(node)
58
- @calls += 1
69
+ @_calls += 1
59
70
  descend(node)
60
71
  end
61
72
 
62
73
  def on_block(node) = deeper { descend(node) }
63
74
 
64
75
  def on_nester(node)
65
- add(node, 1 + @nesting, LABELS.fetch(node.class))
76
+ add(node, 1 + @_nesting, LABELS.fetch(node.class))
66
77
  deeper { descend(node) }
67
78
  end
68
79
 
@@ -3,6 +3,8 @@
3
3
  class Hashira::Complexity::MethodFinding
4
4
  KIND = "complexity"
5
5
 
6
+ Effort = Data.define(:cognitive, :calls, :site, :dominant)
7
+
6
8
  def initialize(score)
7
9
  @score = score
8
10
  end
@@ -14,16 +16,16 @@ class Hashira::Complexity::MethodFinding
14
16
  private
15
17
 
16
18
  def detail
17
- { cognitive: @score.cognitive, calls: @score.calls, site: "#{@score.file}:#{@score.line}", dominant: }
19
+ Effort.new(cognitive: @score.cognitive, calls: @score.calls, site: "#{@score.file}:#{@score.line}", dominant:)
18
20
  end
19
21
 
20
22
  def evidence
21
- @score.increments.group_by(&:label).map { |label, incs| lines(label, incs) }
23
+ @score.increments.group_by(&:label).map { |label, increments| lines(label, increments) }
22
24
  end
23
25
 
24
- def lines(label, incs)
25
- lines = incs.map(&:line).uniq
26
- "#{label} +#{incs.sum(&:cost)} (line#{"s" if lines.size > 1} #{lines.join(", ")})"
26
+ def lines(label, increments)
27
+ lines = increments.map(&:line).uniq
28
+ "#{label} +#{increments.sum(&:cost)} (line#{"s" if lines.size > 1} #{lines.join(", ")})"
27
29
  end
28
30
 
29
31
  def dominant
@@ -6,7 +6,7 @@ module Hashira
6
6
 
7
7
  MethodScore =
8
8
  Data.define(:subject, :file, :line, :cognitive, :calls, :increments) do
9
- def to_h = { subject:, file:, line:, cognitive:, calls: }
9
+ def to_h = super.except(:increments)
10
10
 
11
11
  def cells = [subject, cognitive, calls, "#{file}:#{line}"]
12
12
  end
@@ -5,35 +5,37 @@ class Hashira::Complexity::Scores
5
5
 
6
6
  def initialize(project, trees)
7
7
  @project = project
8
- @scores = trees.flat_map { |path, tree| harvest(path, tree) }
8
+ @trees = trees
9
9
  end
10
10
 
11
- def ranked = @scores.sort_by { -it.cognitive }
11
+ def ranked = scores.sort_by { -it.cognitive }
12
12
 
13
- def classes = Hashira::Complexity::Rollup.new(@scores).classes.sort_by { -it.cognitive }
13
+ def classes = Hashira::Complexity::Rollup.new(scores).classes.sort_by { -it.cognitive }
14
14
 
15
15
  def findings = flagged.map { Hashira::Complexity::MethodFinding.new(it).to_finding }
16
16
 
17
17
  private
18
18
 
19
+ def scores = @_scores ||= @trees.flat_map { |path, tree| harvest(path, tree) }
20
+
19
21
  def flagged = ranked.select { it.cognitive >= THRESHOLD }
20
22
 
21
23
  def harvest(path, tree)
22
- rel = @project.relative(path)
23
- sites(tree).map { |full, node| score(rel, full, node) }
24
+ relative = @project.relative(path)
25
+ sites(tree).map { |full, node| score(relative, full, node) }
24
26
  end
25
27
 
26
28
  def sites(tree)
27
29
  found = []
28
- Hashira::Analysis::TypeWalk.each(tree) do |type_node, full|
29
- Hashira::Analysis::Syntax.direct(type_node).each { found << [full, it] }
30
+ Hashira::Analysis::TypeWalk.each(tree) do |type, full|
31
+ Hashira::Analysis::Syntax.direct(type).each { found << [full, it] }
30
32
  end
31
33
  found
32
34
  end
33
35
 
34
- def score(rel, full, node)
36
+ def score(relative, full, node)
35
37
  Hashira::Complexity::MethodScore.new(
36
- subject: subject(full, node), file: rel, line: node.location.start_line,
38
+ subject: subject(full, node), file: relative, line: node.location.start_line,
37
39
  **tallies(Hashira::Complexity::CognitiveScore.new(node))
38
40
  )
39
41
  end
@@ -11,13 +11,13 @@ class Hashira::Coupling::Audiences
11
11
 
12
12
  def split? = parts.size >= 2
13
13
 
14
- def parts = @parts ||= [common, *slices].compact.select { it.constants.size >= MIN }
14
+ def parts = @_parts ||= [common, *slices].compact.select { it.constants.size >= MIN }
15
15
 
16
16
  private
17
17
 
18
- def clients = @clients ||= @usage.keys.sort
18
+ def clients = @_clients ||= @usage.keys.sort
19
19
 
20
- def shared = @shared ||= tally.select { |_constant, users| users.size * 2 > clients.size }.keys.to_set
20
+ def shared = @_shared ||= tally.select { |_constant, users| users.size * 2 > clients.size }.keys.to_set
21
21
 
22
22
  def tally
23
23
  clients.each_with_object(Hash.new { |hash, key| hash[key] = [] }) do |client, seen|
@@ -5,15 +5,13 @@ class Hashira::Coupling::Catalog
5
5
 
6
6
  def initialize(definitions)
7
7
  @definitions = definitions
8
- @naming = Hashira::Coupling::Naming.new(definitions)
9
- @entries = definitions.map { |node, full, folder| entry(node, full, folder) }
10
8
  end
11
9
 
12
- def each(&) = @entries.each(&)
10
+ def each(&) = entries.each(&)
13
11
 
14
- def prefix = @naming.segments
12
+ def prefix = naming.segments
15
13
 
16
- def strip(path) = @naming.strip(path)
14
+ def strip(path) = naming.strip(path)
17
15
 
18
16
  def roots = @definitions.roots
19
17
 
@@ -21,7 +19,11 @@ class Hashira::Coupling::Catalog
21
19
 
22
20
  private
23
21
 
22
+ def naming = @_naming ||= Hashira::Coupling::Naming.new(@definitions)
23
+
24
+ def entries = @_entries ||= @definitions.map { |node, full, folder| entry(node, full, folder) }
25
+
24
26
  def entry(node, full, folder)
25
- Hashira::Coupling::Definition.new(node:, path: @naming.strip(full), folder:)
27
+ Hashira::Coupling::Definition.new(node:, path: naming.strip(full), folder:)
26
28
  end
27
29
  end
@@ -2,28 +2,26 @@
2
2
 
3
3
  class Hashira::Coupling::Census
4
4
  def initialize(project, trees, packaging: :folder)
5
- @catalog = Hashira::Coupling::Catalog.new(Hashira::Coupling::Definitions.new(project, trees))
6
- @placement = Hashira::Coupling::Placement.build(packaging, project, @catalog)
7
- @folding = Hashira::Coupling::NoFolding
8
- @roster = tally
9
- settle
5
+ @project = project
6
+ @trees = trees
7
+ @packaging = packaging
10
8
  end
11
9
 
12
- def packaging = @placement.mode
10
+ def packaging = placement.mode
13
11
 
14
- def types = @roster.types
12
+ def types = roster.types
15
13
 
16
- def prefix = @catalog.prefix
14
+ def prefix = catalog.prefix
17
15
 
18
- def origins = @roster.origins
16
+ def origins = roster.origins
19
17
 
20
- def roots = @catalog.roots
18
+ def roots = catalog.roots
21
19
 
22
- def packages = @roster.packages | @placement.baseline
20
+ def packages = roster.packages | placement.baseline
23
21
 
24
- def folds = @folding.disclosed
22
+ def folds = folding.disclosed
25
23
 
26
- def strip(segments) = @catalog.strip(segments)
24
+ def strip(segments) = catalog.strip(segments)
27
25
 
28
26
  def holder(segments)
29
27
  path = strip(segments)
@@ -34,22 +32,44 @@ class Hashira::Coupling::Census
34
32
 
35
33
  def pinpoint(segments) = scope.pinpoint(segments)
36
34
 
37
- def charge(file, nesting) = translate(@placement.charge(file, nesting))
35
+ def charge(file, nesting) = translate(placement.charge(file, nesting))
38
36
 
39
37
  private
40
38
 
41
- def type?(segments) = @roster.type?(segments)
39
+ def catalog = @_catalog ||= Hashira::Coupling::Catalog.new(Hashira::Coupling::Definitions.new(@project, @trees))
42
40
 
43
- def tally
44
- Hashira::Coupling::Roster.new(@placement.placed.map { |definition, package| [definition, translate(package)] })
41
+ def placement = @_placement ||= Hashira::Coupling::Placement.build(@packaging, @project, catalog)
42
+
43
+ def roster
44
+ settled
45
+ @_roster
46
+ end
47
+
48
+ def folding
49
+ settled
50
+ @_folding
51
+ end
52
+
53
+ def settled
54
+ return if @_settled
55
+ @_settled = true
56
+ @_folding = Hashira::Coupling::NoFolding
57
+ @_roster = tally
58
+ refold
59
+ end
60
+
61
+ def refold
62
+ @_folding = placement.folding(self)
63
+ @_roster = tally unless @_folding.map.empty?
45
64
  end
46
65
 
47
- def settle
48
- @folding = @placement.folding(self)
49
- @roster = tally unless @folding.map.empty?
66
+ def type?(segments) = roster.type?(segments)
67
+
68
+ def tally
69
+ Hashira::Coupling::Roster.new(placement.placed.map { |definition, package| [definition, translate(package)] })
50
70
  end
51
71
 
52
- def translate(package) = @folding.map.fetch(package, package)
72
+ def translate(package) = folding.map.fetch(package, package)
53
73
 
54
- def scope = Hashira::Coupling::Scope.new(@roster.registry, @catalog, @placement)
74
+ def scope = Hashira::Coupling::Scope.new(roster.registry, catalog, placement)
55
75
  end
@@ -4,8 +4,6 @@ class Hashira::Coupling::CycleSearch
4
4
  def initialize(dependencies, package)
5
5
  @dependencies = dependencies
6
6
  @package = package
7
- @predecessor = {}
8
- @queue = dependencies[package].to_a.each { @predecessor[it] = package }
9
7
  end
10
8
 
11
9
  def path
@@ -14,8 +12,14 @@ class Hashira::Coupling::CycleSearch
14
12
 
15
13
  private
16
14
 
15
+ def prepare
16
+ @_predecessor = {}
17
+ @_queue = @dependencies[@package].to_a.each { @_predecessor[it] = @package }
18
+ end
19
+
17
20
  def cycle?
18
- while (node = @queue.shift)
21
+ prepare
22
+ while (node = @_queue.shift)
19
23
  return true if node == @package
20
24
  visit(node)
21
25
  end
@@ -23,15 +27,15 @@ class Hashira::Coupling::CycleSearch
23
27
 
24
28
  def visit(node)
25
29
  @dependencies[node].each do |neighbor|
26
- next if @predecessor.key?(neighbor)
27
- @predecessor[neighbor] = node
28
- @queue << neighbor
30
+ next if @_predecessor.key?(neighbor)
31
+ @_predecessor[neighbor] = node
32
+ @_queue << neighbor
29
33
  end
30
34
  end
31
35
 
32
36
  def unwind(path)
33
37
  first = path.first
34
38
  return path if first == @package && path.size > 1
35
- unwind(path.unshift(@predecessor[first]))
39
+ unwind(path.unshift(@_predecessor[first]))
36
40
  end
37
41
  end
@@ -15,7 +15,7 @@ class Hashira::Coupling::Definitions
15
15
  def packages = @trees.keys.map { @project.package(it) }.uniq
16
16
 
17
17
  def roots
18
- @roots ||= @trees.each_value.with_object(Set.new) { |tree, set| survey(tree, set) }
18
+ @_roots ||= @trees.each_value.with_object(Set.new) { |tree, set| survey(tree, set) }
19
19
  end
20
20
 
21
21
  private
@@ -3,15 +3,16 @@
3
3
  class Hashira::Coupling::EdgeMap
4
4
  def initialize(census)
5
5
  @census = census
6
- @dependencies = sets
7
- @evidence = sets
8
- @usage = sets
9
6
  end
10
7
 
11
- attr_reader :dependencies, :evidence, :usage
8
+ def dependencies = @_dependencies ||= sets
9
+
10
+ def evidence = @_evidence ||= sets
11
+
12
+ def usage = @_usage ||= sets
12
13
 
13
14
  def record(source, file, tree)
14
- Hashira::Coupling::References.sightings(tree, @census.roots).each do |segments, line, nesting, home|
15
+ Hashira::Coupling::References.new(@census.roots).sightings(tree).each do |segments, line, nesting, home|
15
16
  note(@census.charge(file, home), locate(segments, nesting), segments, "#{source}:#{line}")
16
17
  end
17
18
  end
@@ -26,8 +27,8 @@ class Hashira::Coupling::EdgeMap
26
27
 
27
28
  def note(from, to, segments, site)
28
29
  return unless to && to != from
29
- @dependencies[from] << to
30
- @evidence[[from, to]] << "#{site}: #{segments.join("::")}"
31
- @usage[[from, to]] << @census.holder(segments)
30
+ dependencies[from] << to
31
+ evidence[[from, to]] << "#{site}: #{segments.join("::")}"
32
+ usage[[from, to]] << @census.holder(segments)
32
33
  end
33
34
  end
@@ -11,7 +11,7 @@ class Hashira::Coupling::Folding
11
11
  @suffixes = suffixes
12
12
  end
13
13
 
14
- def map = @map ||= links.keys.to_h { [it, settle(it, links)] }.reject { |from, to| from == to }
14
+ def map = @_map ||= links.keys.to_h { [it, settle(it, links)] }.reject { |from, to| from == to }
15
15
 
16
16
  def disclosed
17
17
  map.map { |from, to| { from:, to:, via: parents.key?(from) ? "base" : "suffix" } }
@@ -19,10 +19,10 @@ class Hashira::Coupling::Folding
19
19
 
20
20
  private
21
21
 
22
- def links = @links ||= named.merge(parents)
22
+ def links = @_links ||= named.merge(parents)
23
23
 
24
24
  def parents
25
- @parents ||= singles.select { |_name, one| one.superclass }.to_h { |name, one| [name, target(one)] }.compact
25
+ @_parents ||= singles.select { |_name, one| one.superclass }.to_h { |name, one| [name, target(one)] }.compact
26
26
  end
27
27
 
28
28
  def named
@@ -38,11 +38,11 @@ class Hashira::Coupling::Folding
38
38
 
39
39
  def trims?(name, suffix) = name.end_with?(suffix) && name != suffix
40
40
 
41
- def singles = @singles ||= lone.group_by(&:name).transform_values { it.find(&:superclass) || it.last }
41
+ def singles = @_singles ||= lone.group_by(&:name).transform_values { it.find(&:superclass) || it.last }
42
42
 
43
43
  def lone = @definitions.select(&:singular?).reject { anchored.include?(it.name) }
44
44
 
45
- def anchored = @anchored ||= @definitions.select(&:nested?).to_set(&:name)
45
+ def anchored = @_anchored ||= @definitions.select(&:nested?).to_set(&:name)
46
46
 
47
47
  def target(one) = @census.pinpoint(Hashira::Analysis::Syntax.segments(one.superclass))
48
48
 
@@ -2,18 +2,18 @@
2
2
 
3
3
  class Hashira::Coupling::Graph
4
4
  def initialize(project, trees, census)
5
+ @project = project
5
6
  @census = census
6
7
  @trees = trees
7
- @map = Hashira::Coupling::EdgeMap.new(census)
8
- trees.each { |file, tree| @map.record(project.relative(file), file, tree) }
9
- @cycles = Hashira::Coupling::Cycles.new(@map.dependencies, self)
10
8
  end
11
9
 
12
- attr_reader :cycles, :trees
10
+ attr_reader :trees
11
+
12
+ def cycles = @_cycles ||= Hashira::Coupling::Cycles.new(map.dependencies, self)
13
13
 
14
14
  def charge(file) = @census.charge(file, [])
15
15
 
16
- def packages = (@census.packages | @map.dependencies.keys)
16
+ def packages = (@census.packages | dependencies.keys)
17
17
 
18
18
  def packaging = @census.packaging
19
19
 
@@ -34,13 +34,13 @@ class Hashira::Coupling::Graph
34
34
  end
35
35
  end
36
36
 
37
- def evidence(from, to) = @map.evidence[[from, to]]
37
+ def evidence(from, to) = map.evidence[[from, to]]
38
38
 
39
- def usage(package) = incoming(package).to_h { [it, @map.usage[[it, package]]] }
39
+ def usage(package) = incoming(package).to_h { [it, map.usage[[it, package]]] }
40
40
 
41
41
  def constants(edge)
42
42
  from, to = edge.deconstruct
43
- @map.usage[[from, to]].sort
43
+ map.usage[[from, to]].sort
44
44
  end
45
45
 
46
46
  def metric(package)
@@ -59,5 +59,12 @@ class Hashira::Coupling::Graph
59
59
 
60
60
  private
61
61
 
62
- def dependencies = @map.dependencies
62
+ def map
63
+ @_map ||=
64
+ Hashira::Coupling::EdgeMap.new(@census).tap do |edges|
65
+ @trees.each { |file, tree| edges.record(@project.relative(file), file, tree) }
66
+ end
67
+ end
68
+
69
+ def dependencies = map.dependencies
63
70
  end
@@ -9,7 +9,15 @@ module Hashira
9
9
  total.zero? ? 0.0 : efferent.fdiv(total)
10
10
  end
11
11
 
12
- def to_h = { tc: types, ca: afferent, ce: efferent, i: instability }
12
+ def isolated? = (efferent + afferent).zero?
13
+
14
+ def order = [isolated? ? 1 : 0, instability]
15
+
16
+ def to_h = counts.merge(i: (instability unless isolated?))
17
+
18
+ def counts = { tc: types, ca: afferent, ce: efferent }
19
+
20
+ def cells = [types, afferent, efferent, isolated? ? "—" : format("%.2f", instability)]
13
21
  end
14
22
  end
15
23
  end
@@ -2,13 +2,13 @@
2
2
 
3
3
  class Hashira::Coupling::Naming
4
4
  def initialize(definitions)
5
- @segments = Hashira::Coupling::NamespacePrefix.infer(definitions)
5
+ @definitions = definitions
6
6
  end
7
7
 
8
- attr_reader :segments
8
+ def segments = @_segments ||= Hashira::Coupling::NamespacePrefix.infer(@definitions)
9
9
 
10
10
  def strip(path)
11
- return [] if @segments.first(path.length) == path
12
- path.drop(@segments.length.downto(0).find { path.first(it) == @segments.last(it) })
11
+ return [] if segments.first(path.length) == path
12
+ path.drop(segments.length.downto(0).find { path.first(it) == segments.last(it) })
13
13
  end
14
14
  end
@@ -3,34 +3,34 @@
3
3
  require "prism"
4
4
 
5
5
  class Hashira::Coupling::References
6
- def self.list(tree) = new.sightings(tree).map(&:first)
7
-
8
- def self.sightings(tree, roots = nil) = new(roots).sightings(tree)
9
-
10
6
  def initialize(roots = nil)
11
7
  @roots = roots
12
- @found = []
13
- @scopes = [[]]
14
8
  end
15
9
 
10
+ def list(tree) = sightings(tree).map(&:first)
11
+
16
12
  def sightings(tree)
17
13
  collect(tree)
18
- @found
14
+ found
19
15
  end
20
16
 
21
17
  private
22
18
 
23
- def nesting = @scopes.last
19
+ def found = @_found ||= []
20
+
21
+ def scopes = @_scopes ||= [[]]
22
+
23
+ def nesting = scopes.last
24
24
 
25
25
  def collect(node, home = nesting)
26
26
  return unless node
27
- return @found << sighting(node, home) if constant?(node)
27
+ return found << sighting(node, home) if constant?(node)
28
28
  return enter(node) if definition?(node)
29
29
  node.compact_child_nodes.each { collect(it, home) }
30
30
  end
31
31
 
32
32
  def sighting(node, home)
33
- [syntax.segments(node), node.location.start_line, syntax.cbase?(node) ? nil : nesting, home]
33
+ [syntax.segments(node), node.location.start_line, syntax.rooted?(node) ? nil : nesting, home]
34
34
  end
35
35
 
36
36
  def syntax = Hashira::Analysis::Syntax
@@ -48,8 +48,8 @@ class Hashira::Coupling::References
48
48
  def anchor(node) = syntax.anchor(nesting, syntax.segments(node.constant_path), @roots)
49
49
 
50
50
  def inside(scope)
51
- @scopes << scope
51
+ scopes << scope
52
52
  yield
53
- @scopes.pop
53
+ scopes.pop
54
54
  end
55
55
  end