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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +94 -0
- data/README.md +57 -3
- data/exe/hashira +1 -1
- data/lib/hashira/analysis/finding.rb +11 -1
- data/lib/hashira/analysis/syntax.rb +6 -6
- data/lib/hashira/analysis/type_walk.rb +4 -0
- data/lib/hashira/churn.rb +1 -10
- data/lib/hashira/ci/accepted.rb +9 -7
- data/lib/hashira/ci/baseline.rb +53 -33
- data/lib/hashira/ci/comparison.rb +33 -0
- data/lib/hashira/ci/diff.rb +0 -15
- data/lib/hashira/ci/gate.rb +3 -3
- data/lib/hashira/ci/mark.rb +7 -0
- data/lib/hashira/ci/ratchet.rb +12 -4
- data/lib/hashira/ci/ratchet_report.rb +4 -4
- data/lib/hashira/ci/scope.rb +0 -1
- data/lib/hashira/ci/slice.rb +21 -0
- data/lib/hashira/ci/sweep.rb +13 -0
- data/lib/hashira/cli/arguments.rb +12 -8
- data/lib/hashira/cli/choice.rb +9 -0
- data/lib/hashira/cli/command_line.rb +8 -5
- data/lib/hashira/cli/fail_on.rb +6 -6
- data/lib/hashira/cli/flag.rb +3 -1
- data/lib/hashira/cli/flags.rb +10 -0
- data/lib/hashira/cli/format.rb +1 -1
- data/lib/hashira/cli/needs.rb +13 -2
- data/lib/hashira/cli/only.rb +16 -0
- data/lib/hashira/cli/options.rb +8 -10
- data/lib/hashira/cli/package_by.rb +1 -1
- data/lib/hashira/cli/run.rb +9 -8
- data/lib/hashira/cli.rb +22 -21
- data/lib/hashira/complexity/cognitive_score.rb +44 -33
- data/lib/hashira/complexity/method_finding.rb +7 -5
- data/lib/hashira/complexity/method_score.rb +1 -1
- data/lib/hashira/complexity/scores.rb +11 -9
- data/lib/hashira/coupling/audiences.rb +3 -3
- data/lib/hashira/coupling/catalog.rb +8 -6
- data/lib/hashira/coupling/census.rb +42 -22
- data/lib/hashira/coupling/cycle_search.rb +11 -7
- data/lib/hashira/coupling/definitions.rb +1 -7
- data/lib/hashira/coupling/edge_map.rb +9 -8
- data/lib/hashira/coupling/folding.rb +5 -5
- data/lib/hashira/coupling/graph.rb +16 -9
- data/lib/hashira/coupling/metric.rb +3 -1
- data/lib/hashira/coupling/naming.rb +4 -4
- data/lib/hashira/coupling/references.rb +12 -12
- data/lib/hashira/coupling/report.rb +7 -9
- data/lib/hashira/coupling/roll_call.rb +1 -1
- data/lib/hashira/coupling/roll_call_findings.rb +1 -1
- data/lib/hashira/coupling/roster.rb +37 -12
- data/lib/hashira/coupling/rule.rb +1 -1
- data/lib/hashira/coupling/sdp_violation_findings.rb +3 -1
- data/lib/hashira/duplication/clones.rb +1 -1
- data/lib/hashira/duplication/clusters.rb +10 -7
- data/lib/hashira/duplication/delta.rb +1 -4
- data/lib/hashira/duplication/duplication_finding.rb +4 -2
- data/lib/hashira/duplication/fragment.rb +20 -2
- data/lib/hashira/duplication/harvest.rb +7 -5
- data/lib/hashira/duplication/index.rb +3 -2
- data/lib/hashira/duplication/literal.rb +25 -0
- data/lib/hashira/duplication/similarity.rb +2 -2
- data/lib/hashira/duplication/variance.rb +7 -1
- data/lib/hashira/error.rb +0 -3
- data/lib/hashira/focus.rb +29 -0
- data/lib/hashira/git_log.rb +19 -0
- data/lib/hashira/hotspots/file_cost.rb +1 -1
- data/lib/hashira/hotspots/rollup.rb +2 -2
- data/lib/hashira/pipeline.rb +22 -14
- data/lib/hashira/project.rb +35 -34
- data/lib/hashira/report/columns.rb +14 -8
- data/lib/hashira/report/graph_payload.rb +1 -1
- data/lib/hashira/report/hotspot_table.rb +1 -1
- data/lib/hashira/report/json.rb +2 -1
- data/lib/hashira/report/metrics_table.rb +3 -3
- data/lib/hashira/report/phrases.rb +23 -18
- data/lib/hashira/report/smell_phrases.rb +2 -2
- data/lib/hashira/smells/branches.rb +62 -0
- data/lib/hashira/smells/census.rb +11 -6
- data/lib/hashira/smells/check.rb +1 -5
- data/lib/hashira/smells/contexts.rb +7 -5
- data/lib/hashira/smells/control_parameter.rb +1 -1
- data/lib/hashira/smells/data_clump.rb +2 -4
- data/lib/hashira/smells/duplicate_method_call.rb +25 -5
- data/lib/hashira/smells/feature_envy.rb +3 -3
- data/lib/hashira/smells/foreign.rb +14 -14
- data/lib/hashira/smells/instance_variable_assumption.rb +5 -15
- data/lib/hashira/smells/kind.rb +9 -0
- data/lib/hashira/smells/lineage.rb +92 -0
- data/lib/hashira/smells/manual_dispatch.rb +1 -1
- data/lib/hashira/smells/module_initialize.rb +0 -2
- data/lib/hashira/smells/nil_check.rb +1 -1
- data/lib/hashira/smells/ownership.rb +19 -7
- data/lib/hashira/smells/param_check.rb +2 -2
- data/lib/hashira/smells/refs.rb +13 -7
- data/lib/hashira/smells/repeated_conditional.rb +1 -3
- data/lib/hashira/smells/report.rb +16 -7
- data/lib/hashira/smells/too_many_instance_variables.rb +3 -3
- data/lib/hashira/smells/visibility.rb +18 -11
- data/lib/hashira/trees.rb +8 -4
- data/lib/hashira/version.rb +1 -1
- data/lib/hashira.rb +12 -0
- 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
|
-
|
|
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 |
|
|
32
|
-
Hashira::Smells::MethodContext.new(owner: name, node:
|
|
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
|
data/lib/hashira/smells/check.rb
CHANGED
|
@@ -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.
|
|
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(
|
|
12
|
+
def names(definition) = parts(definition).flat_map { expand(it) }
|
|
13
13
|
|
|
14
|
-
def arguments(
|
|
14
|
+
def arguments(definition) = parts(definition).grep_v(Prism::BlockParameterNode).flat_map { expand(it) }
|
|
15
15
|
|
|
16
|
-
def parts(
|
|
17
|
-
node =
|
|
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 = @
|
|
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 = @
|
|
12
|
+
def candidates = @_candidates ||= subject.owned.map { [it, it.arguments.sort] }
|
|
15
13
|
|
|
16
|
-
def clumps = @
|
|
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
|
|
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
|
-
@
|
|
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
|
|
28
|
-
@
|
|
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 = @
|
|
10
|
+
def refs = @_refs ||= Hashira::Smells::Refs.new(subject.node)
|
|
11
11
|
|
|
12
|
-
def envied = @
|
|
12
|
+
def envied = @_envied ||= refs.envious.reject { foreign.dismiss?(it) }
|
|
13
13
|
|
|
14
|
-
def foreign = @
|
|
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
|
-
|
|
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
|
-
|
|
40
|
-
Hashira::Analysis::Syntax.segments(
|
|
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? =
|
|
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 =
|
|
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
|
-
|
|
80
|
+
body.grep(kind).select { it.name == name }
|
|
79
81
|
end
|
|
80
82
|
|
|
81
83
|
def snares(name)
|
|
82
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
27
|
+
def assigned = subject.assigned
|
|
36
28
|
|
|
37
|
-
def
|
|
29
|
+
def assumed = @_assumed ||= (read - assigned).uniq.sort.reject { cache?(it) }
|
|
38
30
|
|
|
39
|
-
def
|
|
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
|
|
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,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
|
-
@
|
|
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
|
|
|
@@ -9,7 +9,7 @@ class Hashira::Smells::NilCheck < Hashira::Smells::Check
|
|
|
9
9
|
|
|
10
10
|
def smelly? = checks.any?
|
|
11
11
|
|
|
12
|
-
def checks = @
|
|
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
|
-
@
|
|
6
|
-
@tables = {}
|
|
7
|
-
trees.each { survey(it) }
|
|
5
|
+
@trees = trees
|
|
8
6
|
end
|
|
9
7
|
|
|
10
|
-
def owned?(segments)
|
|
8
|
+
def owned?(segments)
|
|
9
|
+
surveyed
|
|
10
|
+
@_suffixes.include?(segments.join("::"))
|
|
11
|
+
end
|
|
11
12
|
|
|
12
|
-
def keys(segments)
|
|
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
|
-
@
|
|
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 { @
|
|
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
|
-
@
|
|
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
|
-
@
|
|
31
|
+
@_predicate ||= spread(Hashira::Smells::Conditions.condition(@node))
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
def spread(condition) = condition ? Hashira::Analysis::NodeWalk.collect(condition) : []
|
data/lib/hashira/smells/refs.rb
CHANGED
|
@@ -10,23 +10,29 @@ class Hashira::Smells::Refs
|
|
|
10
10
|
Prism::InstanceVariableOperatorWriteNode, Prism::InstanceVariableTargetNode
|
|
11
11
|
].freeze
|
|
12
12
|
|
|
13
|
-
def initialize(
|
|
14
|
-
@
|
|
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) =
|
|
19
|
+
def lines(name) = tallies.fetch(name, [])
|
|
21
20
|
|
|
22
21
|
def envious
|
|
23
|
-
peak =
|
|
24
|
-
names =
|
|
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) = (@
|
|
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
|
-
@
|
|
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 =
|
|
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
|
|
29
|
+
PROBES = (CHECKS - JUDGES).freeze
|
|
25
30
|
|
|
26
31
|
def initialize(project, trees)
|
|
27
|
-
@
|
|
28
|
-
@
|
|
32
|
+
@project = project
|
|
33
|
+
@trees = trees
|
|
29
34
|
end
|
|
30
35
|
|
|
31
|
-
def findings = @
|
|
36
|
+
def findings = @_findings ||= sniff(types, JUDGES) + sniff(methods, PROBES) + sprawl
|
|
32
37
|
|
|
33
38
|
private
|
|
34
39
|
|
|
35
|
-
def
|
|
40
|
+
def census = @_census ||= Hashira::Smells::Census.new(@project, @trees)
|
|
36
41
|
|
|
37
|
-
def
|
|
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
|
-
@
|
|
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 }
|