Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/inline.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,14 @@ module StringCollection
end
```

The constraint can also be a type alias that expands to class instance types, interface types, or an intersection of them:

```ruby
# @rbs module-self: loggable_each[String]
module LoggingCollection
end
```

You can add a description after `--`:

```ruby
Expand Down
17 changes: 17 additions & 0 deletions docs/syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -639,6 +639,8 @@ _module-alias-decl_ ::= `module` _module-name_ `=` _module-name_

_module-self-types_ ::= _class-name_ _type-arguments_ `,` _module-self-types_ (Class instance)
| _interface-name_ _type-arguments_ `,` _module-self-types_ (Interface)
| _alias-name_ _type-arguments_ `,` _module-self-types_ (Type alias)
| _module-self-type_ `&` _module-self-types_ (Intersection)

_interface-decl_ ::= `interface` _interface-name_ _module-type-parameters_ _interface-members_ `end`

Expand Down Expand Up @@ -681,6 +683,21 @@ end

The `Enumerable` module above requires `each` method for enumerating objects.

The self type can be a class, a module, an interface, or a type alias.
Multiple self types can be separated with `,` or `&`, so `module Foo : Bar & _Baz` is the same as `module Foo : Bar, _Baz`.
A type alias used as a self type must expand to a class instance type, an interface type, or an intersection of them, which is equivalent to listing each of them as self types.

```rbs
module Logging
def log: (String) -> void
end

type loggable_each[A] = _Each[A, void] & Logging

module LoggingEnumerable[A] : loggable_each[A] # Same as `module LoggingEnumerable[A] : _Each[A, void], Logging`
end
```

* Self type arguments and generic class bounds are not *classish-context* nor *self-context*

### Class/module alias declaration
Expand Down
5 changes: 4 additions & 1 deletion lib/rbs/cli/validate.rb
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,11 @@ def validate_class_module_definition
end

self_params =
if self_type.name.class?
case
when self_type.name.class?
@env.module_entry(self_type.name, normalized: true)&.type_params
when self_type.name.alias?
@env.type_alias_decls[@env.normalize_type_name(self_type.name)]&.decl&.type_params
else
@env.interface_decls[self_type.name]&.decl&.type_params
end
Expand Down
64 changes: 48 additions & 16 deletions lib/rbs/definition_builder/ancestor_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -249,22 +249,7 @@ def one_instance_ancestors(type_name)
self_types.push Definition::Ancestor::Instance.new(name: BuiltinNames::Object.name, args: [], source: nil)
else
entry.self_types.each do |module_self|
NoSelfTypeFoundError.check!(module_self, env: env)
InvalidTypeApplicationError.check2!(type_name: module_self.name, args: module_self.args, env: env, location: module_self.location)

module_name = module_self.name
if module_name.class?
module_entry = env.module_class_entry(module_name, normalized: true) or raise
module_name = module_entry.name
self_args = AST::TypeParam.normalize_args(module_entry.type_params, module_self.args)
end
if module_name.interface?
interface_entry = env.interface_decls.fetch(module_name)
self_args = AST::TypeParam.normalize_args(interface_entry.decl.type_params, module_self.args)
end
self_args or raise

self_types.push Definition::Ancestor::Instance.new(name: module_name, args: self_args, source: module_self)
push_self_type(self_types, module_self, name: module_self.name, args: module_self.args, location: module_self.location, expanding: [])
end
end
end
Expand Down Expand Up @@ -353,6 +338,53 @@ def one_interface_ancestors(type_name)
end
end

def push_self_type(self_types, module_self, name:, args:, location:, expanding:)
NoSelfTypeFoundError.check!(module_self, env: env, type_name: name, location: location)
name = env.normalize_type_name(name)
InvalidTypeApplicationError.check2!(type_name: name, args: args, env: env, location: location)

