hashira 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +155 -0
- data/README.md +127 -25
- data/lib/hashira/analysis/finding.rb +7 -6
- data/lib/hashira/analysis/node_walk.rb +3 -3
- data/lib/hashira/analysis/syntax.rb +21 -6
- data/lib/hashira/analysis/type_walk.rb +7 -9
- data/lib/hashira/churn.rb +11 -13
- data/lib/hashira/ci/accepted.rb +27 -31
- data/lib/hashira/ci/baseline.rb +22 -24
- data/lib/hashira/ci/diff.rb +8 -7
- data/lib/hashira/ci/edge_diff_report.rb +13 -17
- data/lib/hashira/ci/finding_diff_report.rb +14 -18
- data/lib/hashira/ci/gate.rb +19 -23
- data/lib/hashira/ci/improvement.rb +9 -14
- data/lib/hashira/ci/ratchet.rb +32 -27
- data/lib/hashira/ci/ratchet_report.rb +26 -32
- data/lib/hashira/cli/arguments.rb +23 -0
- data/lib/hashira/cli/command_line.rb +55 -71
- data/lib/hashira/cli/fail_on.rb +24 -19
- data/lib/hashira/cli/options.rb +9 -4
- data/lib/hashira/cli/package_by.rb +14 -0
- data/lib/hashira/cli/run.rb +33 -27
- data/lib/hashira/cli/skip.rb +14 -20
- data/lib/hashira/cli/usage.rb +44 -41
- data/lib/hashira/cli.rb +17 -19
- data/lib/hashira/complexity/boolean_run.rb +13 -15
- data/lib/hashira/complexity/cognitive_score.rb +68 -66
- data/lib/hashira/complexity/if_chain.rb +33 -38
- data/lib/hashira/complexity/method_finding.rb +20 -42
- data/lib/hashira/complexity/method_score.rb +9 -7
- data/lib/hashira/complexity/rescue_scan.rb +16 -21
- data/lib/hashira/complexity/rollup.rb +13 -15
- data/lib/hashira/complexity/scores.rb +44 -0
- data/lib/hashira/coupling/audiences.rb +49 -0
- data/lib/hashira/coupling/catalog.rb +27 -0
- data/lib/hashira/coupling/census.rb +55 -0
- data/lib/hashira/coupling/constant_registry.rb +47 -0
- data/lib/hashira/coupling/cycle_findings.rb +31 -0
- data/lib/hashira/coupling/cycle_search.rb +37 -0
- data/lib/hashira/coupling/cycles.rb +14 -0
- data/lib/hashira/coupling/definition.rb +22 -0
- data/lib/hashira/coupling/definitions.rb +31 -0
- data/lib/hashira/coupling/edge.rb +10 -0
- data/lib/hashira/coupling/edge_map.rb +33 -0
- data/lib/hashira/coupling/folder_placement.rb +17 -0
- data/lib/hashira/coupling/folding.rb +56 -0
- data/lib/hashira/coupling/graph.rb +63 -0
- data/lib/hashira/coupling/metric.rb +15 -0
- data/lib/hashira/coupling/mixed_audience_findings.rb +32 -0
- data/lib/hashira/coupling/namespace_placement.rb +28 -0
- data/lib/hashira/{analysis → coupling}/namespace_prefix.rb +11 -11
- data/lib/hashira/coupling/naming.rb +14 -0
- data/lib/hashira/coupling/no_folding.rb +13 -0
- data/lib/hashira/coupling/placement.rb +19 -0
- data/lib/hashira/coupling/references.rb +55 -0
- data/lib/hashira/coupling/report.rb +28 -0
- data/lib/hashira/coupling/roll_call.rb +45 -0
- data/lib/hashira/coupling/roll_call_findings.rb +30 -0
- data/lib/hashira/coupling/roster.rb +25 -0
- data/lib/hashira/coupling/rule.rb +18 -0
- data/lib/hashira/coupling/scope.rb +28 -0
- data/lib/hashira/coupling/sdp_check.rb +14 -0
- data/lib/hashira/coupling/sdp_violation_findings.rb +25 -0
- data/lib/hashira/coupling/wide_edge_findings.rb +25 -0
- data/lib/hashira/coupling/words.rb +29 -0
- data/lib/hashira/diagram/dot.rb +8 -11
- data/lib/hashira/diagram/mermaid.rb +11 -16
- data/lib/hashira/diagram/source.rb +15 -0
- data/lib/hashira/duplication/clones.rb +17 -0
- data/lib/hashira/duplication/cluster.rb +14 -13
- data/lib/hashira/duplication/clusters.rb +46 -0
- data/lib/hashira/duplication/delta.rb +21 -30
- data/lib/hashira/duplication/duplication_finding.rb +17 -23
- data/lib/hashira/duplication/fragment.rb +19 -23
- data/lib/hashira/duplication/grouping.rb +12 -16
- data/lib/hashira/duplication/harvest.rb +27 -0
- data/lib/hashira/duplication/index.rb +20 -24
- data/lib/hashira/duplication/maximal.rb +11 -15
- data/lib/hashira/duplication/near_miss.rb +19 -22
- data/lib/hashira/duplication/sequence.rb +20 -22
- data/lib/hashira/duplication/similarity.rb +31 -34
- data/lib/hashira/duplication/union_find.rb +10 -14
- data/lib/hashira/duplication/variance.rb +35 -43
- data/lib/hashira/error.rb +4 -2
- data/lib/hashira/hotspots/file_cost.rb +8 -7
- data/lib/hashira/hotspots/rollup.rb +19 -21
- data/lib/hashira/pipeline.rb +41 -31
- data/lib/hashira/project.rb +50 -49
- data/lib/hashira/report/complexity_table.rb +27 -31
- data/lib/hashira/report/dependency_map.rb +17 -22
- data/lib/hashira/report/finding_lines.rb +15 -19
- data/lib/hashira/report/graph_payload.rb +14 -20
- data/lib/hashira/report/hotspot_table.rb +32 -37
- data/lib/hashira/report/json.rb +32 -32
- data/lib/hashira/report/metrics_table.rb +46 -33
- data/lib/hashira/report/phrases.rb +98 -0
- data/lib/hashira/report/smell_phrases.rb +68 -0
- data/lib/hashira/report/text.rb +59 -52
- data/lib/hashira/smells/census.rb +32 -0
- data/lib/hashira/smells/check.rb +30 -0
- data/lib/hashira/smells/conditions.rb +44 -0
- data/lib/hashira/smells/contexts.rb +64 -0
- data/lib/hashira/smells/control_parameter.rb +22 -0
- data/lib/hashira/smells/data_clump.rb +30 -0
- data/lib/hashira/smells/duplicate_method_call.rb +54 -0
- data/lib/hashira/smells/feature_envy.rb +17 -0
- data/lib/hashira/smells/instance_variable_assumption.rb +44 -0
- data/lib/hashira/smells/manual_dispatch.rb +16 -0
- data/lib/hashira/smells/module_initialize.rb +9 -0
- data/lib/hashira/smells/nil_check.rb +32 -0
- data/lib/hashira/smells/param_check.rb +51 -0
- data/lib/hashira/smells/refs.rb +53 -0
- data/lib/hashira/smells/repeated_conditional.rb +29 -0
- data/lib/hashira/smells/report.rb +31 -0
- data/lib/hashira/smells/scope.rb +29 -0
- data/lib/hashira/smells/too_many_instance_variables.rb +24 -0
- data/lib/hashira/smells/utility_function.rb +13 -0
- data/lib/hashira/smells/visibility.rb +64 -0
- data/lib/hashira/version.rb +1 -1
- data/lib/hashira.rb +131 -60
- metadata +62 -20
- data/lib/hashira/analysis/census.rb +0 -39
- data/lib/hashira/analysis/constant_registry.rb +0 -40
- data/lib/hashira/analysis/cycle_findings.rb +0 -33
- data/lib/hashira/analysis/cycle_search.rb +0 -44
- data/lib/hashira/analysis/definitions.rb +0 -27
- data/lib/hashira/analysis/edge.rb +0 -9
- data/lib/hashira/analysis/edge_map.rb +0 -36
- data/lib/hashira/analysis/graph.rb +0 -56
- data/lib/hashira/analysis/metric.rb +0 -14
- data/lib/hashira/analysis/references.rb +0 -39
- data/lib/hashira/analysis/rule.rb +0 -22
- data/lib/hashira/analysis/sdp_check.rb +0 -18
- data/lib/hashira/analysis/sdp_violation_findings.rb +0 -27
- data/lib/hashira/complexity/analyzer.rb +0 -45
- data/lib/hashira/diagram/renderer.rb +0 -20
- data/lib/hashira/duplication/analyzer.rb +0 -21
- data/lib/hashira/duplication/clusterer.rb +0 -50
- data/lib/hashira/duplication/extractor.rb +0 -31
|
@@ -1,30 +1,24 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
@churn = churn
|
|
9
|
-
end
|
|
10
|
-
|
|
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
|
|
16
|
-
|
|
17
|
-
private
|
|
3
|
+
class Hashira::Duplication::DuplicationFinding
|
|
4
|
+
def initialize(cluster, churn)
|
|
5
|
+
@cluster = cluster
|
|
6
|
+
@churn = churn
|
|
7
|
+
end
|
|
18
8
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
9
|
+
def to_finding
|
|
10
|
+
site = @cluster.canonical
|
|
11
|
+
Hashira::Analysis::Finding.new(kind: "duplication", package: site.location, digest: site.digest, detail:, evidence:)
|
|
12
|
+
end
|
|
22
13
|
|
|
23
|
-
|
|
14
|
+
private
|
|
24
15
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
16
|
+
def detail
|
|
17
|
+
{
|
|
18
|
+
size: @cluster.size, mass: @cluster.mass,
|
|
19
|
+
kind: Hashira::Duplication::Delta.new(@cluster).kind, hot: @churn.hot?(@cluster.sites)
|
|
20
|
+
}
|
|
29
21
|
end
|
|
22
|
+
|
|
23
|
+
def evidence = @cluster.sites.sort_by(&:rank).map(&:range)
|
|
30
24
|
end
|
|
@@ -2,41 +2,37 @@
|
|
|
2
2
|
|
|
3
3
|
require "digest"
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class Fragment
|
|
8
|
-
DIGEST_LENGTH = 12
|
|
5
|
+
class Hashira::Duplication::Fragment
|
|
6
|
+
DIGEST_LENGTH = 12
|
|
9
7
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
def initialize(file, roots)
|
|
9
|
+
@file = file
|
|
10
|
+
@roots = roots
|
|
11
|
+
end
|
|
14
12
|
|
|
15
|
-
|
|
13
|
+
attr_reader :file
|
|
16
14
|
|
|
17
|
-
|
|
15
|
+
def types = @types ||= nodes.map(&:type)
|
|
18
16
|
|
|
19
|
-
|
|
17
|
+
def digest = Digest::SHA256.hexdigest(shape).slice(0, DIGEST_LENGTH)
|
|
20
18
|
|
|
21
|
-
|
|
19
|
+
def shape = types.join(",")
|
|
22
20
|
|
|
23
|
-
|
|
21
|
+
def mass = types.size
|
|
24
22
|
|
|
25
|
-
|
|
23
|
+
def line = @roots.first.location.start_line
|
|
26
24
|
|
|
27
|
-
|
|
25
|
+
def finish = @roots.last.location.end_line
|
|
28
26
|
|
|
29
|
-
|
|
27
|
+
def location = "#{file}:#{line}"
|
|
30
28
|
|
|
31
|
-
|
|
29
|
+
def range = "#{file}:#{line}-#{finish}"
|
|
32
30
|
|
|
33
|
-
|
|
31
|
+
def rank = [file, line]
|
|
34
32
|
|
|
35
|
-
|
|
33
|
+
def overlaps?(other) = file == other.file && line <= other.finish && other.line <= finish
|
|
36
34
|
|
|
37
|
-
|
|
35
|
+
def touches?(others) = others.any? { overlaps?(it) }
|
|
38
36
|
|
|
39
|
-
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
@group = group
|
|
8
|
-
end
|
|
3
|
+
class Hashira::Duplication::Grouping
|
|
4
|
+
def initialize(group)
|
|
5
|
+
@group = group
|
|
6
|
+
end
|
|
9
7
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
def cluster
|
|
9
|
+
sites = distinct
|
|
10
|
+
Hashira::Duplication::Cluster.new(sites) if sites.size >= 2
|
|
11
|
+
end
|
|
14
12
|
|
|
15
|
-
|
|
13
|
+
private
|
|
16
14
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
class Hashira::Duplication::Harvest
|
|
6
|
+
WHOLE = [Prism::DefNode, Prism::WhenNode, Prism::RescueNode].freeze
|
|
7
|
+
|
|
8
|
+
def initialize(project, trees)
|
|
9
|
+
@project = project
|
|
10
|
+
@fragments = trees.flat_map { |path, tree| scan(@project.relative(path), tree) }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
attr_reader :fragments
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
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
|
|
21
|
+
|
|
22
|
+
def windows(rel, nodes) = runs(nodes).flat_map { Hashira::Duplication::Sequence.new(rel, it).fragments }
|
|
23
|
+
|
|
24
|
+
def runs(nodes) = nodes.filter_map { it.body if it.is_a?(Prism::StatementsNode) }
|
|
25
|
+
|
|
26
|
+
def wholes(nodes) = nodes.select { WHOLE.include?(it.class) }
|
|
27
|
+
end
|
|
@@ -1,35 +1,31 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
RARE = 2
|
|
7
|
-
MAX_BUCKET = 60
|
|
3
|
+
class Hashira::Duplication::Index
|
|
4
|
+
RARE = 2
|
|
5
|
+
MAX_BUCKET = 60
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
def initialize(fragments)
|
|
8
|
+
@fragments = fragments
|
|
9
|
+
@frequency = frequencies(fragments)
|
|
10
|
+
end
|
|
13
11
|
|
|
14
|
-
|
|
12
|
+
def buckets = grouped.values.select { |bucket| bucket.size.between?(2, MAX_BUCKET) }
|
|
15
13
|
|
|
16
|
-
|
|
14
|
+
private
|
|
17
15
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
def frequencies(fragments)
|
|
17
|
+
fragments.each_with_object(Hash.new(0)) { |fragment, counts| tally(counts, fragment) }
|
|
18
|
+
end
|
|
21
19
|
|
|
22
|
-
|
|
20
|
+
def tally(counts, fragment) = fragment.types.uniq.each { |type| counts[type] += 1 }
|
|
23
21
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
28
|
+
def file(index, fragment) = rarest(fragment).each { |type| index[type] << fragment }
|
|
31
29
|
|
|
32
|
-
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
11
|
+
def pairs = Hashira::Duplication::Index.new(@fragments).buckets.flat_map { |bucket| verified(bucket) }.uniq
|
|
16
12
|
|
|
17
|
-
|
|
13
|
+
private
|
|
18
14
|
|
|
19
|
-
|
|
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
|
-
|
|
23
|
-
|
|
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
|
-
|
|
22
|
+
def drifted?(first, second)
|
|
23
|
+
first != second && Hashira::Duplication::Similarity.new(first, second).meets?(THRESHOLD)
|
|
24
|
+
end
|
|
26
25
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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,36 +1,34 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
11
|
-
@file = file
|
|
12
|
-
@statements = statements
|
|
13
|
-
end
|
|
13
|
+
def fragments = segments.flat_map { windows(it) }
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
private
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
def segments = runs.chunk { listing?(it) }.filter_map { |listed, group| group.flatten(1) unless listed }
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
def listing?(run) = run.size >= LIST_RUN
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
def runs = shaped.slice_when { |left, right| left.last != right.last }.map { it.map(&:first) }
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
def shaped = @statements.map { [it, fragment([it]).types] }
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
def windows(segment) = lengths(segment).flat_map { |length| slide(segment, length) }
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
def lengths(segment) = MIN_STATEMENTS..[segment.size, MAX_STATEMENTS].min
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
def slide(segment, length) = spans(segment, length).map { fragment(segment[it, length]) }
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
def spans(segment, length) = 0..(segment.size - length)
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
end
|
|
35
|
-
end
|
|
33
|
+
def fragment(roots) = Hashira::Duplication::Fragment.new(@file, roots)
|
|
36
34
|
end
|
|
@@ -1,50 +1,47 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
@right = right
|
|
9
|
-
end
|
|
3
|
+
class Hashira::Duplication::Similarity
|
|
4
|
+
def initialize(left, right)
|
|
5
|
+
@left = left
|
|
6
|
+
@right = right
|
|
7
|
+
end
|
|
10
8
|
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
def ratio
|
|
10
|
+
return 0.0 if @left.empty? || @right.empty?
|
|
11
|
+
normalized(lcs)
|
|
12
|
+
end
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
end
|
|
14
|
+
def meets?(threshold) = ceiling >= threshold && ratio >= threshold
|
|
16
15
|
|
|
17
|
-
|
|
16
|
+
private
|
|
18
17
|
|
|
19
|
-
|
|
18
|
+
def ceiling = normalized(overlap)
|
|
20
19
|
|
|
21
|
-
|
|
20
|
+
def normalized(length) = (2.0 * length) / (@left.size + @right.size)
|
|
22
21
|
|
|
23
|
-
|
|
22
|
+
def overlap
|
|
23
|
+
@left.count { taken?(@right.tally, it) }
|
|
24
|
+
end
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
def taken?(counts, token)
|
|
27
|
+
return false unless counts.fetch(token, 0).positive?
|
|
28
|
+
counts[token] -= 1
|
|
29
|
+
true
|
|
30
|
+
end
|
|
29
31
|
|
|
30
|
-
|
|
31
|
-
return false unless counts.fetch(token, 0).positive?
|
|
32
|
+
def lcs = @left.reduce(blank) { |prev, token| advance(prev, token) }.last
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
true
|
|
35
|
-
end
|
|
34
|
+
def blank = Array.new(@right.size + 1, 0)
|
|
36
35
|
|
|
37
|
-
|
|
36
|
+
def advance(prev, token)
|
|
37
|
+
@right.each_index.reduce([0]) { |row, index| row << cell(prev, row, token, index) }
|
|
38
|
+
end
|
|
38
39
|
|
|
39
|
-
|
|
40
|
+
def cell(prev, row, token, index)
|
|
41
|
+
match?(token, index) ? prev[index] + 1 : carry(prev, row, index)
|
|
42
|
+
end
|
|
40
43
|
|
|
41
|
-
|
|
42
|
-
@right.each_index.reduce([0]) { |row, index| row << cell(prev, row, token, index) }
|
|
43
|
-
end
|
|
44
|
+
def match?(token, index) = @right[index] == token
|
|
44
45
|
|
|
45
|
-
|
|
46
|
-
@right[index] == token ? prev[index] + 1 : [prev[index + 1], row[index]].max
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
end
|
|
46
|
+
def carry(prev, row, index) = [prev[index + 1], row[index]].max
|
|
50
47
|
end
|
|
@@ -1,21 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
@parent = {}
|
|
8
|
-
end
|
|
3
|
+
class Hashira::Duplication::UnionFind
|
|
4
|
+
def initialize
|
|
5
|
+
@parent = {}
|
|
6
|
+
end
|
|
9
7
|
|
|
10
|
-
|
|
8
|
+
def union(left, right) = @parent[root(left)] = root(right)
|
|
11
9
|
|
|
12
|
-
|
|
10
|
+
def clusters = @parent.keys.group_by { root(it) }.values
|
|
13
11
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
|
@@ -1,57 +1,49 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
@other = other
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def kinds
|
|
19
|
-
return [:structure] if @canonical.types != @other.types
|
|
20
|
-
|
|
21
|
-
differing.map { |node| category(node) }.uniq
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def shape_only?
|
|
25
|
-
return false unless @canonical.types == @other.types
|
|
3
|
+
class Hashira::Duplication::Variance
|
|
4
|
+
LITERALS = %i[integer_node float_node string_node symbol_node].freeze
|
|
5
|
+
VALUED = %i[integer_node float_node].freeze
|
|
6
|
+
NAMED = %i[call_node constant_read_node constant_path_node
|
|
7
|
+
local_variable_read_node local_variable_write_node
|
|
8
|
+
instance_variable_read_node instance_variable_write_node].freeze
|
|
9
|
+
CONSTANTS = %i[constant_read_node constant_path_node].freeze
|
|
10
|
+
|
|
11
|
+
def initialize(canonical, other)
|
|
12
|
+
@canonical = canonical
|
|
13
|
+
@other = other
|
|
14
|
+
end
|
|
26
15
|
|
|
27
|
-
|
|
28
|
-
|
|
16
|
+
def kinds
|
|
17
|
+
return [:structure] if @canonical.types != @other.types
|
|
18
|
+
differing.map { |node| category(node) }.uniq
|
|
19
|
+
end
|
|
29
20
|
|
|
30
|
-
|
|
21
|
+
def structural?
|
|
22
|
+
return false unless @canonical.types == @other.types
|
|
23
|
+
named.any? && named.all? { |left, right| left.name != right.name }
|
|
24
|
+
end
|
|
31
25
|
|
|
32
|
-
|
|
26
|
+
private
|
|
33
27
|
|
|
34
|
-
|
|
28
|
+
def pairs = @canonical.nodes.zip(@other.nodes)
|
|
35
29
|
|
|
36
|
-
|
|
30
|
+
def named = @named ||= pairs.select { |left, _| NAMED.include?(left.type) }
|
|
37
31
|
|
|
38
|
-
|
|
32
|
+
def differing = pairs.select { |pair| varies?(*pair) }.map(&:first)
|
|
39
33
|
|
|
40
|
-
|
|
41
|
-
type = node.type
|
|
42
|
-
return :literal if LITERALS.include?(type)
|
|
34
|
+
def varies?(left, right) = signature(left) != signature(right)
|
|
43
35
|
|
|
44
|
-
|
|
45
|
-
|
|
36
|
+
def category(node)
|
|
37
|
+
type = node.type
|
|
38
|
+
return :literal if LITERALS.include?(type)
|
|
39
|
+
CONSTANTS.include?(type) ? :constant : :message
|
|
40
|
+
end
|
|
46
41
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
42
|
+
def signature(node)
|
|
43
|
+
LITERALS.include?(node.type) ? literal(node) : label(node)
|
|
44
|
+
end
|
|
50
45
|
|
|
51
|
-
|
|
52
|
-
end
|
|
46
|
+
def label(node) = (node.name if NAMED.include?(node.type))
|
|
53
47
|
|
|
54
|
-
|
|
55
|
-
end
|
|
56
|
-
end
|
|
48
|
+
def literal(node) = VALUED.include?(node.type) ? node.value : node.unescaped
|
|
57
49
|
end
|
data/lib/hashira/error.rb
CHANGED
|
@@ -4,16 +4,17 @@ module Hashira
|
|
|
4
4
|
module Hotspots
|
|
5
5
|
CHURN_FLOOR = 1
|
|
6
6
|
|
|
7
|
-
FileCost =
|
|
8
|
-
|
|
7
|
+
FileCost =
|
|
8
|
+
Data.define(:file, :cognitive, :duplication, :churn) do
|
|
9
|
+
def cost = cognitive + duplication
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
def rank = cost * heat
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
def heat = [churn, CHURN_FLOOR].max
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
def to_h = { file:, cognitive:, duplication:, churn:, cost:, rank: }
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
def cells = [file, cognitive, duplication, churn, rank]
|
|
18
|
+
end
|
|
18
19
|
end
|
|
19
20
|
end
|