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.
Files changed (140) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +155 -0
  3. data/README.md +127 -25
  4. data/lib/hashira/analysis/finding.rb +7 -6
  5. data/lib/hashira/analysis/node_walk.rb +3 -3
  6. data/lib/hashira/analysis/syntax.rb +21 -6
  7. data/lib/hashira/analysis/type_walk.rb +7 -9
  8. data/lib/hashira/churn.rb +11 -13
  9. data/lib/hashira/ci/accepted.rb +27 -31
  10. data/lib/hashira/ci/baseline.rb +22 -24
  11. data/lib/hashira/ci/diff.rb +8 -7
  12. data/lib/hashira/ci/edge_diff_report.rb +13 -17
  13. data/lib/hashira/ci/finding_diff_report.rb +14 -18
  14. data/lib/hashira/ci/gate.rb +19 -23
  15. data/lib/hashira/ci/improvement.rb +9 -14
  16. data/lib/hashira/ci/ratchet.rb +32 -27
  17. data/lib/hashira/ci/ratchet_report.rb +26 -32
  18. data/lib/hashira/cli/arguments.rb +23 -0
  19. data/lib/hashira/cli/command_line.rb +55 -71
  20. data/lib/hashira/cli/fail_on.rb +24 -19
  21. data/lib/hashira/cli/options.rb +9 -4
  22. data/lib/hashira/cli/package_by.rb +14 -0
  23. data/lib/hashira/cli/run.rb +33 -27
  24. data/lib/hashira/cli/skip.rb +14 -20
  25. data/lib/hashira/cli/usage.rb +44 -41
  26. data/lib/hashira/cli.rb +17 -19
  27. data/lib/hashira/complexity/boolean_run.rb +13 -15
  28. data/lib/hashira/complexity/cognitive_score.rb +68 -66
  29. data/lib/hashira/complexity/if_chain.rb +33 -38
  30. data/lib/hashira/complexity/method_finding.rb +20 -42
  31. data/lib/hashira/complexity/method_score.rb +9 -7
  32. data/lib/hashira/complexity/rescue_scan.rb +16 -21
  33. data/lib/hashira/complexity/rollup.rb +13 -15
  34. data/lib/hashira/complexity/scores.rb +44 -0
  35. data/lib/hashira/coupling/audiences.rb +49 -0
  36. data/lib/hashira/coupling/catalog.rb +27 -0
  37. data/lib/hashira/coupling/census.rb +55 -0
  38. data/lib/hashira/coupling/constant_registry.rb +47 -0
  39. data/lib/hashira/coupling/cycle_findings.rb +31 -0
  40. data/lib/hashira/coupling/cycle_search.rb +37 -0
  41. data/lib/hashira/coupling/cycles.rb +14 -0
  42. data/lib/hashira/coupling/definition.rb +22 -0
  43. data/lib/hashira/coupling/definitions.rb +31 -0
  44. data/lib/hashira/coupling/edge.rb +10 -0
  45. data/lib/hashira/coupling/edge_map.rb +33 -0
  46. data/lib/hashira/coupling/folder_placement.rb +17 -0
  47. data/lib/hashira/coupling/folding.rb +56 -0
  48. data/lib/hashira/coupling/graph.rb +63 -0
  49. data/lib/hashira/coupling/metric.rb +15 -0
  50. data/lib/hashira/coupling/mixed_audience_findings.rb +32 -0
  51. data/lib/hashira/coupling/namespace_placement.rb +28 -0
  52. data/lib/hashira/{analysis → coupling}/namespace_prefix.rb +11 -11
  53. data/lib/hashira/coupling/naming.rb +14 -0
  54. data/lib/hashira/coupling/no_folding.rb +13 -0
  55. data/lib/hashira/coupling/placement.rb +19 -0
  56. data/lib/hashira/coupling/references.rb +55 -0
  57. data/lib/hashira/coupling/report.rb +28 -0
  58. data/lib/hashira/coupling/roll_call.rb +45 -0
  59. data/lib/hashira/coupling/roll_call_findings.rb +30 -0
  60. data/lib/hashira/coupling/roster.rb +25 -0
  61. data/lib/hashira/coupling/rule.rb +18 -0
  62. data/lib/hashira/coupling/scope.rb +28 -0
  63. data/lib/hashira/coupling/sdp_check.rb +14 -0
  64. data/lib/hashira/coupling/sdp_violation_findings.rb +25 -0
  65. data/lib/hashira/coupling/wide_edge_findings.rb +25 -0
  66. data/lib/hashira/coupling/words.rb +29 -0
  67. data/lib/hashira/diagram/dot.rb +8 -11
  68. data/lib/hashira/diagram/mermaid.rb +11 -16
  69. data/lib/hashira/diagram/source.rb +15 -0
  70. data/lib/hashira/duplication/clones.rb +17 -0
  71. data/lib/hashira/duplication/cluster.rb +14 -13
  72. data/lib/hashira/duplication/clusters.rb +46 -0
  73. data/lib/hashira/duplication/delta.rb +21 -30
  74. data/lib/hashira/duplication/duplication_finding.rb +17 -23
  75. data/lib/hashira/duplication/fragment.rb +19 -23
  76. data/lib/hashira/duplication/grouping.rb +12 -16
  77. data/lib/hashira/duplication/harvest.rb +27 -0
  78. data/lib/hashira/duplication/index.rb +20 -24
  79. data/lib/hashira/duplication/maximal.rb +11 -15
  80. data/lib/hashira/duplication/near_miss.rb +19 -22
  81. data/lib/hashira/duplication/sequence.rb +20 -22
  82. data/lib/hashira/duplication/similarity.rb +31 -34
  83. data/lib/hashira/duplication/union_find.rb +10 -14
  84. data/lib/hashira/duplication/variance.rb +35 -43
  85. data/lib/hashira/error.rb +4 -2
  86. data/lib/hashira/hotspots/file_cost.rb +8 -7
  87. data/lib/hashira/hotspots/rollup.rb +19 -21
  88. data/lib/hashira/pipeline.rb +41 -31
  89. data/lib/hashira/project.rb +50 -49
  90. data/lib/hashira/report/complexity_table.rb +27 -31
  91. data/lib/hashira/report/dependency_map.rb +17 -22
  92. data/lib/hashira/report/finding_lines.rb +15 -19
  93. data/lib/hashira/report/graph_payload.rb +14 -20
  94. data/lib/hashira/report/hotspot_table.rb +32 -37
  95. data/lib/hashira/report/json.rb +32 -32
  96. data/lib/hashira/report/metrics_table.rb +46 -33
  97. data/lib/hashira/report/phrases.rb +98 -0
  98. data/lib/hashira/report/smell_phrases.rb +68 -0
  99. data/lib/hashira/report/text.rb +59 -52
  100. data/lib/hashira/smells/census.rb +32 -0
  101. data/lib/hashira/smells/check.rb +30 -0
  102. data/lib/hashira/smells/conditions.rb +44 -0
  103. data/lib/hashira/smells/contexts.rb +64 -0
  104. data/lib/hashira/smells/control_parameter.rb +22 -0
  105. data/lib/hashira/smells/data_clump.rb +30 -0
  106. data/lib/hashira/smells/duplicate_method_call.rb +54 -0
  107. data/lib/hashira/smells/feature_envy.rb +17 -0
  108. data/lib/hashira/smells/instance_variable_assumption.rb +44 -0
  109. data/lib/hashira/smells/manual_dispatch.rb +16 -0
  110. data/lib/hashira/smells/module_initialize.rb +9 -0
  111. data/lib/hashira/smells/nil_check.rb +32 -0
  112. data/lib/hashira/smells/param_check.rb +51 -0
  113. data/lib/hashira/smells/refs.rb +53 -0
  114. data/lib/hashira/smells/repeated_conditional.rb +29 -0
  115. data/lib/hashira/smells/report.rb +31 -0
  116. data/lib/hashira/smells/scope.rb +29 -0
  117. data/lib/hashira/smells/too_many_instance_variables.rb +24 -0
  118. data/lib/hashira/smells/utility_function.rb +13 -0
  119. data/lib/hashira/smells/visibility.rb +64 -0
  120. data/lib/hashira/version.rb +1 -1
  121. data/lib/hashira.rb +131 -60
  122. metadata +62 -20
  123. data/lib/hashira/analysis/census.rb +0 -39
  124. data/lib/hashira/analysis/constant_registry.rb +0 -40
  125. data/lib/hashira/analysis/cycle_findings.rb +0 -33
  126. data/lib/hashira/analysis/cycle_search.rb +0 -44
  127. data/lib/hashira/analysis/definitions.rb +0 -27
  128. data/lib/hashira/analysis/edge.rb +0 -9
  129. data/lib/hashira/analysis/edge_map.rb +0 -36
  130. data/lib/hashira/analysis/graph.rb +0 -56
  131. data/lib/hashira/analysis/metric.rb +0 -14
  132. data/lib/hashira/analysis/references.rb +0 -39
  133. data/lib/hashira/analysis/rule.rb +0 -22
  134. data/lib/hashira/analysis/sdp_check.rb +0 -18
  135. data/lib/hashira/analysis/sdp_violation_findings.rb +0 -27
  136. data/lib/hashira/complexity/analyzer.rb +0 -45
  137. data/lib/hashira/diagram/renderer.rb +0 -20
  138. data/lib/hashira/duplication/analyzer.rb +0 -21
  139. data/lib/hashira/duplication/clusterer.rb +0 -50
  140. data/lib/hashira/duplication/extractor.rb +0 -31
