hashira 0.7.0 → 0.9.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 (103) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +94 -0
  3. data/README.md +57 -3
  4. data/exe/hashira +1 -1
  5. data/lib/hashira/analysis/finding.rb +11 -1
  6. data/lib/hashira/analysis/syntax.rb +6 -6
  7. data/lib/hashira/analysis/type_walk.rb +4 -0
  8. data/lib/hashira/churn.rb +1 -10
  9. data/lib/hashira/ci/accepted.rb +9 -7
  10. data/lib/hashira/ci/baseline.rb +53 -33
  11. data/lib/hashira/ci/comparison.rb +33 -0
  12. data/lib/hashira/ci/diff.rb +0 -15
  13. data/lib/hashira/ci/gate.rb +3 -3
  14. data/lib/hashira/ci/mark.rb +7 -0
  15. data/lib/hashira/ci/ratchet.rb +12 -4
  16. data/lib/hashira/ci/ratchet_report.rb +4 -4
  17. data/lib/hashira/ci/scope.rb +0 -1
  18. data/lib/hashira/ci/slice.rb +21 -0
  19. data/lib/hashira/ci/sweep.rb +13 -0
  20. data/lib/hashira/cli/arguments.rb +12 -8
  21. data/lib/hashira/cli/choice.rb +9 -0
  22. data/lib/hashira/cli/command_line.rb +8 -5
  23. data/lib/hashira/cli/fail_on.rb +6 -6
  24. data/lib/hashira/cli/flag.rb +3 -1
  25. data/lib/hashira/cli/flags.rb +10 -0
  26. data/lib/hashira/cli/format.rb +1 -1
  27. data/lib/hashira/cli/needs.rb +13 -2
  28. data/lib/hashira/cli/only.rb +16 -0
  29. data/lib/hashira/cli/options.rb +8 -10
  30. data/lib/hashira/cli/package_by.rb +1 -1
  31. data/lib/hashira/cli/run.rb +9 -8
  32. data/lib/hashira/cli.rb +22 -21
  33. data/lib/hashira/complexity/cognitive_score.rb +44 -33
  34. data/lib/hashira/complexity/method_finding.rb +7 -5
  35. data/lib/hashira/complexity/method_score.rb +1 -1
  36. data/lib/hashira/complexity/scores.rb +11 -9
  37. data/lib/hashira/coupling/audiences.rb +3 -3
  38. data/lib/hashira/coupling/catalog.rb +8 -6
  39. data/lib/hashira/coupling/census.rb +42 -22
  40. data/lib/hashira/coupling/cycle_search.rb +11 -7
  41. data/lib/hashira/coupling/definitions.rb +1 -7
  42. data/lib/hashira/coupling/edge_map.rb +9 -8
  43. data/lib/hashira/coupling/folding.rb +5 -5
  44. data/lib/hashira/coupling/graph.rb +16 -9
  45. data/lib/hashira/coupling/metric.rb +3 -1
  46. data/lib/hashira/coupling/naming.rb +4 -4
  47. data/lib/hashira/coupling/references.rb +12 -12
  48. data/lib/hashira/coupling/report.rb +7 -9
  49. data/lib/hashira/coupling/roll_call.rb +1 -1
  50. data/lib/hashira/coupling/roll_call_findings.rb +1 -1
  51. data/lib/hashira/coupling/roster.rb +37 -12
  52. data/lib/hashira/coupling/rule.rb +1 -1
  53. data/lib/hashira/coupling/sdp_violation_findings.rb +3 -1
  54. data/lib/hashira/duplication/clones.rb +1 -1
  55. data/lib/hashira/duplication/clusters.rb +10 -7
  56. data/lib/hashira/duplication/delta.rb +1 -4
  57. data/lib/hashira/duplication/duplication_finding.rb +4 -2
  58. data/lib/hashira/duplication/fragment.rb +20 -2
  59. data/lib/hashira/duplication/harvest.rb +7 -5
  60. data/lib/hashira/duplication/index.rb +3 -2
  61. data/lib/hashira/duplication/literal.rb +25 -0
  62. data/lib/hashira/duplication/similarity.rb +2 -2
  63. data/lib/hashira/duplication/variance.rb +7 -1
  64. data/lib/hashira/error.rb +0 -3
  65. data/lib/hashira/focus.rb +29 -0
  66. data/lib/hashira/git_log.rb +19 -0
  67. data/lib/hashira/hotspots/file_cost.rb +1 -1
  68. data/lib/hashira/hotspots/rollup.rb +2 -2
  69. data/lib/hashira/pipeline.rb +22 -14
  70. data/lib/hashira/project.rb +35 -34
  71. data/lib/hashira/report/columns.rb +14 -8
  72. data/lib/hashira/report/graph_payload.rb +1 -1
  73. data/lib/hashira/report/hotspot_table.rb +1 -1
  74. data/lib/hashira/report/json.rb +2 -1
  75. data/lib/hashira/report/metrics_table.rb +3 -3
  76. data/lib/hashira/report/phrases.rb +23 -18
  77. data/lib/hashira/report/smell_phrases.rb +2 -2
  78. data/lib/hashira/smells/branches.rb +62 -0
  79. data/lib/hashira/smells/census.rb +11 -6
  80. data/lib/hashira/smells/check.rb +1 -5
  81. data/lib/hashira/smells/contexts.rb +7 -5
  82. data/lib/hashira/smells/control_parameter.rb +1 -1
  83. data/lib/hashira/smells/data_clump.rb +2 -4
  84. data/lib/hashira/smells/duplicate_method_call.rb +25 -5
  85. data/lib/hashira/smells/feature_envy.rb +3 -3
  86. data/lib/hashira/smells/foreign.rb +14 -14
  87. data/lib/hashira/smells/instance_variable_assumption.rb +5 -15
  88. data/lib/hashira/smells/kind.rb +9 -0
  89. data/lib/hashira/smells/lineage.rb +92 -0
  90. data/lib/hashira/smells/manual_dispatch.rb +1 -1
  91. data/lib/hashira/smells/module_initialize.rb +0 -2
  92. data/lib/hashira/smells/nil_check.rb +1 -1
  93. data/lib/hashira/smells/ownership.rb +19 -7
  94. data/lib/hashira/smells/param_check.rb +2 -2
  95. data/lib/hashira/smells/refs.rb +13 -7
  96. data/lib/hashira/smells/repeated_conditional.rb +1 -3
  97. data/lib/hashira/smells/report.rb +16 -7
  98. data/lib/hashira/smells/too_many_instance_variables.rb +3 -3
  99. data/lib/hashira/smells/visibility.rb +18 -11
  100. data/lib/hashira/trees.rb +8 -4
  101. data/lib/hashira/version.rb +1 -1
  102. data/lib/hashira.rb +12 -0
  103. metadata +13 -1
