spoom 1.2.1 → 1.2.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.
@@ -0,0 +1,403 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Spoom
5
+ module Deadcode
6
+ class Indexer < SyntaxTree::Visitor
7
+ extend T::Sig
8
+
9
+ sig { returns(String) }
10
+ attr_reader :path, :file_name
11
+
12
+ sig { returns(Index) }
13
+ attr_reader :index
14
+
15
+ sig { params(path: String, source: String, index: Index, plugins: T::Array[Plugins::Base]).void }
16
+ def initialize(path, source, index, plugins: [])
17
+ super()
18
+
19
+ @path = path
20
+ @file_name = T.let(File.basename(path), String)
21
+ @source = source
22
+ @index = index
23
+ @plugins = plugins
24
+ @previous_node = T.let(nil, T.nilable(SyntaxTree::Node))
25
+ @names_nesting = T.let([], T::Array[String])
26
+ @nodes_nesting = T.let([], T::Array[SyntaxTree::Node])
27
+ @in_const_field = T.let(false, T::Boolean)
28
+ @in_opassign = T.let(false, T::Boolean)
29
+ @in_symbol_literal = T.let(false, T::Boolean)
30
+ end
31
+
32
+ # Visit
33
+
34
+ sig { override.params(node: T.nilable(SyntaxTree::Node)).void }
35
+ def visit(node)
36
+ return unless node
37
+
38
+ @nodes_nesting << node
39
+ super
40
+ @nodes_nesting.pop
41
+ @previous_node = node
42
+ end
43
+
44
+ sig { override.params(node: SyntaxTree::AliasNode).void }
45
+ def visit_alias(node)
46
+ reference_method(node_string(node.right), node)
47
+ end
48
+
49
+ sig { override.params(node: SyntaxTree::ARef).void }
50
+ def visit_aref(node)
51
+ super
52
+
53
+ reference_method("[]", node)
54
+ end
55
+
56
+ sig { override.params(node: SyntaxTree::ARefField).void }
57
+ def visit_aref_field(node)
58
+ super
59
+
60
+ reference_method("[]=", node)
61
+ end
62
+
63
+ sig { override.params(node: SyntaxTree::ArgBlock).void }
64
+ def visit_arg_block(node)
65
+ value = node.value
66
+
67
+ case value
68
+ when SyntaxTree::SymbolLiteral
69
+ # If the block call is something like `x.select(&:foo)`, we need to reference the `foo` method
70
+ reference_method(symbol_string(value), node)
71
+ when SyntaxTree::VCall
72
+ # If the block call is something like `x.select { ... }`, we need to visit the block
73
+ super
74
+ end
75
+ end
76
+
77
+ sig { override.params(node: SyntaxTree::Binary).void }
78
+ def visit_binary(node)
79
+ super
80
+
81
+ op = node.operator
82
+
83
+ # Reference the operator itself
84
+ reference_method(op.to_s, node)
85
+
86
+ case op
87
+ when :<, :>, :<=, :>=
88
+ # For comparison operators, we also reference the `<=>` method
89
+ reference_method("<=>", node)
90
+ end
91
+ end
92
+
93
+ sig { override.params(node: SyntaxTree::CallNode).void }
94
+ def visit_call(node)
95
+ visit_send(
96
+ Send.new(
97
+ node: node,
98
+ name: node_string(node.message),
99
+ recv: node.receiver,
100
+ args: call_args(node.arguments),
101
+ ),
102
+ )
103
+ end
104
+
105
+ sig { override.params(node: SyntaxTree::ClassDeclaration).void }
106
+ def visit_class(node)
107
+ const_name = node_string(node.constant)
108
+ @names_nesting << const_name
109
+ define_class(T.must(const_name.split("::").last), @names_nesting.join("::"), node)
110
+
111
+ # We do not call `super` here because we don't want to visit the `constant` again
112
+ visit(node.superclass) if node.superclass
113
+ visit(node.bodystmt)
114
+
115
+ @names_nesting.pop
116
+ end
117
+
118
+ sig { override.params(node: SyntaxTree::Command).void }
119
+ def visit_command(node)
120
+ visit_send(
121
+ Send.new(
122
+ node: node,
123
+ name: node_string(node.message),
124
+ args: call_args(node.arguments),
125
+ block: node.block,
126
+ ),
127
+ )
128
+ end
129
+
130
+ sig { override.params(node: SyntaxTree::CommandCall).void }
131
+ def visit_command_call(node)
132
+ visit_send(
133
+ Send.new(
134
+ node: node,
135
+ name: node_string(node.message),
136
+ recv: node.receiver,
137
+ args: call_args(node.arguments),
138
+ block: node.block,
139
+ ),
140
+ )
141
+ end
142
+
143
+ sig { override.params(node: SyntaxTree::Const).void }
144
+ def visit_const(node)
145
+ reference_constant(node.value, node) unless @in_symbol_literal
146
+ end
147
+
148
+ sig { override.params(node: SyntaxTree::ConstPathField).void }
149
+ def visit_const_path_field(node)
150
+ # We do not call `super` here because we don't want to visit the `constant` again
151
+ visit(node.parent)
152
+
153
+ name = node.constant.value
154
+ full_name = [*@names_nesting, node_string(node.parent), name].join("::")
155
+ define_constant(name, full_name, node)
156
+ end
157
+
158
+ sig { override.params(node: SyntaxTree::DefNode).void }
159
+ def visit_def(node)
160
+ super
161
+
162
+ name = node_string(node.name)
163
+ define_method(name, [*@names_nesting, name].join("::"), node)
164
+ end
165
+
166
+ sig { override.params(node: SyntaxTree::Field).void }
167
+ def visit_field(node)
168
+ visit(node.parent)
169
+
170
+ name = node.name
171
+ case name
172
+ when SyntaxTree::Const
173
+ name = name.value
174
+ full_name = [*@names_nesting, node_string(node.parent), name].join("::")
175
+ define_constant(name, full_name, node)
176
+ when SyntaxTree::Ident
177
+ reference_method(name.value, node) if @in_opassign
178
+ reference_method("#{name.value}=", node)
179
+ end
180
+ end
181
+
182
+ sig { override.params(node: SyntaxTree::ModuleDeclaration).void }
183
+ def visit_module(node)
184
+ const_name = node_string(node.constant)
185
+ @names_nesting << const_name
186
+ define_module(T.must(const_name.split("::").last), @names_nesting.join("::"), node)
187
+
188
+ # We do not call `super` here because we don't want to visit the `constant` again
189
+ visit(node.bodystmt)
190
+
191
+ @names_nesting.pop
192
+ end
193
+
194
+ sig { override.params(node: SyntaxTree::OpAssign).void }
195
+ def visit_opassign(node)
196
+ # Both `FOO = x` and `FOO += x` yield a VarField node, but the former is a constant definition and the latter is
197
+ # a constant reference. We need to distinguish between the two cases.
198
+ @in_opassign = true
199
+ super
200
+ @in_opassign = false
201
+ end
202
+
203
+ sig { params(send: Send).void }
204
+ def visit_send(send)
205
+ visit(send.recv)
206
+
207
+ case send.name
208
+ when "attr_reader"
209
+ send.args.each do |arg|
210
+ next unless arg.is_a?(SyntaxTree::SymbolLiteral)
211
+
212
+ name = symbol_string(arg)
213
+ define_attr_reader(name, [*@names_nesting, name].join("::"), arg)
214
+ end
215
+ when "attr_writer"
216
+ send.args.each do |arg|
217
+ next unless arg.is_a?(SyntaxTree::SymbolLiteral)
218
+
219
+ name = symbol_string(arg)
220
+ define_attr_writer("#{name}=", "#{[*@names_nesting, name].join("::")}=", arg)
221
+ end
222
+ when "attr_accessor"
223
+ send.args.each do |arg|
224
+ next unless arg.is_a?(SyntaxTree::SymbolLiteral)
225
+
226
+ name = symbol_string(arg)
227
+ full_name = [*@names_nesting, name].join("::")
228
+ define_attr_reader(name, full_name, arg)
229
+ define_attr_writer("#{name}=", "#{full_name}=", arg)
230
+ end
231
+ else
232
+ @plugins.each do |plugin|
233
+ plugin.on_send(self, send)
234
+ end
235
+
236
+ reference_method(send.name, send.node)
237
+ visit_all(send.args)
238
+ visit(send.block)
239
+ end
240
+ end
241
+
242
+ sig { override.params(node: SyntaxTree::SymbolLiteral).void }
243
+ def visit_symbol_literal(node)
244
+ # Something like `:FOO` will yield a Const node but we do not want to treat it as a constant reference.
245
+ # So we need to distinguish between the two cases.
246
+ @in_symbol_literal = true
247
+ super
248
+ @in_symbol_literal = false
249
+ end
250
+
251
+ sig { override.params(node: SyntaxTree::TopConstField).void }
252
+ def visit_top_const_field(node)
253
+ define_constant(node.constant.value, node.constant.value, node)
254
+ end
255
+
256
+ sig { override.params(node: SyntaxTree::VarField).void }
257
+ def visit_var_field(node)
258
+ value = node.value
259
+ case value
260
+ when SyntaxTree::Const
261
+ if @in_opassign
262
+ reference_constant(value.value, node)
263
+ else
264
+ name = value.value
265
+ define_constant(name, [*@names_nesting, name].join("::"), node)
266
+ end
267
+ when SyntaxTree::Ident
268
+ reference_method(value.value, node) if @in_opassign
269
+ reference_method("#{value.value}=", node)
270
+ end
271
+ end
272
+
273
+ sig { override.params(node: SyntaxTree::VCall).void }
274
+ def visit_vcall(node)
275
+ visit_send(Send.new(node: node, name: node_string(node.value)))
276
+ end
277
+
278
+ # Definition indexing
279
+
280
+ sig { params(name: String, full_name: String, node: SyntaxTree::Node).void }
281
+ def define_attr_reader(name, full_name, node)
282
+ definition = Definition.new(
283
+ kind: Definition::Kind::AttrReader,
284
+ name: name,
285
+ full_name: full_name,
286
+ location: node_location(node),
287
+ )
288
+ @index.define(definition)
289
+ @plugins.each { |plugin| plugin.on_define_accessor(self, definition) }
290
+ end
291
+
292
+ sig { params(name: String, full_name: String, node: SyntaxTree::Node).void }
293
+ def define_attr_writer(name, full_name, node)
294
+ definition = Definition.new(
295
+ kind: Definition::Kind::AttrWriter,
296
+ name: name,
297
+ full_name: full_name,
298
+ location: node_location(node),
299
+ )
300
+ @index.define(definition)
301
+ @plugins.each { |plugin| plugin.on_define_accessor(self, definition) }
302
+ end
303
+
304
+ sig { params(name: String, full_name: String, node: SyntaxTree::Node).void }
305
+ def define_class(name, full_name, node)
306
+ definition = Definition.new(
307
+ kind: Definition::Kind::Class,
308
+ name: name,
309
+ full_name: full_name,
310
+ location: node_location(node),
311
+ )
312
+ @index.define(definition)
313
+ @plugins.each { |plugin| plugin.on_define_class(self, definition) }
314
+ end
315
+
316
+ sig { params(name: String, full_name: String, node: SyntaxTree::Node).void }
317
+ def define_constant(name, full_name, node)
318
+ definition = Definition.new(
319
+ kind: Definition::Kind::Constant,
320
+ name: name,
321
+ full_name: full_name,
322
+ location: node_location(node),
323
+ )
324
+ @index.define(definition)
325
+ @plugins.each { |plugin| plugin.on_define_constant(self, definition) }
326
+ end
327
+
328
+ sig { params(name: String, full_name: String, node: SyntaxTree::Node).void }
329
+ def define_method(name, full_name, node)
330
+ definition = Definition.new(
331
+ kind: Definition::Kind::Method,
332
+ name: name,
333
+ full_name: full_name,
334
+ location: node_location(node),
335
+ )
336
+ @index.define(definition)
337
+ @plugins.each { |plugin| plugin.on_define_method(self, definition) }
338
+ end
339
+
340
+ sig { params(name: String, full_name: String, node: SyntaxTree::Node).void }
341
+ def define_module(name, full_name, node)
342
+ definition = Definition.new(
343
+ kind: Definition::Kind::Module,
344
+ name: name,
345
+ full_name: full_name,
346
+ location: node_location(node),
347
+ )
348
+ @index.define(definition)
349
+ @plugins.each { |plugin| plugin.on_define_module(self, definition) }
350
+ end
351
+
352
+ # Reference indexing
353
+
354
+ sig { params(name: String, node: SyntaxTree::Node).void }
355
+ def reference_constant(name, node)
356
+ @index.reference(Reference.new(name: name, kind: Reference::Kind::Constant, location: node_location(node)))
357
+ end
358
+
359
+ sig { params(name: String, node: SyntaxTree::Node).void }
360
+ def reference_method(name, node)
361
+ @index.reference(Reference.new(name: name, kind: Reference::Kind::Method, location: node_location(node)))
362
+ end
363
+
364
+ # Node utils
365
+
366
+ sig { params(node: T.any(Symbol, SyntaxTree::Node)).returns(String) }
367
+ def node_string(node)
368
+ case node
369
+ when Symbol
370
+ node.to_s
371
+ else
372
+ T.must(@source[node.location.start_char...node.location.end_char])
373
+ end
374
+ end
375
+
376
+ sig { params(node: SyntaxTree::Node).returns(Location) }
377
+ def node_location(node)
378
+ Location.from_syntax_tree(@path, node.location)
379
+ end
380
+
381
+ sig { params(node: SyntaxTree::Node).returns(String) }
382
+ def symbol_string(node)
383
+ node_string(node).delete_prefix(":")
384
+ end
385
+
386
+ sig do
387
+ params(
388
+ node: T.any(SyntaxTree::Args, SyntaxTree::ArgParen, SyntaxTree::ArgsForward, NilClass),
389
+ ).returns(T::Array[SyntaxTree::Node])
390
+ end
391
+ def call_args(node)
392
+ case node
393
+ when SyntaxTree::ArgParen
394
+ call_args(node.arguments)
395
+ when SyntaxTree::Args
396
+ node.parts
397
+ else
398
+ []
399
+ end
400
+ end
401
+ end
402
+ end
403
+ end
@@ -0,0 +1,58 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Spoom
5
+ module Deadcode
6
+ class Location
7
+ extend T::Sig
8
+
9
+ include Comparable
10
+
11
+ class LocationError < Spoom::Error; end
12
+
13
+ class << self
14
+ extend T::Sig
15
+
16
+ sig { params(file: String, location: SyntaxTree::Location).returns(Location) }
17
+ def from_syntax_tree(file, location)
18
+ new(file, location.start_line, location.start_column, location.end_line, location.end_column)
19
+ end
20
+ end
21
+
22
+ sig { returns(String) }
23
+ attr_reader :file
24
+
25
+ sig { returns(Integer) }
26
+ attr_reader :start_line, :start_column, :end_line, :end_column
27
+
28
+ sig do
29
+ params(
30
+ file: String,
31
+ start_line: Integer,
32
+ start_column: Integer,
33
+ end_line: Integer,
34
+ end_column: Integer,
35
+ ).void
36
+ end
37
+ def initialize(file, start_line, start_column, end_line, end_column)
38
+ @file = file
39
+ @start_line = start_line
40
+ @start_column = start_column
41
+ @end_line = end_line
42
+ @end_column = end_column
43
+ end
44
+
45
+ sig { override.params(other: BasicObject).returns(T.nilable(Integer)) }
46
+ def <=>(other)
47
+ return unless Location === other
48
+
49
+ to_s <=> other.to_s
50
+ end
51
+
52
+ sig { returns(String) }
53
+ def to_s
54
+ "#{@file}:#{@start_line}:#{@start_column}-#{@end_line}:#{@end_column}"
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,201 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "set"
5
+
6
+ module Spoom
7
+ module Deadcode
8
+ module Plugins
9
+ class Base
10
+ extend T::Sig
11
+ extend T::Helpers
12
+
13
+ abstract!
14
+
15
+ class << self
16
+ extend T::Sig
17
+
18
+ # Plugins DSL
19
+
20
+ # Mark methods matching `names` as ignored.
21
+ #
22
+ # Names can be either strings or regexps:
23
+ #
24
+ # ~~~rb
25
+ # class MyPlugin < Spoom::Deadcode::Plugins::Base
26
+ # ignore_method_names(
27
+ # "foo",
28
+ # "bar",
29
+ # /baz.*/,
30
+ # )
31
+ # end
32
+ # ~~~
33
+ sig { params(names: T.any(String, Regexp)).void }
34
+ def ignore_method_names(*names)
35
+ save_names_and_patterns(names, :@ignored_method_names, :@ignored_method_patterns)
36
+ end
37
+
38
+ private
39
+
40
+ sig { params(names: T::Array[T.any(String, Regexp)], names_variable: Symbol, patterns_variable: Symbol).void }
41
+ def save_names_and_patterns(names, names_variable, patterns_variable)
42
+ ignored_names = instance_variable_set(names_variable, Set.new)
43
+ ignored_patterns = instance_variable_set(patterns_variable, [])
44
+
45
+ names.each do |name|
46
+ case name
47
+ when String
48
+ ignored_names << name
49
+ when Regexp
50
+ ignored_patterns << name
51
+ end
52
+ end
53
+ end
54
+ end
55
+
56
+ # Indexing event methods
57
+
58
+ # Called when an accessor is defined.
59
+ #
60
+ # Will be called when the indexer processes a `attr_reader`, `attr_writer` or `attr_accessor` node.
61
+ # Note that when this method is called, the definition for the node has already been added to the index.
62
+ # It is still possible to ignore it from the plugin:
63
+ #
64
+ # ~~~rb
65
+ # class MyPlugin < Spoom::Deadcode::Plugins::Base
66
+ # def on_define_accessor(indexer, definition)
67
+ # definition.ignored! if definition.name == "foo"
68
+ # end
69
+ # end
70
+ # ~~~
71
+ sig { params(indexer: Indexer, definition: Definition).void }
72
+ def on_define_accessor(indexer, definition)
73
+ # no-op
74
+ end
75
+
76
+ # Called when a class is defined.
77
+ #
78
+ # Will be called when the indexer processes a `class` node.
79
+ # Note that when this method is called, the definition for the node has already been added to the index.
80
+ # It is still possible to ignore it from the plugin:
81
+ #
82
+ # ~~~rb
83
+ # class MyPlugin < Spoom::Deadcode::Plugins::Base
84
+ # def on_define_class(indexer, definition)
85
+ # definition.ignored! if definition.name == "Foo"
86
+ # end
87
+ # end
88
+ # ~~~
89
+ sig { params(indexer: Indexer, definition: Definition).void }
90
+ def on_define_class(indexer, definition)
91
+ # no-op
92
+ end
93
+
94
+ # Called when a constant is defined.
95
+ #
96
+ # Will be called when the indexer processes a `CONST =` node.
97
+ # Note that when this method is called, the definition for the node has already been added to the index.
98
+ # It is still possible to ignore it from the plugin:
99
+ #
100
+ # ~~~rb
101
+ # class MyPlugin < Spoom::Deadcode::Plugins::Base
102
+ # def on_define_constant(indexer, definition)
103
+ # definition.ignored! if definition.name == "FOO"
104
+ # end
105
+ # end
106
+ # ~~~
107
+ sig { params(indexer: Indexer, definition: Definition).void }
108
+ def on_define_constant(indexer, definition)
109
+ # no-op
110
+ end
111
+
112
+ # Called when a method is defined.
113
+ #
114
+ # Will be called when the indexer processes a `def` or `defs` node.
115
+ # Note that when this method is called, the definition for the node has already been added to the index.
116
+ # It is still possible to ignore it from the plugin:
117
+ #
118
+ # ~~~rb
119
+ # class MyPlugin < Spoom::Deadcode::Plugins::Base
120
+ # def on_define_method(indexer, definition)
121
+ # super # So the `ignore_method_names` DSL is still applied
122
+ #
123
+ # definition.ignored! if definition.name == "foo"
124
+ # end
125
+ # end
126
+ # ~~~
127
+ sig { params(indexer: Indexer, definition: Definition).void }
128
+ def on_define_method(indexer, definition)
129
+ definition.ignored! if ignored_method_name?(definition.name)
130
+ end
131
+
132
+ # Called when a module is defined.
133
+ #
134
+ # Will be called when the indexer processes a `module` node.
135
+ # Note that when this method is called, the definition for the node has already been added to the index.
136
+ # It is still possible to ignore it from the plugin:
137
+ #
138
+ # ~~~rb
139
+ # class MyPlugin < Spoom::Deadcode::Plugins::Base
140
+ # def on_define_module(indexer, definition)
141
+ # definition.ignored! if definition.name == "Foo"
142
+ # end
143
+ # end
144
+ # ~~~
145
+ sig { params(indexer: Indexer, definition: Definition).void }
146
+ def on_define_module(indexer, definition)
147
+ # no-op
148
+ end
149
+
150
+ # Called when a send is being processed
151
+ #
152
+ # ~~~rb
153
+ # class MyPlugin < Spoom::Deadcode::Plugins::Base
154
+ # def on_send(indexer, send)
155
+ # return unless send.name == "dsl_method"
156
+ # return if send.args.empty?
157
+ #
158
+ # method_name = indexer.node_string(send.args.first).delete_prefix(":")
159
+ # indexer.reference_method(method_name, send.node)
160
+ # end
161
+ # end
162
+ # ~~~
163
+ sig { params(indexer: Indexer, send: Send).void }
164
+ def on_send(indexer, send)
165
+ # no-op
166
+ end
167
+
168
+ private
169
+
170
+ sig { params(name: String).returns(T::Boolean) }
171
+ def ignored_method_name?(name)
172
+ ignored_name?(name, :@ignored_method_names, :@ignored_method_patterns)
173
+ end
174
+
175
+ sig { params(const: Symbol).returns(T::Set[String]) }
176
+ def names(const)
177
+ self.class.instance_variable_get(const) || Set.new
178
+ end
179
+
180
+ sig { params(name: String, names_variable: Symbol, patterns_variable: Symbol).returns(T::Boolean) }
181
+ def ignored_name?(name, names_variable, patterns_variable)
182
+ names(names_variable).include?(name) || patterns(patterns_variable).any? { |pattern| pattern.match?(name) }
183
+ end
184
+
185
+ sig { params(const: Symbol).returns(T::Array[Regexp]) }
186
+ def patterns(const)
187
+ self.class.instance_variable_get(const) || []
188
+ end
189
+
190
+ sig { params(indexer: Indexer, send: Send).void }
191
+ def reference_send_first_symbol_as_method(indexer, send)
192
+ first_arg = send.args.first
193
+ return unless first_arg.is_a?(SyntaxTree::SymbolLiteral)
194
+
195
+ name = indexer.node_string(first_arg.value)
196
+ indexer.reference_method(name, send.node)
197
+ end
198
+ end
199
+ end
200
+ end
201
+ end