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.
Files changed (94) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +141 -0
  3. data/README.md +53 -5
  4. data/lib/hashira/analysis/catalog.rb +27 -0
  5. data/lib/hashira/analysis/census.rb +42 -35
  6. data/lib/hashira/analysis/constant_registry.rb +43 -0
  7. data/lib/hashira/analysis/cycle_findings.rb +34 -29
  8. data/lib/hashira/analysis/cycle_search.rb +28 -35
  9. data/lib/hashira/analysis/cycles.rb +14 -0
  10. data/lib/hashira/analysis/definition.rb +22 -0
  11. data/lib/hashira/analysis/definitions.rb +22 -18
  12. data/lib/hashira/analysis/edge.rb +4 -3
  13. data/lib/hashira/analysis/edge_map.rb +23 -26
  14. data/lib/hashira/analysis/finding.rb +7 -6
  15. data/lib/hashira/analysis/folder_placement.rb +17 -0
  16. data/lib/hashira/analysis/folding.rb +54 -0
  17. data/lib/hashira/analysis/graph.rb +35 -38
  18. data/lib/hashira/analysis/metric.rb +8 -7
  19. data/lib/hashira/analysis/namespace_placement.rb +28 -0
  20. data/lib/hashira/analysis/namespace_prefix.rb +32 -0
  21. data/lib/hashira/analysis/naming.rb +14 -0
  22. data/lib/hashira/analysis/no_folding.rb +13 -0
  23. data/lib/hashira/analysis/node_walk.rb +3 -3
  24. data/lib/hashira/analysis/placement.rb +19 -0
  25. data/lib/hashira/analysis/references.rb +14 -13
  26. data/lib/hashira/analysis/resolver.rb +28 -0
  27. data/lib/hashira/analysis/roster.rb +25 -0
  28. data/lib/hashira/analysis/rule.rb +10 -14
  29. data/lib/hashira/analysis/sdp_check.rb +8 -12
  30. data/lib/hashira/analysis/sdp_violation_findings.rb +20 -18
  31. data/lib/hashira/analysis/syntax.rb +21 -6
  32. data/lib/hashira/analysis/type_walk.rb +7 -9
  33. data/lib/hashira/churn.rb +11 -13
  34. data/lib/hashira/ci/accepted.rb +27 -31
  35. data/lib/hashira/ci/baseline.rb +22 -24
  36. data/lib/hashira/ci/diff.rb +8 -7
  37. data/lib/hashira/ci/edge_diff_report.rb +13 -17
  38. data/lib/hashira/ci/finding_diff_report.rb +14 -18
  39. data/lib/hashira/ci/gate.rb +19 -23
  40. data/lib/hashira/ci/improvement.rb +9 -14
  41. data/lib/hashira/ci/ratchet.rb +36 -27
  42. data/lib/hashira/ci/ratchet_report.rb +26 -32
  43. data/lib/hashira/cli/arguments.rb +23 -0
  44. data/lib/hashira/cli/command_line.rb +55 -71
  45. data/lib/hashira/cli/fail_on.rb +15 -20
  46. data/lib/hashira/cli/options.rb +9 -4
  47. data/lib/hashira/cli/package_by.rb +14 -0
  48. data/lib/hashira/cli/run.rb +29 -27
  49. data/lib/hashira/cli/skip.rb +14 -20
  50. data/lib/hashira/cli/usage.rb +41 -41
  51. data/lib/hashira/cli.rb +17 -19
  52. data/lib/hashira/complexity/analyzer.rb +30 -32
  53. data/lib/hashira/complexity/boolean_run.rb +11 -15
  54. data/lib/hashira/complexity/cognitive_score.rb +61 -66
  55. data/lib/hashira/complexity/if_chain.rb +33 -38
  56. data/lib/hashira/complexity/method_finding.rb +43 -48
  57. data/lib/hashira/complexity/method_score.rb +9 -7
  58. data/lib/hashira/complexity/rescue_scan.rb +16 -21
  59. data/lib/hashira/complexity/rollup.rb +13 -15
  60. data/lib/hashira/diagram/dot.rb +8 -11
  61. data/lib/hashira/diagram/mermaid.rb +11 -16
  62. data/lib/hashira/diagram/renderer.rb +10 -15
  63. data/lib/hashira/duplication/analyzer.rb +10 -14
  64. data/lib/hashira/duplication/cluster.rb +15 -12
  65. data/lib/hashira/duplication/clusterer.rb +31 -29
  66. data/lib/hashira/duplication/delta.rb +29 -30
  67. data/lib/hashira/duplication/duplication_finding.rb +19 -21
  68. data/lib/hashira/duplication/extractor.rb +15 -19
  69. data/lib/hashira/duplication/fragment.rb +19 -23
  70. data/lib/hashira/duplication/grouping.rb +12 -16
  71. data/lib/hashira/duplication/index.rb +20 -24
  72. data/lib/hashira/duplication/maximal.rb +11 -15
  73. data/lib/hashira/duplication/near_miss.rb +19 -22
  74. data/lib/hashira/duplication/sequence.rb +20 -22
  75. data/lib/hashira/duplication/similarity.rb +28 -35
  76. data/lib/hashira/duplication/union_find.rb +10 -14
  77. data/lib/hashira/duplication/variance.rb +36 -44
  78. data/lib/hashira/error.rb +4 -2
  79. data/lib/hashira/hotspots/file_cost.rb +8 -7
  80. data/lib/hashira/hotspots/rollup.rb +19 -21
  81. data/lib/hashira/pipeline.rb +36 -31
  82. data/lib/hashira/project.rb +53 -32
  83. data/lib/hashira/report/complexity_table.rb +27 -31
  84. data/lib/hashira/report/dependency_map.rb +17 -22
  85. data/lib/hashira/report/finding_lines.rb +15 -19
  86. data/lib/hashira/report/graph_payload.rb +14 -20
  87. data/lib/hashira/report/hotspot_table.rb +32 -37
  88. data/lib/hashira/report/json.rb +30 -32
  89. data/lib/hashira/report/metrics_table.rb +46 -33
  90. data/lib/hashira/report/text.rb +59 -52
  91. data/lib/hashira/version.rb +1 -1
  92. data/lib/hashira.rb +92 -55
  93. metadata +16 -2
  94. data/lib/hashira/analysis/root_namespace.rb +0 -14
