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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +80 -0
- data/README.md +61 -6
- data/exe/hashira +1 -1
- data/lib/hashira/analysis/finding.rb +10 -0
- data/lib/hashira/analysis/type_walk.rb +4 -0
- data/lib/hashira/boundaries.rb +64 -0
- data/lib/hashira/ci/baseline.rb +26 -7
- data/lib/hashira/ci/comparison.rb +14 -5
- data/lib/hashira/ci/{edge_diff_report.rb → edge_changes.rb} +1 -1
- data/lib/hashira/ci/{finding_diff_report.rb → finding_changes.rb} +1 -1
- data/lib/hashira/ci/mark.rb +7 -0
- data/lib/hashira/ci/ratchet.rb +3 -1
- data/lib/hashira/ci/ratchet_report.rb +2 -2
- data/lib/hashira/ci/slice.rb +7 -3
- data/lib/hashira/ci/sweep.rb +1 -1
- data/lib/hashira/cli/flag.rb +1 -1
- data/lib/hashira/cli/flags.rb +1 -1
- data/lib/hashira/cli/options.rb +16 -6
- data/lib/hashira/cli/run.rb +1 -6
- data/lib/hashira/{cli.rb → cli/session.rb} +7 -5
- data/lib/hashira/coupling/{cycle_findings.rb → cycle.rb} +1 -1
- data/lib/hashira/coupling/definitions.rb +1 -7
- data/lib/hashira/coupling/{roll_call_findings.rb → echo.rb} +1 -1
- data/lib/hashira/coupling/{sdp_violation_findings.rb → imbalance.rb} +3 -3
- data/lib/hashira/coupling/{mixed_audience_findings.rb → mixed_audience.rb} +1 -1
- data/lib/hashira/coupling/report.rb +5 -5
- data/lib/hashira/coupling/{wide_edge_findings.rb → wide_edge.rb} +1 -1
- data/lib/hashira/duplication/clusters.rb +1 -1
- data/lib/hashira/duplication/fragment.rb +18 -0
- data/lib/hashira/duplication/literal.rb +25 -0
- data/lib/hashira/duplication/variance.rb +7 -1
- data/lib/hashira/pipeline.rb +8 -1
- data/lib/hashira/project.rb +4 -1
- data/lib/hashira/report/smell_phrases.rb +5 -5
- data/lib/hashira/smells/{instance_variable_assumption.rb → assumed_state.rb} +5 -15
- data/lib/hashira/smells/boundary_sprawl.rb +3 -2
- data/lib/hashira/smells/branches.rb +62 -0
- data/lib/hashira/smells/census.rb +8 -2
- data/lib/hashira/smells/contexts.rb +3 -1
- data/lib/hashira/smells/lineage.rb +92 -0
- data/lib/hashira/smells/{duplicate_method_call.rb → repeated_call.rb} +25 -5
- data/lib/hashira/smells/report.rb +10 -7
- data/lib/hashira/smells/{too_many_instance_variables.rb → state_sprawl.rb} +1 -1
- data/lib/hashira/version.rb +1 -1
- data/lib/hashira.rb +19 -11
- metadata +17 -12
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "
|
|
4
|
-
require_relative "
|
|
5
|
-
require_relative "
|
|
3
|
+
require_relative "cycle"
|
|
4
|
+
require_relative "mixed_audience"
|
|
5
|
+
require_relative "echo"
|
|
6
6
|
require_relative "rule"
|
|
7
|
-
require_relative "
|
|
8
|
-
require_relative "
|
|
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
|
|
@@ -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
|
|
data/lib/hashira/pipeline.rb
CHANGED
|
@@ -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
|
data/lib/hashira/project.rb
CHANGED
|
@@ -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
|
|
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
|
|
36
|
-
"#{finding.package} reads instance variables
|
|
37
|
-
"Assign them
|
|
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
|
|
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::
|
|
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 -
|
|
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::
|
|
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
|
|
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)
|
|
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 "
|
|
7
|
+
require_relative "repeated_call"
|
|
8
8
|
require_relative "feature_envy"
|
|
9
9
|
require_relative "foreign"
|
|
10
|
-
require_relative "
|
|
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 "
|
|
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::
|
|
24
|
+
Hashira::Smells::DataClump, Hashira::Smells::AssumedState,
|
|
25
25
|
Hashira::Smells::ModuleInitialize, Hashira::Smells::RepeatedConditional,
|
|
26
|
-
Hashira::Smells::
|
|
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
|
|
data/lib/hashira/version.rb
CHANGED