hashira 0.4.0 → 0.5.1
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 +86 -0
- data/README.md +90 -30
- data/lib/hashira/analysis/finding.rb +2 -2
- data/lib/hashira/analysis/syntax.rb +6 -2
- data/lib/hashira/churn.rb +1 -1
- data/lib/hashira/ci/ratchet.rb +5 -9
- data/lib/hashira/cli/command_line.rb +10 -49
- data/lib/hashira/cli/fail_on.rb +9 -5
- data/lib/hashira/cli/flag.rb +29 -0
- data/lib/hashira/cli/flags.rb +50 -0
- data/lib/hashira/cli/format.rb +13 -0
- data/lib/hashira/cli/options.rb +2 -0
- data/lib/hashira/cli/run.rb +8 -4
- data/lib/hashira/cli/usage.rb +20 -21
- data/lib/hashira/cli.rb +1 -1
- data/lib/hashira/complexity/boolean_run.rb +5 -3
- data/lib/hashira/complexity/cognitive_score.rb +21 -14
- data/lib/hashira/complexity/if_chain.rb +14 -14
- data/lib/hashira/complexity/method_finding.rb +4 -19
- data/lib/hashira/complexity/rescue_scan.rb +9 -9
- data/lib/hashira/complexity/{analyzer.rb → scores.rb} +6 -5
- data/lib/hashira/coupling/audiences.rb +49 -0
- data/lib/hashira/{analysis → coupling}/catalog.rb +3 -3
- data/lib/hashira/{analysis → coupling}/census.rb +17 -8
- data/lib/hashira/{analysis → coupling}/constant_registry.rb +11 -6
- data/lib/hashira/coupling/cycle_findings.rb +31 -0
- data/lib/hashira/{analysis → coupling}/cycle_search.rb +1 -1
- data/lib/hashira/{analysis → coupling}/cycles.rb +2 -2
- data/lib/hashira/{analysis → coupling}/definition.rb +6 -2
- data/lib/hashira/{analysis → coupling}/definitions.rb +5 -2
- data/lib/hashira/{analysis → coupling}/edge.rb +1 -1
- data/lib/hashira/{analysis → coupling}/edge_map.rb +10 -10
- data/lib/hashira/{analysis → coupling}/folder_placement.rb +2 -2
- data/lib/hashira/{analysis → coupling}/folding.rb +4 -2
- data/lib/hashira/{analysis → coupling}/graph.rb +18 -8
- data/lib/hashira/{analysis → coupling}/metric.rb +1 -1
- data/lib/hashira/coupling/mixed_audience_findings.rb +32 -0
- data/lib/hashira/{analysis → coupling}/namespace_placement.rb +3 -3
- data/lib/hashira/{analysis → coupling}/namespace_prefix.rb +1 -1
- data/lib/hashira/{analysis → coupling}/naming.rb +2 -2
- data/lib/hashira/{analysis → coupling}/no_folding.rb +1 -1
- data/lib/hashira/{analysis → coupling}/placement.rb +3 -3
- data/lib/hashira/coupling/references.rb +55 -0
- data/lib/hashira/coupling/report.rb +28 -0
- data/lib/hashira/coupling/roll_call.rb +45 -0
- data/lib/hashira/coupling/roll_call_findings.rb +30 -0
- data/lib/hashira/{analysis → coupling}/roster.rb +6 -2
- data/lib/hashira/{analysis → coupling}/rule.rb +1 -1
- data/lib/hashira/{analysis/resolver.rb → coupling/scope.rb} +7 -3
- data/lib/hashira/{analysis → coupling}/sdp_check.rb +1 -1
- data/lib/hashira/coupling/sdp_violation_findings.rb +25 -0
- data/lib/hashira/coupling/wide_edge_findings.rb +25 -0
- data/lib/hashira/coupling/words.rb +29 -0
- data/lib/hashira/diagram/{renderer.rb → source.rb} +2 -2
- data/lib/hashira/duplication/{analyzer.rb → clones.rb} +3 -3
- data/lib/hashira/duplication/{clusterer.rb → clusters.rb} +3 -3
- data/lib/hashira/duplication/delta.rb +6 -14
- data/lib/hashira/duplication/duplication_finding.rb +8 -10
- data/lib/hashira/duplication/{extractor.rb → harvest.rb} +1 -1
- data/lib/hashira/duplication/sequence.rb +3 -1
- data/lib/hashira/duplication/similarity.rb +5 -1
- data/lib/hashira/duplication/variance.rb +3 -3
- data/lib/hashira/hotspots/rollup.rb +1 -1
- data/lib/hashira/pipeline.rb +18 -10
- data/lib/hashira/report/complexity_table.rb +1 -1
- data/lib/hashira/report/finding_lines.rb +1 -1
- data/lib/hashira/report/json.rb +5 -3
- data/lib/hashira/report/phrases.rb +98 -0
- data/lib/hashira/report/smell_phrases.rb +68 -0
- data/lib/hashira/smells/census.rb +32 -0
- data/lib/hashira/smells/check.rb +34 -0
- data/lib/hashira/smells/conditions.rb +44 -0
- data/lib/hashira/smells/contexts.rb +64 -0
- data/lib/hashira/smells/control_parameter.rb +22 -0
- data/lib/hashira/smells/data_clump.rb +32 -0
- data/lib/hashira/smells/duplicate_method_call.rb +54 -0
- data/lib/hashira/smells/feature_envy.rb +17 -0
- data/lib/hashira/smells/instance_variable_assumption.rb +46 -0
- data/lib/hashira/smells/manual_dispatch.rb +16 -0
- data/lib/hashira/smells/module_initialize.rb +11 -0
- data/lib/hashira/smells/nil_check.rb +32 -0
- data/lib/hashira/smells/param_check.rb +51 -0
- data/lib/hashira/smells/refs.rb +53 -0
- data/lib/hashira/smells/repeated_conditional.rb +31 -0
- data/lib/hashira/smells/report.rb +34 -0
- data/lib/hashira/smells/scope.rb +29 -0
- data/lib/hashira/smells/too_many_instance_variables.rb +26 -0
- data/lib/hashira/smells/utility_function.rb +13 -0
- data/lib/hashira/smells/visibility.rb +64 -0
- data/lib/hashira/version.rb +1 -1
- data/lib/hashira.rb +68 -30
- metadata +63 -31
- data/lib/hashira/analysis/cycle_findings.rb +0 -38
- data/lib/hashira/analysis/references.rb +0 -40
- data/lib/hashira/analysis/sdp_violation_findings.rb +0 -29
data/lib/hashira/cli/usage.rb
CHANGED
|
@@ -1,43 +1,42 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "flags"
|
|
4
|
+
|
|
3
5
|
module Hashira::CLI::Usage
|
|
4
|
-
|
|
6
|
+
PAGES = %i[help version].freeze
|
|
7
|
+
|
|
8
|
+
HEADER = <<~TEXT
|
|
5
9
|
Usage: hashira [DIRECTORY ...] [options]
|
|
6
10
|
|
|
7
|
-
Coupling, cognitive-complexity, and
|
|
8
|
-
Prism, rolled up per file as a ranked list of hotspots.
|
|
9
|
-
directory, auto-detects lib/<gem>.
|
|
11
|
+
Coupling, cognitive-complexity, duplication, and code-smell metrics
|
|
12
|
+
for Ruby, via Prism, rolled up per file as a ranked list of hotspots.
|
|
13
|
+
With no directory, auto-detects lib/<gem>.
|
|
10
14
|
|
|
11
15
|
Options:
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
kinds: cycles, sdp, complexity, duplication
|
|
16
|
-
--skip ANALYZERS drop an analyzer; comma-separated: coupling, complexity, duplication
|
|
17
|
-
--package-by WHAT group coupling by: auto, folder, or namespace
|
|
18
|
-
(top-level constant). Default auto: namespace for
|
|
19
|
-
Rails apps (config/application.rb in or beside the
|
|
20
|
-
analyzed directory), folder otherwise
|
|
21
|
-
--ratchet fail when edges or findings appear that the
|
|
22
|
-
baseline lacks
|
|
23
|
-
--update-baseline record the current edges and findings
|
|
24
|
-
--baseline PATH baseline file (default: hashira_baseline.json)
|
|
25
|
-
-h, --help print this help
|
|
26
|
-
--version print the version
|
|
16
|
+
TEXT
|
|
17
|
+
|
|
18
|
+
FOOTER = <<~TEXT
|
|
27
19
|
|
|
28
20
|
Findings accepted by design can be recorded in the baseline as
|
|
29
21
|
"accepted": [{"kind": "...", "package": "...", "reason": "..."}]
|
|
30
22
|
— they leave reports and gates, keeping a one-line reminder each.
|
|
31
23
|
Clones are named by "digest" instead of "package", so an acceptance
|
|
32
24
|
survives the lines above it moving. Read digests from --json.
|
|
33
|
-
|
|
25
|
+
TEXT
|
|
34
26
|
|
|
35
27
|
module_function
|
|
36
28
|
|
|
37
|
-
def help = emit(
|
|
29
|
+
def help = emit(HEADER + options + FOOTER)
|
|
38
30
|
|
|
39
31
|
def version = emit("hashira #{Hashira::VERSION}")
|
|
40
32
|
|
|
33
|
+
def options = Hashira::CLI::FLAGS.map { rows(it) }.join
|
|
34
|
+
|
|
35
|
+
def rows(flag)
|
|
36
|
+
first, *rest = flag.text
|
|
37
|
+
[" #{flag.label.ljust(21)}#{first}\n", *rest.map { "#{" " * 23}#{it}\n" }].join
|
|
38
|
+
end
|
|
39
|
+
|
|
41
40
|
def emit(output)
|
|
42
41
|
puts(output)
|
|
43
42
|
0
|
data/lib/hashira/cli.rb
CHANGED
|
@@ -5,14 +5,16 @@ class Hashira::Complexity::BooleanRun
|
|
|
5
5
|
@scorer = scorer
|
|
6
6
|
end
|
|
7
7
|
|
|
8
|
-
def apply(node
|
|
8
|
+
def apply(node)
|
|
9
9
|
@scorer.add(node, 1, "boolean")
|
|
10
|
-
operands(node).each { @scorer.visit(it
|
|
10
|
+
operands(node).each { @scorer.visit(it) }
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
private
|
|
14
14
|
|
|
15
15
|
def operands(node)
|
|
16
|
-
node.compact_child_nodes.flat_map {
|
|
16
|
+
node.compact_child_nodes.flat_map { kin?(it, node) ? operands(it) : [it] }
|
|
17
17
|
end
|
|
18
|
+
|
|
19
|
+
def kin?(child, node) = child.instance_of?(node.class)
|
|
18
20
|
end
|
|
@@ -27,41 +27,48 @@ class Hashira::Complexity::CognitiveScore
|
|
|
27
27
|
def initialize(def_node)
|
|
28
28
|
@increments = []
|
|
29
29
|
@calls = 0
|
|
30
|
-
|
|
30
|
+
@nesting = 0
|
|
31
|
+
visit(def_node.body)
|
|
31
32
|
end
|
|
32
33
|
|
|
33
|
-
attr_reader :increments, :calls
|
|
34
|
+
attr_reader :increments, :calls, :nesting
|
|
34
35
|
|
|
35
36
|
def total = @increments.sum(&:cost)
|
|
36
37
|
|
|
37
|
-
def visit(node
|
|
38
|
+
def visit(node)
|
|
38
39
|
return unless node
|
|
39
|
-
__send__(HANDLERS.fetch(node.class, :descend), node
|
|
40
|
+
__send__(HANDLERS.fetch(node.class, :descend), node)
|
|
40
41
|
end
|
|
41
42
|
|
|
42
43
|
def add(node, cost, label)
|
|
43
44
|
@increments << Hashira::Complexity::Increment.new(line: node.location.start_line, cost:, label:)
|
|
44
45
|
end
|
|
45
46
|
|
|
47
|
+
def deeper
|
|
48
|
+
@nesting += 1
|
|
49
|
+
yield
|
|
50
|
+
@nesting -= 1
|
|
51
|
+
end
|
|
52
|
+
|
|
46
53
|
private
|
|
47
54
|
|
|
48
|
-
def descend(node
|
|
55
|
+
def descend(node) = node.compact_child_nodes.each { visit(it) }
|
|
49
56
|
|
|
50
|
-
def on_call(node
|
|
57
|
+
def on_call(node)
|
|
51
58
|
@calls += 1
|
|
52
|
-
descend(node
|
|
59
|
+
descend(node)
|
|
53
60
|
end
|
|
54
61
|
|
|
55
|
-
def on_block(node
|
|
62
|
+
def on_block(node) = deeper { descend(node) }
|
|
56
63
|
|
|
57
|
-
def on_nester(node
|
|
58
|
-
add(node, 1 + nesting, LABELS.fetch(node.class))
|
|
59
|
-
|
|
64
|
+
def on_nester(node)
|
|
65
|
+
add(node, 1 + @nesting, LABELS.fetch(node.class))
|
|
66
|
+
deeper { descend(node) }
|
|
60
67
|
end
|
|
61
68
|
|
|
62
|
-
def on_if(node
|
|
69
|
+
def on_if(node) = Hashira::Complexity::IfChain.new(self).apply(node)
|
|
63
70
|
|
|
64
|
-
def on_begin(node
|
|
71
|
+
def on_begin(node) = Hashira::Complexity::RescueScan.new(self).apply(node)
|
|
65
72
|
|
|
66
|
-
def on_boolean(node
|
|
73
|
+
def on_boolean(node) = Hashira::Complexity::BooleanRun.new(self).apply(node)
|
|
67
74
|
end
|
|
@@ -7,34 +7,34 @@ class Hashira::Complexity::IfChain
|
|
|
7
7
|
@scorer = scorer
|
|
8
8
|
end
|
|
9
9
|
|
|
10
|
-
def apply(node
|
|
11
|
-
return ternary(node
|
|
12
|
-
branch(node, 1 + nesting,
|
|
10
|
+
def apply(node)
|
|
11
|
+
return ternary(node) unless node.if_keyword
|
|
12
|
+
branch(node, 1 + @scorer.nesting, "if")
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
private
|
|
16
16
|
|
|
17
|
-
def branch(node, cost,
|
|
17
|
+
def branch(node, cost, label)
|
|
18
18
|
@scorer.add(node, cost, label)
|
|
19
|
-
@scorer.visit(node.predicate
|
|
20
|
-
@scorer.visit(node.statements
|
|
21
|
-
tail(node.subsequent
|
|
19
|
+
@scorer.visit(node.predicate)
|
|
20
|
+
@scorer.deeper { @scorer.visit(node.statements) }
|
|
21
|
+
tail(node.subsequent)
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
-
def tail(node
|
|
24
|
+
def tail(node)
|
|
25
25
|
case node
|
|
26
|
-
when Prism::IfNode then branch(node, 1,
|
|
27
|
-
when Prism::ElseNode then otherwise(node
|
|
26
|
+
when Prism::IfNode then branch(node, 1, "elsif")
|
|
27
|
+
when Prism::ElseNode then otherwise(node)
|
|
28
28
|
end
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
-
def otherwise(node
|
|
31
|
+
def otherwise(node)
|
|
32
32
|
@scorer.add(node, 1, "else")
|
|
33
|
-
@scorer.visit(node.statements
|
|
33
|
+
@scorer.deeper { @scorer.visit(node.statements) }
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
-
def ternary(node
|
|
36
|
+
def ternary(node)
|
|
37
37
|
@scorer.add(node, 1, "ternary")
|
|
38
|
-
node.compact_child_nodes.each { @scorer.visit(it
|
|
38
|
+
node.compact_child_nodes.each { @scorer.visit(it) }
|
|
39
39
|
end
|
|
40
40
|
end
|
|
@@ -1,33 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class Hashira::Complexity::MethodFinding
|
|
4
|
-
|
|
5
|
-
"if" => "flatten the branching — guard clauses, early returns, or polymorphism.",
|
|
6
|
-
"elsif" => "replace the elsif ladder with a lookup or polymorphic dispatch.",
|
|
7
|
-
"else" => "flatten the branching — guard clauses, early returns, or polymorphism.",
|
|
8
|
-
"case" => "a case this size often wants polymorphism or a dispatch table.",
|
|
9
|
-
"boolean" => "name the compound condition in a predicate method.",
|
|
10
|
-
"rescue" => "narrow the rescue, or lift error handling to the caller.",
|
|
11
|
-
"while" => "extract the loop body into its own method.",
|
|
12
|
-
"until" => "extract the loop body into its own method.",
|
|
13
|
-
"for" => "extract the loop body into its own method.",
|
|
14
|
-
"unless" => "invert to a guard clause or a named predicate.",
|
|
15
|
-
"ternary" => "extract the nested ternary into a named method."
|
|
16
|
-
}.freeze
|
|
4
|
+
KIND = "complexity"
|
|
17
5
|
|
|
18
6
|
def initialize(score)
|
|
19
7
|
@score = score
|
|
20
8
|
end
|
|
21
9
|
|
|
22
10
|
def to_finding
|
|
23
|
-
Hashira::Analysis::Finding.new(kind:
|
|
11
|
+
Hashira::Analysis::Finding.new(kind: KIND, package: @score.subject, detail:, evidence:)
|
|
24
12
|
end
|
|
25
13
|
|
|
26
14
|
private
|
|
27
15
|
|
|
28
|
-
def
|
|
29
|
-
|
|
30
|
-
"(#{@score.file}:#{@score.line}). #{advice}"
|
|
16
|
+
def detail
|
|
17
|
+
{ cognitive: @score.cognitive, calls: @score.calls, site: "#{@score.file}:#{@score.line}", dominant: }
|
|
31
18
|
end
|
|
32
19
|
|
|
33
20
|
def evidence
|
|
@@ -39,8 +26,6 @@ class Hashira::Complexity::MethodFinding
|
|
|
39
26
|
"#{label} +#{incs.sum(&:cost)} (line#{"s" if lines.size > 1} #{lines.join(", ")})"
|
|
40
27
|
end
|
|
41
28
|
|
|
42
|
-
def advice = ADVICE.fetch(dominant)
|
|
43
|
-
|
|
44
29
|
def dominant
|
|
45
30
|
@score.increments.group_by(&:label).transform_values { it.sum(&:cost) }.max_by(&:last).first
|
|
46
31
|
end
|
|
@@ -5,19 +5,19 @@ class Hashira::Complexity::RescueScan
|
|
|
5
5
|
@scorer = scorer
|
|
6
6
|
end
|
|
7
7
|
|
|
8
|
-
def apply(node
|
|
9
|
-
@scorer.visit(node.statements
|
|
10
|
-
clauses(node.rescue_clause
|
|
11
|
-
@scorer.visit(node.else_clause
|
|
12
|
-
@scorer.visit(node.ensure_clause
|
|
8
|
+
def apply(node)
|
|
9
|
+
@scorer.visit(node.statements)
|
|
10
|
+
clauses(node.rescue_clause)
|
|
11
|
+
@scorer.visit(node.else_clause)
|
|
12
|
+
@scorer.visit(node.ensure_clause)
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
private
|
|
16
16
|
|
|
17
|
-
def clauses(node
|
|
17
|
+
def clauses(node)
|
|
18
18
|
return unless node
|
|
19
|
-
@scorer.add(node, 1 + nesting, "rescue")
|
|
20
|
-
@scorer.visit(node.statements
|
|
21
|
-
clauses(node.subsequent
|
|
19
|
+
@scorer.add(node, 1 + @scorer.nesting, "rescue")
|
|
20
|
+
@scorer.deeper { @scorer.visit(node.statements) }
|
|
21
|
+
clauses(node.subsequent)
|
|
22
22
|
end
|
|
23
23
|
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
class Hashira::Complexity::
|
|
3
|
+
class Hashira::Complexity::Scores
|
|
4
4
|
THRESHOLD = 10
|
|
5
5
|
|
|
6
6
|
def initialize(project, trees)
|
|
@@ -8,7 +8,7 @@ class Hashira::Complexity::Analyzer
|
|
|
8
8
|
@scores = trees.flat_map { |path, tree| harvest(path, tree) }
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
-
def
|
|
11
|
+
def ranked = @scores.sort_by { -it.cognitive }
|
|
12
12
|
|
|
13
13
|
def classes = Hashira::Complexity::Rollup.new(@scores).classes.sort_by { -it.cognitive }
|
|
14
14
|
|
|
@@ -16,7 +16,7 @@ class Hashira::Complexity::Analyzer
|
|
|
16
16
|
|
|
17
17
|
private
|
|
18
18
|
|
|
19
|
-
def flagged =
|
|
19
|
+
def flagged = ranked.select { it.cognitive >= THRESHOLD }
|
|
20
20
|
|
|
21
21
|
def harvest(path, tree)
|
|
22
22
|
rel = @project.relative(path)
|
|
@@ -32,12 +32,13 @@ class Hashira::Complexity::Analyzer
|
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
def score(rel, full, node)
|
|
35
|
-
score = Hashira::Complexity::CognitiveScore.new(node)
|
|
36
35
|
Hashira::Complexity::MethodScore.new(
|
|
37
36
|
subject: subject(full, node), file: rel, line: node.location.start_line,
|
|
38
|
-
|
|
37
|
+
**tallies(Hashira::Complexity::CognitiveScore.new(node))
|
|
39
38
|
)
|
|
40
39
|
end
|
|
41
40
|
|
|
41
|
+
def tallies(score) = { cognitive: score.total, calls: score.calls, increments: score.increments }
|
|
42
|
+
|
|
42
43
|
def subject(full, node) = "#{full.join("::")}#{node.receiver ? "." : "#"}#{node.name}"
|
|
43
44
|
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Hashira::Coupling::Audiences
|
|
4
|
+
MIN = 2
|
|
5
|
+
|
|
6
|
+
Part = Data.define(:users, :constants, :shared)
|
|
7
|
+
|
|
8
|
+
def initialize(usage)
|
|
9
|
+
@usage = usage
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def split? = parts.size >= 2
|
|
13
|
+
|
|
14
|
+
def parts = @parts ||= [common, *slices].compact.select { it.constants.size >= MIN }
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def clients = @clients ||= @usage.keys.sort
|
|
19
|
+
|
|
20
|
+
def shared = @shared ||= tally.select { |_constant, users| users.size * 2 > clients.size }.keys.to_set
|
|
21
|
+
|
|
22
|
+
def tally
|
|
23
|
+
clients.each_with_object(Hash.new { |hash, key| hash[key] = [] }) do |client, seen|
|
|
24
|
+
@usage[client].each { seen[it] << client }
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def common
|
|
29
|
+
return if shared.empty?
|
|
30
|
+
Part.new(users: clients.select { @usage[it].intersect?(shared) }, constants: shared.sort, shared: true)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def slices
|
|
34
|
+
groups.sort_by(&:first).map { |members| Part.new(users: members, constants: pooled(members).sort, shared: false) }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def pooled(members) = members.reduce(Set.new) { |pool, client| pool | residue(client) }
|
|
38
|
+
|
|
39
|
+
def residue(client) = @usage[client] - shared
|
|
40
|
+
|
|
41
|
+
def groups
|
|
42
|
+
clients.reduce([]) do |formed, client|
|
|
43
|
+
linked, apart = formed.partition { |members| joined?(members, client) }
|
|
44
|
+
apart + [linked.flatten + [client]]
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def joined?(members, client) = members.any? { residue(it).intersect?(residue(client)) }
|
|
49
|
+
end
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
class Hashira::
|
|
3
|
+
class Hashira::Coupling::Catalog
|
|
4
4
|
include Enumerable
|
|
5
5
|
|
|
6
6
|
def initialize(definitions)
|
|
7
7
|
@definitions = definitions
|
|
8
|
-
@naming = Hashira::
|
|
8
|
+
@naming = Hashira::Coupling::Naming.new(definitions)
|
|
9
9
|
@entries = definitions.map { |node, full, folder| entry(node, full, folder) }
|
|
10
10
|
end
|
|
11
11
|
|
|
@@ -22,6 +22,6 @@ class Hashira::Analysis::Catalog
|
|
|
22
22
|
private
|
|
23
23
|
|
|
24
24
|
def entry(node, full, folder)
|
|
25
|
-
Hashira::
|
|
25
|
+
Hashira::Coupling::Definition.new(node:, path: @naming.strip(full), folder:)
|
|
26
26
|
end
|
|
27
27
|
end
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
class Hashira::
|
|
3
|
+
class Hashira::Coupling::Census
|
|
4
4
|
def initialize(project, trees, packaging: :folder)
|
|
5
|
-
@catalog = Hashira::
|
|
6
|
-
@placement = Hashira::
|
|
7
|
-
@folding = Hashira::
|
|
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
8
|
@roster = tally
|
|
9
9
|
settle
|
|
10
10
|
end
|
|
@@ -23,16 +23,25 @@ class Hashira::Analysis::Census
|
|
|
23
23
|
|
|
24
24
|
def folds = @folding.disclosed
|
|
25
25
|
|
|
26
|
-
def
|
|
26
|
+
def strip(segments) = @catalog.strip(segments)
|
|
27
27
|
|
|
28
|
-
def
|
|
28
|
+
def holder(segments)
|
|
29
|
+
path = strip(segments)
|
|
30
|
+
(path.length.downto(1).map { path.first(it) }.find { type?(it) } || path).join("::")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def resolve(segments, nesting = []) = scope.resolve(segments, nesting)
|
|
34
|
+
|
|
35
|
+
def pinpoint(segments) = scope.pinpoint(segments)
|
|
29
36
|
|
|
30
37
|
def charge(file, nesting) = translate(@placement.charge(file, nesting))
|
|
31
38
|
|
|
32
39
|
private
|
|
33
40
|
|
|
41
|
+
def type?(segments) = @roster.type?(segments)
|
|
42
|
+
|
|
34
43
|
def tally
|
|
35
|
-
Hashira::
|
|
44
|
+
Hashira::Coupling::Roster.new(@placement.placed.map { |definition, package| [definition, translate(package)] })
|
|
36
45
|
end
|
|
37
46
|
|
|
38
47
|
def settle
|
|
@@ -42,5 +51,5 @@ class Hashira::Analysis::Census
|
|
|
42
51
|
|
|
43
52
|
def translate(package) = @folding.map.fetch(package, package)
|
|
44
53
|
|
|
45
|
-
def
|
|
54
|
+
def scope = Hashira::Coupling::Scope.new(@roster.registry, @catalog, @placement)
|
|
46
55
|
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
class Hashira::
|
|
3
|
+
class Hashira::Coupling::ConstantRegistry
|
|
4
4
|
AMBIGUOUS = Object.new.freeze
|
|
5
5
|
|
|
6
6
|
def initialize
|
|
@@ -16,10 +16,9 @@ class Hashira::Analysis::ConstantRegistry
|
|
|
16
16
|
(1...path.length).each { claim(@shorthand, path.drop(it), package) }
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
def package(path)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
end
|
|
19
|
+
def package(path) = settle(anchored(path) || enclosing(path))
|
|
20
|
+
|
|
21
|
+
def rooted(path) = settle(@origins[path.join("::")] || enclosing(path))
|
|
23
22
|
|
|
24
23
|
def exact(path) = @origins[path.join("::")]
|
|
25
24
|
|
|
@@ -32,12 +31,18 @@ class Hashira::Analysis::ConstantRegistry
|
|
|
32
31
|
claims[key] = claims.fetch(key, package) == package ? package : AMBIGUOUS
|
|
33
32
|
end
|
|
34
33
|
|
|
34
|
+
def settle(found) = (found unless found == AMBIGUOUS)
|
|
35
|
+
|
|
35
36
|
def anchored(path)
|
|
36
37
|
key = path.join("::")
|
|
37
38
|
@origins[key] || @shorthand[key]
|
|
38
39
|
end
|
|
39
40
|
|
|
40
41
|
def enclosing(path)
|
|
41
|
-
(path
|
|
42
|
+
prefixes(path).filter_map { @origins[it] }.first
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def prefixes(path)
|
|
46
|
+
(path.length - 1).downto(1).map { path.first(it).join("::") }
|
|
42
47
|
end
|
|
43
48
|
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "rule"
|
|
4
|
+
|
|
5
|
+
class Hashira::Coupling::CycleFindings < Hashira::Coupling::Rule
|
|
6
|
+
KIND = "cycle"
|
|
7
|
+
|
|
8
|
+
def list
|
|
9
|
+
loops.map { entry(it) }
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def loops
|
|
15
|
+
cycles = graph.cycles
|
|
16
|
+
graph.packages.select { cycles.through?(it) }.sort.map { cycles.path(it) }.uniq { it[..-2].sort }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def entry(path)
|
|
20
|
+
finding(package: path.first, cycle: path, evidence: evidence(path), detail: detail(graph.cycles.weakest(path)))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def detail(weak)
|
|
24
|
+
from, to = weak
|
|
25
|
+
{ weak:, weight: graph.weight(from, to) }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def evidence(path)
|
|
29
|
+
path.each_cons(2).flat_map { |from, to| graph.evidence(from, to).to_a.first(2) }
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
class Hashira::
|
|
3
|
+
class Hashira::Coupling::Cycles
|
|
4
4
|
def initialize(dependencies, graph)
|
|
5
5
|
@dependencies = dependencies
|
|
6
6
|
@graph = graph
|
|
@@ -8,7 +8,7 @@ class Hashira::Analysis::Cycles
|
|
|
8
8
|
|
|
9
9
|
def through?(package) = !!path(package)
|
|
10
10
|
|
|
11
|
-
def path(package) = Hashira::
|
|
11
|
+
def path(package) = Hashira::Coupling::CycleSearch.new(@dependencies, package).path
|
|
12
12
|
|
|
13
13
|
def weakest(trail) = trail.each_cons(2).min_by { |from, to| @graph.weight(from, to) }
|
|
14
14
|
end
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
require "prism"
|
|
4
4
|
|
|
5
5
|
module Hashira
|
|
6
|
-
module
|
|
6
|
+
module Coupling
|
|
7
7
|
Definition =
|
|
8
8
|
Data.define(:node, :path, :folder) do
|
|
9
9
|
def name = path.first
|
|
@@ -16,7 +16,11 @@ module Hashira
|
|
|
16
16
|
|
|
17
17
|
def superclass = node.superclass
|
|
18
18
|
|
|
19
|
-
def
|
|
19
|
+
def module? = node.is_a?(Prism::ModuleNode)
|
|
20
|
+
|
|
21
|
+
def type? = klass? || module?
|
|
22
|
+
|
|
23
|
+
def counted? = klass? || (module? && !Hashira::Analysis::Syntax.direct(node).empty?)
|
|
20
24
|
end
|
|
21
25
|
end
|
|
22
26
|
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
class Hashira::
|
|
3
|
+
class Hashira::Coupling::Definitions
|
|
4
4
|
include Enumerable
|
|
5
5
|
|
|
6
6
|
def initialize(project, trees)
|
|
@@ -26,6 +26,9 @@ class Hashira::Analysis::Definitions
|
|
|
26
26
|
|
|
27
27
|
def scan(file, tree)
|
|
28
28
|
package = @project.package(file)
|
|
29
|
-
Hashira::Analysis::TypeWalk.each(tree, roots: roots)
|
|
29
|
+
Hashira::Analysis::TypeWalk.each(tree, roots: roots) do |node, full|
|
|
30
|
+
yield(node, full, package)
|
|
31
|
+
Hashira::Analysis::Syntax.constants(node).each { yield(it, full + [it.name.to_s], package) }
|
|
32
|
+
end
|
|
30
33
|
end
|
|
31
34
|
end
|
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
class Hashira::
|
|
4
|
-
def initialize(
|
|
5
|
-
@project = project
|
|
3
|
+
class Hashira::Coupling::EdgeMap
|
|
4
|
+
def initialize(census)
|
|
6
5
|
@census = census
|
|
7
6
|
@dependencies = sets
|
|
8
7
|
@evidence = sets
|
|
8
|
+
@usage = sets
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
-
attr_reader :dependencies, :evidence
|
|
11
|
+
attr_reader :dependencies, :evidence, :usage
|
|
12
12
|
|
|
13
|
-
def record(file, tree)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
note(@census.charge(file, home), locate(segments, nesting), "#{source}:#{line}: #{segments.join("::")}")
|
|
13
|
+
def record(source, file, tree)
|
|
14
|
+
Hashira::Coupling::References.sightings(tree, @census.roots).each do |segments, line, nesting, home|
|
|
15
|
+
note(@census.charge(file, home), locate(segments, nesting), segments, "#{source}:#{line}")
|
|
17
16
|
end
|
|
18
17
|
end
|
|
19
18
|
|
|
@@ -25,9 +24,10 @@ class Hashira::Analysis::EdgeMap
|
|
|
25
24
|
nesting ? @census.resolve(segments, nesting) : @census.pinpoint(segments)
|
|
26
25
|
end
|
|
27
26
|
|
|
28
|
-
def note(from, to,
|
|
27
|
+
def note(from, to, segments, site)
|
|
29
28
|
return unless to && to != from
|
|
30
29
|
@dependencies[from] << to
|
|
31
|
-
@evidence[[from, to]] <<
|
|
30
|
+
@evidence[[from, to]] << "#{site}: #{segments.join("::")}"
|
|
31
|
+
@usage[[from, to]] << @census.holder(segments)
|
|
32
32
|
end
|
|
33
33
|
end
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "placement"
|
|
4
4
|
|
|
5
|
-
class Hashira::
|
|
5
|
+
class Hashira::Coupling::FolderPlacement < Hashira::Coupling::Placement
|
|
6
6
|
def mode = :folder
|
|
7
7
|
|
|
8
8
|
def placed = catalog.map { [it, it.folder] }
|
|
@@ -13,5 +13,5 @@ class Hashira::Analysis::FolderPlacement < Hashira::Analysis::Placement
|
|
|
13
13
|
|
|
14
14
|
def skip?(_segments) = false
|
|
15
15
|
|
|
16
|
-
def folding(_census) = Hashira::
|
|
16
|
+
def folding(_census) = Hashira::Coupling::NoFolding
|
|
17
17
|
end
|