hashira 0.2.0 → 0.4.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 (94) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +141 -0
  3. data/README.md +53 -5
  4. data/lib/hashira/analysis/catalog.rb +27 -0
  5. data/lib/hashira/analysis/census.rb +42 -35
  6. data/lib/hashira/analysis/constant_registry.rb +43 -0
  7. data/lib/hashira/analysis/cycle_findings.rb +34 -29
  8. data/lib/hashira/analysis/cycle_search.rb +28 -35
  9. data/lib/hashira/analysis/cycles.rb +14 -0
  10. data/lib/hashira/analysis/definition.rb +22 -0
  11. data/lib/hashira/analysis/definitions.rb +22 -18
  12. data/lib/hashira/analysis/edge.rb +4 -3
  13. data/lib/hashira/analysis/edge_map.rb +23 -26
  14. data/lib/hashira/analysis/finding.rb +7 -6
  15. data/lib/hashira/analysis/folder_placement.rb +17 -0
  16. data/lib/hashira/analysis/folding.rb +54 -0
  17. data/lib/hashira/analysis/graph.rb +35 -38
  18. data/lib/hashira/analysis/metric.rb +8 -7
  19. data/lib/hashira/analysis/namespace_placement.rb +28 -0
  20. data/lib/hashira/analysis/namespace_prefix.rb +32 -0
  21. data/lib/hashira/analysis/naming.rb +14 -0
  22. data/lib/hashira/analysis/no_folding.rb +13 -0
  23. data/lib/hashira/analysis/node_walk.rb +3 -3
  24. data/lib/hashira/analysis/placement.rb +19 -0
  25. data/lib/hashira/analysis/references.rb +14 -13
  26. data/lib/hashira/analysis/resolver.rb +28 -0
  27. data/lib/hashira/analysis/roster.rb +25 -0
  28. data/lib/hashira/analysis/rule.rb +10 -14
  29. data/lib/hashira/analysis/sdp_check.rb +8 -12
  30. data/lib/hashira/analysis/sdp_violation_findings.rb +20 -18
  31. data/lib/hashira/analysis/syntax.rb +21 -6
  32. data/lib/hashira/analysis/type_walk.rb +7 -9
  33. data/lib/hashira/churn.rb +11 -13
  34. data/lib/hashira/ci/accepted.rb +27 -31
  35. data/lib/hashira/ci/baseline.rb +22 -24
  36. data/lib/hashira/ci/diff.rb +8 -7
  37. data/lib/hashira/ci/edge_diff_report.rb +13 -17
  38. data/lib/hashira/ci/finding_diff_report.rb +14 -18
  39. data/lib/hashira/ci/gate.rb +19 -23
  40. data/lib/hashira/ci/improvement.rb +9 -14
  41. data/lib/hashira/ci/ratchet.rb +36 -27
  42. data/lib/hashira/ci/ratchet_report.rb +26 -32
  43. data/lib/hashira/cli/arguments.rb +23 -0
  44. data/lib/hashira/cli/command_line.rb +55 -71
  45. data/lib/hashira/cli/fail_on.rb +15 -20
  46. data/lib/hashira/cli/options.rb +9 -4
  47. data/lib/hashira/cli/package_by.rb +14 -0
  48. data/lib/hashira/cli/run.rb +29 -27
  49. data/lib/hashira/cli/skip.rb +14 -20
  50. data/lib/hashira/cli/usage.rb +41 -41
  51. data/lib/hashira/cli.rb +17 -19
  52. data/lib/hashira/complexity/analyzer.rb +30 -32
  53. data/lib/hashira/complexity/boolean_run.rb +11 -15
  54. data/lib/hashira/complexity/cognitive_score.rb +61 -66
  55. data/lib/hashira/complexity/if_chain.rb +33 -38
  56. data/lib/hashira/complexity/method_finding.rb +43 -48
  57. data/lib/hashira/complexity/method_score.rb +9 -7
  58. data/lib/hashira/complexity/rescue_scan.rb +16 -21
  59. data/lib/hashira/complexity/rollup.rb +13 -15
  60. data/lib/hashira/diagram/dot.rb +8 -11
  61. data/lib/hashira/diagram/mermaid.rb +11 -16
  62. data/lib/hashira/diagram/renderer.rb +10 -15
  63. data/lib/hashira/duplication/analyzer.rb +10 -14
  64. data/lib/hashira/duplication/cluster.rb +15 -12
  65. data/lib/hashira/duplication/clusterer.rb +31 -29
  66. data/lib/hashira/duplication/delta.rb +29 -30
  67. data/lib/hashira/duplication/duplication_finding.rb +19 -21
  68. data/lib/hashira/duplication/extractor.rb +15 -19
  69. data/lib/hashira/duplication/fragment.rb +19 -23
  70. data/lib/hashira/duplication/grouping.rb +12 -16
  71. data/lib/hashira/duplication/index.rb +20 -24
  72. data/lib/hashira/duplication/maximal.rb +11 -15
  73. data/lib/hashira/duplication/near_miss.rb +19 -22
  74. data/lib/hashira/duplication/sequence.rb +20 -22
  75. data/lib/hashira/duplication/similarity.rb +28 -35
  76. data/lib/hashira/duplication/union_find.rb +10 -14
  77. data/lib/hashira/duplication/variance.rb +36 -44
  78. data/lib/hashira/error.rb +4 -2
  79. data/lib/hashira/hotspots/file_cost.rb +8 -7
  80. data/lib/hashira/hotspots/rollup.rb +19 -21
  81. data/lib/hashira/pipeline.rb +36 -31
  82. data/lib/hashira/project.rb +53 -32
  83. data/lib/hashira/report/complexity_table.rb +27 -31
  84. data/lib/hashira/report/dependency_map.rb +17 -22
  85. data/lib/hashira/report/finding_lines.rb +15 -19
  86. data/lib/hashira/report/graph_payload.rb +14 -20
  87. data/lib/hashira/report/hotspot_table.rb +32 -37
  88. data/lib/hashira/report/json.rb +30 -32
  89. data/lib/hashira/report/metrics_table.rb +46 -33
  90. data/lib/hashira/report/text.rb +59 -52
  91. data/lib/hashira/version.rb +1 -1
  92. data/lib/hashira.rb +92 -55
  93. metadata +16 -2
  94. data/lib/hashira/analysis/root_namespace.rb +0 -14
