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
@@ -2,39 +2,37 @@
2
2
 
3
3
  require "json"
4
4
 
5
- module Hashira
6
- module CI
7
- class Baseline
8
- SCHEMA_VERSION = 2
5
+ class Hashira::CI::Baseline
6
+ SCHEMA_VERSION = 3
9
7
 
10
- def self.load(path) = new(path, File.exist?(path) ? JSON.parse(File.read(path)) : {})
8
+ def self.load(path) = new(path, File.exist?(path) ? JSON.parse(File.read(path)) : {})
11
9
 
12
- def initialize(path, recorded)
13
- @path = path
14
- @recorded = recorded
15
- end
10
+ def initialize(path, recorded)
11
+ @path = path
12
+ @recorded = recorded
13
+ end
14
+
15
+ attr_reader :path
16
16
 
17
- attr_reader :path
17
+ def exist? = File.exist?(@path)
18
18
 
19
- def exist? = File.exist?(@path)
19
+ def edges = @recorded.fetch("edges", [])
20
20
 
21
- def edges = @recorded.fetch("edges", [])
21
+ def findings = @recorded.fetch("findings", [])
22
22
 
23
- def findings = @recorded.fetch("findings", [])
23
+ def findings? = @recorded.key?("findings")
24
24
 
25
- def ratchets_findings? = @recorded.key?("findings")
25
+ def packaging = @recorded.fetch("packaging", "folder")
26
26
 
27
- def write(edges, findings)
28
- File.write(@path, JSON.pretty_generate(payload(edges, findings)) << "\n")
29
- end
27
+ def write(edges, findings, packaging:)
28
+ File.write(@path, JSON.pretty_generate(payload(edges, findings, packaging)) << "\n")
29
+ end
30
30
 
31
- private
31
+ private
32
32
 
33
- def payload(edges, findings)
34
- base = { version: SCHEMA_VERSION, edges:, findings: }
35
- accepted = Accepted.new(@recorded.fetch("accepted", [])).entries
36
- accepted.empty? ? base : base.merge(accepted:)
37
- end
38
- end
33
+ def payload(edges, findings, packaging)
34
+ base = { version: SCHEMA_VERSION, packaging:, edges:, findings: }
35
+ accepted = Hashira::CI::Accepted.new(@recorded.fetch("accepted", [])).entries
36
+ accepted.empty? ? base : base.merge(accepted:)
39
37
  end
40
38
  end
@@ -2,14 +2,15 @@
2
2
 
3
3
  module Hashira
4
4
  module CI
5
- Diff = Data.define(:added, :removed) do
6
- def self.between(current, recorded)
7
- new(added: current - recorded, removed: recorded - current)
8
- end
5
+ Diff =
6
+ Data.define(:added, :removed) do
7
+ def self.between(current, recorded)
8
+ new(added: current - recorded, removed: recorded - current)
9
+ end
9
10
 
10
- def empty? = added.empty? && removed.empty?
11
+ def empty? = added.empty? && removed.empty?
11
12
 
12
- def worse? = !added.empty?
13
- end
13
+ def worse? = !added.empty?
14
+ end
14
15
  end
15
16
  end
@@ -1,24 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Hashira
4
- module CI
5
- class EdgeDiffReport
6
- def initialize(graph, io: $stdout)
7
- @graph = graph
8
- @io = io
9
- end
3
+ class Hashira::CI::EdgeDiffReport
4
+ def initialize(graph, io: $stdout)
5
+ @graph = graph
6
+ @io = io
7
+ end
10
8
 
11
- def print(diff)
12
- diff.added.each { print_edge(it) }
13
- Improvement.new("Edges removed", io: @io).print(diff.removed)
14
- end
9
+ def print(diff)
10
+ diff.added.each { emit(it) }
11
+ Hashira::CI::Improvement.new("Edges removed", io: @io).print(diff.removed)
12
+ end
15
13
 
16
- private
14
+ private
17
15
 