@@ -6,18 +6,23 @@ class Hashira::Smells::Census
6
6
  def initialize(project, trees)
7
7
  @project = project
8
8
  @trees = trees
9
- @ownership = Hashira::Smells::Ownership.new(trees.values)
10
9
  end
11
10
 
12
- attr_reader :ownership
11
+ def ownership = @_ownership ||= Hashira::Smells::Ownership.new(@trees.values)
13
12
 
14
- def types = @trees.flat_map { |path, tree| harvest(@project.relative(path), tree) }
13
+ def types = @_types ||= placed(@trees.flat_map { |path, tree| harvest(@project.relative(path), tree) })
15
14
 
16
15
  private
17
16
 
17
+ def placed(found) = settled(found, Hashira::Smells::Lineage.new(found))
18
+
19
+ def settled(found, lineage) = found.map { it.with(assigned: lineage.assigned(it)) }
20
+
21
+ def roots = @_roots ||= Hashira::Analysis::TypeWalk.roots(@trees)
22
+
18
23
  def harvest(file, tree)
19
24
  found = []
20
- Hashira::Analysis::TypeWalk.each(tree) { |node, full| found << context(file, node, full.join("::")) }
25
+ Hashira::Analysis::TypeWalk.each(tree, roots:) { |node, full| found << context(file, node, full.join("::")) }
21
26
  found
22
27
  end
23
28
 
@@ -28,8 +33,8 @@ class Hashira::Smells::Census
28
33
  def kind(node) = node.is_a?(Prism::ModuleNode) ? :module : :class
29
34
 
30
35
  def defs(name, node, file)
31
- Hashira::Smells::Visibility.new(node).entries.map do |def_node, section|
32
- Hashira::Smells::MethodContext.new(owner: name, node: def_node, file:, section:, ownership: @ownership)
36
+ Hashira::Smells::Visibility.new(node).entries.map do |definition, section|
37
+ Hashira::Smells::MethodContext.new(owner: name, node: definition, file:, section:, ownership:)
33
38
  end
