spoom 1.2.1 → 1.2.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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