@@ -1,39 +1,38 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Hashira
4
- module Duplication
5
- class Delta
6
- ADVICE = {
7
- identical: "byte-for-byte identical — extract a shared method and call it from each site.",
8
- literal: "differs only in literal values — extract a method, pass them as arguments.",
9
- message: "differs only in the receiver or message extract a method taking the receiver.",
10
- constant: "differs only in a constant extract a method and parameterize it.",
11
- structure: "the control flow differs — extract the common core, but verify by hand (lower confidence).",
12
- mixed: "extract the shared shape and pass what differs as parameters."
13
- }.freeze
14
-
15
- def initialize(cluster)
16
- @cluster = cluster
17
- end
18
-
19
- def summary = ADVICE.fetch(kind)
3
+ class Hashira::Duplication::Delta
4
+ ADVICE = {
5
+ identical: "byte-for-byte identical — extract a shared method and call it from each site.",
6
+ literal: "differs only in literal values — extract a method, pass them as arguments.",
7
+ message: "differs only in the receiver or message — extract a method taking the receiver.",
8
+ constant: "differs only in a constant — extract a method and parameterize it.",
9
+ structure: "the control flow differs extract the common core, but verify by hand (lower confidence).",
10
+ mixed: "extract the shared shape and pass what differs as parameters."
11
+ }.freeze
12
+
13
+ def initialize(cluster)
14
+ @cluster = cluster
15
+ end
20
16
 
