hashira 0.6.0 → 0.8.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 (110) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +106 -0
  3. data/README.md +148 -53
  4. data/exe/hashira +1 -1
  5. data/lib/hashira/analysis/finding.rb +5 -1
  6. data/lib/hashira/analysis/syntax.rb +6 -6
  7. data/lib/hashira/churn.rb +3 -4
  8. data/lib/hashira/ci/accepted.rb +9 -10
  9. data/lib/hashira/ci/baseline.rb +39 -12
  10. data/lib/hashira/ci/comparison.rb +24 -0
  11. data/lib/hashira/ci/diff.rb +4 -8
  12. data/lib/hashira/ci/finding_diff_report.rb +10 -0
  13. data/lib/hashira/ci/gate.rb +10 -2
  14. data/lib/hashira/ci/improvement.rb +1 -1
  15. data/lib/hashira/ci/ratchet.rb +41 -10
  16. data/lib/hashira/ci/ratchet_report.rb +8 -6
  17. data/lib/hashira/ci/scope.rb +10 -0
  18. data/lib/hashira/ci/slice.rb +17 -0
  19. data/lib/hashira/ci/status.rb +7 -0
  20. data/lib/hashira/ci/sweep.rb +13 -0
  21. data/lib/hashira/cli/arguments.rb +17 -8
  22. data/lib/hashira/cli/choice.rb +9 -0
  23. data/lib/hashira/cli/command_line.rb +8 -5
  24. data/lib/hashira/cli/fail_on.rb +17 -7
  25. data/lib/hashira/cli/flag.rb +3 -1
  26. data/lib/hashira/cli/flags.rb +23 -0
  27. data/lib/hashira/cli/format.rb +1 -1
  28. data/lib/hashira/cli/needs.rb +45 -0
  29. data/lib/hashira/cli/only.rb +16 -0
  30. data/lib/hashira/cli/options.rb +12 -3
  31. data/lib/hashira/cli/package_by.rb +1 -1
  32. data/lib/hashira/cli/run.rb +36 -8
  33. data/lib/hashira/cli/top.rb +13 -0
  34. data/lib/hashira/cli/usage.rb +5 -1
  35. data/lib/hashira/cli.rb +31 -11
  36. data/lib/hashira/complexity/cognitive_score.rb +44 -33
  37. data/lib/hashira/complexity/method_finding.rb +7 -5
  38. data/lib/hashira/complexity/method_score.rb +1 -1
  39. data/lib/hashira/complexity/scores.rb +11 -9
  40. data/lib/hashira/coupling/audiences.rb +3 -3
  41. data/lib/hashira/coupling/catalog.rb +8 -6
  42. data/lib/hashira/coupling/census.rb +42 -22
  43. data/lib/hashira/coupling/cycle_search.rb +11 -7
  44. data/lib/hashira/coupling/definitions.rb +1 -1
  45. data/lib/hashira/coupling/edge_map.rb +9 -8
  46. data/lib/hashira/coupling/folding.rb +5 -5
  47. data/lib/hashira/coupling/graph.rb +16 -9
  48. data/lib/hashira/coupling/metric.rb +9 -1
  49. data/lib/hashira/coupling/naming.rb +4 -4
  50. data/lib/hashira/coupling/references.rb +12 -12
  51. data/lib/hashira/coupling/report.rb +7 -9
  52. data/lib/hashira/coupling/roll_call.rb +1 -1
  53. data/lib/hashira/coupling/roll_call_findings.rb +1 -1
  54. data/lib/hashira/coupling/roster.rb +37 -12
  55. data/lib/hashira/coupling/rule.rb +1 -1
  56. data/lib/hashira/coupling/sdp_violation_findings.rb +3 -1
  57. data/lib/hashira/diagram/dot.rb +9 -6
  58. data/lib/hashira/diagram/mermaid.rb +11 -8
  59. data/lib/hashira/diagram/source.rb +5 -2
  60. data/lib/hashira/duplication/clones.rb +1 -1
  61. data/lib/hashira/duplication/clusters.rb +10 -7
  62. data/lib/hashira/duplication/delta.rb +1 -4
  63. data/lib/hashira/duplication/duplication_finding.rb +4 -2
  64. data/lib/hashira/duplication/fragment.rb +2 -2
  65. data/lib/hashira/duplication/harvest.rb +7 -5
  66. data/lib/hashira/duplication/index.rb +3 -2
  67. data/lib/hashira/duplication/similarity.rb +2 -2
  68. data/lib/hashira/duplication/variance.rb +1 -1
  69. data/lib/hashira/error.rb +0 -3
  70. data/lib/hashira/focus.rb +29 -0
  71. data/lib/hashira/git_log.rb +19 -0
  72. data/lib/hashira/hotspots/file_cost.rb +1 -1
  73. data/lib/hashira/hotspots/rollup.rb +2 -2
  74. data/lib/hashira/pipeline.rb +26 -24
  75. data/lib/hashira/project.rb +47 -32
  76. data/lib/hashira/report/columns.rb +54 -0
  77. data/lib/hashira/report/complexity_table.rb +12 -16
  78. data/lib/hashira/report/dependency_map.rb +1 -0
  79. data/lib/hashira/report/graph_payload.rb +1 -1
  80. data/lib/hashira/report/hotspot_table.rb +6 -14
  81. data/lib/hashira/report/json.rb +10 -2
  82. data/lib/hashira/report/metrics_table.rb +19 -20
  83. data/lib/hashira/report/notices.rb +37 -0
  84. data/lib/hashira/report/phrases.rb +25 -18
  85. data/lib/hashira/report/text.rb +24 -9
  86. data/lib/hashira/report/view.rb +4 -1
  87. data/lib/hashira/smells/census.rb +3 -4
  88. data/lib/hashira/smells/check.rb +1 -5
  89. data/lib/hashira/smells/contexts.rb +4 -4
  90. data/lib/hashira/smells/control_parameter.rb +1 -1
  91. data/lib/hashira/smells/data_clump.rb +2 -4
  92. data/lib/hashira/smells/duplicate_method_call.rb +3 -3
  93. data/lib/hashira/smells/feature_envy.rb +3 -3
  94. data/lib/hashira/smells/foreign.rb +14 -14
  95. data/lib/hashira/smells/instance_variable_assumption.rb +3 -3
  96. data/lib/hashira/smells/kind.rb +9 -0
  97. data/lib/hashira/smells/manual_dispatch.rb +1 -1
  98. data/lib/hashira/smells/module_initialize.rb +0 -2
  99. data/lib/hashira/smells/nil_check.rb +1 -1
  100. data/lib/hashira/smells/ownership.rb +19 -7
  101. data/lib/hashira/smells/param_check.rb +2 -2
  102. data/lib/hashira/smells/refs.rb +13 -7
  103. data/lib/hashira/smells/repeated_conditional.rb +1 -3
  104. data/lib/hashira/smells/report.rb +16 -7
  105. data/lib/hashira/smells/too_many_instance_variables.rb +3 -3
  106. data/lib/hashira/smells/visibility.rb +18 -11
  107. data/lib/hashira/trees.rb +26 -0
  108. data/lib/hashira/version.rb +1 -1
  109. data/lib/hashira.rb +14 -0
  110. metadata +16 -1