18
- def print_edge(edge)
19
- @io.puts "NEW EDGE #{edge} — introduced by:"
20
- @graph.evidence_for(edge.from, edge.to).to_a.sort.each { @io.puts " · #{it}" }
21
- end
22
- end
16
+ def emit(edge)
17
+ @io.puts("NEW EDGE #{edge} — introduced by:")
18
+ @graph.evidence(edge.from, edge.to).to_a.sort.each { @io.puts(" · #{it}") }
23
19
  end
24
20
  end
@@ -1,26 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Hashira
4
- module CI
5
- class FindingDiffReport
6
- def initialize(findings, io: $stdout)
7
- @findings = findings
8
- @io = io
9
- end
3
+ class Hashira::CI::FindingDiffReport
4
+ def initialize(findings, io: $stdout)
5
+ @findings = findings
6
+ @io = io
7
+ end
10
8
 
11
- def print(diff)
12
- introduced(diff.added).each { print_finding(it) }
13
- Improvement.new("Findings resolved", io: @io).print(diff.removed)
14
- end
9
+ def print(diff)
10
+ introduced(diff.added).each { emit(it) }
11
+ Hashira::CI::Improvement.new("Findings resolved", io: @io).print(diff.removed)
12
+ end
15
13
 
16
- private
14
+ private
17
15
 
18
- def introduced(added) = added.filter_map { |signature| @findings.find { it.signature == signature } }
16
+ def introduced(added) = added.filter_map { |signature| @findings.find { it.signature == signature } }
19
17
 
20
- def print_finding(finding)
21
- @io.puts "NEW FINDING:"
22
- Report::FindingLines.new(finding, indent: " ", io: @io).print_with_overflow
23
- end
24
- end
18
+ def emit(finding)
19
+ @io.puts("NEW FINDING:")
20
+ Hashira::Report::FindingLines.new(finding, indent: " ", io: @io).emit
25
21
  end
26
22
  end
@@ -1,31 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Hashira
4
- module CI
5
- class Gate
6
- def initialize(findings, kinds, io: $stdout)
7
- @findings = findings
8
- @kinds = kinds
9
- @io = io
10
- end
3
+ class Hashira::CI::Gate
4
+ def initialize(findings, kinds, io: $stdout)
5
+ @findings = findings
6
+ @kinds = kinds
7
+ @io = io
8
+ end
11
9
 
12
- def check
13
- offending = @findings.all.select { @kinds.include?(it.kind) }
14
- offending.empty? ? report_clean : report_failure(offending)
15
- end
10
+ def check
11
+ offending = @findings.all.select { @kinds.include?(it.kind) }
12
+ offending.empty? ? clean : failure(offending)
13
+ end
16
14
 
17
- private
15
+ private
18
16
 
19
- def report_failure(offending)
20
- offending.each { Report::FindingLines.new(it, io: @io).print }
21
- @io.puts "\nGate FAILED: #{offending.size} finding(s) of kind #{@kinds.join(", ")}."
22
- 1
23
- end
17
+ def failure(offending)
18
+ offending.each { Hashira::Report::FindingLines.new(it, io: @io).print }
19
+ @io.puts("\nGate FAILED: #{offending.size} finding(s) of kind #{@kinds.join(", ")}.")
20
+ 1
21
+ end
24
22
 
25
- def report_clean
26
- @io.puts "Gate OK: no findings of kind #{@kinds.join(", ")}."
27
- 0
28
- end
29
- end
23
+ def clean
24
+ @io.puts("Gate OK: no findings of kind #{@kinds.join(", ")}.")
25
+ 0
30
26
  end
31
27
  end
@@ -1,19 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Hashira
4
- module CI
5
- class Improvement
6
- def initialize(label, io: $stdout)
7
- @label = label
8
- @io = io
9
- end
10
-
11
- def print(removed)
12
- return if removed.empty?
3
+ class Hashira::CI::Improvement
4
+ def initialize(label, io: $stdout)
5
+ @label = label
6
+ @io = io
7
+ end
13
8
 
