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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +94 -0
- data/README.md +57 -3
- data/exe/hashira +1 -1
- data/lib/hashira/analysis/finding.rb +11 -1
- data/lib/hashira/analysis/syntax.rb +6 -6
- data/lib/hashira/analysis/type_walk.rb +4 -0
- data/lib/hashira/churn.rb +1 -10
- data/lib/hashira/ci/accepted.rb +9 -7
- data/lib/hashira/ci/baseline.rb +53 -33
- data/lib/hashira/ci/comparison.rb +33 -0
- data/lib/hashira/ci/diff.rb +0 -15
- data/lib/hashira/ci/gate.rb +3 -3
- data/lib/hashira/ci/mark.rb +7 -0
- data/lib/hashira/ci/ratchet.rb +12 -4
- data/lib/hashira/ci/ratchet_report.rb +4 -4
- data/lib/hashira/ci/scope.rb +0 -1
- data/lib/hashira/ci/slice.rb +21 -0
- data/lib/hashira/ci/sweep.rb +13 -0
- data/lib/hashira/cli/arguments.rb +12 -8
- data/lib/hashira/cli/choice.rb +9 -0
- data/lib/hashira/cli/command_line.rb +8 -5
- data/lib/hashira/cli/fail_on.rb +6 -6
- data/lib/hashira/cli/flag.rb +3 -1
- data/lib/hashira/cli/flags.rb +10 -0
- data/lib/hashira/cli/format.rb +1 -1
- data/lib/hashira/cli/needs.rb +13 -2
- data/lib/hashira/cli/only.rb +16 -0
- data/lib/hashira/cli/options.rb +8 -10
- data/lib/hashira/cli/package_by.rb +1 -1
- data/lib/hashira/cli/run.rb +9 -8
- data/lib/hashira/cli.rb +22 -21
- data/lib/hashira/complexity/cognitive_score.rb +44 -33
- data/lib/hashira/complexity/method_finding.rb +7 -5
- data/lib/hashira/complexity/method_score.rb +1 -1
- data/lib/hashira/complexity/scores.rb +11 -9
- data/lib/hashira/coupling/audiences.rb +3 -3
- data/lib/hashira/coupling/catalog.rb +8 -6
- data/lib/hashira/coupling/census.rb +42 -22
- data/lib/hashira/coupling/cycle_search.rb +11 -7
- data/lib/hashira/coupling/definitions.rb +1 -7
- data/lib/hashira/coupling/edge_map.rb +9 -8
- data/lib/hashira/coupling/folding.rb +5 -5
- data/lib/hashira/coupling/graph.rb +16 -9
- data/lib/hashira/coupling/metric.rb +3 -1
- data/lib/hashira/coupling/naming.rb +4 -4
- data/lib/hashira/coupling/references.rb +12 -12
- data/lib/hashira/coupling/report.rb +7 -9
- data/lib/hashira/coupling/roll_call.rb +1 -1
- data/lib/hashira/coupling/roll_call_findings.rb +1 -1
- data/lib/hashira/coupling/roster.rb +37 -12
- data/lib/hashira/coupling/rule.rb +1 -1
- data/lib/hashira/coupling/sdp_violation_findings.rb +3 -1
- data/lib/hashira/duplication/clones.rb +1 -1
- data/lib/hashira/duplication/clusters.rb +10 -7
- data/lib/hashira/duplication/delta.rb +1 -4
- data/lib/hashira/duplication/duplication_finding.rb +4 -2
- data/lib/hashira/duplication/fragment.rb +20 -2
- data/lib/hashira/duplication/harvest.rb +7 -5
- data/lib/hashira/duplication/index.rb +3 -2
- data/lib/hashira/duplication/literal.rb +25 -0
- data/lib/hashira/duplication/similarity.rb +2 -2
- data/lib/hashira/duplication/variance.rb +7 -1
- data/lib/hashira/error.rb +0 -3
- data/lib/hashira/focus.rb +29 -0
- data/lib/hashira/git_log.rb +19 -0
- data/lib/hashira/hotspots/file_cost.rb +1 -1
- data/lib/hashira/hotspots/rollup.rb +2 -2
- data/lib/hashira/pipeline.rb +22 -14
- data/lib/hashira/project.rb +35 -34
- data/lib/hashira/report/columns.rb +14 -8
- data/lib/hashira/report/graph_payload.rb +1 -1
- data/lib/hashira/report/hotspot_table.rb +1 -1
- data/lib/hashira/report/json.rb +2 -1
- data/lib/hashira/report/metrics_table.rb +3 -3
- data/lib/hashira/report/phrases.rb +23 -18
- data/lib/hashira/report/smell_phrases.rb +2 -2
- data/lib/hashira/smells/branches.rb +62 -0
- data/lib/hashira/smells/census.rb +11 -6
- data/lib/hashira/smells/check.rb +1 -5
- data/lib/hashira/smells/contexts.rb +7 -5
- data/lib/hashira/smells/control_parameter.rb +1 -1
- data/lib/hashira/smells/data_clump.rb +2 -4
- data/lib/hashira/smells/duplicate_method_call.rb +25 -5
- data/lib/hashira/smells/feature_envy.rb +3 -3
- data/lib/hashira/smells/foreign.rb +14 -14
- data/lib/hashira/smells/instance_variable_assumption.rb +5 -15
- data/lib/hashira/smells/kind.rb +9 -0
- data/lib/hashira/smells/lineage.rb +92 -0
- data/lib/hashira/smells/manual_dispatch.rb +1 -1
- data/lib/hashira/smells/module_initialize.rb +0 -2
- data/lib/hashira/smells/nil_check.rb +1 -1
- data/lib/hashira/smells/ownership.rb +19 -7
- data/lib/hashira/smells/param_check.rb +2 -2
- data/lib/hashira/smells/refs.rb +13 -7
- data/lib/hashira/smells/repeated_conditional.rb +1 -3
- data/lib/hashira/smells/report.rb +16 -7
- data/lib/hashira/smells/too_many_instance_variables.rb +3 -3
- data/lib/hashira/smells/visibility.rb +18 -11
- data/lib/hashira/trees.rb +8 -4
- data/lib/hashira/version.rb +1 -1
- data/lib/hashira.rb +12 -0
- metadata +13 -1
|
@@ -2,28 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
class Hashira::Coupling::Census
|
|
4
4
|
def initialize(project, trees, packaging: :folder)
|
|
5
|
-
@
|
|
6
|
-
@
|
|
7
|
-
@
|
|
8
|
-
@roster = tally
|
|
9
|
-
settle
|
|
5
|
+
@project = project
|
|
6
|
+
@trees = trees
|
|
7
|
+
@packaging = packaging
|
|
10
8
|
end
|
|
11
9
|
|
|
12
|
-
def packaging =
|
|
10
|
+
def packaging = placement.mode
|
|
13
11
|
|
|
14
|
-
def types =
|
|
12
|
+
def types = roster.types
|
|
15
13
|
|
|
16
|
-
def prefix =
|
|
14
|
+
def prefix = catalog.prefix
|
|
17
15
|
|
|
18
|
-
def origins =
|
|
16
|
+
def origins = roster.origins
|
|
19
17
|
|
|
20
|
-
def roots =
|
|
18
|
+
def roots = catalog.roots
|
|
21
19
|
|
|
22
|
-
def packages =
|
|
20
|
+
def packages = roster.packages | placement.baseline
|
|
23
21
|
|
|
24
|
-
def folds =
|
|
22
|
+
def folds = folding.disclosed
|
|
25
23
|
|
|
26
|
-
def 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(
|
|
35
|
+
def charge(file, nesting) = translate(placement.charge(file, nesting))
|
|
38
36
|
|
|
39
37
|
private
|
|
40
38
|
|
|
41
|
-
def
|
|
39
|
+
def catalog = @_catalog ||= Hashira::Coupling::Catalog.new(Hashira::Coupling::Definitions.new(@project, @trees))
|
|
42
40
|
|
|
43
|
-
def
|
|
44
|
-
|
|
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
|
|
48
|
-
|
|
49
|
-
|
|
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) =
|
|
72
|
+
def translate(package) = folding.map.fetch(package, package)
|
|
53
73
|
|
|
54
|
-
def scope = Hashira::Coupling::Scope.new(
|
|
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
|
-
|
|
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 @
|
|
27
|
-
@
|
|
28
|
-
@
|
|
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(@
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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 = @
|
|
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 = @
|
|
22
|
+
def links = @_links ||= named.merge(parents)
|
|
23
23
|
|
|
24
24
|
def parents
|
|
25
|
-
@
|
|
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 = @
|
|
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 = @
|
|
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 :
|
|
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 |
|
|
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) =
|
|
37
|
+
def evidence(from, to) = map.evidence[[from, to]]
|
|
38
38
|
|
|
39
|
-
def usage(package) = incoming(package).to_h { [it,
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
@
|
|
5
|
+
@definitions = definitions
|
|
6
6
|
end
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
def segments = @_segments ||= Hashira::Coupling::NamespacePrefix.infer(@definitions)
|
|
9
9
|
|
|
10
10
|
def strip(path)
|
|
11
|
-
return [] if
|
|
12
|
-
path.drop(
|
|
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
|
-
|
|
14
|
+
found
|
|
19
15
|
end
|
|
20
16
|
|
|
21
17
|
private
|
|
22
18
|
|
|
23
|
-
def
|
|
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
|
|
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.
|
|
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
|
-
|
|
51
|
+
scopes << scope
|
|
52
52
|
yield
|
|
53
|
-
|
|
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
|
-
@
|
|
15
|
+
@trees = trees
|
|
16
|
+
@packaging = packaging
|
|
19
17
|
end
|
|
20
18
|
|
|
21
|
-
|
|
19
|
+
def graph = @_graph ||= Hashira::Coupling::Graph.new(@project, @trees, census)
|
|
22
20
|
|
|
23
|
-
def findings = RULES.flat_map { it.new(@project,
|
|
21
|
+
def findings = RULES.flat_map { it.new(@project, graph).list }
|
|
24
22
|
|
|
25
23
|
private
|
|
26
24
|
|
|
27
|
-
def census
|
|
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 = @
|
|
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
|
-
@
|
|
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
|
-
@
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
@placed = placed
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def registry
|
|
9
|
+
admitted
|
|
10
|
+
@_registry
|
|
10
11
|
end
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
def types
|
|
14
|
+
admitted
|
|
15
|
+
@_types
|
|
16
|
+
end
|
|
13
17
|
|
|
14
|
-
def origins =
|
|
18
|
+
def origins = registry.origins
|
|
15
19
|
|
|
16
|
-
def type?(path)
|
|
20
|
+
def type?(path)
|
|
21
|
+
admitted
|
|
22
|
+
@_typed.include?(path)
|
|
23
|
+
end
|
|
17
24
|
|
|
18
|
-
def 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
|
-
@
|
|
26
|
-
@
|
|
27
|
-
@
|
|
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 = @
|
|
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
|
-
|
|
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 = @
|
|
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(
|
|
11
|
-
@
|
|
12
|
-
@sets = Hashira::Duplication::UnionFind.new
|
|
10
|
+
def initialize(all)
|
|
11
|
+
@all = all
|
|
13
12
|
end
|
|
14
13
|
|
|
15
14
|
def sorted
|
|
16
|
-
|
|
17
|
-
Hashira::Duplication::NearMiss.new(
|
|
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
|
|
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 =
|
|
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 = @
|
|
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 = @
|
|
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
|