Skip to content

Commit 7ebb16c

Browse files
committed
Fix attribute registration in .rbs files
References #1583
1 parent 80dd414 commit 7ebb16c

3 files changed

Lines changed: 132 additions & 8 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# main
22

3+
- Fix attribute registration in .rbs files (#1583)
4+
35
# [0.9.42] - April 16th, 2026
46

57
[0.9.42]: https://github.com/lsegal/yard/compare/v0.9.41...v0.9.42

lib/yard/handlers/rbs/attribute_handler.rb

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ class YARD::Handlers::RBS::AttributeHandler < YARD::Handlers::RBS::Base
1515
case statement.type
1616
when :attr_reader
1717
register_reader(attr_name, yard_types, mscope)
18+
register_existing_attribute_method(attr_name, "#{attr_name}=", :write, mscope)
1819
when :attr_writer
20+
register_existing_attribute_method(attr_name, attr_name, :read, mscope)
1921
register_writer(attr_name, yard_types, mscope)
2022
when :attr_accessor
2123
register_reader(attr_name, yard_types, mscope)
@@ -26,18 +28,52 @@ class YARD::Handlers::RBS::AttributeHandler < YARD::Handlers::RBS::Base
2628
private
2729

2830
def register_reader(name, types, scope)
29-
obj = register MethodObject.new(namespace, name, scope)
30-
if types && !obj.has_tag?(:return)
31-
obj.add_tag YARD::Tags::Tag.new(:return, '', types)
32-
end
31+
obj = MethodObject.new(namespace, name, scope)
32+
obj.source ||= "def #{name}\n @#{name}\nend"
33+
obj.signature ||= "def #{name}"
34+
obj = register(obj)
35+
obj.docstring = "Returns the value of attribute #{name}." if obj.docstring.blank?(false)
36+
apply_tag_types(obj, :return, types)
37+
namespace.attributes[obj.scope][name] ||= SymbolHash[:read => nil, :write => nil]
38+
namespace.attributes[obj.scope][name][:read] = obj
3339
obj
3440
end
3541

3642
def register_writer(name, types, scope)
37-
obj = register MethodObject.new(namespace, "#{name}=", scope)
38-
if types && !obj.has_tag?(:param)
39-
obj.add_tag YARD::Tags::Tag.new(:param, '', types, "value")
40-
end
43+
obj = MethodObject.new(namespace, "#{name}=", scope)
44+
obj.parameters = [['value', nil]]
45+
obj.source ||= "def #{name}=(value)\n @#{name} = value\nend"
46+
obj.signature ||= "def #{name}=(value)"
47+
obj = register(obj)
48+
obj.docstring = "Sets the attribute #{name}\n@param value the value to set the attribute #{name} to." if obj.docstring.blank?(false)
49+
apply_tag_types(obj, :param, types, "value")
50+
namespace.attributes[obj.scope][name] ||= SymbolHash[:read => nil, :write => nil]
51+
namespace.attributes[obj.scope][name][:write] = obj
4152
obj
4253
end
54+
55+
def register_existing_attribute_method(attr_name, meth_name, type, scope)
56+
namespace.attributes[scope][attr_name] ||= SymbolHash[:read => nil, :write => nil]
57+
return if namespace.attributes[scope][attr_name][type]
58+
59+
obj = namespace.children.find do |other|
60+
other.name == meth_name.to_sym && other.scope == scope
61+
end
62+
63+
namespace.attributes[scope][attr_name][type] = obj if obj
64+
end
65+
66+
def apply_tag_types(obj, tag_name, types, tag_param_name = nil)
67+
return unless types
68+
69+
tag = obj.tags(tag_name).find do |existing_tag|
70+
existing_tag.name == tag_param_name
71+
end
72+
73+
if tag
74+
tag.types ||= types
75+
else
76+
obj.add_tag YARD::Tags::Tag.new(tag_name, '', types, tag_param_name)
77+
end
78+
end
4379
end

spec/handlers/rbs/attribute_handler_spec.rb

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,30 @@
22
require File.dirname(__FILE__) + '/spec_helper'
33

44
RSpec.describe YARD::Handlers::RBS::AttributeHandler do
5+
def parse_ruby(src)
6+
YARD::Registry.clear
7+
YARD.parse_string(src)
8+
end
9+
10+
def attribute_snapshot(namespace_path, attr_name, scope = :instance)
11+
namespace = Registry.at(namespace_path)
12+
attribute = namespace.attributes[scope][attr_name]
13+
read = attribute[:read]
14+
write = attribute[:write]
15+
16+
{
17+
:read_path => read && read.path,
18+
:write_path => write && write.path,
19+
:read_docstring => read ? read.docstring.to_s : nil,
20+
:write_docstring => write ? write.docstring.to_s : nil,
21+
:read_signature => read && read.signature,
22+
:write_signature => write && write.signature,
23+
:write_parameters => write && write.parameters,
24+
:read_is_attribute => read && read.is_attribute?,
25+
:write_is_attribute => write && write.is_attribute?
26+
}
27+
end
28+
529
it "registers a reader method for attr_reader" do
630
parse_rbs <<-RBS
731
class Foo
@@ -13,6 +37,29 @@ class Foo
1337
expect(obj.tag(:return).types).to eq ['String']
1438
end
1539

40+
it "registers attr_reader as a YARD attribute like Ruby parsing does" do
41+
parse_rbs <<-RBS
42+
class Foo
43+
attr_reader name: String
44+
end
45+
RBS
46+
47+
rbs_snapshot = attribute_snapshot('Foo', :name)
48+
reader = Registry.at('Foo#name')
49+
50+
expect(reader.attr_info[:read]).to eq reader
51+
expect(reader.attr_info[:write]).to be_nil
52+
expect(reader.is_attribute?).to be true
53+
54+
parse_ruby <<-RUBY
55+
class Foo
56+
attr_reader :name
57+
end
58+
RUBY
59+
60+
expect(attribute_snapshot('Foo', :name)).to eq rbs_snapshot
61+
end
62+
1663
it "registers a writer method for attr_writer" do
1764
parse_rbs <<-RBS
1865
class Foo
@@ -34,6 +81,32 @@ class Foo
3481
expect(Registry.at('Foo#age=')).to be_a(CodeObjects::MethodObject)
3582
end
3683

84+
it "registers class-side attr_accessor as a YARD attribute like Ruby parsing does" do
85+
parse_rbs <<-RBS
86+
class Foo
87+
attr_accessor self.count: Integer
88+
end
89+
RBS
90+
91+
rbs_snapshot = attribute_snapshot('Foo', :count, :class)
92+
reader = Registry.at('Foo.count')
93+
writer = Registry.at('Foo.count=')
94+
95+
expect(reader.attr_info[:read]).to eq reader
96+
expect(reader.attr_info[:write]).to eq writer
97+
expect(writer.is_attribute?).to be true
98+
99+
parse_ruby <<-RUBY
100+
class Foo
101+
class << self
102+
attr_accessor :count
103+
end
104+
end
105+
RUBY
106+
107+
expect(attribute_snapshot('Foo', :count, :class)).to eq rbs_snapshot
108+
end
109+
37110
it "registers a class-side reader for attr_reader self.name" do
38111
parse_rbs <<-RBS
39112
class Foo
@@ -55,6 +128,19 @@ class Foo
55128
expect(Registry.at('Foo#name').docstring).to eq "The user's name."
56129
end
57130

131+
it "links an existing complementary method into attr_info" do
132+
parse_rbs <<-RBS
133+
class Foo
134+
def name: () -> String
135+
attr_writer name: String
136+
end
137+
RBS
138+
139+
writer = Registry.at('Foo#name=')
140+
expect(writer.attr_info[:read]).to eq Registry.at('Foo#name')
141+
expect(writer.attr_info[:write]).to eq writer
142+
end
143+
58144
it "handles nullable attribute type" do
59145
parse_rbs <<-RBS
60146
class Foo

0 commit comments

Comments
 (0)