hashira 0.6.0 → 0.7.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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +68 -0
  3. data/README.md +119 -53
  4. data/lib/hashira/analysis/finding.rb +4 -0
  5. data/lib/hashira/churn.rb +10 -2
  6. data/lib/hashira/ci/accepted.rb +1 -4
  7. data/lib/hashira/ci/baseline.rb +28 -5
  8. data/lib/hashira/ci/diff.rb +15 -4
  9. data/lib/hashira/ci/finding_diff_report.rb +10 -0
  10. data/lib/hashira/ci/gate.rb +10 -2
  11. data/lib/hashira/ci/improvement.rb +1 -1
  12. data/lib/hashira/ci/ratchet.rb +33 -8
  13. data/lib/hashira/ci/ratchet_report.rb +4 -2
  14. data/lib/hashira/ci/scope.rb +11 -0
  15. data/lib/hashira/ci/status.rb +7 -0
  16. data/lib/hashira/cli/arguments.rb +6 -1
  17. data/lib/hashira/cli/fail_on.rb +12 -2
  18. data/lib/hashira/cli/flags.rb +13 -0
  19. data/lib/hashira/cli/needs.rb +34 -0
  20. data/lib/hashira/cli/options.rb +14 -3
  21. data/lib/hashira/cli/run.rb +32 -5
  22. data/lib/hashira/cli/top.rb +13 -0
  23. data/lib/hashira/cli/usage.rb +5 -1
  24. data/lib/hashira/cli.rb +24 -5
  25. data/lib/hashira/coupling/metric.rb +7 -1
  26. data/lib/hashira/diagram/dot.rb +9 -6
  27. data/lib/hashira/diagram/mermaid.rb +11 -8
  28. data/lib/hashira/diagram/source.rb +5 -2
  29. data/lib/hashira/pipeline.rb +12 -18
  30. data/lib/hashira/project.rb +19 -5
  31. data/lib/hashira/report/columns.rb +48 -0
  32. data/lib/hashira/report/complexity_table.rb +12 -16
  33. data/lib/hashira/report/dependency_map.rb +1 -0
  34. data/lib/hashira/report/hotspot_table.rb +6 -14
  35. data/lib/hashira/report/json.rb +9 -2
  36. data/lib/hashira/report/metrics_table.rb +19 -20
  37. data/lib/hashira/report/notices.rb +37 -0
  38. data/lib/hashira/report/phrases.rb +2 -0
  39. data/lib/hashira/report/text.rb +24 -9
  40. data/lib/hashira/report/view.rb +4 -1
  41. data/lib/hashira/trees.rb +22 -0
  42. data/lib/hashira/version.rb +1 -1
  43. data/lib/hashira.rb +6 -0
  44. metadata +8 -1
@@ -1,47 +1,46 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Hashira::Report::MetricsTable
4
- def initialize(graph, io: $stdout)
4
+ TOP = 25
5
+
6
+ def initialize(graph, top: TOP, io: $stdout)
5
7
  @graph = graph
8
+ @top = top
6
9
  @io = io
7
10
  end
8
11
 
9
- LIMIT = 25
12
+ HEADERS = %w[package TC Ca Ce I Cyc].freeze
10
13
 
11
14
  def print
12
- heading
13
- kept.each { |package, metric| @io.puts(row(package, metric)) }
15
+ Hashira::Report::Columns.new(HEADERS, kept.map { |package, metric| row(package, metric) }, io: @io).print
14
16
  note unless spared.empty?
17
+ more unless over.zero?
15
18
  legend
16
19
  end
17
20
 
18
21
  private
19
22
 
20
- def ranked = @graph.metrics.sort_by { |_package, metric| metric.instability }
23
+ def ranked = @graph.metrics.sort_by { |_package, metric| metric.order }
21
24
 
22
- def split = @split ||= (ranked.size > LIMIT ? ranked.partition { |_package, metric| !leaf?(metric) } : [ranked, []])
25
+ def split = @split ||= (ranked.size > @top ? ranked.partition { |_package, metric| !leaf?(metric) } : [ranked, []])
23
26
 
24
- def kept = split.first
27
+ def kept = split.first.first(@top)
25
28
 
26
29
  def spared = split.last
27
30
 
