hashira 0.8.0 → 0.10.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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +80 -0
  3. data/README.md +61 -6
  4. data/exe/hashira +1 -1
  5. data/lib/hashira/analysis/finding.rb +10 -0
  6. data/lib/hashira/analysis/type_walk.rb +4 -0
  7. data/lib/hashira/boundaries.rb +64 -0
  8. data/lib/hashira/ci/baseline.rb +26 -7
  9. data/lib/hashira/ci/comparison.rb +14 -5
  10. data/lib/hashira/ci/{edge_diff_report.rb → edge_changes.rb} +1 -1
  11. data/lib/hashira/ci/{finding_diff_report.rb → finding_changes.rb} +1 -1
  12. data/lib/hashira/ci/mark.rb +7 -0
  13. data/lib/hashira/ci/ratchet.rb +3 -1
  14. data/lib/hashira/ci/ratchet_report.rb +2 -2
  15. data/lib/hashira/ci/slice.rb +7 -3
  16. data/lib/hashira/ci/sweep.rb +1 -1
  17. data/lib/hashira/cli/flag.rb +1 -1
  18. data/lib/hashira/cli/flags.rb +1 -1
  19. data/lib/hashira/cli/options.rb +16 -6
  20. data/lib/hashira/cli/run.rb +1 -6
  21. data/lib/hashira/{cli.rb → cli/session.rb} +7 -5
  22. data/lib/hashira/coupling/{cycle_findings.rb → cycle.rb} +1 -1
  23. data/lib/hashira/coupling/definitions.rb +1 -7
  24. data/lib/hashira/coupling/{roll_call_findings.rb → echo.rb} +1 -1
  25. data/lib/hashira/coupling/{sdp_violation_findings.rb → imbalance.rb} +3 -3
  26. data/lib/hashira/coupling/{mixed_audience_findings.rb → mixed_audience.rb} +1 -1
  27. data/lib/hashira/coupling/report.rb +5 -5
  28. data/lib/hashira/coupling/{wide_edge_findings.rb → wide_edge.rb} +1 -1
  29. data/lib/hashira/duplication/clusters.rb +1 -1
  30. data/lib/hashira/duplication/fragment.rb +18 -0
  31. data/lib/hashira/duplication/literal.rb +25 -0
  32. data/lib/hashira/duplication/variance.rb +7 -1
  33. data/lib/hashira/pipeline.rb +8 -1
  34. data/lib/hashira/project.rb +4 -1
  35. data/lib/hashira/report/smell_phrases.rb +5 -5
  36. data/lib/hashira/smells/{instance_variable_assumption.rb → assumed_state.rb} +5 -15
  37. data/lib/hashira/smells/boundary_sprawl.rb +3 -2
  38. data/lib/hashira/smells/branches.rb +62 -0
  39. data/lib/hashira/smells/census.rb +8 -2
  40. data/lib/hashira/smells/contexts.rb +3 -1
  41. data/lib/hashira/smells/lineage.rb +92 -0
  42. data/lib/hashira/smells/{duplicate_method_call.rb → repeated_call.rb} +25 -5
  43. data/lib/hashira/smells/report.rb +10 -7
  44. data/lib/hashira/smells/{too_many_instance_variables.rb → state_sprawl.rb} +1 -1
  45. data/lib/hashira/version.rb +1 -1
  46. data/lib/hashira.rb +19 -11
  47. metadata +17 -12
@@ -2,7 +2,7 @@
2
2
 
3
3
  require_relative "rule"
4
4
 
5
- class Hashira::Coupling::MixedAudienceFindings < Hashira::Coupling::Rule
5
+ class Hashira::Coupling::MixedAudience < Hashira::Coupling::Rule
6
6
  KIND = "mixed_audience"
7
7
 
8
8
  def list
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "cycle_findings"
4
- require_relative "mixed_audience_findings"
5
- require_relative "roll_call_findings"
3
+ require_relative "cycle"
4
+ require_relative "mixed_audience"
5
+ require_relative "echo"
6
6
  require_relative "rule"