case
when name.class?
module_entry = env.module_class_entry(name, normalized: true) or raise
name = module_entry.name
self_args = AST::TypeParam.normalize_args(module_entry.type_params, args)
when name.interface?
interface_entry = env.interface_decls.fetch(name)
self_args = AST::TypeParam.normalize_args(interface_entry.decl.type_params, args)
when name.alias?
if expanding.include?(name)
raise RecursiveTypeAliasError.new(alias_names: [*expanding, name], location: location)
end

# Type aliases are expanded into the class instance and interface types they're composed of
alias_decl = env.type_alias_decls.fetch(name).decl
alias_args = AST::TypeParam.normalize_args(alias_decl.type_params, args)
subst = AST::TypeParam.application(alias_decl.type_params, alias_args) || Substitution.new

each_self_type_component(alias_decl.type.sub(subst), module_self) do |type|
push_self_type(self_types, module_self, name: type.name, args: type.args, location: type.location || location, expanding: [*expanding, name])
end
return
end

self_args or raise

self_types.push Definition::Ancestor::Instance.new(name: name, args: self_args, source: module_self)
end

def each_self_type_component(type, module_self, &block)
case type
when Types::ClassInstance, Types::Interface, Types::Alias
yield type
when Types::Intersection
type.types.each do |t|
each_self_type_component(t, module_self, &block)
end
else
raise InvalidSelfTypeError.new(type: type, module_self: module_self)
end
end

def mixin_ancestors0(decl, type_name, align_params:, included_modules:, included_interfaces:, extended_modules:, prepended_modules:, extended_interfaces:)
case decl
when AST::Declarations::Base
Expand Down
24 changes: 21 additions & 3 deletions lib/rbs/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -215,9 +215,27 @@ def initialize(type_name:, location:)
super "#{Location.to_string location}: Could not find self type: #{type_name}"
end

def self.check!(self_type, env:)
self_name = env.normalize_type_name(self_type.name)
(env.module_name?(self_name) || env.interface_name?(self_name)) or raise new(type_name: self_type.name, location: self_type.location)
def self.check!(self_type, env:, type_name: self_type.name, location: self_type.location)
self_name = env.normalize_type_name(type_name)
(env.module_name?(self_name) || env.interface_name?(self_name) || env.type_alias_name?(self_name)) or raise new(type_name: type_name, location: location)
end
end

class InvalidSelfTypeError < DefinitionError
include DetailedMessageable

attr_reader :type
attr_reader :module_self

def initialize(type:, module_self:)
@type = type
@module_self = module_self

super "#{Location.to_string location}: Self type must be composed of class instance or interface types: #{module_self.name} expands to `#{type}`"
end

def location
module_self.location
end
end

Expand Down
10 changes: 10 additions & 0 deletions sig/ancestor_builder.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,16 @@ module RBS
extended_modules: Array[Definition::Ancestor::Instance]?,
extended_interfaces: Array[Definition::Ancestor::Instance]?) -> void

# Pushes the self type to `self_types`, expanding type aliases into the class instance and interface types they're composed of
#
def push_self_type: (Array[Definition::Ancestor::Instance] self_types, AST::Declarations::Module::Self module_self, name: TypeName, args: Array[Types::t], location: Location[untyped, untyped]?, expanding: Array[TypeName]) -> void

# Yields the components of an expanded self type alias
#
# Raises `InvalidSelfTypeError` if the type is not a class instance type, an interface type, an alias type, or an intersection of them.
#
def each_self_type_component: (Types::t type, AST::Declarations::Module::Self module_self) { (Types::ClassInstance | Types::Interface | Types::Alias) -> void } -> void