34
39
  end
35
40
  end
@@ -1,10 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Hashira::Smells::Check
4
- def self.kind = name.split("::").last.gsub(/(?<=[a-z])(?=[A-Z])/, "_").downcase
5
-
6
- def self.judge? = false
7
-
8
4
  def initialize(subject)
9
5
  @subject = subject
10
6
  end
@@ -18,7 +14,7 @@ class Hashira::Smells::Check
18
14
 
19
15
  attr_reader :subject
20
16
 
21
- def kind = self.class.kind
17
+ def kind = Hashira::Smells::Kind.new(self.class).to_s
22
18
 
23
19
  def label = subject.subject
24
20
 
@@ -9,12 +9,12 @@ module Hashira
9
9
 
10
10
  module_function
11
11
 
12
- def names(def_node) = parts(def_node).flat_map { expand(it) }
12
+ def names(definition) = parts(definition).flat_map { expand(it) }
13
13
 
14
- def arguments(def_node) = parts(def_node).grep_v(Prism::BlockParameterNode).flat_map { expand(it) }
14
+ def arguments(definition) = parts(definition).grep_v(Prism::BlockParameterNode).flat_map { expand(it) }
15
15
 
16
- def parts(def_node)
17
- node = def_node.parameters
16
+ def parts(definition)
17
+ node = definition.parameters
18
18
  return [] unless node
19
19
  node.requireds + node.optionals + node.posts + node.keywords +
20
20
  [node.rest, node.keyword_rest, node.block].compact
@@ -51,7 +51,9 @@ module Hashira
51
51
  end
52
52
 
53
53
  TypeContext =
54
- Data.define(:name, :node, :kind, :file, :defs) do
54
+ Data.define(:name, :node, :kind, :file, :defs, :assigned) do
55
+ def initialize(assigned: nil, **rest) = super
56
+
55
57
  def line = node.location.start_line
56
58
 
57
59
  def subject = name
@@ -5,7 +5,7 @@ class Hashira::Smells::ControlParameter < Hashira::Smells::Check
5
5
 
6
6
  def smelly? = culprits.any?
7
7
 
8
- def culprits = @culprits ||= subject.parameters.filter_map { |name| culprit(name) }
8
+ def culprits = @_culprits ||= subject.parameters.filter_map { |name| culprit(name) }
9
9
 
10
10
  def culprit(name)
11
11
  lines = spots(name).uniq
@@ -5,15 +5,13 @@ class Hashira::Smells::DataClump < Hashira::Smells::Check
5
5
 
6
6
  MIN_SIZE = 2
7
7
 
8
- def self.judge? = true
9
-
10
8
  private
11
9
 
12
10
  def smelly? = clumps.any?
13
11
 
14
- def candidates = @candidates ||= subject.owned.map { [it, it.arguments.sort] }
12
+ def candidates = @_candidates ||= subject.owned.map { [it, it.arguments.sort] }
15
13
 
16
- def clumps = @clumps ||= shared.map { |clump| [clump, holders(clump)] }
14
+ def clumps = @_clumps ||= shared.map { |clump| [clump, holders(clump)] }
17
15
 
18
16
  def shared = candidates.combination(MAX_COPIES + 1).filter_map { |group| clump(group) }.uniq
19
17
 
@@ -5,27 +5,47 @@ require "prism"
5
5
  class Hashira::Smells::DuplicateMethodCall < Hashira::Smells::Check
6
6
  LIMIT = 1
7
7
 
8
+ MINTS = %i[new dup clone allocate rand srand].freeze
9
+
10
+ SOURCES = %w[SecureRandom Random].freeze
11
+
12
+ LITERALS = [
13
+ Prism::StringNode, Prism::SymbolNode, Prism::ArrayNode, Prism::HashNode,
14
+ Prism::IntegerNode, Prism::FloatNode, Prism::RegularExpressionNode
15
+ ].freeze
16
+
8
17
  private
9
18
 
10
19
  def smelly? = repeats.any?
11
20
 
12
- def calls = @calls ||= Hashira::Smells::Scope.inside(subject.node).grep(Prism::CallNode)
21
+ def calls
22
+ @_calls ||= Hashira::Smells::Scope.inside(subject.node).grep(Prism::CallNode).reject { minted?(it) }
23
+ end
24
+
25
+ def minted?(node) = mints?(node.name) || spawns?(node.receiver)
26
+
27
+ def mints?(name) = MINTS.include?(name)
28
+
29
+ def spawns?(receiver) = LITERALS.include?(receiver.class) || SOURCES.include?(receiver&.slice)
13
30
 
