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.
Files changed (94) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +141 -0
  3. data/README.md +53 -5
  4. data/lib/hashira/analysis/catalog.rb +27 -0
  5. data/lib/hashira/analysis/census.rb +42 -35
  6. data/lib/hashira/analysis/constant_registry.rb +43 -0
  7. data/lib/hashira/analysis/cycle_findings.rb +34 -29
  8. data/lib/hashira/analysis/cycle_search.rb +28 -35
  9. data/lib/hashira/analysis/cycles.rb +14 -0
  10. data/lib/hashira/analysis/definition.rb +22 -0
  11. data/lib/hashira/analysis/definitions.rb +22 -18
  12. data/lib/hashira/analysis/edge.rb +4 -3
  13. data/lib/hashira/analysis/edge_map.rb +23 -26
  14. data/lib/hashira/analysis/finding.rb +7 -6
  15. data/lib/hashira/analysis/folder_placement.rb +17 -0
  16. data/lib/hashira/analysis/folding.rb +54 -0
  17. data/lib/hashira/analysis/graph.rb +35 -38
  18. data/lib/hashira/analysis/metric.rb +8 -7
  19. data/lib/hashira/analysis/namespace_placement.rb +28 -0
  20. data/lib/hashira/analysis/namespace_prefix.rb +32 -0
  21. data/lib/hashira/analysis/naming.rb +14 -0
  22. data/lib/hashira/analysis/no_folding.rb +13 -0
  23. data/lib/hashira/analysis/node_walk.rb +3 -3
  24. data/lib/hashira/analysis/placement.rb +19 -0
  25. data/lib/hashira/analysis/references.rb +14 -13
  26. data/lib/hashira/analysis/resolver.rb +28 -0
  27. data/lib/hashira/analysis/roster.rb +25 -0
  28. data/lib/hashira/analysis/rule.rb +10 -14
  29. data/lib/hashira/analysis/sdp_check.rb +8 -12
  30. data/lib/hashira/analysis/sdp_violation_findings.rb +20 -18
  31. data/lib/hashira/analysis/syntax.rb +21 -6
  32. data/lib/hashira/analysis/type_walk.rb +7 -9
  33. data/lib/hashira/churn.rb +11 -13
  34. data/lib/hashira/ci/accepted.rb +27 -31
  35. data/lib/hashira/ci/baseline.rb +22 -24
  36. data/lib/hashira/ci/diff.rb +8 -7
  37. data/lib/hashira/ci/edge_diff_report.rb +13 -17
  38. data/lib/hashira/ci/finding_diff_report.rb +14 -18
  39. data/lib/hashira/ci/gate.rb +19 -23
  40. data/lib/hashira/ci/improvement.rb +9 -14
  41. data/lib/hashira/ci/ratchet.rb +36 -27
  42. data/lib/hashira/ci/ratchet_report.rb +26 -32
  43. data/lib/hashira/cli/arguments.rb +23 -0
  44. data/lib/hashira/cli/command_line.rb +55 -71
  45. data/lib/hashira/cli/fail_on.rb +15 -20
  46. data/lib/hashira/cli/options.rb +9 -4
  47. data/lib/hashira/cli/package_by.rb +14 -0
  48. data/lib/hashira/cli/run.rb +29 -27
  49. data/lib/hashira/cli/skip.rb +14 -20
  50. data/lib/hashira/cli/usage.rb +41 -41
  51. data/lib/hashira/cli.rb +17 -19
  52. data/lib/hashira/complexity/analyzer.rb +30 -32
  53. data/lib/hashira/complexity/boolean_run.rb +11 -15
  54. data/lib/hashira/complexity/cognitive_score.rb +61 -66
  55. data/lib/hashira/complexity/if_chain.rb +33 -38
  56. data/lib/hashira/complexity/method_finding.rb +43 -48
  57. data/lib/hashira/complexity/method_score.rb +9 -7
  58. data/lib/hashira/complexity/rescue_scan.rb +16 -21
  59. data/lib/hashira/complexity/rollup.rb +13 -15
  60. data/lib/hashira/diagram/dot.rb +8 -11
  61. data/lib/hashira/diagram/mermaid.rb +11 -16
  62. data/lib/hashira/diagram/renderer.rb +10 -15
  63. data/lib/hashira/duplication/analyzer.rb +10 -14
  64. data/lib/hashira/duplication/cluster.rb +15 -12
  65. data/lib/hashira/duplication/clusterer.rb +31 -29
  66. data/lib/hashira/duplication/delta.rb +29 -30
  67. data/lib/hashira/duplication/duplication_finding.rb +19 -21
  68. data/lib/hashira/duplication/extractor.rb +15 -19
  69. data/lib/hashira/duplication/fragment.rb +19 -23
  70. data/lib/hashira/duplication/grouping.rb +12 -16
  71. data/lib/hashira/duplication/index.rb +20 -24
  72. data/lib/hashira/duplication/maximal.rb +11 -15
  73. data/lib/hashira/duplication/near_miss.rb +19 -22
  74. data/lib/hashira/duplication/sequence.rb +20 -22
  75. data/lib/hashira/duplication/similarity.rb +28 -35
  76. data/lib/hashira/duplication/union_find.rb +10 -14
  77. data/lib/hashira/duplication/variance.rb +36 -44
  78. data/lib/hashira/error.rb +4 -2
  79. data/lib/hashira/hotspots/file_cost.rb +8 -7
  80. data/lib/hashira/hotspots/rollup.rb +19 -21
  81. data/lib/hashira/pipeline.rb +36 -31
  82. data/lib/hashira/project.rb +53 -32
  83. data/lib/hashira/report/complexity_table.rb +27 -31
  84. data/lib/hashira/report/dependency_map.rb +17 -22
  85. data/lib/hashira/report/finding_lines.rb +15 -19
  86. data/lib/hashira/report/graph_payload.rb +14 -20
  87. data/lib/hashira/report/hotspot_table.rb +32 -37
  88. data/lib/hashira/report/json.rb +30 -32
  89. data/lib/hashira/report/metrics_table.rb +46 -33
  90. data/lib/hashira/report/text.rb +59 -52
  91. data/lib/hashira/version.rb +1 -1
  92. data/lib/hashira.rb +92 -55
  93. metadata +16 -2
  94. data/lib/hashira/analysis/root_namespace.rb +0 -14
