hashira 0.7.0 → 0.9.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 (103) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +94 -0
  3. data/README.md +57 -3
  4. data/exe/hashira +1 -1
  5. data/lib/hashira/analysis/finding.rb +11 -1
  6. data/lib/hashira/analysis/syntax.rb +6 -6
  7. data/lib/hashira/analysis/type_walk.rb +4 -0
  8. data/lib/hashira/churn.rb +1 -10
  9. data/lib/hashira/ci/accepted.rb +9 -7
  10. data/lib/hashira/ci/baseline.rb +53 -33
  11. data/lib/hashira/ci/comparison.rb +33 -0
  12. data/lib/hashira/ci/diff.rb +0 -15
  13. data/lib/hashira/ci/gate.rb +3 -3
  14. data/lib/hashira/ci/mark.rb +7 -0
  15. data/lib/hashira/ci/ratchet.rb +12 -4
  16. data/lib/hashira/ci/ratchet_report.rb +4 -4
  17. data/lib/hashira/ci/scope.rb +0 -1
  18. data/lib/hashira/ci/slice.rb +21 -0
  19. data/lib/hashira/ci/sweep.rb +13 -0
  20. data/lib/hashira/cli/arguments.rb +12 -8
  21. data/lib/hashira/cli/choice.rb +9 -0
  22. data/lib/hashira/cli/command_line.rb +8 -5
  23. data/lib/hashira/cli/fail_on.rb +6 -6
  24. data/lib/hashira/cli/flag.rb +3 -1
  25. data/lib/hashira/cli/flags.rb +10 -0
  26. data/lib/hashira/cli/format.rb +1 -1
  27. data/lib/hashira/cli/needs.rb +13 -2
  28. data/lib/hashira/cli/only.rb +16 -0
  29. data/lib/hashira/cli/options.rb +8 -10
  30. data/lib/hashira/cli/package_by.rb +1 -1
  31. data/lib/hashira/cli/run.rb +9 -8
  32. data/lib/hashira/cli.rb +22 -21
  33. data/lib/hashira/complexity/cognitive_score.rb +44 -33
  34. data/lib/hashira/complexity/method_finding.rb +7 -5
  35. data/lib/hashira/complexity/method_score.rb +1 -1
  36. data/lib/hashira/complexity/scores.rb +11 -9
  37. data/lib/hashira/coupling/audiences.rb +3 -3
  38. data/lib/hashira/coupling/catalog.rb +8 -6
  39. data/lib/hashira/coupling/census.rb +42 -22
  40. data/lib/hashira/coupling/cycle_search.rb +11 -7
  41. data/lib/hashira/coupling/definitions.rb +1 -7
  42. data/lib/hashira/coupling/edge_map.rb +9 -8
  43. data/lib/hashira/coupling/folding.rb +5 -5
  44. data/lib/hashira/coupling/graph.rb +16 -9
  45. data/lib/hashira/coupling/metric.rb +3 -1
  46. data/lib/hashira/coupling/naming.rb +4 -4
  47. data/lib/hashira/coupling/references.rb +12 -12
  48. data/lib/hashira/coupling/report.rb +7 -9
  49. data/lib/hashira/coupling/roll_call.rb +1 -1
  50. data/lib/hashira/coupling/roll_call_findings.rb +1 -1
  51. data/lib/hashira/coupling/roster.rb +37 -12
  52. data/lib/hashira/coupling/rule.rb +1 -1
  53. data/lib/hashira/coupling/sdp_violation_findings.rb +3 -1
  54. data/lib/hashira/duplication/clones.rb +1 -1
  55. data/lib/hashira/duplication/clusters.rb +10 -7
  56. data/lib/hashira/duplication/delta.rb +1 -4
  57. data/lib/hashira/duplication/duplication_finding.rb +4 -2
  58. data/lib/hashira/duplication/fragment.rb +20 -2
  59. data/lib/hashira/duplication/harvest.rb +7 -5
  60. data/lib/hashira/duplication/index.rb +3 -2
  61. data/lib/hashira/duplication/literal.rb +25 -0
  62. data/lib/hashira/duplication/similarity.rb +2 -2
  63. data/lib/hashira/duplication/variance.rb +7 -1
  64. data/lib/hashira/error.rb +0 -3
  65. data/lib/hashira/focus.rb +29 -0
  66. data/lib/hashira/git_log.rb +19 -0
  67. data/lib/hashira/hotspots/file_cost.rb +1 -1
  68. data/lib/hashira/hotspots/rollup.rb +2 -2
  69. data/lib/hashira/pipeline.rb +22 -14
  70. data/lib/hashira/project.rb +35 -34
  71. data/lib/hashira/report/columns.rb +14 -8
  72. data/lib/hashira/report/graph_payload.rb +1 -1
  73. data/lib/hashira/report/hotspot_table.rb +1 -1
  74. data/lib/hashira/report/json.rb +2 -1
  75. data/lib/hashira/report/metrics_table.rb +3 -3
  76. data/lib/hashira/report/phrases.rb +23 -18
  77. data/lib/hashira/report/smell_phrases.rb +2 -2
  78. data/lib/hashira/smells/branches.rb +62 -0
  79. data/lib/hashira/smells/census.rb +11 -6
  80. data/lib/hashira/smells/check.rb +1 -5
  81. data/lib/hashira/smells/contexts.rb +7 -5
  82. data/lib/hashira/smells/control_parameter.rb +1 -1
  83. data/lib/hashira/smells/data_clump.rb +2 -4
  84. data/lib/hashira/smells/duplicate_method_call.rb +25 -5
  85. data/lib/hashira/smells/feature_envy.rb +3 -3
  86. data/lib/hashira/smells/foreign.rb +14 -14
  87. data/lib/hashira/smells/instance_variable_assumption.rb +5 -15
  88. data/lib/hashira/smells/kind.rb +9 -0
  89. data/lib/hashira/smells/lineage.rb +92 -0
  90. data/lib/hashira/smells/manual_dispatch.rb +1 -1
  91. data/lib/hashira/smells/module_initialize.rb +0 -2
  92. data/lib/hashira/smells/nil_check.rb +1 -1
  93. data/lib/hashira/smells/ownership.rb +19 -7
  94. data/lib/hashira/smells/param_check.rb +2 -2
  95. data/lib/hashira/smells/refs.rb +13 -7
  96. data/lib/hashira/smells/repeated_conditional.rb +1 -3
  97. data/lib/hashira/smells/report.rb +16 -7
  98. data/lib/hashira/smells/too_many_instance_variables.rb +3 -3
  99. data/lib/hashira/smells/visibility.rb +18 -11
  100. data/lib/hashira/trees.rb +8 -4
  101. data/lib/hashira/version.rb +1 -1
  102. data/lib/hashira.rb +12 -0
  103. metadata +13 -1
