Skip to content

Commit e59e949

Browse files
authored
Merge pull request #2630 from ruby/inline--super-class
Support inheritance in Inline RBS declaration
2 parents 5c6d617 + 9cf771a commit e59e949

File tree

9 files changed

+406
-13
lines changed

9 files changed

+406
-13
lines changed

docs/inline.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,26 @@ end
5656

5757
This creates the types `::Client` and `::Client::Error`.
5858

59+
### Inheritance
60+
61+
Class declarations can have a super class.
62+
63+
```ruby
64+
class UsersController < ApplicationController
65+
end
66+
```
67+
68+
The super class specification must be a constant.
69+
70+
The super class specification allows type applications.
71+
72+
```ruby
73+
class StringArray < Array #[String]
74+
end
75+
```
76+
5977
### Current Limitations
6078

61-
- Inheritance is not supported
6279
- Generic class definitions are not supported
6380

6481
## Modules

lib/rbs/ast/ruby/declarations.rb

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,60 @@ def initialize(buffer)
1616
end
1717

1818
class ClassDecl < Base
19+
class SuperClass
20+
attr_reader :type_name_location
21+
22+
attr_reader :operator_location
23+
24+
attr_reader :type_name
25+
26+
attr_reader :type_annotation
27+
28+
def initialize(type_name_location, operator_location, type_name, type_annotation)
29+
@type_name_location = type_name_location
30+
@operator_location = operator_location
31+
@type_name = type_name
32+
@type_annotation = type_annotation
33+
end
34+
35+
def type_args
36+
if type_annotation
37+
type_annotation.type_args
38+
else
39+
[]
40+
end
41+
end
42+
43+
def location
44+
if type_annotation
45+
Location.new(
46+
type_name_location.buffer,
47+
type_name_location.start_pos,
48+
type_annotation.location.end_pos
49+
)
50+
else
51+
type_name_location
52+
end
53+
end
54+
55+
alias name type_name
56+
alias args type_args
57+
end
58+
1959
attr_reader :class_name
2060

2161
attr_reader :members
2262

2363
attr_reader :node
2464

25-
def initialize(buffer, name, node)
65+
attr_reader :super_class
66+
67+
def initialize(buffer, name, node, super_class)
2668
super(buffer)
2769
@class_name = name
2870
@node = node
2971
@members = []
72+
@super_class = super_class
3073
end
3174

3275
def each_decl(&block)
@@ -39,8 +82,6 @@ def each_decl(&block)
3982
end
4083
end
4184

42-
def super_class = nil
43-
4485
def type_params = []
4586

4687
def location

lib/rbs/environment.rb

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -679,7 +679,16 @@ def resolve_ruby_decl(resolver, decl, context:, prefix:)
679679
inner_context = [context, full_name] #: Resolver::context
680680
inner_prefix = full_name.to_namespace
681681

682-
AST::Ruby::Declarations::ClassDecl.new(decl.buffer, full_name, decl.node).tap do |resolved|
682+
super_class = decl.super_class&.yield_self do |super_class|
683+
AST::Ruby::Declarations::ClassDecl::SuperClass.new(
684+
super_class.type_name_location,
685+
super_class.operator_location,
686+
absolute_type_name(resolver, nil, super_class.name, context: context),
687+
super_class.type_annotation&.map_type_name {|name, _, _| absolute_type_name(resolver, nil, name, context: context) }
688+
)
689+
end
690+
691+
AST::Ruby::Declarations::ClassDecl.new(decl.buffer, full_name, decl.node, super_class).tap do |resolved|
683692
decl.members.each do |member|
684693
case member
685694
when AST::Ruby::Declarations::Base

lib/rbs/inline_parser.rb

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ def initialize(location, message)
2626
NotImplementedYet = _ = Class.new(Base)
2727
NonConstantClassName = _ = Class.new(Base)
2828
NonConstantModuleName = _ = Class.new(Base)
29+
NonConstantSuperClassName = _ = Class.new(Base)
2930
TopLevelMethodDefinition = _ = Class.new(Base)
3031
TopLevelAttributeDefinition = _ = Class.new(Base)
3132
UnusedInlineAnnotation = _ = Class.new(Base)
@@ -100,7 +101,13 @@ def visit_class_node(node)
100101
return
101102
end
102103

