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.
@@ -1,278 +0,0 @@
1
- # typed: strict
2
- # frozen_string_literal: true
3
-
4
- require "rbi"
5
-
6
- module Spoom
7
- module Sorbet
8
- class Assertions
9
- class << self
10
- #: (String, file: String) -> String
11
- def rbi_to_rbs(ruby_contents, file:)
12
- old_encoding = ruby_contents.encoding
13
- ruby_contents = ruby_contents.encode("UTF-8") unless old_encoding == "UTF-8"
14
- ruby_bytes = ruby_contents.bytes
15
-
16
- assigns = collect_assigns(ruby_contents, file: file)
17
-
18
- assigns.reverse.each do |assign|
19
- # Adjust the end offset to locate the end of the line:
20
- #
21
- # So this:
22
- #
23
- # (a = T.let(nil, T.nilable(String)))
24
- #
25
- # properly becomes:
26
- #
27
- # (a = nil) #: String?
28
- #
29
- # This is important to avoid translating the `nil` as `nil` instead of `nil #: String?`
30
- end_offset = assign.node.location.end_offset
31
- end_offset += 1 while (ruby_bytes[end_offset] != "\n".ord) && (end_offset < ruby_bytes.size)
32
- T.unsafe(ruby_bytes).insert(end_offset, *" #: #{assign.rbs_type}".bytes)
33
-
34
- # Rewrite the value
35
- start_offset = assign.operator_loc.end_offset
36
- end_offset = assign.node.value.location.start_offset + assign.node.value.location.length
37
- ruby_bytes[start_offset...end_offset] = " #{dedent_value(assign)}".bytes
38
- end
39
-
40
- ruby_bytes.pack("C*").force_encoding(old_encoding)
41
- end
42
-
43
- private
44
-
45
- #: (String, file: String) -> Array[AssignNode]
46
- def collect_assigns(ruby_contents, file:)
47
- node = Spoom.parse_ruby(ruby_contents, file: file)
48
- visitor = Locator.new
49
- visitor.visit(node)
50
- visitor.assigns
51
- end
52
-
53
- #: (AssignNode) -> String
54
- def dedent_value(assign)
55
- if assign.value.location.start_line == assign.node.location.start_line
56
- # The value is on the same line as the assign, so we can just return the slice as is:
57
- # ```rb
58
- # a = T.let(nil, T.nilable(String))
59
- # ```
60
- # becomes
61
- # ```rb
62
- # a = nil #: String?
63
- # ```
64
- return assign.value.slice
65
- end
66
-
67
- # The value is on a different line, so we need to dedent it:
68
- # ```rb
69
- # a = T.let(
70
- # [
71
- # 1, 2, 3,
72
- # ],
73
- # T::Array[Integer],
74
- # )
75
- # ```
76
- # becomes
77
- # ```rb
78
- # a = [
79
- # 1, 2, 3,
80
- # ] #: Array[Integer]
81
- # ```
82
- indent = assign.value.location.start_line - assign.node.location.start_line
83
- lines = assign.value.slice.lines
84
- if lines.size > 1
85
- lines[1..]&.each_with_index do |line, i|
86
- lines[i + 1] = line.delete_prefix(" " * indent)
87
- end
88
- end
89
- lines.join
90
- end
91
- end
92
-
93
- AssignType = T.type_alias do
94
- T.any(
95
- Prism::ClassVariableAndWriteNode,
96
- Prism::ClassVariableOrWriteNode,
97
- Prism::ClassVariableOperatorWriteNode,
98
- Prism::ClassVariableWriteNode,
99
- Prism::ConstantAndWriteNode,
100
- Prism::ConstantOrWriteNode,
101
- Prism::ConstantOperatorWriteNode,
102
- Prism::ConstantWriteNode,
103
- Prism::ConstantPathAndWriteNode,
104
- Prism::ConstantPathOrWriteNode,
105
- Prism::ConstantPathOperatorWriteNode,
106
- Prism::ConstantPathWriteNode,
107
- Prism::GlobalVariableAndWriteNode,
108
- Prism::GlobalVariableOrWriteNode,
109
- Prism::GlobalVariableOperatorWriteNode,
110
- Prism::GlobalVariableWriteNode,
111
- Prism::InstanceVariableAndWriteNode,
112
- Prism::InstanceVariableOperatorWriteNode,
113
- Prism::InstanceVariableOrWriteNode,
114
- Prism::InstanceVariableWriteNode,
115
- Prism::LocalVariableAndWriteNode,
116
- Prism::LocalVariableOperatorWriteNode,
117
- Prism::LocalVariableOrWriteNode,
118
- Prism::LocalVariableWriteNode,
119
- )
120
- end
121
-
122
- class AssignNode
123
- #: AssignType
124
- attr_reader :node
125
-
126
- #: Prism::Location
127
- attr_reader :operator_loc
128
-
129
- #: Prism::Node
130
- attr_reader :value, :type
131
-
132
- #: (AssignType, Prism::Location, Prism::Node, Prism::Node) -> void
133
- def initialize(node, operator_loc, value, type)
134
- @node = node
135
- @operator_loc = operator_loc
136
- @value = value
137
- @type = type
138
- end
139
-
140
- #: -> String
141
- def rbs_type
142
- RBI::Type.parse_node(type).rbs_string
143
- end
144
- end
145
-
146
- class Locator < Spoom::Visitor
147
- ANNOTATION_METHODS = [:let] #: Array[Symbol]
148
-
149
- #: Array[AssignNode]
150
- attr_reader :assigns
151
-
152
- #: -> void
153
- def initialize
154
- super
155
- @assigns = [] #: Array[AssignNode]
156
- end
157
-
158
- #: (AssignType) -> void
159
- def visit_assign(node)
160
- call = node.value
161
- return unless call.is_a?(Prism::CallNode) && t_annotation?(call)
162
-
163
- # We do not support translating heredocs yet because the `#: ` would need to be added to the first line
164
- # and it will requires us to adapt the annotation detection in Sorbet. But Sorbet desugars them into bare
165
- # strings making them impossible to detect.
166
- value = T.must(call.arguments&.arguments&.first)
167
- return if contains_heredoc?(value)
168
-
169
- operator_loc = case node
170
- when Prism::ClassVariableOperatorWriteNode,
171
- Prism::ConstantOperatorWriteNode,
172
- Prism::ConstantPathOperatorWriteNode,
173
- Prism::GlobalVariableOperatorWriteNode,
174
- Prism::InstanceVariableOperatorWriteNode,
175
- Prism::LocalVariableOperatorWriteNode
176
- node.binary_operator_loc
177
- else
178
- node.operator_loc
179
- end
180
-
181
- @assigns << AssignNode.new(
182
- node,
183
- operator_loc,
184
- value,
185
- T.must(call.arguments&.arguments&.last),
186
- )
187
- end
188
-
189
- alias_method(:visit_class_variable_and_write_node, :visit_assign)
190
- alias_method(:visit_class_variable_operator_write_node, :visit_assign)
191
- alias_method(:visit_class_variable_or_write_node, :visit_assign)
192
- alias_method(:visit_class_variable_write_node, :visit_assign)
193
-
194
- alias_method(:visit_constant_and_write_node, :visit_assign)
195
- alias_method(:visit_constant_operator_write_node, :visit_assign)
196
- alias_method(:visit_constant_or_write_node, :visit_assign)
197
- alias_method(:visit_constant_write_node, :visit_assign)
198
-
199
- alias_method(:visit_constant_path_and_write_node, :visit_assign)
200
- alias_method(:visit_constant_path_operator_write_node, :visit_assign)
201
- alias_method(:visit_constant_path_or_write_node, :visit_assign)
202
- alias_method(:visit_constant_path_write_node, :visit_assign)
203
-
204
- alias_method(:visit_global_variable_and_write_node, :visit_assign)
205
- alias_method(:visit_global_variable_operator_write_node, :visit_assign)
206
- alias_method(:visit_global_variable_or_write_node, :visit_assign)
207
- alias_method(:visit_global_variable_write_node, :visit_assign)
208
-
209
- alias_method(:visit_instance_variable_and_write_node, :visit_assign)
210
- alias_method(:visit_instance_variable_operator_write_node, :visit_assign)
211
- alias_method(:visit_instance_variable_or_write_node, :visit_assign)
212
- alias_method(:visit_instance_variable_write_node, :visit_assign)
213
-
214
- alias_method(:visit_local_variable_and_write_node, :visit_assign)
215
- alias_method(:visit_local_variable_operator_write_node, :visit_assign)
216
- alias_method(:visit_local_variable_or_write_node, :visit_assign)
217
- alias_method(:visit_local_variable_write_node, :visit_assign)
218
-
219
- alias_method(:visit_multi_write_node, :visit_assign)
220
-
221
- # Is this node a `T` or `::T` constant?
222
- #: (Prism::Node?) -> bool
223
- def t?(node)
224
- case node
225
- when Prism::ConstantReadNode
226
- node.name == :T
227
- when Prism::ConstantPathNode
228
- node.parent.nil? && node.name == :T
229
- else
230
- false
231
- end
232
- end
233
-
234
- # Is this node a `T.let` or `T.cast`?
235
- #: (Prism::CallNode) -> bool
236
- def t_annotation?(node)
237
- return false unless t?(node.receiver)
238
- return false unless ANNOTATION_METHODS.include?(node.name)
239
- return false unless node.arguments&.arguments&.size == 2
240
-
241
- true
242
- end
243
-
244
- #: (Prism::Node) -> bool
245
- def contains_heredoc?(node)
246
- visitor = HeredocVisitor.new
247
- visitor.visit(node)
248
- visitor.contains_heredoc
249
- end
250
-
251
- class HeredocVisitor < Spoom::Visitor
252
- #: bool
253
- attr_reader :contains_heredoc
254
-
255
- #: -> void
256
- def initialize
257
- @contains_heredoc = false #: bool
258
-
259
- super
260
- end
261
-
262
- # @override
263
- #: (Prism::Node?) -> void
264
- def visit(node)
265
- return if node.nil?
266
-
267
- case node
268
- when Prism::StringNode, Prism::InterpolatedStringNode
269
- return @contains_heredoc = !!node.opening_loc&.slice&.match?(/<<~|<<-/)
270
- end
271
-
272
- super
273
- end
274
- end
275
- end
276
- end
277
- end
278
- end
@@ -1,281 +0,0 @@
1
- # typed: strict
2
- # frozen_string_literal: true
3
-
4
- require "rbi"
5
-
6
- module Spoom
7
- module Sorbet
8
- class Sigs
9
- class Error < Spoom::Error; end
10
- class << self
11
- #: (String ruby_contents) -> String
12
- def strip(ruby_contents)
13
- sigs = collect_sorbet_sigs(ruby_contents)
14
- lines_to_strip = sigs.flat_map { |sig, _| (sig.loc&.begin_line..sig.loc&.end_line).to_a }
15
-
16
- lines = []
17
- ruby_contents.lines.each_with_index do |line, index|
18
- lines << line unless lines_to_strip.include?(index + 1)
19
- end
20
- lines.join
21
- end
22
-
23
- #: (String ruby_contents, positional_names: bool) -> String
24
- def rbi_to_rbs(ruby_contents, positional_names: true)
25
- ruby_contents = ruby_contents.dup
26
- sigs = collect_sorbet_sigs(ruby_contents)
27
-
28
- sigs.each do |sig, node|
29
- scanner = Scanner.new(ruby_contents)
30
- start_index = scanner.find_char_position(
31
- T.must(sig.loc&.begin_line&.pred),
32
- T.must(sig.loc).begin_column,
33
- )
34
- end_index = scanner.find_char_position(
35
- sig.loc&.end_line&.pred,
36
- T.must(sig.loc).end_column,
37
- )
38
- rbs = RBIToRBSTranslator.translate(sig, node, positional_names: positional_names)
39
- ruby_contents[start_index...end_index] = rbs
40
- end
41
-
42
- ruby_contents
43
- end
44
-
45
- #: (String ruby_contents) -> String
46
- def rbs_to_rbi(ruby_contents)
47
- ruby_contents = ruby_contents.dup
48
- rbs_comments = collect_rbs_comments(ruby_contents)
49
-
50
- rbs_comments.each do |rbs_comment, node|
51
- scanner = Scanner.new(ruby_contents)
52
- start_index = scanner.find_char_position(
53
- T.must(rbs_comment.loc&.begin_line&.pred),
54
- T.must(rbs_comment.loc).begin_column,
55
- )
56
- end_index = scanner.find_char_position(
57
- rbs_comment.loc&.end_line&.pred,
58
- T.must(rbs_comment.loc).end_column,
59
- )
60
- rbi = RBSToRBITranslator.translate(rbs_comment, node)
61
- next unless rbi
62
-
63
- ruby_contents[start_index...end_index] = rbi
64
- end
65
-
66
- ruby_contents
67
- end
68
-
69
- private
70
-
71
- #: (String ruby_contents) -> Array[[RBI::Sig, (RBI::Method | RBI::Attr)]]
72
- def collect_sorbet_sigs(ruby_contents)
73
- tree = RBI::Parser.parse_string(ruby_contents)
74
- visitor = SigsLocator.new
75
- visitor.visit(tree)
76
- visitor.sigs.sort_by { |sig, _node| -T.must(sig.loc&.begin_line) }
77
- end
78
-
79
- #: (String ruby_contents) -> Array[[RBI::RBSComment, (RBI::Method | RBI::Attr)]]
80
- def collect_rbs_comments(ruby_contents)
81
- tree = RBI::Parser.parse_string(ruby_contents)
82
- visitor = SigsLocator.new
83
- visitor.visit(tree)
84
- visitor.rbs_comments.sort_by { |comment, _node| -T.must(comment.loc&.begin_line) }
85
- end
86
- end
87
-
88
- class SigsLocator < RBI::Visitor
89
- #: Array[[RBI::Sig, (RBI::Method | RBI::Attr)]]
90
- attr_reader :sigs
91
-
92
- #: Array[[RBI::RBSComment, (RBI::Method | RBI::Attr)]]
93
- attr_reader :rbs_comments
94
-
95
- #: -> void
96
- def initialize
97
- super
98
- @sigs = [] #: Array[[RBI::Sig, (RBI::Method | RBI::Attr)]]
99
- @rbs_comments = [] #: Array[[RBI::RBSComment, (RBI::Method | RBI::Attr)]]
100
- end
101
-
102
- # @override
103
- #: (RBI::Node? node) -> void
104
- def visit(node)
105
- return unless node
106
-
107
- case node
108
- when RBI::Method, RBI::Attr
109
- node.sigs.each do |sig|
110
- next if sig.is_abstract
111
-
112
- @sigs << [sig, node]
113
- end
114
- node.comments.grep(RBI::RBSComment).each do |rbs_comment|
115
- @rbs_comments << [rbs_comment, node]
116
- end
117
- when RBI::Tree
118
- visit_all(node.nodes)
119
- end
120
- end
121
- end
122
-
123
- class RBIToRBSTranslator
124
- class << self
125
- #: (RBI::Sig sig, (RBI::Method | RBI::Attr) node, positional_names: bool) -> String
126
- def translate(sig, node, positional_names: true)
127
- case node
128
- when RBI::Method
129
- translate_method_sig(sig, node, positional_names: positional_names)
130
- when RBI::Attr
131
- translate_attr_sig(sig, node, positional_names: positional_names)
132
- end
133
- end
134
-
135
- private
136
-
137
- #: (RBI::Sig sig, RBI::Method node, positional_names: bool) -> String
138
- def translate_method_sig(sig, node, positional_names: true)
139
- out = StringIO.new
140
- p = RBI::RBSPrinter.new(out: out, indent: sig.loc&.begin_column, positional_names: positional_names)
141
-
142
- if sig.without_runtime
143
- p.printn("# @without_runtime")
144
- p.printt
145
- end
146
-
147
- if node.sigs.any?(&:is_final)
148
- p.printn("# @final")
149
- p.printt
150
- end
151
-
152
- if node.sigs.any?(&:is_abstract)
153
- p.printn("# @abstract")
154
- p.printt
155
- end
156
-
157
- if node.sigs.any?(&:is_override)
158
- if node.sigs.any?(&:allow_incompatible_override)
159
- p.printn("# @override(allow_incompatible: true)")
160
- else
161
- p.printn("# @override")
162
- end
163
- p.printt
164
- end
165
-
166
- if node.sigs.any?(&:is_overridable)
167
- p.printn("# @overridable")
168
- p.printt
169
- end
170
-
171
- p.print("#: ")
172
- p.send(:print_method_sig, node, sig)
173
-
174
- out.string
175
- end
176
-
177
- #: (RBI::Sig sig, RBI::Attr node, positional_names: bool) -> String
178
- def translate_attr_sig(sig, node, positional_names: true)
179
- out = StringIO.new
180
- p = RBI::RBSPrinter.new(out: out, positional_names: positional_names)
181
- p.print_attr_sig(node, sig)
182
- "#: #{out.string}"
183
- end
184
- end
185
- end
186
-
187
- class RBSToRBITranslator
188
- class << self
189
- extend T::Sig
190
-
191
- #: (RBI::RBSComment comment, (RBI::Method | RBI::Attr) node) -> String?
192
- def translate(comment, node)
193
- case node
194
- when RBI::Method
195
- translate_method_sig(comment, node)
196
- when RBI::Attr
197
- translate_attr_sig(comment, node)
198
- end
199
- rescue RBS::ParsingError
200
- nil
201
- end
202
-
203
- private
204
-
205
- #: (RBI::RBSComment rbs_comment, RBI::Method node) -> String
206
- def translate_method_sig(rbs_comment, node)
207
- method_type = ::RBS::Parser.parse_method_type(rbs_comment.text)
208
- translator = RBI::RBS::MethodTypeTranslator.new(node)
209
- translator.visit(method_type)
210
-
211
- # TODO: move this to `rbi`
212
- res = translator.result
213
- node.comments.each do |comment|
214
- case comment.text
215
- when "@abstract"
216
- res.is_abstract = true
217
- when "@final"
218
- res.is_final = true
219
- when "@override"
220
- res.is_override = true
221
- when "@override(allow_incompatible: true)"
222
- res.is_override = true
223
- res.allow_incompatible_override = true
224
- when "@overridable"
225
- res.is_overridable = true
226
- when "@without_runtime"
227
- res.without_runtime = true
228
- end
229
- end
230
-
231
- res.string.strip
232
- end
233
-
234
- #: (RBI::RBSComment comment, RBI::Attr node) -> String
235
- def translate_attr_sig(comment, node)
236
- attr_type = ::RBS::Parser.parse_type(comment.text)
237
- sig = RBI::Sig.new
238
-
239
- if node.is_a?(RBI::AttrWriter)
240
- if node.names.size != 1
241
- raise Error, "AttrWriter must have exactly one name"
242
- end
243
-
244
- name = T.must(node.names.first)
245
- sig.params << RBI::SigParam.new(name.to_s, RBI::RBS::TypeTranslator.translate(attr_type))
246
- end
247
-
248
- sig.return_type = RBI::RBS::TypeTranslator.translate(attr_type)
249
- sig.string.strip
250
- end
251
- end
252
- end
253
-
254
- # From https://github.com/Shopify/ruby-lsp/blob/9154bfc6ef/lib/ruby_lsp/document.rb#L127
255
- class Scanner
256
- LINE_BREAK = 0x0A #: Integer
257
-
258
- #: (String source) -> void
259
- def initialize(source)
260
- @current_line = 0 #: Integer
261
- @pos = 0 #: Integer
262
- @source = source.codepoints #: Array[Integer]
263
- end
264
-
265
- # Finds the character index inside the source string for a given line and column
266
- #: (Integer line, Integer character) -> Integer
267
- def find_char_position(line, character)
268
- # Find the character index for the beginning of the requested line
269
- until @current_line == line
270
- @pos += 1 until LINE_BREAK == @source[@pos]
271
- @pos += 1
272
- @current_line += 1
273
- end
274
-
275
- # The final position is the beginning of the line plus the requested column
276
- @pos + character
277
- end
278
- end
279
- end
280
- end
281
- end