diff --git a/docs/inline.md b/docs/inline.md index 731dac35a..bd6bb0da8 100644 --- a/docs/inline.md +++ b/docs/inline.md @@ -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 diff --git a/docs/syntax.md b/docs/syntax.md index e3848e701..44636bd20 100644 --- a/docs/syntax.md +++ b/docs/syntax.md @@ -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` @@ -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 diff --git a/lib/rbs/cli/validate.rb b/lib/rbs/cli/validate.rb index 436a58d5e..04a2f654c 100644 --- a/lib/rbs/cli/validate.rb +++ b/lib/rbs/cli/validate.rb @@ -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 diff --git a/lib/rbs/definition_builder/ancestor_builder.rb b/lib/rbs/definition_builder/ancestor_builder.rb index 7cc2c7543..70cd6eff8 100644 --- a/lib/rbs/definition_builder/ancestor_builder.rb +++ b/lib/rbs/definition_builder/ancestor_builder.rb @@ -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 @@ -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 diff --git a/lib/rbs/errors.rb b/lib/rbs/errors.rb index 8c368daf5..e0bd18410 100644 --- a/lib/rbs/errors.rb +++ b/lib/rbs/errors.rb @@ -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 diff --git a/sig/ancestor_builder.rbs b/sig/ancestor_builder.rbs index fbe0097f5..55b6a0d4a 100644 --- a/sig/ancestor_builder.rbs +++ b/sig/ancestor_builder.rbs @@ -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?, diff --git a/sig/errors.rbs b/sig/errors.rbs index c2c716ade..5ea4802bc 100644 --- a/sig/errors.rbs +++ b/sig/errors.rbs @@ -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 diff --git a/src/parser.c b/src/parser.c index fc6f59c72..8b3d3b7ea 100644 --- a/src/parser.c +++ b/src/parser.c @@ -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 `&` ... `&` + + An intersection of self types is equivalent to listing them with `,`. module_self_type ::= | module_name `[` type_list <`]`> + | + | alias_name `[` type_list <`]`> */ RBS_NODISCARD static bool parse_module_self_types(rbs_parser_t *parser, rbs_node_list_t *array) { @@ -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()); @@ -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; @@ -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; } diff --git a/test/rbs/ancestor_builder_test.rb b/test/rbs/ancestor_builder_test.rb index bbd4d2d61..46327c948 100644 --- a/test/rbs/ancestor_builder_test.rb +++ b/test/rbs/ancestor_builder_test.rb @@ -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| @@ -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")] = < 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")] = < bool end end + def test_module_selfs_type_alias + Parser.parse_signature(<<-RBS).tap do |_, _, decls| +module Foo : foo, Bar::baz[String], _Each +end + RBS + module_decl = decls[0] + + assert_instance_of Declarations::Module, module_decl + assert_equal [ + Declarations::Module::Self.new(name: type_name("foo"), args: [], location: nil), + Declarations::Module::Self.new(name: type_name("Bar::baz"), args: [parse_type("String")], location: nil), + Declarations::Module::Self.new(name: type_name("_Each"), args: [], location: nil) + ], module_decl.self_types + assert_equal "foo", module_decl.self_types[0].location[:name].source + assert_equal "Bar::baz[String]", module_decl.self_types[1].location.source + assert_equal "[String]", module_decl.self_types[1].location[:args].source + end + end + + def test_module_selfs_intersection + Parser.parse_signature(<<-RBS).tap do |_, _, decls| +module Foo : Bar & Baz[String], _Each & foo +end + RBS + module_decl = decls[0] + + # An intersection is equivalent to listing the self types with `,` + assert_equal [ + Declarations::Module::Self.new(name: type_name("Bar"), args: [], location: nil), + Declarations::Module::Self.new(name: type_name("Baz"), args: [parse_type("String")], location: nil), + Declarations::Module::Self.new(name: type_name("_Each"), args: [], location: nil), + Declarations::Module::Self.new(name: type_name("foo"), args: [], location: nil) + ], module_decl.self_types + assert_equal "Baz[String]", module_decl.self_types[1].location.source + assert_equal "Bar & Baz[String], _Each & foo", module_decl.location[:self_types].source + end + end + def test_module_selfs Parser.parse_signature(<<-RBS).tap do |_, _, decls| module Enumerable[A, B] : _Each, Object