7
- require_relative "sdp_violation_findings"
8
- require_relative "wide_edge_findings"
7
+ require_relative "imbalance"
8
+ require_relative "wide_edge"
9
9
 
10
10
  class Hashira::Coupling::Report
11
11
  RULES = Hashira::Coupling::Rule.subclasses.sort_by(&:name).freeze
@@ -2,7 +2,7 @@
2
2
 
3
3
  require_relative "rule"
4
4
 
5
- class Hashira::Coupling::WideEdgeFindings < Hashira::Coupling::Rule
5
+ class Hashira::Coupling::WideEdge < Hashira::Coupling::Rule
6
6
  KIND = "wide_edge"
7
7
 
8
8
  WIDTH = 5
@@ -19,7 +19,7 @@ class Hashira::Duplication::Clusters
19
19
 
20
20
  private
21
21
 
22
- def fragments = @_fragments ||= @all.select { |fragment| fragment.mass >= PREFILTER }
22
+ def fragments = @_fragments ||= @all.select { |fragment| fragment.mass >= PREFILTER }.reject(&:schema?)
23
23
 
24
24
  def sets = @_sets ||= Hashira::Duplication::UnionFind.new
25
25
 
@@ -1,10 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "digest"
4
+ require "prism"
4
5
 
5
6
  class Hashira::Duplication::Fragment
6
7
  DIGEST_LENGTH = 12
7
8
 
9
+ SCHEMA = %i[
10
+ class_node module_node statements_node arguments_node assoc_node
11
+ array_node hash_node keyword_hash_node constant_read_node constant_path_node
12
+ symbol_node string_node integer_node float_node true_node false_node nil_node
13
+ ].freeze
14
+
8
15
  def initialize(file, roots)
9
16
  @file = file
10
17
  @roots = roots
@@ -20,6 +27,8 @@ class Hashira::Duplication::Fragment
20
27
 
21
28
  def mass = types.size
22
29
 
30
+ def schema? = nodes.all? { directive?(it) }
31
+
23
32
  def line = @roots.first.location.start_line
24
33
 
25
34
  def finish = @roots.last.location.end_line
@@ -35,4 +44,13 @@ class Hashira::Duplication::Fragment
35
44
  def touches?(others) = others.any? { overlaps?(it) }
36
45
 
37
46
  def nodes = @_nodes ||= @roots.flat_map { Hashira::Analysis::NodeWalk.collect(it) }
47
+
48
+ private
49
+
50
+ def directive?(node)
51
+ return SCHEMA.include?(node.type) unless node.is_a?(Prism::CallNode)
52
+ !node.receiver && !node.block && literals?(node.arguments)
53
+ end
54
+
55
+ def literals?(arguments) = Hashira::Duplication::Literal.new(arguments).literals?
38
56
  end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ class Hashira::Duplication::Literal
6
+ TYPES = [
7
+ Prism::ArrayNode, Prism::AssocNode, Prism::FalseNode, Prism::FloatNode,
8
+ Prism::HashNode, Prism::IntegerNode, Prism::KeywordHashNode, Prism::NilNode,
9
+ Prism::StringNode, Prism::SymbolNode, Prism::TrueNode
10
+ ].freeze
11
+
12
+ def initialize(node)
13
+ @node = node
14
+ end
15
+
16
+ def literal? = TYPES.include?(@node.class) && parts.all?(&:literal?)
17
+
18
+ def literals? = parts.all?(&:literal?)
19
+
20
+ private
21
+
22
+ def parts = children.map { Hashira::Duplication::Literal.new(it) }
23
+
24
+ def children = Array(@node&.compact_child_nodes)
25
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "prism"
4
+
3
5
  class Hashira::Duplication::Variance
4
6
  LITERALS = %i[integer_node float_node string_node symbol_node].freeze
5
7
  VALUED = %i[integer_node float_node].freeze
@@ -27,7 +29,11 @@ class Hashira::Duplication::Variance
27
29
 
