hashira 0.7.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 (97) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +38 -0
  3. data/README.md +29 -0
  4. data/exe/hashira +1 -1
  5. data/lib/hashira/analysis/finding.rb +1 -1
  6. data/lib/hashira/analysis/syntax.rb +6 -6
  7. data/lib/hashira/churn.rb +1 -10
  8. data/lib/hashira/ci/accepted.rb +9 -7
  9. data/lib/hashira/ci/baseline.rb +34 -30
  10. data/lib/hashira/ci/comparison.rb +24 -0
  11. data/lib/hashira/ci/diff.rb +0 -15
  12. data/lib/hashira/ci/gate.rb +3 -3
  13. data/lib/hashira/ci/ratchet.rb +10 -4
  14. data/lib/hashira/ci/ratchet_report.rb +4 -4
  15. data/lib/hashira/ci/scope.rb +0 -1
  16. data/lib/hashira/ci/slice.rb +17 -0
  17. data/lib/hashira/ci/sweep.rb +13 -0
  18. data/lib/hashira/cli/arguments.rb +12 -8
  19. data/lib/hashira/cli/choice.rb +9 -0
  20. data/lib/hashira/cli/command_line.rb +8 -5
  21. data/lib/hashira/cli/fail_on.rb +6 -6
  22. data/lib/hashira/cli/flag.rb +3 -1
  23. data/lib/hashira/cli/flags.rb +10 -0
  24. data/lib/hashira/cli/format.rb +1 -1
  25. data/lib/hashira/cli/needs.rb +13 -2
  26. data/lib/hashira/cli/only.rb +16 -0
  27. data/lib/hashira/cli/options.rb +8 -10
  28. data/lib/hashira/cli/package_by.rb +1 -1
  29. data/lib/hashira/cli/run.rb +9 -8
  30. data/lib/hashira/cli.rb +22 -21
  31. data/lib/hashira/complexity/cognitive_score.rb +44 -33
  32. data/lib/hashira/complexity/method_finding.rb +7 -5
  33. data/lib/hashira/complexity/method_score.rb +1 -1
  34. data/lib/hashira/complexity/scores.rb +11 -9
  35. data/lib/hashira/coupling/audiences.rb +3 -3
  36. data/lib/hashira/coupling/catalog.rb +8 -6
  37. data/lib/hashira/coupling/census.rb +42 -22
  38. data/lib/hashira/coupling/cycle_search.rb +11 -7
  39. data/lib/hashira/coupling/definitions.rb +1 -1
  40. data/lib/hashira/coupling/edge_map.rb +9 -8
  41. data/lib/hashira/coupling/folding.rb +5 -5
  42. data/lib/hashira/coupling/graph.rb +16 -9
  43. data/lib/hashira/coupling/metric.rb +3 -1
  44. data/lib/hashira/coupling/naming.rb +4 -4
  45. data/lib/hashira/coupling/references.rb +12 -12
  46. data/lib/hashira/coupling/report.rb +7 -9
  47. data/lib/hashira/coupling/roll_call.rb +1 -1
  48. data/lib/hashira/coupling/roll_call_findings.rb +1 -1
  49. data/lib/hashira/coupling/roster.rb +37 -12
  50. data/lib/hashira/coupling/rule.rb +1 -1
  51. data/lib/hashira/coupling/sdp_violation_findings.rb +3 -1
  52. data/lib/hashira/duplication/clones.rb +1 -1
  53. data/lib/hashira/duplication/clusters.rb +10 -7
  54. data/lib/hashira/duplication/delta.rb +1 -4
  55. data/lib/hashira/duplication/duplication_finding.rb +4 -2
  56. data/lib/hashira/duplication/fragment.rb +2 -2
  57. data/lib/hashira/duplication/harvest.rb +7 -5
  58. data/lib/hashira/duplication/index.rb +3 -2
  59. data/lib/hashira/duplication/similarity.rb +2 -2
  60. data/lib/hashira/duplication/variance.rb +1 -1
  61. data/lib/hashira/error.rb +0 -3
  62. data/lib/hashira/focus.rb +29 -0
  63. data/lib/hashira/git_log.rb +19 -0
  64. data/lib/hashira/hotspots/file_cost.rb +1 -1
  65. data/lib/hashira/hotspots/rollup.rb +2 -2
  66. data/lib/hashira/pipeline.rb +22 -14
  67. data/lib/hashira/project.rb +35 -34
  68. data/lib/hashira/report/columns.rb +14 -8
  69. data/lib/hashira/report/graph_payload.rb +1 -1
  70. data/lib/hashira/report/hotspot_table.rb +1 -1
  71. data/lib/hashira/report/json.rb +2 -1
  72. data/lib/hashira/report/metrics_table.rb +3 -3
  73. data/lib/hashira/report/phrases.rb +23 -18
  74. data/lib/hashira/smells/census.rb +3 -4
  75. data/lib/hashira/smells/check.rb +1 -5
  76. data/lib/hashira/smells/contexts.rb +4 -4
  77. data/lib/hashira/smells/control_parameter.rb +1 -1
  78. data/lib/hashira/smells/data_clump.rb +2 -4
  79. data/lib/hashira/smells/duplicate_method_call.rb +3 -3
  80. data/lib/hashira/smells/feature_envy.rb +3 -3
  81. data/lib/hashira/smells/foreign.rb +14 -14
  82. data/lib/hashira/smells/instance_variable_assumption.rb +3 -3
  83. data/lib/hashira/smells/kind.rb +9 -0
  84. data/lib/hashira/smells/manual_dispatch.rb +1 -1
  85. data/lib/hashira/smells/module_initialize.rb +0 -2
  86. data/lib/hashira/smells/nil_check.rb +1 -1
  87. data/lib/hashira/smells/ownership.rb +19 -7
  88. data/lib/hashira/smells/param_check.rb +2 -2
  89. data/lib/hashira/smells/refs.rb +13 -7
  90. data/lib/hashira/smells/repeated_conditional.rb +1 -3
  91. data/lib/hashira/smells/report.rb +16 -7
  92. data/lib/hashira/smells/too_many_instance_variables.rb +3 -3
  93. data/lib/hashira/smells/visibility.rb +18 -11
  94. data/lib/hashira/trees.rb +8 -4
  95. data/lib/hashira/version.rb +1 -1
  96. data/lib/hashira.rb +8 -0
  97. metadata +9 -1
