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 <scheglov@google.com>
Commit-Queue: Paul Berry <paulberry@google.com>
This commit is contained in:
Paul Berry
2024-05-10 15:42:51 +00:00
committed by Commit Queue
parent 3a35eeac88
commit 1b075fb407
3 changed files with 270 additions and 236 deletions
+57 -78
View File
@@ -363,7 +363,8 @@ Pattern objectPattern({
required List<RecordPatternField> 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);
}
+189 -134
View File
@@ -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<Type> 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<T>` 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<Type> {
@@ -112,7 +133,12 @@ class NamedType implements SharedNamedType<Type> {
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<Type> 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<Type>? 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<Type>? 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<int?>`, 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<Type> {
@@ -265,6 +280,7 @@ class RecordType extends Type implements SharedRecordType<Type> {
RecordType({
required this.positionalTypes,
required this.namedTypes,
super.nullabilitySuffix = NullabilitySuffix.none,
}) : super._();
@override
@@ -288,6 +304,7 @@ class RecordType extends Type implements SharedRecordType<Type> {
return RecordType(
positionalTypes: newPositional ?? positionalTypes,
namedTypes: newNamed ?? namedTypes,
nullabilitySuffix: nullabilitySuffix,
);
}
@@ -310,9 +327,16 @@ class RecordType extends Type implements SharedRecordType<Type> {
return RecordType(
positionalTypes: newPositional ?? positionalTypes,
namedTypes: newNamed ?? namedTypes,
nullabilitySuffix: nullabilitySuffix,
);
}
@override
Type withNullability(NullabilitySuffix suffix) => RecordType(
positionalTypes: positionalTypes,
namedTypes: namedTypes,
nullabilitySuffix: suffix);
List<NamedType>? _closureWithRespectToUnknownNamed(
{required bool covariant}) {
List<NamedType>? newNamed;
@@ -342,7 +366,7 @@ class RecordType extends Type implements SharedRecordType<Type> {
}
@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<Type> {
}
}
/// 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<R> and Future<R> <: 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<S> 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<S> 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<S0> then:
if (t0 is FutureOrType) {
if (t0 is FutureOrType && t0.nullabilitySuffix == NullabilitySuffix.none) {
var s0 = t0.typeArgument;
// - T0 <: T1 iff Future<S0> <: 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<S1> 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();
@@ -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*');
});
});
});
}