hashira 0.3.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 (140) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +155 -0
  3. data/README.md +127 -25
  4. data/lib/hashira/analysis/finding.rb +7 -6
  5. data/lib/hashira/analysis/node_walk.rb +3 -3
  6. data/lib/hashira/analysis/syntax.rb +21 -6
  7. data/lib/hashira/analysis/type_walk.rb +7 -9
  8. data/lib/hashira/churn.rb +11 -13
  9. data/lib/hashira/ci/accepted.rb +27 -31
  10. data/lib/hashira/ci/baseline.rb +22 -24
  11. data/lib/hashira/ci/diff.rb +8 -7
  12. data/lib/hashira/ci/edge_diff_report.rb +13 -17
  13. data/lib/hashira/ci/finding_diff_report.rb +14 -18
  14. data/lib/hashira/ci/gate.rb +19 -23
  15. data/lib/hashira/ci/improvement.rb +9 -14
  16. data/lib/hashira/ci/ratchet.rb +32 -27
  17. data/lib/hashira/ci/ratchet_report.rb +26 -32
  18. data/lib/hashira/cli/arguments.rb +23 -0
  19. data/lib/hashira/cli/command_line.rb +55 -71
  20. data/lib/hashira/cli/fail_on.rb +24 -19
  21. data/lib/hashira/cli/options.rb +9 -4
  22. data/lib/hashira/cli/package_by.rb +14 -0
  23. data/lib/hashira/cli/run.rb +33 -27
  24. data/lib/hashira/cli/skip.rb +14 -20
  25. data/lib/hashira/cli/usage.rb +44 -41
  26. data/lib/hashira/cli.rb +17 -19
  27. data/lib/hashira/complexity/boolean_run.rb +13 -15
  28. data/lib/hashira/complexity/cognitive_score.rb +68 -66
  29. data/lib/hashira/complexity/if_chain.rb +33 -38
  30. data/lib/hashira/complexity/method_finding.rb +20 -42
  31. data/lib/hashira/complexity/method_score.rb +9 -7
  32. data/lib/hashira/complexity/rescue_scan.rb +16 -21
  33. data/lib/hashira/complexity/rollup.rb +13 -15
  34. data/lib/hashira/complexity/scores.rb +44 -0
  35. data/lib/hashira/coupling/audiences.rb +49 -0
  36. data/lib/hashira/coupling/catalog.rb +27 -0
  37. data/lib/hashira/coupling/census.rb +55 -0
  38. data/lib/hashira/coupling/constant_registry.rb +47 -0
  39. data/lib/hashira/coupling/cycle_findings.rb +31 -0
  40. data/lib/hashira/coupling/cycle_search.rb +37 -0
  41. data/lib/hashira/coupling/cycles.rb +14 -0
  42. data/lib/hashira/coupling/definition.rb +22 -0
  43. data/lib/hashira/coupling/definitions.rb +31 -0
  44. data/lib/hashira/coupling/edge.rb +10 -0
  45. data/lib/hashira/coupling/edge_map.rb +33 -0
  46. data/lib/hashira/coupling/folder_placement.rb +17 -0
  47. data/lib/hashira/coupling/folding.rb +56 -0
  48. data/lib/hashira/coupling/graph.rb +63 -0
  49. data/lib/hashira/coupling/metric.rb +15 -0
  50. data/lib/hashira/coupling/mixed_audience_findings.rb +32 -0
  51. data/lib/hashira/coupling/namespace_placement.rb +28 -0
  52. data/lib/hashira/{analysis → coupling}/namespace_prefix.rb +11 -11
  53. data/lib/hashira/coupling/naming.rb +14 -0
  54. data/lib/hashira/coupling/no_folding.rb +13 -0
  55. data/lib/hashira/coupling/placement.rb +19 -0
  56. data/lib/hashira/coupling/references.rb +55 -0
  57. data/lib/hashira/coupling/report.rb +28 -0
  58. data/lib/hashira/coupling/roll_call.rb +45 -0
  59. data/lib/hashira/coupling/roll_call_findings.rb +30 -0
  60. data/lib/hashira/coupling/roster.rb +25 -0
  61. data/lib/hashira/coupling/rule.rb +18 -0
  62. data/lib/hashira/coupling/scope.rb +28 -0
  63. data/lib/hashira/coupling/sdp_check.rb +14 -0
  64. data/lib/hashira/coupling/sdp_violation_findings.rb +25 -0
  65. data/lib/hashira/coupling/wide_edge_findings.rb +25 -0
  66. data/lib/hashira/coupling/words.rb +29 -0
  67. data/lib/hashira/diagram/dot.rb +8 -11
  68. data/lib/hashira/diagram/mermaid.rb +11 -16
  69. data/lib/hashira/diagram/source.rb +15 -0
  70. data/lib/hashira/duplication/clones.rb +17 -0
  71. data/lib/hashira/duplication/cluster.rb +14 -13
  72. data/lib/hashira/duplication/clusters.rb +46 -0
  73. data/lib/hashira/duplication/delta.rb +21 -30
  74. data/lib/hashira/duplication/duplication_finding.rb +17 -23
  75. data/lib/hashira/duplication/fragment.rb +19 -23
  76. data/lib/hashira/duplication/grouping.rb +12 -16
  77. data/lib/hashira/duplication/harvest.rb +27 -0
  78. data/lib/hashira/duplication/index.rb +20 -24
  79. data/lib/hashira/duplication/maximal.rb +11 -15
  80. data/lib/hashira/duplication/near_miss.rb +19 -22
  81. data/lib/hashira/duplication/sequence.rb +20 -22
  82. data/lib/hashira/duplication/similarity.rb +31 -34
  83. data/lib/hashira/duplication/union_find.rb +10 -14
  84. data/lib/hashira/duplication/variance.rb +35 -43
  85. data/lib/hashira/error.rb +4 -2
  86. data/lib/hashira/hotspots/file_cost.rb +8 -7
  87. data/lib/hashira/hotspots/rollup.rb +19 -21
  88. data/lib/hashira/pipeline.rb +41 -31
  89. data/lib/hashira/project.rb +50 -49
  90. data/lib/hashira/report/complexity_table.rb +27 -31
  91. data/lib/hashira/report/dependency_map.rb +17 -22
  92. data/lib/hashira/report/finding_lines.rb +15 -19
  93. data/lib/hashira/report/graph_payload.rb +14 -20
  94. data/lib/hashira/report/hotspot_table.rb +32 -37
  95. data/lib/hashira/report/json.rb +32 -32
  96. data/lib/hashira/report/metrics_table.rb +46 -33
  97. data/lib/hashira/report/phrases.rb +98 -0
  98. data/lib/hashira/report/smell_phrases.rb +68 -0
  99. data/lib/hashira/report/text.rb +59 -52
  100. data/lib/hashira/smells/census.rb +32 -0
  101. data/lib/hashira/smells/check.rb +30 -0
  102. data/lib/hashira/smells/conditions.rb +44 -0
  103. data/lib/hashira/smells/contexts.rb +64 -0
  104. data/lib/hashira/smells/control_parameter.rb +22 -0
  105. data/lib/hashira/smells/data_clump.rb +30 -0
  106. data/lib/hashira/smells/duplicate_method_call.rb +54 -0
  107. data/lib/hashira/smells/feature_envy.rb +17 -0
  108. data/lib/hashira/smells/instance_variable_assumption.rb +44 -0
  109. data/lib/hashira/smells/manual_dispatch.rb +16 -0
  110. data/lib/hashira/smells/module_initialize.rb +9 -0
  111. data/lib/hashira/smells/nil_check.rb +32 -0
  112. data/lib/hashira/smells/param_check.rb +51 -0
  113. data/lib/hashira/smells/refs.rb +53 -0
  114. data/lib/hashira/smells/repeated_conditional.rb +29 -0
  115. data/lib/hashira/smells/report.rb +31 -0
  116. data/lib/hashira/smells/scope.rb +29 -0
  117. data/lib/hashira/smells/too_many_instance_variables.rb +24 -0
  118. data/lib/hashira/smells/utility_function.rb +13 -0
  119. data/lib/hashira/smells/visibility.rb +64 -0
  120. data/lib/hashira/version.rb +1 -1
  121. data/lib/hashira.rb +131 -60
  122. metadata +62 -20
  123. data/lib/hashira/analysis/census.rb +0 -39
  124. data/lib/hashira/analysis/constant_registry.rb +0 -40
  125. data/lib/hashira/analysis/cycle_findings.rb +0 -33
  126. data/lib/hashira/analysis/cycle_search.rb +0 -44
  127. data/lib/hashira/analysis/definitions.rb +0 -27
  128. data/lib/hashira/analysis/edge.rb +0 -9
  129. data/lib/hashira/analysis/edge_map.rb +0 -36
  130. data/lib/hashira/analysis/graph.rb +0 -56
  131. data/lib/hashira/analysis/metric.rb +0 -14
  132. data/lib/hashira/analysis/references.rb +0 -39
  133. data/lib/hashira/analysis/rule.rb +0 -22
  134. data/lib/hashira/analysis/sdp_check.rb +0 -18
  135. data/lib/hashira/analysis/sdp_violation_findings.rb +0 -27
  136. data/lib/hashira/complexity/analyzer.rb +0 -45
  137. data/lib/hashira/diagram/renderer.rb +0 -20
  138. data/lib/hashira/duplication/analyzer.rb +0 -21
  139. data/lib/hashira/duplication/clusterer.rb +0 -50
  140. data/lib/hashira/duplication/extractor.rb +0 -31
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hashira
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Giacomo GK
@@ -26,22 +26,8 @@ files:
26
26
  - README.md