28
30
  def pairs = @canonical.nodes.zip(@other.nodes)
29
31
 
30
- def named = @_named ||= pairs.select { |left, _| NAMED.include?(left.type) }
32
+ def named = @_named ||= pairs.select { |left, _| NAMED.include?(left.type) && !inner?(left) }
33
+
34
+ def inner?(node) = nested.any? { it.equal?(node) }
35
+
36
+ def nested = @_nested ||= @canonical.nodes.grep(Prism::ConstantPathNode).filter_map(&:parent)
31
37
 
32
38
  def differing = pairs.select { |pair| varies?(*pair) }.map(&:first)
33
39
 
@@ -22,6 +22,11 @@ class Hashira::Pipeline
22
22
 
23
23
  attr_reader :project
24
24
 
25
+ def verify
26
+ boundaries.interpreted
27
+ self
28
+ end
29
+
25
30
  def unparsed = parsed.unparsed
26
31
 
27
32
  def graph = coupling.graph
@@ -30,7 +35,7 @@ class Hashira::Pipeline
30
35
 
31
36
  def duplication = @_duplication ||= analyzed(:duplication, Hashira::Duplication::Clones, churn)
32
37
 
33
- def smells = @_smells ||= analyzed(:smells, Hashira::Smells::Report)
38
+ def smells = @_smells ||= analyzed(:smells, Hashira::Smells::Report, boundaries)
34
39
 
35
40
  def hotspots
36
41
  @_hotspots ||= Hashira::Hotspots::Rollup.new(complexity, duplication, churn) if complexity || duplication
@@ -52,6 +57,8 @@ class Hashira::Pipeline
52
57
 
53
58
  def parsed = @_parsed ||= Hashira::Trees.new(@project)
54
59
 
60
+ def boundaries = @_boundaries ||= Hashira::Boundaries.new(@project.boundaries, parsed.all)
61
+
55
62
  def coupling
56
63
  @_coupling ||= Hashira::Coupling::Report.new(@project, parsed.all, packaging: settle(@packaging))
57
64
  end
@@ -3,10 +3,13 @@
3
3
  class Hashira::Project
4
4
  ROOT_PACKAGE = "(root)"
5
5
 
6
- def initialize(requested)
6
+ def initialize(requested, boundaries: [])
7
7
  @requested = requested
8
+ @boundaries = boundaries
8
9
  end
9
10
 
11
+ attr_reader :boundaries
12
+
10
13
  def directories = @_directories ||= resolved
11
14
 
12
15
  def rails? = directories.any? { config?(File.expand_path(it)) }
@@ -14,7 +14,7 @@ module Hashira::Report::Phrases
14
14
  "Introduce a parameter object."
15
15
  end
16
16
 
17
- def on_duplicate_method_call(finding)
17
+ def on_repeated_call(finding)
18
18
  "#{finding.package} repeats identical calls (#{finding.detail[:site]}). " \
19
19
  "Name the result in a local variable."
20
20
  end
@@ -32,9 +32,9 @@ module Hashira::Report::Phrases
32
32
  "The behavior may belong on #{names.first}."
33
33
  end
34
34
 
35
- def on_instance_variable_assumption(finding)
36
- "#{finding.package} reads instance variables never set in initialize (#{finding.detail[:site]}). " \
37
- "Assign them in initialize, or pass the data explicitly."
35
+ def on_assumed_state(finding)
36
+ "#{finding.package} reads instance variables nothing in the class assigns (#{finding.detail[:site]}). " \
37
+ "Assign them where the object is built, or pass the data explicitly."
38
38
  end
39
39
 
40
40
  def on_manual_dispatch(finding)
@@ -56,7 +56,7 @@ module Hashira::Report::Phrases
56
56
  tally(finding, "branches on the same test %d times", "Replace the scattered checks with polymorphism.")
57
57
  end
58
58
 
59
- def on_too_many_instance_variables(finding)
59
+ def on_state_sprawl(finding)
60
60
  tally(finding, "holds %d instance variables", "Split the class, or gather related fields into value objects.")
