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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +38 -0
- data/README.md +29 -0
- data/exe/hashira +1 -1
- data/lib/hashira/analysis/finding.rb +1 -1
- data/lib/hashira/analysis/syntax.rb +6 -6
- data/lib/hashira/churn.rb +1 -10
- data/lib/hashira/ci/accepted.rb +9 -7
- data/lib/hashira/ci/baseline.rb +34 -30
- data/lib/hashira/ci/comparison.rb +24 -0
- data/lib/hashira/ci/diff.rb +0 -15
- data/lib/hashira/ci/gate.rb +3 -3
- data/lib/hashira/ci/ratchet.rb +10 -4
- data/lib/hashira/ci/ratchet_report.rb +4 -4
- data/lib/hashira/ci/scope.rb +0 -1
- data/lib/hashira/ci/slice.rb +17 -0
- data/lib/hashira/ci/sweep.rb +13 -0
- data/lib/hashira/cli/arguments.rb +12 -8
- data/lib/hashira/cli/choice.rb +9 -0
- data/lib/hashira/cli/command_line.rb +8 -5
- data/lib/hashira/cli/fail_on.rb +6 -6
- data/lib/hashira/cli/flag.rb +3 -1
- data/lib/hashira/cli/flags.rb +10 -0
- data/lib/hashira/cli/format.rb +1 -1
- data/lib/hashira/cli/needs.rb +13 -2
- data/lib/hashira/cli/only.rb +16 -0
- data/lib/hashira/cli/options.rb +8 -10
- data/lib/hashira/cli/package_by.rb +1 -1
- data/lib/hashira/cli/run.rb +9 -8
- data/lib/hashira/cli.rb +22 -21
- data/lib/hashira/complexity/cognitive_score.rb +44 -33
- data/lib/hashira/complexity/method_finding.rb +7 -5
- data/lib/hashira/complexity/method_score.rb +1 -1
- data/lib/hashira/complexity/scores.rb +11 -9
- data/lib/hashira/coupling/audiences.rb +3 -3
- data/lib/hashira/coupling/catalog.rb +8 -6
- data/lib/hashira/coupling/census.rb +42 -22
- data/lib/hashira/coupling/cycle_search.rb +11 -7
- data/lib/hashira/coupling/definitions.rb +1 -1
- data/lib/hashira/coupling/edge_map.rb +9 -8
- data/lib/hashira/coupling/folding.rb +5 -5
- data/lib/hashira/coupling/graph.rb +16 -9
- data/lib/hashira/coupling/metric.rb +3 -1
- data/lib/hashira/coupling/naming.rb +4 -4
- data/lib/hashira/coupling/references.rb +12 -12
- data/lib/hashira/coupling/report.rb +7 -9
- data/lib/hashira/coupling/roll_call.rb +1 -1
- data/lib/hashira/coupling/roll_call_findings.rb +1 -1
- data/lib/hashira/coupling/roster.rb +37 -12
- data/lib/hashira/coupling/rule.rb +1 -1
- data/lib/hashira/coupling/sdp_violation_findings.rb +3 -1
- data/lib/hashira/duplication/clones.rb +1 -1
- data/lib/hashira/duplication/clusters.rb +10 -7
- data/lib/hashira/duplication/delta.rb +1 -4
- data/lib/hashira/duplication/duplication_finding.rb +4 -2
- data/lib/hashira/duplication/fragment.rb +2 -2
- data/lib/hashira/duplication/harvest.rb +7 -5
- data/lib/hashira/duplication/index.rb +3 -2
- data/lib/hashira/duplication/similarity.rb +2 -2
- data/lib/hashira/duplication/variance.rb +1 -1
- data/lib/hashira/error.rb +0 -3
- data/lib/hashira/focus.rb +29 -0
- data/lib/hashira/git_log.rb +19 -0
- data/lib/hashira/hotspots/file_cost.rb +1 -1
- data/lib/hashira/hotspots/rollup.rb +2 -2
- data/lib/hashira/pipeline.rb +22 -14
- data/lib/hashira/project.rb +35 -34
- data/lib/hashira/report/columns.rb +14 -8
- data/lib/hashira/report/graph_payload.rb +1 -1
- data/lib/hashira/report/hotspot_table.rb +1 -1
- data/lib/hashira/report/json.rb +2 -1
- data/lib/hashira/report/metrics_table.rb +3 -3
- data/lib/hashira/report/phrases.rb +23 -18
- data/lib/hashira/smells/census.rb +3 -4
- data/lib/hashira/smells/check.rb +1 -5
- data/lib/hashira/smells/contexts.rb +4 -4
- data/lib/hashira/smells/control_parameter.rb +1 -1
- data/lib/hashira/smells/data_clump.rb +2 -4
- data/lib/hashira/smells/duplicate_method_call.rb +3 -3
- data/lib/hashira/smells/feature_envy.rb +3 -3
- data/lib/hashira/smells/foreign.rb +14 -14
- data/lib/hashira/smells/instance_variable_assumption.rb +3 -3
- data/lib/hashira/smells/kind.rb +9 -0
- data/lib/hashira/smells/manual_dispatch.rb +1 -1
- data/lib/hashira/smells/module_initialize.rb +0 -2
- data/lib/hashira/smells/nil_check.rb +1 -1
- data/lib/hashira/smells/ownership.rb +19 -7
- data/lib/hashira/smells/param_check.rb +2 -2
- data/lib/hashira/smells/refs.rb +13 -7
- data/lib/hashira/smells/repeated_conditional.rb +1 -3
- data/lib/hashira/smells/report.rb +16 -7
- data/lib/hashira/smells/too_many_instance_variables.rb +3 -3
- data/lib/hashira/smells/visibility.rb +18 -11
- data/lib/hashira/trees.rb +8 -4
- data/lib/hashira/version.rb +1 -1
- data/lib/hashira.rb +8 -0
- metadata +9 -1
data/lib/hashira/cli/options.rb
CHANGED
|
@@ -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.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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.
|
|
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::
|
|
12
|
+
raise(Hashira::CLI::Choice.unknown("--package-by", value, CHOICES))
|
|
13
13
|
end
|
|
14
14
|
end
|
data/lib/hashira/cli/run.rb
CHANGED
|
@@ -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 = @
|
|
18
|
+
def findings = @_findings ||= accepted.screen(@pipeline.findings)
|
|
18
19
|
|
|
19
20
|
def accepted
|
|
20
21
|
path = @options.baseline
|
|
21
|
-
stop = Hashira::CI::Baseline.
|
|
22
|
+
stop = Hashira::CI::Baseline.new(path).trouble
|
|
22
23
|
raise(Hashira::Error, stop) if stop
|
|
23
|
-
Hashira::CI::Accepted.
|
|
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 = @
|
|
59
|
+
def ratchet = @_ratchet ||= Hashira::CI::Ratchet.new(graph, findings.all, baseline)
|
|
59
60
|
|
|
60
61
|
def baseline
|
|
61
|
-
Hashira::CI::Baseline.
|
|
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
|
|
8
|
-
|
|
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
|
-
|
|
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
|
|
33
|
+
def elapsed(started) = format("%.1f", Process.clock_gettime(Process::CLOCK_MONOTONIC) - started)
|
|
18
34
|
|
|
19
|
-
def
|
|
35
|
+
def failure(error)
|
|
20
36
|
Kernel.warn("hashira: #{error.message}")
|
|
21
37
|
MISUSE
|
|
22
38
|
end
|
|
23
39
|
|
|
24
|
-
def
|
|
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::
|
|
8
|
-
Prism::UnlessNode => :on_nester,
|
|
9
|
-
Prism::
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
Prism::CaseMatchNode =>
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
24
|
+
def calls
|
|
25
|
+
walked
|
|
26
|
+
@_calls
|
|
27
|
+
end
|
|
35
28
|
|
|
36
|
-
def
|
|
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
|
-
@
|
|
42
|
+
@_increments << Hashira::Complexity::Increment.new(line: node.location.start_line, cost:, label:)
|
|
45
43
|
end
|
|
46
44
|
|
|
47
45
|
def deeper
|
|
48
|
-
@
|
|
46
|
+
@_nesting += 1
|
|
49
47
|
yield
|
|
50
|
-
@
|
|
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
|
-
@
|
|
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 + @
|
|
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
|
-
|
|
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,
|
|
23
|
+
@score.increments.group_by(&:label).map { |label, increments| lines(label, increments) }
|
|
22
24
|
end
|
|
23
25
|
|
|
24
|
-
def lines(label,
|
|
25
|
-
lines =
|
|
26
|
-
"#{label} +#{
|
|
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 =
|
|
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
|
-
@
|
|
8
|
+
@trees = trees
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
-
def ranked =
|
|
11
|
+
def ranked = scores.sort_by { -it.cognitive }
|
|
12
12
|
|
|
13
|
-
def classes = Hashira::Complexity::Rollup.new(
|
|
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
|
-
|
|
23
|
-
sites(tree).map { |full, node| score(
|
|
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 |
|
|
29
|
-
Hashira::Analysis::Syntax.direct(
|
|
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(
|
|
36
|
+
def score(relative, full, node)
|
|
35
37
|
Hashira::Complexity::MethodScore.new(
|
|
36
|
-
subject: subject(full, node), file:
|
|
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 = @
|
|
14
|
+
def parts = @_parts ||= [common, *slices].compact.select { it.constants.size >= MIN }
|
|
15
15
|
|
|
16
16
|
private
|
|
17
17
|
|
|
18
|
-
def clients = @
|
|
18
|
+
def clients = @_clients ||= @usage.keys.sort
|
|
19
19
|
|
|
20
|
-
def shared = @
|
|
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(&) =
|
|
10
|
+
def each(&) = entries.each(&)
|
|
13
11
|
|
|
14
|
-
def prefix =
|
|
12
|
+
def prefix = naming.segments
|
|
15
13
|
|
|
16
|
-
def 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:
|
|
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
|
-
@
|
|
6
|
-
@
|
|
7
|
-
@
|
|
8
|
-
@roster = tally
|
|
9
|
-
settle
|
|
5
|
+
@project = project
|
|
6
|
+
@trees = trees
|
|
7
|
+
@packaging = packaging
|
|
10
8
|
end
|
|
11
9
|
|
|
12
|
-
def packaging =
|
|
10
|
+
def packaging = placement.mode
|
|
13
11
|
|
|
14
|
-
def types =
|
|
12
|
+
def types = roster.types
|
|
15
13
|
|
|
16
|
-
def prefix =
|
|
14
|
+
def prefix = catalog.prefix
|
|
17
15
|
|
|
18
|
-
def origins =
|
|
16
|
+
def origins = roster.origins
|
|
19
17
|
|
|
20
|
-
def roots =
|
|
18
|
+
def roots = catalog.roots
|
|
21
19
|
|
|
22
|
-
def packages =
|
|
20
|
+
def packages = roster.packages | placement.baseline
|
|
23
21
|
|
|
24
|
-
def folds =
|
|
22
|
+
def folds = folding.disclosed
|
|
25
23
|
|
|
26
|
-
def 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(
|
|
35
|
+
def charge(file, nesting) = translate(placement.charge(file, nesting))
|
|
38
36
|
|
|
39
37
|
private
|
|
40
38
|
|
|
41
|
-
def
|
|
39
|
+
def catalog = @_catalog ||= Hashira::Coupling::Catalog.new(Hashira::Coupling::Definitions.new(@project, @trees))
|
|
42
40
|
|
|
43
|
-
def
|
|
44
|
-
|
|
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
|
|
48
|
-
|
|
49
|
-
|
|
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) =
|
|
72
|
+
def translate(package) = folding.map.fetch(package, package)
|
|
53
73
|
|
|
54
|
-
def scope = Hashira::Coupling::Scope.new(
|
|
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
|
-
|
|
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 @
|
|
27
|
-
@
|
|
28
|
-
@
|
|
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(@
|
|
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
|
-
@
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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 = @
|
|
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 = @
|
|
22
|
+
def links = @_links ||= named.merge(parents)
|
|
23
23
|
|
|
24
24
|
def parents
|
|
25
|
-
@
|
|
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 = @
|
|
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 = @
|
|
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
|
|