@@ -1,36 +1,33 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Hashira
4
- module Analysis
5
- class EdgeMap
6
- def initialize(project, census)
7
- @project = project
8
- @census = census
9
- @dependencies = empty_sets
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
- attr_reader :dependencies, :evidence
11
+ attr_reader :dependencies, :evidence
14
12
 
15
- def record(file, tree)
16
- from = @project.package_for(file)
17
- source = @project.relative(file)
18
- References.each_sighting(tree).each do |segments, line|
19
- record_reference(from, source, segments, line)
20
- end
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
- private
20
+ private
24
21
 
25
- def empty_sets = Hash.new { |hash, key| hash[key] = Set.new }
22
+ def sets = Hash.new { |hash, key| hash[key] = Set.new }
26
23
 
27
- def record_reference(from, source, segments, line)
28
- to = @census.resolve(segments)
29
- return unless to && to != from
24
+ def locate(segments, nesting)
25
+ nesting ? @census.resolve(segments, nesting) : @census.pinpoint(segments)
26
+ end
30
27
 
31
- @dependencies[from] << to
32
- @evidence[[from, to]] << "#{source}:#{line}: #{segments.join("::")}"
33
- end
34
- end
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 = Data.define(:kind, :package, :message, :evidence, :cycle, :digest) do
6
- def initialize(cycle: nil, digest: nil, **rest) = super
5
+ Finding =
6
+ Data.define(:kind, :package, :message, :evidence, :cycle, :digest) do
7
+ def initialize(cycle: nil, digest: nil, **rest) = super
7
8
 
8
- def signature = "#{kind}:#{identity}"
9
+ def signature = "#{kind}:#{identity}"
9
10
 
10
- def identity = digest || package
11
+ def identity = digest || package
11
12
 
12
- def to_h = super.compact
13
- end
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
- module Hashira
4
- module Analysis
5
- class Graph
6
- def initialize(project, trees, census)
7
- @census = census
8
- @edge_map = EdgeMap.new(project, census)
9
- trees.each { |file, tree| @edge_map.record(file, tree) }
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
- def packages = @census.packages
11
+ attr_reader :cycles
13
12
 
14
- def dependencies_of(package) = dependencies[package].to_a.sort
13
+ def packages = (@census.packages | @map.dependencies.keys)
15
14
 
16
- def dependents_of(package) = packages.select { dependencies[it].include?(package) }.sort
15
+ def packaging = @census.packaging
17
16
 
18
- def edge_list
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
- def weighted_edges
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
- def evidence_for(from, to) = @edge_map.evidence[[from, to]]
21
+ def incoming(package) = packages.select { dependencies[it].include?(package) }.sort
30
22
 
31
- def metric_for(package)
32
- Metric.new(type_count: @census.type_count[package],
33
- afferent: dependents_of(package).size,
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
- def metrics = packages.to_h { [it, metric_for(it)] }
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
- def sdp_violations = SdpCheck.new(dependencies, metrics).violations
34
+ def evidence(from, to) = @map.evidence[[from, to]]
40
35
 
41
- def cyclic?(package) = !!cycle_path(package)
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
- def weight(from, to) = evidence_for(from, to).size
44
+ def metrics = packages.to_h { [it, metric(it)] }
44
45
 
45
- def cycle_path(package) = CycleSearch.new(dependencies, package).path
46
+ def violations = Hashira::Analysis::SdpCheck.new(dependencies, metrics).violations
46
47
 
47
- def weakest_edge(path)
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
- private
50
+ private
52
51
 
53
- def dependencies = @edge_map.dependencies
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 = Data.define(:type_count, :afferent, :efferent) do
6
- def instability
7
- total = efferent + afferent
8
- total.zero? ? 0.0 : efferent.to_f / total
9
- end
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
- def to_h = { tc: type_count, ca: afferent, ce: efferent, i: instability }
12
- end
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
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hashira
4
+ module Analysis
5
+ module NoFolding
6
+ module_function
7
+
8
+ def map = {}
9
+
10
+ def disclosed = []
11
+ end
12
+ end
13
+ end
@@ -5,14 +5,14 @@ module Hashira
5
5
  module NodeWalk
6
6
  module_function
7
7
 
8
- def each_node(node, &)
8
+ def each(node, &)
9
9
  yield(node)
10
- node.compact_child_nodes.each { each_node(it, &) }
10
+ node.compact_child_nodes.each { each(it, &) }
11
11
  end
12
12
 
13
13
  def collect(node)
14
14
  found = []
15
- each_node(node) { found << it }
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
- each_sighting(tree).map(&:first)
11
+ sightings(tree).map(&:first)
12
12
  end
13
13
 
14
- def each_sighting(tree)
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 << [Syntax.path_segments(node), node.location.start_line] if constant?(node)
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
- branches(node).each { collect(it, accumulator) }
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 definition_branches(node)
34
- superclass = node.is_a?(Prism::ClassNode) ? node.superclass : nil
35
- [superclass, node.body]
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
- module Hashira
4
- module Analysis
5
- class Rule
6
- def initialize(project, graph)
7
- @project = project
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
- private
9
+ private
12
10
 
13
- attr_reader :project, :graph
11
+ attr_reader :project, :graph
14
12
 
15
- def metrics = @metrics ||= graph.metrics
13
+ def metrics = @metrics ||= graph.metrics
16
14
 
17
- def finding(**attributes)
18
- Finding.new(kind: self.class::KIND, cycle: nil, **attributes)
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
- module Hashira
4
- module Analysis
5
- class SdpCheck
6
- def initialize(dependencies, metrics)
7
- @dependencies = dependencies
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
- def violations
12
- @dependencies.flat_map do |from, tos|
13
- tos.select { @metrics[it].instability > @metrics[from].instability }.map { [from, it] }
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
- module Hashira
4
- module Analysis
5
- class SdpViolationFindings < Rule
6
- KIND = "sdp_violation"
3
+ require_relative "rule"
7
4
 
8
- def list
9
- graph.sdp_violations.sort_by { |from, to| instability(from) - instability(to) }
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
- private
8
+ def list
9
+ ranked.map { |from, to| violation(from, to) }
10
+ end
11
+
12
+ private
14
13
 
15
- def violation_finding(from, to)
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
- def instability(package) = metrics[package].instability
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
- def label(package) = format("%.2f", instability(package))
25
- end
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 path_segments(node)
10
+ def segments(node)
11
11
  case node
12
12
  when Prism::ConstantReadNode then [node.name.to_s]
13
- when Prism::ConstantPathNode then path_segments(node.parent) + name_of(node)
13
+ when Prism::ConstantPathNode then segments(node.parent) + label(node)
14
14
  else []
15
15
  end
16
16
  end
17
17
 
18
- def name_of(node) = [node.name.to_s]
18
+ def label(node) = [node.name.to_s]
19
19
 
20
- def direct_definitions(type_node)
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
- statements = body.is_a?(Prism::StatementsNode) ? body.body : [body]
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