@@ -2,28 +2,26 @@
2
2
 
3
3
  class Hashira::Coupling::Census
4
4
  def initialize(project, trees, packaging: :folder)
5
- @catalog = Hashira::Coupling::Catalog.new(Hashira::Coupling::Definitions.new(project, trees))
6
- @placement = Hashira::Coupling::Placement.build(packaging, project, @catalog)
7
- @folding = Hashira::Coupling::NoFolding
8
- @roster = tally
9
- settle
5
+ @project = project
6
+ @trees = trees
7
+ @packaging = packaging
10
8
  end
11
9
 
12
- def packaging = @placement.mode
10
+ def packaging = placement.mode
13
11
 
14
- def types = @roster.types
12
+ def types = roster.types
15
13
 
16
- def prefix = @catalog.prefix
14
+ def prefix = catalog.prefix
17
15
 
18
- def origins = @roster.origins
16
+ def origins = roster.origins
19
17
 
20
- def roots = @catalog.roots
18
+ def roots = catalog.roots
21
19
 
22
- def packages = @roster.packages | @placement.baseline
20
+ def packages = roster.packages | placement.baseline
23
21
 
24
- def folds = @folding.disclosed
22
+ def folds = folding.disclosed
25
23
 
26
- def strip(segments) = @catalog.strip(segments)
24
+ def strip(segments) = catalog.strip(segments)
27
25
 
28
26
  def holder(segments)
29
27
  path = strip(segments)
@@ -34,22 +32,44 @@ class Hashira::Coupling::Census
34
32
 
35
33
  def pinpoint(segments) = scope.pinpoint(segments)
36
34
 
37
- def charge(file, nesting) = translate(@placement.charge(file, nesting))
35
+ def charge(file, nesting) = translate(placement.charge(file, nesting))
38
36
 
39
37
  private
40
38
 
41
- def type?(segments) = @roster.type?(segments)
39
+ def catalog = @_catalog ||= Hashira::Coupling::Catalog.new(Hashira::Coupling::Definitions.new(@project, @trees))
42
40
 
