spoom 1.6.2 → 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,274 +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 node.sigs.any?(&:is_final)
143
- p.printn("# @final")
144
- p.printt
145
- end
146
-
147
- if node.sigs.any?(&:is_abstract)
148
- p.printn("# @abstract")
149
- p.printt
150
- end
151
-
152
- if node.sigs.any?(&:is_override)
153
- if node.sigs.any?(&:allow_incompatible_override)
154
- p.printn("# @override(allow_incompatible: true)")
155
- else
156
- p.printn("# @override")
157
- end
158
- p.printt
159
- end
160
-
161
- if node.sigs.any?(&:is_overridable)
162
- p.printn("# @overridable")
163
- p.printt
164
- end
165
-
166
- p.print("#: ")
167
- p.send(:print_method_sig, node, sig)
168
-
169
- out.string
170
- end
171
-
172
- #: (RBI::Sig sig, RBI::Attr node, positional_names: bool) -> String
173
- def translate_attr_sig(sig, node, positional_names: true)
174
- out = StringIO.new
175
- p = RBI::RBSPrinter.new(out: out, positional_names: positional_names)
176
- p.print_attr_sig(node, sig)
177
- "#: #{out.string}"
178
- end
179
- end
180
- end
181
-
182
- class RBSToRBITranslator
183
- class << self
184
- extend T::Sig
185
-
186
- #: (RBI::RBSComment comment, (RBI::Method | RBI::Attr) node) -> String?
187
- def translate(comment, node)
188
- case node
189
- when RBI::Method
190
- translate_method_sig(comment, node)
191
- when RBI::Attr
192
- translate_attr_sig(comment, node)
193
- end
194
- rescue RBS::ParsingError
195
- nil
196
- end
197
-
198
- private
199
-
200
- #: (RBI::RBSComment rbs_comment, RBI::Method node) -> String
201
- def translate_method_sig(rbs_comment, node)
202
- method_type = ::RBS::Parser.parse_method_type(rbs_comment.text)
203
- translator = RBI::RBS::MethodTypeTranslator.new(node)
204
- translator.visit(method_type)
205
-
206
- # TODO: move this to `rbi`
207
- res = translator.result
208
- node.comments.each do |comment|
209
- case comment.text
210
- when "@abstract"
211
- res.is_abstract = true
212
- when "@final"
213
- res.is_final = true
214
- when "@override"
215
- res.is_override = true
216
- when "@override(allow_incompatible: true)"
217
- res.is_override = true
218
- res.allow_incompatible_override = true
219
- when "@overridable"
220
- res.is_overridable = true
221
- end
222
- end
223
-
224
- res.string.strip
225
- end
226
-
227
- #: (RBI::RBSComment comment, RBI::Attr node) -> String
228
- def translate_attr_sig(comment, node)
229
- attr_type = ::RBS::Parser.parse_type(comment.text)
230
- sig = RBI::Sig.new
231
-
232
- if node.is_a?(RBI::AttrWriter)
233
- if node.names.size != 1
234
- raise Error, "AttrWriter must have exactly one name"
235
- end
236
-
237
- name = T.must(node.names.first)
238
- sig.params << RBI::SigParam.new(name.to_s, RBI::RBS::TypeTranslator.translate(attr_type))
239
- end
240
-
241
- sig.return_type = RBI::RBS::TypeTranslator.translate(attr_type)
242
- sig.string.strip
243
- end
244
- end
245
- end
246
-
247
- # From https://github.com/Shopify/ruby-lsp/blob/9154bfc6ef/lib/ruby_lsp/document.rb#L127
248
- class Scanner
249
- LINE_BREAK = 0x0A #: Integer
250
-
251
- #: (String source) -> void
252
- def initialize(source)
253
- @current_line = 0 #: Integer
254
- @pos = 0 #: Integer
255
- @source = source.codepoints #: Array[Integer]
256
- end
257
-
258
- # Finds the character index inside the source string for a given line and column
259
- #: (Integer line, Integer character) -> Integer
260
- def find_char_position(line, character)
261
- # Find the character index for the beginning of the requested line
262
- until @current_line == line
263
- @pos += 1 until LINE_BREAK == @source[@pos]
264
- @pos += 1
265
- @current_line += 1
266
- end
267
-
268
- # The final position is the beginning of the line plus the requested column
269
- @pos + character
270
- end
271
- end
272
- end
273
- end
274
- end