@@ -1,25 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Hashira::CLI
4
- PAGE = { directories: [], baseline: "", fail_on: [], skip: [], packaging: :auto, top: nil, compact: nil }.freeze
5
-
6
4
  Options =
7
- Data.define(:directories, :mode, :baseline, :fail_on, :skip, :packaging, :top, :compact) do
5
+ Data.define(:directories, :mode, :baseline, :fail_on, :skip, :only, :packaging, :top, :compact) do
8
6
  def self.parse(argv) = CommandLine.new(argv).options
9
7
 
10
- def self.page(mode) = new(**PAGE, mode:)
11
-
12
- def initialize(**attributes)
13
- super
14
- Needs.check(self)
8
+ def self.build(mode)
9
+ new(
10
+ directories: [], baseline: "", fail_on: [], skip: [], only: [],
11
+ packaging: :auto, top: nil, compact: nil, mode:
12
+ )
15
13
  end
16
14
 
17
15
  def pipeline
18
- chosen = Hashira::Project.detect(directories)
16
+ chosen = Hashira::Project.new(directories)
19
17
  told = Hashira::Report::Notices.new
20
18
  told.scanning(chosen.files.size)
21
19
  told.rails if directories.empty? && File.exist?("config/application.rb")
22
- Hashira::Pipeline.new(chosen, enabled: analyzers, packaging:)
20
+ Hashira::Pipeline.new(chosen, enabled: analyzers, packaging:, only:)
23
21
  end
24
22
 
25
23
  def analyzers = Hashira::Pipeline::ANALYZERS - skip
@@ -9,6 +9,6 @@ module Hashira::CLI::PackageBy
9
9
  return :auto if value.empty?
10
10
  choice = value.to_sym
11
11
  return choice if CHOICES.include?(choice)
12
- raise(Hashira::Error.unknown("--package-by", value, CHOICES))
12
+ raise(Hashira::CLI::Choice.unknown("--package-by", value, CHOICES))
13
13
  end
14
14
  end