21
- def kind
22
- tags = kinds
23
- return :identical if tags.empty?
24
- return :structure if tags.include?(:structure)
17
+ def summary = ADVICE.fetch(kind)
25
18
 
26
- tags.size == 1 ? tags.first : :mixed
27
- end
19
+ def kind
20
+ tags = kinds
21
+ return :identical if tags.empty?
22
+ return :structure if tags.include?(:structure)
23
+ tags.size == 1 ? tags.first : :mixed
24
+ end
28
25
 
29
- def to_h
30
- { mass: @cluster.mass, sites: @cluster.size, kind:,
31
- locations: @cluster.sites.sort_by(&:sort_key).map(&:range) }
32
- end
26
+ def to_h
27
+ {
28
+ mass: @cluster.mass, sites: @cluster.size, kind:,
29
+ locations: @cluster.sites.sort_by(&:rank).map(&:range)
30
+ }
31
+ end
33
32
 
34
- private
33
+ private
35
34
 
36
- def kinds = @cluster.others.flat_map { |other| Variance.new(@cluster.canonical, other).kinds }.uniq
37
- end
35
+ def kinds
36
+ @cluster.others.flat_map { |other| Hashira::Duplication::Variance.new(@cluster.canonical, other).kinds }.uniq
38
37
  end
39
38
  end
@@ -1,30 +1,28 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Hashira
4
- module Duplication
5
- class DuplicationFinding
6
- def initialize(cluster, churn)
7
- @cluster = cluster
8
- @churn = churn
9
- end
3
+ class Hashira::Duplication::DuplicationFinding
4
+ def initialize(cluster, churn)
5
+ @cluster = cluster
6
+ @churn = churn
7
+ end
10
8
 
11
- def to_finding
12
- site = @cluster.canonical
13
- Analysis::Finding.new(kind: "duplication", package: site.location, digest: site.digest,
14
- cycle: nil, message:, evidence:)
15
- end
9
+ def to_finding
10
+ site = @cluster.canonical
11
+ Hashira::Analysis::Finding.new(
12
+ kind: "duplication", package: site.location, digest: site.digest,
13
+ cycle: nil, message:, evidence:
14
+ )
15
+ end
16
16
 
17
- private
17
+ private
18
18
 
19
- def message
20
- "#{@cluster.size} similar fragments (mass #{@cluster.mass}) — #{Delta.new(@cluster).summary}#{note}"
21
- end
19
+ def message
20
+ "#{@cluster.size} similar fragments (mass #{@cluster.mass}) — #{Hashira::Duplication::Delta.new(@cluster).summary}#{note}"
21
+ end
22
22
 
23
- def evidence = @cluster.sites.sort_by(&:sort_key).map(&:range)
23
+ def evidence = @cluster.sites.sort_by(&:rank).map(&:range)
24
24
 
25
- def note
26
- @churn.hot?(@cluster.sites) ? " Both sites change often — fix one, miss the other." : ""
27
- end
28
- end
25
+ def note
26
+ @churn.hot?(@cluster.sites) ? " Both sites change often — fix one, miss the other." : ""
29
27
  end
30
28
  end
@@ -2,30 +2,26 @@
2
2
 
3
3
  require "prism"
4
4
 
5
- module Hashira
6
- module Duplication
7
- class Extractor
8
- WHOLE = [Prism::DefNode, Prism::WhenNode, Prism::RescueNode].freeze
5
+ class Hashira::Duplication::Extractor
6
+ WHOLE = [Prism::DefNode, Prism::WhenNode, Prism::RescueNode].freeze
9
7
 
10
- def initialize(project, trees)
11
- @project = project
12
- @fragments = trees.flat_map { |path, tree| from_tree(@project.relative(path), tree) }
13
- end
8
+ def initialize(project, trees)
9
+ @project = project
10
+ @fragments = trees.flat_map { |path, tree| scan(@project.relative(path), tree) }
11
+ end
14
12
 
15
- attr_reader :fragments
13
+ attr_reader :fragments
16
14
 
