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.
@@ -9,22 +9,75 @@ module Spoom
9
9
  LINE_BREAK = "\n".ord #: Integer
10
10
 
11
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)
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
- private
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) -> void
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
- return if @last_sigs.empty?
42
- return if @last_sigs.any? { |_, sig| sig.is_abstract }
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(@last_sigs)
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
- @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)
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
- visit_class_annotation(node)
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
- @nesting << node
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
- @nesting.pop
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
- return if @last_sigs.empty?
134
- return if @last_sigs.any? { |_, sig| sig.is_abstract }
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(@last_sigs)
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
- @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)
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
- 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)
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 node) -> void
172
- def visit_class_annotation(node)
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
- klass = @nesting.last #: as Prism::Node
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(klass.location.start_offset, "# @abstract\n#{indent}")
206
+ @rewriter << Source::Insert.new(parent.location.start_offset, "# @abstract\n#{indent}")
187
207
  when "interface!"
188
- @rewriter << Source::Insert.new(klass.location.start_offset, "# @interface\n#{indent}")
208
+ @rewriter << Source::Insert.new(parent.location.start_offset, "# @interface\n#{indent}")
189
209
  when "sealed!"
190
- @rewriter << Source::Insert.new(klass.location.start_offset, "# @sealed\n#{indent}")
210
+ @rewriter << Source::Insert.new(parent.location.start_offset, "# @sealed\n#{indent}")
191
211
  when "final!"
192
- @rewriter << Source::Insert.new(klass.location.start_offset, "# @final\n#{indent}")
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(klass.location.start_offset, "# @requires_ancestor: #{srb_type.rbs_string}\n#{indent}")
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
- @edits.sort_by(&:range).reverse_each do |edit|
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
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Spoom
5
- VERSION = "1.7.2"
5
+ VERSION = "1.7.4"
6
6
  end