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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/lib/spoom/cli/deadcode.rb +21 -17
  3. data/lib/spoom/deadcode/index.rb +178 -10
  4. data/lib/spoom/deadcode/indexer.rb +14 -435
  5. data/lib/spoom/deadcode/plugins/action_mailer.rb +3 -3
  6. data/lib/spoom/deadcode/plugins/action_mailer_preview.rb +9 -3
  7. data/lib/spoom/deadcode/plugins/actionpack.rb +12 -9
  8. data/lib/spoom/deadcode/plugins/active_model.rb +8 -8
  9. data/lib/spoom/deadcode/plugins/active_record.rb +5 -5
  10. data/lib/spoom/deadcode/plugins/active_support.rb +4 -4
  11. data/lib/spoom/deadcode/plugins/base.rb +70 -57
  12. data/lib/spoom/deadcode/plugins/graphql.rb +8 -8
  13. data/lib/spoom/deadcode/plugins/minitest.rb +4 -3
  14. data/lib/spoom/deadcode/plugins/namespaces.rb +9 -12
  15. data/lib/spoom/deadcode/plugins/rails.rb +9 -9
  16. data/lib/spoom/deadcode/plugins/rubocop.rb +13 -17
  17. data/lib/spoom/deadcode/plugins/ruby.rb +9 -9
  18. data/lib/spoom/deadcode/plugins/sorbet.rb +15 -18
  19. data/lib/spoom/deadcode/plugins/thor.rb +5 -4
  20. data/lib/spoom/deadcode/plugins.rb +4 -5
  21. data/lib/spoom/deadcode/remover.rb +7 -7
  22. data/lib/spoom/deadcode/send.rb +1 -0
  23. data/lib/spoom/deadcode.rb +4 -73
  24. data/lib/spoom/location.rb +84 -0
  25. data/lib/spoom/model/builder.rb +246 -0
  26. data/lib/spoom/model/model.rb +328 -0
  27. data/lib/spoom/model/namespace_visitor.rb +50 -0
  28. data/lib/spoom/model/reference.rb +49 -0
  29. data/lib/spoom/model/references_visitor.rb +200 -0
  30. data/lib/spoom/model.rb +10 -0
  31. data/lib/spoom/parse.rb +28 -0
  32. data/lib/spoom/poset.rb +197 -0
  33. data/lib/spoom/sorbet/errors.rb +5 -3
  34. data/lib/spoom/sorbet/lsp/errors.rb +1 -1
  35. data/lib/spoom/sorbet.rb +1 -1
  36. data/lib/spoom/version.rb +1 -1
  37. data/lib/spoom/visitor.rb +755 -0
  38. data/lib/spoom.rb +2 -0
  39. metadata +20 -13
  40. data/lib/spoom/deadcode/location.rb +0 -86
  41. data/lib/spoom/deadcode/reference.rb +0 -34
  42. data/lib/spoom/deadcode/visitor.rb +0 -755
@@ -4,84 +4,15 @@
4
4
  require "erubi"
5
5
  require "prism"
6
6
 
7
- require_relative "deadcode/visitor"
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