hashira 0.6.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 +106 -0
- data/README.md +148 -53
- data/exe/hashira +1 -1
- data/lib/hashira/analysis/finding.rb +5 -1
- data/lib/hashira/analysis/syntax.rb +6 -6
- data/lib/hashira/churn.rb +3 -4
- data/lib/hashira/ci/accepted.rb +9 -10
- data/lib/hashira/ci/baseline.rb +39 -12
- data/lib/hashira/ci/comparison.rb +24 -0
- data/lib/hashira/ci/diff.rb +4 -8
- data/lib/hashira/ci/finding_diff_report.rb +10 -0
- data/lib/hashira/ci/gate.rb +10 -2
- data/lib/hashira/ci/improvement.rb +1 -1
- data/lib/hashira/ci/ratchet.rb +41 -10
- data/lib/hashira/ci/ratchet_report.rb +8 -6
- data/lib/hashira/ci/scope.rb +10 -0
- data/lib/hashira/ci/slice.rb +17 -0
- data/lib/hashira/ci/status.rb +7 -0
- data/lib/hashira/ci/sweep.rb +13 -0
- data/lib/hashira/cli/arguments.rb +17 -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 +17 -7
- data/lib/hashira/cli/flag.rb +3 -1
- data/lib/hashira/cli/flags.rb +23 -0
- data/lib/hashira/cli/format.rb +1 -1
- data/lib/hashira/cli/needs.rb +45 -0
- data/lib/hashira/cli/only.rb +16 -0
- data/lib/hashira/cli/options.rb +12 -3
- data/lib/hashira/cli/package_by.rb +1 -1
- data/lib/hashira/cli/run.rb +36 -8
- data/lib/hashira/cli/top.rb +13 -0
- data/lib/hashira/cli/usage.rb +5 -1
- data/lib/hashira/cli.rb +31 -11
- 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 +9 -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/diagram/dot.rb +9 -6
- data/lib/hashira/diagram/mermaid.rb +11 -8
- data/lib/hashira/diagram/source.rb +5 -2
- 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 +26 -24
- data/lib/hashira/project.rb +47 -32
- data/lib/hashira/report/columns.rb +54 -0
- data/lib/hashira/report/complexity_table.rb +12 -16
- data/lib/hashira/report/dependency_map.rb +1 -0
- data/lib/hashira/report/graph_payload.rb +1 -1
- data/lib/hashira/report/hotspot_table.rb +6 -14
- data/lib/hashira/report/json.rb +10 -2
- data/lib/hashira/report/metrics_table.rb +19 -20
- data/lib/hashira/report/notices.rb +37 -0
- data/lib/hashira/report/phrases.rb +25 -18
- data/lib/hashira/report/text.rb +24 -9
- data/lib/hashira/report/view.rb +4 -1
- 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 +26 -0
- data/lib/hashira/version.rb +1 -1
- data/lib/hashira.rb +14 -0
- metadata +16 -1
|
@@ -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
|
data/lib/hashira/diagram/dot.rb
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class Hashira::Diagram::Dot
|
|
4
|
-
def initialize(edges)
|
|
4
|
+
def initialize(edges, packages)
|
|
5
5
|
@edges = edges
|
|
6
|
+
@packages = packages
|
|
6
7
|
end
|
|
7
8
|
|
|
8
|
-
def source
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
def source = "digraph hashira {\n rankdir=LR;\n#{lines.join("\n")}\n}"
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def lines = @packages.map { %( "#{it}";) } + @edges.map { |from, to, weight| link(from, to, weight) }
|
|
14
|
+
|
|
15
|
+
def link(from, to, weight) = %( "#{from}" -> "#{to}" [label="#{weight}"];)
|
|
13
16
|
end
|
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class Hashira::Diagram::Mermaid
|
|
4
|
-
def initialize(edges)
|
|
4
|
+
def initialize(edges, packages)
|
|
5
5
|
@edges = edges
|
|
6
|
-
@
|
|
6
|
+
@packages = packages
|
|
7
|
+
@ids = {}
|
|
7
8
|
end
|
|
8
9
|
|
|
9
|
-
def source
|
|
10
|
-
"graph LR\n#{@edges.map { |from, to, weight| " #{node(from)} -->|#{weight}| #{node(to)}" }.join("\n")}"
|
|
11
|
-
end
|
|
10
|
+
def source = "graph LR\n#{lines.join("\n")}"
|
|
12
11
|
|
|
13
12
|
private
|
|
14
13
|
|
|
15
|
-
def
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
def lines = @packages.map { " #{declare(it)}" } + @edges.map { |from, to, weight| link(from, to, weight) }
|
|
15
|
+
|
|
16
|
+
def link(from, to, weight) = " #{id(from)} -->|#{weight}| #{id(to)}"
|
|
17
|
+
|
|
18
|
+
def declare(package) = "#{id(package)}[#{package.to_json}]"
|
|
19
|
+
|
|
20
|
+
def id(package) = @ids[package] ||= "p#{@ids.size}"
|
|
18
21
|
end
|
|
@@ -8,8 +8,11 @@ class Hashira::Diagram::Source
|
|
|
8
8
|
end
|
|
9
9
|
|
|
10
10
|
def print
|
|
11
|
-
|
|
12
|
-
@io.puts((@format == :dot ? Hashira::Diagram::Dot.new(edges) : Hashira::Diagram::Mermaid.new(edges)).source)
|
|
11
|
+
@io.puts(drawing.new(@graph.weighted, @graph.packages.sort).source)
|
|
13
12
|
0
|
|
14
13
|
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def drawing = @format == :dot ? Hashira::Diagram::Dot : Hashira::Diagram::Mermaid
|
|
15
18
|
end
|
|
@@ -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,56 +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
|
|
25
|
+
def unparsed = parsed.unparsed
|
|
26
26
|
|
|
27
|
-
def
|
|
28
|
-
@complexity ||= Hashira::Complexity::Scores.new(@project, @trees) if enabled?(:complexity)
|
|
29
|
-
end
|
|
27
|
+
def graph = coupling.graph
|
|
30
28
|
|
|
31
|
-
def
|
|
32
|
-
@duplication ||= Hashira::Duplication::Clones.new(@project, @trees, churn) if enabled?(:duplication)
|
|
33
|
-
end
|
|
29
|
+
def complexity = @_complexity ||= analyzed(:complexity, Hashira::Complexity::Scores)
|
|
34
30
|
|
|
35
|
-
def
|
|
36
|
-
|
|
37
|
-
|
|
31
|
+
def duplication = @_duplication ||= analyzed(:duplication, Hashira::Duplication::Clones, churn)
|
|
32
|
+
|
|
33
|
+
def smells = @_smells ||= analyzed(:smells, Hashira::Smells::Report)
|
|
38
34
|
|
|
39
35
|
def hotspots
|
|
40
|
-
@
|
|
36
|
+
@_hotspots ||= Hashira::Hotspots::Rollup.new(complexity, duplication, churn) if complexity || duplication
|
|
41
37
|
end
|
|
42
38
|
|
|
43
|
-
def churn = @
|
|
39
|
+
def churn = @_churn ||= Hashira::Churn.build(@project.directories.first)
|
|
44
40
|
|
|
45
41
|
def enabled?(analyzer) = @enabled.include?(analyzer)
|
|
46
42
|
|
|
47
|
-
def
|
|
43
|
+
def analyzed(name, kind, *extra)
|
|
44
|
+
kind.new(@project, parsed.all, *extra) if enabled?(name)
|
|
45
|
+
end
|
|
46
|
+
|
|
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
|
-
|
|
60
|
-
def parse(path)
|
|
61
|
-
Prism.parse_file(path).value
|
|
62
|
-
rescue SystemCallError => error
|
|
63
|
-
raise(Hashira::Error, "cannot read #{path} (#{error.message})")
|
|
64
|
-
end
|
|
65
67
|
end
|