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
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: hashira
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Giacomo GK
|
|
@@ -26,22 +26,8 @@ files:
|
|
|
26
26
|
- README.md
|
|
27
27
|
- exe/hashira
|
|
28
28
|
- lib/hashira.rb
|
|
29
|
-
- lib/hashira/analysis/census.rb
|
|
30
|
-
- lib/hashira/analysis/constant_registry.rb
|
|
31
|
-
- lib/hashira/analysis/cycle_findings.rb
|
|
32
|
-
- lib/hashira/analysis/cycle_search.rb
|
|
33
|
-
- lib/hashira/analysis/definitions.rb
|
|
34
|
-
- lib/hashira/analysis/edge.rb
|
|
35
|
-
- lib/hashira/analysis/edge_map.rb
|
|
36
29
|
- lib/hashira/analysis/finding.rb
|
|
37
|
-
- lib/hashira/analysis/graph.rb
|
|
38
|
-
- lib/hashira/analysis/metric.rb
|
|
39
|
-
- lib/hashira/analysis/namespace_prefix.rb
|
|
40
30
|
- lib/hashira/analysis/node_walk.rb
|
|
41
|
-
- lib/hashira/analysis/references.rb
|
|
42
|
-
- lib/hashira/analysis/rule.rb
|
|
43
|
-
- lib/hashira/analysis/sdp_check.rb
|
|
44
|
-
- lib/hashira/analysis/sdp_violation_findings.rb
|
|
45
31
|
- lib/hashira/analysis/syntax.rb
|
|
46
32
|
- lib/hashira/analysis/type_walk.rb
|
|
47
33
|
- lib/hashira/churn.rb
|
|
@@ -55,13 +41,14 @@ files:
|
|
|
55
41
|
- lib/hashira/ci/ratchet.rb
|
|
56
42
|
- lib/hashira/ci/ratchet_report.rb
|
|
57
43
|
- lib/hashira/cli.rb
|
|
44
|
+
- lib/hashira/cli/arguments.rb
|
|
58
45
|
- lib/hashira/cli/command_line.rb
|
|
59
46
|
- lib/hashira/cli/fail_on.rb
|
|
60
47
|
- lib/hashira/cli/options.rb
|
|
48
|
+
- lib/hashira/cli/package_by.rb
|
|
61
49
|
- lib/hashira/cli/run.rb
|
|
62
50
|
- lib/hashira/cli/skip.rb
|
|
63
51
|
- lib/hashira/cli/usage.rb
|
|
64
|
-
- lib/hashira/complexity/analyzer.rb
|
|
65
52
|
- lib/hashira/complexity/boolean_run.rb
|
|
66
53
|
- lib/hashira/complexity/cognitive_score.rb
|
|
67
54
|
- lib/hashira/complexity/if_chain.rb
|
|
@@ -69,17 +56,50 @@ files:
|
|
|
69
56
|
- lib/hashira/complexity/method_score.rb
|
|
70
57
|
- lib/hashira/complexity/rescue_scan.rb
|
|
71
58
|
- lib/hashira/complexity/rollup.rb
|
|
59
|
+
- lib/hashira/complexity/scores.rb
|
|
60
|
+
- lib/hashira/coupling/audiences.rb
|
|
61
|
+
- lib/hashira/coupling/catalog.rb
|
|
62
|
+
- lib/hashira/coupling/census.rb
|
|
63
|
+
- lib/hashira/coupling/constant_registry.rb
|
|
64
|
+
- lib/hashira/coupling/cycle_findings.rb
|
|
65
|
+
- lib/hashira/coupling/cycle_search.rb
|
|
66
|
+
- lib/hashira/coupling/cycles.rb
|
|
67
|
+
- lib/hashira/coupling/definition.rb
|
|
68
|
+
- lib/hashira/coupling/definitions.rb
|
|
69
|
+
- lib/hashira/coupling/edge.rb
|
|
70
|
+
- lib/hashira/coupling/edge_map.rb
|
|
71
|
+
- lib/hashira/coupling/folder_placement.rb
|
|
72
|
+
- lib/hashira/coupling/folding.rb
|
|
73
|
+
- lib/hashira/coupling/graph.rb
|
|
74
|
+
- lib/hashira/coupling/metric.rb
|
|
75
|
+
- lib/hashira/coupling/mixed_audience_findings.rb
|
|
76
|
+
- lib/hashira/coupling/namespace_placement.rb
|
|
77
|
+
- lib/hashira/coupling/namespace_prefix.rb
|
|
78
|
+
- lib/hashira/coupling/naming.rb
|
|
79
|
+
- lib/hashira/coupling/no_folding.rb
|
|
80
|
+
- lib/hashira/coupling/placement.rb
|
|
81
|
+
- lib/hashira/coupling/references.rb
|
|
82
|
+
- lib/hashira/coupling/report.rb
|
|
83
|
+
- lib/hashira/coupling/roll_call.rb
|
|
84
|
+
- lib/hashira/coupling/roll_call_findings.rb
|
|
85
|
+
- lib/hashira/coupling/roster.rb
|
|
86
|
+
- lib/hashira/coupling/rule.rb
|
|
87
|
+
- lib/hashira/coupling/scope.rb
|
|
88
|
+
- lib/hashira/coupling/sdp_check.rb
|
|
89
|
+
- lib/hashira/coupling/sdp_violation_findings.rb
|
|
90
|
+
- lib/hashira/coupling/wide_edge_findings.rb
|
|
91
|
+
- lib/hashira/coupling/words.rb
|
|
72
92
|
- lib/hashira/diagram/dot.rb
|
|
73
93
|
- lib/hashira/diagram/mermaid.rb
|
|
74
|
-
- lib/hashira/diagram/
|
|
75
|
-
- lib/hashira/duplication/
|
|
94
|
+
- lib/hashira/diagram/source.rb
|
|
95
|
+
- lib/hashira/duplication/clones.rb
|
|
76
96
|
- lib/hashira/duplication/cluster.rb
|
|
77
|
-
- lib/hashira/duplication/
|
|
97
|
+
- lib/hashira/duplication/clusters.rb
|
|
78
98
|
- lib/hashira/duplication/delta.rb
|
|
79
99
|
- lib/hashira/duplication/duplication_finding.rb
|
|
80
|
-
- lib/hashira/duplication/extractor.rb
|
|
81
100
|
- lib/hashira/duplication/fragment.rb
|
|
82
101
|
- lib/hashira/duplication/grouping.rb
|
|
102
|
+
- lib/hashira/duplication/harvest.rb
|
|
83
103
|
- lib/hashira/duplication/index.rb
|
|
84
104
|
- lib/hashira/duplication/maximal.rb
|
|
85
105
|
- lib/hashira/duplication/near_miss.rb
|
|
@@ -99,8 +119,30 @@ files:
|
|
|
99
119
|
- lib/hashira/report/hotspot_table.rb
|
|
100
120
|
- lib/hashira/report/json.rb
|
|
101
121
|
- lib/hashira/report/metrics_table.rb
|
|
122
|
+
- lib/hashira/report/phrases.rb
|
|
123
|
+
- lib/hashira/report/smell_phrases.rb
|
|
102
124
|
- lib/hashira/report/text.rb
|
|
103
125
|
- lib/hashira/report/view.rb
|
|
126
|
+
- lib/hashira/smells/census.rb
|
|
127
|
+
- lib/hashira/smells/check.rb
|
|
128
|
+
- lib/hashira/smells/conditions.rb
|
|
129
|
+
- lib/hashira/smells/contexts.rb
|
|
130
|
+
- lib/hashira/smells/control_parameter.rb
|
|
131
|
+
- lib/hashira/smells/data_clump.rb
|
|
132
|
+
- lib/hashira/smells/duplicate_method_call.rb
|
|
133
|
+
- lib/hashira/smells/feature_envy.rb
|
|
134
|
+
- lib/hashira/smells/instance_variable_assumption.rb
|
|
135
|
+
- lib/hashira/smells/manual_dispatch.rb
|
|
136
|
+
- lib/hashira/smells/module_initialize.rb
|
|
137
|
+
- lib/hashira/smells/nil_check.rb
|
|
138
|
+
- lib/hashira/smells/param_check.rb
|
|
139
|
+
- lib/hashira/smells/refs.rb
|
|
140
|
+
- lib/hashira/smells/repeated_conditional.rb
|
|
141
|
+
- lib/hashira/smells/report.rb
|
|
142
|
+
- lib/hashira/smells/scope.rb
|
|
143
|
+
- lib/hashira/smells/too_many_instance_variables.rb
|
|
144
|
+
- lib/hashira/smells/utility_function.rb
|
|
145
|
+
- lib/hashira/smells/visibility.rb
|
|
104
146
|
- lib/hashira/version.rb
|
|
105
147
|
homepage: https://github.com/giacope/hashira
|
|
106
148
|
licenses:
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Hashira
|
|
4
|
-
module Analysis
|
|
5
|
-
class Census
|
|
6
|
-
def initialize(project, trees)
|
|
7
|
-
@definitions = Definitions.new(project, trees)
|
|
8
|
-
@type_count = Hash.new(0)
|
|
9
|
-
@registry = ConstantRegistry.new
|
|
10
|
-
@namespace_prefix = NamespacePrefix.infer(@definitions)
|
|
11
|
-
take
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
attr_reader :type_count, :namespace_prefix
|
|
15
|
-
|
|
16
|
-
def declaring_package = @registry.declaring_package
|
|
17
|
-
|
|
18
|
-
def packages = (@type_count.keys | @definitions.packages)
|
|
19
|
-
|
|
20
|
-
def resolve(segments) = @registry.package_for(after_prefix(segments))
|
|
21
|
-
|
|
22
|
-
private
|
|
23
|
-
|
|
24
|
-
def take
|
|
25
|
-
@definitions.each do |node, full, package|
|
|
26
|
-
@registry.register(after_prefix(full), package)
|
|
27
|
-
@type_count[package] += 1 unless Syntax.direct_definitions(node).empty?
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
def after_prefix(segments)
|
|
32
|
-
return [] if @namespace_prefix.first(segments.length) == segments
|
|
33
|
-
|
|
34
|
-
depth = @namespace_prefix.length.downto(0).find { segments.first(it) == @namespace_prefix.last(it) }
|
|
35
|
-
segments.drop(depth)
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
end
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Hashira
|
|
4
|
-
module Analysis
|
|
5
|
-
class ConstantRegistry
|
|
6
|
-
AMBIGUOUS = Object.new.freeze
|
|
7
|
-
|
|
8
|
-
def initialize
|
|
9
|
-
@declaring_package = {}
|
|
10
|
-
@shorthand = {}
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
attr_reader :declaring_package
|
|
14
|
-
|
|
15
|
-
def register(path, package)
|
|
16
|
-
return if path.empty?
|
|
17
|
-
|
|
18
|
-
claim(@declaring_package, path, package)
|
|
19
|
-
(1...path.length).each { claim(@shorthand, path.drop(it), package) }
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def package_for(path)
|
|
23
|
-
found = path.length.downto(1).filter_map { claimed(path.first(it)) }.first
|
|
24
|
-
found unless found == AMBIGUOUS
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
private
|
|
28
|
-
|
|
29
|
-
def claim(claims, path, package)
|
|
30
|
-
key = path.join("::")
|
|
31
|
-
claims[key] = claims.fetch(key, package) == package ? package : AMBIGUOUS
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def claimed(path)
|
|
35
|
-
key = path.join("::")
|
|
36
|
-
@declaring_package[key] || @shorthand[key]
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
end
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Hashira
|
|
4
|
-
module Analysis
|
|
5
|
-
class CycleFindings < Rule
|
|
6
|
-
KIND = "cycle"
|
|
7
|
-
|
|
8
|
-
def list
|
|
9
|
-
graph.packages.select { graph.cyclic?(it) }.sort.map { cycle_finding(it) }
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
private
|
|
13
|
-
|
|
14
|
-
def cycle_finding(package)
|
|
15
|
-
path = graph.cycle_path(package)
|
|
16
|
-
finding(package:, cycle: path, evidence: evidence(path),
|
|
17
|
-
message: message(package, path, graph.weakest_edge(path)))
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def message(package, path, weak_edge)
|
|
21
|
-
weak_from, weak_to = weak_edge
|
|
22
|
-
weight = graph.weight(weak_from, weak_to)
|
|
23
|
-
"#{package} can reach itself: #{path.join(" -> ")} — any change may ripple back " \
|
|
24
|
-
"around. The lightest edge on this cycle is #{weak_from} -> #{weak_to} " \
|
|
25
|
-
"(#{weight} ref#{"s" unless weight == 1})."
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def evidence(path)
|
|
29
|
-
path.each_cons(2).flat_map { |from, to| graph.evidence_for(from, to).to_a.first(2) }
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
end
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Hashira
|
|
4
|
-
module Analysis
|
|
5
|
-
class CycleSearch
|
|
6
|
-
def initialize(dependencies, package)
|
|
7
|
-
@dependencies = dependencies
|
|
8
|
-
@package = package
|
|
9
|
-
@predecessor = {}
|
|
10
|
-
@queue = dependencies[package].to_a.each { @predecessor[it] = package }
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def path
|
|
14
|
-
trace_back([@package]) if cycle?
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
private
|
|
18
|
-
|
|
19
|
-
def cycle?
|
|
20
|
-
while (node = @queue.shift)
|
|
21
|
-
return true if node == @package
|
|
22
|
-
|
|
23
|
-
visit(node)
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def visit(node)
|
|
28
|
-
@dependencies[node].each do |neighbor|
|
|
29
|
-
next if @predecessor.key?(neighbor)
|
|
30
|
-
|
|
31
|
-
@predecessor[neighbor] = node
|
|
32
|
-
@queue << neighbor
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def trace_back(path)
|
|
37
|
-
first = path.first
|
|
38
|
-
return path if first == @package && path.size > 1
|
|
39
|
-
|
|
40
|
-
trace_back(path.unshift(@predecessor[first]))
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
end
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Hashira
|
|
4
|
-
module Analysis
|
|
5
|
-
class Definitions
|
|
6
|
-
include Enumerable
|
|
7
|
-
|
|
8
|
-
def initialize(project, trees)
|
|
9
|
-
@project = project
|
|
10
|
-
@trees = trees
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def each(&)
|
|
14
|
-
@trees.each { |file, tree| definitions_in(file, tree, &) }
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def packages = @trees.keys.map { @project.package_for(it) }.uniq
|
|
18
|
-
|
|
19
|
-
private
|
|
20
|
-
|
|
21
|
-
def definitions_in(file, tree)
|
|
22
|
-
package = @project.package_for(file)
|
|
23
|
-
TypeWalk.each_definition(tree) { |node, full| yield node, full, package }
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
end
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Hashira
|
|
4
|
-
module Analysis
|
|
5
|
-
class EdgeMap
|
|
6
|
-
def initialize(project, census)
|
|
7
|
-
@project = project
|
|
8
|
-
@census = census
|
|
9
|
-
@dependencies = empty_sets
|
|
10
|
-
@evidence = empty_sets
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
attr_reader :dependencies, :evidence
|
|
14
|
-
|
|
15
|
-
def record(file, tree)
|
|
16
|
-
from = @project.package_for(file)
|
|
17
|
-
source = @project.relative(file)
|
|
18
|
-
References.each_sighting(tree).each do |segments, line|
|
|
19
|
-
record_reference(from, source, segments, line)
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
private
|
|
24
|
-
|
|
25
|
-
def empty_sets = Hash.new { |hash, key| hash[key] = Set.new }
|
|
26
|
-
|
|
27
|
-
def record_reference(from, source, segments, line)
|
|
28
|
-
to = @census.resolve(segments)
|
|
29
|
-
return unless to && to != from
|
|
30
|
-
|
|
31
|
-
@dependencies[from] << to
|
|
32
|
-
@evidence[[from, to]] << "#{source}:#{line}: #{segments.join("::")}"
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
end
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Hashira
|
|
4
|
-
module Analysis
|
|
5
|
-
class Graph
|
|
6
|
-
def initialize(project, trees, census)
|
|
7
|
-
@census = census
|
|
8
|
-
@edge_map = EdgeMap.new(project, census)
|
|
9
|
-
trees.each { |file, tree| @edge_map.record(file, tree) }
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def packages = @census.packages
|
|
13
|
-
|
|
14
|
-
def dependencies_of(package) = dependencies[package].to_a.sort
|
|
15
|
-
|
|
16
|
-
def dependents_of(package) = packages.select { dependencies[it].include?(package) }.sort
|
|
17
|
-
|
|
18
|
-
def edge_list
|
|
19
|
-
dependencies.sort.flat_map { |from, tos| tos.sort.map { Edge.new(from:, to: it) } }
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def weighted_edges
|
|
23
|
-
edge_list.map do |edge|
|
|
24
|
-
from, to = edge.deconstruct
|
|
25
|
-
[from, to, weight(from, to)]
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def evidence_for(from, to) = @edge_map.evidence[[from, to]]
|
|
30
|
-
|
|
31
|
-
def metric_for(package)
|
|
32
|
-
Metric.new(type_count: @census.type_count[package],
|
|
33
|
-
afferent: dependents_of(package).size,
|
|
34
|
-
efferent: dependencies[package].size)
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def metrics = packages.to_h { [it, metric_for(it)] }
|
|
38
|
-
|
|
39
|
-
def sdp_violations = SdpCheck.new(dependencies, metrics).violations
|
|
40
|
-
|
|
41
|
-
def cyclic?(package) = !!cycle_path(package)
|
|
42
|
-
|
|
43
|
-
def weight(from, to) = evidence_for(from, to).size
|
|
44
|
-
|
|
45
|
-
def cycle_path(package) = CycleSearch.new(dependencies, package).path
|
|
46
|
-
|
|
47
|
-
def weakest_edge(path)
|
|
48
|
-
path.each_cons(2).min_by { |from, to| weight(from, to) }
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
private
|
|
52
|
-
|
|
53
|
-
def dependencies = @edge_map.dependencies
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
end
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Hashira
|
|
4
|
-
module Analysis
|
|
5
|
-
Metric = Data.define(:type_count, :afferent, :efferent) do
|
|
6
|
-
def instability
|
|
7
|
-
total = efferent + afferent
|
|
8
|
-
total.zero? ? 0.0 : efferent.to_f / total
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
def to_h = { tc: type_count, ca: afferent, ce: efferent, i: instability }
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
end
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "prism"
|
|
4
|
-
|
|
5
|
-
module Hashira
|
|
6
|
-
module Analysis
|
|
7
|
-
module References
|
|
8
|
-
module_function
|
|
9
|
-
|
|
10
|
-
def list(tree)
|
|
11
|
-
each_sighting(tree).map(&:first)
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def each_sighting(tree)
|
|
15
|
-
[].tap { collect(tree, it) }
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def collect(node, accumulator)
|
|
19
|
-
return unless node
|
|
20
|
-
return accumulator << [Syntax.path_segments(node), node.location.start_line] if constant?(node)
|
|
21
|
-
|
|
22
|
-
branches(node).each { collect(it, accumulator) }
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def constant?(node) = node.is_a?(Prism::ConstantPathNode) || node.is_a?(Prism::ConstantReadNode)
|
|
26
|
-
|
|
27
|
-
def branches(node)
|
|
28
|
-
definition?(node) ? definition_branches(node) : node.compact_child_nodes
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
def definition?(node) = node.is_a?(Prism::ClassNode) || node.is_a?(Prism::ModuleNode)
|
|
32
|
-
|
|
33
|
-
def definition_branches(node)
|
|
34
|
-
superclass = node.is_a?(Prism::ClassNode) ? node.superclass : nil
|
|
35
|
-
[superclass, node.body]
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
end
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Hashira
|
|
4
|
-
module Analysis
|
|
5
|
-
class Rule
|
|
6
|
-
def initialize(project, graph)
|
|
7
|
-
@project = project
|
|
8
|
-
@graph = graph
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
private
|
|
12
|
-
|
|
13
|
-
attr_reader :project, :graph
|
|
14
|
-
|
|
15
|
-
def metrics = @metrics ||= graph.metrics
|
|
16
|
-
|
|
17
|
-
def finding(**attributes)
|
|
18
|
-
Finding.new(kind: self.class::KIND, cycle: nil, **attributes)
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
end
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Hashira
|
|
4
|
-
module Analysis
|
|
5
|
-
class SdpCheck
|
|
6
|
-
def initialize(dependencies, metrics)
|
|
7
|
-
@dependencies = dependencies
|
|
8
|
-
@metrics = metrics
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
def violations
|
|
12
|
-
@dependencies.flat_map do |from, tos|
|
|
13
|
-
tos.select { @metrics[it].instability > @metrics[from].instability }.map { [from, it] }
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
end
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Hashira
|
|
4
|
-
module Analysis
|
|
5
|
-
class SdpViolationFindings < Rule
|
|
6
|
-
KIND = "sdp_violation"
|
|
7
|
-
|
|
8
|
-
def list
|
|
9
|
-
graph.sdp_violations.sort_by { |from, to| instability(from) - instability(to) }
|
|
10
|
-
.map { |from, to| violation_finding(from, to) }
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
private
|
|
14
|
-
|
|
15
|
-
def violation_finding(from, to)
|
|
16
|
-
finding(package: from, evidence: graph.evidence_for(from, to).to_a.first(5),
|
|
17
|
-
message: "#{from} (I=#{label(from)}) depends on the LESS stable #{to} " \
|
|
18
|
-
"(I=#{label(to)}) — churn in #{to} will force churn in #{from}. " \
|
|
19
|
-
"Invert the edge or extract the stable part of #{to} that #{from} needs.")
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def instability(package) = metrics[package].instability
|
|
23
|
-
|
|
24
|
-
def label(package) = format("%.2f", instability(package))
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
end
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Hashira
|
|
4
|
-
module Complexity
|
|
5
|
-
class Analyzer
|
|
6
|
-
THRESHOLD = 10
|
|
7
|
-
|
|
8
|
-
def initialize(project, trees)
|
|
9
|
-
@project = project
|
|
10
|
-
@scores = trees.flat_map { |path, tree| scores_for(path, tree) }
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def methods = @scores.sort_by { -it.cognitive }
|
|
14
|
-
|
|
15
|
-
def classes = Rollup.new(@scores).classes.sort_by { -it.cognitive }
|
|
16
|
-
|
|
17
|
-
def findings = flagged.map { MethodFinding.new(it).to_finding }
|
|
18
|
-
|
|
19
|
-
private
|
|
20
|
-
|
|
21
|
-
def flagged = methods.select { it.cognitive >= THRESHOLD }
|
|
22
|
-
|
|
23
|
-
def scores_for(path, tree)
|
|
24
|
-
rel = @project.relative(path)
|
|
25
|
-
methods_in(tree).map { |full, node| build_score(rel, full, node) }
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def methods_in(tree)
|
|
29
|
-
found = []
|
|
30
|
-
Analysis::TypeWalk.each_definition(tree) do |type_node, full|
|
|
31
|
-
Analysis::Syntax.direct_definitions(type_node).each { found << [full, it] }
|
|
32
|
-
end
|
|
33
|
-
found
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def build_score(rel, full, node)
|
|
37
|
-
score = CognitiveScore.new(node)
|
|
38
|
-
MethodScore.new(subject: subject(full, node), file: rel, line: node.location.start_line,
|
|
39
|
-
cognitive: score.total, calls: score.calls, increments: score.increments)
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def subject(full, node) = "#{full.join("::")}#{node.receiver ? "." : "#"}#{node.name}"
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
end
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Hashira
|
|
4
|
-
module Diagram
|
|
5
|
-
class Renderer
|
|
6
|
-
def initialize(graph, format, io: $stdout)
|
|
7
|
-
@graph = graph
|
|
8
|
-
@format = format
|
|
9
|
-
@io = io
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def display
|
|
13
|
-
edges = @graph.weighted_edges
|
|
14
|
-
renderer = @format == :dot ? Dot.new(edges) : Mermaid.new(edges)
|
|
15
|
-
@io.puts renderer.source
|
|
16
|
-
0
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
end
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Hashira
|
|
4
|
-
module Duplication
|
|
5
|
-
class Analyzer
|
|
6
|
-
def initialize(project, trees, churn)
|
|
7
|
-
@project = project
|
|
8
|
-
@trees = trees
|
|
9
|
-
@churn = churn
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def clusters = @clusters ||= Clusterer.new(fragments).clusters.sort_by { -it.mass }
|
|
13
|
-
|
|
14
|
-
def findings = clusters.map { |cluster| DuplicationFinding.new(cluster, @churn).to_finding }
|
|
15
|
-
|
|
16
|
-
private
|
|
17
|
-
|
|
18
|
-
def fragments = Extractor.new(@project, @trees).fragments
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
end
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Hashira
|
|
4
|
-
module Duplication
|
|
5
|
-
class Clusterer
|
|
6
|
-
PREFILTER = 12
|
|
7
|
-
BASE_MASS = 16
|
|
8
|
-
NEAR_MASS = 40
|
|
9
|
-
PAIR = 2
|
|
10
|
-
PENALTY_PER_RECURRENCE = 2
|
|
11
|
-
|
|
12
|
-
def initialize(fragments)
|
|
13
|
-
@fragments = fragments.select { |fragment| fragment.mass >= PREFILTER }
|
|
14
|
-
@sets = UnionFind.new
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def clusters
|
|
18
|
-
@fragments.group_by(&:types).each_value { |group| chain(group) }
|
|
19
|
-
NearMiss.new(@fragments).pairs.each { |left, right| @sets.union(left, right) }
|
|
20
|
-
Maximal.new(sized).reduced
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
private
|
|
24
|
-
|
|
25
|
-
def chain(group) = group.each_cons(2) { |left, right| @sets.union(left, right) }
|
|
26
|
-
|
|
27
|
-
def sized = built.filter_map { admitted(it) }
|
|
28
|
-
|
|
29
|
-
def admitted(cluster) = [cluster, exact_core(cluster)].compact.find { fits?(it) }
|
|
30
|
-
|
|
31
|
-
def fits?(cluster) = cluster.mass >= floor(cluster)
|
|
32
|
-
|
|
33
|
-
def exact_core(cluster) = Grouping.new(cluster.exact_sites).cluster
|
|
34
|
-
|
|
35
|
-
def built = @sets.clusters.filter_map { |group| Grouping.new(group).cluster }
|
|
36
|
-
|
|
37
|
-
def floor(cluster) = base(cluster) + idiom_penalty(cluster)
|
|
38
|
-
|
|
39
|
-
def base(cluster) = thin_evidence?(cluster) ? NEAR_MASS : BASE_MASS
|
|
40
|
-
|
|
41
|
-
def thin_evidence?(cluster) = !one_shape?(cluster) || cluster.shape_only?
|
|
42
|
-
|
|
43
|
-
def idiom_penalty(cluster) = recurrences(cluster) * PENALTY_PER_RECURRENCE
|
|
44
|
-
|
|
45
|
-
def recurrences(cluster) = [cluster.size - PAIR, 0].max
|
|
46
|
-
|
|
47
|
-
def one_shape?(cluster) = cluster.sites.map(&:types).uniq.size == 1
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
end
|