43
- def tally
44
- Hashira::Coupling::Roster.new(@placement.placed.map { |definition, package| [definition, translate(package)] })
41
+ def placement = @_placement ||= Hashira::Coupling::Placement.build(@packaging, @project, catalog)
42
+
43
+ def roster
44
+ settled
45
+ @_roster
46
+ end
47
+
48
+ def folding
49
+ settled
50
+ @_folding
51
+ end
52
+
53
+ def settled
54
+ return if @_settled
55
+ @_settled = true
56
+ @_folding = Hashira::Coupling::NoFolding
57
+ @_roster = tally
58
+ refold
59
+ end
60
+
61
+ def refold
62
+ @_folding = placement.folding(self)
63
+ @_roster = tally unless @_folding.map.empty?
45
64
  end
46
65
 
47
- def settle
48
- @folding = @placement.folding(self)
49
- @roster = tally unless @folding.map.empty?
66
+ def type?(segments) = roster.type?(segments)
67
+
68
+ def tally
69
+ Hashira::Coupling::Roster.new(placement.placed.map { |definition, package| [definition, translate(package)] })
50
70
  end
51
71
 
52
- def translate(package) = @folding.map.fetch(package, package)
72
+ def translate(package) = folding.map.fetch(package, package)
53
73
 
54
- def scope = Hashira::Coupling::Scope.new(@roster.registry, @catalog, @placement)
74
+ def scope = Hashira::Coupling::Scope.new(roster.registry, catalog, placement)
55
75
  end
@@ -4,8 +4,6 @@ class Hashira::Coupling::CycleSearch
4
4
  def initialize(dependencies, package)
5
5
  @dependencies = dependencies
6
6
  @package = package
7
- @predecessor = {}
8
- @queue = dependencies[package].to_a.each { @predecessor[it] = package }
9
7
  end
10
8
 
11
9
  def path
@@ -14,8 +12,14 @@ class Hashira::Coupling::CycleSearch
14
12
 
15
13
  private
16
14
 
15
+ def prepare
16
+ @_predecessor = {}
17
+ @_queue = @dependencies[@package].to_a.each { @_predecessor[it] = @package }
18
+ end
19
+
17
20
  def cycle?
18
- while (node = @queue.shift)
21
+ prepare
22
+ while (node = @_queue.shift)
19
23
  return true if node == @package
20
24
  visit(node)
21
25
  end
@@ -23,15 +27,15 @@ class Hashira::Coupling::CycleSearch
23
27
 
24
28
  def visit(node)
25
29
  @dependencies[node].each do |neighbor|
26
- next if @predecessor.key?(neighbor)
27
- @predecessor[neighbor] = node
28
- @queue << neighbor
30
+ next if @_predecessor.key?(neighbor)
31
+ @_predecessor[neighbor] = node
32
+ @_queue << neighbor
29
33
  end
30
34
  end
31
35
 
32
36
  def unwind(path)
33
37
  first = path.first
34
38
  return path if first == @package && path.size > 1
35
- unwind(path.unshift(@predecessor[first]))
39
+ unwind(path.unshift(@_predecessor[first]))
36
40
  end
37
41
  end
@@ -14,16 +14,10 @@ class Hashira::Coupling::Definitions
14
14
 
15
15
  def packages = @trees.keys.map { @project.package(it) }.uniq
16
16
 
17
- def roots
18
- @roots ||= @trees.each_value.with_object(Set.new) { |tree, set| survey(tree, set) }
19
- end
17
+ def roots = @_roots ||= Hashira::Analysis::TypeWalk.roots(@trees)
20
18
 
21
19
  private
22
20
 
23
- def survey(tree, set)
24
- Hashira::Analysis::TypeWalk.each(tree) { |_node, full| set << full }
25
- end
26
-
27
21
  def scan(file, tree)
28
22
  package = @project.package(file)
29
23
  Hashira::Analysis::TypeWalk.each(tree, roots: roots) do |node, full|
@@ -3,15 +3,16 @@
3
3
  class Hashira::Coupling::EdgeMap
4
4
  def initialize(census)
5
5
  @census = census
6
- @dependencies = sets
7
- @evidence = sets
8
- @usage = sets
9
6
  end
10
7
 
11
- attr_reader :dependencies, :evidence, :usage
8
+ def dependencies = @_dependencies ||= sets
9
+
10
+ def evidence = @_evidence ||= sets
11
+
12
+ def usage = @_usage ||= sets
12
13
 
13
14
  def record(source, file, tree)
14
- Hashira::Coupling::References.sightings(tree, @census.roots).each do |segments, line, nesting, home|
15
+ Hashira::Coupling::References.new(@census.roots).sightings(tree).each do |segments, line, nesting, home|
15
16
  note(@census.charge(file, home), locate(segments, nesting), segments, "#{source}:#{line}")
16
17
  end
17
18
  end
@@ -26,8 +27,8 @@ class Hashira::Coupling::EdgeMap
26
27
 