@@ -3,26 +3,24 @@
3
3
  require_relative "cycle_findings"
4
4
  require_relative "mixed_audience_findings"
5
5
  require_relative "roll_call_findings"
6
+ require_relative "rule"
6
7
  require_relative "sdp_violation_findings"
7
8
  require_relative "wide_edge_findings"
8
9
 
9
10
  class Hashira::Coupling::Report
10
- RULES = [
11
- Hashira::Coupling::CycleFindings, Hashira::Coupling::SdpViolationFindings,
12
- Hashira::Coupling::MixedAudienceFindings, Hashira::Coupling::WideEdgeFindings,
13
- Hashira::Coupling::RollCallFindings
14
- ].freeze
11
+ RULES = Hashira::Coupling::Rule.subclasses.sort_by(&:name).freeze
15
12
 
16
13
  def initialize(project, trees, packaging:)
17
14
  @project = project
18
- @graph = Hashira::Coupling::Graph.new(project, trees, census(trees, packaging))
15
+ @trees = trees
16
+ @packaging = packaging
19
17
  end
20
18
 
21
- attr_reader :graph
19
+ def graph = @_graph ||= Hashira::Coupling::Graph.new(@project, @trees, census)
22
20
 
23
- def findings = RULES.flat_map { it.new(@project, @graph).list }
21
+ def findings = RULES.flat_map { it.new(@project, graph).list }
24
22
 