27
27
  - exe/hashira
28
28
  - lib/hashira.rb
29
- - lib/hashira/analysis/census.rb
30
- - lib/hashira/analysis/constant_registry.rb
31
- - lib/hashira/analysis/cycle_findings.rb
32
- - lib/hashira/analysis/cycle_search.rb
33
- - lib/hashira/analysis/definitions.rb
34
- - lib/hashira/analysis/edge.rb
35
- - lib/hashira/analysis/edge_map.rb
36
29
  - lib/hashira/analysis/finding.rb
37
- - lib/hashira/analysis/graph.rb
38
- - lib/hashira/analysis/metric.rb
39
- - lib/hashira/analysis/namespace_prefix.rb
40
30
  - lib/hashira/analysis/node_walk.rb
41
- - lib/hashira/analysis/references.rb
42
- - lib/hashira/analysis/rule.rb
43
- - lib/hashira/analysis/sdp_check.rb
44
- - lib/hashira/analysis/sdp_violation_findings.rb
45
31
  - lib/hashira/analysis/syntax.rb
46
32
  - lib/hashira/analysis/type_walk.rb
47
33
  - lib/hashira/churn.rb
@@ -55,13 +41,14 @@ files:
55
41
  - lib/hashira/ci/ratchet.rb
56
42
  - lib/hashira/ci/ratchet_report.rb
