Skip to content

Commit 2d24ccf

Browse files
paracycleKaanOzkan
authored andcommitted
Rewrite type aliases
1 parent f86c7c2 commit 2d24ccf

6 files changed

Lines changed: 256 additions & 1 deletion

File tree

lib/spoom/parse.rb

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,22 @@ def parse_ruby(ruby, file:, comments: false)
2424

2525
result.value
2626
end
27+
28+
#: (String ruby, file: String) -> [Prism::Node, Array[Prism::Comment]]
29+
def parse_ruby_with_comments(ruby, file:)
30+
result = Prism.parse(ruby)
31+
32+
unless result.success?
33+
message = +"Error while parsing #{file}:\n"
34+
result.errors.each do |e|
35+
message << "- #{e.message} (at #{e.location.start_line}:#{e.location.start_column})\n"
36+
end
37+
raise ParseError, message
38+
end
39+
40+
result.attach_comments!
41+
42+
[result.value, result.comments]
43+
end
2744
end
2845
end

lib/spoom/rbs.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ def initialize(string, location)
7070

7171
class Annotation < Comment; end
7272
class Signature < Comment; end
73+
class TypeAlias < Comment; end
7374

7475
module ExtractRBSComments
7576
#: (Prism::Node) -> Comments

lib/spoom/sorbet/translate/rbs_comments_to_sorbet_sigs.rb

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,16 @@ def initialize(ruby_contents, file:, max_line_length: nil)
1414
@max_line_length = max_line_length
1515
end
1616

17+
# @override
18+
#: (Prism::ProgramNode node) -> void
19+
def visit_program_node(node)
20+
# Process all type aliases from the entire file first
21+
apply_type_aliases(@comments)
22+
23+
# Now process the rest of the file with type aliases available
24+
super
25+
end
26+
1727
# @override
1828
#: (Prism::ClassNode node) -> void
1929
def visit_class_node(node)
@@ -274,6 +284,77 @@ def already_extends?(node, constant_regex)
274284
true
275285
end
276286
end
287+
288+
#: (Array[Prism::Comment]) -> Array[RBS::TypeAlias]
289+
def collect_type_aliases(comments)
290+
type_aliases = [] #: Array[RBS::TypeAlias]
291+
292+
return type_aliases if comments.empty?
293+
294+
continuation_comments = [] #: Array[Prism::Comment]
295+
296+
comments.reverse_each do |comment|
297+
string = comment.slice
298+
299+
if string.start_with?("#:")
300+
string = string.delete_prefix("#:").strip
301+
location = comment.location
302+
303+
if string.start_with?("type ")
304+
continuation_comments.reverse_each do |continuation_comment|
305+
string = "#{string}#{continuation_comment.slice.delete_prefix("#|")}"
306+
location = location.join(continuation_comment.location)
307+
end
308+
309+
type_aliases << Spoom::RBS::TypeAlias.new(string, location)
310+
end
311+
312+
# Clear the continuation comments regardless of whether we found a type alias or not
313+
continuation_comments.clear
314+
elsif string.start_with?("#|")
315+
continuation_comments << comment
316+
else
317+
continuation_comments.clear
318+
end
319+
end
320+
321+
type_aliases
322+
end
323+
324+
#: (Array[Prism::Comment]) -> void
325+
def apply_type_aliases(comments)
326+
type_aliases = collect_type_aliases(comments)
327+
328+
type_aliases.each do |type_alias|
329+
indent = " " * type_alias.location.start_column
330+
insert_pos = adjust_to_line_start(type_alias.location.start_offset)
331+
332+
from = insert_pos
333+
to = adjust_to_line_end(type_alias.location.end_offset)
334+
335+
*, decls = ::RBS::Parser.parse_signature(type_alias.string)
336+
337+
# We only expect there to be a single type alias declaration
338+
next unless decls.size == 1 && decls.first.is_a?(::RBS::AST::Declarations::TypeAlias)
339+
340+
rbs_type = decls.first
341+
sorbet_type = RBI::RBS::TypeTranslator.translate(rbs_type.type)
342+
343+
alias_name = ::RBS::TypeName.new(
344+
namespace: rbs_type.name.namespace,
345+
name: rbs_type.name.name.to_s.gsub(/(?:^|_)([a-z\d]*)/i) do |match|
346+
match = match.delete_prefix("_")
347+
!match.empty? ? match[0].upcase.concat(match[1..-1]) : +""
348+
end,
349+
)
350+
351+
@rewriter << Source::Delete.new(from, to)
352+
@rewriter << Source::Insert.new(insert_pos, "#{indent}#{alias_name} = T.type_alias { #{sorbet_type.to_rbi} }\n")
353+
rescue ::RBS::ParsingError, ::RBI::Error
354+
# Ignore type aliases with errors
355+
next
356+
end
357+
end
277358
end
278359
end
279360
end

lib/spoom/sorbet/translate/translator.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@ def initialize(ruby_contents, file:)
1919
ruby_contents.encode("UTF-8")
2020
end #: String
2121

22-
node = Spoom.parse_ruby(ruby_contents, file: file, comments: true)
22+
node, comments = Spoom.parse_ruby_with_comments(ruby_contents, file: file)
2323
@node = node #: Prism::Node
24+
@comments = comments #: Array[Prism::Comment]
2425
@ruby_bytes = ruby_contents.bytes #: Array[Integer]
2526
@rewriter = Spoom::Source::Rewriter.new #: Source::Rewriter
2627
end