def mixin_ancestors0: (AST::Declarations::Class | AST::Declarations::Module | AST::Declarations::Interface | AST::Ruby::Declarations::ClassDecl | AST::Ruby::Declarations::ModuleDecl,
TypeName,
align_params: Substitution?,
Expand Down
16 changes: 15 additions & 1 deletion sig/errors.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,21 @@ module RBS

# The type name in `self` is automatically normalized
#
def self.check!: (AST::Declarations::Module::Self, env: Environment) -> void
def self.check!: (AST::Declarations::Module::Self, env: Environment, ?type_name: TypeName, ?location: Location[untyped, untyped]?) -> void
end

# The error raised when a type alias used as a module self type expands to a type
# other than class instance types, interface types, or intersections of them.
#
class InvalidSelfTypeError < DefinitionError
include DetailedMessageable

attr_reader type: Types::t
attr_reader module_self: AST::Declarations::Module::Self

def initialize: (type: Types::t, module_self: AST::Declarations::Module::Self) -> void

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

class NoMixinFoundError < DefinitionError
Expand Down
11 changes: 8 additions & 3 deletions src/parser.c
Original file line number Diff line number Diff line change
Expand Up @@ -2646,9 +2646,14 @@ static bool parse_interface_decl(rbs_parser_t *parser, rbs_position_t comment_po

/*
module_self_types ::= {`:`} module_self_type `,` ... `,` <module_self_type>
| {`:`} module_self_type `&` ... `&` <module_self_type>

An intersection of self types is equivalent to listing them with `,`.

module_self_type ::= <module_name>
| module_name `[` type_list <`]`>
| <alias_name>
| alias_name `[` type_list <`]`>
*/
RBS_NODISCARD
static bool parse_module_self_types(rbs_parser_t *parser, rbs_node_list_t *array) {
Expand All @@ -2660,7 +2665,7 @@ static bool parse_module_self_types(rbs_parser_t *parser, rbs_node_list_t *array

rbs_range_t name_range;
rbs_type_name_t *module_name = NULL;
CHECK_PARSE(parse_type_name(parser, (TypeNameKind) (CLASS_NAME | INTERFACE_NAME), &name_range, &module_name));
CHECK_PARSE(parse_type_name(parser, (TypeNameKind) (CLASS_NAME | INTERFACE_NAME | ALIAS_NAME), &name_range, &module_name));
self_range.end = name_range.end;

rbs_node_list_t *args = rbs_node_list_new(ALLOCATOR());
Expand All @@ -2679,7 +2684,7 @@ static bool parse_module_self_types(rbs_parser_t *parser, rbs_node_list_t *array
self_type->args_range = RBS_RANGE_LEX2AST(args_range);
rbs_node_list_append(array, (rbs_node_t *) self_type);

if (parser->next_token.type == pCOMMA) {
if (parser->next_token.type == pCOMMA || parser->next_token.type == pAMP) {
rbs_parser_advance(parser);
} else {
break;
Expand Down Expand Up @@ -4124,7 +4129,7 @@ static bool parse_inline_leading_annotation(rbs_parser_t *parser, rbs_ast_ruby_a

rbs_range_t name_range;
rbs_type_name_t *type_name = NULL;
if (!parse_type_name(parser, (TypeNameKind) (CLASS_NAME | INTERFACE_NAME), &name_range, &type_name)) {
if (!parse_type_name(parser, (TypeNameKind) (CLASS_NAME | INTERFACE_NAME | ALIAS_NAME), &name_range, &type_name)) {
return false;
}

Expand Down
98 changes: 98 additions & 0 deletions test/rbs/ancestor_builder_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ class RBS::AncestorBuilderTest < Test::Unit::TestCase
RecursiveAncestorError = RBS::RecursiveAncestorError
SuperclassMismatchError = RBS::SuperclassMismatchError
MixinClassError = RBS::MixinClassError
InvalidSelfTypeError = RBS::InvalidSelfTypeError
RecursiveTypeAliasError = RBS::RecursiveTypeAliasError
NoSelfTypeFoundError = RBS::NoSelfTypeFoundError

def test_one_ancestors_class
SignatureManager.new(system_builtin: true) do |manager|
Expand Down Expand Up @@ -186,6 +189,101 @@ module M[out B] : _EachItem[B], _FooItem[Array[B]]
end
end

def test_one_ancestors_module_self_types_type_alias
SignatureManager.new(system_builtin: true) do |manager|
manager.files[Pathname("a.rbs")] = <<EOF
interface _Items[T]
end

module Mixin
end

class Base[T]
end

type items = _Items[String]
type mixin = Mixin & items
type base[T] = Base[T] & mixin

module M : base[Integer], Comparable
end

module N
type local = Mixin

module M : local
end
end
EOF
manager.build do |env|
builder = DefinitionBuilder::AncestorBuilder.new(env: env)

builder.one_instance_ancestors(type_name("::M")).tap do |a|
# Type aliases are expanded into the class instance and interface types they're composed of
assert_equal [
Ancestor::Instance.new(name: type_name("::Base"), args: [parse_type("::Integer")], source: nil),
Ancestor::Instance.new(name: type_name("::Mixin"), args: [], source: nil),
Ancestor::Instance.new(name: type_name("::_Items"), args: [parse_type("::String")], source: nil),
Ancestor::Instance.new(name: type_name("::Comparable"), args: [], source: nil)
],
a.self_types

a.self_types or raise
assert_equal ["base[Integer]", "base[Integer]", "base[Integer]", "Comparable"], a.self_types.map { _1.source.location.source }
end

builder.one_instance_ancestors(type_name("::N::M")).tap do |a|
assert_equal [Ancestor::Instance.new(name: type_name("::Mixin"), args: [], source: nil)], a.self_types
end
end
end
end

def test_one_ancestors_module_self_types_type_alias_error
SignatureManager.new(system_builtin: true) do |manager|
manager.files[Pathname("a.rbs")] = <<EOF
type union = String | Integer

type rec1 = rec2
type rec2 = rec1 & String

module M1 : union
end

module M2 : rec1
end

module M3 : unknown
end

module M4 : union[String]
end
EOF
manager.build do |env|
builder = DefinitionBuilder::AncestorBuilder.new(env: env)

assert_raises InvalidSelfTypeError do
builder.one_instance_ancestors(type_name("::M1"))
end.tap do |error|
assert_equal "union", error.location.source
assert_equal parse_type("::String | ::Integer"), error.type
end

assert_raises RecursiveTypeAliasError do
builder.one_instance_ancestors(type_name("::M2"))
end

assert_raises NoSelfTypeFoundError do
builder.one_instance_ancestors(type_name("::M3"))
end

assert_raises InvalidTypeApplicationError do
builder.one_instance_ancestors(type_name("::M4"))
end
end
end
end

def test_one_ancestors_module_no_self_type
SignatureManager.new(system_builtin: true) do |manager|
manager.files[Pathname("foo.rbs")] = <<EOF
Expand Down
29 changes: 29 additions & 0 deletions test/rbs/definition_builder_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,35 @@ module M : _StringConvertible
end
end

def test_build_instance_module_self_types_type_alias
SignatureManager.new do |manager|
manager.files[Pathname("foo.rbs")] = <<EOF
interface _Reader[T]
def read: () -> T
end

module Writer
def write: (String) -> void
end

type io[T] = _Reader[T] & Writer

module M : io[String]
alias get read
end
EOF
manager.build do |env|
builder = DefinitionBuilder.new(env: env)

builder.build_instance(type_name("::M")).tap do |definition|
assert_method_definition definition.methods[:read], ["() -> ::String"], accessibility: :public
assert_method_definition definition.methods[:write], ["(::String) -> void"], accessibility: :public
assert_method_definition definition.methods[:get], ["() -> ::String"], accessibility: :public
end
end
end
end

def test_build_instance_module_self_types_type_param_alignment
SignatureManager.new do |manager|
manager.files[Pathname("a.rbs")] = <<EOF
Expand Down
Loading
Loading