hashira 0.7.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 (97) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +38 -0
  3. data/README.md +29 -0
  4. data/exe/hashira +1 -1
  5. data/lib/hashira/analysis/finding.rb +1 -1
  6. data/lib/hashira/analysis/syntax.rb +6 -6
  7. data/lib/hashira/churn.rb +1 -10
  8. data/lib/hashira/ci/accepted.rb +9 -7
  9. data/lib/hashira/ci/baseline.rb +34 -30
  10. data/lib/hashira/ci/comparison.rb +24 -0
  11. data/lib/hashira/ci/diff.rb +0 -15
  12. data/lib/hashira/ci/gate.rb +3 -3
  13. data/lib/hashira/ci/ratchet.rb +10 -4
  14. data/lib/hashira/ci/ratchet_report.rb +4 -4
  15. data/lib/hashira/ci/scope.rb +0 -1
  16. data/lib/hashira/ci/slice.rb +17 -0
  17. data/lib/hashira/ci/sweep.rb +13 -0
  18. data/lib/hashira/cli/arguments.rb +12 -8
  19. data/lib/hashira/cli/choice.rb +9 -0
  20. data/lib/hashira/cli/command_line.rb +8 -5
  21. data/lib/hashira/cli/fail_on.rb +6 -6
  22. data/lib/hashira/cli/flag.rb +3 -1
  23. data/lib/hashira/cli/flags.rb +10 -0
  24. data/lib/hashira/cli/format.rb +1 -1
  25. data/lib/hashira/cli/needs.rb +13 -2
  26. data/lib/hashira/cli/only.rb +16 -0
  27. data/lib/hashira/cli/options.rb +8 -10
  28. data/lib/hashira/cli/package_by.rb +1 -1
  29. data/lib/hashira/cli/run.rb +9 -8
  30. data/lib/hashira/cli.rb +22 -21
  31. data/lib/hashira/complexity/cognitive_score.rb +44 -33
  32. data/lib/hashira/complexity/method_finding.rb +7 -5
  33. data/lib/hashira/complexity/method_score.rb +1 -1
  34. data/lib/hashira/complexity/scores.rb +11 -9
  35. data/lib/hashira/coupling/audiences.rb +3 -3
  36. data/lib/hashira/coupling/catalog.rb +8 -6
  37. data/lib/hashira/coupling/census.rb +42 -22
  38. data/lib/hashira/coupling/cycle_search.rb +11 -7
  39. data/lib/hashira/coupling/definitions.rb +1 -1
  40. data/lib/hashira/coupling/edge_map.rb +9 -8
  41. data/lib/hashira/coupling/folding.rb +5 -5
  42. data/lib/hashira/coupling/graph.rb +16 -9
  43. data/lib/hashira/coupling/metric.rb +3 -1
  44. data/lib/hashira/coupling/naming.rb +4 -4
  45. data/lib/hashira/coupling/references.rb +12 -12
  46. data/lib/hashira/coupling/report.rb +7 -9
  47. data/lib/hashira/coupling/roll_call.rb +1 -1
  48. data/lib/hashira/coupling/roll_call_findings.rb +1 -1
  49. data/lib/hashira/coupling/roster.rb +37 -12
  50. data/lib/hashira/coupling/rule.rb +1 -1
  51. data/lib/hashira/coupling/sdp_violation_findings.rb +3 -1
  52. data/lib/hashira/duplication/clones.rb +1 -1
  53. data/lib/hashira/duplication/clusters.rb +10 -7
  54. data/lib/hashira/duplication/delta.rb +1 -4
  55. data/lib/hashira/duplication/duplication_finding.rb +4 -2
  56. data/lib/hashira/duplication/fragment.rb +2 -2
  57. data/lib/hashira/duplication/harvest.rb +7 -5
  58. data/lib/hashira/duplication/index.rb +3 -2
  59. data/lib/hashira/duplication/similarity.rb +2 -2
  60. data/lib/hashira/duplication/variance.rb +1 -1
  61. data/lib/hashira/error.rb +0 -3
  62. data/lib/hashira/focus.rb +29 -0
  63. data/lib/hashira/git_log.rb +19 -0
  64. data/lib/hashira/hotspots/file_cost.rb +1 -1
  65. data/lib/hashira/hotspots/rollup.rb +2 -2
  66. data/lib/hashira/pipeline.rb +22 -14
  67. data/lib/hashira/project.rb +35 -34
  68. data/lib/hashira/report/columns.rb +14 -8
  69. data/lib/hashira/report/graph_payload.rb +1 -1
  70. data/lib/hashira/report/hotspot_table.rb +1 -1
  71. data/lib/hashira/report/json.rb +2 -1
  72. data/lib/hashira/report/metrics_table.rb +3 -3
  73. data/lib/hashira/report/phrases.rb +23 -18
  74. data/lib/hashira/smells/census.rb +3 -4
  75. data/lib/hashira/smells/check.rb +1 -5
  76. data/lib/hashira/smells/contexts.rb +4 -4
  77. data/lib/hashira/smells/control_parameter.rb +1 -1
  78. data/lib/hashira/smells/data_clump.rb +2 -4
  79. data/lib/hashira/smells/duplicate_method_call.rb +3 -3
  80. data/lib/hashira/smells/feature_envy.rb +3 -3
  81. data/lib/hashira/smells/foreign.rb +14 -14
  82. data/lib/hashira/smells/instance_variable_assumption.rb +3 -3
  83. data/lib/hashira/smells/kind.rb +9 -0
  84. data/lib/hashira/smells/manual_dispatch.rb +1 -1
  85. data/lib/hashira/smells/module_initialize.rb +0 -2
  86. data/lib/hashira/smells/nil_check.rb +1 -1
  87. data/lib/hashira/smells/ownership.rb +19 -7
  88. data/lib/hashira/smells/param_check.rb +2 -2
  89. data/lib/hashira/smells/refs.rb +13 -7
  90. data/lib/hashira/smells/repeated_conditional.rb +1 -3
  91. data/lib/hashira/smells/report.rb +16 -7
  92. data/lib/hashira/smells/too_many_instance_variables.rb +3 -3
  93. data/lib/hashira/smells/visibility.rb +18 -11
  94. data/lib/hashira/trees.rb +8 -4
  95. data/lib/hashira/version.rb +1 -1
  96. data/lib/hashira.rb +8 -0
  97. metadata +9 -1
