Skip to content

Commit 82e1477

Browse files
authored
Merge pull request #2623 from ruby/inline-attributes
Inline RBS declaration for attributes (`attr_***`)
2 parents 1940626 + 7f070c4 commit 82e1477

File tree

12 files changed

+817
-6
lines changed

12 files changed

+817
-6
lines changed

lib/rbs/ast/ruby/members.rb

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,50 @@ class ExtendMember < MixinMember
248248

249249
class PrependMember < MixinMember
250250
end
251+
252+
class AttributeMember < Base
253+
attr_reader :node
254+
attr_reader :name_nodes
255+
attr_reader :type_annotation
256+
attr_reader :leading_comment
257+
258+
def initialize(buffer, node, name_nodes, leading_comment, type_annotation)
259+
super(buffer)
260+
@node = node
261+
@name_nodes = name_nodes
262+
@leading_comment = leading_comment
263+
@type_annotation = type_annotation
264+
end
265+
266+
def names
267+
name_nodes.map do |node|
268+
node.unescaped.to_sym
269+
end
270+
end
271+
272+
def location
273+
rbs_location(node.location)
274+
end
275+
276+
def name_locations
277+
name_nodes.map do |name_node|
278+
rbs_location(name_node.location)
279+
end
280+
end
281+
282+
def type
283+
type_annotation&.type
284+
end
285+
end
286+
287+
class AttrReaderMember < AttributeMember
288+
end
289+
290+
class AttrWriterMember < AttributeMember
291+
end
292+
293+
class AttrAccessorMember < AttributeMember
294+
end
251295
end
252296
end
253297
end

lib/rbs/definition_builder.rb

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,20 @@ def define_instance(definition, type_name, subst, define_class_vars:)
151151
end
152152
end
153153

154+
when AST::Ruby::Members::AttrReaderMember, AST::Ruby::Members::AttrWriterMember, AST::Ruby::Members::AttrAccessorMember
155+
member.names.each do |name|
156+
ivar_name = :"@#{name}"
157+
attr_type = member.type || Types::Bases::Any.new(location: nil)
158+
159+
insert_variable(
160+
type_name,
161+
definition.instance_variables,
162+
name: ivar_name,
163+
type: attr_type,
164+
source: member
165+
)
166+
end
167+
154168
when AST::Members::InstanceVariable
155169
insert_variable(
156170
type_name,
@@ -766,6 +780,59 @@ def define_method(methods, definition, method, subst, self_type_methods, defined
766780
)
767781

768782
method_definition.annotations.replace(original.annotations)
783+
when AST::Ruby::Members::AttrReaderMember, AST::Ruby::Members::AttrWriterMember, AST::Ruby::Members::AttrAccessorMember
784+
if duplicated_method = methods[method.name]
785+
raise DuplicatedMethodDefinitionError.new(
786+
type: definition.self_type,
787+
method_name: method.name,
788+
members: [*duplicated_method.members, original]
789+
)
790+
end
791+
792+
attr_type = original.type || Types::Bases::Any.new(location: nil)
793+
method_type =
794+
if method.name.to_s.end_with?("=")
795+
# setter
796+
MethodType.new(
797+
type_params: [],
798+
type: Types::Function.empty(attr_type).update(
799+
required_positionals: [
800+
Types::Function::Param.new(type: attr_type, name: method.name.to_s.chomp("=").to_sym)
801+
]
802+
),
803+
block: nil,
804+
location: original.location
805+
)
806+
else
807+
# getter
808+
MethodType.new(
809+
type_params: [],
810+
type: Types::Function.empty(attr_type),
811+
block: nil,
812+
location: original.location
813+
)
814+
end
815+
816+
if implemented_in
817+
super_method = existing_method
818+
end
819+
820+
method_definition = Definition::Method.new(
821+
super_method: super_method,
822+
defs: [
823+
Definition::Method::TypeDef.new(
824+
type: method_type,
825+
member: original,
826+
defined_in: defined_in,
827+
implemented_in: implemented_in
828+
)
829+
],
830+
accessibility: method.accessibility,
831+
alias_of: nil,
832+
alias_member: nil
833+
)
834+
835+
method_definition.annotations.replace([])
769836
when AST::Ruby::Members::DefMember
770837
if duplicated_method = methods[method.name]
771838
raise DuplicatedMethodDefinitionError.new(

lib/rbs/definition_builder/method_builder.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,8 @@ def build_instance(type_name)
149149
member: member,
150150
accessibility: :public
151151
)
152+
when AST::Ruby::Members::AttrReaderMember, AST::Ruby::Members::AttrWriterMember, AST::Ruby::Members::AttrAccessorMember
153+
build_ruby_attribute(methods, type, member: member, accessibility: :public)
152154
end
153155
end
154156
end
@@ -227,6 +229,24 @@ def build_attribute(methods, type, member:, accessibility:)
227229
end
228230
end
229231

232+
def build_ruby_attribute(methods, type, member:, accessibility:)
233+
member.names.each do |name|
234+
if member.is_a?(AST::Ruby::Members::AttrReaderMember) || member.is_a?(AST::Ruby::Members::AttrAccessorMember)
235+
defn = methods.methods[name] ||= Methods::Definition.empty(type: type, name: name)
236+
237+
defn.accessibilities << accessibility
238+
defn.originals << member
239+
end
240+
241+
if member.is_a?(AST::Ruby::Members::AttrWriterMember) || member.is_a?(AST::Ruby::Members::AttrAccessorMember)
242+
defn = methods.methods[:"#{name}="] ||= Methods::Definition.empty(type: type, name: :"#{name}=")
243+
244+
defn.accessibilities << accessibility
245+
defn.originals << member
246+
end
247+
end
248+
end
249+
230250
def build_method(methods, type, member:, accessibility:)
231251
defn = methods.methods[member.name] ||= Methods::Definition.empty(type: type, name: member.name)
232252

lib/rbs/diff.rb

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,13 @@ def definition_method_to_s(key, kind, definition_method)
104104
detail_to_s = @detail ? "[#{definition_method.defined_in} #{definition_method.accessibility}] " : ""
105105
if definition_method.alias_of
106106
first_def = definition_method.alias_of.defs.first #: Definition::Method::TypeDef
107-
"#{detail_to_s}alias #{prefix}#{key} #{prefix}#{first_def.member.name}"
107+
member_name = case first_def.member
108+
when AST::Members::Base
109+
first_def.member.name
110+
else
111+
raise
112+
end
113+
"#{detail_to_s}alias #{prefix}#{key} #{prefix}#{member_name}"
108114
else
109115
"#{detail_to_s}def #{prefix}#{key}: #{definition_method.method_types.join(" | ")}"
110116
end

lib/rbs/environment.rb

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -748,6 +748,33 @@ def resolve_ruby_member(resolver, member, context:)
748748
absolute_type_name(resolver, nil, member.module_name, context: context),
749749
resolved_annotation
750750
)
751+
when AST::Ruby::Members::AttrReaderMember
752+
resolved_type_annotation = member.type_annotation&.map_type_name {|name, _, _| absolute_type_name(resolver, nil, name, context: context) }
753+
AST::Ruby::Members::AttrReaderMember.new(
754+
member.buffer,
755+
member.node,
756+
member.name_nodes,
757+
member.leading_comment,
758+
resolved_type_annotation
759+
)
760+
when AST::Ruby::Members::AttrWriterMember
761+
resolved_type_annotation = member.type_annotation&.map_type_name {|name, _, _| absolute_type_name(resolver, nil, name, context: context) }
762+
AST::Ruby::Members::AttrWriterMember.new(
763+
member.buffer,
764+
member.node,
765+
member.name_nodes,
766+
member.leading_comment,
767+
resolved_type_annotation
768+
)
769+
when AST::Ruby::Members::AttrAccessorMember
770+
resolved_type_annotation = member.type_annotation&.map_type_name {|name, _, _| absolute_type_name(resolver, nil, name, context: context) }
771+
AST::Ruby::Members::AttrAccessorMember.new(
772+
member.buffer,
773+
member.node,
774+
member.name_nodes,
775+
member.leading_comment,
776+
resolved_type_annotation
777+
)
751778
else
752779
raise "Unknown member type: #{member.class}"
753780
end

