hashira 0.2.0 → 0.4.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 +141 -0
- data/README.md +53 -5
- data/lib/hashira/analysis/catalog.rb +27 -0
- data/lib/hashira/analysis/census.rb +42 -35
- data/lib/hashira/analysis/constant_registry.rb +43 -0
- data/lib/hashira/analysis/cycle_findings.rb +34 -29
- data/lib/hashira/analysis/cycle_search.rb +28 -35
- data/lib/hashira/analysis/cycles.rb +14 -0
- data/lib/hashira/analysis/definition.rb +22 -0
- data/lib/hashira/analysis/definitions.rb +22 -18
- data/lib/hashira/analysis/edge.rb +4 -3
- data/lib/hashira/analysis/edge_map.rb +23 -26
- data/lib/hashira/analysis/finding.rb +7 -6
- data/lib/hashira/analysis/folder_placement.rb +17 -0
- data/lib/hashira/analysis/folding.rb +54 -0
- data/lib/hashira/analysis/graph.rb +35 -38
- data/lib/hashira/analysis/metric.rb +8 -7
- data/lib/hashira/analysis/namespace_placement.rb +28 -0
- data/lib/hashira/analysis/namespace_prefix.rb +32 -0
- data/lib/hashira/analysis/naming.rb +14 -0
- data/lib/hashira/analysis/no_folding.rb +13 -0
- data/lib/hashira/analysis/node_walk.rb +3 -3
- data/lib/hashira/analysis/placement.rb +19 -0
- data/lib/hashira/analysis/references.rb +14 -13
- data/lib/hashira/analysis/resolver.rb +28 -0
- data/lib/hashira/analysis/roster.rb +25 -0
- data/lib/hashira/analysis/rule.rb +10 -14
- data/lib/hashira/analysis/sdp_check.rb +8 -12
- data/lib/hashira/analysis/sdp_violation_findings.rb +20 -18
- 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 +36 -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 +15 -20
- data/lib/hashira/cli/options.rb +9 -4
- data/lib/hashira/cli/package_by.rb +14 -0
- data/lib/hashira/cli/run.rb +29 -27
- data/lib/hashira/cli/skip.rb +14 -20
- data/lib/hashira/cli/usage.rb +41 -41
- data/lib/hashira/cli.rb +17 -19
- data/lib/hashira/complexity/analyzer.rb +30 -32
- data/lib/hashira/complexity/boolean_run.rb +11 -15
- data/lib/hashira/complexity/cognitive_score.rb +61 -66
- data/lib/hashira/complexity/if_chain.rb +33 -38
- data/lib/hashira/complexity/method_finding.rb +43 -48
- 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/diagram/dot.rb +8 -11
- data/lib/hashira/diagram/mermaid.rb +11 -16
- data/lib/hashira/diagram/renderer.rb +10 -15
- data/lib/hashira/duplication/analyzer.rb +10 -14
- data/lib/hashira/duplication/cluster.rb +15 -12
- data/lib/hashira/duplication/clusterer.rb +31 -29
- data/lib/hashira/duplication/delta.rb +29 -30
- data/lib/hashira/duplication/duplication_finding.rb +19 -21
- data/lib/hashira/duplication/extractor.rb +15 -19
- data/lib/hashira/duplication/fragment.rb +19 -23
- data/lib/hashira/duplication/grouping.rb +12 -16
- 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 +28 -35
- data/lib/hashira/duplication/union_find.rb +10 -14
- data/lib/hashira/duplication/variance.rb +36 -44
- 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 +36 -31
- data/lib/hashira/project.rb +53 -32
- 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 +30 -32
- data/lib/hashira/report/metrics_table.rb +46 -33
- data/lib/hashira/report/text.rb +59 -52
- data/lib/hashira/version.rb +1 -1
- data/lib/hashira.rb +92 -55
- metadata +16 -2
- data/lib/hashira/analysis/root_namespace.rb +0 -14
|
@@ -1,57 +1,49 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
@other = other
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def kinds
|
|
19
|
-
return [:structure] if @canonical.types != @other.types
|
|
20
|
-
|
|
21
|
-
differing.map { |node| category(node) }.uniq
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def shape_only?
|
|
25
|
-
return false unless @canonical.types == @other.types
|
|
26
|
-
|
|
27
|
-
named.any? && named.all? { |left, right| left.name != right.name }
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
private
|
|
3
|
+
class Hashira::Duplication::Variance
|
|
4
|
+
LITERALS = %i[integer_node float_node string_node symbol_node].freeze
|
|
5
|
+
VALUED = %i[integer_node float_node].freeze
|
|
6
|
+
NAMED = %i[call_node constant_read_node constant_path_node
|
|
7
|
+
local_variable_read_node local_variable_write_node
|
|
8
|
+
instance_variable_read_node instance_variable_write_node].freeze
|
|
9
|
+
CONSTANTS = %i[constant_read_node constant_path_node].freeze
|
|
10
|
+
|
|
11
|
+
def initialize(canonical, other)
|
|
12
|
+
@canonical = canonical
|
|
13
|
+
@other = other
|
|
14
|
+
end
|
|
31
15
|
|
|
32
|
-
|
|
16
|
+
def kinds
|
|
17
|
+
return [:structure] if @canonical.types != @other.types
|
|
18
|
+
differing.map { |node| category(node) }.uniq
|
|
19
|
+
end
|
|
33
20
|
|
|
34
|
-
|
|
21
|
+
def structural?
|
|
22
|
+
return false unless @canonical.types == @other.types
|
|
23
|
+
named.any? && named.all? { |left, right| left.name != right.name }
|
|
24
|
+
end
|
|
35
25
|
|
|
36
|
-
|
|
26
|
+
private
|
|
37
27
|
|
|
38
|
-
|
|
28
|
+
def pairs = @canonical.nodes.zip(@other.nodes)
|
|
39
29
|
|
|
40
|
-
|
|
41
|
-
type = node.type
|
|
42
|
-
return :literal if LITERALS.include?(type)
|
|
30
|
+
def named = @named ||= pairs.select { |left, _| NAMED.include?(left.type) }
|
|
43
31
|
|
|
44
|
-
|
|
45
|
-
end
|
|
32
|
+
def differing = pairs.select { |pair| varies?(*pair) }.map(&:first)
|
|
46
33
|
|
|
47
|
-
|
|
48
|
-
type = node.type
|
|
49
|
-
return literal(node) if LITERALS.include?(type)
|
|
34
|
+
def varies?(left, right) = signature(left) != signature(right)
|
|
50
35
|
|
|
51
|
-
|
|
52
|
-
|
|
36
|
+
def category(node)
|
|
37
|
+
type = node.type
|
|
38
|
+
return :literal if LITERALS.include?(type)
|
|
39
|
+
CONSTANTS.include?(type) ? :constant : :message
|
|
40
|
+
end
|
|
53
41
|
|
|
54
|
-
|
|
55
|
-
|
|
42
|
+
def signature(node)
|
|
43
|
+
type = node.type
|
|
44
|
+
return literal(node) if LITERALS.include?(type)
|
|
45
|
+
node.name if NAMED.include?(type)
|
|
56
46
|
end
|
|
47
|
+
|
|
48
|
+
def literal(node) = VALUED.include?(node.type) ? node.value : node.unescaped
|
|
57
49
|
end
|
data/lib/hashira/error.rb
CHANGED
|
@@ -4,16 +4,17 @@ module Hashira
|
|
|
4
4
|
module Hotspots
|
|
5
5
|
CHURN_FLOOR = 1
|
|
6
6
|
|
|
7
|
-
FileCost =
|
|
8
|
-
|
|
7
|
+
FileCost =
|
|
8
|
+
Data.define(:file, :cognitive, :duplication, :churn) do
|
|
9
|
+
def cost = cognitive + duplication
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
def rank = cost * heat
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
def heat = [churn, CHURN_FLOOR].max
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
def to_h = { file:, cognitive:, duplication:, churn:, cost:, rank: }
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
def cells = [file, cognitive, duplication, churn, rank]
|
|
18
|
+
end
|
|
18
19
|
end
|
|
19
20
|
end
|
|
@@ -1,33 +1,31 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
@churn = churn
|
|
10
|
-
end
|
|
3
|
+
class Hashira::Hotspots::Rollup
|
|
4
|
+
def initialize(complexity, duplication, churn)
|
|
5
|
+
@complexity = complexity
|
|
6
|
+
@duplication = duplication
|
|
7
|
+
@churn = churn
|
|
8
|
+
end
|
|
11
9
|
|
|
12
|
-
|
|
10
|
+
def files = costs.reject { it.cost.zero? }.sort_by { [-it.rank, it.file] }
|
|
13
11
|
|
|
14
|
-
|
|
12
|
+
private
|
|
15
13
|
|
|
16
|
-
|
|
14
|
+
def costs = (cognitive.keys | duplicated.keys).map { cost(it) }
|
|
17
15
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
def cost(file)
|
|
17
|
+
Hashira::Hotspots::FileCost.new(
|
|
18
|
+
file:, cognitive: cognitive[file], duplication: duplicated[file], churn: @churn.hits(file)
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
def scores = @complexity ? @complexity.methods : []
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
def clusters = @duplication ? @duplication.clusters : []
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
def cognitive = @cognitive ||= bucketed(scores.map { [it.file, it.cognitive] })
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
def duplicated = @duplicated ||= bucketed(clusters.flat_map(&:masses))
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
end
|
|
32
|
-
end
|
|
30
|
+
def bucketed(charges) = charges.each_with_object(Hash.new(0)) { |(file, cost), total| total[file] += cost }
|
|
33
31
|
end
|
data/lib/hashira/pipeline.rb
CHANGED
|
@@ -2,49 +2,54 @@
|
|
|
2
2
|
|
|
3
3
|
require "prism"
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
ANALYZERS = %i[coupling complexity duplication].freeze
|
|
5
|
+
class Hashira::Pipeline
|
|
6
|
+
ANALYZERS = %i[coupling complexity duplication].freeze
|
|
8
7
|
|
|
9
|
-
|
|
8
|
+
RULES = [Hashira::Analysis::CycleFindings, Hashira::Analysis::SdpViolationFindings].freeze
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
def initialize(project, enabled: ANALYZERS, packaging: :auto)
|
|
11
|
+
@project = project
|
|
12
|
+
@enabled = enabled
|
|
13
|
+
@trees = project.files.to_h { [it, parse(it)] }
|
|
14
|
+
@graph = Hashira::Analysis::Graph.new(project, @trees, census(settle(packaging)))
|
|
15
|
+
end
|
|
17
16
|
|
|
18
|
-
|
|
17
|
+
attr_reader :project, :graph
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
def complexity
|
|
20
|
+
@complexity ||= Hashira::Complexity::Analyzer.new(@project, @trees) if enabled?(:complexity)
|
|
21
|
+
end
|
|
23
22
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
def duplication
|
|
24
|
+
@duplication ||= Hashira::Duplication::Analyzer.new(@project, @trees, churn) if enabled?(:duplication)
|
|
25
|
+
end
|
|
27
26
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
def hotspots
|
|
28
|
+
@hotspots ||= Hashira::Hotspots::Rollup.new(complexity, duplication, churn) if complexity || duplication
|
|
29
|
+
end
|
|
31
30
|
|
|
32
|
-
|
|
31
|
+
def churn = @churn ||= Hashira::Churn.scan
|
|
33
32
|
|
|
34
|
-
|
|
33
|
+
def enabled?(analyzer) = @enabled.include?(analyzer)
|
|
35
34
|
|
|
36
|
-
|
|
35
|
+
def findings = structural + listed(complexity) + listed(duplication)
|
|
37
36
|
|
|
38
|
-
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def census(packaging) = Hashira::Analysis::Census.new(@project, @trees, packaging:)
|
|
40
|
+
|
|
41
|
+
def settle(packaging)
|
|
42
|
+
return packaging unless packaging == :auto
|
|
43
|
+
@project.rails? ? :namespace : :folder
|
|
44
|
+
end
|
|
39
45
|
|
|
40
|
-
|
|
46
|
+
def structural = enabled?(:coupling) ? RULES.flat_map { it.new(@project, @graph).list } : []
|
|
41
47
|
|
|
42
|
-
|
|
48
|
+
def listed(analyzer) = analyzer ? analyzer.findings : []
|
|
43
49
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
end
|
|
50
|
+
def parse(path)
|
|
51
|
+
Prism.parse_file(path).value
|
|
52
|
+
rescue SystemCallError => error
|
|
53
|
+
raise(Hashira::Error, "cannot read #{path} (#{error.message})")
|
|
49
54
|
end
|
|
50
55
|
end
|
data/lib/hashira/project.rb
CHANGED
|
@@ -1,50 +1,71 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
ROOT_PACKAGE = "(root)"
|
|
3
|
+
class Hashira::Project
|
|
4
|
+
ROOT_PACKAGE = "(root)"
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
def self.detect(directories)
|
|
7
|
+
new((directories.empty? ? defaults : directories).map { descend(it.delete_suffix("/")) })
|
|
8
|
+
end
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
def self.defaults
|
|
11
|
+
raise(Hashira::Error, "no lib/ directory here — pass the source directory explicitly") if Dir["lib/*/"].empty?
|
|
12
|
+
["lib"]
|
|
13
|
+
end
|
|
14
|
+
private_class_method :defaults
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
def self.descend(directory)
|
|
17
|
+
child = sole(directory)
|
|
18
|
+
return directory unless child && Dir["#{child}/*/"].any? && (Dir["#{directory}/*.rb"] - ["#{child}.rb"]).empty?
|
|
19
|
+
descend(child)
|
|
20
|
+
end
|
|
18
21
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
+
def self.sole(directory)
|
|
23
|
+
subdirectories = Dir["#{directory}/*/"]
|
|
24
|
+
subdirectories.size == 1 ? subdirectories.first.delete_suffix("/") : nil
|
|
25
|
+
end
|
|
26
|
+
private_class_method :descend, :sole
|
|
22
27
|
|
|
23
|
-
|
|
24
|
-
|
|
28
|
+
def initialize(directories)
|
|
29
|
+
missing = directories.reject { Dir.exist?(it) }
|
|
30
|
+
raise(Hashira::Error, "no such directory: #{missing.join(", ")}") unless missing.empty?
|
|
31
|
+
@directories = directories.map { it.delete_suffix("/") }
|
|
32
|
+
@rails = @directories.any? { config?(File.expand_path(it)) }
|
|
33
|
+
end
|
|
25
34
|
|
|
26
|
-
|
|
35
|
+
attr_reader :directories
|
|
27
36
|
|
|
28
|
-
|
|
37
|
+
def rails? = @rails
|
|
29
38
|
|
|
30
|
-
|
|
31
|
-
first, rest = relative(path).delete_suffix(".rb").split("/", 2)
|
|
32
|
-
rest || folder?(path, first) ? first : ROOT_PACKAGE
|
|
33
|
-
end
|
|
39
|
+
def files = @directories.flat_map { Dir["#{it}/**/*.rb"] }.sort
|
|
34
40
|
|
|
35
|
-
|
|
41
|
+
def package(path)
|
|
42
|
+
first, rest = relative(path).delete_suffix(".rb").split("/", 2)
|
|
43
|
+
return ROOT_PACKAGE unless rest || folder?(path, first)
|
|
44
|
+
contested.include?(first) ? "#{parent(path)}/#{first}" : first
|
|
45
|
+
end
|
|
36
46
|
|
|
37
|
-
|
|
47
|
+
def relative(path) = path.delete_prefix("#{parent(path)}/")
|
|
38
48
|
|
|
39
|
-
|
|
49
|
+
def label = @directories.join(", ")
|
|
40
50
|
|
|
41
|
-
|
|
51
|
+
def root = ROOT_PACKAGE
|
|
42
52
|
|
|
43
|
-
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def config?(directory)
|
|
56
|
+
["config/application.rb", "../config/application.rb"].any? { File.exist?(File.expand_path(it, directory)) }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def folder?(path, name) = Dir.exist?("#{parent(path)}/#{name}")
|
|
60
|
+
|
|
61
|
+
def contested
|
|
62
|
+
@contested ||=
|
|
63
|
+
@directories.flat_map { |directory| Dir["#{directory}/*/"].map { File.basename(it) } }
|
|
64
|
+
.tally.filter_map { |name, count| name if count > 1 }
|
|
65
|
+
end
|
|
44
66
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
end
|
|
67
|
+
def parent(path)
|
|
68
|
+
@directories.find { path.start_with?("#{it}/") } ||
|
|
69
|
+
raise(Hashira::Error, "#{path} is outside the analyzed directories")
|
|
49
70
|
end
|
|
50
71
|
end
|
|
@@ -1,40 +1,36 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
class Hashira::Report::ComplexityTable
|
|
4
|
+
TOP = 10
|
|
5
|
+
METHOD_ROW = "%-44s %4s %6s %s"
|
|
6
|
+
CLASS_ROW = "%-32s %5s %8s %6s"
|
|
7
|
+
METHOD_TITLE = "Cognitive complexity — worst methods (Cog = how hard to read, Calls = message sends)"
|
|
8
|
+
CLASS_TITLE = "Per-class rollup (Cog total survives extract-method; Peak is the worst method it hides)"
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def print
|
|
16
|
-
section(@complexity.methods, METHOD_ROW, %w[method Cog Calls Loc],
|
|
17
|
-
"Cognitive complexity — worst methods (Cog = how hard to read, Calls = message sends)")
|
|
18
|
-
section(@complexity.classes, CLASS_ROW, %w[class Cog Methods Peak],
|
|
19
|
-
"Per-class rollup (Cog total survives extract-method; Peak is the worst method it hides)")
|
|
20
|
-
end
|
|
10
|
+
def initialize(complexity, io: $stdout)
|
|
11
|
+
@complexity = complexity
|
|
12
|
+
@io = io
|
|
13
|
+
end
|
|
21
14
|
|
|
22
|
-
|
|
15
|
+
def print
|
|
16
|
+
section(@complexity.methods, METHOD_ROW, %w[method Cog Calls Loc], METHOD_TITLE)
|
|
17
|
+
section(@complexity.classes, CLASS_ROW, %w[class Cog Methods Peak], CLASS_TITLE)
|
|
18
|
+
end
|
|
23
19
|
|
|
24
|
-
|
|
25
|
-
heading(row_format, columns, title)
|
|
26
|
-
ranked(scores).each { @io.puts format(row_format, *it.cells) }
|
|
27
|
-
@io.puts
|
|
28
|
-
end
|
|
20
|
+
private
|
|
29
21
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
end
|
|
22
|
+
def section(scores, row_format, columns, title)
|
|
23
|
+
heading(row_format, columns, title)
|
|
24
|
+
ranked(scores).each { @io.puts(format(row_format, *it.cells)) }
|
|
25
|
+
@io.puts
|
|
26
|
+
end
|
|
36
27
|
|
|
37
|
-
|
|
38
|
-
|
|
28
|
+
def heading(row_format, columns, title)
|
|
29
|
+
header = format(row_format, *columns)
|
|
30
|
+
@io.puts("#{title}:\n\n")
|
|
31
|
+
@io.puts(header)
|
|
32
|
+
@io.puts("-" * header.length)
|
|
39
33
|
end
|
|
34
|
+
|
|
35
|
+
def ranked(scores) = scores.select { it.cognitive.positive? }.first(TOP)
|
|
40
36
|
end
|
|
@@ -1,30 +1,25 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
@io = io
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
def print
|
|
12
|
-
@io.puts "Dependencies (DependsUpon(refs) -> | <- UsedBy):"
|
|
13
|
-
@graph.packages.sort.each { @io.puts row(it) }
|
|
14
|
-
end
|
|
3
|
+
class Hashira::Report::DependencyMap
|
|
4
|
+
def initialize(graph, io: $stdout)
|
|
5
|
+
@graph = graph
|
|
6
|
+
@io = io
|
|
7
|
+
end
|
|
15
8
|
|
|
16
|
-
|
|
9
|
+
def print
|
|
10
|
+
@io.puts("Dependencies (DependsUpon(refs) -> | <- UsedBy):")
|
|
11
|
+
@graph.packages.sort.each { @io.puts(row(it)) }
|
|
12
|
+
end
|
|
17
13
|
|
|
18
|
-
|
|
19
|
-
format(" %-12s -> %-32s <- %s", package,
|
|
20
|
-
list(depends_upon(package)), list(@graph.dependents_of(package)))
|
|
21
|
-
end
|
|
14
|
+
private
|
|
22
15
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
16
|
+
def row(package)
|
|
17
|
+
format(" %-12s -> %-32s <- %s", package, list(outgoing(package)), list(@graph.incoming(package)))
|
|
18
|
+
end
|
|
26
19
|
|
|
27
|
-
|
|
28
|
-
|
|
20
|
+
def outgoing(package)
|
|
21
|
+
@graph.outgoing(package).map { "#{it}(#{@graph.weight(package, it)})" }
|
|
29
22
|
end
|
|
23
|
+
|
|
24
|
+
def list(items) = items.empty? ? "(none)" : items.join(", ")
|
|
30
25
|
end
|
|
@@ -1,26 +1,22 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
class FindingLines
|
|
6
|
-
SAMPLE = 4
|
|
3
|
+
class Hashira::Report::FindingLines
|
|
4
|
+
SAMPLE = 4
|
|
7
5
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
6
|
+
def initialize(finding, indent: "", io: $stdout)
|
|
7
|
+
@finding = finding
|
|
8
|
+
@indent = indent
|
|
9
|
+
@io = io
|
|
10
|
+
end
|
|
13
11
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
def print
|
|
13
|
+
@io.puts("#{@indent}#{@finding.kind}: #{@finding.message}")
|
|
14
|
+
@finding.evidence.first(SAMPLE).each { @io.puts("#{@indent} · #{it}") }
|
|
15
|
+
end
|
|
18
16
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
end
|
|
24
|
-
end
|
|
17
|
+
def emit
|
|
18
|
+
print
|
|
19
|
+
overflow = @finding.evidence.size - SAMPLE
|
|
20
|
+
@io.puts("#{@indent} · … (#{overflow} more)") if overflow.positive?
|
|
25
21
|
end
|
|
26
22
|
end
|
|
@@ -1,29 +1,23 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
class Hashira::Report::GraphPayload
|
|
4
|
+
def initialize(graph)
|
|
5
|
+
@graph = graph
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def to_h = { packages:, edges:, folds: @graph.folds }
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
private
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
def packages
|
|
13
|
+
ranked.to_h { |package, metric| [package, metric.to_h.merge(cyclic: @graph.cycles.through?(package))] }
|
|
14
|
+
end
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
@graph.metrics.sort_by { |_package, metric| metric.instability }
|
|
16
|
-
.to_h do |package, metric|
|
|
17
|
-
[package,
|
|
18
|
-
metric.to_h.merge(cyclic: @graph.cyclic?(package))]
|
|
19
|
-
end
|
|
20
|
-
end
|
|
16
|
+
def ranked = @graph.metrics.sort_by { |_package, metric| metric.instability }
|
|
21
17
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
end
|
|
26
|
-
end
|
|
18
|
+
def edges
|
|
19
|
+
@graph.weighted.map do |from, to, weight|
|
|
20
|
+
{ from:, to:, weight:, refs: @graph.evidence(from, to).to_a.sort }
|
|
27
21
|
end
|
|
28
22
|
end
|
|
29
23
|
end
|
|
@@ -1,41 +1,36 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def legend
|
|
36
|
-
@io.puts "\nLegend: Cog cognitive complexity, Dup mass of the clones the file carries,"
|
|
37
|
-
@io.puts " Churn commits touching it, Rank (Cog+Dup) × Churn\n\n"
|
|
38
|
-
end
|
|
39
|
-
end
|
|
3
|
+
class Hashira::Report::HotspotTable
|
|
4
|
+
TOP = 10
|
|
5
|
+
ROW = "%-46s %5s %5s %6s %7s"
|
|
6
|
+
|
|
7
|
+
def initialize(hotspots, io: $stdout)
|
|
8
|
+
@hotspots = hotspots
|
|
9
|
+
@io = io
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def print
|
|
13
|
+
return if ranked.empty?
|
|
14
|
+
heading
|
|
15
|
+
rows
|
|
16
|
+
legend
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def ranked = @ranked ||= @hotspots.files.first(TOP)
|
|
22
|
+
|
|
23
|
+
def rows = ranked.each { @io.puts(format(ROW, *it.cells)) }
|
|
24
|
+
|
|
25
|
+
def heading
|
|
26
|
+
@io.puts("Hotspots — cost × churn (where refactoring pays the most):\n\n")
|
|
27
|
+
header = format(ROW, *%w[file Cog Dup Churn Rank])
|
|
28
|
+
@io.puts(header)
|
|
29
|
+
@io.puts("-" * header.length)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def legend
|
|
33
|
+
@io.puts("\nLegend: Cog cognitive complexity, Dup mass of the clones the file carries,")
|
|
34
|
+
@io.puts(" Churn commits touching it, Rank (Cog+Dup) × Churn\n\n")
|
|
40
35
|
end
|
|
41
36
|
end
|