14
31
  def plain?(node)
15
32
  !node.receiver && !node.arguments && !node.block.is_a?(Prism::BlockArgumentNode)
16
33
  end
17
34
 
18
35
  def repeats
19
- @repeats ||= usual.reject { |_handle, nodes| whole.value?(nodes) }.merge(whole)
36
+ @_repeats ||= together(usual.reject { |_handle, nodes| whole.value?(nodes) }.merge(whole))
20
37
  end
21
38
 
39
+ def together(groups) = groups.select { |_handle, nodes| branches.together?(nodes) }
40
+
41
+ def branches = @_branches ||= Hashira::Smells::Branches.new(subject.node)
42
+
22
43
  def usual
23
- calls.reject { plain?(it) || it.name == :new }
24
- .group_by { handle(it) }.select { |_handle, group| group.size > LIMIT }
44
+ calls.reject { plain?(it) }.group_by { handle(it) }.select { |_handle, group| group.size > LIMIT }
25
45
  end
26
46
 
27
47
  def whole
28
- @whole ||=
48
+ @_whole ||=
29
49
  calls.select { it.block.is_a?(Prism::BlockNode) }
30
50
  .group_by { it.slice.gsub(/\s+/, " ") }.select { |_handle, group| group.size > LIMIT }
31
51
  end
@@ -7,11 +7,11 @@ class Hashira::Smells::FeatureEnvy < Hashira::Smells::Check
7
7
  !subject.singleton? && !subject.mixin? && refs.ego.positive? && envied.any?
8
8
  end
9
9
 
10
- def refs = @refs ||= Hashira::Smells::Refs.new(subject.node)
10
+ def refs = @_refs ||= Hashira::Smells::Refs.new(subject.node)
11
11
 
12
- def envied = @envied ||= refs.envious.reject { foreign.dismiss?(it) }
12
+ def envied = @_envied ||= refs.envious.reject { foreign.dismiss?(it) }
13
13
 
14
- def foreign = @foreign ||= Hashira::Smells::Foreign.new(subject, subject.ownership)
14
+ def foreign = @_foreign ||= Hashira::Smells::Foreign.new(subject, subject.ownership)
15
15
 
16
16
  def detail = { site:, names: envied }
17
17
 
@@ -18,9 +18,7 @@ class Hashira::Smells::Foreign
18
18
  ].freeze
19
19
 
20
20
  def initialize(subject, ownership)
21
- node = subject.node
22
- @body = Hashira::Smells::Scope.inside(node)
23
- @tail = tail(node)
21
+ @subject = subject
24
22
  @ownership = ownership
25
23
  end
26
24
 
@@ -35,12 +33,16 @@ class Hashira::Smells::Foreign
35
33
 
36
34
  private
37
35
 
36
+ def body = @_body ||= Hashira::Smells::Scope.inside(@subject.node)
37
+
38
+ def tail = @_tail ||= Hashira::Analysis::Syntax.statements(@subject.node).compact.last
39
+
38
40
  def convert?
39
- @tail.is_a?(Prism::CallNode) && @tail.name == :new &&
40
- Hashira::Analysis::Syntax.segments(@tail.receiver).any? && stateless?
41
+ tail.is_a?(Prism::CallNode) && tail.name == :new &&
42
+ Hashira::Analysis::Syntax.segments(tail.receiver).any? && stateless?
41
43
  end
42
44
 
43
- def stateless? = @body.none? { STATE.include?(it.class) }
45
+ def stateless? = body.none? { STATE.include?(it.class) }
44
46
 
45
47
  def fenced?(name)
46
48
  tested = tests { it == name }
@@ -48,7 +50,7 @@ class Hashira::Smells::Foreign
48
50
  end
49
51
 
50
52
  def wire?(name)
51
- calls = @body.grep(Prism::CallNode).select { |call| local?(call.receiver) { it == name } }
53
+ calls = body.grep(Prism::CallNode).select { |call| local?(call.receiver) { it == name } }
52
54
  calls.any? && calls.all? { keyed?(it) } && reassignments(name).none?
53
55
  end
54
56
 
@@ -75,11 +77,11 @@ class Hashira::Smells::Foreign
75
77
  def reassignments(name) = among(Prism::LocalVariableOperatorWriteNode, name)
