spoom 1.3.2 → 1.4.0

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 +9 -9
  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 +31 -12
  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 +139 -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 +16 -9
  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,139 @@
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}`: missing file name" unless file
19
+
20
+ return new(file) if rest.nil?
21
+
22
+ start_line, rest = rest.split(":", 2)
23
+ if rest.nil?
24
+ start_line, end_line = T.must(start_line).split("-", 2)
25
+ raise LocationError, "Invalid location string `#{location_string}`: missing end line" unless end_line
26
+
27
+ return new(file, start_line: start_line.to_i, end_line: end_line.to_i) if end_line
28
+ end
29
+
30
+ start_column, rest = rest.split("-", 2)
31
+ raise LocationError, "Invalid location string `#{location_string}`: missing end line and column" if rest.nil?
32
+
33
+ end_line, end_column = rest.split(":", 2)
34
+ raise LocationError,
35
+ "Invalid location string `#{location_string}`: missing end column" unless end_line && end_column
36
+
37
+ new(
38
+ file,
39
+ start_line: start_line.to_i,
40
+ start_column: start_column.to_i,
41
+ end_line: end_line.to_i,
42
+ end_column: end_column.to_i,
43
+ )
44
+ end
45
+
46
+ sig { params(file: String, location: Prism::Location).returns(Location) }
47
+ def from_prism(file, location)
48
+ new(
49
+ file,
50
+ start_line: location.start_line,
51
+ start_column: location.start_column,
52
+ end_line: location.end_line,
53
+ end_column: location.end_column,
54
+ )
55
+ end
56
+ end
57
+
58
+ sig { returns(String) }
59
+ attr_reader :file
60
+
61
+ sig { returns(T.nilable(Integer)) }
62
+ attr_reader :start_line, :start_column, :end_line, :end_column
63
+
64
+ sig do
65
+ params(
66
+ file: String,
67
+ start_line: T.nilable(Integer),
68
+ start_column: T.nilable(Integer),
69
+ end_line: T.nilable(Integer),
70
+ end_column: T.nilable(Integer),
71
+ ).void
72
+ end
73
+ def initialize(file, start_line: nil, start_column: nil, end_line: nil, end_column: nil)
74
+ raise LocationError,
75
+ "Invalid location: end line is required if start line is provided" if start_line && !end_line
76
+ raise LocationError,
77
+ "Invalid location: start line is required if end line is provided" if !start_line && end_line
78
+ raise LocationError,
79
+ "Invalid location: end column is required if start column is provided" if start_column && !end_column
80
+ raise LocationError,
81
+ "Invalid location: start column is required if end column is provided" if !start_column && end_column
82
+ raise LocationError,
83
+ "Invalid location: lines are required if columns are provided" if start_column && !start_line
84
+
85
+ @file = file
86
+ @start_line = start_line
87
+ @start_column = start_column
88
+ @end_line = end_line
89
+ @end_column = end_column
90
+ end
91
+
92
+ sig { params(other: Location).returns(T::Boolean) }
93
+ def include?(other)
94
+ return false unless @file == other.file
95
+ return false if (@start_line || -Float::INFINITY) > (other.start_line || -Float::INFINITY)
96
+ return false if @start_line == other.start_line &&
97
+ (@start_column || -Float::INFINITY) > (other.start_column || -Float::INFINITY)
98
+ return false if (@end_line || Float::INFINITY) < (other.end_line || Float::INFINITY)
99
+ return false if @end_line == other.end_line &&
100
+ (@end_column || Float::INFINITY) < (other.end_column || Float::INFINITY)
101
+
102
+ true
103
+ end
104
+
105
+ sig { override.params(other: BasicObject).returns(T.nilable(Integer)) }
106
+ def <=>(other)
107
+ return unless Location === other
108
+
109
+ comparison_array_self = [
110
+ @file,
111
+ @start_line || -Float::INFINITY,
112
+ @start_column || -Float::INFINITY,
113
+ @end_line || Float::INFINITY,
114
+ @end_column || Float::INFINITY,
115
+ ]
116
+
117
+ comparison_array_other = [
118
+ other.file,
119
+ other.start_line || -Float::INFINITY,
120
+ other.start_column || -Float::INFINITY,
121
+ other.end_line || Float::INFINITY,
122
+ other.end_column || Float::INFINITY,
123
+ ]
124
+
125
+ comparison_array_self <=> comparison_array_other
126
+ end
127
+
128
+ sig { returns(String) }
129
+ def to_s
130
+ if @start_line && @start_column
131
+ "#{@file}:#{@start_line}:#{@start_column}-#{@end_line}:#{@end_column}"
132
+ elsif @start_line
133
+ "#{@file}:#{@start_line}-#{@end_line}"
134
+ else
135
+ @file
136
+ end
137
+ end
138
+ end
139
+ 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