spoom 1.3.2 → 1.3.3

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