76
78
 
77
79
  def among(kind, name)
78
- @body.grep(kind).select { it.name == name }
80
+ body.grep(kind).select { it.name == name }
79
81
  end
80
82
 
81
83
  def snares(name)
82
- @body.grep(Prism::RescueNode).select { it.reference&.name == name }
84
+ body.grep(Prism::RescueNode).select { it.reference&.name == name }
83
85
  end
84
86
 
85
87
  def alien?(exceptions)
@@ -107,15 +109,15 @@ class Hashira::Smells::Foreign
107
109
  end
108
110
 
109
111
  def probes(&)
110
- @body.grep(Prism::CallNode).select { TYPE_TESTS.include?(it.name) && local?(it.receiver, &) }.filter_map { key(it) }
112
+ body.grep(Prism::CallNode).select { TYPE_TESTS.include?(it.name) && local?(it.receiver, &) }.filter_map { key(it) }
111
113
  end
112
114
 
113
115
  def arms(&)
114
- @body.grep(Prism::CaseNode).select { local?(it.predicate, &) }.flat_map(&:conditions).flat_map(&:conditions)
116
+ body.grep(Prism::CaseNode).select { local?(it.predicate, &) }.flat_map(&:conditions).flat_map(&:conditions)
115
117
  end
116
118
 
117
119
  def lookups(&)
118
- @body.grep(Prism::CallNode).select { it.name == :[] && sorts?(key(it), &) }.flat_map { @ownership.keys(Hashira::Analysis::Syntax.segments(it.receiver)) }
120
+ body.grep(Prism::CallNode).select { it.name == :[] && sorts?(key(it), &) }.flat_map { @ownership.keys(Hashira::Analysis::Syntax.segments(it.receiver)) }
119
121
  end
120
122
 
121
123
  def sorts?(argument, &)
@@ -126,6 +128,4 @@ class Hashira::Smells::Foreign
126
128
  def local?(node, &) = node.is_a?(Prism::LocalVariableReadNode) && yield(node.name)
127
129
 
128
130
  def key(call) = call.arguments&.arguments&.first
129
-
130
- def tail(def_node) = Hashira::Analysis::Syntax.statements(def_node).compact.last
131
131
  end
@@ -3,12 +3,6 @@
3
3
  require "prism"
4
4
 
5
5
  class Hashira::Smells::InstanceVariableAssumption < Hashira::Smells::Check