@@ -2,18 +2,18 @@
2
2
 
3
3
  class Hashira::Coupling::Graph
4
4
  def initialize(project, trees, census)
5
+ @project = project
5
6
  @census = census
6
7
  @trees = trees
7
- @map = Hashira::Coupling::EdgeMap.new(census)
8
- trees.each { |file, tree| @map.record(project.relative(file), file, tree) }
9
- @cycles = Hashira::Coupling::Cycles.new(@map.dependencies, self)
10
8
  end
11
9
 
12
- attr_reader :cycles, :trees
10
+ attr_reader :trees
11
+
12
+ def cycles = @_cycles ||= Hashira::Coupling::Cycles.new(map.dependencies, self)
13
13
 
14
14
  def charge(file) = @census.charge(file, [])
15
15
 
16
- def packages = (@census.packages | @map.dependencies.keys)
16
+ def packages = (@census.packages | dependencies.keys)
17
17
 
18
18
  def packaging = @census.packaging
19
19
 
@@ -34,13 +34,13 @@ class Hashira::Coupling::Graph
34
34
  end
35
35
  end
36
36
 
37
- def evidence(from, to) = @map.evidence[[from, to]]
37
+ def evidence(from, to) = map.evidence[[from, to]]
38
38
 
39
- def usage(package) = incoming(package).to_h { [it, @map.usage[[it, package]]] }
39
+ def usage(package) = incoming(package).to_h { [it, map.usage[[it, package]]] }
40
40
 
41
41
  def constants(edge)
42
42
  from, to = edge.deconstruct
43
- @map.usage[[from, to]].sort
43
+ map.usage[[from, to]].sort
44
44
  end
45
45
 
46
46
  def metric(package)
@@ -59,5 +59,12 @@ class Hashira::Coupling::Graph
59
59
 
60
60
  private
61
61
 
62
- def dependencies = @map.dependencies
62
+ def map
63
+ @_map ||=
64
+ Hashira::Coupling::EdgeMap.new(@census).tap do |edges|
65
+ @trees.each { |file, tree| edges.record(@project.relative(file), file, tree) }
66
+ end
67
+ end
68
+
69
+ def dependencies = map.dependencies
63
70
  end
@@ -13,7 +13,9 @@ module Hashira
13
13
 
14
14
  def order = [isolated? ? 1 : 0, instability]
15
15
 
16
- def to_h = { tc: types, ca: afferent, ce: efferent, i: (instability unless isolated?) }
16
+ def to_h = counts.merge(i: (instability unless isolated?))
17
+
18
+ def counts = { tc: types, ca: afferent, ce: efferent }
17
19
 
18
20
  def cells = [types, afferent, efferent, isolated? ? "—" : format("%.2f", instability)]
19
21
  end
@@ -2,13 +2,13 @@
2
2
 
3
3
  class Hashira::Coupling::Naming
4
4
  def initialize(definitions)
5
- @segments = Hashira::Coupling::NamespacePrefix.infer(definitions)
5
+ @definitions = definitions
6
6
  end