25
23
  private
26
24
 
27
- def census(trees, packaging) = Hashira::Coupling::Census.new(@project, trees, packaging:)
25
+ def census = Hashira::Coupling::Census.new(@project, @trees, packaging: @packaging)
28
26
  end
@@ -18,7 +18,7 @@ class Hashira::Coupling::RollCall
18
18
 
19
19
  private
20
20
 
21
- def files = @files ||= @lists.keys.sort
21
+ def files = @_files ||= @lists.keys.sort
22
22
 
23
23
  def candidates
24
24
  files.combination(2).map { |one, two| @lists[one] & @lists[two] }.select { it.size >= MIN_WORDS }.uniq
@@ -12,7 +12,7 @@ class Hashira::Coupling::RollCallFindings < Hashira::Coupling::Rule
12
12
  def rolls = Hashira::Coupling::RollCall.new(lists, homes).rolls
13
13
 
14
14
  def sightings
15
- @sightings ||=
15
+ @_sightings ||=
16
16
  graph.trees.filter_map do |file, tree|
17
17
  words = Hashira::Coupling::Words.list(tree)
18
18
  [project.relative(file), graph.charge(file), words] unless words.empty?
@@ -2,28 +2,53 @@
2
2
 
3
3
  class Hashira::Coupling::Roster
4
4
  def initialize(placed)
5
- @registry = Hashira::Coupling::ConstantRegistry.new
6
- @types = Hash.new(0)
7
- @typed = Set.new
8
- counted = Set.new
9
- placed.each { |definition, package| admit(definition, package, counted) }
5
+ @placed = placed
6
+ end
7
+
8
+ def registry
9
+ admitted
10
+ @_registry
10
11
  end
11
12
 
12
- attr_reader :registry, :types
13
+ def types
14
+ admitted
15
+ @_types
16
+ end
13
17
 
14
- def origins = @registry.origins
18
+ def origins = registry.origins
15
19
 
16
- def type?(path) = @typed.include?(path)
20
+ def type?(path)
21
+ admitted
22
+ @_typed.include?(path)
23
+ end
17
24
 
18
- def packages = @types.keys | @registry.packages
25
+ def packages = types.keys | registry.packages
19
26
 
20
27
  private
21
28
 
29
+ def admitted
30
+ return if @_admitted
31
+ @_admitted = true
32
+ blank
33
+ fill
34
+ end
35
+
36
+ def blank
37
+ @_registry = Hashira::Coupling::ConstantRegistry.new
38
+ @_types = Hash.new(0)
39
+ @_typed = Set.new
40
+ end
41
+
42
+ def fill
43
+ counted = Set.new
44
+ @placed.each { |definition, package| admit(definition, package, counted) }
45
+ end
46
+
22
47
  def admit(definition, package, counted)
23
48
  return unless package
24
49
  path = definition.path
25
- @registry.register(path, package)
26
- @typed << path if definition.type?
27
- @types[package] += 1 if definition.counted? && counted.add?(path)
50
+ @_registry.register(path, package)
51
+ @_typed << path if definition.type?
52
+ @_types[package] += 1 if definition.counted? && counted.add?(path)
28
53
  end