@@ -1,26 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Hashira::CLI::Run
4
- MODES = { update: :update, ratchet: :check, fail_on: :guard, json: :json, dot: :diagram, mermaid: :diagram }.freeze
5
-
6
4
  def initialize(pipeline, options)
7
5
  @pipeline = pipeline
8
6
  @options = options
9
7
  end
10
8
 
9
+ MODES = { update: :update, ratchet: :check, fail_on: :guard }
10
+ .merge(json: :json, dot: :diagram, mermaid: :diagram).freeze
11
+
11
12
  def status = __send__(MODES.fetch(@options.mode, :text))
12
13
 
13
14
  private
14
15
 
15
16
  def graph = @pipeline.graph
16
17
 
17
- def findings = @findings ||= accepted.screen(@pipeline.findings)
18
+ def findings = @_findings ||= accepted.screen(@pipeline.findings)
18
19
 
19
20
  def accepted
20
21
  path = @options.baseline
21
- stop = Hashira::CI::Baseline.trouble(path)
22
+ stop = Hashira::CI::Baseline.new(path).trouble
22
23
  raise(Hashira::Error, stop) if stop
23
- Hashira::CI::Accepted.load(path)
24
+ Hashira::CI::Accepted.build(path)
24
25
  end
25
26
 
26
27
  def update
@@ -32,7 +33,7 @@ class Hashira::CLI::Run
32
33
  def check
33
34
  stop = ratchet.blocker
34
35
  raise(Hashira::Error, stop) if stop
35
- ratchet.check
36
+ ratchet.check(@pipeline.focus)
36
37
  end
37
38
 
38
39
  def guard = gate.check
@@ -55,10 +56,10 @@ class Hashira::CLI::Run
55
56
  told.unparsed(broken.size, broken.first(3).join(", ")) unless broken.empty?
56
57
  end
57
58
 
58
- def ratchet = @ratchet ||= Hashira::CI::Ratchet.new(graph, findings.all, baseline)
59
+ def ratchet = @_ratchet ||= Hashira::CI::Ratchet.new(graph, findings.all, baseline)
59
60
 
60
61
  def baseline
61
- Hashira::CI::Baseline.load(@options.baseline, analyzers: @options.analyzers, targets:)
62
+ Hashira::CI::Baseline.new(@options.baseline, analyzers: @options.analyzers, targets:)
62
63
  end
63
64
 
64
65
  def targets = @pipeline.project.directories
data/lib/hashira/cli.rb CHANGED
@@ -4,40 +4,41 @@ class Hashira::CLI
4
4
  MISUSE = 2
5
5
  BROKEN = 70
6
6
 
7
- def self.run(argv)
8
- dispatch(Options.parse(argv))
7
+ def initialize(argv)
8
+ @argv = argv
9
+ end
10
+
11
+ def status
12
+ dispatch(Options.parse(@argv))
9
13
  rescue Hashira::Error => error
10
14
  failure(error)
11
15
  rescue StandardError => error
12
16
  crash(error)
13
17
  end
14
18
 
15
- def self.dispatch(options) = usage?(options) ? Usage.public_send(options.mode) : new(options).run
19
+ private
20
+
21
+ def dispatch(options) = usage?(options) ? Usage.public_send(options.mode) : timed(options)
22
+
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
31
+ end
16
32
 
17
- def self.usage?(options) = Usage::PAGES.include?(options.mode)
33
+ def elapsed(started) = format("%.1f", Process.clock_gettime(Process::CLOCK_MONOTONIC) - started)
18
34
 
19
- def self.failure(error)
35
+ def failure(error)
20
36
  Kernel.warn("hashira: #{error.message}")
21
37
  MISUSE
22
38
  end
23
39
 
24
- def self.crash(error)
40
+ def crash(error)
25
41
  Hashira::Report::Notices.new.crashed(error)
26
42
  BROKEN
27
43
  end
28
-
29
- def initialize(options)
30
- @started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
31
- @options = options
32
- @notices = Hashira::Report::Notices.new
33
- @pipeline = options.pipeline
34
- end
35
-
36
- def run
37
- Run.new(@pipeline, @options).status.tap { @notices.finished(@pipeline.project.files.size, elapsed) }
38
- end
39
-
40
- private
41
-
42
- def elapsed = format("%.1f", Process.clock_gettime(Process::CLOCK_MONOTONIC) - @started)
43
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