@@ -1,33 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Hashira
4
- module Hotspots
5
- class Rollup
6
- def initialize(complexity, duplication, churn)
7
- @complexity = complexity
8
- @duplication = duplication
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
- def files = costs.reject { it.cost.zero? }.sort_by { [-it.rank, it.file] }
10
+ def files = costs.reject { it.cost.zero? }.sort_by { [-it.rank, it.file] }
13
11
 
14
- private
12
+ private
15
13
 
16
- def costs = (cognitive.keys | duplicated.keys).map { cost_for(it) }
14
+ def costs = (cognitive.keys | duplicated.keys).map { cost(it) }
17
15
 
18
- def cost_for(file)
19
- FileCost.new(file:, cognitive: cognitive[file], duplication: duplicated[file], churn: @churn.hits(file))
20
- end
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
- def scores = @complexity ? @complexity.methods : []
22
+ def scores = @complexity ? @complexity.ranked : []
23
23
 
24
- def clusters = @duplication ? @duplication.clusters : []
24
+ def clusters = @duplication ? @duplication.clusters : []
25
25
 
26
- def cognitive = @cognitive ||= per_file(scores.map { [it.file, it.cognitive] })
26
+ def cognitive = @cognitive ||= bucketed(scores.map { [it.file, it.cognitive] })
27
27
 