17
- private
15
+ private
18
16
 
19
- def from_tree(rel, tree)
20
- nodes = Analysis::NodeWalk.collect(tree)
21
- windows(rel, nodes) + wholes(nodes).map { Fragment.new(rel, [it]) }
22
- end
17
+ def scan(rel, tree)
18
+ nodes = Hashira::Analysis::NodeWalk.collect(tree)
19
+ windows(rel, nodes) + wholes(nodes).map { Hashira::Duplication::Fragment.new(rel, [it]) }
20
+ end
23
21
 
24
- def windows(rel, nodes) = runs(nodes).flat_map { Sequence.new(rel, it).fragments }
22
+ def windows(rel, nodes) = runs(nodes).flat_map { Hashira::Duplication::Sequence.new(rel, it).fragments }
25
23
 
26
- def runs(nodes) = nodes.filter_map { it.body if it.is_a?(Prism::StatementsNode) }
24
+ def runs(nodes) = nodes.filter_map { it.body if it.is_a?(Prism::StatementsNode) }
27
25
 
28
- def wholes(nodes) = nodes.select { WHOLE.include?(it.class) }
29
- end
30
- end
26
+ def wholes(nodes) = nodes.select { WHOLE.include?(it.class) }
31
27
  end
@@ -2,41 +2,37 @@
2
2
 
3
3
  require "digest"
4
4
 
5
- module Hashira
6
- module Duplication
7
- class Fragment
8
- DIGEST_LENGTH = 12
5
+ class Hashira::Duplication::Fragment
6
+ DIGEST_LENGTH = 12
9
7
 
10
- def initialize(file, roots)
11
- @file = file
12
- @roots = roots
13
- end
8
+ def initialize(file, roots)
9
+ @file = file
10
+ @roots = roots
11
+ end
14
12
 
15
- attr_reader :file
13
+ attr_reader :file
16
14
 
17
- def types = @types ||= nodes.map(&:type)
15
+ def types = @types ||= nodes.map(&:type)
18
16
 
19
- def digest = Digest::SHA256.hexdigest(shape).slice(0, DIGEST_LENGTH)
17
+ def digest = Digest::SHA256.hexdigest(shape).slice(0, DIGEST_LENGTH)
20
18
 
21
- def shape = types.join(",")
19
+ def shape = types.join(",")
22
20
 
23
- def mass = types.size
21
+ def mass = types.size
24
22
 
25
- def line = @roots.first.location.start_line
23
+ def line = @roots.first.location.start_line
26
24
 
27
- def finish = @roots.last.location.end_line
25
+ def finish = @roots.last.location.end_line
28
26
 
29
- def location = "#{file}:#{line}"
27
+ def location = "#{file}:#{line}"
30
28
 
31
- def range = "#{file}:#{line}-#{finish}"
29
+ def range = "#{file}:#{line}-#{finish}"
32
30
 
33
- def sort_key = [file, line]
31
+ def rank = [file, line]
34
32
 
35
- def overlaps?(other) = file == other.file && line <= other.finish && other.line <= finish
33
+ def overlaps?(other) = file == other.file && line <= other.finish && other.line <= finish
36
34
 
37
- def overlaps_any?(others) = others.any? { overlaps?(it) }
35
+ def touches?(others) = others.any? { overlaps?(it) }
38
36
 
39
- def nodes = @nodes ||= @roots.flat_map { Analysis::NodeWalk.collect(it) }
40
- end
41
- end
37
+ def nodes = @nodes ||= @roots.flat_map { Hashira::Analysis::NodeWalk.collect(it) }
42
38
  end
@@ -1,24 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Hashira
4
- module Duplication
5
- class Grouping
6
- def initialize(group)
7
- @group = group
8
- end
3
+ class Hashira::Duplication::Grouping
4
+ def initialize(group)
5
+ @group = group
6
+ end
9
7
 