@@ -1,57 +1,49 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Hashira
4
- module Duplication
5
- class Variance
6
- LITERALS = %i[integer_node float_node string_node symbol_node].freeze
7
- VALUED = %i[integer_node float_node].freeze
8
- NAMED = %i[call_node constant_read_node constant_path_node
9
- local_variable_read_node local_variable_write_node
10
- instance_variable_read_node instance_variable_write_node].freeze
11
- CONSTANTS = %i[constant_read_node constant_path_node].freeze
12
-
13
- def initialize(canonical, other)
14
- @canonical = canonical
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
- def pairs = @canonical.nodes.zip(@other.nodes)
16
+ def kinds
17
+ return [:structure] if @canonical.types != @other.types
18
+ differing.map { |node| category(node) }.uniq
19
+ end
33
20
 
34
- def named = @named ||= pairs.select { |left, _| NAMED.include?(left.type) }
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
- def differing = pairs.select { |pair| varies?(*pair) }.map(&:first)
26
+ private
37
27
 
38
- def varies?(left, right) = signature(left) != signature(right)
28
+ def pairs = @canonical.nodes.zip(@other.nodes)
39
29
 
40
- def category(node)
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
- CONSTANTS.include?(type) ? :constant : :message
45
- end
32
+ def differing = pairs.select { |pair| varies?(*pair) }.map(&:first)
46
33
 
47
- def signature(node)
48
- type = node.type
49
- return literal(node) if LITERALS.include?(type)
34
+ def varies?(left, right) = signature(left) != signature(right)
50
35
 
51
- node.name if NAMED.include?(type)
52
- end
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
- def literal(node) = VALUED.include?(node.type) ? node.value : node.unescaped
55
- end
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
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Hashira
4
- class Error < StandardError; end
3
+ class Hashira::Error < StandardError
4
+ def self.unknown(flag, value, choices)
5
+ new("unknown #{flag} #{value.inspect} (use: #{choices.join(", ")})")
6
+ end
5
7
  end
@@ -4,16 +4,17 @@ module Hashira
4
4
  module Hotspots
5
5
  CHURN_FLOOR = 1
6
6
 
7
- FileCost = Data.define(:file, :cognitive, :duplication, :churn) do
8
- def cost = cognitive + duplication
7
+ FileCost =
8
+ Data.define(:file, :cognitive, :duplication, :churn) do
9
+ def cost = cognitive + duplication
9
10
 
10
- def rank = cost * effective_churn
11
+ def rank = cost * heat
11
12
 
12
- def effective_churn = [churn, CHURN_FLOOR].max
13
+ def heat = [churn, CHURN_FLOOR].max
13
14
 
14
- def to_h = { file:, cognitive:, duplication:, churn:, cost:, rank: }
15
+ def to_h = { file:, cognitive:, duplication:, churn:, cost:, rank: }
15
16
 