57
43
  - lib/hashira/cli.rb
44
+ - lib/hashira/cli/arguments.rb
58
45
  - lib/hashira/cli/command_line.rb
59
46
  - lib/hashira/cli/fail_on.rb
60
47
  - lib/hashira/cli/options.rb
48
+ - lib/hashira/cli/package_by.rb
61
49
  - lib/hashira/cli/run.rb
62
50
  - lib/hashira/cli/skip.rb
63
51
  - lib/hashira/cli/usage.rb
64
- - lib/hashira/complexity/analyzer.rb
65
52
  - lib/hashira/complexity/boolean_run.rb
66
53
  - lib/hashira/complexity/cognitive_score.rb
67
54
  - lib/hashira/complexity/if_chain.rb
@@ -69,17 +56,50 @@ files:
69
56
  - lib/hashira/complexity/method_score.rb
70
57
  - lib/hashira/complexity/rescue_scan.rb
71
58
  - lib/hashira/complexity/rollup.rb
59
+ - lib/hashira/complexity/scores.rb
60
+ - lib/hashira/coupling/audiences.rb
61
+ - lib/hashira/coupling/catalog.rb
62
+ - lib/hashira/coupling/census.rb
63
+ - lib/hashira/coupling/constant_registry.rb
64
+ - lib/hashira/coupling/cycle_findings.rb
65
+ - lib/hashira/coupling/cycle_search.rb
66
+ - lib/hashira/coupling/cycles.rb
67
+ - lib/hashira/coupling/definition.rb
68
+ - lib/hashira/coupling/definitions.rb
69
+ - lib/hashira/coupling/edge.rb
70
+ - lib/hashira/coupling/edge_map.rb
71
+ - lib/hashira/coupling/folder_placement.rb
72
+ - lib/hashira/coupling/folding.rb
73
+ - lib/hashira/coupling/graph.rb
74
+ - lib/hashira/coupling/metric.rb
75
+ - lib/hashira/coupling/mixed_audience_findings.rb
76
+ - lib/hashira/coupling/namespace_placement.rb
77
+ - lib/hashira/coupling/namespace_prefix.rb
78
+ - lib/hashira/coupling/naming.rb
79
+ - lib/hashira/coupling/no_folding.rb
80
+ - lib/hashira/coupling/placement.rb
81
+ - lib/hashira/coupling/references.rb
82
+ - lib/hashira/coupling/report.rb
83
+ - lib/hashira/coupling/roll_call.rb
84
+ - lib/hashira/coupling/roll_call_findings.rb
85
+ - lib/hashira/coupling/roster.rb
86
+ - lib/hashira/coupling/rule.rb
87
+ - lib/hashira/coupling/scope.rb
88
+ - lib/hashira/coupling/sdp_check.rb
89
+ - lib/hashira/coupling/sdp_violation_findings.rb
90
+ - lib/hashira/coupling/wide_edge_findings.rb
91
+ - lib/hashira/coupling/words.rb
72
92
  - lib/hashira/diagram/dot.rb