14
- @io.puts "#{@label} (improvement!): #{removed.join(", ")}"
15
- @io.puts "Lock it in: hashira --update-baseline"
16
- end
17
- end
9
+ def print(removed)
10
+ return if removed.empty?
11
+ @io.puts("#{@label} (improvement!): #{removed.join(", ")}")
12
+ @io.puts("Lock it in: hashira --update-baseline")
18
13
  end
19
14
  end
@@ -1,40 +1,45 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Hashira
4
- module CI
5
- class Ratchet
6
- def initialize(graph, findings, baseline_path, io: $stdout)
7
- @graph = graph
8
- @findings = findings
9
- @baseline = Baseline.load(baseline_path)
10
- @io = io
11
- end
3
+ class Hashira::CI::Ratchet
4
+ def initialize(graph, findings, baseline_path, io: $stdout)
5
+ @graph = graph
6
+ @findings = findings
7
+ @baseline = Hashira::CI::Baseline.load(baseline_path)
8
+ @io = io
9
+ end
10
+
11
+ def update
12
+ @baseline.write(edges, digests, packaging:)
13
+ @io.puts("Baseline updated: #{edges.size} edges, #{digests.size} findings.")
14
+ 0
15
+ end
12
16
 
13
- def update
14
- @baseline.write(edge_signatures, finding_signatures)
15
- @io.puts "Baseline updated: #{edge_signatures.size} edges, #{finding_signatures.size} findings."
16
- 0
17
- end
17
+ def check = Hashira::CI::RatchetReport.new(@graph, @findings, io: @io).print(drift, delta)
18
18
 
19
- def check
20
- raise Error, "no baseline at #{@baseline.path} — run --update-baseline first" unless @baseline.exist?
19
+ def blocker
20
+ return "no baseline at #{@baseline.path} — run --update-baseline first" unless @baseline.exist?
21
+ mismatch unless @baseline.packaging == packaging
22
+ end
23
+
24
+ private
21
25
 
22
- RatchetReport.new(@graph, @findings, io: @io).print(edge_diff, finding_diff)
23
- end
26
+ def packaging = @graph.packaging.to_s
24
27
 
25
- private
28
+ def mismatch
29
+ recorded = @baseline.packaging
30
+ "baseline #{@baseline.path} was recorded with --package-by #{recorded}, but this run " \
31
+ "uses #{packaging} — rerun with --package-by #{recorded}, or refresh it with --update-baseline"
32
+ end
26
33
 
27
- def edge_signatures = @graph.edge_list.map(&:to_s)
34
+ def edges = @graph.edges.map(&:to_s)
28
35
 
29
- def finding_signatures = @findings.map(&:signature).uniq.sort
36
+ def digests = @findings.map(&:signature).uniq.sort
30
37
 
31
- def edge_diff = Diff.new(added: new_edges, removed: @baseline.edges - edge_signatures)
38
+ def drift = Hashira::CI::Diff.new(added: fresh, removed: @baseline.edges - edges)
32
39
 
33
- def new_edges = @graph.edge_list.reject { @baseline.edges.include?(it.to_s) }
40
+ def fresh = @graph.edges.reject { @baseline.edges.include?(it.to_s) }
34
41
 
35
- def finding_diff
36
- Diff.between(finding_signatures, @baseline.findings) if @baseline.ratchets_findings?
37
- end
38
- end
42
+ def delta
43
+ Hashira::CI::Diff.between(digests, @baseline.findings) if @baseline.findings?
39
44
  end
40
45
  end
@@ -1,42 +1,36 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Hashira
4
- module CI
5
- class RatchetReport
6
- def initialize(graph, findings, io: $stdout)
7
- @graph = graph
8
- @findings = findings
9
- @io = io
10
- end
11
-
12
- def print(edges, findings)
13
- return unchanged if quiet?(edges, findings)
14
-
15
- details(edges, findings)
16
- advice(edges, findings)
17
- 1
18
- end
3
+ class Hashira::CI::RatchetReport
4
+ def initialize(graph, findings, io: $stdout)
5
+ @graph = graph
6
+ @findings = findings
7
+ @io = io
8
+ end
19
9
 
