hashira 0.4.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 +58 -0
- data/README.md +86 -25
- data/lib/hashira/analysis/finding.rb +2 -2
- data/lib/hashira/ci/ratchet.rb +5 -9
- data/lib/hashira/cli/fail_on.rb +15 -5
- data/lib/hashira/cli/run.rb +8 -4
- data/lib/hashira/cli/usage.rb +8 -5
- data/lib/hashira/complexity/boolean_run.rb +5 -3
- data/lib/hashira/complexity/cognitive_score.rb +21 -14
- data/lib/hashira/complexity/if_chain.rb +14 -14
- data/lib/hashira/complexity/method_finding.rb +3 -20
- data/lib/hashira/complexity/rescue_scan.rb +9 -9
- data/lib/hashira/complexity/{analyzer.rb → scores.rb} +6 -5
- data/lib/hashira/coupling/audiences.rb +49 -0
- data/lib/hashira/{analysis → coupling}/catalog.rb +3 -3
- data/lib/hashira/{analysis → coupling}/census.rb +17 -8
- data/lib/hashira/{analysis → coupling}/constant_registry.rb +6 -2
- data/lib/hashira/coupling/cycle_findings.rb +31 -0
- data/lib/hashira/{analysis → coupling}/cycle_search.rb +1 -1
- data/lib/hashira/{analysis → coupling}/cycles.rb +2 -2
- data/lib/hashira/{analysis → coupling}/definition.rb +1 -1
- data/lib/hashira/{analysis → coupling}/definitions.rb +1 -1
- data/lib/hashira/{analysis → coupling}/edge.rb +1 -1
- data/lib/hashira/{analysis → coupling}/edge_map.rb +10 -10
- data/lib/hashira/{analysis → coupling}/folder_placement.rb +2 -2
- data/lib/hashira/{analysis → coupling}/folding.rb +4 -2
- data/lib/hashira/{analysis → coupling}/graph.rb +18 -8
- data/lib/hashira/{analysis → coupling}/metric.rb +1 -1
- data/lib/hashira/coupling/mixed_audience_findings.rb +32 -0
- data/lib/hashira/{analysis → coupling}/namespace_placement.rb +3 -3
- data/lib/hashira/{analysis → coupling}/namespace_prefix.rb +1 -1
- data/lib/hashira/{analysis → coupling}/naming.rb +2 -2
- data/lib/hashira/{analysis → coupling}/no_folding.rb +1 -1
- data/lib/hashira/{analysis → coupling}/placement.rb +3 -3
- 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/{analysis → coupling}/roster.rb +2 -2
- data/lib/hashira/{analysis → coupling}/rule.rb +1 -1
- data/lib/hashira/{analysis/resolver.rb → coupling/scope.rb} +2 -2
- data/lib/hashira/{analysis → coupling}/sdp_check.rb +1 -1
- 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/{renderer.rb → source.rb} +2 -2
- data/lib/hashira/duplication/{analyzer.rb → clones.rb} +3 -3
- data/lib/hashira/duplication/{clusterer.rb → clusters.rb} +3 -3
- data/lib/hashira/duplication/delta.rb +6 -14
- data/lib/hashira/duplication/duplication_finding.rb +6 -10
- data/lib/hashira/duplication/{extractor.rb → harvest.rb} +1 -1
- data/lib/hashira/duplication/sequence.rb +3 -1
- data/lib/hashira/duplication/similarity.rb +5 -1
- data/lib/hashira/duplication/variance.rb +3 -3
- data/lib/hashira/hotspots/rollup.rb +1 -1
- data/lib/hashira/pipeline.rb +15 -10
- data/lib/hashira/report/complexity_table.rb +1 -1
- data/lib/hashira/report/finding_lines.rb +1 -1
- data/lib/hashira/report/json.rb +5 -3
- data/lib/hashira/report/phrases.rb +98 -0
- data/lib/hashira/report/smell_phrases.rb +68 -0
- 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 +65 -30
- metadata +60 -31
- data/lib/hashira/analysis/cycle_findings.rb +0 -38
- data/lib/hashira/analysis/references.rb +0 -40
- data/lib/hashira/analysis/sdp_violation_findings.rb +0 -29
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
class Hashira::Complexity::
|
|
3
|
+
class Hashira::Complexity::Scores
|
|
4
4
|
THRESHOLD = 10
|
|
5
5
|
|
|
6
6
|
def initialize(project, trees)
|
|
@@ -8,7 +8,7 @@ class Hashira::Complexity::Analyzer
|
|
|
8
8
|
@scores = trees.flat_map { |path, tree| harvest(path, tree) }
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
-
def
|
|
11
|
+
def ranked = @scores.sort_by { -it.cognitive }
|
|
12
12
|
|
|
13
13
|
def classes = Hashira::Complexity::Rollup.new(@scores).classes.sort_by { -it.cognitive }
|
|
14
14
|
|
|
@@ -16,7 +16,7 @@ class Hashira::Complexity::Analyzer
|
|
|
16
16
|
|
|
17
17
|
private
|
|
18
18
|
|
|
19
|
-
def flagged =
|
|
19
|
+
def flagged = ranked.select { it.cognitive >= THRESHOLD }
|
|
20
20
|
|
|
21
21
|
def harvest(path, tree)
|
|
22
22
|
rel = @project.relative(path)
|
|
@@ -32,12 +32,13 @@ class Hashira::Complexity::Analyzer
|
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
def score(rel, full, node)
|
|
35
|
-
score = Hashira::Complexity::CognitiveScore.new(node)
|
|
36
35
|
Hashira::Complexity::MethodScore.new(
|
|
37
36
|
subject: subject(full, node), file: rel, line: node.location.start_line,
|
|
38
|
-
|
|
37
|
+
**tallies(Hashira::Complexity::CognitiveScore.new(node))
|
|
39
38
|
)
|
|
40
39
|
end
|
|
41
40
|
|
|
41
|
+
def tallies(score) = { cognitive: score.total, calls: score.calls, increments: score.increments }
|
|
42
|
+
|
|
42
43
|
def subject(full, node) = "#{full.join("::")}#{node.receiver ? "." : "#"}#{node.name}"
|
|
43
44
|
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Hashira::Coupling::Audiences
|
|
4
|
+
MIN = 2
|
|
5
|
+
|
|
6
|
+
Part = Data.define(:users, :constants, :shared)
|
|
7
|
+
|
|
8
|
+
def initialize(usage)
|
|
9
|
+
@usage = usage
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def split? = parts.size >= 2
|
|
13
|
+
|
|
14
|
+
def parts = @parts ||= [common, *slices].compact.select { it.constants.size >= MIN }
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def clients = @clients ||= @usage.keys.sort
|
|
19
|
+
|
|
20
|
+
def shared = @shared ||= tally.select { |_constant, users| users.size * 2 > clients.size }.keys.to_set
|
|
21
|
+
|
|
22
|
+
def tally
|
|
23
|
+
clients.each_with_object(Hash.new { |hash, key| hash[key] = [] }) do |client, seen|
|
|
24
|
+
@usage[client].each { seen[it] << client }
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def common
|
|
29
|
+
return if shared.empty?
|
|
30
|
+
Part.new(users: clients.select { @usage[it].intersect?(shared) }, constants: shared.sort, shared: true)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def slices
|
|
34
|
+
groups.sort_by(&:first).map { |members| Part.new(users: members, constants: pooled(members).sort, shared: false) }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def pooled(members) = members.reduce(Set.new) { |pool, client| pool | residue(client) }
|
|
38
|
+
|
|
39
|
+
def residue(client) = @usage[client] - shared
|
|
40
|
+
|
|
41
|
+
def groups
|
|
42
|
+
clients.reduce([]) do |formed, client|
|
|
43
|
+
linked, apart = formed.partition { |members| joined?(members, client) }
|
|
44
|
+
apart + [linked.flatten + [client]]
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def joined?(members, client) = members.any? { residue(it).intersect?(residue(client)) }
|
|
49
|
+
end
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
class Hashira::
|
|
3
|
+
class Hashira::Coupling::Catalog
|
|
4
4
|
include Enumerable
|
|
5
5
|
|
|
6
6
|
def initialize(definitions)
|
|
7
7
|
@definitions = definitions
|
|
8
|
-
@naming = Hashira::
|
|
8
|
+
@naming = Hashira::Coupling::Naming.new(definitions)
|
|
9
9
|
@entries = definitions.map { |node, full, folder| entry(node, full, folder) }
|
|
10
10
|
end
|
|
11
11
|
|
|
@@ -22,6 +22,6 @@ class Hashira::Analysis::Catalog
|
|
|
22
22
|
private
|
|
23
23
|
|
|
24
24
|
def entry(node, full, folder)
|
|
25
|
-
Hashira::
|
|
25
|
+
Hashira::Coupling::Definition.new(node:, path: @naming.strip(full), folder:)
|
|
26
26
|
end
|
|
27
27
|
end
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
class Hashira::
|
|
3
|
+
class Hashira::Coupling::Census
|
|
4
4
|
def initialize(project, trees, packaging: :folder)
|
|
5
|
-
@catalog = Hashira::
|
|
6
|
-
@placement = Hashira::
|
|
7
|
-
@folding = Hashira::
|
|
5
|
+
@catalog = Hashira::Coupling::Catalog.new(Hashira::Coupling::Definitions.new(project, trees))
|
|
6
|
+
@placement = Hashira::Coupling::Placement.build(packaging, project, @catalog)
|
|
7
|
+
@folding = Hashira::Coupling::NoFolding
|
|
8
8
|
@roster = tally
|
|
9
9
|
settle
|
|
10
10
|
end
|
|
@@ -23,16 +23,25 @@ class Hashira::Analysis::Census
|
|
|
23
23
|
|
|
24
24
|
def folds = @folding.disclosed
|
|
25
25
|
|
|
26
|
-
def
|
|
26
|
+
def strip(segments) = @catalog.strip(segments)
|
|
27
27
|
|
|
28
|
-
def
|
|
28
|
+
def holder(segments)
|
|
29
|
+
path = strip(segments)
|
|
30
|
+
(path.length.downto(1).map { path.first(it) }.find { type?(it) } || path).join("::")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def resolve(segments, nesting = []) = scope.resolve(segments, nesting)
|
|
34
|
+
|
|
35
|
+
def pinpoint(segments) = scope.pinpoint(segments)
|
|
29
36
|
|
|
30
37
|
def charge(file, nesting) = translate(@placement.charge(file, nesting))
|
|
31
38
|
|
|
32
39
|
private
|
|
33
40
|
|
|
41
|
+
def type?(segments) = @roster.origins.key?(segments.join("::"))
|
|
42
|
+
|
|
34
43
|
def tally
|
|
35
|
-
Hashira::
|
|
44
|
+
Hashira::Coupling::Roster.new(@placement.placed.map { |definition, package| [definition, translate(package)] })
|
|
36
45
|
end
|
|
37
46
|
|
|
38
47
|
def settle
|
|
@@ -42,5 +51,5 @@ class Hashira::Analysis::Census
|
|
|
42
51
|
|
|
43
52
|
def translate(package) = @folding.map.fetch(package, package)
|
|
44
53
|
|
|
45
|
-
def
|
|
54
|
+
def scope = Hashira::Coupling::Scope.new(@roster.registry, @catalog, @placement)
|
|
46
55
|
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
class Hashira::
|
|
3
|
+
class Hashira::Coupling::ConstantRegistry
|
|
4
4
|
AMBIGUOUS = Object.new.freeze
|
|
5
5
|
|
|
6
6
|
def initialize
|
|
@@ -38,6 +38,10 @@ class Hashira::Analysis::ConstantRegistry
|
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
def enclosing(path)
|
|
41
|
-
(path
|
|
41
|
+
prefixes(path).filter_map { @origins[it] }.first
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def prefixes(path)
|
|
45
|
+
(path.length - 1).downto(1).map { path.first(it).join("::") }
|
|
42
46
|
end
|
|
43
47
|
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "rule"
|
|
4
|
+
|
|
5
|
+
class Hashira::Coupling::CycleFindings < Hashira::Coupling::Rule
|
|
6
|
+
KIND = "cycle"
|
|
7
|
+
|
|
8
|
+
def list
|
|
9
|
+
loops.map { entry(it) }
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def loops
|
|
15
|
+
cycles = graph.cycles
|
|
16
|
+
graph.packages.select { cycles.through?(it) }.sort.map { cycles.path(it) }.uniq { it[..-2].sort }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def entry(path)
|
|
20
|
+
finding(package: path.first, cycle: path, evidence: evidence(path), detail: detail(graph.cycles.weakest(path)))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def detail(weak)
|
|
24
|
+
from, to = weak
|
|
25
|
+
{ weak:, weight: graph.weight(from, to) }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def evidence(path)
|
|
29
|
+
path.each_cons(2).flat_map { |from, to| graph.evidence(from, to).to_a.first(2) }
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
class Hashira::
|
|
3
|
+
class Hashira::Coupling::Cycles
|
|
4
4
|
def initialize(dependencies, graph)
|
|
5
5
|
@dependencies = dependencies
|
|
6
6
|
@graph = graph
|
|
@@ -8,7 +8,7 @@ class Hashira::Analysis::Cycles
|
|
|
8
8
|
|
|
9
9
|
def through?(package) = !!path(package)
|
|
10
10
|
|
|
11
|
-
def path(package) = Hashira::
|
|
11
|
+
def path(package) = Hashira::Coupling::CycleSearch.new(@dependencies, package).path
|
|
12
12
|
|
|
13
13
|
def weakest(trail) = trail.each_cons(2).min_by { |from, to| @graph.weight(from, to) }
|
|
14
14
|
end
|
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
class Hashira::
|
|
4
|
-
def initialize(
|
|
5
|
-
@project = project
|
|
3
|
+
class Hashira::Coupling::EdgeMap
|
|
4
|
+
def initialize(census)
|
|
6
5
|
@census = census
|
|
7
6
|
@dependencies = sets
|
|
8
7
|
@evidence = sets
|
|
8
|
+
@usage = sets
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
-
attr_reader :dependencies, :evidence
|
|
11
|
+
attr_reader :dependencies, :evidence, :usage
|
|
12
12
|
|
|
13
|
-
def record(file, tree)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
note(@census.charge(file, home), locate(segments, nesting), "#{source}:#{line}: #{segments.join("::")}")
|
|
13
|
+
def record(source, file, tree)
|
|
14
|
+
Hashira::Coupling::References.sightings(tree, @census.roots).each do |segments, line, nesting, home|
|
|
15
|
+
note(@census.charge(file, home), locate(segments, nesting), segments, "#{source}:#{line}")
|
|
17
16
|
end
|
|
18
17
|
end
|
|
19
18
|
|
|
@@ -25,9 +24,10 @@ class Hashira::Analysis::EdgeMap
|
|
|
25
24
|
nesting ? @census.resolve(segments, nesting) : @census.pinpoint(segments)
|
|
26
25
|
end
|
|
27
26
|
|
|
28
|
-
def note(from, to,
|
|
27
|
+
def note(from, to, segments, site)
|
|
29
28
|
return unless to && to != from
|
|
30
29
|
@dependencies[from] << to
|
|
31
|
-
@evidence[[from, to]] <<
|
|
30
|
+
@evidence[[from, to]] << "#{site}: #{segments.join("::")}"
|
|
31
|
+
@usage[[from, to]] << @census.holder(segments)
|
|
32
32
|
end
|
|
33
33
|
end
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "placement"
|
|
4
4
|
|
|
5
|
-
class Hashira::
|
|
5
|
+
class Hashira::Coupling::FolderPlacement < Hashira::Coupling::Placement
|
|
6
6
|
def mode = :folder
|
|
7
7
|
|
|
8
8
|
def placed = catalog.map { [it, it.folder] }
|
|
@@ -13,5 +13,5 @@ class Hashira::Analysis::FolderPlacement < Hashira::Analysis::Placement
|
|
|
13
13
|
|
|
14
14
|
def skip?(_segments) = false
|
|
15
15
|
|
|
16
|
-
def folding(_census) = Hashira::
|
|
16
|
+
def folding(_census) = Hashira::Coupling::NoFolding
|
|
17
17
|
end
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "prism"
|
|
4
4
|
|
|
5
|
-
class Hashira::
|
|
5
|
+
class Hashira::Coupling::Folding
|
|
6
6
|
SUFFIXES = %w[Resource Serializer Policy Decorator].freeze
|
|
7
7
|
|
|
8
8
|
def initialize(definitions, census, suffixes: [])
|
|
@@ -32,10 +32,12 @@ class Hashira::Analysis::Folding
|
|
|
32
32
|
def pair(name, stem) = ([name, stem] if stem && @census.packages.include?(stem))
|
|
33
33
|
|
|
34
34
|
def crop(name)
|
|
35
|
-
suffix = @suffixes.find {
|
|
35
|
+
suffix = @suffixes.find { trims?(name, it) }
|
|
36
36
|
suffix && name.delete_suffix(suffix)
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
+
def trims?(name, suffix) = name.end_with?(suffix) && name != suffix
|
|
40
|
+
|
|
39
41
|
def singles = @singles ||= lone.group_by(&:name).transform_values { it.find(&:superclass) || it.last }
|
|
40
42
|
|
|
41
43
|
def lone = @definitions.select(&:singular?).reject { anchored.include?(it.name) }
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
class Hashira::
|
|
3
|
+
class Hashira::Coupling::Graph
|
|
4
4
|
def initialize(project, trees, census)
|
|
5
5
|
@census = census
|
|
6
|
-
@
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
@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)
|
|
9
10
|
end
|
|
10
11
|
|
|
11
|
-
attr_reader :cycles
|
|
12
|
+
attr_reader :cycles, :trees
|
|
13
|
+
|
|
14
|
+
def charge(file) = @census.charge(file, [])
|
|
12
15
|
|
|
13
16
|
def packages = (@census.packages | @map.dependencies.keys)
|
|
14
17
|
|
|
@@ -21,7 +24,7 @@ class Hashira::Analysis::Graph
|
|
|
21
24
|
def incoming(package) = packages.select { dependencies[it].include?(package) }.sort
|
|
22
25
|
|
|
23
26
|
def edges
|
|
24
|
-
dependencies.sort.flat_map { |from, tos| tos.sort.map { Hashira::
|
|
27
|
+
dependencies.sort.flat_map { |from, tos| tos.sort.map { Hashira::Coupling::Edge.new(from:, to: it) } }
|
|
25
28
|
end
|
|
26
29
|
|
|
27
30
|
def weighted
|
|
@@ -33,8 +36,15 @@ class Hashira::Analysis::Graph
|
|
|
33
36
|
|
|
34
37
|
def evidence(from, to) = @map.evidence[[from, to]]
|
|
35
38
|
|
|
39
|
+
def usage(package) = incoming(package).to_h { [it, @map.usage[[it, package]]] }
|
|
40
|
+
|
|
41
|
+
def constants(edge)
|
|
42
|
+
from, to = edge.deconstruct
|
|
43
|
+
@map.usage[[from, to]].sort
|
|
44
|
+
end
|
|
45
|
+
|
|
36
46
|
def metric(package)
|
|
37
|
-
Hashira::
|
|
47
|
+
Hashira::Coupling::Metric.new(
|
|
38
48
|
types: @census.types[package],
|
|
39
49
|
afferent: incoming(package).size,
|
|
40
50
|
efferent: dependencies[package].size
|
|
@@ -43,7 +53,7 @@ class Hashira::Analysis::Graph
|
|
|
43
53
|
|
|
44
54
|
def metrics = packages.to_h { [it, metric(it)] }
|
|
45
55
|
|
|
46
|
-
def violations = Hashira::
|
|
56
|
+
def violations = Hashira::Coupling::SdpCheck.new(dependencies, metrics).violations
|
|
47
57
|
|
|
48
58
|
def weight(from, to) = evidence(from, to).size
|
|
49
59
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "rule"
|
|
4
|
+
|
|
5
|
+
class Hashira::Coupling::MixedAudienceFindings < Hashira::Coupling::Rule
|
|
6
|
+
KIND = "mixed_audience"
|
|
7
|
+
|
|
8
|
+
def list
|
|
9
|
+
graph.packages.sort.filter_map { verdict(it) }
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def verdict(package)
|
|
15
|
+
audiences = Hashira::Coupling::Audiences.new(graph.usage(package))
|
|
16
|
+
detail(package, audiences.parts) if audiences.split?
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def detail(package, parts)
|
|
20
|
+
finding(
|
|
21
|
+
package:, evidence: parts.flat_map do
|
|
22
|
+
sightings(package, it).first(2)
|
|
23
|
+
end, detail: { parts: parts.map(&:to_h) }
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def sightings(package, part)
|
|
28
|
+
part.users.flat_map { |client| graph.evidence(client, package).to_a.select { mentions?(it, part.constants) } }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def mentions?(line, constants) = constants.any? { line.end_with?(": #{it}", "::#{it}") }
|
|
32
|
+
end
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "placement"
|
|
4
4
|
|
|
5
|
-
class Hashira::
|
|
5
|
+
class Hashira::Coupling::NamespacePlacement < Hashira::Coupling::Placement
|
|
6
6
|
RAILS_BASES =
|
|
7
7
|
%w[ApplicationRecord ApplicationController ApplicationJob ApplicationMailer
|
|
8
8
|
ApplicationHelper ApplicationCable ApplicationResource ApplicationSerializer
|
|
@@ -20,9 +20,9 @@ class Hashira::Analysis::NamespacePlacement < Hashira::Analysis::Placement
|
|
|
20
20
|
|
|
21
21
|
def skip?(segments) = project.rails? && RAILS_BASES.include?(segments.first)
|
|
22
22
|
|
|
23
|
-
def folding(census) = Hashira::
|
|
23
|
+
def folding(census) = Hashira::Coupling::Folding.new(catalog, census, suffixes: suffixes)
|
|
24
24
|
|
|
25
25
|
private
|
|
26
26
|
|
|
27
|
-
def suffixes = project.rails? ? Hashira::
|
|
27
|
+
def suffixes = project.rails? ? Hashira::Coupling::Folding::SUFFIXES : []
|
|
28
28
|
end
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
class Hashira::
|
|
3
|
+
class Hashira::Coupling::Naming
|
|
4
4
|
def initialize(definitions)
|
|
5
|
-
@segments = Hashira::
|
|
5
|
+
@segments = Hashira::Coupling::NamespacePrefix.infer(definitions)
|
|
6
6
|
end
|
|
7
7
|
|
|
8
8
|
attr_reader :segments
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
class Hashira::
|
|
3
|
+
class Hashira::Coupling::Placement
|
|
4
4
|
def self.build(packaging, project, catalog)
|
|
5
5
|
{
|
|
6
|
-
folder: Hashira::
|
|
7
|
-
namespace: Hashira::
|
|
6
|
+
folder: Hashira::Coupling::FolderPlacement,
|
|
7
|
+
namespace: Hashira::Coupling::NamespacePlacement
|
|
8
8
|
}.fetch(packaging).new(project, catalog)
|
|
9
9
|
end
|
|
10
10
|
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
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
|
+
def initialize(roots = nil)
|
|
11
|
+
@roots = roots
|
|
12
|
+
@found = []
|
|
13
|
+
@scopes = [[]]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def sightings(tree)
|
|
17
|
+
collect(tree)
|
|
18
|
+
@found
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def nesting = @scopes.last
|
|
24
|
+
|
|
25
|
+
def collect(node, home = nesting)
|
|
26
|
+
return unless node
|
|
27
|
+
return @found << sighting(node, home) if constant?(node)
|
|
28
|
+
return enter(node) if definition?(node)
|
|
29
|
+
node.compact_child_nodes.each { collect(it, home) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def sighting(node, home)
|
|
33
|
+
[syntax.segments(node), node.location.start_line, syntax.cbase?(node) ? nil : nesting, home]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def syntax = Hashira::Analysis::Syntax
|
|
37
|
+
|
|
38
|
+
def constant?(node) = node.is_a?(Prism::ConstantPathNode) || node.is_a?(Prism::ConstantReadNode)
|
|
39
|
+
|
|
40
|
+
def definition?(node) = node.is_a?(Prism::ClassNode) || node.is_a?(Prism::ModuleNode)
|
|
41
|
+
|
|
42
|
+
def enter(node)
|
|
43
|
+
opened = nesting + [anchor(node)]
|
|
44
|
+
collect(node.superclass, opened) if node.is_a?(Prism::ClassNode)
|
|
45
|
+
inside(opened) { collect(node.body) }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def anchor(node) = syntax.anchor(nesting, syntax.segments(node.constant_path), @roots)
|
|
49
|
+
|
|
50
|
+
def inside(scope)
|
|
51
|
+
@scopes << scope
|
|
52
|
+
yield
|
|
53
|
+
@scopes.pop
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "cycle_findings"
|
|
4
|
+
require_relative "mixed_audience_findings"
|
|
5
|
+
require_relative "roll_call_findings"
|
|
6
|
+
require_relative "sdp_violation_findings"
|
|
7
|
+
require_relative "wide_edge_findings"
|
|
8
|
+
|
|
9
|
+
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
|
|
15
|
+
|
|
16
|
+
def initialize(project, trees, packaging:)
|
|
17
|
+
@project = project
|
|
18
|
+
@graph = Hashira::Coupling::Graph.new(project, trees, census(trees, packaging))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
attr_reader :graph
|
|
22
|
+
|
|
23
|
+
def findings = RULES.flat_map { it.new(@project, @graph).list }
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def census(trees, packaging) = Hashira::Coupling::Census.new(@project, trees, packaging:)
|
|
28
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Hashira::Coupling::RollCall
|
|
4
|
+
MIN_WORDS = 3
|
|
5
|
+
|
|
6
|
+
MIN_FILES = 3
|
|
7
|
+
|
|
8
|
+
MIN_PACKAGES = 2
|
|
9
|
+
|
|
10
|
+
Roll = Data.define(:words, :files, :packages)
|
|
11
|
+
|
|
12
|
+
def initialize(lists, homes)
|
|
13
|
+
@lists = lists
|
|
14
|
+
@homes = homes
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def rolls = maximal(candidates.filter_map { entry(it) }).sort_by(&:words)
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def files = @files ||= @lists.keys.sort
|
|
22
|
+
|
|
23
|
+
def candidates
|
|
24
|
+
files.combination(2).map { |one, two| @lists[one] & @lists[two] }.select { it.size >= MIN_WORDS }.uniq
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def entry(words)
|
|
28
|
+
holders = files.select { @lists[it] >= words }
|
|
29
|
+
spread = holders.map { @homes[it] }.uniq.sort
|
|
30
|
+
return unless holders.size >= MIN_FILES && spread.size >= MIN_PACKAGES
|
|
31
|
+
Roll.new(words: words.sort, files: holders, packages: spread)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def maximal(rolls)
|
|
35
|
+
rolls.reject { |roll| swallowed?(roll, rolls) }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def swallowed?(roll, rolls) = rolls.any? { swallows?(it, roll) }
|
|
39
|
+
|
|
40
|
+
def swallows?(big, small)
|
|
41
|
+
mine = big.words
|
|
42
|
+
theirs = small.words
|
|
43
|
+
mine != theirs && (theirs - mine).empty?
|
|
44
|
+
end
|
|
45
|
+
end
|