31
+ def over = split.first.size - kept.size
32
+
28
33
  def leaf?(metric) = metric.types <= 1 && metric.efferent.zero? && metric.afferent <= 1
29
34
 
30
- def note
31
- @io.puts(" + #{spared.size} single-type leaf packages (TC ≤ 1, Ca ≤ 1, Ce = 0) — hidden for brevity")
32
- end
35
+ def note = @io.puts(" + #{leaves} (TC ≤ 1, Ca ≤ 1, Ce = 0) — hidden for brevity")
33
36
 
34
- def heading
35
- @io.puts(format("%-12s %3s %3s %3s %5s %-3s", *%w[package TC Ca Ce I Cyc]))
36
- @io.puts("-" * 40)
37
- end
37
+ def leaves = Hashira::Report::Phrases.count(spared.size, "single-type leaf package")
38
38
 
39
- def row(package, metric)
40
- format(
41
- "%-12s %3d %3d %3d %5.2f %-3s", package, *metric.to_h.values,
42
- (@graph.cycles.through?(package) ? "YES" : "-")
43
- )
44
- end
39
+ def more = @io.puts(" … and #{over} more — raise the cap with --top, or read them all with --json")
40
+
41
+ def row(package, metric) = [package, *metric.cells, cyc(package)]
42
+
43
+ def cyc(package) = @graph.cycles.through?(package) ? "YES" : "-"
45
44
 
46
45
  def legend
47
46
  @io.puts("\nLegend: TC total types, Ca afferent (incoming), Ce efferent (outgoing),")
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Hashira::Report::Notices
4
+ ISSUES = "https://github.com/giacope/hashira/issues"
5
+
6
+ def initialize(io: $stderr)
7
+ @io = io
8
+ end
9
+
10
+ def scanning(files) = interactive { @io.puts("hashira: reading #{files} files…") }
11
+
12
+ def finished(files, seconds) = interactive { @io.puts("hashira: #{files} files in #{seconds}s") }
13
+
14
+ def rails
15
+ @io.puts("hashira: this looks like a Rails root — `hashira app` reads the application, not just lib/")
16
+ end
17
+
18
+ def churn(label)
19
+ @io.puts("hashira: no git history for #{label} — hotspots are ranked by cost alone")
20
+ end
21
+
22
+ def crashed(error)
23
+ @io.puts("hashira: internal error — #{error.class}: #{error.message}")
24
+ @io.puts(" at #{error.backtrace.first}")
25
+ @io.puts(" this is a bug in hashira — please report it at #{ISSUES}")
26
+ end
27
+
28
+ def unparsed(count, sample)
29
+ @io.puts("hashira: #{count} of the files did not parse — #{sample}")
30
+ end
31
+
32
+ private
33
+
34
+ def interactive
35
+ yield if @io.tty?
36
+ end
37
+ end
@@ -81,6 +81,8 @@ module Hashira::Report::Phrases
81
81
 
82
82
  def score(value) = format("%.2f", value)
83
83
 
84
+ def count(number, noun) = "#{number} #{number == 1 ? noun : "#{noun}s"}"
85
+
84
86
  def clause(part) = "#{part[:users].join(", ")} #{verb(part)} #{part[:constants].join(", ")}"
85
87
 
86
88
  def verb(part)
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Hashira::Report::Text
4
+ FINDINGS = 25
5
+
4
6
  def initialize(view, io: $stdout)
5
7
  @view = view
6
8
  @io = io
@@ -19,31 +21,37 @@ class Hashira::Report::Text
19
21
  def coupling
20
22
  graph = @view.graph
21
23
  header(graph)
22
- Hashira::Report::MetricsTable.new(graph, io: @io).print
24
+ table(Hashira::Report::MetricsTable, graph)
23
25
  Hashira::Report::DependencyMap.new(graph, io: @io).print
24
26
  folded(graph.folds)
25
27
  end
26
28
 
27
29
  def folded(folds)
28
30
  return if folds.empty?
29
- @io.puts("\nFolded (single-type classes joined to their base or domain):")
31
+ @io.puts("Folded (single-type classes joined to their base or domain):")
30
32
  folds.each { @io.puts(" #{it[:from]} -> #{it[:to]} (#{it[:via]})") }