rbi/spoom.rbi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2584,6 +2584,7 @@ module Spoom::RBS::ExtractRBSComments
25842584
end
25852585

25862586
class Spoom::RBS::Signature < ::Spoom::RBS::Comment; end
2587+
class Spoom::RBS::TypeAlias < ::Spoom::RBS::Comment; end
25872588
Spoom::SPOOM_PATH = T.let(T.unsafe(nil), String)
25882589
module Spoom::Sorbet; end
25892590
Spoom::Sorbet::BIN_PATH = T.let(T.unsafe(nil), String)

test/spoom/sorbet/translate/rbs_comments_to_sorbet_sigs_test.rb

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,160 @@ def bar; end
466466
assert_equal(contents, rbs_comments_to_sorbet_sigs(contents))
467467
end
468468

469+
def test_translate_type_alias
470+
contents = <<~RB
471+
module Aliases
472+
#: type foo = Integer | String
473+
#: type multiLine =
474+
#| Integer |
475+
#| String
476+
end
477+
478+
#: (Aliases::foo a) -> Aliases::multiLine
479+
def bar(a)
480+
42
481+
end
482+
RB
483+
484+
assert_equal(<<~RB, rbs_comments_to_sorbet_sigs(contents))
485+
module Aliases
486+
Foo = T.type_alias { T.any(Integer, String) }
487+
MultiLine = T.type_alias { T.any(Integer, String) }
488+
end
489+
490+
sig { params(a: Aliases::Foo).returns(Aliases::MultiLine) }
491+
def bar(a)
492+
42
493+
end
494+
RB
495+
end
496+
497+
def test_translate_type_alias_with_complex_type
498+
contents = <<~RB
499+
#: type Foo::user_id = Integer
500+
#: type ::Bar::user_data = { id: Foo::user_id, name: String }
501+
502+
#: (::Bar::user_data data) -> Foo::user_id
503+
def process_user(data)
504+
data[:id]
505+
end
506+
RB
507+
508+
assert_equal(<<~RB, rbs_comments_to_sorbet_sigs(contents))
509+
Foo::UserId = T.type_alias { Integer }
510+
::Bar::UserData = T.type_alias { { id: Foo::UserId, name: String } }
511+
512+
sig { params(data: ::Bar::UserData).returns(Foo::UserId) }
513+
def process_user(data)
514+
data[:id]
515+
end
516+
RB
517+
end
518+
519+
def test_translate_type_alias_in_class
520+
contents = <<~RB
521+
class Example
522+
#: type status = :pending | :completed | :failed
523+
524+
#: () -> status
525+
def get_status
526+
:pending
527+
end
528+
end
529+
RB
530+
531+
assert_equal(<<~RB, rbs_comments_to_sorbet_sigs(contents))
532+
class Example
533+
Status = T.type_alias { T.untyped }
534+
535+
sig { returns(Status) }
536+
def get_status
537+
:pending
538+
end
539+
end
540+
RB
541+
end
542+
543+
def test_translate_type_alias_with_generics
544+
contents = <<~RB
545+
#: type list = Array[Integer]
546+
547+
#: (list items) -> list
548+
def double_items(items)
549+
items.map { |x| x * 2 }
550+
end
551+
RB
552+
553+
assert_equal(<<~RB, rbs_comments_to_sorbet_sigs(contents))
554+
List = T.type_alias { T::Array[Integer] }
555+
556+
sig { params(items: List).returns(List) }
557+
def double_items(items)
558+
items.map { |x| x * 2 }
559+
end
560+
RB
561+
end
562+
563+
def test_translate_type_alias_with_union
564+
contents = <<~RB
565+
#: type nullable_string = String?
566+
567+
#: (nullable_string text) -> String
568+
def ensure_string(text)
569+
text || ""
570+
end
571+
RB
572+
573+
assert_equal(<<~RB, rbs_comments_to_sorbet_sigs(contents))
574+
NullableString = T.type_alias { T.nilable(String) }
575+
576+
sig { params(text: NullableString).returns(String) }
577+
def ensure_string(text)
578+
text || ""
579+
end
580+
RB
581+
end
582+
583+
def test_translate_type_alias_that_does_not_exist
584+
contents = <<~RB
585+
#: () -> notFound
586+
def foo
587+
end
588+
RB
589+
590+
assert_equal(<<~RB, rbs_comments_to_sorbet_sigs(contents))
591+
sig { returns(NotFound) }
592+
def foo
593+
end
594+
RB
595+
end
596+
597+
def test_translate_broken_type_alias_continuation
598+
contents = <<~RB
599+
#: type multiLine =
600+
#| String
601+
#| | Integer
602+
# foo bar baz
603+
#| | Symbol
604+
605+
#: () -> multiLine
606+
def foo
607+
""
608+
end
609+
RB
610+
611+
assert_equal(<<~RB, rbs_comments_to_sorbet_sigs(contents))
612+
MultiLine = T.type_alias { T.any(String, Integer) }
613+
# foo bar baz
614+
#| | Symbol
615+
616+
sig { returns(MultiLine) }
617+
def foo
618+
""
619+
end
620+
RB
621+
end
622+
469623
private
470624

471625
#: (String, ?max_line_length: Integer?) -> String

0 commit comments

Comments
 (0)