20
- private
10
+ def print(edges, findings)
11
+ return unchanged if quiet?(edges, findings)
12
+ details(edges, findings)
13
+ advice(edges, findings)
14
+ 1
15
+ end
21
16
 
22
- def quiet?(*diffs) = diffs.compact.all?(&:empty?)
17
+ private
23
18
 
24
- def unchanged
25
- @io.puts "Ratchet OK: #{@graph.edge_list.size} edges, #{@findings.size} findings, unchanged."
26
- 0
27
- end
19
+ def quiet?(*diffs) = diffs.compact.all?(&:empty?)
28
20
 
29
- def details(edges, findings)
30
- EdgeDiffReport.new(@graph, io: @io).print(edges)
31
- FindingDiffReport.new(@findings, io: @io).print(findings) if findings
32
- end
21
+ def unchanged
22
+ @io.puts("Ratchet OK: #{@graph.edges.size} edges, #{@findings.size} findings, unchanged.")
23
+ 0
24
+ end
33
25
 
34
- def advice(*diffs)
35
- return unless diffs.compact.any?(&:worse?)
26
+ def details(edges, findings)
27
+ Hashira::CI::EdgeDiffReport.new(@graph, io: @io).print(edges)
28
+ Hashira::CI::FindingDiffReport.new(@findings, io: @io).print(findings) if findings
29
+ end
36
30
 
37
- @io.puts "\nRatchet FAILED. Either fix what regressed, or — if it is deliberate —"
38
- @io.puts "record the decision: update the baseline, or accept it with a reason."
39
- end
40
- end
31
+ def advice(*diffs)
32
+ return unless diffs.compact.any?(&:worse?)
33
+ @io.puts("\nRatchet FAILED. Either fix what regressed, or — if it is deliberate —")
34
+ @io.puts("record the decision: update the baseline, or accept it with a reason.")
41
35
  end
42
36
  end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Hashira::CLI::Arguments
4
+ def initialize(argv)
5
+ @argv = argv.dup
6
+ end
7
+
8
+ def take(flag, default)
9
+ position = @argv.index(flag)
10
+ return default unless position
11
+ _flag, value = @argv.slice!(position, 2)
12
+ raise(Hashira::Error, "#{flag} needs a value") unless value
13
+ value
14
+ end
15
+
16
+ def delete(flag) = @argv.delete(flag)
17
+
18
+ def rest
19
+ stray = @argv.find { it.start_with?("-") }
20
+ raise(Hashira::Error, "unknown option #{stray}") if stray
21
+ @argv
22
+ end
23
+ end
@@ -1,92 +1,76 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Hashira
4
- class CLI
5
- class CommandLine
6
- DEFAULT_BASELINE = "hashira_baseline.json"
3
+ class Hashira::CLI::CommandLine
4
+ DEFAULT_BASELINE = "hashira_baseline.json"
7
5
 
8
- FORMATS = %w[text json dot mermaid].freeze
6
+ FORMATS = %w[text json dot mermaid].freeze
9
7
 
10
- CI_FLAGS = { "--update-baseline" => :update_baseline, "--ratchet" => :ratchet }.freeze
8
+ CI_FLAGS = { "--update-baseline" => :update, "--ratchet" => :ratchet }.freeze
11
9
 
12
- def initialize(argv)
13
- @arguments = argv.dup
14
- end
15
-
16
- def options = usage_options || parsed_options
17
-
18
- private
19
-
20
- def usage_options
21
- return usage(:help) if delete("--help") || delete("-h")
22
-
23
- usage(:version) if delete("--version")
24
- end
10
+ def initialize(argv)
11
+ @arguments = Hashira::CLI::Arguments.new(argv)
12
+ end
25
13
 