28
- def duplicated = @duplicated ||= per_file(clusters.flat_map(&:site_masses))
28
+ def duplicated = @duplicated ||= bucketed(clusters.flat_map(&:masses))
29
29
 
30
- def per_file(charges) = charges.each_with_object(Hash.new(0)) { |(file, cost), total| total[file] += cost }
31
- end
32
- end
30
+ def bucketed(charges) = charges.each_with_object(Hash.new(0)) { |(file, cost), total| total[file] += cost }
33
31
  end
@@ -1,50 +1,60 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "prism"
4
+ require_relative "coupling/report"
4
5
 
5
- module Hashira
6
- class Pipeline
7
- ANALYZERS = %i[coupling complexity duplication].freeze
6
+ class Hashira::Pipeline
7
+ ANALYZERS = %i[coupling complexity duplication smells].freeze
8
8
 
9
- RULES = [Analysis::CycleFindings, Analysis::SdpViolationFindings].freeze
9
+ STRUCTURAL = Hashira::Coupling::Report::RULES.map { it::KIND }.freeze
10
10
 
11
- def initialize(project, enabled: ANALYZERS)
12
- @project = project
13
- @enabled = enabled
14
- @trees = project.files.to_h { [it, parse(it)] }
15
- @graph = Analysis::Graph.new(project, @trees, Analysis::Census.new(project, @trees))
16
- end
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
- attr_reader :project, :graph
18
+ attr_reader :project
19
19
 
