spoom 1.6.3 → 1.7.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.
- checksums.yaml +4 -4
- data/lib/spoom/cli/srb/assertions.rb +1 -1
- data/lib/spoom/cli/srb/sigs.rb +8 -10
- 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/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 +2 -0
- data/rbi/spoom.rbi +241 -155
- metadata +25 -4
- data/lib/spoom/sorbet/assertions.rb +0 -278
- data/lib/spoom/sorbet/sigs.rb +0 -281
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 232ecacb9d25232ca17356951f795baf011b5ec9dd4c18b5688be1c474bccf2b
|
4
|
+
data.tar.gz: 824782c6b5b810cae1a88805e3dd196a4499f4f04fce86e57fc933873958cbd9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: de1b4478214f3be4f3369b7930cd6a3b96dac3e6c5ba506a3e25380bf1733b093996851dc59537ef56c2700eb63e3d0c317cc3ab44d0690e0255da38c0e6da77
|
7
|
+
data.tar.gz: 61e14bcfd81428eb251b514b4b21c0febd5df783bc5221e68d1c5ca44e3c5ae34017c12e13d034d331c31ee8900a38847e3712197797431122c5a24d57fc144b
|
@@ -19,7 +19,7 @@ module Spoom
|
|
19
19
|
"in `#{files.size}` file#{files.size == 1 ? "" : "s"}...\n\n")
|
20
20
|
|
21
21
|
transformed_files = transform_files(files) do |file, contents|
|
22
|
-
Spoom::Sorbet::
|
22
|
+
Spoom::Sorbet::Translate.sorbet_assertions_to_rbs_comments(contents, file: file)
|
23
23
|
end
|
24
24
|
|
25
25
|
say("Translated type assertions in `#{transformed_files}` file#{transformed_files == 1 ? "" : "s"}.")
|
data/lib/spoom/cli/srb/sigs.rb
CHANGED
@@ -1,8 +1,6 @@
|
|
1
1
|
# typed: true
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
-
require "spoom/sorbet/sigs"
|
5
|
-
|
6
4
|
module Spoom
|
7
5
|
module Cli
|
8
6
|
module Srb
|
@@ -34,12 +32,12 @@ module Spoom
|
|
34
32
|
|
35
33
|
case from
|
36
34
|
when "rbi"
|
37
|
-
transformed_files = transform_files(files) do |
|
38
|
-
Spoom::Sorbet::
|
35
|
+
transformed_files = transform_files(files) do |file, contents|
|
36
|
+
Spoom::Sorbet::Translate.sorbet_sigs_to_rbs_comments(contents, file: file, positional_names: options[:positional_names])
|
39
37
|
end
|
40
38
|
when "rbs"
|
41
|
-
transformed_files = transform_files(files) do |
|
42
|
-
Spoom::Sorbet::
|
39
|
+
transformed_files = transform_files(files) do |file, contents|
|
40
|
+
Spoom::Sorbet::Translate.rbs_comments_to_sorbet_sigs(contents, file: file)
|
43
41
|
end
|
44
42
|
end
|
45
43
|
|
@@ -52,8 +50,8 @@ module Spoom
|
|
52
50
|
|
53
51
|
say("Stripping signatures from `#{files.size}` file#{files.size == 1 ? "" : "s"}...\n\n")
|
54
52
|
|
55
|
-
transformed_files = transform_files(files) do |
|
56
|
-
Spoom::Sorbet::
|
53
|
+
transformed_files = transform_files(files) do |file, contents|
|
54
|
+
Spoom::Sorbet::Translate.strip_sorbet_sigs(contents, file: file)
|
57
55
|
end
|
58
56
|
|
59
57
|
say("Stripped signatures from `#{transformed_files}` file#{transformed_files == 1 ? "" : "s"}.")
|
@@ -93,8 +91,8 @@ module Spoom
|
|
93
91
|
# Then, we transform the copied files to translate all the RBS signatures into RBI signatures.
|
94
92
|
say("Translating signatures from RBS to RBI...")
|
95
93
|
files = collect_files([copy_context.absolute_path])
|
96
|
-
transform_files(files) do |
|
97
|
-
Spoom::Sorbet::
|
94
|
+
transform_files(files) do |file, contents|
|
95
|
+
Spoom::Sorbet::Translate.rbs_comments_to_sorbet_sigs(contents, file: file)
|
98
96
|
end
|
99
97
|
|
100
98
|
# We need to inject `extend T::Sig` to be sure all the classes can run the `sig{}` blocks.
|
data/lib/spoom/deadcode/index.rb
CHANGED
@@ -49,10 +49,10 @@ module Spoom
|
|
49
49
|
|
50
50
|
#: (String rb, file: String, ?plugins: Array[Plugins::Base]) -> void
|
51
51
|
def index_ruby(rb, file:, plugins: [])
|
52
|
-
node
|
52
|
+
node = Spoom.parse_ruby(rb, file: file, comments: true)
|
53
53
|
|
54
54
|
# Index definitions
|
55
|
-
model_builder = Model::Builder.new(@model, file
|
55
|
+
model_builder = Model::Builder.new(@model, file)
|
56
56
|
model_builder.visit(node)
|
57
57
|
|
58
58
|
# Index references
|
@@ -41,6 +41,11 @@ module Spoom
|
|
41
41
|
"before_validation",
|
42
42
|
].freeze #: Array[String]
|
43
43
|
|
44
|
+
CALLBACK_CONDITIONS = [
|
45
|
+
"if",
|
46
|
+
"unless",
|
47
|
+
].freeze #: Array[String]
|
48
|
+
|
44
49
|
CRUD_METHODS = [
|
45
50
|
"assign_attributes",
|
46
51
|
"create",
|
@@ -63,9 +68,23 @@ module Spoom
|
|
63
68
|
#: (Send send) -> void
|
64
69
|
def on_send(send)
|
65
70
|
if send.recv.nil? && CALLBACKS.include?(send.name)
|
71
|
+
# Process direct symbol arguments
|
66
72
|
send.each_arg(Prism::SymbolNode) do |arg|
|
67
73
|
@index.reference_method(arg.unescaped, send.location)
|
68
74
|
end
|
75
|
+
|
76
|
+
# Process hash arguments for conditions like if: :method_name
|
77
|
+
send.each_arg_assoc do |key, value|
|
78
|
+
key = key.slice.delete_suffix(":")
|
79
|
+
|
80
|
+
case key
|
81
|
+
when *CALLBACK_CONDITIONS
|
82
|
+
if value&.is_a?(Prism::SymbolNode)
|
83
|
+
@index.reference_method(value.unescaped, send.location)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
69
88
|
return
|
70
89
|
end
|
71
90
|
|
data/lib/spoom/model/builder.rb
CHANGED
@@ -5,15 +5,12 @@ module Spoom
|
|
5
5
|
class Model
|
6
6
|
# Populate a Model by visiting the nodes from a Ruby file
|
7
7
|
class Builder < NamespaceVisitor
|
8
|
-
#: (Model model, String file
|
9
|
-
def initialize(model, file
|
8
|
+
#: (Model model, String file) -> void
|
9
|
+
def initialize(model, file)
|
10
10
|
super()
|
11
11
|
|
12
12
|
@model = model
|
13
13
|
@file = file
|
14
|
-
@comments_by_line = comments.to_h do |c|
|
15
|
-
[c.location.start_line, c]
|
16
|
-
end #: Hash[Integer, Prism::Comment]
|
17
14
|
@namespace_nesting = [] #: Array[Namespace]
|
18
15
|
@visibility_stack = [Visibility::Public] #: Array[Visibility]
|
19
16
|
@last_sigs = [] #: Array[Sig]
|
@@ -263,22 +260,20 @@ module Spoom
|
|
263
260
|
|
264
261
|
#: (Prism::Node node) -> Array[Comment]
|
265
262
|
def node_comments(node)
|
263
|
+
last_line = node.location.start_line
|
266
264
|
comments = []
|
267
265
|
|
268
|
-
|
269
|
-
|
266
|
+
node.location.leading_comments.reverse_each do |comment|
|
267
|
+
if comment.location.start_line < last_line - 1
|
268
|
+
break
|
269
|
+
end
|
270
270
|
|
271
|
-
|
272
|
-
comment = @comments_by_line[line]
|
273
|
-
break unless comment
|
271
|
+
last_line = comment.location.start_line
|
274
272
|
|
275
|
-
|
273
|
+
comments.unshift(Comment.new(
|
276
274
|
comment.slice.gsub(/^#\s?/, "").rstrip,
|
277
275
|
Location.from_prism(@file, comment.location),
|
278
|
-
)
|
279
|
-
|
280
|
-
comments.unshift(spoom_comment)
|
281
|
-
@comments_by_line.delete(line)
|
276
|
+
))
|
282
277
|
end
|
283
278
|
|
284
279
|
comments
|
data/lib/spoom/model/model.rb
CHANGED
@@ -84,7 +84,7 @@ module Spoom
|
|
84
84
|
#: Array[Comment]
|
85
85
|
attr_reader :comments
|
86
86
|
|
87
|
-
#: (Symbol symbol, owner: Namespace?, location: Location,
|
87
|
+
#: (Symbol symbol, owner: Namespace?, location: Location, comments: Array[Comment]) -> void
|
88
88
|
def initialize(symbol, owner:, location:, comments:)
|
89
89
|
@symbol = symbol
|
90
90
|
@owner = owner
|
data/lib/spoom/parse.rb
CHANGED
@@ -7,8 +7,8 @@ module Spoom
|
|
7
7
|
class ParseError < Error; end
|
8
8
|
|
9
9
|
class << self
|
10
|
-
#: (String ruby, file: String) -> Prism::Node
|
11
|
-
def parse_ruby(ruby, file:)
|
10
|
+
#: (String ruby, file: String, ?comments: bool) -> Prism::Node
|
11
|
+
def parse_ruby(ruby, file:, comments: false)
|
12
12
|
result = Prism.parse(ruby)
|
13
13
|
unless result.success?
|
14
14
|
message = +"Error while parsing #{file}:\n"
|
@@ -20,23 +20,9 @@ module Spoom
|
|
20
20
|
raise ParseError, message
|
21
21
|
end
|
22
22
|
|
23
|
-
result.
|
24
|
-
end
|
25
|
-
|
26
|
-
#: (String ruby, file: String) -> [Prism::Node, Array[Prism::Comment]]
|
27
|
-
def parse_ruby_with_comments(ruby, file:)
|
28
|
-
result = Prism.parse(ruby)
|
29
|
-
unless result.success?
|
30
|
-
message = +"Error while parsing #{file}:\n"
|
31
|
-
|
32
|
-
result.errors.each do |e|
|
33
|
-
message << "- #{e.message} (at #{e.location.start_line}:#{e.location.start_column})\n"
|
34
|
-
end
|
23
|
+
result.attach_comments! if comments
|
35
24
|
|
36
|
-
|
37
|
-
end
|
38
|
-
|
39
|
-
[result.value, result.comments]
|
25
|
+
result.value
|
40
26
|
end
|
41
27
|
end
|
42
28
|
end
|
data/lib/spoom/rbs.rb
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Spoom
|
5
|
+
module RBS
|
6
|
+
class Comments
|
7
|
+
#: Array[Annotations]
|
8
|
+
attr_reader :annotations
|
9
|
+
|
10
|
+
#: Array[Signature]
|
11
|
+
attr_reader :signatures
|
12
|
+
|
13
|
+
#: -> void
|
14
|
+
def initialize
|
15
|
+
@annotations = [] #: Array[Annotations]
|
16
|
+
@signatures = [] #: Array[Signature]
|
17
|
+
end
|
18
|
+
|
19
|
+
#: -> bool
|
20
|
+
def empty?
|
21
|
+
@annotations.empty? && @signatures.empty?
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class Comment
|
26
|
+
#: String
|
27
|
+
attr_reader :string
|
28
|
+
|
29
|
+
#: Prism::Location
|
30
|
+
attr_reader :location
|
31
|
+
|
32
|
+
#: (String, Prism::Location) -> void
|
33
|
+
def initialize(string, location)
|
34
|
+
@string = string
|
35
|
+
@location = location
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class Annotations < Comment; end
|
40
|
+
class Signature < Comment; end
|
41
|
+
|
42
|
+
module ExtractRBSComments
|
43
|
+
#: (Prism::Node) -> Comments
|
44
|
+
def node_rbs_comments(node)
|
45
|
+
res = Comments.new
|
46
|
+
|
47
|
+
comments = node.location.leading_comments.reverse
|
48
|
+
return res if comments.empty?
|
49
|
+
|
50
|
+
continuation_comments = [] #: Array[Prism::Comment]
|
51
|
+
|
52
|
+
comments.each do |comment|
|
53
|
+
string = comment.slice
|
54
|
+
|
55
|
+
if string.start_with?("# @")
|
56
|
+
string = string.delete_prefix("#").strip
|
57
|
+
res.annotations << Annotations.new(string, comment.location)
|
58
|
+
elsif string.start_with?("#: ")
|
59
|
+
string = string.delete_prefix("#:").strip
|
60
|
+
location = comment.location
|
61
|
+
|
62
|
+
continuation_comments.reverse_each do |continuation_comment|
|
63
|
+
string = "#{string}#{continuation_comment.slice.delete_prefix("#|")}"
|
64
|
+
location = location.join(continuation_comment.location)
|
65
|
+
end
|
66
|
+
continuation_comments.clear
|
67
|
+
res.signatures << Signature.new(string, location)
|
68
|
+
elsif string.start_with?("#|")
|
69
|
+
continuation_comments << comment
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
res
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -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
|