103-
class_decl = AST::Ruby::Declarations::ClassDecl.new(buffer, class_name, node)
104+
# Parse super class if present
105+
super_class = if node.superclass
106+
node.inheritance_operator_loc or raise
107+
parse_super_class(node.superclass, node.inheritance_operator_loc)
108+
end
109+
110+
class_decl = AST::Ruby::Declarations::ClassDecl.new(buffer, class_name, node, super_class)
104111
insert_declaration(class_decl)
105112
push_module_nesting(class_decl) do
106113
visit_child_nodes(node)
@@ -352,6 +359,39 @@ def report_unused_block(block)
352359
end
353360
end
354361
end
362+
363+
def parse_super_class(super_class_expr, inheritance_operator_loc)
364+
# Check if the superclass is a constant
365+
unless super_class_name = constant_as_type_name(super_class_expr)
366+
diagnostics << Diagnostic::NonConstantSuperClassName.new(
367+
rbs_location(super_class_expr.location),
368+
"Super class name must be a constant"
369+
)
370+
return nil
371+
end
372+
373+
# Look for type application annotation in trailing comments
374+
# For example: class StringArray < Array #[String]
375+
trailing_block = comments.trailing_block!(super_class_expr.location)
376+
type_annotation = nil
377+
378+
if trailing_block
379+
case annotation = trailing_block.trailing_annotation([])
380+
when AST::Ruby::Annotations::TypeApplicationAnnotation
381+
type_annotation = annotation
382+
else
383+
report_unused_annotation(annotation)
384+
end
385+
end
386+
387+
# Create SuperClass object
388+
AST::Ruby::Declarations::ClassDecl::SuperClass.new(
389+
rbs_location(super_class_expr.location),
390+
rbs_location(inheritance_operator_loc),
391+
super_class_name,
392+
type_annotation
393+
)
394+
end
355395
end
356396
end
357397
end

sig/ast/ruby/declarations.rbs

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,41 @@ module RBS
1414
end
1515

1616
class ClassDecl < Base
17+
class SuperClass
18+
attr_reader operator_location: Location
19+
20+
attr_reader type_name_location: Location
21+
22+
attr_reader type_name: TypeName
23+
24+
attr_reader type_annotation: Annotations::TypeApplicationAnnotation?
25+
26+
def type_args: () -> Array[Types::t]
27+
28+
alias name type_name
29+
30+
alias args type_args
31+
32+
def initialize: (Location type_name_location, Location operator_location, TypeName, RBS::AST::Ruby::Annotations::TypeApplicationAnnotation?) -> void
33+
34+
def location: () -> Location
35+
end
36+
1737
type member = t | Members::t
1838

1939
attr_reader class_name: TypeName
2040

2141
attr_reader node: Prism::ClassNode
2242

43+
attr_reader super_class: SuperClass?
44+
2345
attr_reader members: Array[member]
2446

25-
def initialize: (Buffer, TypeName, Prism::ClassNode) -> void
47+
def initialize: (Buffer, TypeName, Prism::ClassNode, SuperClass?) -> void
2648

2749
def each_decl: () { (t) -> void } -> void
2850
| () -> Enumerator[t]
2951

30-
def super_class: () -> nil
31-
3252
def type_params: () -> Array[AST::TypeParam]
3353

3454
def location: () -> Location

sig/errors.rbs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -308,19 +308,21 @@ module RBS
308308
# InheritModuleError is raised if a class definition inherits a module (not a class)
309309
#
310310
class InheritModuleError < DefinitionError
311+
type super_class = AST::Declarations::Class::Super | AST::Ruby::Declarations::ClassDecl::SuperClass
312+
311313
include DetailedMessageable
312314

313-
attr_reader super_decl: AST::Declarations::Class::Super
315+
attr_reader super_decl: super_class
314316

315-
def initialize: (AST::Declarations::Class::Super) -> void
317+
def initialize: (super_class) -> void
316318

317319
def location: () -> Location[untyped, untyped]?
318320

319321
# Confirms if `super` inherits specifies a class
320322
#
321323
# Automatically normalize the name of super.
322324
#
323-
def self.check!: (AST::Declarations::Class::Super, env: Environment) -> void
325+
def self.check!: (super_class, env: Environment) -> void
324326
end
325327

326328
class RecursiveTypeAliasError < BaseError

sig/inline_parser.rbs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ module RBS
2929
class NonConstantModuleName < Base
3030
end
3131

32+
class NonConstantSuperClassName < Base
33+
end
34+
3235
class TopLevelMethodDefinition < Base
3336
end
3437