20
- def complexity
21
- @complexity ||= Complexity::Analyzer.new(@project, @trees) if enabled?(:complexity)
22
- end
20
+ def graph = @coupling.graph
23
21
 
24
- def duplication
25
- @duplication ||= Duplication::Analyzer.new(@project, @trees, churn) if enabled?(:duplication)
26
- end
22
+ def complexity
23
+ @complexity ||= Hashira::Complexity::Scores.new(@project, @trees) if enabled?(:complexity)
24
+ end
27
25
 
28
- def hotspots
29
- @hotspots ||= Hotspots::Rollup.new(complexity, duplication, churn) if complexity || duplication
30
- end
26
+ def duplication
27
+ @duplication ||= Hashira::Duplication::Clones.new(@project, @trees, churn) if enabled?(:duplication)
28
+ end
31
29
 
32
- def churn = @churn ||= Churn.from_git
30
+ def smells
31
+ @smells ||= Hashira::Smells::Report.new(@project, @trees) if enabled?(:smells)
32
+ end
33
33
 
34
- def enabled?(analyzer) = @enabled.include?(analyzer)
34
+ def hotspots
35
+ @hotspots ||= Hashira::Hotspots::Rollup.new(complexity, duplication, churn) if complexity || duplication
36
+ end
35
37
 
36
- def findings = coupling_findings + listed(complexity) + listed(duplication)
38
+ def churn = @churn ||= Hashira::Churn.scan
37
39
 
38
- private
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
- def coupling_findings = enabled?(:coupling) ? RULES.flat_map { it.new(@project, @graph).list } : []
51
+ def structural = enabled?(:coupling) ? @coupling.findings : []
41
52
 
42
- def listed(analyzer) = analyzer ? analyzer.findings : []
53
+ def listed(analyzer) = analyzer ? analyzer.findings : []
43
54
 
44
- def parse(path)
45
- Prism.parse_file(path).value
46
- rescue SystemCallError => error
47
- raise Error, "cannot read #{path} (#{error.message})"
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
@@ -1,70 +1,71 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Hashira
4
- class Project
5
- ROOT_PACKAGE = "(root)"
3
+ class Hashira::Project
4
+ ROOT_PACKAGE = "(root)"
6
5
 
7
- def self.detect(directories)
8
- targets = directories.empty? ? default_directories : directories
9
- new(targets.map { descend(it.delete_suffix("/")) })
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
- def self.descend(directory)
20
- child = only_subdirectory(directory)
21
- return directory unless child && Dir["#{child}/*/"].any? && (Dir["#{directory}/*.rb"] - ["#{child}.rb"]).empty?
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
- descend(child)
24
- end
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
- def self.only_subdirectory(directory)
27
- subdirectories = Dir["#{directory}/*/"]
28
- subdirectories.size == 1 ? subdirectories.first.delete_suffix("/") : nil
29
- end
30
- private_class_method :descend, :only_subdirectory
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
- def initialize(directories)
33
- missing = directories.reject { Dir.exist?(it) }
34
- raise Error, "no such directory: #{missing.join(", ")}" unless missing.empty?
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
- @directories = directories.map { it.delete_suffix("/") }
37
- end
35
+ attr_reader :directories
38
36
 
39
- attr_reader :directories
37
+ def rails? = @rails
40
38
 
