hashira 0.3.0 → 0.5.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 +155 -0
- data/README.md +127 -25
- data/lib/hashira/analysis/finding.rb +7 -6
- data/lib/hashira/analysis/node_walk.rb +3 -3
- data/lib/hashira/analysis/syntax.rb +21 -6
- data/lib/hashira/analysis/type_walk.rb +7 -9
- data/lib/hashira/churn.rb +11 -13
- data/lib/hashira/ci/accepted.rb +27 -31
- data/lib/hashira/ci/baseline.rb +22 -24
- data/lib/hashira/ci/diff.rb +8 -7
- data/lib/hashira/ci/edge_diff_report.rb +13 -17
- data/lib/hashira/ci/finding_diff_report.rb +14 -18
- data/lib/hashira/ci/gate.rb +19 -23
- data/lib/hashira/ci/improvement.rb +9 -14
- data/lib/hashira/ci/ratchet.rb +32 -27
- data/lib/hashira/ci/ratchet_report.rb +26 -32
- data/lib/hashira/cli/arguments.rb +23 -0
- data/lib/hashira/cli/command_line.rb +55 -71
- data/lib/hashira/cli/fail_on.rb +24 -19
- data/lib/hashira/cli/options.rb +9 -4
- data/lib/hashira/cli/package_by.rb +14 -0
- data/lib/hashira/cli/run.rb +33 -27
- data/lib/hashira/cli/skip.rb +14 -20
- data/lib/hashira/cli/usage.rb +44 -41
- data/lib/hashira/cli.rb +17 -19
- data/lib/hashira/complexity/boolean_run.rb +13 -15
- data/lib/hashira/complexity/cognitive_score.rb +68 -66
- data/lib/hashira/complexity/if_chain.rb +33 -38
- data/lib/hashira/complexity/method_finding.rb +20 -42
- data/lib/hashira/complexity/method_score.rb +9 -7
- data/lib/hashira/complexity/rescue_scan.rb +16 -21
- data/lib/hashira/complexity/rollup.rb +13 -15
- data/lib/hashira/complexity/scores.rb +44 -0
- data/lib/hashira/coupling/audiences.rb +49 -0
- data/lib/hashira/coupling/catalog.rb +27 -0
- data/lib/hashira/coupling/census.rb +55 -0
- data/lib/hashira/coupling/constant_registry.rb +47 -0
- data/lib/hashira/coupling/cycle_findings.rb +31 -0
- data/lib/hashira/coupling/cycle_search.rb +37 -0
- data/lib/hashira/coupling/cycles.rb +14 -0
- data/lib/hashira/coupling/definition.rb +22 -0
- data/lib/hashira/coupling/definitions.rb +31 -0
- data/lib/hashira/coupling/edge.rb +10 -0
- data/lib/hashira/coupling/edge_map.rb +33 -0
- data/lib/hashira/coupling/folder_placement.rb +17 -0
- data/lib/hashira/coupling/folding.rb +56 -0
- data/lib/hashira/coupling/graph.rb +63 -0
- data/lib/hashira/coupling/metric.rb +15 -0
- data/lib/hashira/coupling/mixed_audience_findings.rb +32 -0
- data/lib/hashira/coupling/namespace_placement.rb +28 -0
- data/lib/hashira/{analysis → coupling}/namespace_prefix.rb +11 -11
- data/lib/hashira/coupling/naming.rb +14 -0
- data/lib/hashira/coupling/no_folding.rb +13 -0
- data/lib/hashira/coupling/placement.rb +19 -0
- 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/coupling/roster.rb +25 -0
- data/lib/hashira/coupling/rule.rb +18 -0
- data/lib/hashira/coupling/scope.rb +28 -0
- data/lib/hashira/coupling/sdp_check.rb +14 -0
- 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/dot.rb +8 -11
- data/lib/hashira/diagram/mermaid.rb +11 -16
- data/lib/hashira/diagram/source.rb +15 -0
- data/lib/hashira/duplication/clones.rb +17 -0
- data/lib/hashira/duplication/cluster.rb +14 -13
- data/lib/hashira/duplication/clusters.rb +46 -0
- data/lib/hashira/duplication/delta.rb +21 -30
- data/lib/hashira/duplication/duplication_finding.rb +17 -23
- data/lib/hashira/duplication/fragment.rb +19 -23
- data/lib/hashira/duplication/grouping.rb +12 -16
- data/lib/hashira/duplication/harvest.rb +27 -0
- data/lib/hashira/duplication/index.rb +20 -24
- data/lib/hashira/duplication/maximal.rb +11 -15
- data/lib/hashira/duplication/near_miss.rb +19 -22
- data/lib/hashira/duplication/sequence.rb +20 -22
- data/lib/hashira/duplication/similarity.rb +31 -34
- data/lib/hashira/duplication/union_find.rb +10 -14
- data/lib/hashira/duplication/variance.rb +35 -43
- data/lib/hashira/error.rb +4 -2
- data/lib/hashira/hotspots/file_cost.rb +8 -7
- data/lib/hashira/hotspots/rollup.rb +19 -21
- data/lib/hashira/pipeline.rb +41 -31
- data/lib/hashira/project.rb +50 -49
- data/lib/hashira/report/complexity_table.rb +27 -31
- data/lib/hashira/report/dependency_map.rb +17 -22
- data/lib/hashira/report/finding_lines.rb +15 -19
- data/lib/hashira/report/graph_payload.rb +14 -20
- data/lib/hashira/report/hotspot_table.rb +32 -37
- data/lib/hashira/report/json.rb +32 -32
- data/lib/hashira/report/metrics_table.rb +46 -33
- data/lib/hashira/report/phrases.rb +98 -0
- data/lib/hashira/report/smell_phrases.rb +68 -0
- data/lib/hashira/report/text.rb +59 -52
- data/lib/hashira/smells/census.rb +32 -0
- data/lib/hashira/smells/check.rb +30 -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 +30 -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 +44 -0
- data/lib/hashira/smells/manual_dispatch.rb +16 -0
- data/lib/hashira/smells/module_initialize.rb +9 -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 +29 -0
- data/lib/hashira/smells/report.rb +31 -0
- data/lib/hashira/smells/scope.rb +29 -0
- data/lib/hashira/smells/too_many_instance_variables.rb +24 -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 +131 -60
- metadata +62 -20
- data/lib/hashira/analysis/census.rb +0 -39
- data/lib/hashira/analysis/constant_registry.rb +0 -40
- data/lib/hashira/analysis/cycle_findings.rb +0 -33
- data/lib/hashira/analysis/cycle_search.rb +0 -44
- data/lib/hashira/analysis/definitions.rb +0 -27
- data/lib/hashira/analysis/edge.rb +0 -9
- data/lib/hashira/analysis/edge_map.rb +0 -36
- data/lib/hashira/analysis/graph.rb +0 -56
- data/lib/hashira/analysis/metric.rb +0 -14
- data/lib/hashira/analysis/references.rb +0 -39
- data/lib/hashira/analysis/rule.rb +0 -22
- data/lib/hashira/analysis/sdp_check.rb +0 -18
- data/lib/hashira/analysis/sdp_violation_findings.rb +0 -27
- data/lib/hashira/complexity/analyzer.rb +0 -45
- data/lib/hashira/diagram/renderer.rb +0 -20
- data/lib/hashira/duplication/analyzer.rb +0 -21
- data/lib/hashira/duplication/clusterer.rb +0 -50
- data/lib/hashira/duplication/extractor.rb +0 -31
|
@@ -1,22 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
@scores = scores
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
def classes = @scores.group_by { class_name(it.subject) }.map { |name, group| score(name, group) }
|
|
3
|
+
class Hashira::Complexity::Rollup
|
|
4
|
+
def initialize(scores)
|
|
5
|
+
@scores = scores
|
|
6
|
+
end
|
|
11
7
|
|
|
12
|
-
|
|
8
|
+
def classes = @scores.group_by { owner(it.subject) }.map { |name, group| score(name, group) }
|
|
13
9
|
|
|
14
|
-
|
|
15
|
-
ClassScore.new(name:, cognitive: group.sum(&:cognitive), method_count: group.size,
|
|
16
|
-
peak: group.map(&:cognitive).max)
|
|
17
|
-
end
|
|
10
|
+
private
|
|
18
11
|
|
|
19
|
-
|
|
20
|
-
|
|
12
|
+
def score(name, group)
|
|
13
|
+
Hashira::Complexity::ClassScore.new(
|
|
14
|
+
name:, cognitive: group.sum(&:cognitive), method_count: group.size,
|
|
15
|
+
peak: group.map(&:cognitive).max
|
|
16
|
+
)
|
|
21
17
|
end
|
|
18
|
+
|
|
19
|
+
def owner(subject) = subject.split(/[#.]/, 2).first
|
|
22
20
|
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Hashira::Complexity::Scores
|
|
4
|
+
THRESHOLD = 10
|
|
5
|
+
|
|
6
|
+
def initialize(project, trees)
|
|
7
|
+
@project = project
|
|
8
|
+
@scores = trees.flat_map { |path, tree| harvest(path, tree) }
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def ranked = @scores.sort_by { -it.cognitive }
|
|
12
|
+
|
|
13
|
+
def classes = Hashira::Complexity::Rollup.new(@scores).classes.sort_by { -it.cognitive }
|
|
14
|
+
|
|
15
|
+
def findings = flagged.map { Hashira::Complexity::MethodFinding.new(it).to_finding }
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def flagged = ranked.select { it.cognitive >= THRESHOLD }
|
|
20
|
+
|
|
21
|
+
def harvest(path, tree)
|
|
22
|
+
rel = @project.relative(path)
|
|
23
|
+
sites(tree).map { |full, node| score(rel, full, node) }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def sites(tree)
|
|
27
|
+
found = []
|
|
28
|
+
Hashira::Analysis::TypeWalk.each(tree) do |type_node, full|
|
|
29
|
+
Hashira::Analysis::Syntax.direct(type_node).each { found << [full, it] }
|
|
30
|
+
end
|
|
31
|
+
found
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def score(rel, full, node)
|
|
35
|
+
Hashira::Complexity::MethodScore.new(
|
|
36
|
+
subject: subject(full, node), file: rel, line: node.location.start_line,
|
|
37
|
+
**tallies(Hashira::Complexity::CognitiveScore.new(node))
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def tallies(score) = { cognitive: score.total, calls: score.calls, increments: score.increments }
|
|
42
|
+
|
|
43
|
+
def subject(full, node) = "#{full.join("::")}#{node.receiver ? "." : "#"}#{node.name}"
|
|
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
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Hashira::Coupling::Catalog
|
|
4
|
+
include Enumerable
|
|
5
|
+
|
|
6
|
+
def initialize(definitions)
|
|
7
|
+
@definitions = definitions
|
|
8
|
+
@naming = Hashira::Coupling::Naming.new(definitions)
|
|
9
|
+
@entries = definitions.map { |node, full, folder| entry(node, full, folder) }
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def each(&) = @entries.each(&)
|
|
13
|
+
|
|
14
|
+
def prefix = @naming.segments
|
|
15
|
+
|
|
16
|
+
def strip(path) = @naming.strip(path)
|
|
17
|
+
|
|
18
|
+
def roots = @definitions.roots
|
|
19
|
+
|
|
20
|
+
def folders = @definitions.packages
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def entry(node, full, folder)
|
|
25
|
+
Hashira::Coupling::Definition.new(node:, path: @naming.strip(full), folder:)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Hashira::Coupling::Census
|
|
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
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def packaging = @placement.mode
|
|
13
|
+
|
|
14
|
+
def types = @roster.types
|
|
15
|
+
|
|
16
|
+
def prefix = @catalog.prefix
|
|
17
|
+
|
|
18
|
+
def origins = @roster.origins
|
|
19
|
+
|
|
20
|
+
def roots = @catalog.roots
|
|
21
|
+
|
|
22
|
+
def packages = @roster.packages | @placement.baseline
|
|
23
|
+
|
|
24
|
+
def folds = @folding.disclosed
|
|
25
|
+
|
|
26
|
+
def strip(segments) = @catalog.strip(segments)
|
|
27
|
+
|
|
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)
|
|
36
|
+
|
|
37
|
+
def charge(file, nesting) = translate(@placement.charge(file, nesting))
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def type?(segments) = @roster.origins.key?(segments.join("::"))
|
|
42
|
+
|
|
43
|
+
def tally
|
|
44
|
+
Hashira::Coupling::Roster.new(@placement.placed.map { |definition, package| [definition, translate(package)] })
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def settle
|
|
48
|
+
@folding = @placement.folding(self)
|
|
49
|
+
@roster = tally unless @folding.map.empty?
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def translate(package) = @folding.map.fetch(package, package)
|
|
53
|
+
|
|
54
|
+
def scope = Hashira::Coupling::Scope.new(@roster.registry, @catalog, @placement)
|
|
55
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Hashira::Coupling::ConstantRegistry
|
|
4
|
+
AMBIGUOUS = Object.new.freeze
|
|
5
|
+
|
|
6
|
+
def initialize
|
|
7
|
+
@origins = {}
|
|
8
|
+
@shorthand = {}
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
attr_reader :origins
|
|
12
|
+
|
|
13
|
+
def register(path, package)
|
|
14
|
+
return if path.empty?
|
|
15
|
+
claim(@origins, path, package)
|
|
16
|
+
(1...path.length).each { claim(@shorthand, path.drop(it), package) }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def package(path)
|
|
20
|
+
found = anchored(path) || enclosing(path)
|
|
21
|
+
found unless found == AMBIGUOUS
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def exact(path) = @origins[path.join("::")]
|
|
25
|
+
|
|
26
|
+
def packages = (@origins.values.uniq - [AMBIGUOUS])
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def claim(claims, path, package)
|
|
31
|
+
key = path.join("::")
|
|
32
|
+
claims[key] = claims.fetch(key, package) == package ? package : AMBIGUOUS
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def anchored(path)
|
|
36
|
+
key = path.join("::")
|
|
37
|
+
@origins[key] || @shorthand[key]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def enclosing(path)
|
|
41
|
+
prefixes(path).filter_map { @origins[it] }.first
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def prefixes(path)
|
|
45
|
+
(path.length - 1).downto(1).map { path.first(it).join("::") }
|
|
46
|
+
end
|
|
47
|
+
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
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Hashira::Coupling::CycleSearch
|
|
4
|
+
def initialize(dependencies, package)
|
|
5
|
+
@dependencies = dependencies
|
|
6
|
+
@package = package
|
|
7
|
+
@predecessor = {}
|
|
8
|
+
@queue = dependencies[package].to_a.each { @predecessor[it] = package }
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def path
|
|
12
|
+
unwind([@package]) if cycle?
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def cycle?
|
|
18
|
+
while (node = @queue.shift)
|
|
19
|
+
return true if node == @package
|
|
20
|
+
visit(node)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def visit(node)
|
|
25
|
+
@dependencies[node].each do |neighbor|
|
|
26
|
+
next if @predecessor.key?(neighbor)
|
|
27
|
+
@predecessor[neighbor] = node
|
|
28
|
+
@queue << neighbor
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def unwind(path)
|
|
33
|
+
first = path.first
|
|
34
|
+
return path if first == @package && path.size > 1
|
|
35
|
+
unwind(path.unshift(@predecessor[first]))
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Hashira::Coupling::Cycles
|
|
4
|
+
def initialize(dependencies, graph)
|
|
5
|
+
@dependencies = dependencies
|
|
6
|
+
@graph = graph
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def through?(package) = !!path(package)
|
|
10
|
+
|
|
11
|
+
def path(package) = Hashira::Coupling::CycleSearch.new(@dependencies, package).path
|
|
12
|
+
|
|
13
|
+
def weakest(trail) = trail.each_cons(2).min_by { |from, to| @graph.weight(from, to) }
|
|
14
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
module Hashira
|
|
6
|
+
module Coupling
|
|
7
|
+
Definition =
|
|
8
|
+
Data.define(:node, :path, :folder) do
|
|
9
|
+
def name = path.first
|
|
10
|
+
|
|
11
|
+
def nested? = path.length > 1
|
|
12
|
+
|
|
13
|
+
def klass? = node.is_a?(Prism::ClassNode)
|
|
14
|
+
|
|
15
|
+
def singular? = klass? && !nested?
|
|
16
|
+
|
|
17
|
+
def superclass = node.superclass
|
|
18
|
+
|
|
19
|
+
def counted? = klass? || !Hashira::Analysis::Syntax.direct(node).empty?
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Hashira::Coupling::Definitions
|
|
4
|
+
include Enumerable
|
|
5
|
+
|
|
6
|
+
def initialize(project, trees)
|
|
7
|
+
@project = project
|
|
8
|
+
@trees = trees
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def each(&)
|
|
12
|
+
@trees.each { |file, tree| scan(file, tree, &) }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def packages = @trees.keys.map { @project.package(it) }.uniq
|
|
16
|
+
|
|
17
|
+
def roots
|
|
18
|
+
@roots ||= @trees.each_value.with_object(Set.new) { |tree, set| survey(tree, set) }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def survey(tree, set)
|
|
24
|
+
Hashira::Analysis::TypeWalk.each(tree) { |_node, full| set << full }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def scan(file, tree)
|
|
28
|
+
package = @project.package(file)
|
|
29
|
+
Hashira::Analysis::TypeWalk.each(tree, roots: roots) { |node, full| yield(node, full, package) }
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Hashira::Coupling::EdgeMap
|
|
4
|
+
def initialize(census)
|
|
5
|
+
@census = census
|
|
6
|
+
@dependencies = sets
|
|
7
|
+
@evidence = sets
|
|
8
|
+
@usage = sets
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
attr_reader :dependencies, :evidence, :usage
|
|
12
|
+
|
|
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}")
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def sets = Hash.new { |hash, key| hash[key] = Set.new }
|
|
22
|
+
|
|
23
|
+
def locate(segments, nesting)
|
|
24
|
+
nesting ? @census.resolve(segments, nesting) : @census.pinpoint(segments)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def note(from, to, segments, site)
|
|
28
|
+
return unless to && to != from
|
|
29
|
+
@dependencies[from] << to
|
|
30
|
+
@evidence[[from, to]] << "#{site}: #{segments.join("::")}"
|
|
31
|
+
@usage[[from, to]] << @census.holder(segments)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "placement"
|
|
4
|
+
|
|
5
|
+
class Hashira::Coupling::FolderPlacement < Hashira::Coupling::Placement
|
|
6
|
+
def mode = :folder
|
|
7
|
+
|
|
8
|
+
def placed = catalog.map { [it, it.folder] }
|
|
9
|
+
|
|
10
|
+
def baseline = catalog.folders
|
|
11
|
+
|
|
12
|
+
def charge(file, _nesting) = project.package(file)
|
|
13
|
+
|
|
14
|
+
def skip?(_segments) = false
|
|
15
|
+
|
|
16
|
+
def folding(_census) = Hashira::Coupling::NoFolding
|
|
17
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
class Hashira::Coupling::Folding
|
|
6
|
+
SUFFIXES = %w[Resource Serializer Policy Decorator].freeze
|
|
7
|
+
|
|
8
|
+
def initialize(definitions, census, suffixes: [])
|
|
9
|
+
@definitions = definitions
|
|
10
|
+
@census = census
|
|
11
|
+
@suffixes = suffixes
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def map = @map ||= links.keys.to_h { [it, settle(it, links)] }.reject { |from, to| from == to }
|
|
15
|
+
|
|
16
|
+
def disclosed
|
|
17
|
+
map.map { |from, to| { from:, to:, via: parents.key?(from) ? "base" : "suffix" } }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def links = @links ||= named.merge(parents)
|
|
23
|
+
|
|
24
|
+
def parents
|
|
25
|
+
@parents ||= singles.select { |_name, one| one.superclass }.to_h { |name, one| [name, target(one)] }.compact
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def named
|
|
29
|
+
singles.keys.filter_map { |name| pair(name, crop(name)) }.to_h
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def pair(name, stem) = ([name, stem] if stem && @census.packages.include?(stem))
|
|
33
|
+
|
|
34
|
+
def crop(name)
|
|
35
|
+
suffix = @suffixes.find { trims?(name, it) }
|
|
36
|
+
suffix && name.delete_suffix(suffix)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def trims?(name, suffix) = name.end_with?(suffix) && name != suffix
|
|
40
|
+
|
|
41
|
+
def singles = @singles ||= lone.group_by(&:name).transform_values { it.find(&:superclass) || it.last }
|
|
42
|
+
|
|
43
|
+
def lone = @definitions.select(&:singular?).reject { anchored.include?(it.name) }
|
|
44
|
+
|
|
45
|
+
def anchored = @anchored ||= @definitions.select(&:nested?).to_set(&:name)
|
|
46
|
+
|
|
47
|
+
def target(one) = @census.pinpoint(Hashira::Analysis::Syntax.segments(one.superclass))
|
|
48
|
+
|
|
49
|
+
def settle(name, links)
|
|
50
|
+
trail = [name]
|
|
51
|
+
while (target = links[name]) && !trail.include?(target)
|
|
52
|
+
trail << (name = target)
|
|
53
|
+
end
|
|
54
|
+
target ? trail.drop(trail.index(target)).min : name
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Hashira::Coupling::Graph
|
|
4
|
+
def initialize(project, trees, census)
|
|
5
|
+
@census = census
|
|
6
|
+
@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
|
+
end
|
|
11
|
+
|
|
12
|
+
attr_reader :cycles, :trees
|
|
13
|
+
|
|
14
|
+
def charge(file) = @census.charge(file, [])
|
|
15
|
+
|
|
16
|
+
def packages = (@census.packages | @map.dependencies.keys)
|
|
17
|
+
|
|
18
|
+
def packaging = @census.packaging
|
|
19
|
+
|
|
20
|
+
def folds = @census.folds
|
|
21
|
+
|
|
22
|
+
def outgoing(package) = dependencies[package].to_a.sort
|
|
23
|
+
|
|
24
|
+
def incoming(package) = packages.select { dependencies[it].include?(package) }.sort
|
|
25
|
+
|
|
26
|
+
def edges
|
|
27
|
+
dependencies.sort.flat_map { |from, tos| tos.sort.map { Hashira::Coupling::Edge.new(from:, to: it) } }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def weighted
|
|
31
|
+
edges.map do |edge|
|
|
32
|
+
from, to = edge.deconstruct
|
|
33
|
+
[from, to, weight(from, to)]
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def evidence(from, to) = @map.evidence[[from, to]]
|
|
38
|
+
|
|
39
|
+
def usage(package) = incoming(package).to_h { [it, @map.usage[[it, package]]] }
|
|
40
|
+
|
|
41
|
+
def constants(edge)
|
|
42
|
+
from, to = edge.deconstruct
|
|
43
|
+
@map.usage[[from, to]].sort
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def metric(package)
|
|
47
|
+
Hashira::Coupling::Metric.new(
|
|
48
|
+
types: @census.types[package],
|
|
49
|
+
afferent: incoming(package).size,
|
|
50
|
+
efferent: dependencies[package].size
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def metrics = packages.to_h { [it, metric(it)] }
|
|
55
|
+
|
|
56
|
+
def violations = Hashira::Coupling::SdpCheck.new(dependencies, metrics).violations
|
|
57
|
+
|
|
58
|
+
def weight(from, to) = evidence(from, to).size
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def dependencies = @map.dependencies
|
|
63
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hashira
|
|
4
|
+
module Coupling
|
|
5
|
+
Metric =
|
|
6
|
+
Data.define(:types, :afferent, :efferent) do
|
|
7
|
+
def instability
|
|
8
|
+
total = efferent + afferent
|
|
9
|
+
total.zero? ? 0.0 : efferent.fdiv(total)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def to_h = { tc: types, ca: afferent, ce: efferent, i: instability }
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "rule"
|
|
4
|
+
|
|
5
|
+
class Hashira::Coupling::MixedAudienceFindings < Hashira::Coupling::Rule
|
|
6
|
+
KIND = "mixed_audience"
|
|
7
|
+
|
|
8
|
+
def list
|
|
9
|
+
graph.packages.sort.filter_map { verdict(it) }
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def verdict(package)
|
|
15
|
+
audiences = Hashira::Coupling::Audiences.new(graph.usage(package))
|
|
16
|
+
detail(package, audiences.parts) if audiences.split?
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def detail(package, parts)
|
|
20
|
+
finding(
|
|
21
|
+
package:, evidence: parts.flat_map do
|
|
22
|
+
sightings(package, it).first(2)
|
|
23
|
+
end, detail: { parts: parts.map(&:to_h) }
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def sightings(package, part)
|
|
28
|
+
part.users.flat_map { |client| graph.evidence(client, package).to_a.select { mentions?(it, part.constants) } }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def mentions?(line, constants) = constants.any? { line.end_with?(": #{it}", "::#{it}") }
|
|
32
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "placement"
|
|
4
|
+
|
|
5
|
+
class Hashira::Coupling::NamespacePlacement < Hashira::Coupling::Placement
|
|
6
|
+
RAILS_BASES =
|
|
7
|
+
%w[ApplicationRecord ApplicationController ApplicationJob ApplicationMailer
|
|
8
|
+
ApplicationHelper ApplicationCable ApplicationResource ApplicationSerializer
|
|
9
|
+
ApplicationPolicy ApplicationDecorator].freeze
|
|
10
|
+
|
|
11
|
+
def mode = :namespace
|
|
12
|
+
|
|
13
|
+
def placed = catalog.map { [it, it.name] }
|
|
14
|
+
|
|
15
|
+
def baseline = []
|
|
16
|
+
|
|
17
|
+
def charge(_file, nesting)
|
|
18
|
+
nesting.reverse_each.filter_map { catalog.strip(it).first }.first || project.root
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def skip?(segments) = project.rails? && RAILS_BASES.include?(segments.first)
|
|
22
|
+
|
|
23
|
+
def folding(census) = Hashira::Coupling::Folding.new(catalog, census, suffixes: suffixes)
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def suffixes = project.rails? ? Hashira::Coupling::Folding::SUFFIXES : []
|
|
28
|
+
end
|