mini_types: introduce new classes for special built-in types.
New classes are introduced into the "mini_types" representation to
represent `dynamic`, `FutureOr<T>`, `Never`, `Null`, `void`, and the
"invalid" type. Previously, these were all represented simply using
`PrimaryType`.
Introducing new classes for these types makes the "mini_types"
representation more consistent with the representations used by the
analyzer and the front end. Since the "mini_types" representation is
used solely for unit testing the shared logic in the
`_fe_analyzer_shared` package, it's hard to justify unnecessary
differences between it and the analyzer and front end representations;
these differences just make it harder to effectively share code.
Change-Id: Ie633feef914c5ce4540b9da681a84b7f53fd8f38
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/365340
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
Reviewed-by: Chloe Stefantsova <cstefantsova@google.com>
Commit-Queue: Paul Berry <paulberry@google.com>
diff --git a/pkg/_fe_analyzer_shared/test/mini_ast.dart b/pkg/_fe_analyzer_shared/test/mini_ast.dart
index bce642a..2452b8e 100644
--- a/pkg/_fe_analyzer_shared/test/mini_ast.dart
+++ b/pkg/_fe_analyzer_shared/test/mini_ast.dart
@@ -1004,7 +1004,7 @@
ExpressionTypeAnalysisResult<Type> visit(Harness h, TypeSchema schema) {
var promotedType = promotable._getPromotedType(h);
expect(promotedType?.type, expectedTypeStr, reason: 'at $location');
- return SimpleTypeAnalysisResult(type: Type('Null'));
+ return SimpleTypeAnalysisResult(type: NullType.instance);
}
}
@@ -1023,7 +1023,7 @@
ExpressionTypeAnalysisResult<Type> visit(Harness h, TypeSchema schema) {
expect(h.flow.isReachable, expectedReachable, reason: 'at $location');
h.irBuilder.atom('null', Kind.expression, location: location);
- return new SimpleTypeAnalysisResult(type: Type('Null'));
+ return new SimpleTypeAnalysisResult(type: NullType.instance);
}
}
@@ -1790,7 +1790,7 @@
return _members['Object.$memberName']!;
default:
// It's legal to look up any member on the type `dynamic`.
- if (type.type == 'dynamic') {
+ if (type is DynamicType) {
return null;
}
// But an attempt to look up an unknown member on any other type
@@ -2703,17 +2703,8 @@
late final Type intType = Type('int');
@override
- late final Type neverType = Type('Never');
-
- @override
- late final Type nullType = Type('Null');
-
- @override
late final Type doubleType = Type('double');
- @override
- late final Type dynamicType = Type('dynamic');
-
bool? _legacy;
final Map<String, bool> _exhaustiveness = Map.of(_coreExhaustiveness);
@@ -2737,7 +2728,10 @@
final Type boolType = Type('bool');
@override
- Type get errorType => Type('error');
+ Type get dynamicType => DynamicType.instance;
+
+ @override
+ Type get errorType => InvalidType.instance;
bool get legacy => _legacy ?? false;
@@ -2745,6 +2739,12 @@
_legacy = value;
}
+ @override
+ Type get neverType => NeverType.instance;
+
+ @override
+ Type get nullType => NullType.instance;
+
/// Updates the harness with a new result for [downwardInfer].
void addDownwardInfer({
required String name,
@@ -2788,7 +2788,7 @@
TypeClassification classifyType(Type type) {
if (isSubtypeOf(type, Type('Object'))) {
return TypeClassification.nonNullable;
- } else if (isSubtypeOf(type, Type('Null'))) {
+ } else if (isSubtypeOf(type, NullType.instance)) {
return TypeClassification.nullOrEquivalent;
} else {
return TypeClassification.potentiallyNullable;
@@ -2881,8 +2881,8 @@
@override
bool isAssignableTo(Type fromType, Type toType) {
if (legacy && isSubtypeOf(toType, fromType)) return true;
- if (fromType.type == 'dynamic') return true;
- if (fromType.type == 'error') return true;
+ if (fromType is DynamicType) return true;
+ if (fromType is InvalidType) return true;
return isSubtypeOf(fromType, toType);
}
@@ -2892,12 +2892,10 @@
}
@override
- bool isDynamic(Type type) =>
- type is PrimaryType && type.name == 'dynamic' && type.args.isEmpty;
+ bool isDynamic(Type type) => type is DynamicType;
@override
- bool isError(Type type) =>
- type is PrimaryType && type.name == 'error' && type.args.isEmpty;
+ bool isError(Type type) => type is InvalidType;
@override
bool isExtensionType(Type type) {
@@ -2919,16 +2917,16 @@
@override
bool isNever(Type type) {
- return type.type == 'Never';
+ return type is NeverType;
}
@override
bool isNonNullable(TypeSchema typeSchema) {
Type type = typeSchema.toType();
- if (isDynamic(type) ||
+ if (type is DynamicType ||
typeSchema is SharedUnknownType ||
- isVoid(type) ||
- isNull(type)) {
+ type is VoidType ||
+ type is NullType) {
return false;
} else if (type is PromotedTypeVariableType) {
return isNonNullable(typeToSchema(type.promotion));
@@ -2940,13 +2938,11 @@
// TODO(cstefantsova): Update to a fast-pass implementation when the
// mini-ast testing framework supports looking up superinterfaces of
// extension types or looking up bounds of type parameters.
- return _typeSystem.isSubtype(new Type('Null'), type);
+ return _typeSystem.isSubtype(NullType.instance, type);
}
@override
- bool isNull(Type type) {
- return type.type == 'Null';
- }
+ bool isNull(Type type) => type is NullType;
@override
bool isObject(Type type) {
@@ -2981,8 +2977,7 @@
}
@override
- bool isVoid(Type type) =>
- type is PrimaryType && type.name == 'void' && type.args.isEmpty;
+ bool isVoid(Type type) => type is VoidType;
@override
TypeSchema iterableTypeSchema(TypeSchema elementTypeSchema) {
@@ -3006,15 +3001,15 @@
return type1;
} else if (promoteToNonNull(type2) == type1) {
return type2;
- } else if (type1.type == 'Null' && promoteToNonNull(type2) != type2) {
+ } else if (type1 is NullType && promoteToNonNull(type2) != type2) {
// type2 is already nullable
return type2;
- } else if (type2.type == 'Null' && promoteToNonNull(type1) != type1) {
+ } else if (type2 is NullType && promoteToNonNull(type1) != type1) {
// type1 is already nullable
return type1;
- } else if (type1.type == 'Never') {
+ } else if (type1 is NeverType) {
return type2;
- } else if (type2.type == 'Never') {
+ } else if (type2 is NeverType) {
return type1;
} else {
var typeNames = [type1.type, type2.type];
@@ -3025,11 +3020,11 @@
}
@override
- Type makeNullable(Type type) => lub(type, Type('Null'));
+ Type makeNullable(Type type) => lub(type, NullType.instance);
@override
TypeSchema makeTypeSchemaNullable(TypeSchema typeSchema) =>
- TypeSchema.fromType(lub(typeSchema.toType(), Type('Null')));
+ TypeSchema.fromType(lub(typeSchema.toType(), NullType.instance));
@override
Type mapType({
@@ -3050,10 +3045,8 @@
@override
Type? matchFutureOr(Type type) {
Type underlyingType = withNullabilitySuffix(type, NullabilitySuffix.none);
- if (underlyingType is PrimaryType && underlyingType.args.length == 1) {
- if (underlyingType.name == 'FutureOr') {
- return underlyingType.args[0];
- }
+ if (underlyingType is FutureOrType) {
+ return underlyingType.typeArgument;
}
return null;
}
@@ -3143,8 +3136,8 @@
Type promoteToNonNull(Type type) {
if (type is QuestionType) {
return type.innerType;
- } else if (type.type == 'Null') {
- return Type('Never');
+ } else if (type is NullType) {
+ return NeverType.instance;
} else {
return type;
}
@@ -3202,7 +3195,7 @@
@override
bool typeSchemaIsDynamic(TypeSchema typeSchema) {
var type = typeSchema.toType();
- return type is PrimaryType && type.name == 'dynamic' && type.args.isEmpty;
+ return type is DynamicType;
}
@override
@@ -3366,7 +3359,7 @@
var rhsType =
h.typeAnalyzer.analyzeExpression(rhs, h.operations.unknownType);
h.flow.nullAwareAccess_end();
- var type = h.operations.lub(rhsType, Type('Null'));
+ var type = h.operations.lub(rhsType, NullType.instance);
h.irBuilder.apply(
_fakeMethodName, [Kind.expression, Kind.expression], Kind.expression,
location: location);
@@ -5404,8 +5397,6 @@
final _irBuilder = MiniIRBuilder();
- late final Type nullType = Type('Null');
-
@override
final TypeAnalyzerOptions options;
@@ -5425,6 +5416,8 @@
FlowAnalysis<Node, Statement, Expression, Var, Type> get flow =>
_harness.flow;
+ Type get nullType => NullType.instance;
+
@override
MiniAstOperations get operations => _harness.operations;
diff --git a/pkg/_fe_analyzer_shared/test/mini_types.dart b/pkg/_fe_analyzer_shared/test/mini_types.dart
index ab58808..3c6c10a 100644
--- a/pkg/_fe_analyzer_shared/test/mini_types.dart
+++ b/pkg/_fe_analyzer_shared/test/mini_types.dart
@@ -8,6 +8,14 @@
import 'package:_fe_analyzer_shared/src/types/shared_type.dart';
+/// Representation of the type `dynamic` suitable for unit testing of code in
+/// the `_fe_analyzer_shared` package.
+class DynamicType extends _SpecialSimpleType {
+ static final instance = DynamicType._();
+
+ DynamicType._() : super._('dynamic');
+}
+
/// Representation of a function type suitable for unit testing of code in the
/// `_fe_analyzer_shared` package.
///
@@ -57,6 +65,38 @@
}
}
+/// 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)
+ : super._withSpecialName('FutureOr', args: [typeArgument]);
+
+ Type get typeArgument => args.single;
+
+ @override
+ Type? closureWithRespectToUnknown({required bool covariant}) {
+ Type? newArg =
+ typeArgument.closureWithRespectToUnknown(covariant: covariant);
+ if (newArg == null) return null;
+ return FutureOrType(newArg);
+ }
+
+ @override
+ Type? recursivelyDemote({required bool covariant}) {
+ Type? newArg = typeArgument.recursivelyDemote(covariant: covariant);
+ if (newArg == null) return null;
+ return FutureOrType(newArg);
+ }
+}
+
+/// Representation of an invalid type suitable for unit testing of code in the
+/// `_fe_analyzer_shared` package.
+class InvalidType extends _SpecialSimpleType {
+ static final instance = InvalidType._();
+
+ InvalidType._() : super._('error');
+}
+
class NamedType implements SharedNamedType<Type> {
@override
final String name;
@@ -67,6 +107,22 @@
NamedType({required this.name, required this.type});
}
+/// Representation of the type `Never` suitable for unit testing of code in the
+/// `_fe_analyzer_shared` package.
+class NeverType extends _SpecialSimpleType {
+ static final instance = NeverType._();
+
+ NeverType._() : super._('Never');
+}
+
+/// Representation of the type `Null` suitable for unit testing of code in the
+/// `_fe_analyzer_shared` package.
+class NullType extends _SpecialSimpleType {
+ static final instance = NullType._();
+
+ NullType._() : super._('Null');
+}
+
/// Exception thrown if a type fails to parse properly.
class ParseError extends Error {
final String message;
@@ -85,10 +141,12 @@
class PrimaryType extends Type {
/// Names of primary types not originating from a class, a mixin, or an enum.
static const List<String> namedNonInterfaceTypes = [
+ 'dynamic',
+ 'error',
'FutureOr',
'Never',
'Null',
- 'dynamic',
+ 'void'
];
/// The name of the type.
@@ -97,7 +155,19 @@
/// 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._() {
+ if (namedNonInterfaceTypes.contains(name)) {
+ throw StateError('Tried to create a PrimaryType with special name $name');
+ }
+ }
+
+ PrimaryType._withSpecialName(this.name, {this.args = const []}) : super._() {
+ if (!namedNonInterfaceTypes.contains(name)) {
+ throw StateError(
+ 'Tried to use PrimaryType._withSpecialName with non-special name '
+ '$name');
+ }
+ }
bool get isInterfaceType => !namedNonInterfaceTypes.contains(name);
@@ -148,7 +218,7 @@
@override
Type? recursivelyDemote({required bool covariant}) =>
- covariant ? innerType : new PrimaryType('Never');
+ covariant ? innerType : NeverType.instance;
@override
String _toString({required bool allowSuffixes}) {
@@ -413,8 +483,6 @@
'String': (_) => [Type('Object')],
};
- static final _nullType = Type('Null');
-
static final _objectQuestionType = Type('Object?');
static final _objectType = Type('Object');
@@ -435,10 +503,10 @@
Type factor(Type t, Type s) {
// If T <: S then Never
- if (isSubtype(t, s)) return Type('Never');
+ 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, s)) {
+ if (t is QuestionType && isSubtype(NullType.instance, s)) {
return factor(t.innerType, s);
}
@@ -446,20 +514,22 @@
if (t is QuestionType) return QuestionType(factor(t.innerType, s));
// Else if T is R* and Null <: S then factor(R, S)
- if (t is StarType && isSubtype(_nullType, s)) return factor(t.innerType, s);
+ if (t is StarType && isSubtype(NullType.instance, s)) {
+ return factor(t.innerType, s);
+ }
// Else if T is R* then factor(R, S)*
if (t is StarType) return StarType(factor(t.innerType, s));
// Else if T is FutureOr<R> and Future<R> <: S then factor(R, S)
- if (t is PrimaryType && t.args.length == 1 && t.name == 'FutureOr') {
- var r = t.args[0];
+ if (t is FutureOrType) {
+ var r = t.typeArgument;
if (isSubtype(PrimaryType('Future', args: [r]), s)) return factor(r, s);
}
// Else if T is FutureOr<R> and R <: S then factor(Future<R>, S)
- if (t is PrimaryType && t.args.length == 1 && t.name == 'FutureOr') {
- var r = t.args[0];
+ if (t is FutureOrType) {
+ var r = t.typeArgument;
if (isSubtype(r, s)) return factor(PrimaryType('Future', args: [r]), s);
}
@@ -492,14 +562,12 @@
if (_isTop(t1)) return true;
// Left Top: if T0 is dynamic or void then T0 <: T1 if Object? <: T1
- if (t0 is PrimaryType &&
- t0.args.isEmpty &&
- (t0.name == 'dynamic' || t0.name == 'error' || t0.name == 'void')) {
+ if (t0 is DynamicType || t0 is InvalidType || t0 is VoidType) {
return isSubtype(_objectQuestionType, t1);
}
// Left Bottom: if T0 is Never then T0 <: T1
- if (t0 is PrimaryType && t0.args.isEmpty && t0.name == 'Never') return true;
+ if (t0 is NeverType) return true;
// Right Object: if T1 is Object then:
if (t1 is PrimaryType && t1.args.isEmpty && t1.name == 'Object') {
@@ -515,8 +583,8 @@
}
// - if T0 is FutureOr<S> for some S, then T0 <: T1 iff S <: Object.
- if (t0 is PrimaryType && t0.args.length == 1 && t0.name == 'FutureOr') {
- return isSubtype(t0.args[0], _objectType);
+ if (t0 is FutureOrType) {
+ return isSubtype(t0.typeArgument, _objectType);
}
// - if T0 is S* for any S, then T0 <: T1 iff S <: T1
@@ -525,12 +593,10 @@
// - 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
// false).
- if (t0 is PrimaryType &&
- t0.args.isEmpty &&
- (t0.name == 'Null' ||
- t0.name == 'dynamic' ||
- t0.name == 'error' ||
- t0.name == 'void') ||
+ if (t0 is NullType ||
+ t0 is DynamicType ||
+ t0 is InvalidType ||
+ t0 is VoidType ||
t0 is QuestionType) {
return false;
}
@@ -540,20 +606,18 @@
}
// Left Null: if T0 is Null then:
- if (t0 is PrimaryType && t0.args.isEmpty && t0.name == 'Null') {
+ if (t0 is NullType) {
// - if T1 is a type variable (promoted or not) the query is false
if (_isTypeVar(t1)) return false;
// - If T1 is FutureOr<S> for some S, then the query is true iff
// Null <: S.
- if (t1 is PrimaryType && t1.args.length == 1 && t1.name == 'FutureOr') {
- return isSubtype(_nullType, t1.args[0]);
+ if (t1 is FutureOrType) {
+ return isSubtype(NullType.instance, t1.typeArgument);
}
// - If T1 is Null, S? or S* for some S, then the query is true.
- if (t1 is PrimaryType && t1.args.isEmpty && t1.name == 'Null' ||
- t1 is QuestionType ||
- t1 is StarType) {
+ if (t1 is NullType || t1 is QuestionType || t1 is StarType) {
return true;
}
@@ -574,8 +638,8 @@
}
// Left FutureOr: if T0 is FutureOr<S0> then:
- if (t0 is PrimaryType && t0.args.length == 1 && t0.name == 'FutureOr') {
- var s0 = t0.args[0];
+ if (t0 is FutureOrType) {
+ var s0 = t0.typeArgument;
// - T0 <: T1 iff Future<S0> <: T1 and S0 <: T1
return isSubtype(PrimaryType('Future', args: [s0]), t1) &&
@@ -585,7 +649,7 @@
// Left Nullable: if T0 is S0? then:
if (t0 is QuestionType) {
// - T0 <: T1 iff S0 <: T1 and Null <: T1
- return isSubtype(t0.innerType, t1) && isSubtype(_nullType, t1);
+ return isSubtype(t0.innerType, t1) && isSubtype(NullType.instance, t1);
}
// Type Variable Reflexivity 1: if T0 is a type variable X0 or a promoted
@@ -614,8 +678,8 @@
}
// Right FutureOr: if T1 is FutureOr<S1> then:
- if (t1 is PrimaryType && t1.args.length == 1 && t1.name == 'FutureOr') {
- var s1 = t1.args[0];
+ if (t1 is FutureOrType) {
+ var s1 = t1.typeArgument;
// - T0 <: T1 iff any of the following hold:
return
@@ -640,7 +704,7 @@
// - either T0 <: S1
isSubtype(t0, s1) ||
// - or T0 <: Null
- isSubtype(t0, _nullType) ||
+ isSubtype(t0, NullType.instance) ||
// - or T0 is X0 and X0 has bound S0 and S0 <: T1
t0 is PrimaryType &&
_isTypeVar(t0) &&
@@ -817,8 +881,7 @@
bool _isTop(Type t) {
if (t is PrimaryType) {
- return t.args.isEmpty &&
- (t.name == 'dynamic' || t.name == 'error' || t.name == 'void');
+ return t is DynamicType || t is InvalidType || t is VoidType;
} else if (t is QuestionType) {
var innerType = t.innerType;
return innerType is PrimaryType &&
@@ -858,7 +921,7 @@
@override
Type closureWithRespectToUnknown({required bool covariant}) =>
- covariant ? Type('Object?') : Type('Never');
+ covariant ? Type('Object?') : NeverType.instance;
@override
Type? recursivelyDemote({required bool covariant}) => null;
@@ -867,6 +930,30 @@
String _toString({required bool allowSuffixes}) => '?';
}
+/// Representation of the type `void` suitable for unit testing of code in the
+/// `_fe_analyzer_shared` package.
+class VoidType extends _SpecialSimpleType {
+ static final instance = VoidType._();
+
+ VoidType._() : super._('void');
+}
+
+/// Shared implementation of the types `void`, `dynamic`, `null`, `Never`, and
+/// the invalid type.
+///
+/// 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();
+
+ @override
+ Type? closureWithRespectToUnknown({required bool covariant}) => null;
+
+ @override
+ Type? recursivelyDemote({required bool covariant}) => null;
+}
+
class _TypeParser {
static final _typeTokenizationRegexp =
RegExp(_identifierPattern + r'|\(|\)|<|>|,|\?|\*|&|{|}');
@@ -1053,7 +1140,39 @@
} else {
typeArgs = const [];
}
- return PrimaryType(typeName, args: typeArgs);
+ if (typeName == 'dynamic') {
+ if (typeArgs.isNotEmpty) {
+ throw ParseError('`dynamic` does not accept type arguments');
+ }
+ return DynamicType.instance;
+ } else if (typeName == 'error') {
+ if (typeArgs.isNotEmpty) {
+ throw ParseError('`error` does not accept type arguments');
+ }
+ return InvalidType.instance;
+ } else if (typeName == 'FutureOr') {
+ if (typeArgs.length != 1) {
+ throw ParseError('`FutureOr` requires exactly one type argument');
+ }
+ return FutureOrType(typeArgs.single);
+ } else if (typeName == 'Never') {
+ if (typeArgs.isNotEmpty) {
+ throw ParseError('`Never` does not accept type arguments');
+ }
+ return NeverType.instance;
+ } else if (typeName == 'Null') {
+ if (typeArgs.isNotEmpty) {
+ throw ParseError('`Null` does not accept type arguments');
+ }
+ return NullType.instance;
+ } else if (typeName == 'void') {
+ if (typeArgs.isNotEmpty) {
+ throw ParseError('`void` does not accept type arguments');
+ }
+ return VoidType.instance;
+ } else {
+ return PrimaryType(typeName, args: typeArgs);
+ }
}
static Type parse(String typeStr) {
diff --git a/pkg/_fe_analyzer_shared/test/mini_types_test.dart b/pkg/_fe_analyzer_shared/test/mini_types_test.dart
index 72891c3..d902d36 100644
--- a/pkg/_fe_analyzer_shared/test/mini_types_test.dart
+++ b/pkg/_fe_analyzer_shared/test/mini_types_test.dart
@@ -35,6 +35,31 @@
test('invalid type arg separator', () {
expect(() => Type('Map<int) String>'), throwsParseError);
});
+
+ test('dynamic', () {
+ expect(Type('dynamic'), same(DynamicType.instance));
+ });
+
+ test('error', () {
+ expect(Type('error'), same(InvalidType.instance));
+ });
+
+ test('FutureOr', () {
+ var t = Type('FutureOr<int>') as FutureOrType;
+ expect(t.typeArgument.type, 'int');
+ });
+
+ test('Never', () {
+ expect(Type('Never'), same(NeverType.instance));
+ });
+
+ test('Null', () {
+ expect(Type('Null'), same(NullType.instance));
+ });
+
+ test('void', () {
+ expect(Type('void'), same(VoidType.instance));
+ });
});
test('invalid initial token', () {