41
- def files = @directories.flat_map { Dir["#{it}/**/*.rb"] }.sort
39
+ def files = @directories.flat_map { Dir["#{it}/**/*.rb"] }.sort
42
40
 
43
- def package_for(path)
44
- first, rest = relative(path).delete_suffix(".rb").split("/", 2)
45
- return ROOT_PACKAGE unless rest || folder?(path, first)
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
- contested.include?(first) ? "#{directory_of(path)}/#{first}" : first
48
- end
47
+ def relative(path) = path.delete_prefix("#{parent(path)}/")
49
48
 
50
- def relative(path) = path.delete_prefix("#{directory_of(path)}/")
49
+ def label = @directories.join(", ")
51
50
 
52
- def label = @directories.join(", ")
51
+ def root = ROOT_PACKAGE
53
52
 
54
- def root_package = ROOT_PACKAGE
53
+ private
55
54
 
56
- private
55
+ def config?(directory)
56
+ ["config/application.rb", "../config/application.rb"].any? { File.exist?(File.expand_path(it, directory)) }
57
+ end
57
58
 
58
- def folder?(path, name) = Dir.exist?("#{directory_of(path)}/#{name}")
59
+ def folder?(path, name) = Dir.exist?("#{parent(path)}/#{name}")
59
60
 
60
- def contested
61
- @contested ||= @directories.flat_map { |directory| Dir["#{directory}/*/"].map { File.basename(it) } }
62
- .tally.filter_map { |name, count| name if count > 1 }
63
- end
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
- def directory_of(path)
66
- @directories.find { path.start_with?("#{it}/") } ||
67
- raise(Error, "#{path} is outside the analyzed directories")
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
- module Hashira
4
- module Report
5
- class ComplexityTable
6
- TOP = 10
7
- METHOD_ROW = "%-44s %4s %6s %s"
8
- CLASS_ROW = "%-32s %5s %8s %6s"
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
- def initialize(complexity, io: $stdout)
11
- @complexity = complexity
12
- @io = io
13
- end
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
- private
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
- def section(scores, row_format, columns, title)
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
- def heading(row_format, columns, title)
31
- header = format(row_format, *columns)
32
- @io.puts "#{title}:\n\n"
33
- @io.puts header
34
- @io.puts "-" * header.length
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
- def ranked(scores) = scores.select { it.cognitive.positive? }.first(TOP)
38
- end
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
- module Hashira
4
- module Report
5
- class DependencyMap
6
- def initialize(graph, io: $stdout)
7
- @graph = graph
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
- private
9
+ def print
10
+ @io.puts("Dependencies (DependsUpon(refs) -> | <- UsedBy):")
11
+ @graph.packages.sort.each { @io.puts(row(it)) }
12
+ end
17
13
 
18
- def row(package)
19
- format(" %-12s -> %-32s <- %s", package,
20
- list(depends_upon(package)), list(@graph.dependents_of(package)))
21
- end
14
+ private
22
15
 
23
- def depends_upon(package)
24
- @graph.dependencies_of(package).map { "#{it}(#{@graph.weight(package, it)})" }
25
- end
16
+ def row(package)
17
+ format(" %-12s -> %-32s <- %s", package, list(outgoing(package)), list(@graph.incoming(package)))
18
+ end
26
19
 
27
- def list(items) = items.empty? ? "(none)" : items.join(", ")
28
- end
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
- module Hashira
4
- module Report
5
- class FindingLines
6
- SAMPLE = 4
3
+ class Hashira::Report::FindingLines
4
+ SAMPLE = 4
7
5
 
8
- def initialize(finding, indent: "", io: $stdout)
9
- @finding = finding
10
- @indent = indent
11
- @io = io
12
- end
6
+ def initialize(finding, indent: "", io: $stdout)
7
+ @finding = finding
8
+ @indent = indent
9
+ @io = io
10
+ end
13
11
 