10
- def cluster
11
- sites = distinct
12
- Cluster.new(sites) if sites.size >= 2
13
- end
8
+ def cluster
9
+ sites = distinct
10
+ Hashira::Duplication::Cluster.new(sites) if sites.size >= 2
11
+ end
14
12
 
15
- private
13
+ private
16
14
 
17
- def distinct
18
- @group.sort_by { -it.mass }.each_with_object([]) do |fragment, kept|
19
- kept << fragment unless fragment.overlaps_any?(kept)
20
- end
21
- end
15
+ def distinct
16
+ @group.sort_by { -it.mass }.each_with_object([]) do |fragment, kept|
17
+ kept << fragment unless fragment.touches?(kept)
22
18
  end
23
19
  end
24
20
  end
@@ -1,35 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Hashira
4
- module Duplication
5
- class Index
6
- RARE = 2
7
- MAX_BUCKET = 60
3
+ class Hashira::Duplication::Index
4
+ RARE = 2
5
+ MAX_BUCKET = 60
8
6
 
9
- def initialize(fragments)
10
- @fragments = fragments
11
- @document_frequency = frequencies(fragments)
12
- end
7
+ def initialize(fragments)
8
+ @fragments = fragments
9
+ @frequency = frequencies(fragments)
10
+ end
13
11
 
14
- def buckets = grouped.values.select { |bucket| bucket.size.between?(2, MAX_BUCKET) }
12
+ def buckets = grouped.values.select { |bucket| bucket.size.between?(2, MAX_BUCKET) }
15
13
 
16
- private
14
+ private
17
15
 
18
- def frequencies(fragments)
19
- fragments.each_with_object(Hash.new(0)) { |fragment, counts| tally(counts, fragment) }
20
- end
16
+ def frequencies(fragments)
17
+ fragments.each_with_object(Hash.new(0)) { |fragment, counts| tally(counts, fragment) }
18
+ end
21
19
 
22
- def tally(counts, fragment) = fragment.types.uniq.each { |type| counts[type] += 1 }
20
+ def tally(counts, fragment) = fragment.types.uniq.each { |type| counts[type] += 1 }
23
21
 
24
- def grouped
25
- index = Hash.new { |hash, type| hash[type] = [] }
26
- @fragments.each { |fragment| file(index, fragment) }
27
- index
28
- end
22
+ def grouped
23
+ index = Hash.new { |hash, type| hash[type] = [] }
24
+ @fragments.each { |fragment| file(index, fragment) }
25
+ index
26
+ end
29
27
 
30
- def file(index, fragment) = rarest(fragment).each { |type| index[type] << fragment }
28
+ def file(index, fragment) = rarest(fragment).each { |type| index[type] << fragment }
31
29
 
32
- def rarest(fragment) = fragment.types.uniq.min_by(RARE) { @document_frequency[it] }
33
- end
34
- end
30
+ def rarest(fragment) = fragment.types.uniq.min_by(RARE) { @frequency[it] }
35
31
  end
@@ -1,21 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Hashira
4
- module Duplication
5
- class Maximal
6
- def initialize(clusters)
7
- @clusters = clusters
8
- end
9
-
10
- def reduced
11
- @clusters.sort_by { -it.mass }.each_with_object([]) do |cluster, kept|
12
- kept << cluster unless shadowed_by?(cluster, kept.flat_map(&:sites))
13
- end
14
- end
15
-
16
- private
3
+ class Hashira::Duplication::Maximal
4
+ def initialize(clusters)
5
+ @clusters = clusters
6
+ end
17
7
 
18
- def shadowed_by?(cluster, bigger) = cluster.sites.all? { it.overlaps_any?(bigger) }
8
+ def reduced
9
+ @clusters.sort_by { -it.mass }.each_with_object([]) do |cluster, kept|
10
+ kept << cluster unless within?(cluster, kept.flat_map(&:sites))
19
11
  end
20
12
  end
13
+
14
+ private
15
+
16
+ def within?(cluster, bigger) = cluster.sites.all? { it.touches?(bigger) }
21
17
  end