29
54
  end
@@ -10,7 +10,7 @@ class Hashira::Coupling::Rule
10
10
 
11
11
  attr_reader :project, :graph
12
12
 
13
- def metrics = @metrics ||= graph.metrics
13
+ def metrics = @_metrics ||= graph.metrics
14
14
 
15
15
  def finding(**attributes)
16
16
  Hashira::Analysis::Finding.new(kind: self.class::KIND, cycle: nil, **attributes)
@@ -5,6 +5,8 @@ require_relative "rule"
5
5
  class Hashira::Coupling::SdpViolationFindings < Hashira::Coupling::Rule
6
6
  KIND = "sdp_violation"
7
7
 
8
+ Imbalance = Data.define(:from, :to, :from_instability, :to_instability)
9
+
8
10
  def list
9
11
  ranked.map { |from, to| violation(from, to) }
10
12
  end
@@ -18,7 +20,7 @@ class Hashira::Coupling::SdpViolationFindings < Hashira::Coupling::Rule
18
20
  end
19
21
 
20
22
  def detail(from, to)
21
- { from:, to:, from_instability: instability(from), to_instability: instability(to) }
23
+ Imbalance.new(from:, to:, from_instability: instability(from), to_instability: instability(to))
22
24
  end
23
25
 
24
26
  def instability(package) = metrics[package].instability
@@ -1,13 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Hashira::Diagram::Dot
4
- def initialize(edges)
4
+ def initialize(edges, packages)
5
5
  @edges = edges
6
+ @packages = packages
6
7
  end
7
8
 
8
- def source
9
- "digraph hashira {\n rankdir=LR;\n#{@edges.map do |from, to, weight|
10
- %( "#{from}" -> "#{to}" [label="#{weight}"];)
11
- end.join("\n")}\n}"
12
- end
9
+ def source = "digraph hashira {\n rankdir=LR;\n#{lines.join("\n")}\n}"
10
+
11
+ private
12
+
13
+ def lines = @packages.map { %( "#{it}";) } + @edges.map { |from, to, weight| link(from, to, weight) }
14
+
15
+ def link(from, to, weight) = %( "#{from}" -> "#{to}" [label="#{weight}"];)
13
16
  end
@@ -1,18 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Hashira::Diagram::Mermaid
4
- def initialize(edges)
4
+ def initialize(edges, packages)
5
5
  @edges = edges
6
- @nodes = {}
6
+ @packages = packages
7
+ @ids = {}
7
8
  end
8
9
 
9
- def source
10
- "graph LR\n#{@edges.map { |from, to, weight| " #{node(from)} -->|#{weight}| #{node(to)}" }.join("\n")}"
11
- end
10
+ def source = "graph LR\n#{lines.join("\n")}"
12
11
 
13
12
  private
14
13
 
15
- def node(package)
16
- @nodes[package] ||= "#{package.gsub(/\W/, "_")}[\"#{package}\"]"
17
- end
14
+ def lines = @packages.map { " #{declare(it)}" } + @edges.map { |from, to, weight| link(from, to, weight) }
15
+
16
+ def link(from, to, weight) = " #{id(from)} -->|#{weight}| #{id(to)}"
17
+
18
+ def declare(package) = "#{id(package)}[#{package.to_json}]"
19
+
20
+ def id(package) = @ids[package] ||= "p#{@ids.size}"
18
21
  end
@@ -8,8 +8,11 @@ class Hashira::Diagram::Source
8
8
  end
9
9
 
10
10
  def print
11
- edges = @graph.weighted
12
- @io.puts((@format == :dot ? Hashira::Diagram::Dot.new(edges) : Hashira::Diagram::Mermaid.new(edges)).source)
11
+ @io.puts(drawing.new(@graph.weighted, @graph.packages.sort).source)
13
12
  0
14
13
  end