26
- def usage(mode) = Options.new(directories: [], mode:, baseline: nil, fail_on: [], skip: [])
14
+ def options = shortcuts || parsed
27
15
 
28
- def parsed_options
29
- values = flag_values
30
- mode = parse_mode(values[:fail_on])
31
- reject_unknown_flags
32
- Options.new(directories: @arguments, mode:, **values)
33
- end
16
+ private
34
17
 
35
- def flag_values
36
- { skip: Skip.parse(take_value("--skip")),
37
- fail_on: FailOn.parse(take_value("--fail-on")),
38
- baseline: take_value("--baseline") || DEFAULT_BASELINE }
39
- end
18
+ def shortcuts
19
+ return usage(:help) if delete("--help") || delete("-h")
20
+ usage(:version) if delete("--version")
21
+ end
40
22
 
41
- def parse_mode(fail_on)
42
- case (requested = requested_modes(fail_on).uniq(&:last))
43
- in [] then :text
44
- in [[_flag, mode]] then mode
45
- else conflict(requested)
46
- end
47
- end
23
+ def usage(mode)
24
+ Hashira::CLI::Options.new(directories: [], mode:, baseline: nil, fail_on: [], skip: [], packaging: :auto)
25
+ end
48
26
 
49
- def conflict(requested)
50
- raise Error, "conflicting options: #{requested.map(&:first).join(" and ")}"
51
- end
27
+ def parsed
28
+ values = flags
29
+ values[:mode] = mode(values[:fail_on])
30
+ Hashira::CLI::Options.new(directories: @arguments.rest, **values)
31
+ end
52
32
 
53
- def requested_modes(fail_on)
54
- ci = CI_FLAGS.filter_map { |flag, mode| [flag, mode] if delete(flag) }
55
- ci + fail_on_mode(fail_on) + format_modes
56
- end
33
+ def flags
34
+ {
35
+ skip: Hashira::CLI::Skip.parse(take("--skip", "")),
36
+ fail_on: Hashira::CLI::FailOn.parse(take("--fail-on", "")),
37
+ baseline: take("--baseline", DEFAULT_BASELINE), packaging: grouping
38
+ }
39
+ end
57
40
 
58
- def fail_on_mode(fail_on) = fail_on.empty? ? [] : [["--fail-on", :fail_on]]
41
+ def grouping = Hashira::CLI::PackageBy.parse(take("--package-by", ""))
59
42
 
60
- def format_modes
61
- format = take_format
62
- modes = format ? [["--format #{format}", format.to_sym]] : []
63
- delete("--json") ? modes + [["--json", :json]] : modes
64
- end
43
+ def mode(fail_on)
44
+ case (asked = requested(fail_on).uniq(&:last))
45
+ in [] then :text
46
+ in [[_flag, mode]] then mode
47
+ else conflict(asked)
48
+ end
49
+ end
65
50
 
66
- def take_format
67
- format = take_value("--format")
68
- return format unless format
69
- raise Error, "unknown --format #{format.inspect} (use: #{FORMATS.join(", ")})" unless FORMATS.include?(format)
51
+ def conflict(asked)
52
+ raise(Hashira::Error, "conflicting options: #{asked.map(&:first).join(" and ")}")
53
+ end
70
54
 
71
- format
72
- end
55
+ def requested(fail_on)
56
+ CI_FLAGS.filter_map { |flag, mode| [flag, mode] if delete(flag) } + gate(fail_on) + formats
57
+ end
73
58
 
74
- def take_value(flag)
75
- position = @arguments.index(flag)
76
- return nil unless position
59
+ def gate(fail_on) = fail_on.empty? ? [] : [["--fail-on", :fail_on]]
77
60
 
78
- _flag, value = @arguments.slice!(position, 2)
79
- raise Error, "#{flag} needs a value" unless value
61
+ def formats
62
+ chosen = format
63
+ modes = chosen.empty? ? [] : [["--format #{chosen}", chosen.to_sym]]
64
+ delete("--json") ? modes + [["--json", :json]] : modes
65
+ end
80
66
 