@@ -1,33 +1,30 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Hashira
4
- module Duplication
5
- class NearMiss
6
- THRESHOLD = 0.8
7
- MASS_RATIO = 1.5
3
+ class Hashira::Duplication::NearMiss
4
+ THRESHOLD = 0.8
5
+ MASS_RATIO = 1.5
8
6
 
9
- def initialize(fragments)
10
- @fragments = fragments
11
- end
12
-
13
- def pairs = Index.new(@fragments).buckets.flat_map { |bucket| verified(bucket) }.uniq
7
+ def initialize(fragments)
8
+ @fragments = fragments
9
+ end
14
10
 
15
- private
11
+ def pairs = Hashira::Duplication::Index.new(@fragments).buckets.flat_map { |bucket| verified(bucket) }.uniq
16
12
 
17
- def verified(bucket) = bucket.combination(2).select { |left, right| near?(left, right) }
13
+ private
18
14
 
19
- def near?(left, right)
20
- return false unless comparable?(left, right) && !left.overlaps?(right)
15
+ def verified(bucket) = bucket.combination(2).select { |left, right| near?(left, right) }
21
16
 
22
- drifted?(left.types, right.types)
23
- end
17
+ def near?(left, right)
18
+ return false unless comparable?(left, right) && !left.overlaps?(right)
19
+ drifted?(left.types, right.types)
20
+ end
24
21
 
25
- def drifted?(first, second) = first != second && Similarity.new(first, second).at_least?(THRESHOLD)
22
+ def drifted?(first, second)
23
+ first != second && Hashira::Duplication::Similarity.new(first, second).meets?(THRESHOLD)
24
+ end
26
25
 
27
- def comparable?(left, right)
28
- masses = [left.mass, right.mass]
29
- masses.max <= masses.min * MASS_RATIO
30
- end
31
- end
26
+ def comparable?(left, right)
27
+ masses = [left.mass, right.mass]
28
+ masses.max <= masses.min * MASS_RATIO
32
29
  end
33
30
  end
@@ -1,34 +1,32 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Hashira
4
- module Duplication
5
- class Sequence
6
- MIN_STATEMENTS = 1
7
- MAX_STATEMENTS = 12
8
- LIST_RUN = 3
3
+ class Hashira::Duplication::Sequence
4
+ MIN_STATEMENTS = 1
5
+ MAX_STATEMENTS = 12
6
+ LIST_RUN = 3
7
+
8
+ def initialize(file, statements)
9
+ @file = file
10
+ @statements = statements
11
+ end
9
12
 
10
- def initialize(file, statements)
11
- @file = file
12
- @statements = statements
13
- end
13
+ def fragments = segments.flat_map { windows(it) }
14
14
 
15
- def fragments
16
- return [] if listing?
15
+ private
17
16
 
18
- lengths.flat_map { |length| slide(length) }
19
- end
17
+ def segments = runs.chunk { listing?(it) }.filter_map { |listed, group| group.flatten(1) unless listed }
20
18
 
21
- private
19
+ def listing?(run) = run.size >= LIST_RUN
22
20
 
23
- def listing? = @statements.size >= LIST_RUN && shapes.uniq.size == 1
21
+ def runs = shaped.slice_when { |left, right| left.last != right.last }.map { it.map(&:first) }
24
22
 
25
- def shapes = @statements.map { fragment([it]).types }
23
+ def shaped = @statements.map { [it, fragment([it]).types] }
26
24
 
27
- def lengths = MIN_STATEMENTS..[@statements.size, MAX_STATEMENTS].min
25
+ def windows(segment) = lengths(segment).flat_map { |length| slide(segment, length) }
28
26
 
29
- def slide(length) = (0..(@statements.size - length)).map { fragment(@statements[it, length]) }
27
+ def lengths(segment) = MIN_STATEMENTS..[segment.size, MAX_STATEMENTS].min
30
28
 
