hashira 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +38 -0
- data/README.md +29 -0
- data/exe/hashira +1 -1
- data/lib/hashira/analysis/finding.rb +1 -1
- data/lib/hashira/analysis/syntax.rb +6 -6
- data/lib/hashira/churn.rb +1 -10
- data/lib/hashira/ci/accepted.rb +9 -7
- data/lib/hashira/ci/baseline.rb +34 -30
- data/lib/hashira/ci/comparison.rb +24 -0
- data/lib/hashira/ci/diff.rb +0 -15
- data/lib/hashira/ci/gate.rb +3 -3
- data/lib/hashira/ci/ratchet.rb +10 -4
- data/lib/hashira/ci/ratchet_report.rb +4 -4
- data/lib/hashira/ci/scope.rb +0 -1
- data/lib/hashira/ci/slice.rb +17 -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 -1
- 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 +2 -2
- data/lib/hashira/duplication/harvest.rb +7 -5
- data/lib/hashira/duplication/index.rb +3 -2
- data/lib/hashira/duplication/similarity.rb +2 -2
- data/lib/hashira/duplication/variance.rb +1 -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/smells/census.rb +3 -4
- data/lib/hashira/smells/check.rb +1 -5
- data/lib/hashira/smells/contexts.rb +4 -4
- 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 +3 -3
- 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 +3 -3
- data/lib/hashira/smells/kind.rb +9 -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 +8 -0
- metadata +9 -1
|
@@ -2,18 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
class Hashira::Coupling::Graph
|
|
4
4
|
def initialize(project, trees, census)
|
|
5
|
+
@project = project
|
|
5
6
|
@census = census
|
|
6
7
|
@trees = trees
|
|
7
|
-
@map = Hashira::Coupling::EdgeMap.new(census)
|
|
8
|
-
trees.each { |file, tree| @map.record(project.relative(file), file, tree) }
|
|
9
|
-
@cycles = Hashira::Coupling::Cycles.new(@map.dependencies, self)
|
|
10
8
|
end
|
|
11
9
|
|
|
12
|
-
attr_reader :
|
|
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 }
|
|
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)
|
|
@@ -12,7 +12,7 @@ class Hashira::Duplication::Fragment
|
|
|
12
12
|
|
|
13
13
|
attr_reader :file
|
|
14
14
|
|
|
15
|
-
def types = @
|
|
15
|
+
def types = @_types ||= nodes.map(&:type)
|
|
16
16
|
|
|
17
17
|
def digest = Digest::SHA256.hexdigest(shape).slice(0, DIGEST_LENGTH)
|
|
18
18
|
|
|
@@ -34,5 +34,5 @@ class Hashira::Duplication::Fragment
|
|
|
34
34
|
|
|
35
35
|
def touches?(others) = others.any? { overlaps?(it) }
|
|
36
36
|
|
|
37
|
-
def nodes = @
|
|
37
|
+
def nodes = @_nodes ||= @roots.flat_map { Hashira::Analysis::NodeWalk.collect(it) }
|
|
38
38
|
end
|
|
@@ -7,19 +7,21 @@ class Hashira::Duplication::Harvest
|
|
|
7
7
|
|
|
8
8
|
def initialize(project, trees)
|
|
9
9
|
@project = project
|
|
10
|
-
@
|
|
10
|
+
@trees = trees
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
def fragments = @_fragments ||= @trees.flat_map { |path, tree| scan(@project.relative(path), tree) }
|
|
14
14
|
|
|
15
15
|
private
|
|
16
16
|
|
|
17
|
-
def scan(
|
|
17
|
+
def scan(relative, tree)
|
|
18
18
|
nodes = Hashira::Analysis::NodeWalk.collect(tree)
|
|
19
|
-
windows(
|
|
19
|
+
windows(relative, nodes) + wholes(nodes).map { Hashira::Duplication::Fragment.new(relative, [it]) }
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
def windows(
|
|
22
|
+
def windows(relative, nodes)
|
|
23
|
+
runs(nodes).flat_map { Hashira::Duplication::Sequence.new(relative, it).fragments }
|
|
24
|
+
end
|
|
23
25
|
|
|
24
26
|
def runs(nodes) = nodes.filter_map { it.body if it.is_a?(Prism::StatementsNode) }
|
|
25
27
|
|
|
@@ -6,13 +6,14 @@ class Hashira::Duplication::Index
|
|
|
6
6
|
|
|
7
7
|
def initialize(fragments)
|
|
8
8
|
@fragments = fragments
|
|
9
|
-
@frequency = frequencies(fragments)
|
|
10
9
|
end
|
|
11
10
|
|
|
12
11
|
def buckets = grouped.values.select { |bucket| bucket.size.between?(2, MAX_BUCKET) }
|
|
13
12
|
|
|
14
13
|
private
|
|
15
14
|
|
|
15
|
+
def frequency = @_frequency ||= frequencies(@fragments)
|
|
16
|
+
|
|
16
17
|
def frequencies(fragments)
|
|
17
18
|
fragments.each_with_object(Hash.new(0)) { |fragment, counts| tally(counts, fragment) }
|
|
18
19
|
end
|
|
@@ -27,5 +28,5 @@ class Hashira::Duplication::Index
|
|
|
27
28
|
|
|
28
29
|
def file(index, fragment) = rarest(fragment).each { |type| index[type] << fragment }
|
|
29
30
|
|
|
30
|
-
def rarest(fragment) = fragment.types.uniq.min_by(RARE) {
|
|
31
|
+
def rarest(fragment) = fragment.types.uniq.min_by(RARE) { frequency[it] }
|
|
31
32
|
end
|
|
@@ -8,7 +8,7 @@ class Hashira::Duplication::Similarity
|
|
|
8
8
|
|
|
9
9
|
def ratio
|
|
10
10
|
return 0.0 if @left.empty? || @right.empty?
|
|
11
|
-
normalized(
|
|
11
|
+
normalized(subsequence)
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
def meets?(threshold) = ceiling >= threshold && ratio >= threshold
|
|
@@ -29,7 +29,7 @@ class Hashira::Duplication::Similarity
|
|
|
29
29
|
true
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
-
def
|
|
32
|
+
def subsequence = @left.reduce(blank) { |prev, token| advance(prev, token) }.last
|
|
33
33
|
|
|
34
34
|
def blank = Array.new(@right.size + 1, 0)
|
|
35
35
|
|
|
@@ -27,7 +27,7 @@ class Hashira::Duplication::Variance
|
|
|
27
27
|
|
|
28
28
|
def pairs = @canonical.nodes.zip(@other.nodes)
|
|
29
29
|
|
|
30
|
-
def named = @
|
|
30
|
+
def named = @_named ||= pairs.select { |left, _| NAMED.include?(left.type) }
|
|
31
31
|
|
|
32
32
|
def differing = pairs.select { |pair| varies?(*pair) }.map(&:first)
|
|
33
33
|
|
data/lib/hashira/error.rb
CHANGED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Hashira::Focus
|
|
4
|
+
SITE = /\A(?<path>[^\s:]+\.rb):/
|
|
5
|
+
|
|
6
|
+
def initialize(project, paths)
|
|
7
|
+
@project = project
|
|
8
|
+
@paths = paths
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def narrowing? = !@paths.empty?
|
|
12
|
+
|
|
13
|
+
def narrow(findings)
|
|
14
|
+
return findings unless narrowing?
|
|
15
|
+
findings.select { named?(it) }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def wanted = @_wanted ||= @paths.select { covered?(it) }.map { @project.relative(it) }
|
|
21
|
+
|
|
22
|
+
def covered?(path) = @project.directories.any? { path.start_with?("#{it}/") }
|
|
23
|
+
|
|
24
|
+
def named?(finding) = sites(finding).any? { wanted.include?(it) }
|
|
25
|
+
|
|
26
|
+
def sites(finding) = quotes(finding).filter_map { SITE.match(it)&.[](:path) }
|
|
27
|
+
|
|
28
|
+
def quotes(finding) = [finding.package, finding.detail.to_h[:site], *finding.evidence].compact
|
|
29
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Hashira::GitLog
|
|
4
|
+
LOG = %w[log --no-renames --name-only --format=].freeze
|
|
5
|
+
|
|
6
|
+
def initialize(directory)
|
|
7
|
+
@directory = directory
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def counts = output.split("\n").map(&:strip).reject(&:empty?).tally
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def output
|
|
15
|
+
IO.popen(["git", "-C", @directory, *LOG], err: File::NULL, &:read)
|
|
16
|
+
rescue SystemCallError
|
|
17
|
+
""
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -23,9 +23,9 @@ class Hashira::Hotspots::Rollup
|
|
|
23
23
|
|
|
24
24
|
def clusters = @duplication ? @duplication.clusters : []
|
|
25
25
|
|
|
26
|
-
def cognitive = @
|
|
26
|
+
def cognitive = @_cognitive ||= bucketed(scores.map { [it.file, it.cognitive] })
|
|
27
27
|
|
|
28
|
-
def duplicated = @
|
|
28
|
+
def duplicated = @_duplicated ||= bucketed(clusters.flat_map(&:masses))
|
|
29
29
|
|
|
30
30
|
def bucketed(charges) = charges.each_with_object(Hash.new(0)) { |(file, cost), total| total[file] += cost }
|
|
31
31
|
end
|
data/lib/hashira/pipeline.rb
CHANGED
|
@@ -10,50 +10,58 @@ class Hashira::Pipeline
|
|
|
10
10
|
STRUCTURAL = Hashira::Coupling::Report::RULES.map { it::KIND }.freeze
|
|
11
11
|
|
|
12
12
|
SMELLS = (
|
|
13
|
-
Hashira::Smells::Report::CHECKS.map(
|
|
13
|
+
Hashira::Smells::Report::CHECKS.map { Hashira::Smells::Kind.new(it).to_s } + [Hashira::Smells::BoundarySprawl::KIND]
|
|
14
14
|
).sort.freeze
|
|
15
15
|
|
|
16
|
-
def initialize(project, enabled: ANALYZERS, packaging: :auto)
|
|
16
|
+
def initialize(project, enabled: ANALYZERS, packaging: :auto, only: [])
|
|
17
17
|
@project = project
|
|
18
18
|
@enabled = enabled
|
|
19
|
-
@
|
|
20
|
-
@
|
|
19
|
+
@packaging = packaging
|
|
20
|
+
@only = only
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
attr_reader :project
|
|
24
24
|
|
|
25
|
-
def unparsed =
|
|
25
|
+
def unparsed = parsed.unparsed
|
|
26
26
|
|
|
27
|
-
def graph =
|
|
27
|
+
def graph = coupling.graph
|
|
28
28
|
|
|
29
|
-
def complexity = @
|
|
29
|
+
def complexity = @_complexity ||= analyzed(:complexity, Hashira::Complexity::Scores)
|
|
30
30
|
|
|
31
|
-
def duplication = @
|
|
31
|
+
def duplication = @_duplication ||= analyzed(:duplication, Hashira::Duplication::Clones, churn)
|
|
32
32
|
|
|
33
|
-
def smells = @
|
|
33
|
+
def smells = @_smells ||= analyzed(:smells, Hashira::Smells::Report)
|
|
34
34
|
|
|
35
35
|
def hotspots
|
|
36
|
-
@
|
|
36
|
+
@_hotspots ||= Hashira::Hotspots::Rollup.new(complexity, duplication, churn) if complexity || duplication
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
-
def churn = @
|
|
39
|
+
def churn = @_churn ||= Hashira::Churn.build(@project.directories.first)
|
|
40
40
|
|
|
41
41
|
def enabled?(analyzer) = @enabled.include?(analyzer)
|
|
42
42
|
|
|
43
43
|
def analyzed(name, kind, *extra)
|
|
44
|
-
kind.new(@project,
|
|
44
|
+
kind.new(@project, parsed.all, *extra) if enabled?(name)
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
-
def findings = structural + listed(complexity) + listed(duplication) + listed(smells)
|
|
47
|
+
def findings = focus.narrow(structural + listed(complexity) + listed(duplication) + listed(smells))
|
|
48
|
+
|
|
49
|
+
def focus = @_focus ||= Hashira::Focus.new(@project, @only)
|
|
48
50
|
|
|
49
51
|
private
|
|
50
52
|
|
|
53
|
+
def parsed = @_parsed ||= Hashira::Trees.new(@project)
|
|
54
|
+
|
|
55
|
+
def coupling
|
|
56
|
+
@_coupling ||= Hashira::Coupling::Report.new(@project, parsed.all, packaging: settle(@packaging))
|
|
57
|
+
end
|
|
58
|
+
|
|
51
59
|
def settle(packaging)
|
|
52
60
|
return packaging unless packaging == :auto
|
|
53
61
|
@project.rails? ? :namespace : :folder
|
|
54
62
|
end
|
|
55
63
|
|
|
56
|
-
def structural = enabled?(:coupling) ?
|
|
64
|
+
def structural = enabled?(:coupling) ? coupling.findings : []
|
|
57
65
|
|
|
58
66
|
def listed(analyzer) = analyzer ? analyzer.findings : []
|
|
59
67
|
end
|