33
+ @io.puts
31
34
  end
32
35
 
33
- def complexity = Hashira::Report::ComplexityTable.new(@view.complexity, io: @io).print
36
+ def table(kind, subject) = kind.new(subject, top: @view.top || kind::TOP, io: @io).print
37
+
38
+ def complexity = table(Hashira::Report::ComplexityTable, @view.complexity)
34
39
 
35
- def hotspots = Hashira::Report::HotspotTable.new(@view.hotspots, io: @io).print
40
+ def hotspots = table(Hashira::Report::HotspotTable, @view.hotspots)
36
41
 
37
42
  def header(graph)
38
43
  packages = graph.packages.size
39
- @io.puts(banner(packages))
44
+ @io.puts(banner(graph.packaging, packages))
40
45
  caveat if packages == 1
41
46
  end
42
47
 
43
- def banner(packages)
44
- "Package (layer) metrics for #{@view.project.label} (#{packages} packages, #{total} files)\n\n"
48
+ def banner(grouping, packages)
49
+ "Package (#{grouping}) metrics for #{@view.project.label} " \
50
+ "(#{count(packages, "package")}, #{count(total, "file")})\n\n"
45
51
  end
46
52
 
53
+ def count(number, noun) = Hashira::Report::Phrases.count(number, noun)
54
+
47
55
  def total = @view.project.files.size
48
56
 
49
57
  def caveat
@@ -55,7 +63,7 @@ class Hashira::Report::Text
55
63
 
56
64
  def findings
57
65
  all = @view.findings.all
58
- @io.puts("\nFindings (#{all.size}):")
66
+ @io.puts("Findings (#{all.size}):")
59
67
  list(all)
60
68
  accepted
61
69
  @io.puts("\n Full evidence + machine format: hashira --json") unless all.empty?
@@ -63,7 +71,14 @@ class Hashira::Report::Text
63
71
 
64
72
  def list(all)
65
73
  return @io.puts(" none ✓ — structure is healthy") if all.empty?
66
- all.each { Hashira::Report::FindingLines.new(it, indent: " ", io: @io).emit }
74
+ shown = all.first(@view.top || FINDINGS)
75
+ shown.each { Hashira::Report::FindingLines.new(it, indent: " ", io: @io).emit }
76
+ elided(all.size - shown.size)
77
+ end
78
+
79
+ def elided(rest)
80
+ return if rest.zero?
81
+ @io.puts(" … and #{rest} more — raise the cap with --top, or read them all with --json")
67
82
  end
68
83
 
69
84
  def accepted
@@ -2,6 +2,9 @@
2
2
 
3
3
  module Hashira
4
4
  module Report
5
- View = Data.define(:project, :graph, :complexity, :duplication, :hotspots, :findings)
5
+ View =
6
+ Data.define(:project, :graph, :complexity, :duplication, :hotspots, :findings, :top, :compact) do
7
+ def initialize(top: nil, compact: nil, **) = super
8
+ end
6
9
  end
7
10
  end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ class Hashira::Trees
6
+ def initialize(project)
7
+ @unparsed = []
8
+ @all = project.files.to_h { [it, parse(it)] }
9
+ end
10
+
11
+ attr_reader :all, :unparsed
12
+
13
+ private
14
+
15
+ def parse(path)
16
+ result = Prism.parse_file(path)
17
+ @unparsed << path if result.failure?
18
+ result.value
19
+ rescue SystemCallError => error
20
+ raise(Hashira::Error, "cannot read #{path} (#{error.message})")
21
+ end
22
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hashira
4
- VERSION = "0.6.0"
4
+ VERSION = "0.7.0"
5
5
  end
data/lib/hashira.rb CHANGED
@@ -43,6 +43,8 @@ require_relative "hashira/ci/gate"
43
43
  require_relative "hashira/ci/improvement"
44
44
  require_relative "hashira/ci/ratchet"
45
45
  require_relative "hashira/ci/ratchet_report"
46
+ require_relative "hashira/ci/scope"
47
+ require_relative "hashira/ci/status"
46
48
  require_relative "hashira/cli"
47
49
  require_relative "hashira/cli/arguments"
48
50
  require_relative "hashira/cli/command_line"
