Land new subtyping algorithm; more tests but still not comprehensive.
Includes changes to implicit covariance test.
To redescribe this, for some tear-off C<T>.f, the member type may be
contravariant if T is referenced by a parameter of C<T>.f (such as if it
is of type void Function(T)). This must be a runtime failure if C<T> is
a reference which has been covariantly upcast at runtime. For example,
List<Object>.add, where the list is at runtime an instance of List<int>,
results in a static type of void Function(Object), but a runtime type of
void Function(int), which is unsound.
We omit these checks, however, when it is trivially sound for all cases
Previously, this was the case for tearing off `List<Null>.add`. This is
no longer a valid test case for NNBD where the runtime type of that list
could actually be `List<Never>`.
Therefore the test has been changed to expect `List<Null>.add` to result
in a cast, and a new NNBD test case has been added to test `List<Never>`
cases don't get a cast.
Change-Id: I61d024a249e2c2a249ad5a8037cbe3d103e3ea8b
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/104361
Commit-Queue: Mike Fairhurst <mfairhurst@google.com>
Reviewed-by: Paul Berry <paulberry@google.com>
diff --git a/pkg/analyzer/lib/src/generated/type_system.dart b/pkg/analyzer/lib/src/generated/type_system.dart
index 7b41701..29716e8 100644
--- a/pkg/analyzer/lib/src/generated/type_system.dart
+++ b/pkg/analyzer/lib/src/generated/type_system.dart
@@ -33,7 +33,8 @@
* This is expressed by their topiness (higher = more toppy).
*/
int _getTopiness(DartType t) {
- assert(_isTop(t), 'only Top types have a topiness');
+ // TODO(mfairhurst): switch legacy Top checks to true Top checks
+ assert(_isLegacyTop(t, orTrueTop: true), 'only Top types have a topiness');
// Highest top
if (t.isVoid) return 3;
@@ -50,13 +51,26 @@
}
bool _isBottom(DartType t) {
- return t.isBottom ||
- // TODO(mfairhurst): Remove the exception treating Null* as Top.
- t.isDartCoreNull &&
- (t as TypeImpl).nullabilitySuffix == NullabilitySuffix.star ||
+ return (t.isBottom &&
+ (t as TypeImpl).nullabilitySuffix != NullabilitySuffix.question) ||
identical(t, UnknownInferredType.instance);
}
+/// Is [t] the bottom of the legacy type hierarchy.
+bool _isLegacyBottom(DartType t, {@required bool orTrueBottom}) {
+ return (t.isBottom &&
+ (t as TypeImpl).nullabilitySuffix == NullabilitySuffix.question) ||
+ t.isDartCoreNull ||
+ (orTrueBottom ? _isBottom(t) : false);
+}
+
+/// Is [t] the top of the legacy type hierarch.
+bool _isLegacyTop(DartType t, {@required bool orTrueTop}) =>
+// TODO(mfairhurst): handle FutureOr<LegacyTop> cases, with tests.
+ (t.isObject &&
+ (t as TypeImpl).nullabilitySuffix == NullabilitySuffix.none) ||
+ (orTrueTop ? _isTop(t) : false);
+
bool _isTop(DartType t) {
if (t.isDartAsyncFutureOr) {
return _isTop((t as InterfaceType).typeArguments[0]);
@@ -141,16 +155,23 @@
// For the purpose of GLB, we say some Tops are subtypes (less toppy) than
// the others. Return the least toppy.
- if (_isTop(type1) && _isTop(type2)) {
+ // TODO(mfairhurst): switch legacy Top checks to true Top checks
+ if (_isLegacyTop(type1, orTrueTop: true) &&
+ _isLegacyTop(type2, orTrueTop: true)) {
return _getTopiness(type1) < _getTopiness(type2) ? type1 : type2;
}
// The GLB of top and any type is just that type.
// Also GLB of bottom and any type is bottom.
- if (_isTop(type1) || _isBottom(type2)) {
+ // TODO(mfairhurst): switch legacy Top checks to true Top checks
+ // TODO(mfairhurst): switch legacy Bottom checks to true Bottom checks.
+ if (_isLegacyTop(type1, orTrueTop: true) ||
+ _isLegacyBottom(type2, orTrueBottom: true)) {
return type2;
}
- if (_isTop(type2) || _isBottom(type1)) {
+ // TODO(mfairhurst): switch legacy Bottom checks to true Bottom checks
+ if (_isLegacyTop(type2, orTrueTop: true) ||
+ _isLegacyBottom(type1, orTrueBottom: true)) {
return type1;
}
@@ -515,13 +536,17 @@
if (t is TypeParameterType) {
return false;
}
- if (_isTop(t)) {
+ // TODO(mfairhurst): switch legacy Top checks to true Top checks
+ if (_isLegacyTop(t, orTrueTop: true)) {
return true;
}
if (t is FunctionType) {
- if (!_isTop(t.returnType) ||
- anyParameterType(t, (pt) => !_isBottom(pt))) {
+ // TODO(mfairhurst): switch legacy Top checks to true Top checks
+ // TODO(mfairhurst): switch legacy Bottom checks to true Bottom checks
+ if (!_isLegacyTop(t.returnType, orTrueTop: true) ||
+ anyParameterType(
+ t, (pt) => !_isLegacyBottom(pt, orTrueBottom: true))) {
return false;
} else {
return true;
@@ -531,7 +556,8 @@
if (t is InterfaceType) {
List<DartType> typeArguments = t.typeArguments;
for (DartType typeArgument in typeArguments) {
- if (!_isTop(typeArgument)) return false;
+ // TODO(mfairhurst): switch legacy Top checks to true Top checks
+ if (!_isLegacyTop(typeArgument, orTrueTop: true)) return false;
}
return true;
}
@@ -577,6 +603,14 @@
var t1 = _t1 as TypeImpl;
var t2 = _t2 as TypeImpl;
+ // Convert Null to Never? so that NullabilitySuffix can handle more cases.
+ if (t1.isDartCoreNull) {
+ t1 = BottomTypeImpl.instanceNullable;
+ }
+ if (t2.isDartCoreNull) {
+ t2 = BottomTypeImpl.instanceNullable;
+ }
+
if (identical(t1, t2)) {
return true;
}
@@ -585,25 +619,26 @@
//
// We proceed by eliminating these different classes from consideration.
- // Trivially true.
- //
- // Note that `?` is treated as a top and a bottom type during inference,
- // so it's also covered here.
- if (_isTop(t2) || _isBottom(t1)) {
+ // `?` is treated as a top and a bottom type during inference.
+ if (identical(t1, UnknownInferredType.instance) ||
+ identical(t2, UnknownInferredType.instance)) {
return true;
}
- // Trivially false.
- if (_isTop(t1) || _isBottom(t2)) {
- return false;
+ // Trivial top case.
+ if (_isTop(t2)) {
+ return true;
}
- // TODO(mfairhurst): Convert Null to Never?, to simplify the algorithm, and
- // remove this check.
- if (t1.isDartCoreNull) {
- if (t2.nullabilitySuffix != NullabilitySuffix.none) {
- return true;
- }
+ // Legacy top case. Must be done now to find Object* <: Object.
+ if (t1.nullabilitySuffix == NullabilitySuffix.star &&
+ _isLegacyTop(t2, orTrueTop: false)) {
+ return true;
+ }
+
+ // Having excluded RHS top, this now must be false.
+ if (_isTop(t1)) {
+ return false;
}
// Handle T1? <: T2
@@ -614,16 +649,23 @@
return false;
}
- // T1? <: FutureOr<T2> is true if T2 is S2?.
- // TODO(mfairhurst): handle T1? <: FutureOr<dynamic>, etc.
- if (t2 is InterfaceTypeImpl &&
- (t2.typeArguments[0] as TypeImpl).nullabilitySuffix ==
- NullabilitySuffix.none) {
+ // T1? <: FutureOr<S2> is true if S2 is nullable.
+ final s2 = (t2 as InterfaceType).typeArguments[0];
+ if (!isNullable(s2)) {
return false;
}
}
}
+ // Legacy bottom cases
+ if (_isLegacyBottom(t1, orTrueBottom: true)) {
+ return true;
+ }
+
+ if (_isLegacyBottom(t2, orTrueBottom: true)) {
+ return false;
+ }
+
// Handle FutureOr<T> union type.
if (t1 is InterfaceTypeImpl && t1.isDartAsyncFutureOr) {
var t1TypeArg = t1.typeArguments[0];
@@ -1775,7 +1817,10 @@
return false;
}
- if (_isBottom(t1) || _isTop(t2)) return true;
+ // TODO(mfairhurst): switch legacy Bottom checks to true Bottom checks
+ // TODO(mfairhurst): switch legacy Top checks to true Top checks
+ if (_isLegacyBottom(t1, orTrueBottom: true) ||
+ _isLegacyTop(t2, orTrueTop: true)) return true;
if (t1 is InterfaceType && t2 is InterfaceType) {
return _matchInterfaceSubtypeOf(t1, t2, visited, origin,
@@ -1955,16 +2000,24 @@
// For the purpose of LUB, we say some Tops are subtypes (less toppy) than
// the others. Return the most toppy.
- if (_isTop(type1) && _isTop(type2)) {
+ // TODO(mfairhurst): switch legacy Top checks to true Top checks
+ if (_isLegacyTop(type1, orTrueTop: true) &&
+ _isLegacyTop(type2, orTrueTop: true)) {
return _getTopiness(type1) > _getTopiness(type2) ? type1 : type2;
}
// The least upper bound of top and any type T is top.
// The least upper bound of bottom and any type T is T.
- if (_isTop(type1) || _isBottom(type2)) {
+ // TODO(mfairhurst): switch legacy Top checks to true Top checks
+ // TODO(mfairhurst): switch legacy Bottom checks to true Bottom checks
+ if (_isLegacyTop(type1, orTrueTop: true) ||
+ _isLegacyBottom(type2, orTrueBottom: true)) {
return type1;
}
- if (_isTop(type2) || _isBottom(type1)) {
+ // TODO(mfairhurst): switch legacy Top checks to true Top checks
+ // TODO(mfairhurst): switch legacy Bottom checks to true Bottom checks
+ if (_isLegacyTop(type2, orTrueTop: true) ||
+ _isLegacyBottom(type1, orTrueBottom: true)) {
return type2;
}
diff --git a/pkg/analyzer/lib/src/task/strong/checker.dart b/pkg/analyzer/lib/src/task/strong/checker.dart
index 7725c55..b286b97 100644
--- a/pkg/analyzer/lib/src/task/strong/checker.dart
+++ b/pkg/analyzer/lib/src/task/strong/checker.dart
@@ -896,7 +896,7 @@
// parameters are properly substituted.
var classType = targetType.element.type;
var classLowerBound = classType.instantiate(new List.filled(
- classType.typeParameters.length, typeProvider.bottomType));
+ classType.typeParameters.length, BottomTypeImpl.instance));
var memberLowerBound = inheritance.getMember(
classLowerBound, Name(element.librarySource.uri, element.name));
var expectedType = invokeType.returnType;
diff --git a/pkg/analyzer/test/generated/strong_mode_test.dart b/pkg/analyzer/test/generated/strong_mode_test.dart
index 1f4358e..176d24f 100644
--- a/pkg/analyzer/test/generated/strong_mode_test.dart
+++ b/pkg/analyzer/test/generated/strong_mode_test.dart
@@ -4,6 +4,7 @@
import 'dart:async';
+import 'package:analyzer/dart/analysis/features.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/standard_resolution_map.dart';
import 'package:analyzer/dart/element/element.dart';
@@ -26,6 +27,7 @@
defineReflectiveSuite(() {
defineReflectiveTests(StrongModeCastsTest);
defineReflectiveTests(StrongModeLocalInferenceTest);
+ defineReflectiveTests(StrongModeLocalInferenceTest_NNBD);
defineReflectiveTests(StrongModeStaticTypeAnalyzer2Test);
defineReflectiveTests(StrongModeTypePropagationTest);
});
@@ -1708,6 +1710,12 @@
cD.m1;
cD.m1();
cD.m2();
+
+ cN.f;
+ cN.g;
+ cN.m1;
+ cN.m1();
+ cN.m2();
}
noCasts() {
@@ -1723,12 +1731,6 @@
(d.g)(42);
d.m1()(42);
d.m2()()(42);
-
- cN.f;
- cN.g;
- cN.m1;
- cN.m1();
- cN.m2();
}
''');
var unit = (await computeAnalysisResult(source)).unit;
@@ -3907,6 +3909,233 @@
}
@reflectiveTest
+class StrongModeLocalInferenceTest_NNBD extends ResolverTestCase {
+ @override
+ AnalysisOptions get analysisOptions => new AnalysisOptionsImpl()
+ ..contextFeatures =
+ new FeatureSet.forTesting(additionalFeatures: [Feature.non_nullable]);
+
+ @override
+ void setUp() {
+ //TODO(mfairhurst): why is this override required?
+ super.setUp();
+ AnalysisOptionsImpl options = analysisOptions;
+ resetWith(options: options);
+ }
+
+ test_covarianceChecks_returnFunction() async {
+ // test Never cases.
+ var source = addSource(r'''
+typedef F<T>(T t);
+typedef T R<T>();
+class C<T> {
+ F<T> f = throw '';
+
+ C();
+ factory C.fact() => new C<Never>();
+
+ F<T> get g => throw '';
+ F<T> m1() => throw '';
+ R<F<T>> m2() => throw '';
+
+ casts(C<T> other, T t) {
+ other.f;
+ other.g(t);
+ other.m1();
+ other.m2;
+
+ new C<T>.fact().f(t);
+ new C<int>.fact().g;
+ new C<int>.fact().m1;
+ new C<T>.fact().m2();
+
+ new C<Object>.fact().f(42);
+ new C<Object>.fact().g;
+ new C<Object>.fact().m1;
+ new C<Object>.fact().m2();
+
+ new C.fact().f(42);
+ new C.fact().g;
+ new C.fact().m1;
+ new C.fact().m2();
+ }
+
+ noCasts(T t) {
+ f;
+ g;
+ m1();
+ m2();
+
+ f(t);
+ g(t);
+ (f)(t);
+ (g)(t);
+ m1;
+ m2;
+
+ this.f;
+ this.g;
+ this.m1();
+ this.m2();
+ this.m1;
+ this.m2;
+ (this.m1)();
+ (this.m2)();
+ this.f(t);
+ this.g(t);
+ (this.f)(t);
+ (this.g)(t);
+
+ new C<int>().f;
+ new C<T>().g;
+ new C<int>().m1();
+ new C().m2();
+
+ new D().f;
+ new D().g;
+ new D().m1();
+ new D().m2();
+ }
+}
+class D extends C<num> {
+ noCasts(t) {
+ f;
+ this.g;
+ this.m1();
+ m2;
+
+ super.f;
+ super.g;
+ super.m1;
+ super.m2();
+ }
+}
+
+D d = throw '';
+C<Object> c = throw '';
+C cD = throw '';
+C<Null> cNu = throw '';
+C<Never> cN = throw '';
+F<Object> f = throw '';
+F<Never> fN = throw '';
+R<F<Object>> rf = throw '';
+R<F<Never>> rfN = throw '';
+R<R<F<Object>>> rrf = throw '';
+R<R<F<Never>>> rrfN = throw '';
+Object obj = throw '';
+F<int> fi = throw '';
+R<F<int>> rfi = throw '';
+R<R<F<int>>> rrfi = throw '';
+
+casts() {
+ c.f;
+ c.g;
+ c.m1;
+ c.m1();
+ c.m2();
+
+ fN = c.f;
+ fN = c.g;
+ rfN = c.m1;
+ rrfN = c.m2;
+ fN = c.m1();
+ rfN = c.m2();
+
+ f = c.f;
+ f = c.g;
+ rf = c.m1;
+ rrf = c.m2;
+ f = c.m1();
+ rf = c.m2();
+ c.m2()();
+
+ c.f(obj);
+ c.g(obj);
+ (c.f)(obj);
+ (c.g)(obj);
+ (c.m1)();
+ c.m1()(obj);
+ (c.m2)();
+
+ cD.f;
+ cD.g;
+ cD.m1;
+ cD.m1();
+ cD.m2();
+
+ cNu.f;
+ cNu.g;
+ cNu.m1;
+ cNu.m1();
+ cNu.m2();
+}
+
+noCasts() {
+ fi = d.f;
+ fi = d.g;
+ rfi = d.m1;
+ fi = d.m1();
+ rrfi = d.m2;
+ rfi = d.m2();
+ d.f(42);
+ d.g(42);
+ (d.f)(42);
+ (d.g)(42);
+ d.m1()(42);
+ d.m2()()(42);
+
+ cN.f;
+ cN.g;
+ cN.m1;
+ cN.m1();
+ cN.m2();
+}
+''');
+ var unit = (await computeAnalysisResult(source)).unit;
+ assertNoErrors(source);
+
+ void expectCast(Statement statement, bool hasCast) {
+ var value = (statement as ExpressionStatement).expression;
+ if (value is AssignmentExpression) {
+ value = (value as AssignmentExpression).rightHandSide;
+ }
+ while (value is FunctionExpressionInvocation) {
+ value = (value as FunctionExpressionInvocation).function;
+ }
+ while (value is ParenthesizedExpression) {
+ value = (value as ParenthesizedExpression).expression;
+ }
+ var isCallingGetter =
+ value is MethodInvocation && !value.methodName.name.startsWith('m');
+ var cast = isCallingGetter
+ ? getImplicitOperationCast(value)
+ : getImplicitCast(value);
+ var castKind = isCallingGetter ? 'special cast' : 'cast';
+ expect(cast, hasCast ? isNotNull : isNull,
+ reason: '`$statement` should ' +
+ (hasCast ? '' : 'not ') +
+ 'have a $castKind on `$value`.');
+ }
+
+ for (var s in AstFinder.getStatementsInMethod(unit, 'C', 'noCasts')) {
+ expectCast(s, false);
+ }
+ for (var s in AstFinder.getStatementsInMethod(unit, 'C', 'casts')) {
+ expectCast(s, true);
+ }
+ for (var s in AstFinder.getStatementsInMethod(unit, 'D', 'noCasts')) {
+ expectCast(s, false);
+ }
+ for (var s in AstFinder.getStatementsInTopLevelFunction(unit, 'noCasts')) {
+ expectCast(s, false);
+ }
+ for (var s in AstFinder.getStatementsInTopLevelFunction(unit, 'casts')) {
+ expectCast(s, true);
+ }
+ }
+}
+
+@reflectiveTest
class StrongModeStaticTypeAnalyzer2Test extends StaticTypeAnalyzer2TestShared {
void expectStaticInvokeType(String search, String type) {
var invocation = findIdentifier(search).parent as MethodInvocation;
diff --git a/pkg/analyzer/test/generated/type_system_test.dart b/pkg/analyzer/test/generated/type_system_test.dart
index 964112f..c3df650 100644
--- a/pkg/analyzer/test/generated/type_system_test.dart
+++ b/pkg/analyzer/test/generated/type_system_test.dart
@@ -1867,19 +1867,41 @@
typeProvider = new NonNullableTypeProvider(coreLibrary, asyncLibrary);
}
+ void test_dynamicType() {
+ List<DartType> equivalents = <DartType>[
+ voidType,
+ _question(objectType),
+ _star(objectType),
+ ];
+ List<DartType> subtypes = <DartType>[bottomType, nullType, objectType];
+ _checkGroups(dynamicType, equivalents: equivalents, subtypes: subtypes);
+ }
+
void test_int_nullableTypes() {
List<DartType> equivalents = <DartType>[
intType,
_star(intType),
];
+ List<DartType> subtypes = <DartType>[
+ bottomType,
+ ];
List<DartType> supertypes = <DartType>[
_question(intType),
objectType,
_question(objectType),
];
- List<DartType> unrelated = <DartType>[doubleType, nullType];
+ List<DartType> unrelated = <DartType>[
+ doubleType,
+ nullType,
+ _star(nullType),
+ _question(nullType),
+ _question(bottomType),
+ ];
_checkGroups(intType,
- equivalents: equivalents, supertypes: supertypes, unrelated: unrelated);
+ equivalents: equivalents,
+ supertypes: supertypes,
+ unrelated: unrelated,
+ subtypes: subtypes);
}
void test_intQuestion_nullableTypes() {
@@ -1890,6 +1912,11 @@
List<DartType> subtypes = <DartType>[
intType,
nullType,
+ _question(nullType),
+ _star(nullType),
+ bottomType,
+ _question(bottomType),
+ _star(bottomType),
];
List<DartType> supertypes = <DartType>[
_question(numType),
@@ -1911,7 +1938,14 @@
_question(intType),
_star(intType),
];
- List<DartType> subtypes = <DartType>[nullType];
+ List<DartType> subtypes = <DartType>[
+ nullType,
+ _star(nullType),
+ _question(nullType),
+ bottomType,
+ _star(bottomType),
+ _question(bottomType),
+ ];
List<DartType> supertypes = <DartType>[
numType,
_question(numType),
@@ -1927,6 +1961,61 @@
subtypes: subtypes);
}
+ void test_nullType() {
+ List<DartType> equivalents = <DartType>[
+ nullType,
+ _question(nullType),
+ _star(nullType),
+ _question(bottomType),
+ ];
+ List<DartType> supertypes = <DartType>[
+ _question(intType),
+ _star(intType),
+ _question(objectType),
+ _star(objectType),
+ dynamicType,
+ voidType,
+ ];
+ List<DartType> subtypes = <DartType>[bottomType];
+ List<DartType> unrelated = <DartType>[
+ doubleType,
+ intType,
+ numType,
+ objectType
+ ];
+
+ for (final formOfNull in equivalents) {
+ _checkGroups(formOfNull,
+ equivalents: equivalents,
+ supertypes: supertypes,
+ unrelated: unrelated,
+ subtypes: subtypes);
+ }
+ }
+
+ void test_objectType() {
+ List<DartType> equivalents = <DartType>[
+ _star(objectType),
+ ];
+ List<DartType> supertypes = <DartType>[
+ _question(objectType),
+ dynamicType,
+ voidType,
+ ];
+ List<DartType> subtypes = <DartType>[bottomType];
+ List<DartType> unrelated = <DartType>[
+ _question(doubleType),
+ _question(numType),
+ _question(intType),
+ nullType
+ ];
+ _checkGroups(objectType,
+ equivalents: equivalents,
+ supertypes: supertypes,
+ unrelated: unrelated,
+ subtypes: subtypes);
+ }
+
DartType _question(DartType dartType) =>
(dartType as TypeImpl).withNullability(NullabilitySuffix.question);