14
+
15
+ private
16
+
17
+ def drawing = @format == :dot ? Hashira::Diagram::Dot : Hashira::Diagram::Mermaid
15
18
  end
@@ -7,7 +7,7 @@ class Hashira::Duplication::Clones
7
7
  @churn = churn
8
8
  end
9
9
 
10
- def clusters = @clusters ||= Hashira::Duplication::Clusters.new(fragments).sorted
10
+ def clusters = @_clusters ||= Hashira::Duplication::Clusters.new(fragments).sorted
11
11
 
12
12
  def findings = clusters.map { |cluster| Hashira::Duplication::DuplicationFinding.new(cluster, @churn).to_finding }
13
13
 
@@ -7,20 +7,23 @@ class Hashira::Duplication::Clusters
7
7
  PAIR = 2
8
8
  PENALTY_PER_RECURRENCE = 2
9
9
 
10
- def initialize(fragments)
11
- @fragments = fragments.select { |fragment| fragment.mass >= PREFILTER }
12
- @sets = Hashira::Duplication::UnionFind.new
10
+ def initialize(all)
11
+ @all = all
13
12
  end
14
13
 
15
14
  def sorted
16
- @fragments.group_by(&:types).each_value { |group| chain(group) }
17
- Hashira::Duplication::NearMiss.new(@fragments).pairs.each { |left, right| @sets.union(left, right) }
15
+ fragments.group_by(&:types).each_value { |group| chain(group) }
16
+ Hashira::Duplication::NearMiss.new(fragments).pairs.each { |left, right| sets.union(left, right) }
18
17
  Hashira::Duplication::Maximal.new(sized).reduced.sort_by { -it.mass }
19
18
  end
20
19
 
21
20
  private
22
21
 
23
- def chain(group) = group.each_cons(2) { |left, right| @sets.union(left, right) }
22
+ def fragments = @_fragments ||= @all.select { |fragment| fragment.mass >= PREFILTER }
23
+
24
+ def sets = @_sets ||= Hashira::Duplication::UnionFind.new
25
+
26
+ def chain(group) = group.each_cons(2) { |left, right| sets.union(left, right) }
24
27
 
25
28
  def sized = built.filter_map { admitted(it) }
26
29
 
@@ -30,7 +33,7 @@ class Hashira::Duplication::Clusters
30
33
 
31
34
  def core(cluster) = Hashira::Duplication::Grouping.new(cluster.identical).cluster
32
35
 
33
- def built = @sets.clusters.filter_map { |group| Hashira::Duplication::Grouping.new(group).cluster }
36
+ def built = sets.clusters.filter_map { |group| Hashira::Duplication::Grouping.new(group).cluster }
34
37
 
35
38
  def floor(cluster) = base(cluster) + penalty(cluster)
36
39
 
@@ -11,10 +11,7 @@ class Hashira::Duplication::Delta
11
11
  end
12
12
 
13
13
  def to_h
14
- {
15
- mass: @cluster.mass, sites: @cluster.size, kind:,
16
- locations: @cluster.sites.sort_by(&:rank).map(&:range)
17
- }
14
+ { mass: @cluster.mass, sites: @cluster.size, kind: }.merge(locations: @cluster.sites.sort_by(&:rank).map(&:range))
18
15
  end
19
16
 
20
17
  private
@@ -3,6 +3,8 @@
3
3
  class Hashira::Duplication::DuplicationFinding
4
4
  KIND = "duplication"
5
5
 
6
+ Overlap = Data.define(:size, :mass, :kind, :hot)
7
+
6
8
  def initialize(cluster, churn)
7
9
  @cluster = cluster
8
10
  @churn = churn
@@ -16,10 +18,10 @@ class Hashira::Duplication::DuplicationFinding
16
18
  private
17
19
 
18
20
  def detail
19
- {
21
+ Overlap.new(
20
22
  size: @cluster.size, mass: @cluster.mass,
21
23
  kind: Hashira::Duplication::Delta.new(@cluster).kind, hot: @churn.hot?(@cluster.sites)
22
- }
24
+ )
23
25
  end
