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 61d982d
Show file tree
Hide file tree
Showing 4 changed files with 222 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
9 changes: 9 additions & 0 deletions lib/rbi/model.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,15 @@ def replace(node)
self.parent_tree = nil
end

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
147 changes: 147 additions & 0 deletions lib/rbi/rewriters/replace_attributes_with_methods.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
# 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 AttrReader
sig { returns(T::Array[Method]) }
def convert_to_methods
raise "How can there be multiple?" unless sigs.one?
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 { returns(T::Array[Method]) }
def convert_to_methods
raise "How can there be multiple?" unless sigs.one?
raise "TODO: Support one attr decl with multiple names" unless names.one?

sig = sigs.first
name = names.first.to_s

attribute_type = sig.params.first.type

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

[m]
end
end

class AttrAccessor
sig { 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
raise "How can there be multiple?" unless sigs.one?
raise "TODO: Support one attr decl with multiple names" unless names.one?

sig = sigs.first
name = names.first.to_s

attribute_type = sig.return_type

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

writer = AttrWriter.new(
*names,
visibility: visibility,
sigs: [
Sig.new(
params: [
SigParam.new(
name,
attribute_type,
# loc: ???
# comments: ???
),
],
return_type: attribute_type,
),
],
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 61d982d

Please sign in to comment.