Skip to content

Commit 3f4ce87

Browse files
authored
Merge pull request #791 from Shopify/uk-type-alias-rewriting
Rewrite type aliases
2 parents f86c7c2 + 5fbfe87 commit 3f4ce87

File tree

12 files changed

+262
-15
lines changed

12 files changed

+262
-15
lines changed

lib/spoom/deadcode/index.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def index_erb(erb, file:, plugins: [])
4949

5050
#: (String rb, file: String, ?plugins: Array[Plugins::Base]) -> void
5151
def index_ruby(rb, file:, plugins: [])
52-
node = Spoom.parse_ruby(rb, file: file, comments: true)
52+
node, _ = Spoom.parse_ruby(rb, file: file)
5353

5454
# Index definitions
5555
model_builder = Model::Builder.new(@model, file)

lib/spoom/parse.rb

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,21 @@ module Spoom
77
class ParseError < Error; end
88

99
class << self
10-
#: (String ruby, file: String, ?comments: bool) -> Prism::Node
11-
def parse_ruby(ruby, file:, comments: false)
10+
#: (String ruby, file: String) -> [Prism::Node, Array[Prism::Comment]]
11+
def parse_ruby(ruby, file:)
1212
result = Prism.parse(ruby)
13+
1314
unless result.success?
1415
message = +"Error while parsing #{file}:\n"
15-
1616
result.errors.each do |e|
1717
message << "- #{e.message} (at #{e.location.start_line}:#{e.location.start_column})\n"
1818
end
19-
2019
raise ParseError, message
2120
end
2221

23-
result.attach_comments! if comments
22+
result.attach_comments!
2423

25-
result.value
24+
[result.value, result.comments]
2625
end
2726
end
2827
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/metrics/code_metrics_visitor.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ def collect_code_metrics(files)
1313
counters.increment("files")
1414

1515
content = File.read(file)
16-
node = Spoom.parse_ruby(content, file: file, comments: true)
16+
node, _ = Spoom.parse_ruby(content, file: file)
1717
visitor = CodeMetricsVisitor.new(counters)
1818
visitor.visit(node)
1919
end

lib/spoom/sorbet/translate/rbs_comments_to_sorbet_sigs.rb

Lines changed: 82 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,78 @@ 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+
content = "#{indent}#{alias_name} = T.type_alias { #{sorbet_type.to_rbi} }\n"
353+
@rewriter << Source::Insert.new(insert_pos, content)
354+
rescue ::RBS::ParsingError, ::RBI::Error
355+
# Ignore type aliases with errors
356+
next
357+
end
358+
end
277359
end
278360
end
279361
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(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: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66

77
module Spoom
88
class << self
9-
sig { params(ruby: ::String, file: ::String, comments: T::Boolean).returns(::Prism::Node) }
10-
def parse_ruby(ruby, file:, comments: T.unsafe(nil)); end
9+
sig { params(ruby: ::String, file: ::String).returns([::Prism::Node, T::Array[::Prism::Comment]]) }
10+
def parse_ruby(ruby, file:); end
1111
end
1212
end
1313

@@ -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)
@@ -2893,6 +2894,9 @@ class Spoom::Sorbet::Translate::RBSCommentsToSorbetSigs < ::Spoom::Sorbet::Trans
28932894
sig { override.params(node: ::Prism::ModuleNode).void }
28942895
def visit_module_node(node); end
28952896

2897+
sig { override.params(node: ::Prism::ProgramNode).void }
2898+
def visit_program_node(node); end
2899+
28962900
sig { override.params(node: ::Prism::SingletonClassNode).void }
28972901
def visit_singleton_class_node(node); end
28982902

@@ -2912,6 +2916,12 @@ class Spoom::Sorbet::Translate::RBSCommentsToSorbetSigs < ::Spoom::Sorbet::Trans
29122916
sig { params(annotations: T::Array[::Spoom::RBS::Annotation], sig: ::RBI::Sig).void }
29132917
def apply_member_annotations(annotations, sig); end
29142918

2919+
sig { params(comments: T::Array[::Prism::Comment]).void }
2920+
def apply_type_aliases(comments); end
2921+
2922+
sig { params(comments: T::Array[::Prism::Comment]).returns(T::Array[::Spoom::RBS::TypeAlias]) }
2923+
def collect_type_aliases(comments); end
2924+
29152925
sig { params(def_node: ::Prism::DefNode, comments: ::Spoom::RBS::Comments).void }
29162926
def rewrite_def(def_node, comments); end
29172927

test/spoom/model/builder_test.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -464,7 +464,7 @@ def m1; end
464464
#: (String rb) -> Model
465465
def model(rb)
466466
file = "foo.rb"
467-
node = Spoom.parse_ruby(rb, file: file, comments: true)
467+
node, _ = Spoom.parse_ruby(rb, file: file)
468468
model = Model.new
469469
builder = Builder.new(model, file)
470470
builder.visit(node)

test/spoom/model/model_test.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ module E; end
104104

105105
#: (String rb) -> Model
106106
def model(rb)
107-
node = Spoom.parse_ruby(rb, file: "foo.rb", comments: true)
107+
node, _ = Spoom.parse_ruby(rb, file: "foo.rb")
108108

109109
model = Model.new
110110
builder = Builder.new(model, "foo.rb")

test/spoom/model/namespace_visitor_test.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ module ::M9::M10; end
101101

102102
#: (String rb) -> Hash[String, String]
103103
def namespaces_for_locs(rb)
104-
node = Spoom.parse_ruby(rb, file: "foo.rb")
104+
node, _ = Spoom.parse_ruby(rb, file: "foo.rb")
105105

106106
visitor = NamespacesForLocs.new
107107
visitor.visit(node)

0 commit comments

Comments
 (0)