24
26
 
25
27
  def evidence = @cluster.sites.sort_by(&:rank).map(&:range)
@@ -12,7 +12,7 @@ class Hashira::Duplication::Fragment
12
12
 
13
13
  attr_reader :file
14
14
 
15
- def types = @types ||= nodes.map(&:type)
15
+ def types = @_types ||= nodes.map(&:type)
16
16
 
17
17
  def digest = Digest::SHA256.hexdigest(shape).slice(0, DIGEST_LENGTH)
18
18
 
@@ -34,5 +34,5 @@ class Hashira::Duplication::Fragment
34
34
 
35
35
  def touches?(others) = others.any? { overlaps?(it) }
36
36
 
37
- def nodes = @nodes ||= @roots.flat_map { Hashira::Analysis::NodeWalk.collect(it) }
37
+ def nodes = @_nodes ||= @roots.flat_map { Hashira::Analysis::NodeWalk.collect(it) }
38
38
  end
@@ -7,19 +7,21 @@ class Hashira::Duplication::Harvest
7
7
 
8
8
  def initialize(project, trees)
9
9
  @project = project
10
- @fragments = trees.flat_map { |path, tree| scan(@project.relative(path), tree) }
10
+ @trees = trees
11
11
  end
12
12
 
13
- attr_reader :fragments
13
+ def fragments = @_fragments ||= @trees.flat_map { |path, tree| scan(@project.relative(path), tree) }
14
14
 
15
15
  private
16
16
 
17
- def scan(rel, tree)
17
+ def scan(relative, tree)
18
18
  nodes = Hashira::Analysis::NodeWalk.collect(tree)
19
- windows(rel, nodes) + wholes(nodes).map { Hashira::Duplication::Fragment.new(rel, [it]) }
19
+ windows(relative, nodes) + wholes(nodes).map { Hashira::Duplication::Fragment.new(relative, [it]) }
20
20
  end
21
21
 
22
- def windows(rel, nodes) = runs(nodes).flat_map { Hashira::Duplication::Sequence.new(rel, it).fragments }
22
+ def windows(relative, nodes)
23
+ runs(nodes).flat_map { Hashira::Duplication::Sequence.new(relative, it).fragments }
24
+ end
23
25
 
24
26
  def runs(nodes) = nodes.filter_map { it.body if it.is_a?(Prism::StatementsNode) }
25
27
 
@@ -6,13 +6,14 @@ class Hashira::Duplication::Index
6
6
 
7
7
  def initialize(fragments)
8
8
  @fragments = fragments
9
- @frequency = frequencies(fragments)
10
9
  end
11
10
 
12
11
  def buckets = grouped.values.select { |bucket| bucket.size.between?(2, MAX_BUCKET) }
13
12
 
14
13
  private
15
14
 
15
+ def frequency = @_frequency ||= frequencies(@fragments)
16
+
16
17
  def frequencies(fragments)
17
18
  fragments.each_with_object(Hash.new(0)) { |fragment, counts| tally(counts, fragment) }
18
19
  end
@@ -27,5 +28,5 @@ class Hashira::Duplication::Index
27
28
 
28
29
  def file(index, fragment) = rarest(fragment).each { |type| index[type] << fragment }
29
30
 
30
- def rarest(fragment) = fragment.types.uniq.min_by(RARE) { @frequency[it] }
31
+ def rarest(fragment) = fragment.types.uniq.min_by(RARE) { frequency[it] }
31
32
  end
@@ -8,7 +8,7 @@ class Hashira::Duplication::Similarity
8
8
 
9
9
  def ratio
10
10
  return 0.0 if @left.empty? || @right.empty?
11
- normalized(lcs)
11
+ normalized(subsequence)
12
12
  end
13
13
 
14
14
  def meets?(threshold) = ceiling >= threshold && ratio >= threshold
