hashira 0.2.0 → 0.4.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 +141 -0
- data/README.md +53 -5
- data/lib/hashira/analysis/catalog.rb +27 -0
- data/lib/hashira/analysis/census.rb +42 -35
- data/lib/hashira/analysis/constant_registry.rb +43 -0
- data/lib/hashira/analysis/cycle_findings.rb +34 -29
- data/lib/hashira/analysis/cycle_search.rb +28 -35
- data/lib/hashira/analysis/cycles.rb +14 -0
- data/lib/hashira/analysis/definition.rb +22 -0
- data/lib/hashira/analysis/definitions.rb +22 -18
- data/lib/hashira/analysis/edge.rb +4 -3
- data/lib/hashira/analysis/edge_map.rb +23 -26
- data/lib/hashira/analysis/finding.rb +7 -6
- data/lib/hashira/analysis/folder_placement.rb +17 -0
- data/lib/hashira/analysis/folding.rb +54 -0
- data/lib/hashira/analysis/graph.rb +35 -38
- data/lib/hashira/analysis/metric.rb +8 -7
- data/lib/hashira/analysis/namespace_placement.rb +28 -0
- data/lib/hashira/analysis/namespace_prefix.rb +32 -0
- data/lib/hashira/analysis/naming.rb +14 -0
- data/lib/hashira/analysis/no_folding.rb +13 -0
- data/lib/hashira/analysis/node_walk.rb +3 -3
- data/lib/hashira/analysis/placement.rb +19 -0
- data/lib/hashira/analysis/references.rb +14 -13
- data/lib/hashira/analysis/resolver.rb +28 -0
- data/lib/hashira/analysis/roster.rb +25 -0
- data/lib/hashira/analysis/rule.rb +10 -14
- data/lib/hashira/analysis/sdp_check.rb +8 -12
- data/lib/hashira/analysis/sdp_violation_findings.rb +20 -18
- data/lib/hashira/analysis/syntax.rb +21 -6
- data/lib/hashira/analysis/type_walk.rb +7 -9
- data/lib/hashira/churn.rb +11 -13
- data/lib/hashira/ci/accepted.rb +27 -31
- data/lib/hashira/ci/baseline.rb +22 -24
- data/lib/hashira/ci/diff.rb +8 -7
- data/lib/hashira/ci/edge_diff_report.rb +13 -17
- data/lib/hashira/ci/finding_diff_report.rb +14 -18
- data/lib/hashira/ci/gate.rb +19 -23
- data/lib/hashira/ci/improvement.rb +9 -14
- data/lib/hashira/ci/ratchet.rb +36 -27
- data/lib/hashira/ci/ratchet_report.rb +26 -32
- data/lib/hashira/cli/arguments.rb +23 -0
- data/lib/hashira/cli/command_line.rb +55 -71
- data/lib/hashira/cli/fail_on.rb +15 -20
- data/lib/hashira/cli/options.rb +9 -4
- data/lib/hashira/cli/package_by.rb +14 -0
- data/lib/hashira/cli/run.rb +29 -27
- data/lib/hashira/cli/skip.rb +14 -20
- data/lib/hashira/cli/usage.rb +41 -41
- data/lib/hashira/cli.rb +17 -19
- data/lib/hashira/complexity/analyzer.rb +30 -32
- data/lib/hashira/complexity/boolean_run.rb +11 -15
- data/lib/hashira/complexity/cognitive_score.rb +61 -66
- data/lib/hashira/complexity/if_chain.rb +33 -38
- data/lib/hashira/complexity/method_finding.rb +43 -48
- data/lib/hashira/complexity/method_score.rb +9 -7
- data/lib/hashira/complexity/rescue_scan.rb +16 -21
- data/lib/hashira/complexity/rollup.rb +13 -15
- data/lib/hashira/diagram/dot.rb +8 -11
- data/lib/hashira/diagram/mermaid.rb +11 -16
- data/lib/hashira/diagram/renderer.rb +10 -15
- data/lib/hashira/duplication/analyzer.rb +10 -14
- data/lib/hashira/duplication/cluster.rb +15 -12
- data/lib/hashira/duplication/clusterer.rb +31 -29
- data/lib/hashira/duplication/delta.rb +29 -30
- data/lib/hashira/duplication/duplication_finding.rb +19 -21
- data/lib/hashira/duplication/extractor.rb +15 -19
- data/lib/hashira/duplication/fragment.rb +19 -23
- data/lib/hashira/duplication/grouping.rb +12 -16
- data/lib/hashira/duplication/index.rb +20 -24
- data/lib/hashira/duplication/maximal.rb +11 -15
- data/lib/hashira/duplication/near_miss.rb +19 -22
- data/lib/hashira/duplication/sequence.rb +20 -22
- data/lib/hashira/duplication/similarity.rb +28 -35
- data/lib/hashira/duplication/union_find.rb +10 -14
- data/lib/hashira/duplication/variance.rb +36 -44
- data/lib/hashira/error.rb +4 -2
- data/lib/hashira/hotspots/file_cost.rb +8 -7
- data/lib/hashira/hotspots/rollup.rb +19 -21
- data/lib/hashira/pipeline.rb +36 -31
- data/lib/hashira/project.rb +53 -32
- data/lib/hashira/report/complexity_table.rb +27 -31
- data/lib/hashira/report/dependency_map.rb +17 -22
- data/lib/hashira/report/finding_lines.rb +15 -19
- data/lib/hashira/report/graph_payload.rb +14 -20
- data/lib/hashira/report/hotspot_table.rb +32 -37
- data/lib/hashira/report/json.rb +30 -32
- data/lib/hashira/report/metrics_table.rb +46 -33
- data/lib/hashira/report/text.rb +59 -52
- data/lib/hashira/version.rb +1 -1
- data/lib/hashira.rb +92 -55
- metadata +16 -2
- data/lib/hashira/analysis/root_namespace.rb +0 -14
|
@@ -1,36 +1,33 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
@evidence = empty_sets
|
|
11
|
-
end
|
|
3
|
+
class Hashira::Analysis::EdgeMap
|
|
4
|
+
def initialize(project, census)
|
|
5
|
+
@project = project
|
|
6
|
+
@census = census
|
|
7
|
+
@dependencies = sets
|
|
8
|
+
@evidence = sets
|
|
9
|
+
end
|
|
12
10
|
|
|
13
|
-
|
|
11
|
+
attr_reader :dependencies, :evidence
|
|
14
12
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
end
|
|
13
|
+
def record(file, tree)
|
|
14
|
+
source = @project.relative(file)
|
|
15
|
+
Hashira::Analysis::References.sightings(tree, @census.roots).each do |segments, line, nesting, home|
|
|
16
|
+
note(@census.charge(file, home), locate(segments, nesting), "#{source}:#{line}: #{segments.join("::")}")
|
|
17
|
+
end
|
|
18
|
+
end
|
|
22
19
|
|
|
23
|
-
|
|
20
|
+
private
|
|
24
21
|
|
|
25
|
-
|
|
22
|
+
def sets = Hash.new { |hash, key| hash[key] = Set.new }
|
|
26
23
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
24
|
+
def locate(segments, nesting)
|
|
25
|
+
nesting ? @census.resolve(segments, nesting) : @census.pinpoint(segments)
|
|
26
|
+
end
|
|
30
27
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
28
|
+
def note(from, to, sighting)
|
|
29
|
+
return unless to && to != from
|
|
30
|
+
@dependencies[from] << to
|
|
31
|
+
@evidence[[from, to]] << sighting
|
|
35
32
|
end
|
|
36
33
|
end
|
|
@@ -2,14 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
module Hashira
|
|
4
4
|
module Analysis
|
|
5
|
-
Finding =
|
|
6
|
-
|
|
5
|
+
Finding =
|
|
6
|
+
Data.define(:kind, :package, :message, :evidence, :cycle, :digest) do
|
|
7
|
+
def initialize(cycle: nil, digest: nil, **rest) = super
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
def signature = "#{kind}:#{identity}"
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
def identity = digest || package
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
def to_h = super.compact
|
|
14
|
+
end
|
|
14
15
|
end
|
|
15
16
|
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "placement"
|
|
4
|
+
|
|
5
|
+
class Hashira::Analysis::FolderPlacement < Hashira::Analysis::Placement
|
|
6
|
+
def mode = :folder
|
|
7
|
+
|
|
8
|
+
def placed = catalog.map { [it, it.folder] }
|
|
9
|
+
|
|
10
|
+
def baseline = catalog.folders
|
|
11
|
+
|
|
12
|
+
def charge(file, _nesting) = project.package(file)
|
|
13
|
+
|
|
14
|
+
def skip?(_segments) = false
|
|
15
|
+
|
|
16
|
+
def folding(_census) = Hashira::Analysis::NoFolding
|
|
17
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
class Hashira::Analysis::Folding
|
|
6
|
+
SUFFIXES = %w[Resource Serializer Policy Decorator].freeze
|
|
7
|
+
|
|
8
|
+
def initialize(definitions, census, suffixes: [])
|
|
9
|
+
@definitions = definitions
|
|
10
|
+
@census = census
|
|
11
|
+
@suffixes = suffixes
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def map = @map ||= links.keys.to_h { [it, settle(it, links)] }.reject { |from, to| from == to }
|
|
15
|
+
|
|
16
|
+
def disclosed
|
|
17
|
+
map.map { |from, to| { from:, to:, via: parents.key?(from) ? "base" : "suffix" } }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def links = @links ||= named.merge(parents)
|
|
23
|
+
|
|
24
|
+
def parents
|
|
25
|
+
@parents ||= singles.select { |_name, one| one.superclass }.to_h { |name, one| [name, target(one)] }.compact
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def named
|
|
29
|
+
singles.keys.filter_map { |name| pair(name, crop(name)) }.to_h
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def pair(name, stem) = ([name, stem] if stem && @census.packages.include?(stem))
|
|
33
|
+
|
|
34
|
+
def crop(name)
|
|
35
|
+
suffix = @suffixes.find { name.end_with?(it) && name != it }
|
|
36
|
+
suffix && name.delete_suffix(suffix)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def singles = @singles ||= lone.group_by(&:name).transform_values { it.find(&:superclass) || it.last }
|
|
40
|
+
|
|
41
|
+
def lone = @definitions.select(&:singular?).reject { anchored.include?(it.name) }
|
|
42
|
+
|
|
43
|
+
def anchored = @anchored ||= @definitions.select(&:nested?).to_set(&:name)
|
|
44
|
+
|
|
45
|
+
def target(one) = @census.pinpoint(Hashira::Analysis::Syntax.segments(one.superclass))
|
|
46
|
+
|
|
47
|
+
def settle(name, links)
|
|
48
|
+
trail = [name]
|
|
49
|
+
while (target = links[name]) && !trail.include?(target)
|
|
50
|
+
trail << (name = target)
|
|
51
|
+
end
|
|
52
|
+
target ? trail.drop(trail.index(target)).min : name
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -1,56 +1,53 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
end
|
|
3
|
+
class Hashira::Analysis::Graph
|
|
4
|
+
def initialize(project, trees, census)
|
|
5
|
+
@census = census
|
|
6
|
+
@map = Hashira::Analysis::EdgeMap.new(project, census)
|
|
7
|
+
trees.each { |file, tree| @map.record(file, tree) }
|
|
8
|
+
@cycles = Hashira::Analysis::Cycles.new(@map.dependencies, self)
|
|
9
|
+
end
|
|
11
10
|
|
|
12
|
-
|
|
11
|
+
attr_reader :cycles
|
|
13
12
|
|
|
14
|
-
|
|
13
|
+
def packages = (@census.packages | @map.dependencies.keys)
|
|
15
14
|
|
|
16
|
-
|
|
15
|
+
def packaging = @census.packaging
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
dependencies.sort.flat_map { |from, tos| tos.sort.map { Edge.new(from:, to: it) } }
|
|
20
|
-
end
|
|
17
|
+
def folds = @census.folds
|
|
21
18
|
|
|
22
|
-
|
|
23
|
-
edge_list.map do |edge|
|
|
24
|
-
from, to = edge.deconstruct
|
|
25
|
-
[from, to, weight(from, to)]
|
|
26
|
-
end
|
|
27
|
-
end
|
|
19
|
+
def outgoing(package) = dependencies[package].to_a.sort
|
|
28
20
|
|
|
29
|
-
|
|
21
|
+
def incoming(package) = packages.select { dependencies[it].include?(package) }.sort
|
|
30
22
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
efferent: dependencies[package].size)
|
|
35
|
-
end
|
|
23
|
+
def edges
|
|
24
|
+
dependencies.sort.flat_map { |from, tos| tos.sort.map { Hashira::Analysis::Edge.new(from:, to: it) } }
|
|
25
|
+
end
|
|
36
26
|
|
|
37
|
-
|
|
27
|
+
def weighted
|
|
28
|
+
edges.map do |edge|
|
|
29
|
+
from, to = edge.deconstruct
|
|
30
|
+
[from, to, weight(from, to)]
|
|
31
|
+
end
|
|
32
|
+
end
|
|
38
33
|
|
|
39
|
-
|
|
34
|
+
def evidence(from, to) = @map.evidence[[from, to]]
|
|
40
35
|
|
|
41
|
-
|
|
36
|
+
def metric(package)
|
|
37
|
+
Hashira::Analysis::Metric.new(
|
|
38
|
+
types: @census.types[package],
|
|
39
|
+
afferent: incoming(package).size,
|
|
40
|
+
efferent: dependencies[package].size
|
|
41
|
+
)
|
|
42
|
+
end
|
|
42
43
|
|
|
43
|
-
|
|
44
|
+
def metrics = packages.to_h { [it, metric(it)] }
|
|
44
45
|
|
|
45
|
-
|
|
46
|
+
def violations = Hashira::Analysis::SdpCheck.new(dependencies, metrics).violations
|
|
46
47
|
|
|
47
|
-
|
|
48
|
-
path.each_cons(2).min_by { |from, to| weight(from, to) }
|
|
49
|
-
end
|
|
48
|
+
def weight(from, to) = evidence(from, to).size
|
|
50
49
|
|
|
51
|
-
|
|
50
|
+
private
|
|
52
51
|
|
|
53
|
-
|
|
54
|
-
end
|
|
55
|
-
end
|
|
52
|
+
def dependencies = @map.dependencies
|
|
56
53
|
end
|
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
module Hashira
|
|
4
4
|
module Analysis
|
|
5
|
-
Metric =
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
Metric =
|
|
6
|
+
Data.define(:types, :afferent, :efferent) do
|
|
7
|
+
def instability
|
|
8
|
+
total = efferent + afferent
|
|
9
|
+
total.zero? ? 0.0 : efferent.fdiv(total)
|
|
10
|
+
end
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
def to_h = { tc: types, ca: afferent, ce: efferent, i: instability }
|
|
13
|
+
end
|
|
13
14
|
end
|
|
14
15
|
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "placement"
|
|
4
|
+
|
|
5
|
+
class Hashira::Analysis::NamespacePlacement < Hashira::Analysis::Placement
|
|
6
|
+
RAILS_BASES =
|
|
7
|
+
%w[ApplicationRecord ApplicationController ApplicationJob ApplicationMailer
|
|
8
|
+
ApplicationHelper ApplicationCable ApplicationResource ApplicationSerializer
|
|
9
|
+
ApplicationPolicy ApplicationDecorator].freeze
|
|
10
|
+
|
|
11
|
+
def mode = :namespace
|
|
12
|
+
|
|
13
|
+
def placed = catalog.map { [it, it.name] }
|
|
14
|
+
|
|
15
|
+
def baseline = []
|
|
16
|
+
|
|
17
|
+
def charge(_file, nesting)
|
|
18
|
+
nesting.reverse_each.filter_map { catalog.strip(it).first }.first || project.root
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def skip?(segments) = project.rails? && RAILS_BASES.include?(segments.first)
|
|
22
|
+
|
|
23
|
+
def folding(census) = Hashira::Analysis::Folding.new(catalog, census, suffixes: suffixes)
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def suffixes = project.rails? ? Hashira::Analysis::Folding::SUFFIXES : []
|
|
28
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hashira
|
|
4
|
+
module Analysis
|
|
5
|
+
module NamespacePrefix
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def infer(definitions)
|
|
9
|
+
build(definitions.map { |_node, full, _package| full }.uniq)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def build(fulls, prefix = [])
|
|
13
|
+
depth = prefix.length
|
|
14
|
+
wrapper = wrapper(fulls, depth)
|
|
15
|
+
return prefix unless wrapper
|
|
16
|
+
build(fulls.select { it[depth] == wrapper }, prefix + [wrapper])
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def wrapper(fulls, depth)
|
|
20
|
+
wrapper, count = fulls.filter_map { it[depth] if it[depth + 1] }.tally.max_by { |_wrapper, tally| tally }
|
|
21
|
+
wrapper if wrapper && count == peers(fulls, depth, wrapper)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def peers(fulls, depth, wrapper)
|
|
25
|
+
fulls.count do |full|
|
|
26
|
+
size = full.length
|
|
27
|
+
size > depth && !(size == depth + 1 && full[depth] == wrapper)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Hashira::Analysis::Naming
|
|
4
|
+
def initialize(definitions)
|
|
5
|
+
@segments = Hashira::Analysis::NamespacePrefix.infer(definitions)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
attr_reader :segments
|
|
9
|
+
|
|
10
|
+
def strip(path)
|
|
11
|
+
return [] if @segments.first(path.length) == path
|
|
12
|
+
path.drop(@segments.length.downto(0).find { path.first(it) == @segments.last(it) })
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -5,14 +5,14 @@ module Hashira
|
|
|
5
5
|
module NodeWalk
|
|
6
6
|
module_function
|
|
7
7
|
|
|
8
|
-
def
|
|
8
|
+
def each(node, &)
|
|
9
9
|
yield(node)
|
|
10
|
-
node.compact_child_nodes.each {
|
|
10
|
+
node.compact_child_nodes.each { each(it, &) }
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def collect(node)
|
|
14
14
|
found = []
|
|
15
|
-
|
|
15
|
+
each(node) { found << it }
|
|
16
16
|
found
|
|
17
17
|
end
|
|
18
18
|
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Hashira::Analysis::Placement
|
|
4
|
+
def self.build(packaging, project, catalog)
|
|
5
|
+
{
|
|
6
|
+
folder: Hashira::Analysis::FolderPlacement,
|
|
7
|
+
namespace: Hashira::Analysis::NamespacePlacement
|
|
8
|
+
}.fetch(packaging).new(project, catalog)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def initialize(project, catalog)
|
|
12
|
+
@project = project
|
|
13
|
+
@catalog = catalog
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
attr_reader :project, :catalog
|
|
19
|
+
end
|
|
@@ -8,31 +8,32 @@ module Hashira
|
|
|
8
8
|
module_function
|
|
9
9
|
|
|
10
10
|
def list(tree)
|
|
11
|
-
|
|
11
|
+
sightings(tree).map(&:first)
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
def
|
|
15
|
-
[].tap { collect(tree, it) }
|
|
14
|
+
def sightings(tree, roots = nil)
|
|
15
|
+
[].tap { collect(tree, [], it, roots) }
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
def collect(node, accumulator)
|
|
18
|
+
def collect(node, nesting, accumulator, roots, home = nesting)
|
|
19
19
|
return unless node
|
|
20
|
-
return accumulator <<
|
|
20
|
+
return accumulator << sighting(node, nesting, home) if constant?(node)
|
|
21
|
+
return enter(node, nesting, accumulator, roots) if definition?(node)
|
|
22
|
+
node.compact_child_nodes.each { collect(it, nesting, accumulator, roots, home) }
|
|
23
|
+
end
|
|
21
24
|
|
|
22
|
-
|
|
25
|
+
def sighting(node, nesting, home)
|
|
26
|
+
[Syntax.segments(node), node.location.start_line, Syntax.cbase?(node) ? nil : nesting, home]
|
|
23
27
|
end
|
|
24
28
|
|
|
25
29
|
def constant?(node) = node.is_a?(Prism::ConstantPathNode) || node.is_a?(Prism::ConstantReadNode)
|
|
26
30
|
|
|
27
|
-
def branches(node)
|
|
28
|
-
definition?(node) ? definition_branches(node) : node.compact_child_nodes
|
|
29
|
-
end
|
|
30
|
-
|
|
31
31
|
def definition?(node) = node.is_a?(Prism::ClassNode) || node.is_a?(Prism::ModuleNode)
|
|
32
32
|
|
|
33
|
-
def
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
def enter(node, nesting, accumulator, roots)
|
|
34
|
+
opened = nesting + [Syntax.anchor(nesting, Syntax.segments(node.constant_path), roots)]
|
|
35
|
+
collect(node.superclass, nesting, accumulator, roots, opened) if node.is_a?(Prism::ClassNode)
|
|
36
|
+
collect(node.body, opened, accumulator, roots)
|
|
36
37
|
end
|
|
37
38
|
end
|
|
38
39
|
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Hashira::Analysis::Resolver
|
|
4
|
+
def initialize(registry, catalog, placement)
|
|
5
|
+
@registry = registry
|
|
6
|
+
@catalog = catalog
|
|
7
|
+
@placement = placement
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def resolve(segments, nesting)
|
|
11
|
+
return if @placement.skip?(segments)
|
|
12
|
+
scoped = nesting.reverse_each.filter_map { descend(it, segments) }.first
|
|
13
|
+
scoped ? qualify(scoped) : @registry.package(@catalog.strip(segments))
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def pinpoint(segments)
|
|
17
|
+
return if @placement.skip?(segments)
|
|
18
|
+
qualify(descend([], segments))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def descend(entry, segments)
|
|
24
|
+
segments.length.downto(1).filter_map { @registry.exact(@catalog.strip(entry + segments.first(it))) }.first
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def qualify(found) = (found unless found == Hashira::Analysis::ConstantRegistry::AMBIGUOUS)
|
|
28
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Hashira::Analysis::Roster
|
|
4
|
+
def initialize(placed)
|
|
5
|
+
@registry = Hashira::Analysis::ConstantRegistry.new
|
|
6
|
+
@types = Hash.new(0)
|
|
7
|
+
counted = Set.new
|
|
8
|
+
placed.each { |definition, package| admit(definition, package, counted) }
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
attr_reader :registry, :types
|
|
12
|
+
|
|
13
|
+
def origins = @registry.origins
|
|
14
|
+
|
|
15
|
+
def packages = @types.keys | @registry.packages
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def admit(definition, package, counted)
|
|
20
|
+
return unless package
|
|
21
|
+
path = definition.path
|
|
22
|
+
@registry.register(path, package)
|
|
23
|
+
@types[package] += 1 if definition.counted? && counted.add?(path)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -1,22 +1,18 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
@graph = graph
|
|
9
|
-
end
|
|
3
|
+
class Hashira::Analysis::Rule
|
|
4
|
+
def initialize(project, graph)
|
|
5
|
+
@project = project
|
|
6
|
+
@graph = graph
|
|
7
|
+
end
|
|
10
8
|
|
|
11
|
-
|
|
9
|
+
private
|
|
12
10
|
|
|
13
|
-
|
|
11
|
+
attr_reader :project, :graph
|
|
14
12
|
|
|
15
|
-
|
|
13
|
+
def metrics = @metrics ||= graph.metrics
|
|
16
14
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
end
|
|
20
|
-
end
|
|
15
|
+
def finding(**attributes)
|
|
16
|
+
Hashira::Analysis::Finding.new(kind: self.class::KIND, cycle: nil, **attributes)
|
|
21
17
|
end
|
|
22
18
|
end
|
|
@@ -1,18 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
@metrics = metrics
|
|
9
|
-
end
|
|
3
|
+
class Hashira::Analysis::SdpCheck
|
|
4
|
+
def initialize(dependencies, metrics)
|
|
5
|
+
@dependencies = dependencies
|
|
6
|
+
@metrics = metrics
|
|
7
|
+
end
|
|
10
8
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
end
|
|
15
|
-
end
|
|
9
|
+
def violations
|
|
10
|
+
@dependencies.flat_map do |from, tos|
|
|
11
|
+
tos.select { @metrics[it].instability > @metrics[from].instability }.map { [from, it] }
|
|
16
12
|
end
|
|
17
13
|
end
|
|
18
14
|
end
|
|
@@ -1,27 +1,29 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
module Analysis
|
|
5
|
-
class SdpViolationFindings < Rule
|
|
6
|
-
KIND = "sdp_violation"
|
|
3
|
+
require_relative "rule"
|
|
7
4
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
.map { |from, to| violation_finding(from, to) }
|
|
11
|
-
end
|
|
5
|
+
class Hashira::Analysis::SdpViolationFindings < Hashira::Analysis::Rule
|
|
6
|
+
KIND = "sdp_violation"
|
|
12
7
|
|
|
13
|
-
|
|
8
|
+
def list
|
|
9
|
+
ranked.map { |from, to| violation(from, to) }
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
14
13
|
|
|
15
|
-
|
|
16
|
-
finding(package: from, evidence: graph.evidence_for(from, to).to_a.first(5),
|
|
17
|
-
message: "#{from} (I=#{label(from)}) depends on the LESS stable #{to} " \
|
|
18
|
-
"(I=#{label(to)}) — churn in #{to} will force churn in #{from}. " \
|
|
19
|
-
"Invert the edge or extract the stable part of #{to} that #{from} needs.")
|
|
20
|
-
end
|
|
14
|
+
def ranked = graph.violations.sort_by { |from, to| instability(from) - instability(to) }
|
|
21
15
|
|
|
22
|
-
|
|
16
|
+
def violation(from, to)
|
|
17
|
+
finding(package: from, evidence: graph.evidence(from, to).to_a.first(5), message: message(from, to))
|
|
18
|
+
end
|
|
23
19
|
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
def message(from, to)
|
|
21
|
+
"#{from} (I=#{label(from)}) depends on the LESS stable #{to} " \
|
|
22
|
+
"(I=#{label(to)}) — churn in #{to} will force churn in #{from}. " \
|
|
23
|
+
"Invert the edge or extract the stable part of #{to} that #{from} needs."
|
|
26
24
|
end
|
|
25
|
+
|
|
26
|
+
def instability(package) = metrics[package].instability
|
|
27
|
+
|
|
28
|
+
def label(package) = format("%.2f", instability(package))
|
|
27
29
|
end
|
|
@@ -7,20 +7,35 @@ module Hashira
|
|
|
7
7
|
module Syntax
|
|
8
8
|
module_function
|
|
9
9
|
|
|
10
|
-
def
|
|
10
|
+
def segments(node)
|
|
11
11
|
case node
|
|
12
12
|
when Prism::ConstantReadNode then [node.name.to_s]
|
|
13
|
-
when Prism::ConstantPathNode then
|
|
13
|
+
when Prism::ConstantPathNode then segments(node.parent) + label(node)
|
|
14
14
|
else []
|
|
15
15
|
end
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
def
|
|
18
|
+
def label(node) = [node.name.to_s]
|
|
19
19
|
|
|
20
|
-
def
|
|
20
|
+
def cbase?(node)
|
|
21
|
+
return false unless node.is_a?(Prism::ConstantPathNode)
|
|
22
|
+
parent = node.parent
|
|
23
|
+
!parent || cbase?(parent)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def anchor(stack, segments, roots)
|
|
27
|
+
return (stack.last || []) + segments if segments.length < 2 || !roots
|
|
28
|
+
base = stack.reverse_each.find { roots.include?(it + segments.first(1)) }
|
|
29
|
+
base ? base + segments : hoist(stack, segments, roots)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def hoist(stack, segments, roots)
|
|
33
|
+
roots.include?(segments.first(1)) ? segments : (stack.last || []) + segments
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def direct(type_node)
|
|
21
37
|
body = type_node.body
|
|
22
|
-
|
|
23
|
-
statements.grep(Prism::DefNode)
|
|
38
|
+
(body.is_a?(Prism::StatementsNode) ? body.body : [body]).grep(Prism::DefNode)
|
|
24
39
|
end
|
|
25
40
|
end
|
|
26
41
|
end
|