81
- value
82
- end
67
+ def format
68
+ wanted = take("--format", "")
69
+ return wanted if wanted.empty? || FORMATS.include?(wanted)
70
+ raise(Hashira::Error.unknown("--format", wanted, FORMATS))
71
+ end
83
72
 
84
- def delete(flag) = @arguments.delete(flag)
73
+ def take(flag, default) = @arguments.take(flag, default)
85
74
 
86
- def reject_unknown_flags
87
- stray = @arguments.find { it.start_with?("-") }
88
- raise Error, "unknown option #{stray}" if stray
89
- end
90
- end
91
- end
75
+ def delete(flag) = @arguments.delete(flag)
92
76
  end
@@ -1,28 +1,33 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Hashira
4
- class CLI
5
- module FailOn
6
- KINDS = {
7
- "cycles" => "cycle", "cycle" => "cycle",
8
- "sdp" => "sdp_violation", "sdp_violation" => "sdp_violation",
9
- "complexity" => "complexity",
10
- "duplication" => "duplication", "dupe" => "duplication"
11
- }.freeze
3
+ require_relative "../pipeline"
12
4
 
13
- module_function
5
+ module Hashira::CLI::FailOn
6
+ SMELLS = %w[
7
+ control_parameter data_clump duplicate_method_call feature_envy instance_variable_assumption
8
+ manual_dispatch module_initialize nil_check repeated_conditional too_many_instance_variables
9
+ utility_function
10
+ ].freeze
14
11
 
15
- def parse(list)
16
- return [] unless list
12
+ MEASURES = (Hashira::Pipeline::ANALYZERS - %i[coupling smells]).map(&:to_s).freeze
17
13
 
18
- list.split(",").map { kind(it.strip) }.uniq
19
- end
14
+ KINDS = {
15
+ "cycles" => "cycle", "sdp" => "sdp_violation", "dupe" => "duplication",
16
+ **Hashira::Pipeline::STRUCTURAL.to_h { [it, it] },
17
+ **MEASURES.to_h { [it, it] },
18
+ "smells" => SMELLS, **SMELLS.to_h { [it, it] }
19
+ }.freeze
20
20
 
21
- def kind(name)
22
- KINDS.fetch(name) do
23
- raise Error, "unknown --fail-on kind #{name.inspect} (use: #{KINDS.keys.join(", ")})"
24
- end
25
- end
21
+ module_function
22
+
23
+ def parse(list)
24
+ return [] unless list
25
+ list.split(",").flat_map { Array(kind(it.strip)) }.uniq
26
+ end
27
+
28
+ def kind(name)
29
+ KINDS.fetch(name) do
30
+ raise(Hashira::Error, "unknown --fail-on kind #{name.inspect} (use: #{KINDS.keys.join(", ")})")
26
31
  end
27
32
  end
28
33
  end
@@ -1,9 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Hashira
4
- class CLI
5
- Options = Data.define(:directories, :mode, :baseline, :fail_on, :skip) do
3
+ class Hashira::CLI
4
+ Options =
5
+ Data.define(:directories, :mode, :baseline, :fail_on, :skip, :packaging) do
6
6
  def self.parse(argv) = CommandLine.new(argv).options
7
+
8
+ def pipeline
9
+ Hashira::Pipeline.new(Hashira::Project.detect(directories), enabled: analyzers, packaging:)
10
+ end
11
+
12
+ def analyzers = Hashira::Pipeline::ANALYZERS - skip
7
13
  end
8
- end
9
14
  end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hashira::CLI::PackageBy
4
+ CHOICES = %i[auto folder namespace].freeze
5
+
6
+ module_function
7
+
8
+ def parse(value)
9
+ return :auto if value.empty?
10
+ choice = value.to_sym
11
+ return choice if CHOICES.include?(choice)
12
+ raise(Hashira::Error.unknown("--package-by", value, CHOICES))
13
+ end
14
+ end