@@ -29,7 +29,7 @@ class Hashira::Duplication::Similarity
29
29
  true
30
30
  end
31
31
 
32
- def lcs = @left.reduce(blank) { |prev, token| advance(prev, token) }.last
32
+ def subsequence = @left.reduce(blank) { |prev, token| advance(prev, token) }.last
33
33
 
34
34
  def blank = Array.new(@right.size + 1, 0)
35
35
 
@@ -27,7 +27,7 @@ class Hashira::Duplication::Variance
27
27
 
28
28
  def pairs = @canonical.nodes.zip(@other.nodes)
29
29
 
30
- def named = @named ||= pairs.select { |left, _| NAMED.include?(left.type) }
30
+ def named = @_named ||= pairs.select { |left, _| NAMED.include?(left.type) }
31
31
 
32
32
  def differing = pairs.select { |pair| varies?(*pair) }.map(&:first)
33
33
 
data/lib/hashira/error.rb CHANGED
@@ -1,7 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Hashira::Error < StandardError
4
- def self.unknown(flag, value, choices)
5
- new("unknown #{flag} #{value.inspect} (use: #{choices.join(", ")})")
6
- end
7
4
  end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Hashira::Focus
4
+ SITE = /\A(?<path>[^\s:]+\.rb):/
5
+
6
+ def initialize(project, paths)
7
+ @project = project
8
+ @paths = paths
9
+ end
10
+
11
+ def narrowing? = !@paths.empty?
12
+
13
+ def narrow(findings)
14
+ return findings unless narrowing?
15
+ findings.select { named?(it) }
16
+ end
17
+
18
+ private
19
+
20
+ def wanted = @_wanted ||= @paths.select { covered?(it) }.map { @project.relative(it) }
21
+
22
+ def covered?(path) = @project.directories.any? { path.start_with?("#{it}/") }
23
+
24
+ def named?(finding) = sites(finding).any? { wanted.include?(it) }
25
+
26
+ def sites(finding) = quotes(finding).filter_map { SITE.match(it)&.[](:path) }
27
+
28
+ def quotes(finding) = [finding.package, finding.detail.to_h[:site], *finding.evidence].compact
29
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Hashira::GitLog
4
+ LOG = %w[log --no-renames --name-only --format=].freeze
5
+
6
+ def initialize(directory)
7
+ @directory = directory
8
+ end
9
+
10
+ def counts = output.split("\n").map(&:strip).reject(&:empty?).tally
11
+
12
+ private
13
+
14
+ def output
15
+ IO.popen(["git", "-C", @directory, *LOG], err: File::NULL, &:read)
16
+ rescue SystemCallError
17
+ ""
18
+ end
19
+ end
@@ -12,7 +12,7 @@ module Hashira
12
12
 
13
13
  def heat = [churn, CHURN_FLOOR].max
14
14
 
15
- def to_h = { file:, cognitive:, duplication:, churn:, cost:, rank: }
15
+ def to_h = super.merge(cost:, rank:)
16
16
 
17
17
  def cells = [file, cognitive, duplication, churn, rank]
18
18
  end
@@ -23,9 +23,9 @@ class Hashira::Hotspots::Rollup
23
23
 
24
24
  def clusters = @duplication ? @duplication.clusters : []
25
25
 
26
- def cognitive = @cognitive ||= bucketed(scores.map { [it.file, it.cognitive] })
26
+ def cognitive = @_cognitive ||= bucketed(scores.map { [it.file, it.cognitive] })
27
27
 
28
- def duplicated = @duplicated ||= bucketed(clusters.flat_map(&:masses))
28
+ def duplicated = @_duplicated ||= bucketed(clusters.flat_map(&:masses))
29
29
 
30
30
  def bucketed(charges) = charges.each_with_object(Hash.new(0)) { |(file, cost), total| total[file] += cost }
31
31
  end
