spoom 1.3.2 → 1.3.3
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/lib/spoom/cli/deadcode.rb +21 -17
- data/lib/spoom/deadcode/index.rb +178 -10
- data/lib/spoom/deadcode/indexer.rb +14 -435
- data/lib/spoom/deadcode/plugins/action_mailer.rb +3 -3
- data/lib/spoom/deadcode/plugins/action_mailer_preview.rb +9 -3
- data/lib/spoom/deadcode/plugins/actionpack.rb +12 -9
- data/lib/spoom/deadcode/plugins/active_model.rb +8 -8
- data/lib/spoom/deadcode/plugins/active_record.rb +5 -5
- data/lib/spoom/deadcode/plugins/active_support.rb +4 -4
- data/lib/spoom/deadcode/plugins/base.rb +70 -57
- data/lib/spoom/deadcode/plugins/graphql.rb +8 -8
- data/lib/spoom/deadcode/plugins/minitest.rb +4 -3
- data/lib/spoom/deadcode/plugins/namespaces.rb +9 -12
- data/lib/spoom/deadcode/plugins/rails.rb +9 -9
- data/lib/spoom/deadcode/plugins/rubocop.rb +13 -17
- data/lib/spoom/deadcode/plugins/ruby.rb +9 -9
- data/lib/spoom/deadcode/plugins/sorbet.rb +15 -18
- data/lib/spoom/deadcode/plugins/thor.rb +5 -4
- data/lib/spoom/deadcode/plugins.rb +4 -5
- data/lib/spoom/deadcode/remover.rb +7 -7
- data/lib/spoom/deadcode/send.rb +1 -0
- data/lib/spoom/deadcode.rb +4 -73
- data/lib/spoom/location.rb +84 -0
- data/lib/spoom/model/builder.rb +246 -0
- data/lib/spoom/model/model.rb +328 -0
- data/lib/spoom/model/namespace_visitor.rb +50 -0
- data/lib/spoom/model/reference.rb +49 -0
- data/lib/spoom/model/references_visitor.rb +200 -0
- data/lib/spoom/model.rb +10 -0
- data/lib/spoom/parse.rb +28 -0
- data/lib/spoom/poset.rb +197 -0
- data/lib/spoom/sorbet/errors.rb +5 -3
- data/lib/spoom/sorbet/lsp/errors.rb +1 -1
- data/lib/spoom/sorbet.rb +1 -1
- data/lib/spoom/version.rb +1 -1
- data/lib/spoom/visitor.rb +755 -0
- data/lib/spoom.rb +2 -0
- metadata +20 -13
- data/lib/spoom/deadcode/location.rb +0 -86
- data/lib/spoom/deadcode/reference.rb +0 -34
- data/lib/spoom/deadcode/visitor.rb +0 -755
data/lib/spoom/deadcode.rb
CHANGED
@@ -4,84 +4,15 @@
|
|
4
4
|
require "erubi"
|
5
5
|
require "prism"
|
6
6
|
|
7
|
-
require_relative "
|
7
|
+
require_relative "visitor"
|
8
|
+
require_relative "location"
|
9
|
+
require_relative "parse"
|
10
|
+
|
8
11
|
require_relative "deadcode/erb"
|
9
12
|
require_relative "deadcode/index"
|
10
13
|
require_relative "deadcode/indexer"
|
11
14
|
|
12
|
-
require_relative "deadcode/location"
|
13
15
|
require_relative "deadcode/definition"
|
14
|
-
require_relative "deadcode/reference"
|
15
16
|
require_relative "deadcode/send"
|
16
17
|
require_relative "deadcode/plugins"
|
17
18
|
require_relative "deadcode/remover"
|
18
|
-
|
19
|
-
module Spoom
|
20
|
-
module Deadcode
|
21
|
-
class Error < Spoom::Error
|
22
|
-
extend T::Helpers
|
23
|
-
|
24
|
-
abstract!
|
25
|
-
end
|
26
|
-
|
27
|
-
class ParserError < Error; end
|
28
|
-
|
29
|
-
class IndexerError < Error
|
30
|
-
extend T::Sig
|
31
|
-
|
32
|
-
sig { params(message: String, parent: Exception).void }
|
33
|
-
def initialize(message, parent:)
|
34
|
-
super(message)
|
35
|
-
set_backtrace(parent.backtrace)
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
class << self
|
40
|
-
extend T::Sig
|
41
|
-
|
42
|
-
sig { params(ruby: String, file: String).returns(Prism::Node) }
|
43
|
-
def parse_ruby(ruby, file:)
|
44
|
-
result = Prism.parse(ruby)
|
45
|
-
unless result.success?
|
46
|
-
message = +"Error while parsing #{file}:\n"
|
47
|
-
|
48
|
-
result.errors.each do |e|
|
49
|
-
message << "- #{e.message} (at #{e.location.start_line}:#{e.location.start_column})\n"
|
50
|
-
end
|
51
|
-
|
52
|
-
raise ParserError, message
|
53
|
-
end
|
54
|
-
|
55
|
-
result.value
|
56
|
-
end
|
57
|
-
|
58
|
-
sig do
|
59
|
-
params(
|
60
|
-
index: Index,
|
61
|
-
node: Prism::Node,
|
62
|
-
ruby: String,
|
63
|
-
file: String,
|
64
|
-
plugins: T::Array[Deadcode::Plugins::Base],
|
65
|
-
).void
|
66
|
-
end
|
67
|
-
def index_node(index, node, ruby, file:, plugins: [])
|
68
|
-
visitor = Spoom::Deadcode::Indexer.new(file, ruby, index, plugins: plugins)
|
69
|
-
visitor.visit(node)
|
70
|
-
rescue => e
|
71
|
-
raise IndexerError.new("Error while indexing #{file} (#{e.message})", parent: e)
|
72
|
-
end
|
73
|
-
|
74
|
-
sig { params(index: Index, ruby: String, file: String, plugins: T::Array[Deadcode::Plugins::Base]).void }
|
75
|
-
def index_ruby(index, ruby, file:, plugins: [])
|
76
|
-
node = parse_ruby(ruby, file: file)
|
77
|
-
index_node(index, node, ruby, file: file, plugins: plugins)
|
78
|
-
end
|
79
|
-
|
80
|
-
sig { params(index: Index, erb: String, file: String, plugins: T::Array[Deadcode::Plugins::Base]).void }
|
81
|
-
def index_erb(index, erb, file:, plugins: [])
|
82
|
-
ruby = ERB.new(erb).src
|
83
|
-
index_ruby(index, ruby, file: file, plugins: plugins)
|
84
|
-
end
|
85
|
-
end
|
86
|
-
end
|
87
|
-
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Spoom
|
5
|
+
class Location
|
6
|
+
extend T::Sig
|
7
|
+
|
8
|
+
include Comparable
|
9
|
+
|
10
|
+
class LocationError < Spoom::Error; end
|
11
|
+
|
12
|
+
class << self
|
13
|
+
extend T::Sig
|
14
|
+
|
15
|
+
sig { params(location_string: String).returns(Location) }
|
16
|
+
def from_string(location_string)
|
17
|
+
file, rest = location_string.split(":", 2)
|
18
|
+
raise LocationError, "Invalid location string: #{location_string}" unless file && rest
|
19
|
+
|
20
|
+
start_line, rest = rest.split(":", 2)
|
21
|
+
raise LocationError, "Invalid location string: #{location_string}" unless start_line && rest
|
22
|
+
|
23
|
+
start_column, rest = rest.split("-", 2)
|
24
|
+
raise LocationError, "Invalid location string: #{location_string}" unless start_column && rest
|
25
|
+
|
26
|
+
end_line, end_column = rest.split(":", 2)
|
27
|
+
raise LocationError, "Invalid location string: #{location_string}" unless end_line && end_column
|
28
|
+
|
29
|
+
new(file, start_line.to_i, start_column.to_i, end_line.to_i, end_column.to_i)
|
30
|
+
end
|
31
|
+
|
32
|
+
sig { params(file: String, location: Prism::Location).returns(Location) }
|
33
|
+
def from_prism(file, location)
|
34
|
+
new(file, location.start_line, location.start_column, location.end_line, location.end_column)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
sig { returns(String) }
|
39
|
+
attr_reader :file
|
40
|
+
|
41
|
+
sig { returns(Integer) }
|
42
|
+
attr_reader :start_line, :start_column, :end_line, :end_column
|
43
|
+
|
44
|
+
sig do
|
45
|
+
params(
|
46
|
+
file: String,
|
47
|
+
start_line: Integer,
|
48
|
+
start_column: Integer,
|
49
|
+
end_line: Integer,
|
50
|
+
end_column: Integer,
|
51
|
+
).void
|
52
|
+
end
|
53
|
+
def initialize(file, start_line, start_column, end_line, end_column)
|
54
|
+
@file = file
|
55
|
+
@start_line = start_line
|
56
|
+
@start_column = start_column
|
57
|
+
@end_line = end_line
|
58
|
+
@end_column = end_column
|
59
|
+
end
|
60
|
+
|
61
|
+
sig { params(other: Location).returns(T::Boolean) }
|
62
|
+
def include?(other)
|
63
|
+
return false unless @file == other.file
|
64
|
+
return false if @start_line > other.start_line
|
65
|
+
return false if @start_line == other.start_line && @start_column > other.start_column
|
66
|
+
return false if @end_line < other.end_line
|
67
|
+
return false if @end_line == other.end_line && @end_column < other.end_column
|
68
|
+
|
69
|
+
true
|
70
|
+
end
|
71
|
+
|
72
|
+
sig { override.params(other: BasicObject).returns(T.nilable(Integer)) }
|
73
|
+
def <=>(other)
|
74
|
+
return unless Location === other
|
75
|
+
|
76
|
+
to_s <=> other.to_s
|
77
|
+
end
|
78
|
+
|
79
|
+
sig { returns(String) }
|
80
|
+
def to_s
|
81
|
+
"#{@file}:#{@start_line}:#{@start_column}-#{@end_line}:#{@end_column}"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,246 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Spoom
|
5
|
+
class Model
|
6
|
+
# Populate a Model by visiting the nodes from a Ruby file
|
7
|
+
class Builder < NamespaceVisitor
|
8
|
+
extend T::Sig
|
9
|
+
|
10
|
+
sig { params(model: Model, file: String).void }
|
11
|
+
def initialize(model, file)
|
12
|
+
super()
|
13
|
+
|
14
|
+
@model = model
|
15
|
+
@file = file
|
16
|
+
@namespace_nesting = T.let([], T::Array[Namespace])
|
17
|
+
@visibility_stack = T.let([Visibility::Public], T::Array[Visibility])
|
18
|
+
@last_sigs = T.let([], T::Array[Sig])
|
19
|
+
end
|
20
|
+
|
21
|
+
# Classes
|
22
|
+
|
23
|
+
sig { override.params(node: Prism::ClassNode).void }
|
24
|
+
def visit_class_node(node)
|
25
|
+
@namespace_nesting << Class.new(
|
26
|
+
@model.register_symbol(@names_nesting.join("::")),
|
27
|
+
owner: @namespace_nesting.last,
|
28
|
+
location: node_location(node),
|
29
|
+
superclass_name: node.superclass&.slice,
|
30
|
+
)
|
31
|
+
@visibility_stack << Visibility::Public
|
32
|
+
super
|
33
|
+
@visibility_stack.pop
|
34
|
+
@namespace_nesting.pop
|
35
|
+
@last_sigs.clear
|
36
|
+
end
|
37
|
+
|
38
|
+
sig { override.params(node: Prism::SingletonClassNode).void }
|
39
|
+
def visit_singleton_class_node(node)
|
40
|
+
@namespace_nesting << SingletonClass.new(
|
41
|
+
@model.register_symbol(@names_nesting.join("::")),
|
42
|
+
owner: @namespace_nesting.last,
|
43
|
+
location: node_location(node),
|
44
|
+
)
|
45
|
+
@visibility_stack << Visibility::Public
|
46
|
+
super
|
47
|
+
@visibility_stack.pop
|
48
|
+
@namespace_nesting.pop
|
49
|
+
@last_sigs.clear
|
50
|
+
end
|
51
|
+
|
52
|
+
# Modules
|
53
|
+
|
54
|
+
sig { override.params(node: Prism::ModuleNode).void }
|
55
|
+
def visit_module_node(node)
|
56
|
+
@namespace_nesting << Module.new(
|
57
|
+
@model.register_symbol(@names_nesting.join("::")),
|
58
|
+
owner: @namespace_nesting.last,
|
59
|
+
location: node_location(node),
|
60
|
+
)
|
61
|
+
@visibility_stack << Visibility::Public
|
62
|
+
super
|
63
|
+
@visibility_stack.pop
|
64
|
+
@namespace_nesting.pop
|
65
|
+
@last_sigs.clear
|
66
|
+
end
|
67
|
+
|
68
|
+
# Constants
|
69
|
+
|
70
|
+
sig { override.params(node: Prism::ConstantPathWriteNode).void }
|
71
|
+
def visit_constant_path_write_node(node)
|
72
|
+
@last_sigs.clear
|
73
|
+
|
74
|
+
name = node.target.slice
|
75
|
+
full_name = if name.start_with?("::")
|
76
|
+
name.delete_prefix("::")
|
77
|
+
else
|
78
|
+
[*@names_nesting, name].join("::")
|
79
|
+
end
|
80
|
+
|
81
|
+
Constant.new(
|
82
|
+
@model.register_symbol(full_name),
|
83
|
+
owner: @namespace_nesting.last,
|
84
|
+
location: node_location(node),
|
85
|
+
value: node.value.slice,
|
86
|
+
)
|
87
|
+
|
88
|
+
super
|
89
|
+
end
|
90
|
+
|
91
|
+
sig { override.params(node: Prism::ConstantWriteNode).void }
|
92
|
+
def visit_constant_write_node(node)
|
93
|
+
@last_sigs.clear
|
94
|
+
|
95
|
+
Constant.new(
|
96
|
+
@model.register_symbol([*@names_nesting, node.name.to_s].join("::")),
|
97
|
+
owner: @namespace_nesting.last,
|
98
|
+
location: node_location(node),
|
99
|
+
value: node.value.slice,
|
100
|
+
)
|
101
|
+
|
102
|
+
super
|
103
|
+
end
|
104
|
+
|
105
|
+
sig { override.params(node: Prism::MultiWriteNode).void }
|
106
|
+
def visit_multi_write_node(node)
|
107
|
+
@last_sigs.clear
|
108
|
+
|
109
|
+
node.lefts.each do |const|
|
110
|
+
case const
|
111
|
+
when Prism::ConstantTargetNode, Prism::ConstantPathTargetNode
|
112
|
+
Constant.new(
|
113
|
+
@model.register_symbol([*@names_nesting, const.slice].join("::")),
|
114
|
+
owner: @namespace_nesting.last,
|
115
|
+
location: node_location(const),
|
116
|
+
value: node.value.slice,
|
117
|
+
)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
super
|
122
|
+
end
|
123
|
+
|
124
|
+
# Methods
|
125
|
+
|
126
|
+
sig { override.params(node: Prism::DefNode).void }
|
127
|
+
def visit_def_node(node)
|
128
|
+
recv = node.receiver
|
129
|
+
|
130
|
+
if !recv || recv.is_a?(Prism::SelfNode)
|
131
|
+
Method.new(
|
132
|
+
@model.register_symbol([*@names_nesting, node.name.to_s].join("::")),
|
133
|
+
owner: @namespace_nesting.last,
|
134
|
+
location: node_location(node),
|
135
|
+
visibility: current_visibility,
|
136
|
+
sigs: collect_sigs,
|
137
|
+
)
|
138
|
+
end
|
139
|
+
|
140
|
+
super
|
141
|
+
end
|
142
|
+
|
143
|
+
# Accessors
|
144
|
+
|
145
|
+
sig { override.params(node: Prism::CallNode).void }
|
146
|
+
def visit_call_node(node)
|
147
|
+
return if node.receiver && !node.receiver.is_a?(Prism::SelfNode)
|
148
|
+
|
149
|
+
current_namespace = @namespace_nesting.last
|
150
|
+
|
151
|
+
case node.name
|
152
|
+
when :attr_accessor
|
153
|
+
sigs = collect_sigs
|
154
|
+
node.arguments&.arguments&.each do |arg|
|
155
|
+
next unless arg.is_a?(Prism::SymbolNode)
|
156
|
+
|
157
|
+
AttrAccessor.new(
|
158
|
+
@model.register_symbol([*@names_nesting, arg.slice.delete_prefix(":")].join("::")),
|
159
|
+
owner: current_namespace,
|
160
|
+
location: node_location(arg),
|
161
|
+
visibility: current_visibility,
|
162
|
+
sigs: sigs,
|
163
|
+
)
|
164
|
+
end
|
165
|
+
when :attr_reader
|
166
|
+
sigs = collect_sigs
|
167
|
+
node.arguments&.arguments&.each do |arg|
|
168
|
+
next unless arg.is_a?(Prism::SymbolNode)
|
169
|
+
|
170
|
+
AttrReader.new(
|
171
|
+
@model.register_symbol([*@names_nesting, arg.slice.delete_prefix(":")].join("::")),
|
172
|
+
owner: current_namespace,
|
173
|
+
location: node_location(arg),
|
174
|
+
visibility: current_visibility,
|
175
|
+
sigs: sigs,
|
176
|
+
)
|
177
|
+
end
|
178
|
+
when :attr_writer
|
179
|
+
sigs = collect_sigs
|
180
|
+
node.arguments&.arguments&.each do |arg|
|
181
|
+
next unless arg.is_a?(Prism::SymbolNode)
|
182
|
+
|
183
|
+
AttrWriter.new(
|
184
|
+
@model.register_symbol([*@names_nesting, arg.slice.delete_prefix(":")].join("::")),
|
185
|
+
owner: current_namespace,
|
186
|
+
location: node_location(arg),
|
187
|
+
visibility: current_visibility,
|
188
|
+
sigs: sigs,
|
189
|
+
)
|
190
|
+
end
|
191
|
+
when :include
|
192
|
+
node.arguments&.arguments&.each do |arg|
|
193
|
+
next unless arg.is_a?(Prism::ConstantReadNode) || arg.is_a?(Prism::ConstantPathNode)
|
194
|
+
next unless current_namespace
|
195
|
+
|
196
|
+
current_namespace.mixins << Include.new(arg.slice)
|
197
|
+
end
|
198
|
+
when :prepend
|
199
|
+
node.arguments&.arguments&.each do |arg|
|
200
|
+
next unless arg.is_a?(Prism::ConstantReadNode) || arg.is_a?(Prism::ConstantPathNode)
|
201
|
+
next unless current_namespace
|
202
|
+
|
203
|
+
current_namespace.mixins << Prepend.new(arg.slice)
|
204
|
+
end
|
205
|
+
when :extend
|
206
|
+
node.arguments&.arguments&.each do |arg|
|
207
|
+
next unless arg.is_a?(Prism::ConstantReadNode) || arg.is_a?(Prism::ConstantPathNode)
|
208
|
+
next unless current_namespace
|
209
|
+
|
210
|
+
current_namespace.mixins << Extend.new(arg.slice)
|
211
|
+
end
|
212
|
+
when :public, :private, :protected
|
213
|
+
@visibility_stack << Visibility.from_serialized(node.name.to_s)
|
214
|
+
if node.arguments
|
215
|
+
super
|
216
|
+
@visibility_stack.pop
|
217
|
+
end
|
218
|
+
when :sig
|
219
|
+
@last_sigs << Sig.new(node.slice)
|
220
|
+
else
|
221
|
+
@last_sigs.clear
|
222
|
+
super
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
private
|
227
|
+
|
228
|
+
sig { returns(Visibility) }
|
229
|
+
def current_visibility
|
230
|
+
T.must(@visibility_stack.last)
|
231
|
+
end
|
232
|
+
|
233
|
+
sig { returns(T::Array[Sig]) }
|
234
|
+
def collect_sigs
|
235
|
+
sigs = @last_sigs
|
236
|
+
@last_sigs = []
|
237
|
+
sigs
|
238
|
+
end
|
239
|
+
|
240
|
+
sig { params(node: Prism::Node).returns(Location) }
|
241
|
+
def node_location(node)
|
242
|
+
Location.from_prism(@file, node.location)
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|