73
93
  - lib/hashira/diagram/mermaid.rb
74
- - lib/hashira/diagram/renderer.rb
75
- - lib/hashira/duplication/analyzer.rb
94
+ - lib/hashira/diagram/source.rb
95
+ - lib/hashira/duplication/clones.rb
76
96
  - lib/hashira/duplication/cluster.rb
77
- - lib/hashira/duplication/clusterer.rb
97
+ - lib/hashira/duplication/clusters.rb
78
98
  - lib/hashira/duplication/delta.rb
79
99
  - lib/hashira/duplication/duplication_finding.rb
80
- - lib/hashira/duplication/extractor.rb
81
100
  - lib/hashira/duplication/fragment.rb
82
101
  - lib/hashira/duplication/grouping.rb
102
+ - lib/hashira/duplication/harvest.rb
83
103
  - lib/hashira/duplication/index.rb
84
104
  - lib/hashira/duplication/maximal.rb
85
105
  - lib/hashira/duplication/near_miss.rb
@@ -99,8 +119,30 @@ files:
99
119
  - lib/hashira/report/hotspot_table.rb
100
120
  - lib/hashira/report/json.rb
101
121
  - lib/hashira/report/metrics_table.rb
122
+ - lib/hashira/report/phrases.rb
123
+ - lib/hashira/report/smell_phrases.rb
102
124
  - lib/hashira/report/text.rb