6
- SETTERS = [
7
- Prism::InstanceVariableWriteNode, Prism::InstanceVariableOrWriteNode,
8
- Prism::InstanceVariableAndWriteNode, Prism::InstanceVariableOperatorWriteNode,
9
- Prism::InstanceVariableTargetNode
10
- ].freeze
11
-
12
6
  MAYBES = [
13
7
  Prism::LocalVariableOrWriteNode, Prism::InstanceVariableOrWriteNode,
14
8
  Prism::ClassVariableOrWriteNode, Prism::GlobalVariableOrWriteNode,
@@ -26,21 +20,17 @@ class Hashira::Smells::InstanceVariableAssumption < Hashira::Smells::Check
26
20
  end
27
21
  end
28
22
 
29
- def self.judge? = true
30
-
31
23
  private
32
24
 
33
- def smelly? = subject.kind == :class && assumed.any?
25
+ def smelly? = subject.kind == :class && assigned && assumed.any?
34
26
 
35
- def assumed = @assumed ||= (read - prepared).uniq.sort
27
+ def assigned = subject.assigned
36
28
 
37
- def read = subject.owned.flat_map { Harvest.reads(it.node) }
29
+ def assumed = @_assumed ||= (read - assigned).uniq.sort.reject { cache?(it) }
38
30
 
39
- def prepared
40
- starters.flat_map { Hashira::Smells::Scope.inside(it.node) }.select { SETTERS.include?(it.class) }.map(&:name)
41
- end
31
+ def cache?(name) = name.start_with?("@_")
42
32
 
43
- def starters = subject.owned.select { it.node.name == :initialize }
33
+ def read = subject.owned.flat_map { Harvest.reads(it.node) }
44
34
 
45
35
  def evidence = assumed.map(&:to_s)
46
36
  end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Hashira::Smells::Kind
4
+ def initialize(check)
5
+ @check = check
6
+ end
7
+
8
+ def to_s = @check.name.split("::").last.gsub(/(?<=[a-z])(?=[A-Z])/, "_").downcase
9
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ class Hashira::Smells::Lineage
6
+ MIXINS = %i[include prepend].freeze
7
+
8
+ EXTENSIONS = %i[extend].freeze
9
+
10
+ WRITERS = %i[attr_writer attr_accessor].freeze
11
+
12
+ SETTERS = [
13
+ Prism::InstanceVariableWriteNode, Prism::InstanceVariableOrWriteNode,
14
+ Prism::InstanceVariableAndWriteNode, Prism::InstanceVariableOperatorWriteNode,
15
+ Prism::InstanceVariableTargetNode
16
+ ].freeze
17
+
18
+ def initialize(types)
19
+ @types = types
20
+ end
21
+
22
+ def assigned(context) = kin(context)&.flat_map { writes(it) }&.uniq
23
+
24
+ private
25
+
26
+ def index = @_index ||= @types.group_by(&:name)
27
+
28
+ def kin(context) = walk([context.name], [])&.flat_map { index.fetch(it) }
29
+
30
+ def walk(queue, known)
31
+ return known if queue.empty?
32
+ name, *rest = queue
33
+ return walk(rest, known) if known.include?(name)
34
+ onward(rest, known + [name], parents(index.fetch(name)))
35
+ end
36
+
37
+ def onward(queue, known, found)
38
+ walk(queue + found, known) if found
39
+ end
40
+
41
+ def parents(kin)
42
+ found = kin.flat_map { pointed(it) }
43
+ found unless found.include?(nil) || kin.any? { opaque?(it) }
44
+ end
45
+
46
+ def opaque?(type) = extensions(type).any? { !resolve(type.name, Hashira::Analysis::Syntax.segments(it)) }
47
+
48
+ def extensions(type) = named(type, EXTENSIONS)
49
+
50
+ def pointed(type) = references(type).map { resolve(type.name, it) }
51
+
52
+ def references(type)
53
+ (named(type, MIXINS) + [parent(type)].compact).map { Hashira::Analysis::Syntax.segments(it) }
54
+ end
55
+
56
+ def parent(type) = (type.node.superclass if type.kind == :class)
57
+
58
+ def resolve(owner, segments) = candidates(owner, segments).find { index.key?(it) }
59
+
60
+ def candidates(owner, segments)
61
+ segments.empty? ? [] : scopes(owner).map { (it + segments).join("::") }
62
+ end
63
+
64
+ def scopes(owner)
65
+ parts = owner.split("::")
66
+ parts.size.downto(0).map { parts.first(it) }
67
+ end
68
+
69
+ def writes(type)
70
+ definitions(type).flat_map { setters(it) } + attributes(type)
71
+ end
72
+
73
+ def definitions(type) = sweep(type).grep(Prism::DefNode).reject(&:receiver)
74
+
75
+ def setters(node) = Hashira::Smells::Scope.sweep(node).select { SETTERS.include?(it.class) }.map(&:name)
76
+
77
+ def attributes(type)
78
+ named(type, WRITERS).select { it.is_a?(Prism::SymbolNode) || it.is_a?(Prism::StringNode) }.map { :"@#{it.unescaped}" }
79
+ end
80
+
81
+ def named(type, names) = passed(calls(type).select { names.include?(it.name) })
82
+
83
+ def passed(calls) = calls.flat_map { it.arguments.arguments }
84
+
85
+ def calls(type) = sweep(type).grep(Prism::CallNode).reject(&:receiver).select(&:arguments)
86
+
87
+ def sweep(type) = swept.fetch(type.node) { store(it) }
88
+
89
+ def store(node) = swept[node] = Hashira::Smells::Scope.sweep(node)
90
+
91
+ def swept = @_swept ||= {}.compare_by_identity
92
+ end
@@ -8,7 +8,7 @@ class Hashira::Smells::ManualDispatch < Hashira::Smells::Check
8
8
  def smelly? = sightings.any?
9
9
 
10
10
  def sightings
11
- @sightings ||= Hashira::Smells::Scope.inside(subject.node)
11
+ @_sightings ||= Hashira::Smells::Scope.inside(subject.node)
12
12
  .select { it.is_a?(Prism::CallNode) && it.name == :respond_to? }
13
13
  end
14
14
 
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Hashira::Smells::ModuleInitialize < Hashira::Smells::Check
4
- def self.judge? = true
5
-
6
4
  private
7
5
 
8
6
  def smelly? = subject.kind == :module && flagged
@@ -9,7 +9,7 @@ class Hashira::Smells::NilCheck < Hashira::Smells::Check
9
9
 
10
10
  def smelly? = checks.any?
11
11
 
12
- def checks = @checks ||= Hashira::Smells::Scope.inside(subject.node).select { check?(it) }
12
+ def checks = @_checks ||= Hashira::Smells::Scope.inside(subject.node).select { check?(it) }
13
13
 
14
14
  def check?(node)
15
15
  case node
@@ -2,17 +2,29 @@
2
2
 
3
3
  class Hashira::Smells::Ownership
4
4
  def initialize(trees)
5
- @suffixes = Set.new
6
- @tables = {}
7
- trees.each { survey(it) }
5
+ @trees = trees
8
6
  end
9
7
 
10
- def owned?(segments) = @suffixes.include?(segments.join("::"))
8
+ def owned?(segments)
9
+ surveyed
10
+ @_suffixes.include?(segments.join("::"))
11
+ end
11
12
 
12
- def keys(segments) = @tables.fetch(segments.join("::"), [])
13
+ def keys(segments)
14
+ surveyed
15
+ @_tables.fetch(segments.join("::"), [])
16
+ end
13
17
 
14
18
  private
15
19
 
20
+ def surveyed
21
+ return if @_surveyed
22
+ @_surveyed = true
23
+ @_suffixes = Set.new
24
+ @_tables = {}
25
+ @trees.each { survey(it) }
26
+ end
27
+
16
28
  def survey(tree)
17
29
  Hashira::Analysis::TypeWalk.each(tree) do |node, full|
18
30
  absorb(full)
@@ -27,14 +39,14 @@ class Hashira::Smells::Ownership
27
39
  end
28
40
 
29
41
  def absorb(path)
30
- @suffixes.merge(suffixes(path))
42
+ @_suffixes.merge(suffixes(path))
31
43
  end
32
44
 
33
45
  def chart(path, value)
34
46
  return unless value.is_a?(Prism::HashNode)
35
47
  keys = value.elements.map { spine(it) }
36
48
  return if keys.empty? || keys.any?(&:nil?)
37
- suffixes(path).each { @tables[it] = keys }
49
+ suffixes(path).each { @_tables[it] = keys }
38
50
  end
39
51
 
40
52
  def thaw(value)
@@ -22,13 +22,13 @@ class Hashira::Smells::ParamCheck
22
22
  private
23
23
 
24
24
  def nested
25
- @nested ||= Hashira::Smells::Conditions.nested(branches).map { self.class.new(it, @name) }
25
+ @_nested ||= Hashira::Smells::Conditions.nested(branches).map { self.class.new(it, @name) }
26
26
  end
27
27
 
28
28
  def branches = Hashira::Smells::Conditions.branches(@node)
29
29
 
30
30
  def predicate
31
- @predicate ||= spread(Hashira::Smells::Conditions.condition(@node))
31
+ @_predicate ||= spread(Hashira::Smells::Conditions.condition(@node))
32
32
  end
33
33
 
34
34
  def spread(condition) = condition ? Hashira::Analysis::NodeWalk.collect(condition) : []
@@ -10,23 +10,29 @@ class Hashira::Smells::Refs
10
10
  Prism::InstanceVariableOperatorWriteNode, Prism::InstanceVariableTargetNode
11
11
  ].freeze