31
- def fragment(roots) = Fragment.new(@file, roots)
32
- end
33
- end
29
+ def slide(segment, length) = (0..(segment.size - length)).map { fragment(segment[it, length]) }
30
+
31
+ def fragment(roots) = Hashira::Duplication::Fragment.new(@file, roots)
34
32
  end
@@ -1,50 +1,43 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Hashira
4
- module Duplication
5
- class Similarity
6
- def initialize(left, right)
7
- @left = left
8
- @right = right
9
- end
10
-
11
- def ratio
12
- return 0.0 if @left.empty? || @right.empty?
13
-
14
- normalized(lcs)
15
- end
3
+ class Hashira::Duplication::Similarity
4
+ def initialize(left, right)
5
+ @left = left
6
+ @right = right
7
+ end
16
8
 
17
- def at_least?(threshold) = upper_bound >= threshold && ratio >= threshold
9
+ def ratio
10
+ return 0.0 if @left.empty? || @right.empty?
11
+ normalized(lcs)
12
+ end
18
13
 
19
- private
14
+ def meets?(threshold) = ceiling >= threshold && ratio >= threshold
20
15
 
21
- def upper_bound = normalized(tokens_in_common)
16
+ private
22
17
 
23
- def normalized(length) = (2.0 * length) / (@left.size + @right.size)
18
+ def ceiling = normalized(overlap)
24
19
 
25
- def tokens_in_common
26
- counts = @right.tally
27
- @left.count { taken?(counts, it) }
28
- end
20
+ def normalized(length) = (2.0 * length) / (@left.size + @right.size)
29
21
 
30
- def taken?(counts, token)
31
- return false unless counts.fetch(token, 0).positive?
22
+ def overlap
23
+ @left.count { taken?(@right.tally, it) }
24
+ end
32
25
 
33
- counts[token] -= 1
34
- true
35
- end
26
+ def taken?(counts, token)
27
+ return false unless counts.fetch(token, 0).positive?
28
+ counts[token] -= 1
29
+ true
30
+ end
36
31
 
37
- def lcs = @left.reduce(blank) { |prev, token| next_row(prev, token) }.last
32
+ def lcs = @left.reduce(blank) { |prev, token| advance(prev, token) }.last
38
33
 
39
- def blank = Array.new(@right.size + 1, 0)
34
+ def blank = Array.new(@right.size + 1, 0)
40
35
 
41
- def next_row(prev, token)
42
- @right.each_index.reduce([0]) { |row, index| row << cell(prev, row, token, index) }
43
- end
36
+ def advance(prev, token)
37
+ @right.each_index.reduce([0]) { |row, index| row << cell(prev, row, token, index) }
38
+ end
44
39
 
45
- def cell(prev, row, token, index)
46
- @right[index] == token ? prev[index] + 1 : [prev[index + 1], row[index]].max
47
- end
48
- end
40
+ def cell(prev, row, token, index)
41
+ @right[index] == token ? prev[index] + 1 : [prev[index + 1], row[index]].max
49
42
  end
50
43
  end
@@ -1,21 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Hashira
4
- module Duplication
5
- class UnionFind
6
- def initialize
7
- @parent = {}
8
- end
3
+ class Hashira::Duplication::UnionFind
4
+ def initialize
5
+ @parent = {}
6
+ end
9
7
 
10
- def union(left, right) = @parent[root(left)] = root(right)
8
+ def union(left, right) = @parent[root(left)] = root(right)
11
9
 
12
- def clusters = @parent.keys.group_by { root(it) }.values
10
+ def clusters = @parent.keys.group_by { root(it) }.values
13
11
 
14
- def root(node)
15
- @parent[node] = node unless @parent.key?(node)
16
- found = @parent[node]
17
- found == node ? node : (@parent[node] = root(found))
18
- end
19
- end
12
+ def root(node)
13
+ @parent[node] = node unless @parent.key?(node)
14
+ found = @parent[node]
15
+ found == node ? node : (@parent[node] = root(found))
20
16
  end
21
17
  end