@@ -50,6 +52,7 @@ require_relative "hashira/cli/fail_on"
50
52
  require_relative "hashira/cli/flag"
51
53
  require_relative "hashira/cli/flags"
52
54
  require_relative "hashira/cli/format"
55
+ require_relative "hashira/cli/needs"
53
56
  require_relative "hashira/cli/options"
54
57
  require_relative "hashira/cli/package_by"
55
58
  require_relative "hashira/cli/run"
@@ -118,6 +121,7 @@ require_relative "hashira/hotspots/file_cost"
118
121
  require_relative "hashira/hotspots/rollup"
119
122
  require_relative "hashira/pipeline"
120
123
  require_relative "hashira/project"
124
+ require_relative "hashira/report/columns"
121
125
  require_relative "hashira/report/complexity_table"
122
126
  require_relative "hashira/report/dependency_map"
123
127
  require_relative "hashira/report/finding_lines"
@@ -125,6 +129,7 @@ require_relative "hashira/report/graph_payload"
125
129
  require_relative "hashira/report/hotspot_table"
126
130
  require_relative "hashira/report/json"
127
131
  require_relative "hashira/report/metrics_table"
132
+ require_relative "hashira/report/notices"
128
133
  require_relative "hashira/report/phrases"
129
134
  require_relative "hashira/report/smell_phrases"
130
135
  require_relative "hashira/report/text"
@@ -149,4 +154,5 @@ require_relative "hashira/smells/scope"
149
154
  require_relative "hashira/smells/too_many_instance_variables"
150
155
  require_relative "hashira/smells/utility_function"
151
156
  require_relative "hashira/smells/visibility"
157
+ require_relative "hashira/trees"
152
158
  require_relative "hashira/version"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hashira
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Giacomo GK
@@ -40,6 +40,8 @@ files:
40
40
  - lib/hashira/ci/improvement.rb
41
41
  - lib/hashira/ci/ratchet.rb
42
42
  - lib/hashira/ci/ratchet_report.rb
43
+ - lib/hashira/ci/scope.rb
44
+ - lib/hashira/ci/status.rb
43
45
  - lib/hashira/cli.rb
44
46
  - lib/hashira/cli/arguments.rb
45
47
  - lib/hashira/cli/command_line.rb
@@ -47,10 +49,12 @@ files:
47
49
  - lib/hashira/cli/flag.rb
48
50
  - lib/hashira/cli/flags.rb
49
51
  - lib/hashira/cli/format.rb
52
+ - lib/hashira/cli/needs.rb
50
53
  - lib/hashira/cli/options.rb
51
54
  - lib/hashira/cli/package_by.rb
52
55
  - lib/hashira/cli/run.rb
53
56
  - lib/hashira/cli/skip.rb
57
+ - lib/hashira/cli/top.rb
54
58
  - lib/hashira/cli/usage.rb
55
59
  - lib/hashira/complexity/boolean_run.rb
56
60
  - lib/hashira/complexity/cognitive_score.rb
@@ -115,6 +119,7 @@ files:
115
119
  - lib/hashira/hotspots/rollup.rb
116
120
  - lib/hashira/pipeline.rb
117
121
  - lib/hashira/project.rb
122
+ - lib/hashira/report/columns.rb
118
123
  - lib/hashira/report/complexity_table.rb
119
124
  - lib/hashira/report/dependency_map.rb
120
125
  - lib/hashira/report/finding_lines.rb
@@ -122,6 +127,7 @@ files:
122
127
  - lib/hashira/report/hotspot_table.rb
123
128
  - lib/hashira/report/json.rb
124
129
  - lib/hashira/report/metrics_table.rb
130
+ - lib/hashira/report/notices.rb
125
131
  - lib/hashira/report/phrases.rb
126
132
  - lib/hashira/report/smell_phrases.rb
127
133
  - lib/hashira/report/text.rb
@@ -149,6 +155,7 @@ files:
149
155
  - lib/hashira/smells/too_many_instance_variables.rb
150
156
  - lib/hashira/smells/utility_function.rb
151
157
  - lib/hashira/smells/visibility.rb
158
+ - lib/hashira/trees.rb
152
159
  - lib/hashira/version.rb
153
160
  homepage: https://github.com/giacope/hashira
154
161
  licenses: