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.
Files changed (88) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +58 -0
  3. data/README.md +86 -25
  4. data/lib/hashira/analysis/finding.rb +2 -2
  5. data/lib/hashira/ci/ratchet.rb +5 -9
  6. data/lib/hashira/cli/fail_on.rb +15 -5
  7. data/lib/hashira/cli/run.rb +8 -4
  8. data/lib/hashira/cli/usage.rb +8 -5
  9. data/lib/hashira/complexity/boolean_run.rb +5 -3
  10. data/lib/hashira/complexity/cognitive_score.rb +21 -14
  11. data/lib/hashira/complexity/if_chain.rb +14 -14
  12. data/lib/hashira/complexity/method_finding.rb +3 -20
  13. data/lib/hashira/complexity/rescue_scan.rb +9 -9
  14. data/lib/hashira/complexity/{analyzer.rb → scores.rb} +6 -5
  15. data/lib/hashira/coupling/audiences.rb +49 -0
  16. data/lib/hashira/{analysis → coupling}/catalog.rb +3 -3
  17. data/lib/hashira/{analysis → coupling}/census.rb +17 -8
  18. data/lib/hashira/{analysis → coupling}/constant_registry.rb +6 -2
  19. data/lib/hashira/coupling/cycle_findings.rb +31 -0
  20. data/lib/hashira/{analysis → coupling}/cycle_search.rb +1 -1
  21. data/lib/hashira/{analysis → coupling}/cycles.rb +2 -2
  22. data/lib/hashira/{analysis → coupling}/definition.rb +1 -1
  23. data/lib/hashira/{analysis → coupling}/definitions.rb +1 -1
  24. data/lib/hashira/{analysis → coupling}/edge.rb +1 -1
  25. data/lib/hashira/{analysis → coupling}/edge_map.rb +10 -10
  26. data/lib/hashira/{analysis → coupling}/folder_placement.rb +2 -2
  27. data/lib/hashira/{analysis → coupling}/folding.rb +4 -2
  28. data/lib/hashira/{analysis → coupling}/graph.rb +18 -8
  29. data/lib/hashira/{analysis → coupling}/metric.rb +1 -1
  30. data/lib/hashira/coupling/mixed_audience_findings.rb +32 -0
  31. data/lib/hashira/{analysis → coupling}/namespace_placement.rb +3 -3
  32. data/lib/hashira/{analysis → coupling}/namespace_prefix.rb +1 -1
  33. data/lib/hashira/{analysis → coupling}/naming.rb +2 -2
  34. data/lib/hashira/{analysis → coupling}/no_folding.rb +1 -1
  35. data/lib/hashira/{analysis → coupling}/placement.rb +3 -3
  36. data/lib/hashira/coupling/references.rb +55 -0
  37. data/lib/hashira/coupling/report.rb +28 -0
  38. data/lib/hashira/coupling/roll_call.rb +45 -0
  39. data/lib/hashira/coupling/roll_call_findings.rb +30 -0
  40. data/lib/hashira/{analysis → coupling}/roster.rb +2 -2
  41. data/lib/hashira/{analysis → coupling}/rule.rb +1 -1
  42. data/lib/hashira/{analysis/resolver.rb → coupling/scope.rb} +2 -2
  43. data/lib/hashira/{analysis → coupling}/sdp_check.rb +1 -1
  44. data/lib/hashira/coupling/sdp_violation_findings.rb +25 -0
  45. data/lib/hashira/coupling/wide_edge_findings.rb +25 -0
  46. data/lib/hashira/coupling/words.rb +29 -0
  47. data/lib/hashira/diagram/{renderer.rb → source.rb} +2 -2
  48. data/lib/hashira/duplication/{analyzer.rb → clones.rb} +3 -3
  49. data/lib/hashira/duplication/{clusterer.rb → clusters.rb} +3 -3
  50. data/lib/hashira/duplication/delta.rb +6 -14
  51. data/lib/hashira/duplication/duplication_finding.rb +6 -10
  52. data/lib/hashira/duplication/{extractor.rb → harvest.rb} +1 -1
  53. data/lib/hashira/duplication/sequence.rb +3 -1
  54. data/lib/hashira/duplication/similarity.rb +5 -1
  55. data/lib/hashira/duplication/variance.rb +3 -3
  56. data/lib/hashira/hotspots/rollup.rb +1 -1
  57. data/lib/hashira/pipeline.rb +15 -10
  58. data/lib/hashira/report/complexity_table.rb +1 -1
  59. data/lib/hashira/report/finding_lines.rb +1 -1
  60. data/lib/hashira/report/json.rb +5 -3
  61. data/lib/hashira/report/phrases.rb +98 -0
  62. data/lib/hashira/report/smell_phrases.rb +68 -0
  63. data/lib/hashira/smells/census.rb +32 -0
  64. data/lib/hashira/smells/check.rb +30 -0
  65. data/lib/hashira/smells/conditions.rb +44 -0
  66. data/lib/hashira/smells/contexts.rb +64 -0
  67. data/lib/hashira/smells/control_parameter.rb +22 -0
  68. data/lib/hashira/smells/data_clump.rb +30 -0
  69. data/lib/hashira/smells/duplicate_method_call.rb +54 -0
  70. data/lib/hashira/smells/feature_envy.rb +17 -0
  71. data/lib/hashira/smells/instance_variable_assumption.rb +44 -0
  72. data/lib/hashira/smells/manual_dispatch.rb +16 -0
  73. data/lib/hashira/smells/module_initialize.rb +9 -0
  74. data/lib/hashira/smells/nil_check.rb +32 -0
  75. data/lib/hashira/smells/param_check.rb +51 -0
  76. data/lib/hashira/smells/refs.rb +53 -0
  77. data/lib/hashira/smells/repeated_conditional.rb +29 -0
  78. data/lib/hashira/smells/report.rb +31 -0
  79. data/lib/hashira/smells/scope.rb +29 -0
  80. data/lib/hashira/smells/too_many_instance_variables.rb +24 -0
  81. data/lib/hashira/smells/utility_function.rb +13 -0
  82. data/lib/hashira/smells/visibility.rb +64 -0
  83. data/lib/hashira/version.rb +1 -1
  84. data/lib/hashira.rb +65 -30
  85. metadata +60 -31
  86. data/lib/hashira/analysis/cycle_findings.rb +0 -38
  87. data/lib/hashira/analysis/references.rb +0 -40
  88. 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::Analyzer
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 methods = @scores.sort_by { -it.cognitive }
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 = methods.select { it.cognitive >= THRESHOLD }
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
- cognitive: score.total, calls: score.calls, increments: score.increments
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::Analysis::Catalog
3
+ class Hashira::Coupling::Catalog
4
4
  include Enumerable
5
5
 
6
6
  def initialize(definitions)
7
7
  @definitions = definitions
8
- @naming = Hashira::Analysis::Naming.new(definitions)
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::Analysis::Definition.new(node:, path: @naming.strip(full), folder:)
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::Analysis::Census
3
+ class Hashira::Coupling::Census
4
4
  def initialize(project, trees, packaging: :folder)
5
- @catalog = Hashira::Analysis::Catalog.new(Hashira::Analysis::Definitions.new(project, trees))
6
- @placement = Hashira::Analysis::Placement.build(packaging, project, @catalog)
7
- @folding = Hashira::Analysis::NoFolding
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 resolve(segments, nesting = []) = resolver.resolve(segments, nesting)
26
+ def strip(segments) = @catalog.strip(segments)
27
27
 
28
- def pinpoint(segments) = resolver.pinpoint(segments)
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::Analysis::Roster.new(@placement.placed.map { |definition, package| [definition, translate(package)] })
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 resolver = Hashira::Analysis::Resolver.new(@roster.registry, @catalog, @placement)
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::Analysis::ConstantRegistry
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.length - 1).downto(1).filter_map { @origins[path.first(it).join("::")] }.first
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::Analysis::CycleSearch
3
+ class Hashira::Coupling::CycleSearch
4
4
  def initialize(dependencies, package)
5
5
  @dependencies = dependencies
6
6
  @package = package
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class Hashira::Analysis::Cycles
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::Analysis::CycleSearch.new(@dependencies, package).path
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
@@ -3,7 +3,7 @@
3
3
  require "prism"
4
4
 
5
5
  module Hashira
6
- module Analysis
6
+ module Coupling
7
7
  Definition =
8
8
  Data.define(:node, :path, :folder) do
9
9
  def name = path.first
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class Hashira::Analysis::Definitions
3
+ class Hashira::Coupling::Definitions
4
4
  include Enumerable
5
5
 
6
6
  def initialize(project, trees)
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hashira
4
- module Analysis
4
+ module Coupling
5
5
  Edge =
6
6
  Data.define(:from, :to) do
7
7
  def to_s = "#{from} -> #{to}"
@@ -1,19 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class Hashira::Analysis::EdgeMap
4
- def initialize(project, census)
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
- 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("::")}")
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, sighting)
27
+ def note(from, to, segments, site)
29
28
  return unless to && to != from
30
29
  @dependencies[from] << to
31
- @evidence[[from, to]] << sighting
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::Analysis::FolderPlacement < Hashira::Analysis::Placement
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::Analysis::NoFolding
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::Analysis::Folding
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 { name.end_with?(it) && name != it }
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::Analysis::Graph
3
+ class Hashira::Coupling::Graph
4
4
  def initialize(project, trees, census)
5
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)
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::Analysis::Edge.new(from:, to: it) } }
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::Analysis::Metric.new(
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::Analysis::SdpCheck.new(dependencies, metrics).violations
56
+ def violations = Hashira::Coupling::SdpCheck.new(dependencies, metrics).violations
47
57
 
48
58
  def weight(from, to) = evidence(from, to).size
49
59
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hashira
4
- module Analysis
4
+ module Coupling
5
5
  Metric =
6
6
  Data.define(:types, :afferent, :efferent) do
7
7
  def instability
@@ -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::Analysis::NamespacePlacement < Hashira::Analysis::Placement
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::Analysis::Folding.new(catalog, census, suffixes: suffixes)
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::Analysis::Folding::SUFFIXES : []
27
+ def suffixes = project.rails? ? Hashira::Coupling::Folding::SUFFIXES : []
28
28
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hashira
4
- module Analysis
4
+ module Coupling
5
5
  module NamespacePrefix
6
6
  module_function
7
7
 
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class Hashira::Analysis::Naming
3
+ class Hashira::Coupling::Naming
4
4
  def initialize(definitions)
5
- @segments = Hashira::Analysis::NamespacePrefix.infer(definitions)
5
+ @segments = Hashira::Coupling::NamespacePrefix.infer(definitions)
6
6
  end
7
7
 
8
8
  attr_reader :segments
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hashira
4
- module Analysis
4
+ module Coupling
5
5
  module NoFolding
6
6
  module_function
7
7
 
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class Hashira::Analysis::Placement
3
+ class Hashira::Coupling::Placement
4
4
  def self.build(packaging, project, catalog)
5
5
  {
6
- folder: Hashira::Analysis::FolderPlacement,
7
- namespace: Hashira::Analysis::NamespacePlacement
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