14
- def print
15
- @io.puts "#{@indent}#{@finding.kind}: #{@finding.message}"
16
- @finding.evidence.first(SAMPLE).each { @io.puts "#{@indent} · #{it}" }
17
- end
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
- def print_with_overflow
20
- print
21
- overflow = @finding.evidence.size - SAMPLE
22
- @io.puts "#{@indent} · … (#{overflow} more)" if overflow.positive?
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
- module Hashira
4
- module Report
5
- class GraphPayload
6
- def initialize(graph)
7
- @graph = graph
8
- end
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
- def to_h = { packages:, edges: }
10
+ private
11
11
 
12
- private
12
+ def packages
13
+ ranked.to_h { |package, metric| [package, metric.to_h.merge(cyclic: @graph.cycles.through?(package))] }
14
+ end
13
15
 
14
- def packages
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
- def edges
23
- @graph.weighted_edges.map do |from, to, weight|
24
- { from:, to:, weight:, refs: @graph.evidence_for(from, to).to_a.sort }
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
- module Hashira
4
- module Report
5
- class HotspotTable
6
- TOP = 10
7
- ROW = "%-46s %5s %5s %6s %7s"
8
-
9
- def initialize(hotspots, io: $stdout)
10
- @hotspots = hotspots
11
- @io = io
12
- end
13
-
14
- def print
15
- return if ranked.empty?
16
-
17
- heading
18
- rows
19
- legend
20
- end
21
-
22
- private
23
-
24
- def ranked = @ranked ||= @hotspots.files.first(TOP)
25
-
26
- def rows = ranked.each { @io.puts format(ROW, *it.cells) }
27
-
28
- def heading
29
- @io.puts "Hotspots cost × churn (where refactoring pays the most):\n\n"
30
- header = format(ROW, *%w[file Cog Dup Churn Rank])
31
- @io.puts header
32
- @io.puts "-" * header.length
33
- end
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
@@ -2,47 +2,47 @@
2
2
 
3
3
  require "json"
4
4
 
5
- module Hashira
6
- module Report
7
- class Json
8
- def initialize(view, io: $stdout)
9
- @view = view
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
- def print
14
- @io.puts JSON.pretty_generate(payload)
15
- 0
16
- end
11
+ def print
12
+ @io.puts(JSON.pretty_generate(payload))
13
+ 0
14
+ end
17
15
 
18
- private
16
+ private
19
17
 
20
- def payload = base.merge(graph_payload).merge(sections.compact)
18
+ def payload = base.merge(coupling).merge(sections.compact)
21
19
 
22
- def base = { findings: @view.findings.all.map(&:to_h), accepted: accepted_entries }
20
+ def base = { findings: @view.findings.all.map { rendered(it) }, accepted: accepted }
23
21
 
24
- def graph_payload
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
- def sections
30
- { complexity: (complexity_payload if @view.complexity),
31
- duplication: (duplication_payload if @view.duplication),
32
- hotspots: @view.hotspots&.files&.map(&:to_h) }
33
- end
24
+ def coupling
25
+ graph = @view.graph
26
+ graph ? Hashira::Report::GraphPayload.new(graph).to_h : {}
27
+ end
34
28
 
35
- def accepted_entries
36
- @view.findings.accepted.map { |finding, reason| finding.to_h.merge(reason:) }
37
- end
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
- def complexity_payload = { methods: method_scores, classes: class_scores }
37
+ def accepted
38
+ @view.findings.accepted.map { |finding, reason| rendered(finding).merge(reason:) }
39
+ end
40
40
 
41
- def method_scores = @view.complexity.methods.map(&:to_h)
41
+ def complexity = { methods: the_methods, classes: the_classes }
42
42
 
43
- def class_scores = @view.complexity.classes.map(&:to_h)
43
+ def the_methods = @view.complexity.ranked.map(&:to_h)
44
44
 
45
- def duplication_payload = @view.duplication.clusters.map { Duplication::Delta.new(it).to_h }
46
- end
47
- end
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