Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
amomchilov committed Jun 6, 2024
1 parent 556e965 commit a425158
Show file tree
Hide file tree
Showing 4 changed files with 253 additions and 0 deletions.
1 change: 1 addition & 0 deletions lib/rbi.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
require "rbi/rewriters/nest_non_public_methods"
require "rbi/rewriters/group_nodes"
require "rbi/rewriters/remove_known_definitions"
require "rbi/rewriters/replace_attributes_with_methods"
require "rbi/rewriters/sort_nodes"
require "rbi/parser"
require "rbi/printer"
Expand Down
10 changes: 10 additions & 0 deletions lib/rbi/model.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,16 @@ def replace(node)
self.parent_tree = nil
end

sig { params(nodes: T::Enumerable[Node]).void }
def replace_with_multiple(nodes)
tree = parent_tree
raise unless tree

# Does this work?
nodes.each { |node| tree << node }
detach
end

sig { returns(T.nilable(Scope)) }
def parent_scope
parent = T.let(parent_tree, T.nilable(Tree))
Expand Down
177 changes: 177 additions & 0 deletions lib/rbi/rewriters/replace_attributes_with_methods.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
# typed: strict
# frozen_string_literal: true

module RBI
module Rewriters
class ReplaceAttributesWithMethods < Visitor
extend T::Sig

sig { override.params(node: T.nilable(Node)).void }
def visit(node)
return unless node

case node
when Tree
node.nodes.dup.each do |child|
visit(child)
next unless child.is_a?(Attr)

child.replace_with_multiple(child.convert_to_methods)
child.detach
end
end
end
end
end

class Attr
sig { abstract.returns(T.nilable(String)) }
def attribute_type; end

sig { abstract.returns(T::Array[Method]) }
def convert_to_methods; end

sig { returns(T.nilable(Sig)) }
def ensure_max_one_sig!
raise "Attributes cannot have more than 1 sig" unless sigs.one?

sigs.first
end
end

class AttrReader
sig { override.returns(T.nilable(String)) }
def attribute_type = ensure_max_one_sig!&.return_type

sig { override.returns(T::Array[Method]) }
def convert_to_methods
ensure_max_one_sig!
raise "TODO: Support one attr decl with multiple names" unless names.one?

name = names.first.to_s

m = Method.new(
name,
params: [],
visibility: visibility,
sigs: sigs,
loc: loc,
comments: comments,
)

[m]
end
end

class AttrWriter
sig { override.returns(T.nilable(String)) }
def attribute_type = ensure_max_one_sig!&.params&.first&.type

sig { override.returns(T::Array[Method]) }
def convert_to_methods
ensure_max_one_sig!
raise "TODO: Support one attr decl with multiple names" unless names.one?

name = names.first.to_s

method_sigs = if (attribute_type = self.attribute_type)
[
Sig.new(
params: [
SigParam.new(
name,
attribute_type,
# loc: ???
# comments: ???
),
],
return_type: attribute_type,
),
]
else
[]
end

m = Method.new(
"#{name}=",
params: [
ReqParam.new(
name,
# loc: ???
# comments: ???
),
],
visibility: visibility,
sigs: method_sigs,
loc: loc,
comments: comments,
)

[m]
end
end

class AttrAccessor
sig { override.returns(T.nilable(String)) }
def attribute_type = ensure_max_one_sig!&.return_type

sig { override.returns(T::Array[Method]) }
def convert_to_methods
split_into_reader_writer.flat_map(&:convert_to_methods)
end

sig { returns([AttrReader, AttrWriter]) }
def split_into_reader_writer
ensure_max_one_sig!
raise "TODO: Support one attr decl with multiple names" unless names.one?

name = names.first.to_s

reader = AttrReader.new(
*T.unsafe(names),
visibility: visibility,
sigs: attribute_type ? [Sig.new(return_type: attribute_type)] : [],
loc: loc,
comments: comments,
)

writer_sigs = if (attribute_type = self.attribute_type)
[
Sig.new(
params: [
SigParam.new(
name,
attribute_type,
# loc: ???
# comments: ???
),
],
return_type: attribute_type,
),
]
else
[]
end

writer = AttrWriter.new(
*T.unsafe(names),
visibility: visibility,
sigs: writer_sigs,
loc: loc,
comments: comments,
)

[reader, writer]
end
end

class Tree
extend T::Sig

sig { void }
def replace_attributes_with_methods!
visitor = Rewriters::ReplaceAttributesWithMethods.new
visitor.visit(self)
end
end
end
65 changes: 65 additions & 0 deletions test/rbi/rewriters/replace_attributes_with_methods_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# typed: true
# frozen_string_literal: true

require "test_helper"

module RBI
class ReplaceAttributesWithMethods < Minitest::Test
# TODO: Handle `attr_reader :a, :b, :c`

def test_replaces_attr_reader_with_method
rbi = Parser.parse_string(<<~RBI)
sig { returns(Integer) }
attr_reader :a
RBI

rbi.replace_attributes_with_methods!

assert_equal(<<~RBI, rbi.string)
sig { returns(Integer) }
def a; end
RBI
end

def test_replaces_attr_writer_with_setter_method
# TODO: Should we look for `.void`, `.returns(Integer)`, or either?
rbi = Parser.parse_string(<<~RBI)
sig { params(a: Integer).void }
attr_writer :a
RBI

rbi.replace_attributes_with_methods!

# TODO: Should we generate `.void` or `.returns(Integer)`?
#
# I think `.returns(Integer)`, because that's what the method _actually_ returns:
#
# class C
# attr_writer :foo
# end
#
# C.new.send(:foo=, 123) # => 123
assert_equal(<<~RBI, rbi.string)
sig { params(a: Integer).returns(Integer) }
def a=(a); end
RBI
end

def test_replaces_attr_accessor_with_getter_and_setter_methods
rbi = Parser.parse_string(<<~RBI)
sig { returns(Integer) }
attr_accessor :a
RBI

rbi.replace_attributes_with_methods!

assert_equal(<<~RBI, rbi.string)
sig { returns(Integer) }
def a; end
sig { params(a: Integer).returns(Integer) }
def a=(a); end
RBI
end
end
end

0 comments on commit a425158

Please sign in to comment.