spoom 1.7.2 → 1.7.4
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.
- checksums.yaml +4 -4
- data/Gemfile +2 -0
- data/lib/spoom/cli/helper.rb +1 -4
- data/lib/spoom/cli/srb/sigs.rb +22 -2
- data/lib/spoom/context/bundle.rb +1 -4
- data/lib/spoom/context/exec.rb +1 -4
- data/lib/spoom/context/file_system.rb +1 -4
- data/lib/spoom/context/git.rb +1 -4
- data/lib/spoom/context/sorbet.rb +1 -4
- data/lib/spoom/coverage/d3/base.rb +3 -6
- data/lib/spoom/coverage/d3/pie.rb +1 -4
- data/lib/spoom/coverage/d3/timeline.rb +4 -9
- data/lib/spoom/coverage/report.rb +7 -17
- data/lib/spoom/deadcode/plugins/active_job.rb +19 -0
- data/lib/spoom/deadcode/plugins/base.rb +1 -4
- data/lib/spoom/file_tree.rb +1 -4
- data/lib/spoom/model/model.rb +6 -14
- data/lib/spoom/model/namespace_visitor.rb +1 -4
- data/lib/spoom/poset.rb +3 -8
- data/lib/spoom/rbs.rb +30 -4
- data/lib/spoom/sorbet/lsp/structures.rb +3 -6
- data/lib/spoom/sorbet/metrics/code_metrics_visitor.rb +0 -3
- data/lib/spoom/sorbet/translate/rbs_comments_to_sorbet_sigs.rb +25 -8
- data/lib/spoom/sorbet/translate/sorbet_assertions_to_rbs_comments.rb +67 -8
- data/lib/spoom/sorbet/translate/sorbet_sigs_to_rbs_comments.rb +112 -44
- data/lib/spoom/sorbet/translate.rb +6 -6
- data/lib/spoom/source/rewriter.rb +4 -1
- data/lib/spoom/version.rb +1 -1
- data/rbi/spoom.rbi +94 -72
- metadata +3 -17
@@ -9,22 +9,75 @@ module Spoom
|
|
9
9
|
LINE_BREAK = "\n".ord #: Integer
|
10
10
|
|
11
11
|
# @override
|
12
|
-
#: (Prism::
|
13
|
-
def
|
14
|
-
|
15
|
-
|
12
|
+
#: (Prism::StatementsNode) -> void
|
13
|
+
def visit_statements_node(node)
|
14
|
+
node.body.each do |statement|
|
15
|
+
translated = maybe_translate_assertion(statement)
|
16
|
+
visit(statement) unless translated
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# @override
|
21
|
+
#: (Prism::IfNode) -> void
|
22
|
+
def visit_if_node(node)
|
23
|
+
if node.if_keyword_loc
|
24
|
+
# We do not translate assertions in ternary expressions to avoid altering the semantic of the code.
|
25
|
+
#
|
26
|
+
# For example:
|
27
|
+
# ```rb
|
28
|
+
# a = T.must(b) ? T.must(c) : T.must(d)
|
29
|
+
# ```
|
30
|
+
#
|
31
|
+
# would become
|
32
|
+
# ```rb
|
33
|
+
# a = T.must(b) ? T.must(c) : d #: !nil
|
34
|
+
# ```
|
35
|
+
#
|
36
|
+
# which does not match the original intent.
|
37
|
+
super
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
#: (Prism::Node) -> bool
|
44
|
+
def maybe_translate_assertion(node)
|
45
|
+
node = case node
|
46
|
+
when Prism::MultiWriteNode,
|
47
|
+
Prism::ClassVariableWriteNode, Prism::ClassVariableAndWriteNode, Prism::ClassVariableOperatorWriteNode, Prism::ClassVariableOrWriteNode,
|
48
|
+
Prism::ConstantWriteNode, Prism::ConstantAndWriteNode, Prism::ConstantOperatorWriteNode, Prism::ConstantOrWriteNode,
|
49
|
+
Prism::ConstantPathWriteNode, Prism::ConstantPathAndWriteNode, Prism::ConstantPathOperatorWriteNode, Prism::ConstantPathOrWriteNode,
|
50
|
+
Prism::GlobalVariableWriteNode, Prism::GlobalVariableAndWriteNode, Prism::GlobalVariableOperatorWriteNode, Prism::GlobalVariableOrWriteNode,
|
51
|
+
Prism::InstanceVariableWriteNode, Prism::InstanceVariableAndWriteNode, Prism::InstanceVariableOperatorWriteNode, Prism::InstanceVariableOrWriteNode,
|
52
|
+
Prism::LocalVariableWriteNode, Prism::LocalVariableAndWriteNode, Prism::LocalVariableOperatorWriteNode, Prism::LocalVariableOrWriteNode,
|
53
|
+
Prism::CallAndWriteNode, Prism::CallOperatorWriteNode, Prism::CallOrWriteNode
|
54
|
+
node.value
|
55
|
+
when Prism::CallNode
|
56
|
+
node
|
57
|
+
else
|
58
|
+
return false
|
59
|
+
end
|
60
|
+
|
61
|
+
return false unless node.is_a?(Prism::CallNode)
|
62
|
+
return false unless t_annotation?(node)
|
63
|
+
return false unless at_end_of_line?(node)
|
16
64
|
|
17
65
|
value = T.must(node.arguments&.arguments&.first)
|
18
66
|
rbs_annotation = build_rbs_annotation(node)
|
19
67
|
|
20
68
|
start_offset = node.location.start_offset
|
21
69
|
end_offset = node.location.end_offset
|
22
|
-
@rewriter << Source::Replace.new(start_offset, end_offset - 1, "#{dedent_value(node, value)} #{rbs_annotation}")
|
23
|
-
end
|
24
70
|
|
25
|
-
|
71
|
+
@rewriter << if node.name == :bind
|
72
|
+
Source::Replace.new(start_offset, end_offset - 1, rbs_annotation)
|
73
|
+
else
|
74
|
+
Source::Replace.new(start_offset, end_offset - 1, "#{dedent_value(node, value)} #{rbs_annotation}")
|
75
|
+
end
|
76
|
+
|
77
|
+
true
|
78
|
+
end
|
26
79
|
|
27
|
-
#: (Prism::CallNode) ->
|
80
|
+
#: (Prism::CallNode) -> String
|
28
81
|
def build_rbs_annotation(call)
|
29
82
|
case call.name
|
30
83
|
when :let
|
@@ -35,6 +88,10 @@ module Spoom
|
|
35
88
|
srb_type = call.arguments&.arguments&.last #: as !nil
|
36
89
|
rbs_type = RBI::Type.parse_node(srb_type).rbs_string
|
37
90
|
"#: as #{rbs_type}"
|
91
|
+
when :bind
|
92
|
+
srb_type = call.arguments&.arguments&.last #: as !nil
|
93
|
+
rbs_type = RBI::Type.parse_node(srb_type).rbs_string
|
94
|
+
"#: self as #{rbs_type}"
|
38
95
|
when :must
|
39
96
|
"#: as !nil"
|
40
97
|
when :unsafe
|
@@ -65,6 +122,8 @@ module Spoom
|
|
65
122
|
case node.name
|
66
123
|
when :let, :cast
|
67
124
|
return node.arguments&.arguments&.size == 2
|
125
|
+
when :bind
|
126
|
+
return node.arguments&.arguments&.size == 2 && node.arguments&.arguments&.first.is_a?(Prism::SelfNode)
|
68
127
|
when :must, :unsafe
|
69
128
|
return node.arguments&.arguments&.size == 1
|
70
129
|
end
|
@@ -7,14 +7,18 @@ module Spoom
|
|
7
7
|
# Converts all `sig` nodes to RBS comments in the given Ruby code.
|
8
8
|
# It also handles type members and class annotations.
|
9
9
|
class SorbetSigsToRBSComments < Translator
|
10
|
-
#: (String, file: String, positional_names: bool) -> void
|
11
|
-
def initialize(ruby_contents, file:, positional_names:)
|
10
|
+
#: (String, file: String, positional_names: bool, ?max_line_length: Integer?) -> void
|
11
|
+
def initialize(ruby_contents, file:, positional_names:, max_line_length: nil)
|
12
12
|
super(ruby_contents, file: file)
|
13
13
|
|
14
14
|
@positional_names = positional_names #: bool
|
15
|
-
@nesting = [] #: Array[Prism::ClassNode | Prism::ModuleNode | Prism::SingletonClassNode]
|
16
15
|
@last_sigs = [] #: Array[[Prism::CallNode, RBI::Sig]]
|
16
|
+
@class_annotations = [] #: Array[Prism::CallNode]
|
17
17
|
@type_members = [] #: Array[String]
|
18
|
+
@extend_t_helpers = [] #: Array[Prism::CallNode]
|
19
|
+
@extend_t_generics = [] #: Array[Prism::CallNode]
|
20
|
+
@seen_mixes_in_class_methods = false #: bool
|
21
|
+
@max_line_length = max_line_length #: Integer?
|
18
22
|
end
|
19
23
|
|
20
24
|
# @override
|
@@ -38,26 +42,23 @@ module Spoom
|
|
38
42
|
# @override
|
39
43
|
#: (Prism::DefNode) -> void
|
40
44
|
def visit_def_node(node)
|
41
|
-
|
42
|
-
return if
|
45
|
+
last_sigs = collect_last_sigs
|
46
|
+
return if last_sigs.empty?
|
47
|
+
return if last_sigs.any? { |_, sig| sig.is_abstract }
|
43
48
|
|
44
|
-
apply_member_annotations(
|
49
|
+
apply_member_annotations(last_sigs)
|
45
50
|
|
46
51
|
# Build the RBI::Method node so we can print the method signature as RBS.
|
47
52
|
builder = RBI::Parser::TreeBuilder.new(@ruby_contents, comments: [], file: @file)
|
48
53
|
builder.visit(node)
|
49
54
|
rbi_node = builder.tree.nodes.first #: as RBI::Method
|
50
55
|
|
51
|
-
|
52
|
-
out =
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
p.print("\n")
|
57
|
-
@rewriter << Source::Replace.new(node.location.start_offset, node.location.end_offset, out.string)
|
56
|
+
last_sigs.each do |node, sig|
|
57
|
+
out = rbs_print(node.location.start_column) do |printer|
|
58
|
+
printer.print_method_sig(rbi_node, sig)
|
59
|
+
end
|
60
|
+
@rewriter << Source::Replace.new(node.location.start_offset, node.location.end_offset, out)
|
58
61
|
end
|
59
|
-
|
60
|
-
@last_sigs.clear
|
61
62
|
end
|
62
63
|
|
63
64
|
# @override
|
@@ -71,7 +72,9 @@ module Spoom
|
|
71
72
|
when "extend"
|
72
73
|
visit_extend(node)
|
73
74
|
when "abstract!", "interface!", "sealed!", "final!", "requires_ancestor"
|
74
|
-
|
75
|
+
@class_annotations << node
|
76
|
+
when "mixes_in_class_methods"
|
77
|
+
@seen_mixes_in_class_methods = true
|
75
78
|
else
|
76
79
|
super
|
77
80
|
end
|
@@ -97,19 +100,39 @@ module Spoom
|
|
97
100
|
|
98
101
|
#: (Prism::ClassNode | Prism::ModuleNode | Prism::SingletonClassNode) { -> void } -> void
|
99
102
|
def visit_scope(node, &block)
|
100
|
-
|
103
|
+
old_class_annotations = @class_annotations
|
104
|
+
@class_annotations = []
|
101
105
|
old_type_members = @type_members
|
102
106
|
@type_members = []
|
107
|
+
old_extend_t_helpers = @extend_t_helpers
|
108
|
+
@extend_t_helpers = []
|
109
|
+
old_extend_t_generics = @extend_t_generics
|
110
|
+
@extend_t_generics = []
|
111
|
+
old_seen_mixes_in_class_methods = @seen_mixes_in_class_methods
|
112
|
+
@seen_mixes_in_class_methods = false
|
103
113
|
|
104
114
|
yield
|
105
115
|
|
116
|
+
delete_extend_t_generics
|
117
|
+
|
106
118
|
if @type_members.any?
|
107
119
|
indent = " " * node.location.start_column
|
108
120
|
@rewriter << Source::Insert.new(node.location.start_offset, "#: [#{@type_members.join(", ")}]\n#{indent}")
|
109
121
|
end
|
110
122
|
|
123
|
+
unless @seen_mixes_in_class_methods
|
124
|
+
delete_extend_t_helpers
|
125
|
+
end
|
126
|
+
|
127
|
+
@class_annotations.each do |call|
|
128
|
+
apply_class_annotation(node, call)
|
129
|
+
end
|
130
|
+
|
131
|
+
@class_annotations = old_class_annotations
|
111
132
|
@type_members = old_type_members
|
112
|
-
@
|
133
|
+
@extend_t_helpers = old_extend_t_helpers
|
134
|
+
@extend_t_generics = old_extend_t_generics
|
135
|
+
@seen_mixes_in_class_methods = old_seen_mixes_in_class_methods
|
113
136
|
end
|
114
137
|
|
115
138
|
#: (Prism::CallNode) -> void
|
@@ -130,25 +153,22 @@ module Spoom
|
|
130
153
|
raise Error, "Expected attr_reader, attr_writer, or attr_accessor"
|
131
154
|
end
|
132
155
|
|
133
|
-
|
134
|
-
return if
|
156
|
+
last_sigs = collect_last_sigs
|
157
|
+
return if last_sigs.empty?
|
158
|
+
return if last_sigs.any? { |_, sig| sig.is_abstract }
|
135
159
|
|
136
|
-
apply_member_annotations(
|
160
|
+
apply_member_annotations(last_sigs)
|
137
161
|
|
138
162
|
builder = RBI::Parser::TreeBuilder.new(@ruby_contents, comments: [], file: @file)
|
139
163
|
builder.visit(node)
|
140
164
|
rbi_node = builder.tree.nodes.first #: as RBI::Attr
|
141
165
|
|
142
|
-
|
143
|
-
out =
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
p.print("\n")
|
148
|
-
@rewriter << Source::Replace.new(node.location.start_offset, node.location.end_offset, out.string)
|
166
|
+
last_sigs.each do |node, sig|
|
167
|
+
out = rbs_print(node.location.start_column) do |printer|
|
168
|
+
printer.print_attr_sig(rbi_node, sig)
|
169
|
+
end
|
170
|
+
@rewriter << Source::Replace.new(node.location.start_offset, node.location.end_offset, out)
|
149
171
|
end
|
150
|
-
|
151
|
-
@last_sigs.clear
|
152
172
|
end
|
153
173
|
|
154
174
|
#: (Prism::CallNode node) -> void
|
@@ -160,16 +180,17 @@ module Spoom
|
|
160
180
|
|
161
181
|
arg = node.arguments&.arguments&.first
|
162
182
|
return unless arg.is_a?(Prism::ConstantPathNode)
|
163
|
-
return unless arg.slice.match?(/^(::)?T::Helpers$/) || arg.slice.match?(/^(::)?T::Generic$/)
|
164
183
|
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
184
|
+
case arg.slice
|
185
|
+
when /^(::)?T::Helpers$/
|
186
|
+
@extend_t_helpers << node
|
187
|
+
when /^(::)?T::Generic$/
|
188
|
+
@extend_t_generics << node
|
189
|
+
end
|
169
190
|
end
|
170
191
|
|
171
|
-
#: (Prism::CallNode
|
172
|
-
def
|
192
|
+
#: (Prism::ClassNode | Prism::ModuleNode | Prism::SingletonClassNode, Prism::CallNode) -> void
|
193
|
+
def apply_class_annotation(parent, node)
|
173
194
|
unless node.message == "abstract!" || node.message == "interface!" || node.message == "sealed!" ||
|
174
195
|
node.message == "final!" || node.message == "requires_ancestor"
|
175
196
|
raise Error, "Expected abstract!, interface!, sealed!, final!, or requires_ancestor"
|
@@ -178,18 +199,17 @@ module Spoom
|
|
178
199
|
return unless node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)
|
179
200
|
return unless node.arguments.nil?
|
180
201
|
|
181
|
-
|
182
|
-
indent = " " * klass.location.start_column
|
202
|
+
indent = " " * parent.location.start_column
|
183
203
|
|
184
204
|
case node.message
|
185
205
|
when "abstract!"
|
186
|
-
@rewriter << Source::Insert.new(
|
206
|
+
@rewriter << Source::Insert.new(parent.location.start_offset, "# @abstract\n#{indent}")
|
187
207
|
when "interface!"
|
188
|
-
@rewriter << Source::Insert.new(
|
208
|
+
@rewriter << Source::Insert.new(parent.location.start_offset, "# @interface\n#{indent}")
|
189
209
|
when "sealed!"
|
190
|
-
@rewriter << Source::Insert.new(
|
210
|
+
@rewriter << Source::Insert.new(parent.location.start_offset, "# @sealed\n#{indent}")
|
191
211
|
when "final!"
|
192
|
-
@rewriter << Source::Insert.new(
|
212
|
+
@rewriter << Source::Insert.new(parent.location.start_offset, "# @final\n#{indent}")
|
193
213
|
when "requires_ancestor"
|
194
214
|
block = node.block
|
195
215
|
return unless block.is_a?(Prism::BlockNode)
|
@@ -200,7 +220,7 @@ module Spoom
|
|
200
220
|
|
201
221
|
arg = body.body.first #: as Prism::Node
|
202
222
|
srb_type = RBI::Type.parse_node(arg)
|
203
|
-
@rewriter << Source::Insert.new(
|
223
|
+
@rewriter << Source::Insert.new(parent.location.start_offset, "# @requires_ancestor: #{srb_type.rbs_string}\n#{indent}")
|
204
224
|
end
|
205
225
|
|
206
226
|
from = adjust_to_line_start(node.location.start_offset)
|
@@ -287,6 +307,54 @@ module Spoom
|
|
287
307
|
|
288
308
|
type_member
|
289
309
|
end
|
310
|
+
|
311
|
+
#: -> void
|
312
|
+
def delete_extend_t_helpers
|
313
|
+
@extend_t_helpers.each do |helper|
|
314
|
+
from = adjust_to_line_start(helper.location.start_offset)
|
315
|
+
to = adjust_to_line_end(helper.location.end_offset)
|
316
|
+
to = adjust_to_new_line(to)
|
317
|
+
@rewriter << Source::Delete.new(from, to)
|
318
|
+
end
|
319
|
+
|
320
|
+
@extend_t_helpers.clear
|
321
|
+
end
|
322
|
+
|
323
|
+
#: -> void
|
324
|
+
def delete_extend_t_generics
|
325
|
+
@extend_t_generics.each do |generic|
|
326
|
+
from = adjust_to_line_start(generic.location.start_offset)
|
327
|
+
to = adjust_to_line_end(generic.location.end_offset)
|
328
|
+
to = adjust_to_new_line(to)
|
329
|
+
@rewriter << Source::Delete.new(from, to)
|
330
|
+
end
|
331
|
+
|
332
|
+
@extend_t_generics.clear
|
333
|
+
end
|
334
|
+
|
335
|
+
# Collects the last signatures visited and clears the current list
|
336
|
+
#: -> Array[[Prism::CallNode, RBI::Sig]]
|
337
|
+
def collect_last_sigs
|
338
|
+
last_sigs = @last_sigs
|
339
|
+
@last_sigs = []
|
340
|
+
last_sigs
|
341
|
+
end
|
342
|
+
|
343
|
+
#: (Integer) { (RBI::RBSPrinter) -> void } -> String
|
344
|
+
def rbs_print(indent, &block)
|
345
|
+
out = StringIO.new
|
346
|
+
p = RBI::RBSPrinter.new(out: out, positional_names: @positional_names, max_line_length: @max_line_length)
|
347
|
+
block.call(p)
|
348
|
+
string = out.string
|
349
|
+
|
350
|
+
string.lines.map.with_index do |line, index|
|
351
|
+
if index == 0
|
352
|
+
"#: #{line}"
|
353
|
+
else
|
354
|
+
"#{" " * indent}#| #{line}"
|
355
|
+
end
|
356
|
+
end.join + "\n"
|
357
|
+
end
|
290
358
|
end
|
291
359
|
end
|
292
360
|
end
|
@@ -25,16 +25,16 @@ module Spoom
|
|
25
25
|
|
26
26
|
# Converts all `sig` nodes to RBS comments in the given Ruby code.
|
27
27
|
# It also handles type members and class annotations.
|
28
|
-
#: (String ruby_contents, file: String, ?positional_names: bool) -> String
|
29
|
-
def sorbet_sigs_to_rbs_comments(ruby_contents, file:, positional_names: true)
|
30
|
-
SorbetSigsToRBSComments.new(ruby_contents, file: file, positional_names: positional_names).rewrite
|
28
|
+
#: (String ruby_contents, file: String, ?positional_names: bool, ?max_line_length: Integer?) -> String
|
29
|
+
def sorbet_sigs_to_rbs_comments(ruby_contents, file:, positional_names: true, max_line_length: nil)
|
30
|
+
SorbetSigsToRBSComments.new(ruby_contents, file: file, positional_names: positional_names, max_line_length: max_line_length).rewrite
|
31
31
|
end
|
32
32
|
|
33
33
|
# Converts all the RBS comments in the given Ruby code to `sig` nodes.
|
34
34
|
# It also handles type members and class annotations.
|
35
|
-
#: (String ruby_contents, file: String) -> String
|
36
|
-
def rbs_comments_to_sorbet_sigs(ruby_contents, file:)
|
37
|
-
RBSCommentsToSorbetSigs.new(ruby_contents, file: file).rewrite
|
35
|
+
#: (String ruby_contents, file: String, ?max_line_length: Integer?) -> String
|
36
|
+
def rbs_comments_to_sorbet_sigs(ruby_contents, file:, max_line_length: nil)
|
37
|
+
RBSCommentsToSorbetSigs.new(ruby_contents, file: file, max_line_length: max_line_length).rewrite
|
38
38
|
end
|
39
39
|
|
40
40
|
# Converts all `T.let` and `T.cast` nodes to RBS comments in the given Ruby code.
|
@@ -158,7 +158,10 @@ module Spoom
|
|
158
158
|
def rewrite!(bytes)
|
159
159
|
# To avoid remapping positions after each edit,
|
160
160
|
# we sort the changes by position and apply them in reverse order.
|
161
|
-
|
161
|
+
# When ranges are equal, preserve the original order
|
162
|
+
@edits.each_with_index.sort_by do |(edit, idx)|
|
163
|
+
[edit.range, idx]
|
164
|
+
end.reverse_each do |(edit, _)|
|
162
165
|
edit.apply(bytes)
|
163
166
|
end
|
164
167
|
end
|
data/lib/spoom/version.rb
CHANGED