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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +106 -0
- data/README.md +148 -53
- data/exe/hashira +1 -1
- data/lib/hashira/analysis/finding.rb +5 -1
- data/lib/hashira/analysis/syntax.rb +6 -6
- data/lib/hashira/churn.rb +3 -4
- data/lib/hashira/ci/accepted.rb +9 -10
- data/lib/hashira/ci/baseline.rb +39 -12
- data/lib/hashira/ci/comparison.rb +24 -0
- data/lib/hashira/ci/diff.rb +4 -8
- data/lib/hashira/ci/finding_diff_report.rb +10 -0
- data/lib/hashira/ci/gate.rb +10 -2
- data/lib/hashira/ci/improvement.rb +1 -1
- data/lib/hashira/ci/ratchet.rb +41 -10
- data/lib/hashira/ci/ratchet_report.rb +8 -6
- data/lib/hashira/ci/scope.rb +10 -0
- data/lib/hashira/ci/slice.rb +17 -0
- data/lib/hashira/ci/status.rb +7 -0
- data/lib/hashira/ci/sweep.rb +13 -0
- data/lib/hashira/cli/arguments.rb +17 -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 +17 -7
- data/lib/hashira/cli/flag.rb +3 -1
- data/lib/hashira/cli/flags.rb +23 -0
- data/lib/hashira/cli/format.rb +1 -1
- data/lib/hashira/cli/needs.rb +45 -0
- data/lib/hashira/cli/only.rb +16 -0
- data/lib/hashira/cli/options.rb +12 -3
- data/lib/hashira/cli/package_by.rb +1 -1
- data/lib/hashira/cli/run.rb +36 -8
- data/lib/hashira/cli/top.rb +13 -0
- data/lib/hashira/cli/usage.rb +5 -1
- data/lib/hashira/cli.rb +31 -11
- 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 +9 -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/diagram/dot.rb +9 -6
- data/lib/hashira/diagram/mermaid.rb +11 -8
- data/lib/hashira/diagram/source.rb +5 -2
- 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 +26 -24
- data/lib/hashira/project.rb +47 -32
- data/lib/hashira/report/columns.rb +54 -0
- data/lib/hashira/report/complexity_table.rb +12 -16
- data/lib/hashira/report/dependency_map.rb +1 -0
- data/lib/hashira/report/graph_payload.rb +1 -1
- data/lib/hashira/report/hotspot_table.rb +6 -14
- data/lib/hashira/report/json.rb +10 -2
- data/lib/hashira/report/metrics_table.rb +19 -20
- data/lib/hashira/report/notices.rb +37 -0
- data/lib/hashira/report/phrases.rb +25 -18
- data/lib/hashira/report/text.rb +24 -9
- data/lib/hashira/report/view.rb +4 -1
- 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 +26 -0
- data/lib/hashira/version.rb +1 -1
- data/lib/hashira.rb +14 -0
- 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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def dispatch(options) = usage?(options) ? Usage.public_send(options.mode) : timed(options)
|
|
12
22
|
|
|
13
|
-
def
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
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::
|
|
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
|
|
|
@@ -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 :
|
|
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 |
|
|
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) =
|
|
37
|
+
def evidence(from, to) = map.evidence[[from, to]]
|
|
38
38
|
|
|
39
|
-
def usage(package) = incoming(package).to_h { [it,
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
@
|
|
5
|
+
@definitions = definitions
|
|
6
6
|
end
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
def segments = @_segments ||= Hashira::Coupling::NamespacePrefix.infer(@definitions)
|
|
9
9
|
|
|
10
10
|
def strip(path)
|
|
11
|
-
return [] if
|
|
12
|
-
path.drop(
|
|
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
|
-
|
|
14
|
+
found
|
|
19
15
|
end
|
|
20
16
|
|
|
21
17
|
private
|
|
22
18
|
|
|
23
|
-
def
|
|
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
|
|
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.
|
|
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
|
-
|
|
51
|
+
scopes << scope
|
|
52
52
|
yield
|
|
53
|
-
|
|
53
|
+
scopes.pop
|
|
54
54
|
end
|
|
55
55
|
end
|