spoom 1.6.3 → 1.7.1
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/lib/spoom/cli/srb/assertions.rb +1 -1
- data/lib/spoom/cli/srb/metrics.rb +68 -0
- data/lib/spoom/cli/srb/sigs.rb +8 -10
- data/lib/spoom/cli/srb.rb +4 -0
- data/lib/spoom/context/sorbet.rb +1 -1
- data/lib/spoom/counters.rb +22 -0
- data/lib/spoom/deadcode/index.rb +2 -2
- data/lib/spoom/deadcode/plugins/active_record.rb +19 -0
- data/lib/spoom/model/builder.rb +10 -15
- data/lib/spoom/model/model.rb +1 -1
- data/lib/spoom/parse.rb +4 -18
- data/lib/spoom/rbs.rb +77 -0
- data/lib/spoom/sorbet/metrics/code_metrics_visitor.rb +236 -0
- data/lib/spoom/sorbet/metrics/metrics_file_parser.rb +34 -0
- data/lib/spoom/sorbet/metrics.rb +2 -30
- data/lib/spoom/sorbet/translate/rbs_comments_to_sorbet_sigs.rb +239 -0
- data/lib/spoom/sorbet/translate/sorbet_assertions_to_rbs_comments.rb +123 -0
- data/lib/spoom/sorbet/translate/sorbet_sigs_to_rbs_comments.rb +293 -0
- data/lib/spoom/sorbet/translate/strip_sorbet_sigs.rb +23 -0
- data/lib/spoom/sorbet/translate/translator.rb +71 -0
- data/lib/spoom/sorbet/translate.rb +49 -0
- data/lib/spoom/sorbet.rb +1 -1
- data/lib/spoom/source/rewriter.rb +167 -0
- data/lib/spoom/source.rb +4 -0
- data/lib/spoom/version.rb +1 -1
- data/lib/spoom.rb +3 -0
- data/rbi/spoom.rbi +337 -178
- metadata +29 -4
- data/lib/spoom/sorbet/assertions.rb +0 -278
- data/lib/spoom/sorbet/sigs.rb +0 -281
data/lib/spoom/sorbet/metrics.rb
CHANGED
@@ -1,33 +1,5 @@
|
|
1
1
|
# typed: strict
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
module Spoom
|
7
|
-
module Sorbet
|
8
|
-
module MetricsParser
|
9
|
-
DEFAULT_PREFIX = "ruby_typer.unknown."
|
10
|
-
|
11
|
-
class << self
|
12
|
-
#: (String path, ?String prefix) -> Hash[String, Integer]
|
13
|
-
def parse_file(path, prefix = DEFAULT_PREFIX)
|
14
|
-
parse_string(File.read(path), prefix)
|
15
|
-
end
|
16
|
-
|
17
|
-
#: (String string, ?String prefix) -> Hash[String, Integer]
|
18
|
-
def parse_string(string, prefix = DEFAULT_PREFIX)
|
19
|
-
parse_hash(JSON.parse(string), prefix)
|
20
|
-
end
|
21
|
-
|
22
|
-
#: (Hash[String, untyped] obj, ?String prefix) -> Hash[String, Integer]
|
23
|
-
def parse_hash(obj, prefix = DEFAULT_PREFIX)
|
24
|
-
obj["metrics"].each_with_object(Hash.new(0)) do |metric, metrics|
|
25
|
-
name = metric["name"]
|
26
|
-
name = name.sub(prefix, "")
|
27
|
-
metrics[name] = metric["value"] || 0
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|
4
|
+
require "spoom/sorbet/metrics/code_metrics_visitor"
|
5
|
+
require "spoom/sorbet/metrics/metrics_file_parser"
|
@@ -0,0 +1,239 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Spoom
|
5
|
+
module Sorbet
|
6
|
+
module Translate
|
7
|
+
class RBSCommentsToSorbetSigs < Translator
|
8
|
+
include Spoom::RBS::ExtractRBSComments
|
9
|
+
|
10
|
+
# @override
|
11
|
+
#: (Prism::ClassNode node) -> void
|
12
|
+
def visit_class_node(node)
|
13
|
+
apply_class_annotations(node)
|
14
|
+
|
15
|
+
super
|
16
|
+
end
|
17
|
+
|
18
|
+
# @override
|
19
|
+
#: (Prism::ModuleNode node) -> void
|
20
|
+
def visit_module_node(node)
|
21
|
+
apply_class_annotations(node)
|
22
|
+
|
23
|
+
super
|
24
|
+
end
|
25
|
+
|
26
|
+
# @override
|
27
|
+
#: (Prism::SingletonClassNode node) -> void
|
28
|
+
def visit_singleton_class_node(node)
|
29
|
+
apply_class_annotations(node)
|
30
|
+
|
31
|
+
super
|
32
|
+
end
|
33
|
+
|
34
|
+
# @override
|
35
|
+
#: (Prism::DefNode node) -> void
|
36
|
+
def visit_def_node(node)
|
37
|
+
comments = node_rbs_comments(node)
|
38
|
+
return if comments.empty?
|
39
|
+
|
40
|
+
return if comments.signatures.empty?
|
41
|
+
|
42
|
+
builder = RBI::Parser::TreeBuilder.new(@ruby_contents, comments: [], file: @file)
|
43
|
+
builder.visit(node)
|
44
|
+
rbi_node = builder.tree.nodes.first #: as RBI::Method
|
45
|
+
|
46
|
+
comments.signatures.each do |signature|
|
47
|
+
method_type = ::RBS::Parser.parse_method_type(signature.string)
|
48
|
+
translator = RBI::RBS::MethodTypeTranslator.new(rbi_node)
|
49
|
+
translator.visit(method_type)
|
50
|
+
sig = translator.result
|
51
|
+
apply_member_annotations(comments.annotations, sig)
|
52
|
+
|
53
|
+
@rewriter << Source::Replace.new(
|
54
|
+
signature.location.start_offset,
|
55
|
+
signature.location.end_offset,
|
56
|
+
sig.string,
|
57
|
+
)
|
58
|
+
rescue ::RBS::ParsingError
|
59
|
+
# Ignore signatures with errors
|
60
|
+
next
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# @override
|
65
|
+
#: (Prism::CallNode node) -> void
|
66
|
+
def visit_call_node(node)
|
67
|
+
case node.message
|
68
|
+
when "attr_reader", "attr_writer", "attr_accessor"
|
69
|
+
visit_attr(node)
|
70
|
+
else
|
71
|
+
super
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
#: (Prism::CallNode) -> void
|
78
|
+
def visit_attr(node)
|
79
|
+
comments = node_rbs_comments(node)
|
80
|
+
return if comments.empty?
|
81
|
+
|
82
|
+
return if comments.signatures.empty?
|
83
|
+
|
84
|
+
comments.signatures.each do |signature|
|
85
|
+
attr_type = ::RBS::Parser.parse_type(signature.string)
|
86
|
+
sig = RBI::Sig.new
|
87
|
+
|
88
|
+
if node.message == "attr_writer"
|
89
|
+
if node.arguments&.arguments&.size != 1
|
90
|
+
raise Error, "AttrWriter must have exactly one name"
|
91
|
+
end
|
92
|
+
|
93
|
+
name = node.arguments&.arguments&.first #: as Prism::SymbolNode
|
94
|
+
sig.params << RBI::SigParam.new(
|
95
|
+
name.slice[1..-1], #: as String
|
96
|
+
RBI::RBS::TypeTranslator.translate(attr_type),
|
97
|
+
)
|
98
|
+
end
|
99
|
+
|
100
|
+
sig.return_type = RBI::RBS::TypeTranslator.translate(attr_type)
|
101
|
+
|
102
|
+
apply_member_annotations(comments.annotations, sig)
|
103
|
+
|
104
|
+
@rewriter << Source::Replace.new(
|
105
|
+
signature.location.start_offset,
|
106
|
+
signature.location.end_offset,
|
107
|
+
sig.string,
|
108
|
+
)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
#: (Prism::ClassNode | Prism::ModuleNode | Prism::SingletonClassNode) -> void
|
113
|
+
def apply_class_annotations(node)
|
114
|
+
comments = node_rbs_comments(node)
|
115
|
+
return if comments.empty?
|
116
|
+
|
117
|
+
indent = " " * (node.location.start_column + 2)
|
118
|
+
insert_pos = case node
|
119
|
+
when Prism::ClassNode
|
120
|
+
(node.superclass || node.constant_path).location.end_offset
|
121
|
+
when Prism::ModuleNode
|
122
|
+
node.constant_path.location.end_offset
|
123
|
+
when Prism::SingletonClassNode
|
124
|
+
node.expression.location.end_offset
|
125
|
+
end
|
126
|
+
|
127
|
+
if comments.annotations.any?
|
128
|
+
unless already_extends?(node, /^(::)?T::Helpers$/)
|
129
|
+
@rewriter << Source::Insert.new(insert_pos, "\n#{indent}extend T::Helpers\n")
|
130
|
+
end
|
131
|
+
|
132
|
+
comments.annotations.reverse_each do |annotation|
|
133
|
+
from = adjust_to_line_start(annotation.location.start_offset)
|
134
|
+
to = adjust_to_line_end(annotation.location.end_offset)
|
135
|
+
@rewriter << Source::Delete.new(from, to)
|
136
|
+
|
137
|
+
content = case annotation.string
|
138
|
+
when "@abstract"
|
139
|
+
"abstract!"
|
140
|
+
when "@interface"
|
141
|
+
"interface!"
|
142
|
+
when "@sealed"
|
143
|
+
"sealed!"
|
144
|
+
when "@final"
|
145
|
+
"final!"
|
146
|
+
when /^@requires_ancestor: /
|
147
|
+
srb_type = ::RBS::Parser.parse_type(annotation.string.delete_prefix("@requires_ancestor: "))
|
148
|
+
rbs_type = RBI::RBS::TypeTranslator.translate(srb_type)
|
149
|
+
"requires_ancestor { #{rbs_type} }"
|
150
|
+
end
|
151
|
+
|
152
|
+
newline = node.body.nil? ? "" : "\n"
|
153
|
+
@rewriter << Source::Insert.new(insert_pos, "\n#{indent}#{content}#{newline}")
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
signatures = comments.signatures
|
158
|
+
if signatures.any?
|
159
|
+
signatures.each do |signature|
|
160
|
+
type_params = ::RBS::Parser.parse_type_params(signature.string)
|
161
|
+
next if type_params.empty?
|
162
|
+
|
163
|
+
from = adjust_to_line_start(signature.location.start_offset)
|
164
|
+
to = adjust_to_line_end(signature.location.end_offset)
|
165
|
+
@rewriter << Source::Delete.new(from, to)
|
166
|
+
|
167
|
+
unless already_extends?(node, /^(::)?T::Generic$/)
|
168
|
+
@rewriter << Source::Insert.new(insert_pos, "\n#{indent}extend T::Generic\n")
|
169
|
+
end
|
170
|
+
|
171
|
+
type_params.each do |type_param|
|
172
|
+
type_member = "#{type_param.name} = type_member"
|
173
|
+
|
174
|
+
case type_param.variance
|
175
|
+
when :covariant
|
176
|
+
type_member = "#{type_member}(:out)"
|
177
|
+
when :contravariant
|
178
|
+
type_member = "#{type_member}(:in)"
|
179
|
+
end
|
180
|
+
|
181
|
+
if type_param.upper_bound || type_param.default_type
|
182
|
+
if type_param.upper_bound
|
183
|
+
rbs_type = RBI::RBS::TypeTranslator.translate(type_param.upper_bound)
|
184
|
+
type_member = "#{type_member} {{ upper: #{rbs_type} }}"
|
185
|
+
end
|
186
|
+
|
187
|
+
if type_param.default_type
|
188
|
+
rbs_type = RBI::RBS::TypeTranslator.translate(type_param.default_type)
|
189
|
+
type_member = "#{type_member} {{ fixed: #{rbs_type} }}"
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
newline = node.body.nil? ? "" : "\n"
|
194
|
+
@rewriter << Source::Insert.new(insert_pos, "\n#{indent}#{type_member}#{newline}")
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
#: (Array[RBS::Annotations], RBI::Sig) -> void
|
201
|
+
def apply_member_annotations(annotations, sig)
|
202
|
+
annotations.each do |annotation|
|
203
|
+
case annotation.string
|
204
|
+
when "@abstract"
|
205
|
+
sig.is_abstract = true
|
206
|
+
when "@final"
|
207
|
+
sig.is_final = true
|
208
|
+
when "@override"
|
209
|
+
sig.is_override = true
|
210
|
+
when "@override(allow_incompatible: true)"
|
211
|
+
sig.is_override = true
|
212
|
+
sig.allow_incompatible_override = true
|
213
|
+
when "@overridable"
|
214
|
+
sig.is_overridable = true
|
215
|
+
when "@without_runtime"
|
216
|
+
sig.without_runtime = true
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
#: (Prism::ClassNode | Prism::ModuleNode | Prism::SingletonClassNode, Regexp) -> bool
|
222
|
+
def already_extends?(node, constant_regex)
|
223
|
+
node.child_nodes.any? do |c|
|
224
|
+
next false unless c.is_a?(Prism::CallNode)
|
225
|
+
next false unless c.message == "extend"
|
226
|
+
next false unless c.receiver.nil? || c.receiver.is_a?(Prism::SelfNode)
|
227
|
+
next false unless c.arguments&.arguments&.size == 1
|
228
|
+
|
229
|
+
arg = c.arguments&.arguments&.first
|
230
|
+
next false unless arg.is_a?(Prism::ConstantPathNode)
|
231
|
+
next false unless arg.slice.match?(constant_regex)
|
232
|
+
|
233
|
+
true
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Spoom
|
5
|
+
module Sorbet
|
6
|
+
module Translate
|
7
|
+
# Translates Sorbet assertions to RBS comments.
|
8
|
+
class SorbetAssertionsToRBSComments < Translator
|
9
|
+
LINE_BREAK = "\n".ord #: Integer
|
10
|
+
|
11
|
+
# @override
|
12
|
+
#: (Prism::CallNode) -> void
|
13
|
+
def visit_call_node(node)
|
14
|
+
return super unless t_annotation?(node)
|
15
|
+
return super unless at_end_of_line?(node)
|
16
|
+
|
17
|
+
value = T.must(node.arguments&.arguments&.first)
|
18
|
+
rbs_annotation = build_rbs_annotation(node)
|
19
|
+
|
20
|
+
start_offset = node.location.start_offset
|
21
|
+
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
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
#: (Prism::CallNode) -> void
|
28
|
+
def build_rbs_annotation(call)
|
29
|
+
case call.name
|
30
|
+
when :let
|
31
|
+
srb_type = call.arguments&.arguments&.last #: as !nil
|
32
|
+
rbs_type = RBI::Type.parse_node(srb_type).rbs_string
|
33
|
+
"#: #{rbs_type}"
|
34
|
+
when :cast
|
35
|
+
srb_type = call.arguments&.arguments&.last #: as !nil
|
36
|
+
rbs_type = RBI::Type.parse_node(srb_type).rbs_string
|
37
|
+
"#: as #{rbs_type}"
|
38
|
+
when :must
|
39
|
+
"#: as !nil"
|
40
|
+
when :unsafe
|
41
|
+
"#: as untyped"
|
42
|
+
else
|
43
|
+
raise "Unknown annotation method: #{call.name}"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Is this node a `T` or `::T` constant?
|
48
|
+
#: (Prism::Node?) -> bool
|
49
|
+
def t?(node)
|
50
|
+
case node
|
51
|
+
when Prism::ConstantReadNode
|
52
|
+
node.name == :T
|
53
|
+
when Prism::ConstantPathNode
|
54
|
+
node.parent.nil? && node.name == :T
|
55
|
+
else
|
56
|
+
false
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Is this node a `T.let` or `T.cast`?
|
61
|
+
#: (Prism::CallNode) -> bool
|
62
|
+
def t_annotation?(node)
|
63
|
+
return false unless t?(node.receiver)
|
64
|
+
|
65
|
+
case node.name
|
66
|
+
when :let, :cast
|
67
|
+
return node.arguments&.arguments&.size == 2
|
68
|
+
when :must, :unsafe
|
69
|
+
return node.arguments&.arguments&.size == 1
|
70
|
+
end
|
71
|
+
|
72
|
+
false
|
73
|
+
end
|
74
|
+
|
75
|
+
#: (Prism::Node) -> bool
|
76
|
+
def at_end_of_line?(node)
|
77
|
+
end_offset = node.location.end_offset
|
78
|
+
end_offset += 1 while (@ruby_bytes[end_offset] == " ".ord) && (end_offset < @ruby_bytes.size)
|
79
|
+
@ruby_bytes[end_offset] == LINE_BREAK
|
80
|
+
end
|
81
|
+
|
82
|
+
#: (Prism::Node, Prism::Node) -> String
|
83
|
+
def dedent_value(assign, value)
|
84
|
+
if value.location.start_line == assign.location.start_line
|
85
|
+
# The value is on the same line as the assign, so we can just return the slice as is:
|
86
|
+
# ```rb
|
87
|
+
# a = T.let(nil, T.nilable(String))
|
88
|
+
# ```
|
89
|
+
# becomes
|
90
|
+
# ```rb
|
91
|
+
# a = nil #: String?
|
92
|
+
# ```
|
93
|
+
return value.slice
|
94
|
+
end
|
95
|
+
|
96
|
+
# The value is on a different line, so we need to dedent it:
|
97
|
+
# ```rb
|
98
|
+
# a = T.let(
|
99
|
+
# [
|
100
|
+
# 1, 2, 3,
|
101
|
+
# ],
|
102
|
+
# T::Array[Integer],
|
103
|
+
# )
|
104
|
+
# ```
|
105
|
+
# becomes
|
106
|
+
# ```rb
|
107
|
+
# a = [
|
108
|
+
# 1, 2, 3,
|
109
|
+
# ] #: Array[Integer]
|
110
|
+
# ```
|
111
|
+
indent = value.location.start_line - assign.location.start_line
|
112
|
+
lines = value.slice.lines
|
113
|
+
if lines.size > 1
|
114
|
+
lines[1..]&.each_with_index do |line, i|
|
115
|
+
lines[i + 1] = line.delete_prefix(" " * indent)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
lines.join
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,293 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Spoom
|
5
|
+
module Sorbet
|
6
|
+
module Translate
|
7
|
+
# Converts all `sig` nodes to RBS comments in the given Ruby code.
|
8
|
+
# It also handles type members and class annotations.
|
9
|
+
class SorbetSigsToRBSComments < Translator
|
10
|
+
#: (String, file: String, positional_names: bool) -> void
|
11
|
+
def initialize(ruby_contents, file:, positional_names:)
|
12
|
+
super(ruby_contents, file: file)
|
13
|
+
|
14
|
+
@positional_names = positional_names #: bool
|
15
|
+
@nesting = [] #: Array[Prism::ClassNode | Prism::ModuleNode | Prism::SingletonClassNode]
|
16
|
+
@last_sigs = [] #: Array[[Prism::CallNode, RBI::Sig]]
|
17
|
+
@type_members = [] #: Array[String]
|
18
|
+
end
|
19
|
+
|
20
|
+
# @override
|
21
|
+
#: (Prism::ClassNode) -> void
|
22
|
+
def visit_class_node(node)
|
23
|
+
visit_scope(node) { super }
|
24
|
+
end
|
25
|
+
|
26
|
+
# @override
|
27
|
+
#: (Prism::ModuleNode) -> void
|
28
|
+
def visit_module_node(node)
|
29
|
+
visit_scope(node) { super }
|
30
|
+
end
|
31
|
+
|
32
|
+
# @override
|
33
|
+
#: (Prism::SingletonClassNode) -> void
|
34
|
+
def visit_singleton_class_node(node)
|
35
|
+
visit_scope(node) { super }
|
36
|
+
end
|
37
|
+
|
38
|
+
# @override
|
39
|
+
#: (Prism::DefNode) -> void
|
40
|
+
def visit_def_node(node)
|
41
|
+
return if @last_sigs.empty?
|
42
|
+
return if @last_sigs.any? { |_, sig| sig.is_abstract }
|
43
|
+
|
44
|
+
apply_member_annotations(@last_sigs)
|
45
|
+
|
46
|
+
# Build the RBI::Method node so we can print the method signature as RBS.
|
47
|
+
builder = RBI::Parser::TreeBuilder.new(@ruby_contents, comments: [], file: @file)
|
48
|
+
builder.visit(node)
|
49
|
+
rbi_node = builder.tree.nodes.first #: as RBI::Method
|
50
|
+
|
51
|
+
@last_sigs.each do |node, sig|
|
52
|
+
out = StringIO.new
|
53
|
+
p = RBI::RBSPrinter.new(out: out, indent: node.location.start_column, positional_names: @positional_names)
|
54
|
+
p.print("#: ")
|
55
|
+
p.send(:print_method_sig, rbi_node, sig)
|
56
|
+
p.print("\n")
|
57
|
+
@rewriter << Source::Replace.new(node.location.start_offset, node.location.end_offset, out.string)
|
58
|
+
end
|
59
|
+
|
60
|
+
@last_sigs.clear
|
61
|
+
end
|
62
|
+
|
63
|
+
# @override
|
64
|
+
#: (Prism::CallNode) -> void
|
65
|
+
def visit_call_node(node)
|
66
|
+
case node.message
|
67
|
+
when "sig"
|
68
|
+
visit_sig(node)
|
69
|
+
when "attr_reader", "attr_writer", "attr_accessor"
|
70
|
+
visit_attr(node)
|
71
|
+
when "extend"
|
72
|
+
visit_extend(node)
|
73
|
+
when "abstract!", "interface!", "sealed!", "final!", "requires_ancestor"
|
74
|
+
visit_class_annotation(node)
|
75
|
+
else
|
76
|
+
super
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# @override
|
81
|
+
#: (Prism::ConstantWriteNode) -> void
|
82
|
+
def visit_constant_write_node(node)
|
83
|
+
call = node.value
|
84
|
+
return super unless call.is_a?(Prism::CallNode)
|
85
|
+
return super unless call.message == "type_member"
|
86
|
+
|
87
|
+
@type_members << build_type_member_string(node)
|
88
|
+
|
89
|
+
from = adjust_to_line_start(node.location.start_offset)
|
90
|
+
to = adjust_to_line_end(node.location.end_offset)
|
91
|
+
to = adjust_to_new_line(to)
|
92
|
+
|
93
|
+
@rewriter << Source::Delete.new(from, to)
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
#: (Prism::ClassNode | Prism::ModuleNode | Prism::SingletonClassNode) { -> void } -> void
|
99
|
+
def visit_scope(node, &block)
|
100
|
+
@nesting << node
|
101
|
+
old_type_members = @type_members
|
102
|
+
@type_members = []
|
103
|
+
|
104
|
+
yield
|
105
|
+
|
106
|
+
if @type_members.any?
|
107
|
+
indent = " " * node.location.start_column
|
108
|
+
@rewriter << Source::Insert.new(node.location.start_offset, "#: [#{@type_members.join(", ")}]\n#{indent}")
|
109
|
+
end
|
110
|
+
|
111
|
+
@type_members = old_type_members
|
112
|
+
@nesting.pop
|
113
|
+
end
|
114
|
+
|
115
|
+
#: (Prism::CallNode) -> void
|
116
|
+
def visit_sig(node)
|
117
|
+
return unless sorbet_sig?(node)
|
118
|
+
|
119
|
+
builder = RBI::Parser::SigBuilder.new(@ruby_contents, file: @file)
|
120
|
+
builder.current.loc = node.location
|
121
|
+
builder.visit_call_node(node)
|
122
|
+
builder.current.comments = []
|
123
|
+
|
124
|
+
@last_sigs << [node, builder.current]
|
125
|
+
end
|
126
|
+
|
127
|
+
#: (Prism::CallNode) -> void
|
128
|
+
def visit_attr(node)
|
129
|
+
unless node.message == "attr_reader" || node.message == "attr_writer" || node.message == "attr_accessor"
|
130
|
+
raise Error, "Expected attr_reader, attr_writer, or attr_accessor"
|
131
|
+
end
|
132
|
+
|
133
|
+
return if @last_sigs.empty?
|
134
|
+
return if @last_sigs.any? { |_, sig| sig.is_abstract }
|
135
|
+
|
136
|
+
apply_member_annotations(@last_sigs)
|
137
|
+
|
138
|
+
builder = RBI::Parser::TreeBuilder.new(@ruby_contents, comments: [], file: @file)
|
139
|
+
builder.visit(node)
|
140
|
+
rbi_node = builder.tree.nodes.first #: as RBI::Attr
|
141
|
+
|
142
|
+
@last_sigs.each do |node, sig|
|
143
|
+
out = StringIO.new
|
144
|
+
p = RBI::RBSPrinter.new(out: out, indent: node.location.start_column, positional_names: @positional_names)
|
145
|
+
p.print("#: ")
|
146
|
+
p.print_attr_sig(rbi_node, sig)
|
147
|
+
p.print("\n")
|
148
|
+
@rewriter << Source::Replace.new(node.location.start_offset, node.location.end_offset, out.string)
|
149
|
+
end
|
150
|
+
|
151
|
+
@last_sigs.clear
|
152
|
+
end
|
153
|
+
|
154
|
+
#: (Prism::CallNode node) -> void
|
155
|
+
def visit_extend(node)
|
156
|
+
raise Error, "Expected extend" unless node.message == "extend"
|
157
|
+
|
158
|
+
return unless node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)
|
159
|
+
return unless node.arguments&.arguments&.size == 1
|
160
|
+
|
161
|
+
arg = node.arguments&.arguments&.first
|
162
|
+
return unless arg.is_a?(Prism::ConstantPathNode)
|
163
|
+
return unless arg.slice.match?(/^(::)?T::Helpers$/) || arg.slice.match?(/^(::)?T::Generic$/)
|
164
|
+
|
165
|
+
from = adjust_to_line_start(node.location.start_offset)
|
166
|
+
to = adjust_to_line_end(node.location.end_offset)
|
167
|
+
to = adjust_to_new_line(to)
|
168
|
+
@rewriter << Source::Delete.new(from, to)
|
169
|
+
end
|
170
|
+
|
171
|
+
#: (Prism::CallNode node) -> void
|
172
|
+
def visit_class_annotation(node)
|
173
|
+
unless node.message == "abstract!" || node.message == "interface!" || node.message == "sealed!" ||
|
174
|
+
node.message == "final!" || node.message == "requires_ancestor"
|
175
|
+
raise Error, "Expected abstract!, interface!, sealed!, final!, or requires_ancestor"
|
176
|
+
end
|
177
|
+
|
178
|
+
return unless node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)
|
179
|
+
return unless node.arguments.nil?
|
180
|
+
|
181
|
+
klass = @nesting.last #: as Prism::Node
|
182
|
+
indent = " " * klass.location.start_column
|
183
|
+
|
184
|
+
case node.message
|
185
|
+
when "abstract!"
|
186
|
+
@rewriter << Source::Insert.new(klass.location.start_offset, "# @abstract\n#{indent}")
|
187
|
+
when "interface!"
|
188
|
+
@rewriter << Source::Insert.new(klass.location.start_offset, "# @interface\n#{indent}")
|
189
|
+
when "sealed!"
|
190
|
+
@rewriter << Source::Insert.new(klass.location.start_offset, "# @sealed\n#{indent}")
|
191
|
+
when "final!"
|
192
|
+
@rewriter << Source::Insert.new(klass.location.start_offset, "# @final\n#{indent}")
|
193
|
+
when "requires_ancestor"
|
194
|
+
block = node.block
|
195
|
+
return unless block.is_a?(Prism::BlockNode)
|
196
|
+
|
197
|
+
body = block.body
|
198
|
+
return unless body.is_a?(Prism::StatementsNode)
|
199
|
+
return unless body.body.size == 1
|
200
|
+
|
201
|
+
arg = body.body.first #: as Prism::Node
|
202
|
+
srb_type = RBI::Type.parse_node(arg)
|
203
|
+
@rewriter << Source::Insert.new(klass.location.start_offset, "# @requires_ancestor: #{srb_type.rbs_string}\n#{indent}")
|
204
|
+
end
|
205
|
+
|
206
|
+
from = adjust_to_line_start(node.location.start_offset)
|
207
|
+
to = adjust_to_line_end(node.location.end_offset)
|
208
|
+
to = adjust_to_new_line(to)
|
209
|
+
|
210
|
+
@rewriter << Source::Delete.new(from, to)
|
211
|
+
end
|
212
|
+
|
213
|
+
#: (Array[[Prism::CallNode, RBI::Sig]]) -> void
|
214
|
+
def apply_member_annotations(sigs)
|
215
|
+
return if sigs.empty?
|
216
|
+
|
217
|
+
node, _sig = sigs.first #: as [Prism::CallNode, RBI::Sig]
|
218
|
+
insert_pos = node.location.start_offset
|
219
|
+
|
220
|
+
if sigs.any? { |_, sig| sig.without_runtime }
|
221
|
+
@rewriter << Source::Insert.new(insert_pos, "# @without_runtime\n")
|
222
|
+
end
|
223
|
+
|
224
|
+
if sigs.any? { |_, sig| sig.is_final }
|
225
|
+
@rewriter << Source::Insert.new(insert_pos, "# @final\n")
|
226
|
+
end
|
227
|
+
|
228
|
+
if sigs.any? { |_, sig| sig.is_abstract }
|
229
|
+
@rewriter << Source::Insert.new(insert_pos, "# @abstract\n")
|
230
|
+
end
|
231
|
+
|
232
|
+
if sigs.any? { |_, sig| sig.is_override }
|
233
|
+
@rewriter << if sigs.any? { |_, sig| sig.allow_incompatible_override }
|
234
|
+
Source::Insert.new(insert_pos, "# @override(allow_incompatible: true)\n")
|
235
|
+
else
|
236
|
+
Source::Insert.new(insert_pos, "# @override\n")
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
if sigs.any? { |_, sig| sig.is_overridable }
|
241
|
+
@rewriter << Source::Insert.new(insert_pos, "# @overridable\n")
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
#: (Prism::ConstantWriteNode) -> String
|
246
|
+
def build_type_member_string(node)
|
247
|
+
call = node.value
|
248
|
+
raise Error, "Expected a call node" unless call.is_a?(Prism::CallNode)
|
249
|
+
raise Error, "Expected type_member" unless call.message == "type_member"
|
250
|
+
|
251
|
+
type_member = node.name.to_s
|
252
|
+
|
253
|
+
arg = call.arguments&.arguments&.first
|
254
|
+
if arg.is_a?(Prism::SymbolNode)
|
255
|
+
case arg.slice
|
256
|
+
when ":in"
|
257
|
+
type_member = "in #{type_member}"
|
258
|
+
when ":out"
|
259
|
+
type_member = "out #{type_member}"
|
260
|
+
else
|
261
|
+
raise Error, "Unknown type member variance: #{arg.slice}"
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
block = call.block
|
266
|
+
return type_member unless block.is_a?(Prism::BlockNode)
|
267
|
+
|
268
|
+
body = block.body
|
269
|
+
return type_member unless body.is_a?(Prism::StatementsNode)
|
270
|
+
return type_member unless body.body.size == 1
|
271
|
+
|
272
|
+
hash = body.body.first
|
273
|
+
return type_member unless hash.is_a?(Prism::HashNode)
|
274
|
+
|
275
|
+
hash.elements.each do |element|
|
276
|
+
next unless element.is_a?(Prism::AssocNode)
|
277
|
+
|
278
|
+
type = RBI::Type.parse_node(element.value)
|
279
|
+
|
280
|
+
case element.key.slice
|
281
|
+
when "upper:"
|
282
|
+
type_member = "#{type_member} < #{type.rbs_string}"
|
283
|
+
when "fixed:"
|
284
|
+
type_member = "#{type_member} = #{type.rbs_string}"
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
type_member
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|
292
|
+
end
|
293
|
+
end
|