lib/rbs/inline_parser.rb

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,12 @@ def initialize(location, message)
2727
NonConstantClassName = _ = Class.new(Base)
2828
NonConstantModuleName = _ = Class.new(Base)
2929
TopLevelMethodDefinition = _ = Class.new(Base)
30+
TopLevelAttributeDefinition = _ = Class.new(Base)
3031
UnusedInlineAnnotation = _ = Class.new(Base)
3132
AnnotationSyntaxError = _ = Class.new(Base)
3233
MixinMultipleArguments = _ = Class.new(Base)
3334
MixinNonConstantModule = _ = Class.new(Base)
35+
AttributeNonSymbolName = _ = Class.new(Base)
3436
end
3537

3638
def self.parse(buffer, prism)
@@ -171,7 +173,7 @@ def visit_def_node(node)
171173
end
172174

173175
def visit_call_node(node)
174-
return unless node.receiver.nil? # Only handle top-level calls like include, extend, prepend
176+
return unless node.receiver.nil? # Only handle top-level calls like include, extend, prepend, attr_*
175177

176178
case node.name
177179
when :include, :extend, :prepend
@@ -181,6 +183,19 @@ def visit_call_node(node)
181183
when AST::Ruby::Declarations::ClassDecl, AST::Ruby::Declarations::ModuleDecl
182184
parse_mixin_call(node)
183185
end
186+
when :attr_reader, :attr_writer, :attr_accessor
187+
return if skip_node?(node)
188+
189+
case current = current_module
190+
when AST::Ruby::Declarations::ClassDecl, AST::Ruby::Declarations::ModuleDecl
191+
parse_attribute_call(node)
192+
when nil
193+
# Top-level attribute definition
194+
diagnostics << Diagnostic::TopLevelAttributeDefinition.new(
195+
rbs_location(node.message_loc || node.location),
196+
"Top-level attribute definition is not supported"
197+
)
198+
end
184199
else
185200
visit_child_nodes(node)
186201
end
@@ -244,6 +259,66 @@ def parse_mixin_call(node)
244259
current_module!.members << member
245260
end
246261