61
61
  end
62
62
 
@@ -2,13 +2,7 @@
2
2
 
3
3
  require "prism"
4
4
 
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
-
5
+ class Hashira::Smells::AssumedState < Hashira::Smells::Check
12
6
  MAYBES = [
13
7
  Prism::LocalVariableOrWriteNode, Prism::InstanceVariableOrWriteNode,
14
8
  Prism::ClassVariableOrWriteNode, Prism::GlobalVariableOrWriteNode,
@@ -28,19 +22,15 @@ class Hashira::Smells::InstanceVariableAssumption < Hashira::Smells::Check
28
22
 
29
23
  private
30
24
 
31
- def smelly? = subject.kind == :class && assumed.any?
25
+ def smelly? = subject.kind == :class && assigned && assumed.any?
26
+
27
+ def assigned = subject.assigned
32
28
 
33
- def assumed = @_assumed ||= (read - prepared).uniq.sort.reject { cache?(it) }
29
+ def assumed = @_assumed ||= (read - assigned).uniq.sort.reject { cache?(it) }
34
30
 
35
31
  def cache?(name) = name.start_with?("@_")
36
32
 
37
33
  def read = subject.owned.flat_map { Harvest.reads(it.node) }
38
34
 
39
- def prepared
40
- starters.flat_map { Hashira::Smells::Scope.inside(it.node) }.select { SETTERS.include?(it.class) }.map(&:name)
41
- end
42
-
43
- def starters = subject.owned.select { it.node.name == :initialize }
44
-
45
35
  def evidence = assumed.map(&:to_s)
46
36
  end
@@ -9,13 +9,14 @@ class Hashira::Smells::BoundarySprawl
9
9
 
10
10
  SHOWN = 4
11
11
 
12
- def initialize(subjects, ownership)
12
+ def initialize(subjects, ownership, interpreted = [])
13
13
  @subjects = subjects
14
14
  @ownership = ownership
15
+ @interpreted = interpreted
15
16
  end
16
17
 
17
18
  def findings
18
- reaches.filter_map { |root, contexts| finding(root, contexts) }
19
+ reaches.except(*@interpreted).filter_map { |root, contexts| finding(root, contexts) }
19
20
  end
20
21
 
21
22
  private
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ class Hashira::Smells::Branches
6
+ def initialize(root)
7
+ @root = root
8
+ end
9
+
10
+ def together?(nodes) = nodes.combination(2).any? { |left, right| meet?(trail(left), trail(right)) }
11
+
12
+ private
13
+
14
+ def meet?(left, right) = shared(left, right).none? { parted?(*it) }
15
+
16
+ def shared(left, right) = left.zip(right).select(&:last)
17
+
18
+ def parted?(here, there) = here.first.equal?(there.first) && here.last != there.last
19
+
20
+ def trail(node) = trails.fetch(node)
21
+
22
+ def trails
23
+ return @_trails if @_trails
24
+ @_trails = {}.compare_by_identity
25
+ chart(@root, [])
26
+ @_trails
27
+ end
28
+
29
+ def chart(node, trail)
30
+ @_trails[node] = trail
31
+ node.compact_child_nodes.each { chart(it, trail + taken(node, it)) }
32
+ end
33
+
34
+ def taken(node, child)
35
+ return arm_for(node, child) if node.is_a?(Prism::BeginNode)
36
+ arm = arms(node).index { it.equal?(child) }
37
+ arm ? [[node, arm]] : []
38
+ end
39
+
40
+ def arm_for(node, child)
41
+ rescued = node.rescue_clause
42
+ return [] unless [node.statements, rescued, node.else_clause].any? { it.equal?(child) }
43
+ [[node, rescued.equal?(child) ? 1 : 0]]
44
+ end
45
+
46
+ def arms(node) = (clauses(node) || conditions(node)).compact
47
+
48
+ def clauses(node)
49
+ case node
50
+ when Prism::IfNode, Prism::RescueNode then [node.statements, node.subsequent]
51
+ when Prism::UnlessNode then [node.statements, node.else_clause]
52
+ when Prism::RescueModifierNode then [node.expression, node.rescue_expression]
53
+ end
54
+ end
55
+
56
+ def conditions(node)
57
+ case node
58
+ when Prism::CaseNode, Prism::CaseMatchNode then node.conditions + [node.else_clause]
59
+ else []
60
+ end
61
+ end
62
+ end
@@ -10,13 +10,19 @@ class Hashira::Smells::Census
10
10
 
11
11
  def ownership = @_ownership ||= Hashira::Smells::Ownership.new(@trees.values)
12
12
 
13
- 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) })
14
14
 