7
7
 
8
- attr_reader :segments
8
+ def segments = @_segments ||= Hashira::Coupling::NamespacePrefix.infer(@definitions)
9
9
 
10
10
  def strip(path)
11
- return [] if @segments.first(path.length) == path
12
- path.drop(@segments.length.downto(0).find { path.first(it) == @segments.last(it) })
11
+ return [] if segments.first(path.length) == path
12
+ path.drop(segments.length.downto(0).find { path.first(it) == segments.last(it) })
13
13
  end
14
14
  end
@@ -3,34 +3,34 @@
3
3
  require "prism"
4
4
 
5
5
  class Hashira::Coupling::References
6
- def self.list(tree) = new.sightings(tree).map(&:first)
7
-
8
- def self.sightings(tree, roots = nil) = new(roots).sightings(tree)
9
-
10
6
  def initialize(roots = nil)
11
7
  @roots = roots
12
- @found = []
13
- @scopes = [[]]
14
8
  end
15
9
 
10
+ def list(tree) = sightings(tree).map(&:first)
11
+
16
12
  def sightings(tree)
17
13
  collect(tree)
18
- @found
14
+ found
19
15
  end
20
16
 
21
17
  private
22
18
 
23
- def nesting = @scopes.last
19
+ def found = @_found ||= []
20
+
21
+ def scopes = @_scopes ||= [[]]
22
+
23
+ def nesting = scopes.last
24
24
 
25
25
  def collect(node, home = nesting)
26
26
  return unless node
27
- return @found << sighting(node, home) if constant?(node)
27
+ return found << sighting(node, home) if constant?(node)
28
28
  return enter(node) if definition?(node)
29
29
  node.compact_child_nodes.each { collect(it, home) }
30
30
  end
31
31
 
32
32
  def sighting(node, home)
33
- [syntax.segments(node), node.location.start_line, syntax.cbase?(node) ? nil : nesting, home]
33
+ [syntax.segments(node), node.location.start_line, syntax.rooted?(node) ? nil : nesting, home]
34
34
  end
35
35
 
36
36
  def syntax = Hashira::Analysis::Syntax
@@ -48,8 +48,8 @@ class Hashira::Coupling::References
48
48
  def anchor(node) = syntax.anchor(nesting, syntax.segments(node.constant_path), @roots)
49
49
 
50
50
  def inside(scope)
51
- @scopes << scope
51
+ scopes << scope
52
52
  yield
53
- @scopes.pop
53
+ scopes.pop
54
54
  end
55
55
  end
@@ -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
@@ -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,50 +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
- @parsed = Hashira::Trees.new(project)
20
- @coupling = Hashira::Coupling::Report.new(project, @parsed.all, packaging: settle(packaging))
19
+ @packaging = packaging
20
+ @only = only
21
21
  end
22
22
 
23
23
  attr_reader :project
24
24
 
25
- def unparsed = @parsed.unparsed
25
+ def unparsed = parsed.unparsed
26
26
 
27
- def graph = @coupling.graph
27
+ def graph = coupling.graph
28
28
 
29
- def complexity = @complexity ||= analyzed(:complexity, Hashira::Complexity::Scores)
29
+ def complexity = @_complexity ||= analyzed(:complexity, Hashira::Complexity::Scores)
30
30
 
31
- def duplication = @duplication ||= analyzed(:duplication, Hashira::Duplication::Clones, churn)
31
+ def duplication = @_duplication ||= analyzed(:duplication, Hashira::Duplication::Clones, churn)
32
32
 
33
- def smells = @smells ||= analyzed(:smells, Hashira::Smells::Report)
33
+ def smells = @_smells ||= analyzed(:smells, Hashira::Smells::Report)
34
34
 
35
35
  def hotspots
36
- @hotspots ||= Hashira::Hotspots::Rollup.new(complexity, duplication, churn) if complexity || duplication
36
+ @_hotspots ||= Hashira::Hotspots::Rollup.new(complexity, duplication, churn) if complexity || duplication
37
37
  end
38
38
 
39
- def churn = @churn ||= Hashira::Churn.scan(@project.directories.first)
39
+ def churn = @_churn ||= Hashira::Churn.build(@project.directories.first)
40
40
 
41
41
  def enabled?(analyzer) = @enabled.include?(analyzer)
42
42
 
43
43
  def analyzed(name, kind, *extra)
44
- kind.new(@project, @parsed.all, *extra) if enabled?(name)
44
+ kind.new(@project, parsed.all, *extra) if enabled?(name)
45
45
  end
46
46
 
47
- def findings = structural + listed(complexity) + listed(duplication) + listed(smells)
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
67
  end