262+
def parse_attribute_call(node)
263+
# Get the name nodes (arguments to attr_*)
264+
unless node.arguments && !node.arguments.arguments.empty?
265+
return # No arguments, nothing to do
266+
end
267+
268+
name_nodes = [] #: Array[Prism::SymbolNode]
269+
node.arguments.arguments.each do |arg|
270+
case arg
271+
when Prism::SymbolNode
272+
name_nodes << arg
273+
else
274+
# Non-symbol argument, report error
275+
diagnostics << Diagnostic::AttributeNonSymbolName.new(
276+
rbs_location(arg.location),
277+
"Attribute name must be a symbol"
278+
)
279+
end
280+
end
281+
282+
return if name_nodes.empty?
283+
284+
# Look for leading comment block
285+
leading_block = comments.leading_block!(node)
286+
287+
# Look for trailing type annotation (#: Type)
288+
trailing_block = comments.trailing_block!(node.location)
289+
type_annotation = nil
290+
291+
if trailing_block
292+
case annotation = trailing_block.trailing_annotation([])
293+
when AST::Ruby::Annotations::NodeTypeAssertion
294+
type_annotation = annotation
295+
when AST::Ruby::CommentBlock::AnnotationSyntaxError
296+
diagnostics << Diagnostic::AnnotationSyntaxError.new(
297+
annotation.location, "Syntax error: " + annotation.error.error_message
298+
)
299+
end
300+
end
301+
302+
# Report unused leading annotations since @rbs annotations are not used for attributes
303+
if leading_block
304+
report_unused_block(leading_block)
305+
end
306+
307+
# Create the appropriate member type
308+
member = case node.name
309+
when :attr_reader
310+
AST::Ruby::Members::AttrReaderMember.new(buffer, node, name_nodes, leading_block, type_annotation)
311+
when :attr_writer
312+
AST::Ruby::Members::AttrWriterMember.new(buffer, node, name_nodes, leading_block, type_annotation)
313+
when :attr_accessor
314+
AST::Ruby::Members::AttrAccessorMember.new(buffer, node, name_nodes, leading_block, type_annotation)
315+
else
316+
raise "Unexpected attribute method: #{node.name}"
317+
end
318+
319+
current_module!.members << member
320+
end
321+
247322
def insert_declaration(decl)
248323
if current_module
249324
current_module.members << decl

sig/ast/ruby/members.rbs

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ module RBS
1010
include Helpers::LocationHelper
1111
end
1212

13-
type t = DefMember | IncludeMember | ExtendMember | PrependMember
13+
type t = DefMember
14+
| IncludeMember | ExtendMember | PrependMember
15+
| AttrReaderMember | AttrWriterMember | AttrAccessorMember
1416

1517
class MethodTypeAnnotation
1618
class DocStyle
@@ -93,6 +95,35 @@ module RBS
9395

9496
class PrependMember < MixinMember
9597
end
98+
99+
class AttributeMember < Base
100+
attr_reader node: Prism::CallNode
101+
102+
attr_reader name_nodes: Array[Prism::SymbolNode]
103+
104+
attr_reader type_annotation: Annotations::NodeTypeAssertion?
105+
106+
attr_reader leading_comment: CommentBlock?
107+
108+
def initialize: (Buffer, Prism::CallNode, Array[Prism::SymbolNode] name_nodes, RBS::AST::Ruby::CommentBlock? leading_comment, Annotations::NodeTypeAssertion? type_annotation) -> void
109+
110+
def names: () -> Array[Symbol]
111+
112+
def location: () -> Location
113+
114+
def name_locations: () -> Array[Location]
115+
116+
def type: () -> Types::t?
117+
end
118+
119+
class AttrReaderMember < AttributeMember
120+
end
121+
122+
class AttrWriterMember < AttributeMember
123+
end
124+
125+
class AttrAccessorMember < AttributeMember
126+
end
96127
end
97128
end
98129
end

sig/definition.rbs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ module RBS
1212
| AST::Members::InstanceVariable
1313
| AST::Members::ClassVariable
1414
| AST::Members::ClassInstanceVariable
15+
| AST::Ruby::Members::AttrReaderMember
16+
| AST::Ruby::Members::AttrWriterMember
17+
| AST::Ruby::Members::AttrAccessorMember
1518
attr_reader source: source
1619

1720
def initialize: (parent_variable: Variable?, type: Types::t, declared_in: TypeName, source: source) -> void
@@ -21,7 +24,7 @@ module RBS
2124

2225
class Method
2326
type method_member = AST::Members::MethodDefinition | AST::Members::AttrReader | AST::Members::AttrAccessor | AST::Members::AttrWriter
24-
| AST::Ruby::Members::DefMember
27+
| AST::Ruby::Members::DefMember | AST::Ruby::Members::AttrReaderMember | AST::Ruby::Members::AttrWriterMember | AST::Ruby::Members::AttrAccessorMember
2528

2629
class TypeDef
2730
attr_reader type: MethodType

0 commit comments

Comments
 (0)