12
12
 
13
- def initialize(def_node)
14
- @tallies = {}
15
- Hashira::Smells::Scope.inside(def_node).each { record(it) }
13
+ def initialize(definition)
14
+ @node = definition
16
15
  end
17
16
 
18
17
  def ego = lines(:self).size
19
18
 
20
- def lines(name) = @tallies.fetch(name, [])
19
+ def lines(name) = tallies.fetch(name, [])
21
20
 
22
21
  def envious
23
- peak = @tallies.values.map(&:size).max
24
- names = @tallies.filter_map { |name, sightings| name if sightings.size == peak }
22
+ peak = tallies.values.map(&:size).max
23
+ names = tallies.filter_map { |name, sightings| name if sightings.size == peak }
25
24
  names.include?(:self) ? [] : names
26
25
  end
27
26
 
28
27
  private
29
28
 
29
+ def tallies
30
+ return @_tallies if @_tallies
31
+ @_tallies = {}
32
+ Hashira::Smells::Scope.inside(@node).each { record(it) }
33
+ @_tallies
34
+ end
35
+
30
36
  def record(node)
31
37
  return note(:self, node) if selfish?(node)
32
38
  case node
@@ -49,5 +55,5 @@ class Hashira::Smells::Refs
49
55
  end
50
56
  end