16
- def cells = [file, cognitive, duplication, churn, rank]
17
- end
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
- 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.methods : []
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
@@ -2,49 +2,54 @@
2
2
 
3
3
  require "prism"
4
4
 
5
- module Hashira
6
- class Pipeline
7
- ANALYZERS = %i[coupling complexity duplication].freeze
5
+ class Hashira::Pipeline
6
+ ANALYZERS = %i[coupling complexity duplication].freeze
8
7
 
9
- RULES = [Analysis::CycleFindings, Analysis::SdpViolationFindings].freeze
8
+ RULES = [Hashira::Analysis::CycleFindings, Hashira::Analysis::SdpViolationFindings].freeze
10
9
 
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
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
- attr_reader :project, :graph
17
+ attr_reader :project, :graph
19
18
 
20
- def complexity
21
- @complexity ||= Complexity::Analyzer.new(@project, @trees) if enabled?(:complexity)
22
- end
19
+ def complexity
20
+ @complexity ||= Hashira::Complexity::Analyzer.new(@project, @trees) if enabled?(:complexity)
21
+ end
23
22
 
24
- def duplication
25
- @duplication ||= Duplication::Analyzer.new(@project, @trees, churn) if enabled?(:duplication)
26
- end
23
+ def duplication
24
+ @duplication ||= Hashira::Duplication::Analyzer.new(@project, @trees, churn) if enabled?(:duplication)
25
+ end
27
26
 
28
- def hotspots
29
- @hotspots ||= Hotspots::Rollup.new(complexity, duplication, churn) if complexity || duplication
30
- end
27
+ def hotspots
28
+ @hotspots ||= Hashira::Hotspots::Rollup.new(complexity, duplication, churn) if complexity || duplication
29
+ end
31
30
 
32
- def churn = @churn ||= Churn.from_git
31
+ def churn = @churn ||= Hashira::Churn.scan
33
32
 
34
- def enabled?(analyzer) = @enabled.include?(analyzer)
33
+ def enabled?(analyzer) = @enabled.include?(analyzer)
35
34
 
36
- def findings = coupling_findings + listed(complexity) + listed(duplication)
35
+ def findings = structural + listed(complexity) + listed(duplication)
37
36
 
38
- private
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
- def coupling_findings = enabled?(:coupling) ? RULES.flat_map { it.new(@project, @graph).list } : []
46
+ def structural = enabled?(:coupling) ? RULES.flat_map { it.new(@project, @graph).list } : []
41
47
 
42
- def listed(analyzer) = analyzer ? analyzer.findings : []
48
+ def listed(analyzer) = analyzer ? analyzer.findings : []
43
49
 
44
- def parse(path)
45
- Prism.parse_file(path).value
46
- rescue SystemCallError => error
47
- raise Error, "cannot read #{path} (#{error.message})"
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
@@ -1,50 +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
- directories.empty? ? new(default_directories) : new(directories)
9
- end
6
+ def self.detect(directories)
7
+ new((directories.empty? ? defaults : directories).map { descend(it.delete_suffix("/")) })
8
+ end
10
9
 
11
- def self.default_directories
12
- subdirectories = Dir["lib/*/"].map { File.basename(it) }
13
- raise Error, "no lib/ directory here — pass the source directory explicitly" if subdirectories.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
14
15
 
15
- subdirectories.size == 1 ? ["lib/#{subdirectories.first}"] : ["lib"]
16
- end
17
- private_class_method :default_directories
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
- def initialize(directories)
20
- missing = directories.reject { Dir.exist?(it) }
21
- raise Error, "no such directory: #{missing.join(", ")}" unless missing.empty?
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
- @directories = directories.map { it.delete_suffix("/") }
24
- end
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
- attr_reader :directories
35
+ attr_reader :directories
27
36
 
28
- def files = @directories.flat_map { Dir["#{it}/**/*.rb"] }.sort
37
+ def rails? = @rails
29
38
 
30
- def package_for(path)
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
- def relative(path) = path.delete_prefix("#{directory_of(path)}/")
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
- def label = @directories.join(", ")
47
+ def relative(path) = path.delete_prefix("#{parent(path)}/")
38
48
 
39
- def root_package = ROOT_PACKAGE
49
+ def label = @directories.join(", ")
40
50
 
41
- private
51
+ def root = ROOT_PACKAGE
42
52
 
43
- def folder?(path, name) = Dir.exist?("#{directory_of(path)}/#{name}")
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
- def directory_of(path)
46
- @directories.find { path.start_with?("#{it}/") } ||
47
- raise(Error, "#{path} is outside the analyzed directories")
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
- 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.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
- 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}: #{@finding.message}")
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