@@ -10,56 +10,58 @@ class Hashira::Pipeline
10
10
  STRUCTURAL = Hashira::Coupling::Report::RULES.map { it::KIND }.freeze
11
11
 
12
12
  SMELLS = (
13
- Hashira::Smells::Report::CHECKS.map(&:kind) + [Hashira::Smells::BoundarySprawl::KIND]
13
+ Hashira::Smells::Report::CHECKS.map { Hashira::Smells::Kind.new(it).to_s } + [Hashira::Smells::BoundarySprawl::KIND]
14
14
  ).sort.freeze
15
15
 
16
- def initialize(project, enabled: ANALYZERS, packaging: :auto)
16
+ def initialize(project, enabled: ANALYZERS, packaging: :auto, only: [])
17
17
  @project = project
18
18
  @enabled = enabled
19
- @trees = project.files.to_h { [it, parse(it)] }
20
- @coupling = Hashira::Coupling::Report.new(project, @trees, packaging: settle(packaging))
19
+ @packaging = packaging
20
+ @only = only
21
21
  end
22
22
 
23
23
  attr_reader :project
24
24
 
25
- def graph = @coupling.graph
25
+ def unparsed = parsed.unparsed
26
26
 
27
- def complexity
28
- @complexity ||= Hashira::Complexity::Scores.new(@project, @trees) if enabled?(:complexity)
29
- end
27
+ def graph = coupling.graph
30
28
 
31
- def duplication
32
- @duplication ||= Hashira::Duplication::Clones.new(@project, @trees, churn) if enabled?(:duplication)
33
- end
29
+ def complexity = @_complexity ||= analyzed(:complexity, Hashira::Complexity::Scores)
34
30
 
35
- def smells
36
- @smells ||= Hashira::Smells::Report.new(@project, @trees) if enabled?(:smells)
37
- end
31
+ def duplication = @_duplication ||= analyzed(:duplication, Hashira::Duplication::Clones, churn)
32
+
33
+ def smells = @_smells ||= analyzed(:smells, Hashira::Smells::Report)
38
34
 
39
35
  def hotspots
40
- @hotspots ||= Hashira::Hotspots::Rollup.new(complexity, duplication, churn) if complexity || duplication
36
+ @_hotspots ||= Hashira::Hotspots::Rollup.new(complexity, duplication, churn) if complexity || duplication
41
37
  end
42
38
 
43
- def churn = @churn ||= Hashira::Churn.scan
39
+ def churn = @_churn ||= Hashira::Churn.build(@project.directories.first)
44
40
 
45
41
  def enabled?(analyzer) = @enabled.include?(analyzer)
46
42
 
47
- def findings = structural + listed(complexity) + listed(duplication) + listed(smells)
43
+ def analyzed(name, kind, *extra)
44
+ kind.new(@project, parsed.all, *extra) if enabled?(name)
45
+ end
46
+
47
+ def findings = focus.narrow(structural + listed(complexity) + listed(duplication) + listed(smells))
48
+
49
+ def focus = @_focus ||= Hashira::Focus.new(@project, @only)
48
50
 
49
51
  private
50
52
 
53
+ def parsed = @_parsed ||= Hashira::Trees.new(@project)
54
+
55
+ def coupling
56
+ @_coupling ||= Hashira::Coupling::Report.new(@project, parsed.all, packaging: settle(@packaging))
57
+ end
58
+
51
59
  def settle(packaging)
52
60
  return packaging unless packaging == :auto
53
61
  @project.rails? ? :namespace : :folder
54
62
  end
55
63
 
56
- def structural = enabled?(:coupling) ? @coupling.findings : []
64
+ def structural = enabled?(:coupling) ? coupling.findings : []
57
65
 
58
66
  def listed(analyzer) = analyzer ? analyzer.findings : []
59
-
60
- def parse(path)
61
- Prism.parse_file(path).value
62
- rescue SystemCallError => error
63
- raise(Hashira::Error, "cannot read #{path} (#{error.message})")
64
- end
65
67
  end