103
125
  - lib/hashira/report/view.rb
126
+ - lib/hashira/smells/census.rb
127
+ - lib/hashira/smells/check.rb
128
+ - lib/hashira/smells/conditions.rb
129
+ - lib/hashira/smells/contexts.rb
130
+ - lib/hashira/smells/control_parameter.rb
131
+ - lib/hashira/smells/data_clump.rb
132
+ - lib/hashira/smells/duplicate_method_call.rb
133
+ - lib/hashira/smells/feature_envy.rb
134
+ - lib/hashira/smells/instance_variable_assumption.rb
135
+ - lib/hashira/smells/manual_dispatch.rb
136
+ - lib/hashira/smells/module_initialize.rb
137
+ - lib/hashira/smells/nil_check.rb
138
+ - lib/hashira/smells/param_check.rb
139
+ - lib/hashira/smells/refs.rb
140
+ - lib/hashira/smells/repeated_conditional.rb
141
+ - lib/hashira/smells/report.rb
142
+ - lib/hashira/smells/scope.rb
143
+ - lib/hashira/smells/too_many_instance_variables.rb
144
+ - lib/hashira/smells/utility_function.rb
145
+ - lib/hashira/smells/visibility.rb
104
146
  - lib/hashira/version.rb
105
147
  homepage: https://github.com/giacope/hashira
106
148
  licenses:
