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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4e47e1d01da6e3ee465eccc91bb4e3cd29549bb06d585ec0c3b6c6c129120a56
4
- data.tar.gz: fbff7dfda675c426510d4147ab5ebaa281eb3480eb5bf8c09648b143f46a9c40
3
+ metadata.gz: 232ecacb9d25232ca17356951f795baf011b5ec9dd4c18b5688be1c474bccf2b
4
+ data.tar.gz: 824782c6b5b810cae1a88805e3dd196a4499f4f04fce86e57fc933873958cbd9
5
5
  SHA512:
6
- metadata.gz: 2e555b99d0fca2a6e4a2da4cc5b36313d8823d4736c205187f52bab79670e839251cd22f47ba525c171ca3cff6dc59559b95969f3c7fc6aa103ff7d38f1144fa
7
- data.tar.gz: 8a2c8b5e18b55c1f5813e33f2ebe03c86610de90c96b6d805bded2f79278da2897862a97bf70de5843135086373435e22bffc71f10b9aaf71b67d118797731e7
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::Assertions.rbi_to_rbs(contents, file: file)
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"}.")
@@ -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 |_file, contents|
38
- Spoom::Sorbet::Sigs.rbi_to_rbs(contents, positional_names: options[:positional_names])
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 |_file, contents|
42
- Spoom::Sorbet::Sigs.rbs_to_rbi(contents)
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 |_file, contents|
56
- Spoom::Sorbet::Sigs.strip(contents)
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 |_file, contents|
97
- Spoom::Sorbet::Sigs.rbs_to_rbi(contents)
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.
@@ -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, comments = Spoom.parse_ruby_with_comments(rb, file: file)
52
+ node = Spoom.parse_ruby(rb, file: file, comments: true)
53
53
 
54
54
  # Index definitions
55
- model_builder = Model::Builder.new(@model, file, comments: comments)
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
 
@@ -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, ?comments: Array[Prism::Comment]) -> void
9
- def initialize(model, file, comments:)
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
- start_line = node.location.start_line
269
- start_line -= 1 unless @comments_by_line.key?(start_line)
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
- start_line.downto(1) do |line|
272
- comment = @comments_by_line[line]
273
- break unless comment
271
+ last_line = comment.location.start_line
274
272
 
275
- spoom_comment = Comment.new(
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
@@ -84,7 +84,7 @@ module Spoom
84
84
  #: Array[Comment]
85
85
  attr_reader :comments
86
86
 
87
- #: (Symbol symbol, owner: Namespace?, location: Location, ?comments: Array[Comment]) -> void
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.value
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
- raise ParseError, message
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