27
28
  def note(from, to, segments, site)
28
29
  return unless to && to != from
29
- @dependencies[from] << to
30
- @evidence[[from, to]] << "#{site}: #{segments.join("::")}"
31
- @usage[[from, to]] << @census.holder(segments)
30
+ dependencies[from] << to
31
+ evidence[[from, to]] << "#{site}: #{segments.join("::")}"
32
+ usage[[from, to]] << @census.holder(segments)
32
33
  end
33
34
  end
@@ -11,7 +11,7 @@ class Hashira::Coupling::Folding
11
11
  @suffixes = suffixes
12
12
  end
13
13
 
14
- def map = @map ||= links.keys.to_h { [it, settle(it, links)] }.reject { |from, to| from == to }
14
+ def map = @_map ||= links.keys.to_h { [it, settle(it, links)] }.reject { |from, to| from == to }
15
15
 
16
16
  def disclosed
17
17
  map.map { |from, to| { from:, to:, via: parents.key?(from) ? "base" : "suffix" } }
@@ -19,10 +19,10 @@ class Hashira::Coupling::Folding
19
19
 
20
20
  private
21
21
 
22
- def links = @links ||= named.merge(parents)
22
+ def links = @_links ||= named.merge(parents)
23
23
 
24
24
  def parents
25
- @parents ||= singles.select { |_name, one| one.superclass }.to_h { |name, one| [name, target(one)] }.compact
25
+ @_parents ||= singles.select { |_name, one| one.superclass }.to_h { |name, one| [name, target(one)] }.compact
26
26
  end
27
27
 
28
28
  def named
@@ -38,11 +38,11 @@ class Hashira::Coupling::Folding
38
38
 
39
39
  def trims?(name, suffix) = name.end_with?(suffix) && name != suffix
40
40
 
41
- def singles = @singles ||= lone.group_by(&:name).transform_values { it.find(&:superclass) || it.last }
41
+ def singles = @_singles ||= lone.group_by(&:name).transform_values { it.find(&:superclass) || it.last }
42
42
 
43
43
  def lone = @definitions.select(&:singular?).reject { anchored.include?(it.name) }
44
44
 
45
- def anchored = @anchored ||= @definitions.select(&:nested?).to_set(&:name)
45
+ def anchored = @_anchored ||= @definitions.select(&:nested?).to_set(&:name)
46
46
 
47
47
  def target(one) = @census.pinpoint(Hashira::Analysis::Syntax.segments(one.superclass))
48
48
 
@@ -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 }.reject(&:schema?)
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)
@@ -1,10 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "digest"
4
+ require "prism"
4
5
 
5
6
  class Hashira::Duplication::Fragment
6
7
  DIGEST_LENGTH = 12
7
8
 
9
+ SCHEMA = %i[
10
+ class_node module_node statements_node arguments_node assoc_node
11
+ array_node hash_node keyword_hash_node constant_read_node constant_path_node
12
+ symbol_node string_node integer_node float_node true_node false_node nil_node
13
+ ].freeze
14
+
8
15
  def initialize(file, roots)
9
16
  @file = file
10
17
  @roots = roots
@@ -12,7 +19,7 @@ class Hashira::Duplication::Fragment
12
19
 
13
20
  attr_reader :file
14
21
 
15
- def types = @types ||= nodes.map(&:type)
22
+ def types = @_types ||= nodes.map(&:type)
16
23
 
17
24
  def digest = Digest::SHA256.hexdigest(shape).slice(0, DIGEST_LENGTH)
18
25
 
@@ -20,6 +27,8 @@ class Hashira::Duplication::Fragment
20
27
 
21
28
  def mass = types.size
22
29
 
30
+ def schema? = nodes.all? { directive?(it) }
31
+
23
32
  def line = @roots.first.location.start_line
24
33
 
25
34
  def finish = @roots.last.location.end_line
@@ -34,5 +43,14 @@ class Hashira::Duplication::Fragment
34
43
 
35
44
  def touches?(others) = others.any? { overlaps?(it) }
36
45
 
37
- def nodes = @nodes ||= @roots.flat_map { Hashira::Analysis::NodeWalk.collect(it) }
46
+ def nodes = @_nodes ||= @roots.flat_map { Hashira::Analysis::NodeWalk.collect(it) }
47
+
48
+ private
49
+
50
+ def directive?(node)
51
+ return SCHEMA.include?(node.type) unless node.is_a?(Prism::CallNode)
52
+ !node.receiver && !node.block && literals?(node.arguments)
53
+ end
54
+
55
+ def literals?(arguments) = Hashira::Duplication::Literal.new(arguments).literals?
38
56
  end