15
15
  private
16
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
+
17
23
  def harvest(file, tree)
18
24
  found = []
19
- 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("::")) }
20
26
  found
21
27
  end
22
28
 
@@ -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
@@ -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
@@ -2,26 +2,46 @@
2
2
 
3
3
  require "prism"
4
4
 
5
- class Hashira::Smells::DuplicateMethodCall < Hashira::Smells::Check
5
+ class Hashira::Smells::RepeatedCall < 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
@@ -4,33 +4,34 @@ require_relative "boundary_sprawl"
4
4
  require_relative "check"
5
5
  require_relative "control_parameter"
6
6
  require_relative "data_clump"
7
- require_relative "duplicate_method_call"
7
+ require_relative "repeated_call"
8
8
  require_relative "feature_envy"
9
9
  require_relative "foreign"
10
- require_relative "instance_variable_assumption"
10
+ require_relative "assumed_state"
11
11
  require_relative "kind"
12
12
  require_relative "manual_dispatch"
13
13
  require_relative "module_initialize"
14
14
  require_relative "nil_check"
15
15
  require_relative "ownership"
16
16
  require_relative "repeated_conditional"
17
- require_relative "too_many_instance_variables"
17
+ require_relative "state_sprawl"
18
18
  require_relative "utility_function"
19
19
 
20
20
  class Hashira::Smells::Report
21
21
  CHECKS = Hashira::Smells::Check.subclasses.sort_by(&:name).freeze
22
22
 
23
23
  JUDGES = [
24
- Hashira::Smells::DataClump, Hashira::Smells::InstanceVariableAssumption,
24
+ Hashira::Smells::DataClump, Hashira::Smells::AssumedState,
25
25
  Hashira::Smells::ModuleInitialize, Hashira::Smells::RepeatedConditional,
26
- Hashira::Smells::TooManyInstanceVariables
26
+ Hashira::Smells::StateSprawl
27
27
  ].freeze
28
28
 
29
29
  PROBES = (CHECKS - JUDGES).freeze
30
30
 
31
- def initialize(project, trees)
31
+ def initialize(project, trees, boundaries = nil)
32
32
  @project = project
33
33
  @trees = trees
34
+ @boundaries = boundaries
34
35
  end
35
36
 
36
37
  def findings = @_findings ||= sniff(types, JUDGES) + sniff(methods, PROBES) + sprawl
@@ -43,7 +44,9 @@ class Hashira::Smells::Report
43
44
 
44
45
  def methods = types.flat_map(&:defs)
45
46
 
46
- def sprawl = Hashira::Smells::BoundarySprawl.new(methods, census.ownership).findings
47
+ def sprawl = Hashira::Smells::BoundarySprawl.new(methods, census.ownership, interpreted).findings
48
+
49
+ def interpreted = @boundaries ? @boundaries.interpreted : []
47
50
 
48
51
  def sniff(subjects, checks) = subjects.flat_map { |subject| verdicts(subject, checks) }
49
52
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  require "prism"
4
4
 
5
- class Hashira::Smells::TooManyInstanceVariables < Hashira::Smells::Check
5
+ class Hashira::Smells::StateSprawl < Hashira::Smells::Check
6
6
  LIMIT = 4
7
7
 
8
8
  COUNTED = [
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hashira
4
- VERSION = "0.8.0"
4
+ VERSION = "0.10.0"
5
5
  end