diff --git a/docs/syntax.md b/docs/syntax.md index 57fb2b3a3..de3c2f490 100644 --- a/docs/syntax.md +++ b/docs/syntax.md @@ -5,14 +5,14 @@ ```markdown _type_ ::= _class-name_ _type-arguments_ (Class instance type) | _interface-name_ _type-arguments_ (Interface type) + | _alias-name_ _type-arguments_ (Alias type) | `singleton(` _class-name_ `)` (Class singleton type) - | _alias-name_ (Alias type) | _literal_ (Literal type) | _type_ `|` _type_ (Union type) | _type_ `&` _type_ (Intersection type) | _type_ `?` (Optional type) - | `{` _record-name_ `:` _type_ `,` etc. `}` (Record type) - | `[]` | `[` _type_ `,` etc. `]` (Tuples) + | `{` _record-name_ `:` _type_ `,` etc. `}` (Record type) + | `[]` | `[` _type_ `,` etc. `]` (Tuples) | _type-variable_ (Type variables) | `^(` _parameters_ `) ->` _type_ (Proc type) | `self` @@ -35,8 +35,8 @@ _namespace_ ::= (Empty namespace) | `::` (Root) | _namespace_ /[A-Z]\w*/ `::` (Namespace) -_type-arguments_ ::= (No application) - | `[` _type_ `,` etc. `]` (Type application) +_type-arguments_ ::= (No type arguments) + | `[` _type_ `,` etc. `]` (Type arguments) _literal_ ::= _string-literal_ | _symbol-literal_ @@ -64,25 +64,25 @@ _ToS # _ToS interface ::MyApp::_Each[String] # Interface name with namespace and type application ``` -### Class singleton type - -Class singleton type denotes _the type of a singleton object of a class_. - -``` -singleton(String) -singleton(::Hash) # Class singleton type cannot be parametrized. -``` - ### Alias type Alias type denotes an alias declared with _alias declaration_. The name of type aliases starts with lowercase `[a-z]`. - ``` name ::JSON::t # Alias name with namespace +list[Integer] # Type alias can be generic +``` + +### Class singleton type + +Class singleton type denotes _the type of a singleton object of a class_. + +``` +singleton(String) +singleton(::Hash) # Class singleton type cannot be parametrized. ``` ### Literal type @@ -155,7 +155,7 @@ Elem ``` Type variables cannot be distinguished from _class instance types_. -They are scoped in _class/module/interface declaration_ or _generic method types_. +They are scoped in _class/module/interface/alias declaration_ or _generic method types_. ``` class Ref[T] # Object is scoped in the class declaration. @@ -414,7 +414,6 @@ These work only as _statements_, not per-method specifier. _decl_ ::= _class-decl_ # Class declaration | _module-decl_ # Module declaration | _interface-decl_ # Interface declaration - | _extension-decl_ # Extension declaration | _type-alias-decl_ # Type alias declaration | _const-decl_ # Constant declaration | _global-decl_ # Global declaration @@ -434,9 +433,7 @@ _interface-members_ ::= _method-member_ # Method | _include-member_ # Mixin (include) | _alias-member_ # Alias -_extension-decl_ ::= `extension` _class-name_ _type-parameters_ `(` _extension-name_ `)` _members_ `end` - -_type-alias-decl_ ::= `type` _alias-name_ `=` _type_ +_type-alias-decl_ ::= `type` _alias-name_ _module-type-parameters_ `=` _type_ _const-decl_ ::= _const-name_ `:` _type_ @@ -536,6 +533,12 @@ type subject = Attendee | Speaker type JSON::t = Integer | TrueClass | FalseClass | String | Hash[Symbol, t] | Array[t] ``` +Type alias can be generic like class, module, and interface. + +``` +type list[out T] = [T, list[T]] | nil +``` + ### Constant type declaration You can declare a constant. diff --git a/ext/rbs_extension/parser.c b/ext/rbs_extension/parser.c index 75ea7e0a0..76d6d8f61 100644 --- a/ext/rbs_extension/parser.c +++ b/ext/rbs_extension/parser.c @@ -815,6 +815,8 @@ static VALUE parse_simple(parserstate *state) { } case tULIDENT: // fallthrough + case tLIDENT: + // fallthrough case pCOLON2: { range name_range; range args_range; @@ -857,19 +859,11 @@ static VALUE parse_simple(parserstate *state) { } else if (kind == INTERFACE_NAME) { return rbs_interface(typename, types, location); } else if (kind == ALIAS_NAME) { - return rbs_alias(typename, location); + return rbs_alias(typename, types, location); } else { return Qnil; } } - case tLIDENT: { - VALUE location = rbs_location_current_token(state); - rbs_loc *loc = rbs_check_location(location); - rbs_loc_add_required_child(loc, rb_intern("name"), state->current_token.range); - rbs_loc_add_optional_child(loc, rb_intern("args"), NULL_RANGE); - VALUE typename = parse_type_name(state, ALIAS_NAME, NULL); - return rbs_alias(typename, location); - } case kSINGLETON: { range name_range; range type_range; @@ -1093,12 +1087,98 @@ VALUE parse_const_decl(parserstate *state) { return rbs_ast_decl_constant(typename, type, location, comment); } +/* + module_type_params ::= {} `[` module_type_param `,` ... <`]`> + | {<>} + + module_type_param ::= kUNCHECKED? (kIN|kOUT|) tUIDENT +*/ +VALUE parse_module_type_params(parserstate *state, range *rg) { + VALUE params = rbs_ast_decl_module_type_params(); + + if (state->next_token.type == pLBRACKET) { + parser_advance(state); + + rg->start = state->current_token.range.start; + + while (true) { + VALUE name; + VALUE unchecked = Qfalse; + VALUE variance = ID2SYM(rb_intern("invariant")); + + range param_range = NULL_RANGE; + range name_range; + range variance_range = NULL_RANGE; + range unchecked_range = NULL_RANGE; + + param_range.start = state->next_token.range.start; + + if (state->next_token.type == kUNCHECKED) { + unchecked = Qtrue; + parser_advance(state); + unchecked_range = state->current_token.range; + } + + if (state->next_token.type == kIN || state->next_token.type == kOUT) { + switch (state->next_token.type) { + case kIN: + variance = ID2SYM(rb_intern("contravariant")); + break; + case kOUT: + variance = ID2SYM(rb_intern("covariant")); + break; + default: + rbs_abort(); + } + + parser_advance(state); + variance_range = state->current_token.range; + } + + parser_advance_assert(state, tUIDENT); + name_range = state->current_token.range; + param_range.end = state->current_token.range.end; + + ID id = INTERN_TOKEN(state, state->current_token); + name = ID2SYM(id); + + parser_insert_typevar(state, id); + + VALUE location = rbs_new_location(state->buffer, param_range); + rbs_loc *loc = rbs_check_location(location); + rbs_loc_add_required_child(loc, rb_intern("name"), name_range); + rbs_loc_add_optional_child(loc, rb_intern("variance"), variance_range); + rbs_loc_add_optional_child(loc, rb_intern("unchecked"), unchecked_range); + + VALUE param = rbs_ast_decl_module_type_params_param(name, variance, unchecked, location); + rb_funcall(params, rb_intern("add"), 1, param); + + if (state->next_token.type == pCOMMA) { + parser_advance(state); + } + + if (state->next_token.type == pRBRACKET) { + break; + } + } + + parser_advance_assert(state, pRBRACKET); + rg->end = state->current_token.range.end; + } else { + *rg = NULL_RANGE; + } + + return params; +} + /* type_decl ::= {kTYPE} alias_name `=` */ VALUE parse_type_decl(parserstate *state, position comment_pos, VALUE annotations) { range decl_range; - range keyword_range, name_range, eq_range; + range keyword_range, name_range, params_range, eq_range; + + parser_push_typevar_table(state, true); decl_range.start = state->current_token.range.start; comment_pos = nonnull_pos_or(comment_pos, decl_range.start); @@ -1108,6 +1188,8 @@ VALUE parse_type_decl(parserstate *state, position comment_pos, VALUE annotation parser_advance(state); VALUE typename = parse_type_name(state, ALIAS_NAME, &name_range); + VALUE type_params = parse_module_type_params(state, ¶ms_range); + parser_advance_assert(state, pEQ); eq_range = state->current_token.range; @@ -1118,10 +1200,14 @@ VALUE parse_type_decl(parserstate *state, position comment_pos, VALUE annotation rbs_loc *loc = rbs_check_location(location); rbs_loc_add_required_child(loc, rb_intern("keyword"), keyword_range); rbs_loc_add_required_child(loc, rb_intern("name"), name_range); + rbs_loc_add_optional_child(loc, rb_intern("type_params"), params_range); rbs_loc_add_required_child(loc, rb_intern("eq"), eq_range); + parser_pop_typevar_table(state); + return rbs_ast_decl_alias( typename, + type_params, type, annotations, location, @@ -1184,90 +1270,6 @@ VALUE parse_annotation(parserstate *state) { return rbs_ast_annotation(string, location); } -/* - module_type_params ::= {} `[` module_type_param `,` ... <`]`> - | {<>} - - module_type_param ::= kUNCHECKED? (kIN|kOUT|) tUIDENT -*/ -VALUE parse_module_type_params(parserstate *state, range *rg) { - VALUE params = rbs_ast_decl_module_type_params(); - - if (state->next_token.type == pLBRACKET) { - parser_advance(state); - - rg->start = state->current_token.range.start; - - while (true) { - VALUE name; - VALUE unchecked = Qfalse; - VALUE variance = ID2SYM(rb_intern("invariant")); - - range param_range = NULL_RANGE; - range name_range; - range variance_range = NULL_RANGE; - range unchecked_range = NULL_RANGE; - - param_range.start = state->next_token.range.start; - - if (state->next_token.type == kUNCHECKED) { - unchecked = Qtrue; - parser_advance(state); - unchecked_range = state->current_token.range; - } - - if (state->next_token.type == kIN || state->next_token.type == kOUT) { - switch (state->next_token.type) { - case kIN: - variance = ID2SYM(rb_intern("contravariant")); - break; - case kOUT: - variance = ID2SYM(rb_intern("covariant")); - break; - default: - rbs_abort(); - } - - parser_advance(state); - variance_range = state->current_token.range; - } - - parser_advance_assert(state, tUIDENT); - name_range = state->current_token.range; - param_range.end = state->current_token.range.end; - - ID id = INTERN_TOKEN(state, state->current_token); - name = ID2SYM(id); - - parser_insert_typevar(state, id); - - VALUE location = rbs_new_location(state->buffer, param_range); - rbs_loc *loc = rbs_check_location(location); - rbs_loc_add_required_child(loc, rb_intern("name"), name_range); - rbs_loc_add_optional_child(loc, rb_intern("variance"), variance_range); - rbs_loc_add_optional_child(loc, rb_intern("unchecked"), unchecked_range); - - VALUE param = rbs_ast_decl_module_type_params_param(name, variance, unchecked, location); - rb_funcall(params, rb_intern("add"), 1, param); - - if (state->next_token.type == pCOMMA) { - parser_advance(state); - } - - if (state->next_token.type == pRBRACKET) { - break; - } - } - - parser_advance_assert(state, pRBRACKET); - rg->end = state->current_token.range.end; - } else { - *rg = NULL_RANGE; - } - - return params; -} - /* annotations ::= {} annotation ... | {<>} diff --git a/ext/rbs_extension/ruby_objs.c b/ext/rbs_extension/ruby_objs.c index 9c03e5c92..8f4221495 100644 --- a/ext/rbs_extension/ruby_objs.c +++ b/ext/rbs_extension/ruby_objs.c @@ -70,15 +70,16 @@ VALUE rbs_class_singleton(VALUE typename, VALUE location) { ); } -VALUE rbs_alias(VALUE typename, VALUE location) { - VALUE args = rb_hash_new(); - rb_hash_aset(args, ID2SYM(rb_intern("name")), typename); - rb_hash_aset(args, ID2SYM(rb_intern("location")), location); +VALUE rbs_alias(VALUE typename, VALUE args, VALUE location) { + VALUE kwargs = rb_hash_new(); + rb_hash_aset(kwargs, ID2SYM(rb_intern("name")), typename); + rb_hash_aset(kwargs, ID2SYM(rb_intern("args")), args); + rb_hash_aset(kwargs, ID2SYM(rb_intern("location")), location); return CLASS_NEW_INSTANCE( RBS_Types_Alias, 1, - &args + &kwargs ); } @@ -339,9 +340,10 @@ VALUE rbs_ast_decl_global(VALUE name, VALUE type, VALUE location, VALUE comment) ); } -VALUE rbs_ast_decl_alias(VALUE name, VALUE type, VALUE annotations, VALUE location, VALUE comment) { +VALUE rbs_ast_decl_alias(VALUE name, VALUE type_params, VALUE type, VALUE annotations, VALUE location, VALUE comment) { VALUE args = rb_hash_new(); rb_hash_aset(args, ID2SYM(rb_intern("name")), name); + rb_hash_aset(args, ID2SYM(rb_intern("type_params")), type_params); rb_hash_aset(args, ID2SYM(rb_intern("type")), type); rb_hash_aset(args, ID2SYM(rb_intern("annotations")), annotations); rb_hash_aset(args, ID2SYM(rb_intern("location")), location); diff --git a/ext/rbs_extension/ruby_objs.h b/ext/rbs_extension/ruby_objs.h index 797098b06..15e1b1192 100644 --- a/ext/rbs_extension/ruby_objs.h +++ b/ext/rbs_extension/ruby_objs.h @@ -3,10 +3,10 @@ #include "ruby.h" -VALUE rbs_alias(VALUE typename, VALUE location); +VALUE rbs_alias(VALUE typename, VALUE args, VALUE location); VALUE rbs_ast_annotation(VALUE string, VALUE location); VALUE rbs_ast_comment(VALUE string, VALUE location); -VALUE rbs_ast_decl_alias(VALUE name, VALUE type, VALUE annotations, VALUE location, VALUE comment); +VALUE rbs_ast_decl_alias(VALUE name, VALUE type_params, VALUE type, VALUE annotations, VALUE location, VALUE comment); VALUE rbs_ast_decl_class_super(VALUE name, VALUE args, VALUE location); VALUE rbs_ast_decl_class(VALUE name, VALUE type_params, VALUE super_class, VALUE members, VALUE annotations, VALUE location, VALUE comment); VALUE rbs_ast_decl_constant(VALUE name, VALUE type, VALUE location, VALUE comment); diff --git a/lib/rbs.rb b/lib/rbs.rb index 092c423dd..8d4ece7cb 100644 --- a/lib/rbs.rb +++ b/lib/rbs.rb @@ -45,6 +45,7 @@ require "rbs/ancestor_graph" require "rbs/locator" require "rbs/type_alias_dependency" +require "rbs/type_alias_regularity" require "rbs/collection" require "rbs_extension" diff --git a/lib/rbs/ast/declarations.rb b/lib/rbs/ast/declarations.rb index 42799c512..0fd01e8d5 100644 --- a/lib/rbs/ast/declarations.rb +++ b/lib/rbs/ast/declarations.rb @@ -362,13 +362,15 @@ def to_json(state = _ = nil) class Alias < Base attr_reader :name + attr_reader :type_params attr_reader :type attr_reader :annotations attr_reader :location attr_reader :comment - def initialize(name:, type:, annotations:, location:, comment:) + def initialize(name:, type_params:, type:, annotations:, location:, comment:) @name = name + @type_params = type_params @type = type @annotations = annotations @location = location @@ -378,19 +380,21 @@ def initialize(name:, type:, annotations:, location:, comment:) def ==(other) other.is_a?(Alias) && other.name == name && + other.type_params == type_params && other.type == type end alias eql? == def hash - self.class.hash ^ name.hash ^ type.hash + self.class.hash ^ name.hash ^ type_params.hash ^ type.hash end def to_json(state = _ = nil) { declaration: :alias, name: name, + type_params: type_params, type: type, annotations: annotations, location: location, diff --git a/lib/rbs/cli.rb b/lib/rbs/cli.rb index 9e99bdf35..96fecb17b 100644 --- a/lib/rbs/cli.rb +++ b/lib/rbs/cli.rb @@ -460,7 +460,7 @@ def run_validate(args, options) env.alias_decls.each do |name, decl| stdout.puts "Validating alias: `#{name}`..." - builder.expand_alias(name).tap do |type| + builder.expand_alias1(name).tap do |type| validator.validate_type type, context: [Namespace.root] end validator.validate_type_alias(entry: decl) diff --git a/lib/rbs/definition_builder.rb b/lib/rbs/definition_builder.rb index 0eaf5e6b3..b60943aaf 100644 --- a/lib/rbs/definition_builder.rb +++ b/lib/rbs/definition_builder.rb @@ -781,9 +781,36 @@ def try_cache(type_name, cache:, key: type_name) end def expand_alias(type_name) - entry = env.alias_decls[type_name] or raise "Unknown name for expand_alias: #{type_name}" + expand_alias2(type_name, []) + end + + def expand_alias1(type_name) + entry = env.alias_decls[type_name] or raise "Unknown alias name: #{type_name}" + as = entry.decl.type_params.each.map { Types::Bases::Any.new(location: nil) } + expand_alias2(type_name, as) + end + + def expand_alias2(type_name, args) + entry = env.alias_decls[type_name] or raise "Unknown alias name: #{type_name}" + ensure_namespace!(type_name.namespace, location: entry.decl.location) - entry.decl.type + params = entry.decl.type_params.each.map(&:name) + + unless params.size == args.size + as = "[#{args.join(", ")}]" unless args.empty? + ps = "[#{params.join(", ")}]" unless params.empty? + + raise "Invalid type application: type = #{type_name}#{as}, decl = #{type_name}#{ps}" + end + + type = entry.decl.type + + unless params.empty? + subst = Substitution.build(params, args) + type = type.sub(subst) + end + + type end def update(env:, except:, ancestor_builder:) diff --git a/lib/rbs/environment.rb b/lib/rbs/environment.rb index 81677e151..c8b924c9e 100644 --- a/lib/rbs/environment.rb +++ b/lib/rbs/environment.rb @@ -319,6 +319,7 @@ def resolve_declaration(resolver, decl, outer:, prefix:) when AST::Declarations::Alias AST::Declarations::Alias.new( name: decl.name.with_prefix(prefix), + type_params: decl.type_params, type: absolute_type(resolver, decl.type, context: context), location: decl.location, annotations: decl.annotations, diff --git a/lib/rbs/environment_walker.rb b/lib/rbs/environment_walker.rb index 9629facd3..384da066c 100644 --- a/lib/rbs/environment_walker.rb +++ b/lib/rbs/environment_walker.rb @@ -57,7 +57,7 @@ def tsort_each_child(node, &block) end end when name.alias? - each_type_node builder.expand_alias(name), &block + each_type_node builder.expand_alias1(name), &block else raise "Unexpected TypeNameNode with type_name=#{name}" end @@ -126,6 +126,9 @@ def each_type_node(type, &block) end when RBS::Types::Alias yield TypeNameNode.new(type_name: type.name) + type.args.each do |ty| + each_type_node(ty, &block) + end when RBS::Types::Union, RBS::Types::Intersection, RBS::Types::Tuple type.types.each do |ty| each_type_node ty, &block diff --git a/lib/rbs/errors.rb b/lib/rbs/errors.rb index 3303b3b48..de56e50f8 100644 --- a/lib/rbs/errors.rb +++ b/lib/rbs/errors.rb @@ -431,4 +431,16 @@ def name @alias_names.map(&:name).join(', ') end end + + class NonregularTypeAliasError < LoadingError + attr_reader :diagnostic + attr_reader :location + + def initialize(diagnostic:, location:) + @diagnostic = diagnostic + @location = location + + super "#{Location.to_string location}: Nonregular generic type alias is prohibited: #{diagnostic.type_name}, #{diagnostic.nonregular_type}" + end + end end diff --git a/lib/rbs/type_alias_regularity.rb b/lib/rbs/type_alias_regularity.rb new file mode 100644 index 000000000..54cc2ba2e --- /dev/null +++ b/lib/rbs/type_alias_regularity.rb @@ -0,0 +1,115 @@ +module RBS + class TypeAliasRegularity + class Diagnostic + attr_reader :type_name, :nonregular_type + + def initialize(type_name:, nonregular_type:) + @type_name = type_name + @nonregular_type = nonregular_type + end + end + + attr_reader :env, :builder, :diagnostics + + def initialize(env:) + @env = env + @builder = DefinitionBuilder.new(env: env) + @diagnostics = {} + end + + def validate + diagnostics.clear + + each_mutual_alias_defs do |names| + # Find the first generic type alias in strongly connected component. + # This is to skip the regularity check when the alias is not generic. + names.each do |name| + # @type break: nil + if type = build_alias_type(name) + # Running validation only once from the first generic type is enough, because they are mutual recursive definition. + validate_alias_type(type, names, {}) + break + end + end + end + end + + def validate_alias_type(alias_type, names, types) + if names.include?(alias_type.name) + if ex_type = types[alias_type.name] + unless compatible_args?(ex_type.args, alias_type.args) + diagnostics[alias_type.name] ||= + Diagnostic.new(type_name: alias_type.name, nonregular_type: alias_type) + end + + return + else + types[alias_type.name] = alias_type + end + + expanded = builder.expand_alias2(alias_type.name, alias_type.args) + each_alias_type(expanded) do |at| + validate_alias_type(at, names, types) + end + end + end + + def build_alias_type(name) + entry = env.alias_decls[name] or raise "Unknown alias name: #{name}" + unless entry.decl.type_params.empty? + as = entry.decl.type_params.each.map {|param| Types::Variable.new(name: param.name, location: nil) } + Types::Alias.new(name: name, args: as, location: nil) + end + end + + def compatible_args?(args1, args2) + if args1.size == args2.size + args1.zip(args2).all? do |t1, t2| + t1.is_a?(Types::Bases::Any) || + t2.is_a?(Types::Bases::Any) || + t1 == t2 + end + end + end + + def nonregular?(type_name) + diagnostics[type_name] + end + + def each_mutual_alias_defs(&block) + # @type var each_node: TSort::_EachNode[TypeName] + each_node = __skip__ = -> (&block) do + env.alias_decls.each_value do |decl| + block[decl.name] + end + end + # @type var each_child: TSort::_EachChild[TypeName] + each_child = __skip__ = -> (name, &block) do + type = builder.expand_alias1(name) + each_alias_type(type) do |ty| + block[ty.name] + end + end + + TSort.each_strongly_connected_component(each_node, each_child) do |names| + yield Set.new(names) + end + end + + def each_alias_type(type, &block) + if type.is_a?(RBS::Types::Alias) + yield type + end + + type.each_type do |ty| + each_alias_type(ty, &block) + end + end + + def self.validate(env:) + self.new(env: env).tap do |validator| + validator.validate() + end + end + end +end diff --git a/lib/rbs/types.rb b/lib/rbs/types.rb index 1ebf5fa12..91306b0b5 100644 --- a/lib/rbs/types.rb +++ b/lib/rbs/types.rb @@ -295,39 +295,27 @@ def map_type_name(&block) class Alias attr_reader :location - attr_reader :name - def initialize(name:, location:) + include Application + + def initialize(name:, args:, location:) @name = name + @args = args @location = location end - def ==(other) - other.is_a?(Alias) && other.name == name - end - - alias eql? == - - def hash - self.class.hash ^ name.hash - end - - include NoFreeVariables - include NoSubst - def to_json(state = _ = nil) - { class: :alias, name: name, location: location }.to_json(state) + { class: :alias, name: name, args: args, location: location }.to_json(state) end - def to_s(level = 0) - name.to_s + def sub(s) + Alias.new(name: name, args: args.map {|ty| ty.sub(s) }, location: location) end - include EmptyEachType - - def map_type_name + def map_type_name(&block) Alias.new( name: yield(name, location, self), + args: args.map {|arg| arg.map_type_name(&block) }, location: location ) end diff --git a/lib/rbs/validator.rb b/lib/rbs/validator.rb index e703a7abb..06efa7a2d 100644 --- a/lib/rbs/validator.rb +++ b/lib/rbs/validator.rb @@ -2,10 +2,12 @@ module RBS class Validator attr_reader :env attr_reader :resolver + attr_reader :definition_builder def initialize(env:, resolver:) @env = env @resolver = resolver + @definition_builder = DefinitionBuilder.new(env: env) end def absolute_type(type, context:) @@ -17,8 +19,8 @@ def absolute_type(type, context:) # Validates presence of the relative type, and application arity match. def validate_type(type, context:) case type - when Types::ClassInstance, Types::Interface - # @type var type: Types::ClassInstance | Types::Interface + when Types::ClassInstance, Types::Interface, Types::Alias + # @type var type: Types::ClassInstance | Types::Interface | Types::Alias if type.name.namespace.relative? type = _ = absolute_type(type, context: context) do |_| NoTypeFoundError.check!(type.name.absolute!, env: env, location: type.location) @@ -30,6 +32,8 @@ def validate_type(type, context:) env.class_decls[type.name]&.type_params when Types::Interface env.interface_decls[type.name]&.decl&.type_params + when Types::Alias + env.alias_decls[type.name]&.decl&.type_params end unless type_params @@ -43,8 +47,8 @@ def validate_type(type, context:) location: type.location ) - when Types::Alias, Types::ClassSingleton - # @type var type: Types::Alias | Types::ClassSingleton + when Types::ClassSingleton + # @type var type: Types::ClassSingleton type = _ = absolute_type(type, context: context) { type.name.absolute! } NoTypeFoundError.check!(type.name, env: env, location: type.location) end @@ -55,11 +59,40 @@ def validate_type(type, context:) end def validate_type_alias(entry:) - @type_alias_dependency ||= TypeAliasDependency.new(env: env) - if @type_alias_dependency.circular_definition?(entry.decl.name) + type_name = entry.decl.name + + if type_alias_dependency.circular_definition?(type_name) location = entry.decl.location or raise - raise RecursiveTypeAliasError.new(alias_names: [entry.decl.name], location: location) + raise RecursiveTypeAliasError.new(alias_names: [type_name], location: location) + end + + if diagnostic = type_alias_regularity.nonregular?(type_name) + location = entry.decl.location or raise + raise NonregularTypeAliasError.new(diagnostic: diagnostic, location: location) + end + + unless entry.decl.type_params.empty? + calculator = VarianceCalculator.new(builder: definition_builder) + result = calculator.in_type_alias(name: type_name) + if set = result.incompatible?(entry.decl.type_params) + set.each do |param_name| + param = entry.decl.type_params[param_name] or raise + raise InvalidVarianceAnnotationError.new( + type_name: type_name, + param: param, + location: entry.decl.type.location + ) + end + end end end + + def type_alias_dependency + @type_alias_dependency ||= TypeAliasDependency.new(env: env) + end + + def type_alias_regularity + @type_alias_regularity ||= TypeAliasRegularity.validate(env: env) + end end end diff --git a/lib/rbs/variance_calculator.rb b/lib/rbs/variance_calculator.rb index 557de9db1..169cb4674 100644 --- a/lib/rbs/variance_calculator.rb +++ b/lib/rbs/variance_calculator.rb @@ -54,6 +54,21 @@ def compatible?(var, with_annotation:) false end end + + def incompatible?(params) + # @type set: Hash[Symbol] + set = Set[] + + params.each do |param| + unless compatible?(param.name, with_annotation: param.variance) + set << param.name + end + end + + unless set.empty? + set + end + end end attr_reader :builder @@ -69,19 +84,12 @@ def env def in_method_type(method_type:, variables:) result = Result.new(variables: variables) - method_type.type.each_param do |param| - type(param.type, result: result, context: :contravariant) - end + function(method_type.type, result: result, context: :covariant) if block = method_type.block - block.type.each_param do |param| - type(param.type, result: result, context: :covariant) - end - type(block.type.return_type, result: result, context: :contravariant) + function(block.type, result: result, context: :contravariant) end - type(method_type.type.return_type, result: result, context: :covariant) - result end @@ -97,6 +105,14 @@ def in_inherit(name:, args:, variables:) end end + def in_type_alias(name:) + decl = env.alias_decls[name].decl or raise + variables = decl.type_params.each.map(&:name) + Result.new(variables: variables).tap do |result| + type(decl.type, result: result, context: :covariant) + end + end + def type(type, result:, context:) case type when Types::Variable @@ -110,7 +126,7 @@ def type(type, result:, context:) result.invariant(type.name) end end - when Types::ClassInstance, Types::Interface + when Types::ClassInstance, Types::Interface, Types::Alias NoTypeFoundError.check!(type.name, env: env, location: type.location) @@ -120,6 +136,8 @@ def type(type, result:, context:) env.class_decls[type.name].type_params when Types::Interface env.interface_decls[type.name].decl.type_params + when Types::Alias + env.alias_decls[type.name].decl.type_params end type.args.each.with_index do |ty, i| @@ -130,26 +148,36 @@ def type(type, result:, context:) when :covariant type(ty, result: result, context: context) when :contravariant - # @type var con: variance - con = case context - when :invariant - :invariant - when :covariant - :contravariant - when :contravariant - :covariant - else - raise - end - type(ty, result: result, context: con) + type(ty, result: result, context: negate(context)) end end - when Types::Tuple, Types::Record, Types::Union, Types::Intersection - # Covariant types + when Types::Proc + function(type.type, result: result, context: context) + else type.each_type do |ty| type(ty, result: result, context: context) end end end + + def function(type, result:, context:) + type.each_param do |param| + type(param.type, result: result, context: negate(context)) + end + type(type.return_type, result: result, context: context) + end + + def negate(variance) + case variance + when :invariant + :invariant + when :covariant + :contravariant + when :contravariant + :covariant + else + raise + end + end end end diff --git a/lib/rbs/writer.rb b/lib/rbs/writer.rb index 41974fbe9..6aa407a0b 100644 --- a/lib/rbs/writer.rb +++ b/lib/rbs/writer.rb @@ -119,7 +119,7 @@ def write_decl(decl) when AST::Declarations::Alias write_comment decl.comment write_annotation decl.annotations - puts "type #{decl.name} = #{decl.type}" + puts "type #{name_and_params(decl.name, decl.type_params)} = #{decl.type}" when AST::Declarations::Interface write_comment decl.comment diff --git a/schema/decls.json b/schema/decls.json index 7c029eee2..88aba904b 100644 --- a/schema/decls.json +++ b/schema/decls.json @@ -12,6 +12,18 @@ "name": { "type": "string" }, + "type_params": { + "type": "object", + "properties": { + "params": { + "type": "array", + "items": { + "$ref": "#/definitions/moduleTypeParam" + } + } + }, + "required": ["params"] + }, "type": { "$ref": "types.json" }, @@ -28,7 +40,7 @@ "$ref": "comment.json" } }, - "required": ["declaration", "name", "type", "annotations", "location", "comment"] + "required": ["declaration", "name", "type_params", "type", "annotations", "location", "comment"] }, "constant": { "title": "Constant declaration: `VERSION: String`, ...", diff --git a/schema/types.json b/schema/types.json index a2387412e..b81756dcc 100644 --- a/schema/types.json +++ b/schema/types.json @@ -106,7 +106,7 @@ "required": ["class", "name", "args", "location"] }, "alias": { - "title": "Type alias: `u`, `ty`, `json`, ...", + "title": "Type alias: `u`, `ty`, `json`, `list[Integer]`, ...", "type": "object", "properties": { "class": { @@ -116,11 +116,17 @@ "name": { "type": "string" }, + "args": { + "type": "array", + "items": { + "$ref": "#" + } + }, "location": { "$ref": "location.json" } }, - "required": ["class", "name", "location"] + "required": ["class", "name", "args", "location"] }, "tuple": { "title": "Tuple type: `[Foo, bar]`, ...", diff --git a/sig/declarations.rbs b/sig/declarations.rbs index 474ca1048..4823a92d8 100644 --- a/sig/declarations.rbs +++ b/sig/declarations.rbs @@ -217,19 +217,22 @@ module RBS end class Alias < Base - # type loc = Location - # ^^^^ keyword - # ^^^ name - # ^ eq - type loc = Location[:keyword | :name | :eq, bot] + # type loc[T] = Location[T, bot] + # ^^^^ keyword + # ^^^ name + # ^^^ type_params + # ^ eq + # + type loc = Location[:keyword | :name | :eq, :type_params] attr_reader name: TypeName + attr_reader type_params: ModuleTypeParams attr_reader type: Types::t attr_reader annotations: Array[Annotation] attr_reader location: loc? attr_reader comment: Comment? - def initialize: (name: TypeName, type: Types::t, annotations: Array[Annotation], location: loc?, comment: Comment?) -> void + def initialize: (name: TypeName, type_params: ModuleTypeParams, type: Types::t, annotations: Array[Annotation], location: loc?, comment: Comment?) -> void include _HashEqual include _ToJson diff --git a/sig/definition_builder.rbs b/sig/definition_builder.rbs index d74807003..dc3f42869 100644 --- a/sig/definition_builder.rbs +++ b/sig/definition_builder.rbs @@ -43,8 +43,37 @@ module RBS def define_methods: (Definition, interface_methods: Hash[Symbol, Definition::Method], methods: MethodBuilder::Methods, super_interface_method: bool) -> void + # Expand a type alias of given name without type arguments. + # Raises an error if the type alias requires arguments. + # + # Assume `type foo[T] = [T, T]`: + # + # ``` + # expand_alias("::foo") # => error + # ``` + # def expand_alias: (TypeName) -> Types::t + # Expand a type alias of given name with arguments of `untyped`. + # + # Assume `type foo[T] = [T, T]`: + # + # ``` + # expand_alias1("::foo") # => [untyped, untyped] + # ``` + # + def expand_alias1: (TypeName) -> Types::t + + # Expand a type alias of given name with `args`. + # + # Assume `type foo[T] = [T, T]`: + # + # ``` + # expand_alias2("::foo", ["::Integer"]) # => [::Integer, ::Integer] + # ``` + # + def expand_alias2: (TypeName, Array[Types::t] args) -> Types::t + def update: (env: Environment, ancestor_builder: AncestorBuilder, except: _Each[TypeName]) -> DefinitionBuilder end end diff --git a/sig/environment_walker.rbs b/sig/environment_walker.rbs index 825ac1236..c7e59fbab 100644 --- a/sig/environment_walker.rbs +++ b/sig/environment_walker.rbs @@ -1,4 +1,28 @@ module RBS + # EnvironmentWalker provides topological sort of class/module definitions. + # + # If a method, attribute, or ancestor in a class definition have a reference to another class, it is dependency. + # + # ```rb + # walker = EnvironmentWalker.new(env: env) + # + # walker.each_strongly_connected_component do |scc| + # # Yields an array of strongly connected components. + # end + # ``` + # + # The `#only_ancestors!` method limits the dependency only to ancestors. + # Only super classes and included modules are dependencies with the option. + # This is useful to calculate the dependencies of class hierarchy. + # + # ```rb + # walker = EnvironmentWalker.new(env: env).only_ancestors! + # + # walker.each_strongly_connected_component do |scc| + # # Yields an array of strongly connected components. + # end + # ``` + # class EnvironmentWalker class InstanceNode attr_reader type_name: TypeName @@ -32,6 +56,8 @@ module RBS def tsort_each_child: (node) { (node) -> void } -> void + private + def each_type_name: (Types::t) { (TypeName) -> void } -> void def each_type_node: (Types::t) { (node) -> void } -> void diff --git a/sig/errors.rbs b/sig/errors.rbs index bf4bae552..de95bc2f9 100644 --- a/sig/errors.rbs +++ b/sig/errors.rbs @@ -220,4 +220,14 @@ module RBS def name: () -> String end + + class NonregularTypeAliasError < LoadingError + # Diagnostic reported from `TypeAliasRegularity`. + attr_reader diagnostic: TypeAliasRegularity::Diagnostic + + # Location of the definition. + attr_reader location: Location[untyped, untyped]? + + def initialize: (diagnostic: TypeAliasRegularity::Diagnostic, location: Location[untyped, untyped]?) -> void + end end diff --git a/sig/type_alias_regularity.rbs b/sig/type_alias_regularity.rbs new file mode 100644 index 000000000..ab4e0f748 --- /dev/null +++ b/sig/type_alias_regularity.rbs @@ -0,0 +1,92 @@ +module RBS + # `TypeAliasRegularity` validates if a type alias is regular or not. + # + # Generic and recursive type alias cannot be polymorphic in their definitions. + # + # ```rbs + # type foo[T] = Integer + # | foo[T]? # Allowed. The type argument of `foo` doesn't change. + # + # type bar[T] = Integer + # | foo[T] + # | foo[Array[T]] # Allowed. There are two type arguments `T` and `Array[T]` of `foo`, but it's not definition of `foo`. + # + # type baz[T] = Integer + # | baz[Array[T]] # Error. Recursive definition of `baz` has different type argument from the definition. + # ``` + # + # The `#nonregular?` method can be used to test if given type name is regular or not. + # + # ```rb + # validator = RBS::TypeAliasRegularity.validate(env: env) + # + # validator.nonregular?(TypeName("::foo")) # => nil + # validator.nonregular?(TypeName("::bar")) # => nil + # validator.nonregular?(TypeName("::baz")) # => TypeAliasRegularity::Diagnostic + # ``` + # + # A special case is when the type argument is `untyped`. + # + # ```rbs + # type foo[T] = Integer | foo[untyped] # This is allowed. + # ``` + # + class TypeAliasRegularity + attr_reader env: Environment + + attr_reader builder: DefinitionBuilder + + attr_reader diagnostics: Hash[TypeName, Diagnostic] + + # `Diagnostic` represents an non-regular type alias declaration error. + # It consists of the name of the alias type and a type on which the nonregularity is detected. + # + # ```rbs + # type t[T] = Integer | t[T?] + # ``` + # + # The type `t` is nonregular because it contains `t[T?]` on it's right hand side. + # + # ``` + # diagnostic = validator.nonregular?(TypeName("::t")) + # diagnostic.type_name # => TypeName("::t") + # diagnostic.nonregular_type # => t[T?] + # ``` + # + class Diagnostic + attr_reader type_name: TypeName + + attr_reader nonregular_type: Types::Alias + + def initialize: (type_name: TypeName, nonregular_type: Types::Alias) -> void + end + + # Returns new instance which already run `#validate`. + # + def self.validate: (env: Environment) -> TypeAliasRegularity + + def initialize: (env: Environment) -> void + + # Returns `Diagnostic` instance if the alias type is nonregular. + # Regurns `nil` if the alias type is regular. + # + def nonregular?: (TypeName) -> Diagnostic? + + def validate: () -> void + + private + + def validate_alias_type: (Types::Alias, Set[TypeName], Hash[TypeName, Types::Alias]) -> void + + # Returns alias type for given type name, if the alias is generic. + # Returns nil if the type alias is not generic. + # + def build_alias_type: (TypeName) -> Types::Alias? + + def compatible_args?: (Array[Types::t], Array[Types::t]) -> boolish + + def each_alias_type: (Types::t) { (Types::Alias) -> void } -> void + + def each_mutual_alias_defs: () { (Set[TypeName]) -> void } -> void + end +end diff --git a/sig/types.rbs b/sig/types.rbs index 7c9fe96eb..2ce9767cc 100644 --- a/sig/types.rbs +++ b/sig/types.rbs @@ -227,18 +227,21 @@ module RBS end class Alias - attr_reader name: TypeName + # foo + # ^^^ => name + # + # foo[bar, baz] + # ^^^ => name + # ^^^^^^^^^^ => args + # + type loc = Location[:name, :args] - type loc = Location[bot, bot] + attr_reader location: loc? - def initialize: (name: TypeName, location: loc?) -> void + def initialize: (name: TypeName, args: Array[t], location: loc?) -> void include _TypeBase - include NoFreeVariables - include NoSubst - include EmptyEachType - - attr_reader location: loc? + include Application end class Tuple diff --git a/sig/validator.rbs b/sig/validator.rbs index 4b184f074..4428672cb 100644 --- a/sig/validator.rbs +++ b/sig/validator.rbs @@ -1,8 +1,15 @@ module RBS class Validator attr_reader env: Environment + attr_reader resolver: TypeNameResolver + attr_reader definition_builder: DefinitionBuilder + + attr_reader type_alias_dependency: TypeAliasDependency + + attr_reader type_alias_regularity: TypeAliasRegularity + def initialize: (env: Environment, resolver: TypeNameResolver) -> void def absolute_type: (Types::t, context: TypeNameResolver::context) { (Types::t) -> TypeName } -> Types::t diff --git a/sig/variance_calculator.rbs b/sig/variance_calculator.rbs index 598ee38db..771039b16 100644 --- a/sig/variance_calculator.rbs +++ b/sig/variance_calculator.rbs @@ -1,7 +1,47 @@ module RBS + # Calculate the use variances of type variables in declaration. + # + # ```rb + # calculator = VarianceCalculator.new(builder: builder) + # + # # Calculates variances in a method type + # result = calculator.in_method_type(method_type: method_type, variables: variables) + # + # # Calculates variances in a inheritance/mixin/... + # result = calculator.in_inherit(name: name, args: args, variables: variables) + # + # # Calculates variances in a type alias + # result = calculator.in_type_alias(name: name, args: args, variables: variables) + # ``` + # + # See `RBS::VarianceCaluculator::Result` for information recorded in the `Result` object. + # class VarianceCalculator type variance = :unused | :covariant | :contravariant | :invariant + # Result contains the set of type variables and it's variance in a occurrence. + # + # ```rb + # # Enumerates recorded type variables + # result.each do |name, variance| + # # name is the name of a type variable + # # variance is one of :unused | :covariant | :contravariant | :invariant + # end + # ``` + # + # You can test with `compatible?` method if the type variable occurrences are compatible with specified (annotated) variance. + # + # ```rb + # # When T is declared as `out T` + # result.compatible?(:T, with_annotation: :covariant) + # + # # When T is declared as `in T` + # result.compatible?(:T, with_annotation: :contravariant) + # + # # When T is declared as `T` + # result.compatible?(:T, with_annotation: :invariant) + # ``` + # class Result attr_reader result: Hash[Symbol, variance] @@ -18,6 +58,8 @@ module RBS def include?: (Symbol) -> bool def compatible?: (Symbol, with_annotation: variance) -> bool + + def incompatible?: (AST::Declarations::ModuleTypeParams) -> Set[Symbol]? end attr_reader builder: DefinitionBuilder @@ -30,6 +72,14 @@ module RBS def in_inherit: (name: TypeName, args: Array[Types::t], variables: Array[Symbol]) -> Result + def in_type_alias: (name: TypeName) -> Result + + private + def type: (Types::t, result: Result, context: variance) -> void + + def function: (Types::Function, result: Result, context: variance) -> void + + def negate: (variance) -> variance end end diff --git a/test/rbs/definition_builder_test.rb b/test/rbs/definition_builder_test.rb index 96b74aa34..7579400cf 100644 --- a/test/rbs/definition_builder_test.rb +++ b/test/rbs/definition_builder_test.rb @@ -1969,4 +1969,35 @@ def bar: () -> Y end end end + + def test_expand_alias2 + SignatureManager.new do |manager| + manager.files.merge!(Pathname("foo.rbs") => <<-EOF) +type opt[T] = T | nil +type pair[S, T] = [S, T] + EOF + + manager.build do |env| + builder = DefinitionBuilder.new(env: env) + + assert_equal( + parse_type("::Integer | nil"), + builder.expand_alias2(type_name("::opt"), [parse_type("::Integer")]) + ) + + assert_equal( + parse_type("[::String, bool]"), + builder.expand_alias2(type_name("::pair"), [parse_type("::String"), parse_type("bool")]) + ) + + assert_raises do + builder.expand_alias2(type_name("::opt"), []) + end + + assert_raises do + builder.expand_alias2(type_name("::opt"), [parse_type("bool"), parse_type("top")]) + end +end + end + end end diff --git a/test/rbs/schema_test.rb b/test/rbs/schema_test.rb index 82480edf3..ee6ae3d4a 100644 --- a/test/rbs/schema_test.rb +++ b/test/rbs/schema_test.rb @@ -124,6 +124,7 @@ def test_method_type_schema def test_decls assert_decl RBS::Parser.parse_signature("type Steep::foo = untyped")[0], :alias + assert_decl RBS::Parser.parse_signature("type Steep::foo[A] = A")[0], :alias assert_decl RBS::Parser.parse_signature('Steep::VERSION: "1.2.3"')[0], :constant diff --git a/test/rbs/signature_parsing_test.rb b/test/rbs/signature_parsing_test.rb index c0dcbdb7f..90ffb26ff 100644 --- a/test/rbs/signature_parsing_test.rb +++ b/test/rbs/signature_parsing_test.rb @@ -21,6 +21,7 @@ def test_type_alias assert_instance_of Declarations::Alias, type_decl assert_equal TypeName.new(name: :foo, namespace: Namespace.parse("Steep")), type_decl.name + assert_equal [], type_decl.type_params.each.map(&:name) assert_equal Types::Bases::Any.new(location: nil), type_decl.type assert_equal "type Steep::foo = untyped", type_decl.location.source end @@ -32,6 +33,66 @@ def test_type_alias end end + def test_type_alias_generic + Parser.parse_signature(< void + +type y[unchecked out T] = ^(T) -> void +RBS + assert_equal 2, decls.size + + decls[0].tap do |type_decl| + assert_instance_of Declarations::Alias, type_decl + + type_decl.type_params.params[0].tap do |param| + assert_equal :T, param.name + assert_equal :invariant, param.variance + refute_predicate param, :skip_validation + end + end + + decls[1].tap do |type_decl| + assert_instance_of Declarations::Alias, type_decl + + type_decl.type_params.params[0].tap do |param| + assert_equal :T, param.name + assert_equal :covariant, param.variance + assert_predicate param, :skip_validation + end + end + end + end + def test_constant Parser.parse_signature("FOO: untyped").yield_self do |decls| assert_equal 1, decls.size diff --git a/test/rbs/type_alias_regulartiry_test.rb b/test/rbs/type_alias_regulartiry_test.rb new file mode 100644 index 000000000..241ad844b --- /dev/null +++ b/test/rbs/type_alias_regulartiry_test.rb @@ -0,0 +1,55 @@ +require "test_helper" + +class TypeAliasRegularityTest < Test::Unit::TestCase + include RBS + include TestHelper + + def test_validate + SignatureManager.new do |manager| + manager.add_file("foo.rbs", <<-EOF) +type foo = Integer + +type bar[T] = [bar[T], T, bar[T]] + | nil + +type baz[T] = baz[bar[T]] + | nil + EOF + + manager.build do |env| + validator = TypeAliasRegularity.validate(env: env) + + refute_operator validator, :nonregular?, TypeName("::foo") + refute_operator validator, :nonregular?, TypeName("::bar") + + assert_operator validator, :nonregular?, TypeName("::baz") + assert_equal( + parse_type("::baz[::bar[T]]", variables: [:T]), + validator.nonregular?(TypeName("::baz")).nonregular_type + ) + end + end + end + + def test_validate_mutual + SignatureManager.new do |manager| + manager.add_file("foo.rbs", <<-EOF) +type foo[T] = bar[T] + +type bar[T] = baz[String | T] + +type baz[T] = foo[Array[T]] + EOF + + manager.build do |env| + validator = TypeAliasRegularity.validate(env: env) + + assert_operator validator, :nonregular?, TypeName("::foo") + assert_equal( + parse_type("::foo[Array[::String | T]]", variables: [:T]), + validator.nonregular?(TypeName("::foo")).nonregular_type + ) + end + end + end +end diff --git a/test/rbs/type_parsing_test.rb b/test/rbs/type_parsing_test.rb index ec62fdb30..c71465d59 100644 --- a/test/rbs/type_parsing_test.rb +++ b/test/rbs/type_parsing_test.rb @@ -123,8 +123,12 @@ def test_alias assert_equal "::Foo::foo", type.location.source end - assert_raises RBS::ParsingError do - Parser.parse_type("foo[untyped]") + Parser.parse_type("foo[untyped]").yield_self do |type| + assert_instance_of Types::Alias, type + assert_equal TypeName.new(namespace: Namespace.empty, name: :foo), type.name + assert_equal "foo[untyped]", type.location.source + assert_equal "foo", type.location[:name].source + assert_equal "[untyped]", type.location[:args].source end end diff --git a/test/rbs/variance_calculator_test.rb b/test/rbs/variance_calculator_test.rb index f01477d2c..9535baf07 100644 --- a/test/rbs/variance_calculator_test.rb +++ b/test/rbs/variance_calculator_test.rb @@ -54,6 +54,43 @@ module Bar[out X, in Y, Z] end end + def test_alias_generics + SignatureManager.new do |manager| + manager.files[Pathname("foo.rbs")] = < S + +type c[T, S] = Foo[T, S] + +type d[T] = Foo[T, T] + +class Foo[in T, out S] +end +EOF + manager.build do |env| + builder = DefinitionBuilder.new(env: env) + calculator = VarianceCalculator.new(builder: builder) + + calculator.in_type_alias(name: TypeName("::a")).tap do |result| + assert_equal({ T: :covariant }, result.result) + end + + calculator.in_type_alias(name: TypeName("::b")).tap do |result| + assert_equal({ T: :contravariant, S: :covariant }, result.result) + end + + calculator.in_type_alias(name: TypeName("::c")).tap do |result| + assert_equal({ T: :contravariant, S: :covariant }, result.result) + end + + calculator.in_type_alias(name: TypeName("::d")).tap do |result| + assert_equal({ T: :invariant }, result.result) + end + end + end + end + def test_result result = VarianceCalculator::Result.new(variables: [:A, :B, :C]) result.covariant(:A) diff --git a/test/rbs/writer_test.rb b/test/rbs/writer_test.rb index 25f2c22aa..6d60ba632 100644 --- a/test/rbs/writer_test.rb +++ b/test/rbs/writer_test.rb @@ -133,6 +133,12 @@ class Foo[out A, unchecked B, in C] < Bar[A, C, B] SIG end + def test_generic_alias + assert_writer <<-SIG +type foo[Bar] = Baz + SIG + end + def test_overload assert_writer <<-SIG class Foo diff --git a/test/validator_test.rb b/test/validator_test.rb index 9d47ca09b..1d950324d 100644 --- a/test/validator_test.rb +++ b/test/validator_test.rb @@ -110,7 +110,34 @@ class Bar validator = RBS::Validator.new(env: env, resolver: resolver) env.alias_decls.each do |name, entry| - assert_nil validator.validate_type_alias(entry: entry) + validator.validate_type_alias(entry: entry) + end + end + end + end + + def test_generic_type_aliases + SignatureManager.new do |manager| + manager.add_file("test.rbs", <<-EOF) +type foo[T] = [T, foo[T]] + +type bar[T] = [bar[T?]] + +type baz[out T] = ^(T) -> void + EOF + + manager.build do |env| + resolver = RBS::TypeNameResolver.from_env(env) + validator = RBS::Validator.new(env: env, resolver: resolver) + + validator.validate_type_alias(entry: env.alias_decls[type_name("::foo")]) + + assert_raises RBS::NonregularTypeAliasError do + validator.validate_type_alias(entry: env.alias_decls[type_name("::bar")]) + end + + assert_raises RBS::InvalidVarianceAnnotationError do + validator.validate_type_alias(entry: env.alias_decls[type_name("::baz")]) end end end