@@ -1,39 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Hashira
4
- module Analysis
5
- class Census
6
- def initialize(project, trees)
7
- @definitions = Definitions.new(project, trees)
8
- @type_count = Hash.new(0)
9
- @registry = ConstantRegistry.new
10
- @namespace_prefix = NamespacePrefix.infer(@definitions)
11
- take
12
- end
13
-
14
- attr_reader :type_count, :namespace_prefix
15
-
16
- def declaring_package = @registry.declaring_package
17
-
18
- def packages = (@type_count.keys | @definitions.packages)
19
-
20
- def resolve(segments) = @registry.package_for(after_prefix(segments))
21
-
22
- private
23
-
24
- def take
25
- @definitions.each do |node, full, package|
26
- @registry.register(after_prefix(full), package)
27
- @type_count[package] += 1 unless Syntax.direct_definitions(node).empty?
28
- end
29
- end
30
-
31
- def after_prefix(segments)
32
- return [] if @namespace_prefix.first(segments.length) == segments
33
-
34
- depth = @namespace_prefix.length.downto(0).find { segments.first(it) == @namespace_prefix.last(it) }
35
- segments.drop(depth)
36
- end
37
- end
38
- end
39
- end
@@ -1,40 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Hashira
4
- module Analysis
5
- class ConstantRegistry
6
- AMBIGUOUS = Object.new.freeze
7
-
8
- def initialize
9
- @declaring_package = {}
10
- @shorthand = {}
11
- end
12
-
13
- attr_reader :declaring_package
14
-
15
- def register(path, package)
16
- return if path.empty?
17
-
18
- claim(@declaring_package, path, package)
19
- (1...path.length).each { claim(@shorthand, path.drop(it), package) }
20
- end
21
-
22
- def package_for(path)
23
- found = path.length.downto(1).filter_map { claimed(path.first(it)) }.first
24
- found unless found == AMBIGUOUS
25
- end
26
-
27
- private
28
-
29
- def claim(claims, path, package)
30
- key = path.join("::")
31
- claims[key] = claims.fetch(key, package) == package ? package : AMBIGUOUS
32
- end
33
-
34
- def claimed(path)
35
- key = path.join("::")
36
- @declaring_package[key] || @shorthand[key]
37
- end
38
- end
39
- end
40
- end
@@ -1,33 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Hashira
4
- module Analysis
5
- class CycleFindings < Rule
6
- KIND = "cycle"
7
-
8
- def list
9
- graph.packages.select { graph.cyclic?(it) }.sort.map { cycle_finding(it) }
10
- end
11
-
12
- private
13
-
14
- def cycle_finding(package)
15
- path = graph.cycle_path(package)
16
- finding(package:, cycle: path, evidence: evidence(path),
17
- message: message(package, path, graph.weakest_edge(path)))
18
- end
19
-
20
- def message(package, path, weak_edge)
21
- weak_from, weak_to = weak_edge
22
- weight = graph.weight(weak_from, weak_to)
23
- "#{package} can reach itself: #{path.join(" -> ")} — any change may ripple back " \
24
- "around. The lightest edge on this cycle is #{weak_from} -> #{weak_to} " \
25
- "(#{weight} ref#{"s" unless weight == 1})."
26
- end
27
-
28
- def evidence(path)
29
- path.each_cons(2).flat_map { |from, to| graph.evidence_for(from, to).to_a.first(2) }
30
- end
31
- end
32
- end
33
- end
@@ -1,44 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Hashira
4
- module Analysis
5
- class CycleSearch
6
- def initialize(dependencies, package)
7
- @dependencies = dependencies
8
- @package = package
9
- @predecessor = {}
10
- @queue = dependencies[package].to_a.each { @predecessor[it] = package }
11
- end
12
-
13
- def path
14
- trace_back([@package]) if cycle?
15
- end
16
-
17
- private
18
-
19
- def cycle?
20
- while (node = @queue.shift)
21
- return true if node == @package
22
-
23
- visit(node)
24
- end
25
- end
26
-
27
- def visit(node)
28
- @dependencies[node].each do |neighbor|
29
- next if @predecessor.key?(neighbor)
30
-
31
- @predecessor[neighbor] = node
32
- @queue << neighbor
33
- end
34
- end
35
-
36
- def trace_back(path)
37
- first = path.first
38
- return path if first == @package && path.size > 1
39
-
40
- trace_back(path.unshift(@predecessor[first]))
41
- end
42
- end
43
- end
44
- end
@@ -1,27 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Hashira
4
- module Analysis
5
- class Definitions
6
- include Enumerable
7
-
8
- def initialize(project, trees)
9
- @project = project
10
- @trees = trees
11
- end
12
-
13
- def each(&)
14
- @trees.each { |file, tree| definitions_in(file, tree, &) }
15
- end
16
-
17
- def packages = @trees.keys.map { @project.package_for(it) }.uniq
18
-
19
- private
20
-
21
- def definitions_in(file, tree)
22
- package = @project.package_for(file)
23
- TypeWalk.each_definition(tree) { |node, full| yield node, full, package }
24
- end
25
- end
26
- end
27
- end
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Hashira
4
- module Analysis
5
- Edge = Data.define(:from, :to) do
6
- def to_s = "#{from} -> #{to}"
7
- end
8
- end
9
- end
@@ -1,36 +0,0 @@
1
- # frozen_string_literal: true
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
12
-
13
- attr_reader :dependencies, :evidence
14
-
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
22
-
23
- private
24
-
25
- def empty_sets = Hash.new { |hash, key| hash[key] = Set.new }
26
-
27
- def record_reference(from, source, segments, line)
28
- to = @census.resolve(segments)
29
- return unless to && to != from
30
-
31
- @dependencies[from] << to
32
- @evidence[[from, to]] << "#{source}:#{line}: #{segments.join("::")}"
33
- end
34
- end
35
- end
36
- end
@@ -1,56 +0,0 @@
1
- # frozen_string_literal: true
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
11
-
12
- def packages = @census.packages
13
-
14
- def dependencies_of(package) = dependencies[package].to_a.sort
15
-
16
- def dependents_of(package) = packages.select { dependencies[it].include?(package) }.sort
17
-
18
- def edge_list
19
- dependencies.sort.flat_map { |from, tos| tos.sort.map { Edge.new(from:, to: it) } }
20
- end
21
-
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
28
-
29
- def evidence_for(from, to) = @edge_map.evidence[[from, to]]
30
-
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
36
-
37
- def metrics = packages.to_h { [it, metric_for(it)] }
38
-
39
- def sdp_violations = SdpCheck.new(dependencies, metrics).violations
40
-
41
- def cyclic?(package) = !!cycle_path(package)
42
-
43
- def weight(from, to) = evidence_for(from, to).size
44
-
45
- def cycle_path(package) = CycleSearch.new(dependencies, package).path
46
-
47
- def weakest_edge(path)
48
- path.each_cons(2).min_by { |from, to| weight(from, to) }
49
- end
50
-
51
- private
52
-
53
- def dependencies = @edge_map.dependencies
54
- end
55
- end
56
- end
@@ -1,14 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Hashira
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
10
-
11
- def to_h = { tc: type_count, ca: afferent, ce: efferent, i: instability }
12
- end
13
- end
14
- end
@@ -1,39 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "prism"
4
-
5
- module Hashira
6
- module Analysis
7
- module References
8
- module_function
9
-
10
- def list(tree)
11
- each_sighting(tree).map(&:first)
12
- end
13
-
14
- def each_sighting(tree)
15
- [].tap { collect(tree, it) }
16
- end
17
-
18
- def collect(node, accumulator)
19
- return unless node
20
- return accumulator << [Syntax.path_segments(node), node.location.start_line] if constant?(node)
21
-
22
- branches(node).each { collect(it, accumulator) }
23
- end
24
-
25
- def constant?(node) = node.is_a?(Prism::ConstantPathNode) || node.is_a?(Prism::ConstantReadNode)
26
-
27
- def branches(node)
28
- definition?(node) ? definition_branches(node) : node.compact_child_nodes
29
- end
30
-
31
- def definition?(node) = node.is_a?(Prism::ClassNode) || node.is_a?(Prism::ModuleNode)
32
-
33
- def definition_branches(node)
34
- superclass = node.is_a?(Prism::ClassNode) ? node.superclass : nil
35
- [superclass, node.body]
36
- end
37
- end
38
- end
39
- end
@@ -1,22 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Hashira
4
- module Analysis
5
- class Rule
6
- def initialize(project, graph)
7
- @project = project
8
- @graph = graph
9
- end
10
-
11
- private
12
-
13
- attr_reader :project, :graph
14
-
15
- def metrics = @metrics ||= graph.metrics
16
-
17
- def finding(**attributes)
18
- Finding.new(kind: self.class::KIND, cycle: nil, **attributes)
19
- end
20
- end
21
- end
22
- end
@@ -1,18 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Hashira
4
- module Analysis
5
- class SdpCheck
6
- def initialize(dependencies, metrics)
7
- @dependencies = dependencies
8
- @metrics = metrics
9
- end
10
-
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
16
- end
17
- end
18
- end
@@ -1,27 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Hashira
4
- module Analysis
5
- class SdpViolationFindings < Rule
6
- KIND = "sdp_violation"
7
-
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
12
-
13
- private
14
-
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
21
-
22
- def instability(package) = metrics[package].instability
23
-
24
- def label(package) = format("%.2f", instability(package))
25
- end
26
- end
27
- end
@@ -1,45 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Hashira
4
- module Complexity
5
- class Analyzer
6
- THRESHOLD = 10
7
-
8
- def initialize(project, trees)
9
- @project = project
10
- @scores = trees.flat_map { |path, tree| scores_for(path, tree) }
11
- end
12
-
13
- def methods = @scores.sort_by { -it.cognitive }
14
-
15
- def classes = Rollup.new(@scores).classes.sort_by { -it.cognitive }
16
-
17
- def findings = flagged.map { MethodFinding.new(it).to_finding }
18
-
19
- private
20
-
21
- def flagged = methods.select { it.cognitive >= THRESHOLD }
22
-
23
- def scores_for(path, tree)
24
- rel = @project.relative(path)
25
- methods_in(tree).map { |full, node| build_score(rel, full, node) }
26
- end
27
-
28
- def methods_in(tree)
29
- found = []
30
- Analysis::TypeWalk.each_definition(tree) do |type_node, full|
31
- Analysis::Syntax.direct_definitions(type_node).each { found << [full, it] }
32
- end
33
- found
34
- end
35
-
36
- def build_score(rel, full, node)
37
- score = CognitiveScore.new(node)
38
- MethodScore.new(subject: subject(full, node), file: rel, line: node.location.start_line,
39
- cognitive: score.total, calls: score.calls, increments: score.increments)
40
- end
41
-
42
- def subject(full, node) = "#{full.join("::")}#{node.receiver ? "." : "#"}#{node.name}"
43
- end
44
- end
45
- end
@@ -1,20 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Hashira
4
- module Diagram
5
- class Renderer
6
- def initialize(graph, format, io: $stdout)
7
- @graph = graph
8
- @format = format
9
- @io = io
10
- end
11
-
12
- def display
13
- edges = @graph.weighted_edges
14
- renderer = @format == :dot ? Dot.new(edges) : Mermaid.new(edges)
15
- @io.puts renderer.source
16
- 0
17
- end
18
- end
19
- end
20
- end
@@ -1,21 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Hashira
4
- module Duplication
5
- class Analyzer
6
- def initialize(project, trees, churn)
7
- @project = project
8
- @trees = trees
9
- @churn = churn
10
- end
11
-
12
- def clusters = @clusters ||= Clusterer.new(fragments).clusters.sort_by { -it.mass }
13
-
14
- def findings = clusters.map { |cluster| DuplicationFinding.new(cluster, @churn).to_finding }
15
-
16
- private
17
-
18
- def fragments = Extractor.new(@project, @trees).fragments
19
- end
20
- end
21
- end
@@ -1,50 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Hashira
4
- module Duplication
5
- class Clusterer
6
- PREFILTER = 12
7
- BASE_MASS = 16
8
- NEAR_MASS = 40
9
- PAIR = 2
10
- PENALTY_PER_RECURRENCE = 2
11
-
12
- def initialize(fragments)
13
- @fragments = fragments.select { |fragment| fragment.mass >= PREFILTER }
14
- @sets = UnionFind.new
15
- end
16
-
17
- def clusters
18
- @fragments.group_by(&:types).each_value { |group| chain(group) }
19
- NearMiss.new(@fragments).pairs.each { |left, right| @sets.union(left, right) }
20
- Maximal.new(sized).reduced
21
- end
22
-
23
- private
24
-
25
- def chain(group) = group.each_cons(2) { |left, right| @sets.union(left, right) }
26
-
27
- def sized = built.filter_map { admitted(it) }
28
-
29
- def admitted(cluster) = [cluster, exact_core(cluster)].compact.find { fits?(it) }
30
-
31
- def fits?(cluster) = cluster.mass >= floor(cluster)
32
-
33
- def exact_core(cluster) = Grouping.new(cluster.exact_sites).cluster
34
-
35
- def built = @sets.clusters.filter_map { |group| Grouping.new(group).cluster }
36
-
37
- def floor(cluster) = base(cluster) + idiom_penalty(cluster)
38
-
39
- def base(cluster) = thin_evidence?(cluster) ? NEAR_MASS : BASE_MASS
40
-
41
- def thin_evidence?(cluster) = !one_shape?(cluster) || cluster.shape_only?
42
-
43
- def idiom_penalty(cluster) = recurrences(cluster) * PENALTY_PER_RECURRENCE
44
-
45
- def recurrences(cluster) = [cluster.size - PAIR, 0].max
46
-
47
- def one_shape?(cluster) = cluster.sites.map(&:types).uniq.size == 1
48
- end
49
- end
50
- end