@@ -51,7 +54,7 @@ module RBS
5154
end
5255

5356
type t = NotImplementedYet
54-
| NonConstantClassName | NonConstantModuleName
57+
| NonConstantClassName | NonConstantModuleName | NonConstantSuperClassName
5558
| TopLevelMethodDefinition | TopLevelAttributeDefinition
5659
| UnusedInlineAnnotation | AnnotationSyntaxError
5760
| MixinMultipleArguments | MixinNonConstantModule
@@ -102,6 +105,8 @@ module RBS
102105
def parse_mixin_call: (Prism::CallNode) -> void
103106

104107
def parse_attribute_call: (Prism::CallNode) -> void
108+
109+
def parse_super_class: (Prism::node, Prism::Location) -> Declarations::ClassDecl::SuperClass?
105110
end
106111
end
107112
end

test/rbs/definition_builder_test.rb

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3254,6 +3254,88 @@ def hello(x) = 123
32543254
end
32553255
end
32563256

3257+
def test_inline_decl__class_with_super
3258+
SignatureManager.new do |manager|
3259+
manager.add_ruby_file(Pathname("a.rb"), <<~RUBY)
3260+
class Parent
3261+
# @rbs () -> String
3262+
def parent_method = ""
3263+
end
3264+
3265+
class Child < Parent
3266+
def child_method
3267+
"child"
3268+
end
3269+
end
3270+
RUBY
3271+
3272+
manager.build do |env|
3273+
builder = DefinitionBuilder.new(env: env)
3274+
3275+
builder.build_instance(type_name("::Child")).tap do |definition|
3276+
# Should have methods from both Child and Parent
3277+
assert_equal Set[:child_method, :parent_method], Set.new(definition.methods.keys) & Set[:child_method, :parent_method]
3278+
3279+
definition.methods[:parent_method].tap do |method|
3280+
assert_equal type_name("::Parent"), method.defined_in
3281+
assert_equal type_name("::Parent"), method.implemented_in
3282+
assert_equal [parse_method_type("() -> ::String")], method.method_types
3283+
end
3284+
3285+
definition.methods[:child_method].tap do |method|
3286+
assert_equal type_name("::Child"), method.defined_in
3287+
assert_equal type_name("::Child"), method.implemented_in
3288+
assert_equal [parse_method_type("(?) -> untyped")], method.method_types
3289+
end
3290+
end
3291+
3292+
# Check ancestors
3293+
ancestors = builder.ancestor_builder.instance_ancestors(type_name("::Child"))
3294+
assert_equal type_name("::Parent"), ancestors.ancestors[1].name
3295+
end
3296+
end
3297+
end
3298+
3299+
def test_inline_decl__class_with_super_type_args
3300+
SignatureManager.new do |manager|
3301+
manager.files[Pathname("generics.rbs")] = <<~RBS
3302+
class MyArray[T]
3303+
def first: () -> T
3304+
end
3305+
RBS
3306+
3307+
manager.add_ruby_file("string_array.rb", <<~RUBY)
3308+
class StringArray < MyArray #[String]
3309+
def last
3310+
"last"
3311+
end
3312+
end
3313+
RUBY
3314+
3315+
manager.build do |env|
3316+
builder = DefinitionBuilder.new(env: env)
3317+
3318+
builder.build_instance(type_name("::StringArray")).tap do |definition|
3319+
# Should have methods from both StringArray and MyArray
3320+
assert_equal Set[:last, :first], Set.new(definition.methods.keys) & Set[:last, :first]
3321+
3322+
definition.methods[:first].tap do |method|
3323+
assert_equal type_name("::MyArray"), method.defined_in
3324+
assert_equal type_name("::MyArray"), method.implemented_in
3325+
# Type should be substituted with String
3326+
assert_equal [parse_method_type("() -> ::String")], method.method_types
3327+
end
3328+
3329+
definition.methods[:last].tap do |method|
3330+
assert_equal type_name("::StringArray"), method.defined_in
3331+
assert_equal type_name("::StringArray"), method.implemented_in
3332+
assert_equal [parse_method_type("(?) -> untyped")], method.method_types
3333+
end
3334+
end
3335+
end
3336+
end
3337+
end
3338+
32573339
def test_ruby_mixin_members
32583340
SignatureManager.new do |manager|
32593341
manager.files[Pathname("modules.rbs")] = <<EOF

0 commit comments

Comments
 (0)