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,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.ranked : []
|
|
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
|
@@ -1,50 +1,60 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "prism"
|
|
4
|
+
require_relative "coupling/report"
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
ANALYZERS = %i[coupling complexity duplication].freeze
|
|
6
|
+
class Hashira::Pipeline
|
|
7
|
+
ANALYZERS = %i[coupling complexity duplication smells].freeze
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
STRUCTURAL = Hashira::Coupling::Report::RULES.map { it::KIND }.freeze
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
def initialize(project, enabled: ANALYZERS, packaging: :auto)
|
|
12
|
+
@project = project
|
|
13
|
+
@enabled = enabled
|
|
14
|
+
@trees = project.files.to_h { [it, parse(it)] }
|
|
15
|
+
@coupling = Hashira::Coupling::Report.new(project, @trees, packaging: settle(packaging))
|
|
16
|
+
end
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
attr_reader :project
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
@complexity ||= Complexity::Analyzer.new(@project, @trees) if enabled?(:complexity)
|
|
22
|
-
end
|
|
20
|
+
def graph = @coupling.graph
|
|
23
21
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
def complexity
|
|
23
|
+
@complexity ||= Hashira::Complexity::Scores.new(@project, @trees) if enabled?(:complexity)
|
|
24
|
+
end
|
|
27
25
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
def duplication
|
|
27
|
+
@duplication ||= Hashira::Duplication::Clones.new(@project, @trees, churn) if enabled?(:duplication)
|
|
28
|
+
end
|
|
31
29
|
|
|
32
|
-
|
|
30
|
+
def smells
|
|
31
|
+
@smells ||= Hashira::Smells::Report.new(@project, @trees) if enabled?(:smells)
|
|
32
|
+
end
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
def hotspots
|
|
35
|
+
@hotspots ||= Hashira::Hotspots::Rollup.new(complexity, duplication, churn) if complexity || duplication
|
|
36
|
+
end
|
|
35
37
|
|
|
36
|
-
|
|
38
|
+
def churn = @churn ||= Hashira::Churn.scan
|
|
37
39
|
|
|
38
|
-
|
|
40
|
+
def enabled?(analyzer) = @enabled.include?(analyzer)
|
|
41
|
+
|
|
42
|
+
def findings = structural + listed(complexity) + listed(duplication) + listed(smells)
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def settle(packaging)
|
|
47
|
+
return packaging unless packaging == :auto
|
|
48
|
+
@project.rails? ? :namespace : :folder
|
|
49
|
+
end
|
|
39
50
|
|
|
40
|
-
|
|
51
|
+
def structural = enabled?(:coupling) ? @coupling.findings : []
|
|
41
52
|
|
|
42
|
-
|
|
53
|
+
def listed(analyzer) = analyzer ? analyzer.findings : []
|
|
43
54
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
end
|
|
55
|
+
def parse(path)
|
|
56
|
+
Prism.parse_file(path).value
|
|
57
|
+
rescue SystemCallError => error
|
|
58
|
+
raise(Hashira::Error, "cannot read #{path} (#{error.message})")
|
|
49
59
|
end
|
|
50
60
|
end
|
data/lib/hashira/project.rb
CHANGED
|
@@ -1,70 +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
|
-
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def self.default_directories
|
|
13
|
-
raise Error, "no lib/ directory here — pass the source directory explicitly" if Dir["lib/*/"].empty?
|
|
14
|
-
|
|
15
|
-
["lib"]
|
|
16
|
-
end
|
|
17
|
-
private_class_method :default_directories
|
|
6
|
+
def self.detect(directories)
|
|
7
|
+
new((directories.empty? ? defaults : directories).map { descend(it.delete_suffix("/")) })
|
|
8
|
+
end
|
|
18
9
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
22
15
|
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
25
21
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
31
27
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
35
34
|
|
|
36
|
-
|
|
37
|
-
end
|
|
35
|
+
attr_reader :directories
|
|
38
36
|
|
|
39
|
-
|
|
37
|
+
def rails? = @rails
|
|
40
38
|
|
|
41
|
-
|
|
39
|
+
def files = @directories.flat_map { Dir["#{it}/**/*.rb"] }.sort
|
|
42
40
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
end
|
|
47
|
+
def relative(path) = path.delete_prefix("#{parent(path)}/")
|
|
49
48
|
|
|
50
|
-
|
|
49
|
+
def label = @directories.join(", ")
|
|
51
50
|
|
|
52
|
-
|
|
51
|
+
def root = ROOT_PACKAGE
|
|
53
52
|
|
|
54
|
-
|
|
53
|
+
private
|
|
55
54
|
|
|
56
|
-
|
|
55
|
+
def config?(directory)
|
|
56
|
+
["config/application.rb", "../config/application.rb"].any? { File.exist?(File.expand_path(it, directory)) }
|
|
57
|
+
end
|
|
57
58
|
|
|
58
|
-
|
|
59
|
+
def folder?(path, name) = Dir.exist?("#{parent(path)}/#{name}")
|
|
59
60
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
64
66
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
end
|
|
67
|
+
def parent(path)
|
|
68
|
+
@directories.find { path.start_with?("#{it}/") } ||
|
|
69
|
+
raise(Hashira::Error, "#{path} is outside the analyzed directories")
|
|
69
70
|
end
|
|
70
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.ranked, 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}: #{Hashira::Report::Phrases.message(@finding)}")
|
|
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
|
data/lib/hashira/report/json.rb
CHANGED
|
@@ -2,47 +2,47 @@
|
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
@io = io
|
|
11
|
-
end
|
|
5
|
+
class Hashira::Report::Json
|
|
6
|
+
def initialize(view, io: $stdout)
|
|
7
|
+
@view = view
|
|
8
|
+
@io = io
|
|
9
|
+
end
|
|
12
10
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
def print
|
|
12
|
+
@io.puts(JSON.pretty_generate(payload))
|
|
13
|
+
0
|
|
14
|
+
end
|
|
17
15
|
|
|
18
|
-
|
|
16
|
+
private
|
|
19
17
|
|
|
20
|
-
|
|
18
|
+
def payload = base.merge(coupling).merge(sections.compact)
|
|
21
19
|
|
|
22
|
-
|
|
20
|
+
def base = { findings: @view.findings.all.map { rendered(it) }, accepted: accepted }
|
|
23
21
|
|
|
24
|
-
|
|
25
|
-
graph = @view.graph
|
|
26
|
-
graph ? GraphPayload.new(graph).to_h : {}
|
|
27
|
-
end
|
|
22
|
+
def rendered(finding) = finding.to_h.merge(message: Hashira::Report::Phrases.message(finding))
|
|
28
23
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
end
|
|
24
|
+
def coupling
|
|
25
|
+
graph = @view.graph
|
|
26
|
+
graph ? Hashira::Report::GraphPayload.new(graph).to_h : {}
|
|
27
|
+
end
|
|
34
28
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
29
|
+
def sections
|
|
30
|
+
{
|
|
31
|
+
complexity: (complexity if @view.complexity),
|
|
32
|
+
duplication: (duplication if @view.duplication),
|
|
33
|
+
hotspots: @view.hotspots&.files&.map(&:to_h)
|
|
34
|
+
}
|
|
35
|
+
end
|
|
38
36
|
|
|
39
|
-
|
|
37
|
+
def accepted
|
|
38
|
+
@view.findings.accepted.map { |finding, reason| rendered(finding).merge(reason:) }
|
|
39
|
+
end
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
def complexity = { methods: the_methods, classes: the_classes }
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
def the_methods = @view.complexity.ranked.map(&:to_h)
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
def the_classes = @view.complexity.classes.map(&:to_h)
|
|
46
|
+
|
|
47
|
+
def duplication = @view.duplication.clusters.map { Hashira::Duplication::Delta.new(it).to_h }
|
|
48
48
|
end
|