From 1b075fb407a71b97b0659128b8ca0d09d07dfd02 Mon Sep 17 00:00:00 2001 From: Paul Berry Date: Fri, 10 May 2024 15:42:51 +0000 Subject: [PATCH] Mini_types: rework representation of nullability to match analyzer. The "mini_types" representation (used for unit testing of shared flow analysis and type analysis code) is reworked so that it represents nullabilities in the same way as the analyzer (and in a more similar way to the CFE), namely: each type has a field `nullabilitySuffix` indicating whether the type is followed by `?`, `*`, or neither. This change ensures that `is` and `as` tests using the mini_types representation (e.g. `is FunctionType`) will behave the same way as they ehave in the analyzer and CFE; this in turn should lead to additional opportunities to share code between the analyzer and CFE. Change-Id: Ia5d963eb96e225e62df71a406d4024053311df88 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/365700 Reviewed-by: Konstantin Shcheglov Commit-Queue: Paul Berry --- pkg/_fe_analyzer_shared/test/mini_ast.dart | 135 ++++---- pkg/_fe_analyzer_shared/test/mini_types.dart | 323 ++++++++++-------- .../test/mini_types_test.dart | 48 +-- 3 files changed, 270 insertions(+), 236 deletions(-) diff --git a/pkg/_fe_analyzer_shared/test/mini_ast.dart b/pkg/_fe_analyzer_shared/test/mini_ast.dart index 076b4a2fbd3..5a576950936 100644 --- a/pkg/_fe_analyzer_shared/test/mini_ast.dart +++ b/pkg/_fe_analyzer_shared/test/mini_ast.dart @@ -363,7 +363,8 @@ Pattern objectPattern({ required List fields, }) { var parsedType = Type(requiredType); - if (parsedType is! PrimaryType) { + if (parsedType is! PrimaryType || + parsedType.nullabilitySuffix != NullabilitySuffix.none) { fail('Expected a primary type, got $parsedType'); } return ObjectPattern._( @@ -1816,7 +1817,8 @@ class Harness { var member = getMember(matchedValueType, operator); if (member == null) return null; var memberType = member._type; - if (memberType is! FunctionType) { + if (memberType is! FunctionType || + memberType.nullabilitySuffix != NullabilitySuffix.none) { fail('$matchedValueType.operator$operator has type $memberType; ' 'must be a function type'); } @@ -2820,15 +2822,7 @@ class MiniAstOperations } @override - NullabilitySuffix getNullabilitySuffix(Type type) { - if (type is QuestionType) { - return NullabilitySuffix.question; - } else if (type is StarType) { - return NullabilitySuffix.star; - } else { - return NullabilitySuffix.none; - } - } + NullabilitySuffix getNullabilitySuffix(Type type) => type.nullabilitySuffix; @override TypeDeclarationKind? getTypeDeclarationKind(Type type) { @@ -2885,7 +2879,10 @@ class MiniAstOperations @override bool isDartCoreFunction(Type type) { - return type is PrimaryType && type.name == 'Function' && type.args.isEmpty; + return type is PrimaryType && + type.nullabilitySuffix == NullabilitySuffix.none && + type.name == 'Function' && + type.args.isEmpty; } @override @@ -2896,20 +2893,15 @@ class MiniAstOperations } @override - bool isFunctionType(Type type) { - return withNullabilitySuffix(type, NullabilitySuffix.none) is FunctionType; - } + bool isFunctionType(Type type) => type is FunctionType; @override - bool isInterfaceType(Type type) { - Type underlyingType = withNullabilitySuffix(type, NullabilitySuffix.none); - return underlyingType is PrimaryType && underlyingType.isInterfaceType; - } + bool isInterfaceType(Type type) => + type is PrimaryType && type.isInterfaceType; @override - bool isNever(Type type) { - return type is NeverType; - } + bool isNever(Type type) => + type is NeverType && type.nullabilitySuffix == NullabilitySuffix.none; @override bool isNonNullable(TypeSchema typeSchema) { @@ -2919,9 +2911,10 @@ class MiniAstOperations type is VoidType || type is NullType) { return false; - } else if (type is PromotedTypeVariableType) { + } else if (type is PromotedTypeVariableType && + type.nullabilitySuffix == NullabilitySuffix.none) { return isNonNullable(typeToSchema(type.promotion)); - } else if (type is QuestionType) { + } else if (type.nullabilitySuffix == NullabilitySuffix.question) { return false; } else if (matchFutureOr(type) case Type typeArgument?) { return isNonNullable(typeToSchema(typeArgument)); @@ -2937,7 +2930,10 @@ class MiniAstOperations @override bool isObject(Type type) { - return type is PrimaryType && type.name == 'Object' && type.args.isEmpty; + return type is PrimaryType && + type.nullabilitySuffix == NullabilitySuffix.none && + type.name == 'Object' && + type.args.isEmpty; } @override @@ -2945,9 +2941,7 @@ class MiniAstOperations property.isPromotable; @override - bool isRecordType(Type type) { - return withNullabilitySuffix(type, NullabilitySuffix.none) is RecordType; - } + bool isRecordType(Type type) => type is RecordType; @override bool isSubtypeOf(Type leftType, Type rightType) { @@ -2955,7 +2949,9 @@ class MiniAstOperations } @override - bool isTypeParameterType(Type type) => type is PromotedTypeVariableType; + bool isTypeParameterType(Type type) => + type is PromotedTypeVariableType && + type.nullabilitySuffix == NullabilitySuffix.none; @override bool isTypeSchemaSatisfied( @@ -2995,9 +2991,11 @@ class MiniAstOperations } else if (type2 is NullType && promoteToNonNull(type1) != type1) { // type1 is already nullable return type1; - } else if (type1 is NeverType) { + } else if (type1 is NeverType && + type1.nullabilitySuffix == NullabilitySuffix.none) { return type2; - } else if (type2 is NeverType) { + } else if (type2 is NeverType && + type2.nullabilitySuffix == NullabilitySuffix.none) { return type1; } else { var typeNames = [type1.type, type2.type]; @@ -3032,9 +3030,8 @@ class MiniAstOperations @override Type? matchFutureOr(Type type) { - Type underlyingType = withNullabilitySuffix(type, NullabilitySuffix.none); - if (underlyingType is FutureOrType) { - return underlyingType.typeArgument; + if (type is FutureOrType) { + return type.typeArgument; } return null; } @@ -3047,7 +3044,9 @@ class MiniAstOperations @override Type? matchIterableType(Type type) { - if (type is PrimaryType && type.args.length == 1) { + if (type is PrimaryType && + type.nullabilitySuffix == NullabilitySuffix.none && + type.args.length == 1) { if (type.name == 'Iterable' || type.name == 'List') { return type.args[0]; } @@ -3064,7 +3063,10 @@ class MiniAstOperations @override Type? matchListType(Type type) { - if (type is PrimaryType && type.name == 'List' && type.args.length == 1) { + if (type is PrimaryType && + type.nullabilitySuffix == NullabilitySuffix.none && + type.name == 'List' && + type.args.length == 1) { return type.args[0]; } return null; @@ -3072,7 +3074,10 @@ class MiniAstOperations @override ({Type keyType, Type valueType})? matchMapType(Type type) { - if (type is PrimaryType && type.name == 'Map' && type.args.length == 2) { + if (type is PrimaryType && + type.nullabilitySuffix == NullabilitySuffix.none && + type.name == 'Map' && + type.args.length == 2) { return ( keyType: type.args[0], valueType: type.args[1], @@ -3083,7 +3088,9 @@ class MiniAstOperations @override Type? matchStreamType(Type type) { - if (type is PrimaryType && type.args.length == 1) { + if (type is PrimaryType && + type.nullabilitySuffix == NullabilitySuffix.none && + type.args.length == 1) { if (type.name == 'Stream') { return type.args[0]; } @@ -3093,22 +3100,19 @@ class MiniAstOperations @override TypeDeclarationMatchResult? matchTypeDeclarationType(Type type) { - if (isInterfaceType(type)) { - PrimaryType underlyingType = - withNullabilitySuffix(type, NullabilitySuffix.none) as PrimaryType; + if (type is! PrimaryType) return null; + if (type.isInterfaceType) { return new TypeDeclarationMatchResult( typeDeclarationKind: TypeDeclarationKind.interfaceDeclaration, - typeDeclaration: underlyingType.type, + typeDeclaration: type.type, typeDeclarationType: type, - typeArguments: underlyingType.args); + typeArguments: type.args); } else if (isExtensionType(type)) { - PrimaryType underlyingType = - withNullabilitySuffix(type, NullabilitySuffix.none) as PrimaryType; return new TypeDeclarationMatchResult( typeDeclarationKind: TypeDeclarationKind.extensionTypeDeclaration, - typeDeclaration: underlyingType.type, + typeDeclaration: type.type, typeDeclarationType: type, - typeArguments: underlyingType.args); + typeArguments: type.args); } else { return null; } @@ -3122,8 +3126,8 @@ class MiniAstOperations @override Type promoteToNonNull(Type type) { - if (type is QuestionType) { - return type.innerType; + if (type.nullabilitySuffix == NullabilitySuffix.question) { + return type.withNullability(NullabilitySuffix.none); } else if (type is NullType) { return NeverType.instance; } else { @@ -3215,34 +3219,8 @@ class MiniAstOperations property.whyNotPromotable; @override - Type withNullabilitySuffix(Type type, NullabilitySuffix modifier) { - switch (modifier) { - case NullabilitySuffix.none: - if (type is QuestionType) { - return type.innerType; - } else if (type is StarType) { - return type.innerType; - } else { - return type; - } - case NullabilitySuffix.question: - if (type is QuestionType) { - return type; - } else if (type is StarType) { - return QuestionType(type.innerType); - } else { - return QuestionType(type); - } - case NullabilitySuffix.star: - if (type is QuestionType) { - return StarType(type.innerType); - } else if (type is StarType) { - return type; - } else { - return StarType(type); - } - } - } + Type withNullabilitySuffix(Type type, NullabilitySuffix modifier) => + type.withNullability(modifier); } /// Representation of an expression or statement in the pseudo-Dart language @@ -5564,7 +5542,8 @@ class _MiniAstTypeAnalyzer inputKinds.add(Kind.expression); analyzeExpression( arguments[i], - methodType is FunctionType + methodType is FunctionType && + methodType.nullabilitySuffix == NullabilitySuffix.none ? operations.typeToSchema(methodType.positionalParameters[i]) : operations.unknownType); } diff --git a/pkg/_fe_analyzer_shared/test/mini_types.dart b/pkg/_fe_analyzer_shared/test/mini_types.dart index e7e4eba39e0..230c66215f1 100644 --- a/pkg/_fe_analyzer_shared/test/mini_types.dart +++ b/pkg/_fe_analyzer_shared/test/mini_types.dart @@ -6,6 +6,7 @@ // but light weight enough to be suitable for unit testing of code in the // `_fe_analyzer_shared` package. +import 'package:_fe_analyzer_shared/src/type_inference/nullability_suffix.dart'; import 'package:_fe_analyzer_shared/src/types/shared_type.dart'; /// Surrounds [s] with parentheses if [condition] is `true`, otherwise returns @@ -17,7 +18,11 @@ String _parenthesizeIf(bool condition, String s) => condition ? '($s)' : s; class DynamicType extends _SpecialSimpleType implements SharedDynamicType { static final instance = DynamicType._(); - DynamicType._() : super._('dynamic'); + DynamicType._() + : super._('dynamic', nullabilitySuffix: NullabilitySuffix.none); + + @override + Type withNullability(NullabilitySuffix suffix) => this; } /// Representation of a function type suitable for unit testing of code in the @@ -32,7 +37,9 @@ class FunctionType extends Type { /// A list of the types of positional parameters. final List positionalParameters; - FunctionType(this.returnType, this.positionalParameters) : super._(); + FunctionType(this.returnType, this.positionalParameters, + {super.nullabilitySuffix = NullabilitySuffix.none}) + : super._(); @override Type? closureWithRespectToUnknown({required bool covariant}) { @@ -44,7 +51,8 @@ class FunctionType extends Type { return null; } return FunctionType(newReturnType ?? returnType, - newPositionalParameters ?? positionalParameters); + newPositionalParameters ?? positionalParameters, + nullabilitySuffix: nullabilitySuffix); } @override @@ -56,19 +64,25 @@ class FunctionType extends Type { return null; } return FunctionType(newReturnType ?? returnType, - newPositionalParameters ?? positionalParameters); + newPositionalParameters ?? positionalParameters, + nullabilitySuffix: nullabilitySuffix); } @override - String _toString({required bool parenthesizeIfComplex}) => _parenthesizeIf( - parenthesizeIfComplex, - '$returnType Function(${positionalParameters.join(', ')})'); + Type withNullability(NullabilitySuffix suffix) => + FunctionType(returnType, positionalParameters, nullabilitySuffix: suffix); + + @override + String _toStringWithoutSuffix({required bool parenthesizeIfComplex}) => + _parenthesizeIf(parenthesizeIfComplex, + '$returnType Function(${positionalParameters.join(', ')})'); } /// Representation of the type `FutureOr` suitable for unit testing of code /// in the `_fe_analyzer_shared` package. class FutureOrType extends PrimaryType { - FutureOrType(Type typeArgument) + FutureOrType(Type typeArgument, + {super.nullabilitySuffix = NullabilitySuffix.none}) : super._withSpecialName('FutureOr', args: [typeArgument]); Type get typeArgument => args.single; @@ -78,15 +92,19 @@ class FutureOrType extends PrimaryType { Type? newArg = typeArgument.closureWithRespectToUnknown(covariant: covariant); if (newArg == null) return null; - return FutureOrType(newArg); + return FutureOrType(newArg, nullabilitySuffix: nullabilitySuffix); } @override Type? recursivelyDemote({required bool covariant}) { Type? newArg = typeArgument.recursivelyDemote(covariant: covariant); if (newArg == null) return null; - return FutureOrType(newArg); + return FutureOrType(newArg, nullabilitySuffix: nullabilitySuffix); } + + @override + Type withNullability(NullabilitySuffix suffix) => + FutureOrType(typeArgument, nullabilitySuffix: suffix); } /// Representation of an invalid type suitable for unit testing of code in the @@ -94,7 +112,10 @@ class FutureOrType extends PrimaryType { class InvalidType extends _SpecialSimpleType implements SharedInvalidType { static final instance = InvalidType._(); - InvalidType._() : super._('error'); + InvalidType._() : super._('error', nullabilitySuffix: NullabilitySuffix.none); + + @override + Type withNullability(NullabilitySuffix suffix) => this; } class NamedType implements SharedNamedType { @@ -112,7 +133,12 @@ class NamedType implements SharedNamedType { class NeverType extends _SpecialSimpleType { static final instance = NeverType._(); - NeverType._() : super._('Never'); + NeverType._({super.nullabilitySuffix = NullabilitySuffix.none}) + : super._('Never'); + + @override + Type withNullability(NullabilitySuffix suffix) => + NeverType._(nullabilitySuffix: suffix); } /// Representation of the type `Null` suitable for unit testing of code in the @@ -120,7 +146,10 @@ class NeverType extends _SpecialSimpleType { class NullType extends _SpecialSimpleType { static final instance = NullType._(); - NullType._() : super._('Null'); + NullType._() : super._('Null', nullabilitySuffix: NullabilitySuffix.none); + + @override + Type withNullability(NullabilitySuffix suffix) => this; } /// Exception thrown if a type fails to parse properly. @@ -155,13 +184,17 @@ class PrimaryType extends Type { /// The type arguments, or `const []` if there are no type arguments. final List args; - PrimaryType(this.name, {this.args = const []}) : super._() { + PrimaryType(this.name, + {this.args = const [], super.nullabilitySuffix = NullabilitySuffix.none}) + : super._() { if (namedNonInterfaceTypes.contains(name)) { throw StateError('Tried to create a PrimaryType with special name $name'); } } - PrimaryType._withSpecialName(this.name, {this.args = const []}) : super._() { + PrimaryType._withSpecialName(this.name, + {this.args = const [], super.nullabilitySuffix = NullabilitySuffix.none}) + : super._() { if (!namedNonInterfaceTypes.contains(name)) { throw StateError( 'Tried to use PrimaryType._withSpecialName with non-special name ' @@ -176,18 +209,24 @@ class PrimaryType extends Type { List? newArgs = args.closureWithRespectToUnknown(covariant: covariant); if (newArgs == null) return null; - return PrimaryType(name, args: newArgs); + return PrimaryType(name, + args: newArgs, nullabilitySuffix: nullabilitySuffix); } @override Type? recursivelyDemote({required bool covariant}) { List? newArgs = args.recursivelyDemote(covariant: covariant); if (newArgs == null) return null; - return PrimaryType(name, args: newArgs); + return PrimaryType(name, + args: newArgs, nullabilitySuffix: nullabilitySuffix); } @override - String _toString({required bool parenthesizeIfComplex}) => + Type withNullability(NullabilitySuffix suffix) => + PrimaryType(name, args: args, nullabilitySuffix: suffix); + + @override + String _toStringWithoutSuffix({required bool parenthesizeIfComplex}) => args.isEmpty ? name : '$name<${args.join(', ')}>'; } @@ -201,58 +240,34 @@ class PromotedTypeVariableType extends Type { final Type promotion; - PromotedTypeVariableType(this.innerType, this.promotion) : super._(); + PromotedTypeVariableType(this.innerType, this.promotion, + {super.nullabilitySuffix = NullabilitySuffix.none}) + : super._(); @override Type? closureWithRespectToUnknown({required bool covariant}) { var newPromotion = promotion.closureWithRespectToUnknown(covariant: covariant); if (newPromotion == null) return null; - return PromotedTypeVariableType(innerType, newPromotion); + return PromotedTypeVariableType(innerType, newPromotion, + nullabilitySuffix: nullabilitySuffix); } @override Type? recursivelyDemote({required bool covariant}) => - covariant ? innerType : NeverType.instance; + (covariant ? innerType : NeverType.instance) + .withNullability(nullabilitySuffix); @override - String _toString({required bool parenthesizeIfComplex}) => _parenthesizeIf( - parenthesizeIfComplex, - '${innerType.toString(parenthesizeIfComplex: true)}&' - '${promotion.toString(parenthesizeIfComplex: true)}'); -} - -/// Representation of a nullable type suitable for unit testing of code in the -/// `_fe_analyzer_shared` package. This class is used only for nullable types -/// that are spelled with an explicit trailing `?`, e.g. `double?`; it is not -/// used for e.g. `dynamic` or `FutureOr`, even though those types are -/// nullable as well. -class QuestionType extends Type { - final Type innerType; - - QuestionType(this.innerType) : super._(); + Type withNullability(NullabilitySuffix suffix) => + PromotedTypeVariableType(innerType, promotion, nullabilitySuffix: suffix); @override - Type? closureWithRespectToUnknown({required bool covariant}) { - Type? newInnerType = - innerType.closureWithRespectToUnknown(covariant: covariant); - if (newInnerType == null) return null; - if (newInnerType is QuestionType) return newInnerType; - if (newInnerType is StarType) return QuestionType(newInnerType.innerType); - return QuestionType(newInnerType); - } - - @override - Type? recursivelyDemote({required bool covariant}) { - Type? newInnerType = innerType.recursivelyDemote(covariant: covariant); - if (newInnerType == null) return null; - return QuestionType(newInnerType); - } - - @override - String _toString({required bool parenthesizeIfComplex}) => _parenthesizeIf( - parenthesizeIfComplex, - '${innerType.toString(parenthesizeIfComplex: true)}?'); + String _toStringWithoutSuffix({required bool parenthesizeIfComplex}) => + _parenthesizeIf( + parenthesizeIfComplex, + '${innerType.toString(parenthesizeIfComplex: true)}&' + '${promotion.toString(parenthesizeIfComplex: true)}'); } class RecordType extends Type implements SharedRecordType { @@ -265,6 +280,7 @@ class RecordType extends Type implements SharedRecordType { RecordType({ required this.positionalTypes, required this.namedTypes, + super.nullabilitySuffix = NullabilitySuffix.none, }) : super._(); @override @@ -288,6 +304,7 @@ class RecordType extends Type implements SharedRecordType { return RecordType( positionalTypes: newPositional ?? positionalTypes, namedTypes: newNamed ?? namedTypes, + nullabilitySuffix: nullabilitySuffix, ); } @@ -310,9 +327,16 @@ class RecordType extends Type implements SharedRecordType { return RecordType( positionalTypes: newPositional ?? positionalTypes, namedTypes: newNamed ?? namedTypes, + nullabilitySuffix: nullabilitySuffix, ); } + @override + Type withNullability(NullabilitySuffix suffix) => RecordType( + positionalTypes: positionalTypes, + namedTypes: namedTypes, + nullabilitySuffix: suffix); + List? _closureWithRespectToUnknownNamed( {required bool covariant}) { List? newNamed; @@ -342,7 +366,7 @@ class RecordType extends Type implements SharedRecordType { } @override - String _toString({required bool parenthesizeIfComplex}) { + String _toStringWithoutSuffix({required bool parenthesizeIfComplex}) { var positionalStr = positionalTypes.join(', '); var namedStr = namedTypes.map((e) => '${e.type} ${e.name}').join(', '); if (namedStr.isNotEmpty) { @@ -357,36 +381,6 @@ class RecordType extends Type implements SharedRecordType { } } -/// Representation of a "star" type suitable for unit testing of code in the -/// `_fe_analyzer_shared` package. -class StarType extends Type { - final Type innerType; - - StarType(this.innerType) : super._(); - - @override - Type? closureWithRespectToUnknown({required bool covariant}) { - Type? newInnerType = - innerType.closureWithRespectToUnknown(covariant: covariant); - if (newInnerType == null) return null; - if (newInnerType is StarType) return newInnerType; - if (newInnerType is QuestionType) return newInnerType; - return StarType(newInnerType); - } - - @override - Type? recursivelyDemote({required bool covariant}) { - Type? newInnerType = innerType.recursivelyDemote(covariant: covariant); - if (newInnerType == null) return null; - return StarType(newInnerType); - } - - @override - String _toString({required bool parenthesizeIfComplex}) => _parenthesizeIf( - parenthesizeIfComplex, - '${innerType.toString(parenthesizeIfComplex: true)}*'); -} - /// Representation of a type suitable for unit testing of code in the /// `_fe_analyzer_shared` package. /// @@ -399,9 +393,13 @@ class StarType extends Type { /// defeat this behavior (e.g. so that a type can be passed to `expect`, use /// [Type.withComparisonsAllowed]. abstract class Type implements SharedType { + /// If this type ends in a suffix (`?` or `*`), the suffix it ends with; + /// otherwise [NullabilitySuffix.none]. + final NullabilitySuffix nullabilitySuffix; + factory Type(String typeStr) => _TypeParser.parse(typeStr); - const Type._(); + const Type._({this.nullabilitySuffix = NullabilitySuffix.none}); @override int get hashCode => type.hashCode; @@ -441,16 +439,34 @@ abstract class Type implements SharedType { /// - A promoted type variable type (e.g. `T&int`) @override String toString({bool parenthesizeIfComplex = false}) => - _toString(parenthesizeIfComplex: parenthesizeIfComplex); + switch (nullabilitySuffix) { + NullabilitySuffix.question => _parenthesizeIf( + parenthesizeIfComplex, + '${_toStringWithoutSuffix(parenthesizeIfComplex: true)}' + '?'), + NullabilitySuffix.star => _parenthesizeIf( + parenthesizeIfComplex, + '${_toStringWithoutSuffix(parenthesizeIfComplex: true)}' + '*'), + NullabilitySuffix.none => + _toStringWithoutSuffix(parenthesizeIfComplex: parenthesizeIfComplex), + }; - /// Returns a string representation of this type. + /// Returns a modifies version of this type, with the nullability suffix + /// changed to [suffix]. + /// + /// For types that don't accept a nullability suffix (`dynamic`, InvalidType, + /// `Null`, `_`, and `void`), the type is returned unchanged. + Type withNullability(NullabilitySuffix suffix); + + /// Returns a string representation of the portion of this string that + /// precedes the nullability suffix. /// /// If [parenthesizeIfComplex] is `true`, then the result will be surrounded /// by parenthesis if it takes any of the following forms: - /// - A type with a trailing `?` or `*` /// - A function type (e.g. `void Function()`) /// - A promoted type variable type (e.g. `T&int`) - String _toString({required bool parenthesizeIfComplex}); + String _toStringWithoutSuffix({required bool parenthesizeIfComplex}); } class TypeSchema { @@ -503,20 +519,28 @@ class TypeSystem { if (isSubtype(t, s)) return NeverType.instance; // Else if T is R? and Null <: S then factor(R, S) - if (t is QuestionType && isSubtype(NullType.instance, s)) { - return factor(t.innerType, s); + if (t.nullabilitySuffix == NullabilitySuffix.question && + isSubtype(NullType.instance, s)) { + return factor(t.withNullability(NullabilitySuffix.none), s); } // Else if T is R? then factor(R, S)? - if (t is QuestionType) return QuestionType(factor(t.innerType, s)); + if (t.nullabilitySuffix == NullabilitySuffix.question) { + return factor(t.withNullability(NullabilitySuffix.none), s) + .withNullability(NullabilitySuffix.question); + } // Else if T is R* and Null <: S then factor(R, S) - if (t is StarType && isSubtype(NullType.instance, s)) { - return factor(t.innerType, s); + if (t.nullabilitySuffix == NullabilitySuffix.star && + isSubtype(NullType.instance, s)) { + return factor(t.withNullability(NullabilitySuffix.none), s); } // Else if T is R* then factor(R, S)* - if (t is StarType) return StarType(factor(t.innerType, s)); + if (t.nullabilitySuffix == NullabilitySuffix.star) { + return factor(t.withNullability(NullabilitySuffix.none), s) + .withNullability(NullabilitySuffix.star); + } // Else if T is FutureOr and Future <: S then factor(R, S) if (t is FutureOrType) { @@ -542,8 +566,10 @@ class TypeSystem { // types with a single name and no type arguments (this covers both // primitive types and type variables). if (t0 is PrimaryType && + t0.nullabilitySuffix == NullabilitySuffix.none && t0.args.isEmpty && t1 is PrimaryType && + t1.nullabilitySuffix == NullabilitySuffix.none && t1.args.isEmpty && t0.name == t1.name) { return true; @@ -564,28 +590,39 @@ class TypeSystem { } // Left Bottom: if T0 is Never then T0 <: T1 - if (t0 is NeverType) return true; + if (t0 is NeverType && t0.nullabilitySuffix == NullabilitySuffix.none) { + return true; + } // Right Object: if T1 is Object then: - if (t1 is PrimaryType && t1.args.isEmpty && t1.name == 'Object') { + if (t1 is PrimaryType && + t1.nullabilitySuffix == NullabilitySuffix.none && + t1.args.isEmpty && + t1.name == 'Object') { // - if T0 is an unpromoted type variable with bound B then T0 <: T1 iff // B <: Object - if (t0 is PrimaryType && _isTypeVar(t0)) { + if (t0 is PrimaryType && + t0.nullabilitySuffix == NullabilitySuffix.none && + _isTypeVar(t0)) { return isSubtype(_typeVarBound(t0), _objectType); } // - if T0 is a promoted type variable X & S then T0 <: T1 iff S <: Object - if (t0 is PromotedTypeVariableType) { + if (t0 is PromotedTypeVariableType && + t0.nullabilitySuffix == NullabilitySuffix.none) { return isSubtype(t0.promotion, _objectType); } // - if T0 is FutureOr for some S, then T0 <: T1 iff S <: Object. - if (t0 is FutureOrType) { + if (t0 is FutureOrType && + t0.nullabilitySuffix == NullabilitySuffix.none) { return isSubtype(t0.typeArgument, _objectType); } // - if T0 is S* for any S, then T0 <: T1 iff S <: T1 - if (t0 is StarType) return isSubtype(t0.innerType, t1); + if (t0.nullabilitySuffix == NullabilitySuffix.star) { + return isSubtype(t0.withNullability(NullabilitySuffix.none), t1); + } // - if T0 is Null, dynamic, void, or S? for any S, then the subtyping // does not hold (per above, the result of the subtyping query is @@ -594,7 +631,7 @@ class TypeSystem { t0 is DynamicType || t0 is InvalidType || t0 is VoidType || - t0 is QuestionType) { + t0.nullabilitySuffix == NullabilitySuffix.question) { return false; } @@ -609,12 +646,15 @@ class TypeSystem { // - If T1 is FutureOr for some S, then the query is true iff // Null <: S. - if (t1 is FutureOrType) { + if (t1 is FutureOrType && + t1.nullabilitySuffix == NullabilitySuffix.none) { return isSubtype(NullType.instance, t1.typeArgument); } // - If T1 is Null, S? or S* for some S, then the query is true. - if (t1 is NullType || t1 is QuestionType || t1 is StarType) { + if (t1 is NullType || + t1.nullabilitySuffix == NullabilitySuffix.question || + t1.nullabilitySuffix == NullabilitySuffix.star) { return true; } @@ -623,19 +663,19 @@ class TypeSystem { } // Left Legacy: if T0 is S0* then: - if (t0 is StarType) { + if (t0.nullabilitySuffix == NullabilitySuffix.star) { // - T0 <: T1 iff S0 <: T1. - return isSubtype(t0.innerType, t1); + return isSubtype(t0.withNullability(NullabilitySuffix.none), t1); } // Right Legacy: if T1 is S1* then: - if (t1 is StarType) { + if (t1.nullabilitySuffix == NullabilitySuffix.star) { // - T0 <: T1 iff T0 <: S1?. - return isSubtype(t0, QuestionType(t1.innerType)); + return isSubtype(t0, t1.withNullability(NullabilitySuffix.question)); } // Left FutureOr: if T0 is FutureOr then: - if (t0 is FutureOrType) { + if (t0 is FutureOrType && t0.nullabilitySuffix == NullabilitySuffix.none) { var s0 = t0.typeArgument; // - T0 <: T1 iff Future <: T1 and S0 <: T1 @@ -644,15 +684,17 @@ class TypeSystem { } // Left Nullable: if T0 is S0? then: - if (t0 is QuestionType) { + if (t0.nullabilitySuffix == NullabilitySuffix.question) { // - T0 <: T1 iff S0 <: T1 and Null <: T1 - return isSubtype(t0.innerType, t1) && isSubtype(NullType.instance, t1); + return isSubtype(t0.withNullability(NullabilitySuffix.none), t1) && + isSubtype(NullType.instance, t1); } // Type Variable Reflexivity 1: if T0 is a type variable X0 or a promoted // type variables X0 & S0 and T1 is X0 then: if (_isTypeVar(t0) && t1 is PrimaryType && + t1.nullabilitySuffix == NullabilitySuffix.none && t1.args.isEmpty && _typeVarName(t0) == t1.name) { // - T0 <: T1 @@ -663,19 +705,21 @@ class TypeSystem { // type variables X0 & S0 and T1 is X0 & S1 then: if (_isTypeVar(t0) && t1 is PromotedTypeVariableType && + t1.nullabilitySuffix == NullabilitySuffix.none && _typeVarName(t0) == _typeVarName(t1)) { // - T0 <: T1 iff T0 <: S1. return isSubtype(t0, t1.promotion); } // Right Promoted Variable: if T1 is a promoted type variable X1 & S1 then: - if (t1 is PromotedTypeVariableType) { + if (t1 is PromotedTypeVariableType && + t1.nullabilitySuffix == NullabilitySuffix.none) { // - T0 <: T1 iff T0 <: X1 and T0 <: S1 return isSubtype(t0, t1.innerType) && isSubtype(t0, t1.promotion); } // Right FutureOr: if T1 is FutureOr then: - if (t1 is FutureOrType) { + if (t1 is FutureOrType && t1.nullabilitySuffix == NullabilitySuffix.none) { var s1 = t1.typeArgument; // - T0 <: T1 iff any of the following hold: @@ -693,8 +737,8 @@ class TypeSystem { } // Right Nullable: if T1 is S1? then: - if (t1 is QuestionType) { - var s1 = t1.innerType; + if (t1.nullabilitySuffix == NullabilitySuffix.question) { + var s1 = t1.withNullability(NullabilitySuffix.none); // - T0 <: T1 iff any of the following hold: return @@ -879,20 +923,20 @@ class TypeSystem { bool _isTop(Type t) { if (t is PrimaryType) { return t is DynamicType || t is InvalidType || t is VoidType; - } else if (t is QuestionType) { - var innerType = t.innerType; - return innerType is PrimaryType && - innerType.args.isEmpty && - innerType.name == 'Object'; + } else if (t.nullabilitySuffix == NullabilitySuffix.question) { + return t is PrimaryType && t.args.isEmpty && t.name == 'Object'; } return false; } bool _isTypeVar(Type t) { - if (t is PromotedTypeVariableType) { + if (t is PromotedTypeVariableType && + t.nullabilitySuffix == NullabilitySuffix.none) { assert(_isTypeVar(t.innerType)); return true; - } else if (t is PrimaryType && t.args.isEmpty) { + } else if (t is PrimaryType && + t.nullabilitySuffix == NullabilitySuffix.none && + t.args.isEmpty) { return _typeVarBounds.containsKey(t.name); } else { return false; @@ -903,7 +947,8 @@ class TypeSystem { String _typeVarName(Type t) { assert(_isTypeVar(t)); - if (t is PromotedTypeVariableType) { + if (t is PromotedTypeVariableType && + t.nullabilitySuffix == NullabilitySuffix.none) { return _typeVarName(t.innerType); } else { return (t as PrimaryType).name; @@ -914,17 +959,22 @@ class TypeSystem { /// Representation of the unknown type suitable for unit testing of code in the /// `_fe_analyzer_shared` package. class UnknownType extends Type implements SharedUnknownType { - const UnknownType() : super._(); + const UnknownType({super.nullabilitySuffix = NullabilitySuffix.none}) + : super._(); @override - Type closureWithRespectToUnknown({required bool covariant}) => + Type? closureWithRespectToUnknown({required bool covariant}) => covariant ? Type('Object?') : NeverType.instance; @override Type? recursivelyDemote({required bool covariant}) => null; @override - String _toString({required bool parenthesizeIfComplex}) => '_'; + Type withNullability(NullabilitySuffix suffix) => + UnknownType(nullabilitySuffix: suffix); + + @override + String _toStringWithoutSuffix({required bool parenthesizeIfComplex}) => '_'; } /// Representation of the type `void` suitable for unit testing of code in the @@ -932,7 +982,10 @@ class UnknownType extends Type implements SharedUnknownType { class VoidType extends _SpecialSimpleType implements SharedVoidType { static final instance = VoidType._(); - VoidType._() : super._('void'); + VoidType._() : super._('void', nullabilitySuffix: NullabilitySuffix.none); + + @override + Type withNullability(NullabilitySuffix suffix) => this; } /// Shared implementation of the types `void`, `dynamic`, `null`, `Never`, and @@ -941,8 +994,10 @@ class VoidType extends _SpecialSimpleType implements SharedVoidType { /// These types share the property that they are special cases of [PrimaryType] /// that don't need special functionality for the [closureWithRespectToUnknown] /// and [recursivelyDemote] methods. -class _SpecialSimpleType extends PrimaryType { - _SpecialSimpleType._(super.name) : super._withSpecialName(); +abstract class _SpecialSimpleType extends PrimaryType { + _SpecialSimpleType._(super.name, + {super.nullabilitySuffix = NullabilitySuffix.none}) + : super._withSpecialName(); @override Type? closureWithRespectToUnknown({required bool covariant}) => null; @@ -1034,10 +1089,10 @@ class _TypeParser { Type? _parseSuffix(Type type) { if (_currentToken == '?') { _next(); - return QuestionType(type); + return type.withNullability(NullabilitySuffix.question); } else if (_currentToken == '*') { _next(); - return StarType(type); + return type.withNullability(NullabilitySuffix.star); } else if (_currentToken == '&') { _next(); var promotion = _parseUnsuffixedType(); diff --git a/pkg/_fe_analyzer_shared/test/mini_types_test.dart b/pkg/_fe_analyzer_shared/test/mini_types_test.dart index 8dffe87fd23..4dc17bee0fc 100644 --- a/pkg/_fe_analyzer_shared/test/mini_types_test.dart +++ b/pkg/_fe_analyzer_shared/test/mini_types_test.dart @@ -2,6 +2,7 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +import 'package:_fe_analyzer_shared/src/type_inference/nullability_suffix.dart'; import 'package:test/test.dart'; import 'mini_types.dart'; @@ -68,16 +69,16 @@ main() { test('needs parentheses (question)', () { expect( - QuestionType(PromotedTypeVariableType( - PrimaryType('T'), PrimaryType('U'))) + PromotedTypeVariableType(PrimaryType('T'), PrimaryType('U'), + nullabilitySuffix: NullabilitySuffix.question) .toString(), '(T&U)?'); }); test('needs parentheses (star)', () { expect( - StarType(PromotedTypeVariableType( - PrimaryType('T'), PrimaryType('U'))) + PromotedTypeVariableType(PrimaryType('T'), PrimaryType('U'), + nullabilitySuffix: NullabilitySuffix.star) .toString(), '(T&U)*'); }); @@ -85,13 +86,18 @@ main() { group('QuestionType:', () { test('basic', () { - expect(QuestionType(PrimaryType('T')).toString(), 'T?'); + expect( + PrimaryType('T', nullabilitySuffix: NullabilitySuffix.question) + .toString(), + 'T?'); }); test('needs parentheses', () { expect( PromotedTypeVariableType( - PrimaryType('T'), QuestionType(PrimaryType('U'))) + PrimaryType('T'), + PrimaryType('U', + nullabilitySuffix: NullabilitySuffix.question)) .toString(), 'T&(U?)'); }); @@ -148,13 +154,16 @@ main() { group('StarType:', () { test('basic', () { - expect(StarType(PrimaryType('T')).toString(), 'T*'); + expect( + PrimaryType('T', nullabilitySuffix: NullabilitySuffix.star) + .toString(), + 'T*'); }); test('needs parentheses', () { expect( - PromotedTypeVariableType( - PrimaryType('T'), StarType(PrimaryType('U'))) + PromotedTypeVariableType(PrimaryType('T'), + PrimaryType('U', nullabilitySuffix: NullabilitySuffix.star)) .toString(), 'T&(U*)'); }); @@ -230,13 +239,15 @@ main() { }); test('question type', () { - var t = Type('int?') as QuestionType; - expect(t.innerType.type, 'int'); + var t = Type('int?'); + expect(t.nullabilitySuffix, NullabilitySuffix.question); + expect(t.withNullability(NullabilitySuffix.none).type, 'int'); }); test('star type', () { - var t = Type('int*') as StarType; - expect(t.innerType.type, 'int'); + var t = Type('int*'); + expect(t.nullabilitySuffix, NullabilitySuffix.star); + expect(t.withNullability(NullabilitySuffix.none).type, 'int'); }); test('promoted type variable', () { @@ -682,12 +693,6 @@ main() { expect(Type('_?').closureWithRespectToUnknown(covariant: true)!.type, 'Object?'); }); - - test('contravariant', () { - // Note: we don't normalize `Never?` to `Null`. - expect(Type('_?').closureWithRespectToUnknown(covariant: false)!.type, - 'Never?'); - }); }); group('RecordType:', () { @@ -754,11 +759,6 @@ main() { expect(Type('_*').closureWithRespectToUnknown(covariant: true)!.type, 'Object?'); }); - - test('contravariant', () { - expect(Type('_*').closureWithRespectToUnknown(covariant: false)!.type, - 'Never*'); - }); }); }); }