51
57
 
52
- def note(name, node) = (@tallies[name] ||= []) << node.location.start_line
58
+ def note(name, node) = (@_tallies[name] ||= []) << node.location.start_line
53
59
  end
@@ -5,14 +5,12 @@ require "prism"
5
5
  class Hashira::Smells::RepeatedConditional < Hashira::Smells::Check
6
6
  LIMIT = 2
7
7
 
8
- def self.judge? = true
9
-
10
8
  private
11
9
 
12
10
  def smelly? = subject.kind == :class && repeats.any?
13
11
 
14
12
  def repeats
15
- @repeats ||= predicates.group_by(&:slice).except("block_given?").select { |_test, nodes| nodes.size > LIMIT }
13
+ @_repeats ||= predicates.group_by(&:slice).except("block_given?").select { |_test, nodes| nodes.size > LIMIT }
16
14
  end
17
15
 
18
16
  def predicates = Hashira::Smells::Scope.sweep(subject.node).filter_map { predicate(it) }
@@ -8,6 +8,7 @@ require_relative "duplicate_method_call"
8
8
  require_relative "feature_envy"
9
9
  require_relative "foreign"
10
10
  require_relative "instance_variable_assumption"
11
+ require_relative "kind"
11
12
  require_relative "manual_dispatch"
12
13
  require_relative "module_initialize"
13
14
  require_relative "nil_check"
@@ -19,22 +20,30 @@ require_relative "utility_function"
19
20
  class Hashira::Smells::Report
20
21
  CHECKS = Hashira::Smells::Check.subclasses.sort_by(&:name).freeze
21
22
 
22
- JUDGES = CHECKS.select(&:judge?).freeze
23
+ JUDGES = [
24
+ Hashira::Smells::DataClump, Hashira::Smells::InstanceVariableAssumption,
25
+ Hashira::Smells::ModuleInitialize, Hashira::Smells::RepeatedConditional,
26
+ Hashira::Smells::TooManyInstanceVariables
27
+ ].freeze
23
28
 
24
- PROBES = CHECKS.reject(&:judge?).freeze
29
+ PROBES = (CHECKS - JUDGES).freeze
25
30
 
26
31
  def initialize(project, trees)
27
- @census = Hashira::Smells::Census.new(project, trees)
28
- @types = @census.types
32
+ @project = project
33
+ @trees = trees
29
34
  end
30
35
 
31
- def findings = @findings ||= sniff(@types, JUDGES) + sniff(methods, PROBES) + sprawl
36
+ def findings = @_findings ||= sniff(types, JUDGES) + sniff(methods, PROBES) + sprawl
32
37
 
33
38
  private
34
39
 
35
- def methods = @types.flat_map(&:defs)
40
+ def census = @_census ||= Hashira::Smells::Census.new(@project, @trees)
36
41
 
37
- def sprawl = Hashira::Smells::BoundarySprawl.new(methods, @census.ownership).findings
42
+ def types = @_types ||= census.types
43
+
44
+ def methods = types.flat_map(&:defs)
45
+
46
+ def sprawl = Hashira::Smells::BoundarySprawl.new(methods, census.ownership).findings
38
47
 
39
48
  def sniff(subjects, checks) = subjects.flat_map { |subject| verdicts(subject, checks) }
40
49
 
@@ -10,14 +10,14 @@ class Hashira::Smells::TooManyInstanceVariables < Hashira::Smells::Check
10
10
  Prism::InstanceVariableOperatorWriteNode, Prism::InstanceVariableTargetNode
11
11
  ].freeze
12
12
 
13
- def self.judge? = true
14
-
15
13
  private
16
14
 
17
15
  def smelly? = subject.kind == :class && names.size > LIMIT
18
16
 
19
17
  def names
20
- @names ||= Hashira::Smells::Scope.sweep(subject.node).select { COUNTED.include?(it.class) }.map(&:name).uniq.sort
18
+ @_names ||=
19
+ Hashira::Smells::Scope.sweep(subject.node).select { COUNTED.include?(it.class) }
20
+ .map(&:name).reject { it.start_with?("@_") }.uniq.sort
21
21
  end
22
22
 
23
23
  def detail = { site:, count: names.size }