[cfe] Add DartTypeComparator
The utility class DartTypeComparator allows to compare two dart types
under various assumptions, such as ignoring nullability at the
top-level type node or equating all top types.
Change-Id: I998e4a0a3ac236077cd1bcd12a5ad146ff10bb1d
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/149427
Commit-Queue: Dmitry Stefantsov <dmitryas@google.com>
Reviewed-by: Johnni Winther <johnniwinther@google.com>
diff --git a/pkg/front_end/test/spell_checking_list_common.txt b/pkg/front_end/test/spell_checking_list_common.txt
index 25a39e4..4a1e4e1 100644
--- a/pkg/front_end/test/spell_checking_list_common.txt
+++ b/pkg/front_end/test/spell_checking_list_common.txt
@@ -510,6 +510,7 @@
commonly
compact
comparable
+comparator
compare
comparing
comparison
@@ -997,6 +998,7 @@
equal
equality
equals
+equate
equivalence
equivalent
equivalents
@@ -2268,6 +2270,7 @@
properly
properties
property
+proposition
prototype
proves
provide
diff --git a/pkg/kernel/lib/src/dart_type_equivalence.dart b/pkg/kernel/lib/src/dart_type_equivalence.dart
new file mode 100644
index 0000000..731a5fe
--- /dev/null
+++ b/pkg/kernel/lib/src/dart_type_equivalence.dart
@@ -0,0 +1,281 @@
+// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE.md file.
+
+import '../ast.dart';
+import '../core_types.dart';
+import '../visitor.dart';
+
+class DartTypeEquivalence implements DartTypeVisitor1<bool, DartType> {
+ final CoreTypes coreTypes;
+ final bool equateTopTypes;
+ final bool ignoreAllNullabilities;
+ final bool ignoreTopLevelNullability;
+
+ bool _atTopLevel = true;
+ List<Map<TypeParameter, TypeParameter>> _alphaRenamingStack = [];
+
+ DartTypeEquivalence(this.coreTypes,
+ {this.equateTopTypes = false,
+ this.ignoreAllNullabilities = false,
+ this.ignoreTopLevelNullability = false});
+
+ bool areEqual(DartType type1, DartType type2) {
+ _alphaRenamingStack.clear();
+ _atTopLevel = true;
+ return type1.accept1(this, type2);
+ }
+
+ @override
+ bool defaultDartType(DartType node, DartType other) {
+ throw new UnsupportedError("${node.runtimeType}");
+ }
+
+ @override
+ bool visitBottomType(BottomType node, DartType other) {
+ return other is BottomType;
+ }
+
+ @override
+ bool visitDynamicType(DynamicType node, DartType other) {
+ return equateTopTypes ? coreTypes.isTop(other) : other is DynamicType;
+ }
+
+ @override
+ bool visitFunctionType(FunctionType node, DartType other) {
+ if (other is FunctionType) {
+ if (!_checkAndRegisterNullabilities(
+ node.declaredNullability, other.declaredNullability)) {
+ return false;
+ }
+
+ // If the two types are un-aliased typedef instantiations, check that
+ // those instantiations are equal as well.
+ bool nodeIsUnaliased = node.typedefType != null;
+ bool otherIsUnaliased = other.typedefType != null;
+ if (nodeIsUnaliased != otherIsUnaliased) {
+ return false;
+ }
+ if (node.typedefType != null) {
+ // The assert below checks the implication: if the typedef types are
+ // equal, their un-aliased types are equal as well. The reverse is not
+ // true due to possibly unused type parameters F<int> and F<String> may
+ // be equal when un-aliased if the type parameter of typedef F isn't
+ // used on the right-hand side of the definition of F. The checked
+ // proposition in the assert allows to skip checking the function types
+ // themselves if the typedef types are equal.
+ assert(() {
+ DartTypeEquivalence copy = this.copy();
+ if (!copy.areEqual(node.typedefType, other.typedefType)) {
+ return true;
+ }
+ FunctionType nodeWithoutTypedefType = new FunctionType(
+ node.positionalParameters,
+ node.returnType,
+ node.declaredNullability,
+ namedParameters: node.namedParameters,
+ typeParameters: node.typeParameters,
+ requiredParameterCount: node.requiredParameterCount,
+ typedefType: null);
+ FunctionType otherWithoutTypedefType = new FunctionType(
+ other.positionalParameters,
+ other.returnType,
+ other.declaredNullability,
+ namedParameters: other.namedParameters,
+ typeParameters: other.typeParameters,
+ requiredParameterCount: other.requiredParameterCount,
+ typedefType: null);
+ return copy.areEqual(nodeWithoutTypedefType, otherWithoutTypedefType);
+ }());
+ return node.typedefType.accept1(this, other.typedefType);
+ }
+
+ // Perform simple number checks before the checks on parts.
+ if (node.typeParameters.length != other.typeParameters.length) {
+ return false;
+ }
+ if (node.positionalParameters.length !=
+ other.positionalParameters.length) {
+ return false;
+ }
+ if (node.requiredParameterCount != other.requiredParameterCount) {
+ return false;
+ }
+ if (node.namedParameters.length != other.namedParameters.length) {
+ return false;
+ }
+
+ // Enter new static scope. The scope must be exited before a return. To
+ // void multiple returns, the [result] variable is used below.
+ _pushTypeParameters(node.typeParameters, other.typeParameters);
+ bool result = true;
+
+ for (int i = 0; result && i < node.typeParameters.length; ++i) {
+ if (!node.typeParameters[i].bound
+ .accept1(this, other.typeParameters[i].bound)) {
+ result = false;
+ }
+ // Don't check defaultTypes: they are a convenience mechanism.
+ }
+ for (int i = 0; result && i < node.positionalParameters.length; ++i) {
+ if (!node.positionalParameters[i]
+ .accept1(this, other.positionalParameters[i])) {
+ result = false;
+ }
+ }
+ Map<String, DartType> nodeNamedParameters = {};
+ for (int i = 0; i < node.namedParameters.length; ++i) {
+ nodeNamedParameters[node.namedParameters[i].name] =
+ node.namedParameters[i].type;
+ }
+ for (int i = 0; result && i < other.namedParameters.length; ++i) {
+ String otherName = other.namedParameters[i].name;
+ DartType otherType = other.namedParameters[i].type;
+ if (!nodeNamedParameters.containsKey(otherName) ||
+ !nodeNamedParameters[otherName].accept1(this, otherType)) {
+ result = false;
+ }
+ }
+ if (!node.returnType.accept1(this, other.returnType)) {
+ result = false;
+ }
+
+ _dropTypeParameters();
+ return result;
+ }
+ return false;
+ }
+
+ @override
+ bool visitInterfaceType(InterfaceType node, DartType other) {
+ // First, check Object*, FutureOr<Object?>, etc.
+ if (equateTopTypes && coreTypes.isTop(node)) {
+ return coreTypes.isTop(other);
+ }
+
+ if (other is InterfaceType) {
+ if (!_checkAndRegisterNullabilities(
+ node.declaredNullability, other.declaredNullability)) {
+ return false;
+ }
+ if (node.classNode != other.classNode) {
+ return false;
+ }
+ assert(node.typeArguments.length == other.typeArguments.length);
+ for (int i = 0; i < node.typeArguments.length; ++i) {
+ if (!node.typeArguments[i].accept1(this, other.typeArguments[i])) {
+ return false;
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @override
+ bool visitInvalidType(InvalidType node, DartType other) {
+ return other is InvalidType;
+ }
+
+ @override
+ bool visitNeverType(NeverType node, DartType other) {
+ if (other is NeverType) {
+ return _checkAndRegisterNullabilities(
+ node.declaredNullability, other.declaredNullability);
+ }
+ return false;
+ }
+
+ @override
+ bool visitTypeParameterType(TypeParameterType node, DartType other) {
+ if (other is TypeParameterType) {
+ bool nodeIsIntersection = node.promotedBound != null;
+ bool otherIsIntersection = other.promotedBound != null;
+ if (nodeIsIntersection != otherIsIntersection) {
+ return false;
+ }
+ if (!_checkAndRegisterNullabilities(
+ node.declaredNullability, other.declaredNullability)) {
+ return false;
+ }
+ if (!identical(_lookup(node.parameter), other.parameter)) {
+ return false;
+ }
+ return nodeIsIntersection
+ ? node.promotedBound.accept1(this, other.promotedBound)
+ : true;
+ }
+ return false;
+ }
+
+ @override
+ bool visitTypedefType(TypedefType node, DartType other) {
+ if (other is TypedefType) {
+ if (!_checkAndRegisterNullabilities(
+ node.declaredNullability, other.declaredNullability)) {
+ return false;
+ }
+ if (node.typedefNode != other.typedefNode) {
+ return false;
+ }
+ assert(node.typeArguments.length == other.typeArguments.length);
+ for (int i = 0; i < node.typeArguments.length; ++i) {
+ if (!node.typeArguments[i].accept1(this, node.typeArguments[i])) {
+ return false;
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @override
+ bool visitVoidType(VoidType node, DartType other) {
+ return equateTopTypes ? coreTypes.isTop(other) : other is VoidType;
+ }
+
+ bool _checkAndRegisterNullabilities(
+ Nullability nodeNullability, Nullability otherNullability) {
+ bool result;
+ if (nodeNullability == otherNullability ||
+ ignoreAllNullabilities ||
+ ignoreTopLevelNullability && _atTopLevel) {
+ result = true;
+ } else {
+ result = false;
+ }
+ _atTopLevel = false;
+ return result;
+ }
+
+ void _pushTypeParameters(
+ List<TypeParameter> keys, List<TypeParameter> values) {
+ assert(keys.length == values.length);
+ Map<TypeParameter, TypeParameter> parameters =
+ new Map<TypeParameter, TypeParameter>.identity();
+ for (int i = 0; i < keys.length; ++i) {
+ parameters[keys[i]] = values[i];
+ }
+ _alphaRenamingStack.add(parameters);
+ }
+
+ void _dropTypeParameters() {
+ _alphaRenamingStack.removeLast();
+ }
+
+ TypeParameter _lookup(TypeParameter parameter) {
+ for (int i = _alphaRenamingStack.length - 1; i >= 0; --i) {
+ if (_alphaRenamingStack[i].containsKey(parameter)) {
+ return _alphaRenamingStack[i][parameter];
+ }
+ }
+ return parameter;
+ }
+
+ DartTypeEquivalence copy() {
+ return new DartTypeEquivalence(coreTypes,
+ equateTopTypes: equateTopTypes,
+ ignoreAllNullabilities: ignoreAllNullabilities,
+ ignoreTopLevelNullability: ignoreTopLevelNullability);
+ }
+}
diff --git a/pkg/kernel/test/dart_type_equivalence_test.dart b/pkg/kernel/test/dart_type_equivalence_test.dart
new file mode 100644
index 0000000..4ec0895
--- /dev/null
+++ b/pkg/kernel/test/dart_type_equivalence_test.dart
@@ -0,0 +1,228 @@
+// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
+// 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:expect/expect.dart" show Expect;
+
+import 'package:kernel/ast.dart' hide MapEntry;
+import 'package:kernel/src/dart_type_equivalence.dart';
+import 'package:kernel/testing/type_parser_environment.dart';
+
+run() {
+ // Simple types.
+ areEqual("int", "int");
+ notEqual("int", "String");
+
+ // Simple types with nullabilities.
+ areEqual("int?", "int?");
+ notEqual("int", "int?");
+ notEqual("int?", "String?");
+
+ areEqual("int?", "int?", ignoreAllNullabilities: true);
+ areEqual("int", "int?", ignoreAllNullabilities: true);
+ notEqual("int?", "String?", ignoreAllNullabilities: true);
+
+ areEqual("int?", "int?", ignoreTopLevelNullability: true);
+ areEqual("int", "int?", ignoreTopLevelNullability: true);
+ notEqual("int?", "String?", ignoreTopLevelNullability: true);
+
+ // 1-level deep types.
+ areEqual("List<int?>?", "List<int?>?");
+ notEqual("List<int?>?", "List<int?>");
+ notEqual("List<int?>?", "List<int>?");
+ notEqual("List<int?>?", "List<int>");
+
+ areEqual("List<int?>?", "List<int?>?", ignoreAllNullabilities: true);
+ areEqual("List<int?>?", "List<int?>", ignoreAllNullabilities: true);
+ areEqual("List<int?>?", "List<int>?", ignoreAllNullabilities: true);
+ areEqual("List<int?>?", "List<int>", ignoreAllNullabilities: true);
+
+ areEqual("List<int?>?", "List<int?>?", ignoreTopLevelNullability: true);
+ areEqual("List<int?>?", "List<int?>", ignoreTopLevelNullability: true);
+ notEqual("List<int?>?", "List<int>?", ignoreTopLevelNullability: true);
+ notEqual("List<int?>?", "List<int>", ignoreTopLevelNullability: true);
+
+ // Top types.
+ areEqual("dynamic", "dynamic");
+ notEqual("dynamic", "Object?");
+ notEqual("dynamic", "Object*");
+ notEqual("dynamic", "void");
+ areEqual("Object?", "Object?");
+ notEqual("Object?", "Object*");
+ notEqual("Object?", "void");
+ areEqual("Object*", "Object*");
+ notEqual("Object*", "void");
+ areEqual("void", "void");
+ notEqual("FutureOr<dynamic>", "void");
+ notEqual("FutureOr<FutureOr<Object?>>", "Object*");
+ notEqual("FutureOr<FutureOr<FutureOr<Object*>?>>?", "dynamic");
+ notEqual("FutureOr<Object?>", "FutureOr<Object?>?");
+ notEqual("FutureOr<FutureOr<Object?>>", "FutureOr<FutureOr<Object?>?>?");
+ notEqual("FutureOr<FutureOr<Object?>>", "FutureOr<FutureOr<dynamic>?>?");
+
+ areEqual("dynamic", "Object?", equateTopTypes: true);
+ areEqual("dynamic", "Object*", equateTopTypes: true);
+ areEqual("dynamic", "void", equateTopTypes: true);
+ areEqual("Object?", "Object*", equateTopTypes: true);
+ areEqual("Object?", "void", equateTopTypes: true);
+ areEqual("Object*", "void", equateTopTypes: true);
+ areEqual("FutureOr<dynamic>", "void", equateTopTypes: true);
+ areEqual("FutureOr<FutureOr<Object?>>", "Object*", equateTopTypes: true);
+ areEqual("FutureOr<FutureOr<FutureOr<Object*>?>>?", "dynamic",
+ equateTopTypes: true);
+ areEqual("FutureOr<Object?>", "FutureOr<Object?>?", equateTopTypes: true);
+ areEqual("FutureOr<FutureOr<Object?>>", "FutureOr<FutureOr<Object?>?>?",
+ equateTopTypes: true);
+ areEqual("FutureOr<FutureOr<Object?>>", "FutureOr<FutureOr<dynamic>?>?",
+ equateTopTypes: true);
+
+ areEqual("Object?", "Object*", ignoreAllNullabilities: true);
+ areEqual("FutureOr<Object?>", "FutureOr<Object?>?",
+ ignoreAllNullabilities: true);
+ areEqual("FutureOr<FutureOr<Object?>>", "FutureOr<FutureOr<Object?>?>?",
+ ignoreAllNullabilities: true);
+ notEqual("FutureOr<FutureOr<Object?>>", "FutureOr<FutureOr<dynamic>?>?",
+ ignoreAllNullabilities: true);
+
+ areEqual("FutureOr<Object?>", "FutureOr<Object?>?",
+ ignoreTopLevelNullability: true);
+ notEqual("FutureOr<FutureOr<Object?>>", "FutureOr<FutureOr<Object?>?>?",
+ ignoreTopLevelNullability: true);
+ notEqual("FutureOr<FutureOr<Object?>>", "FutureOr<FutureOr<dynamic>?>?",
+ ignoreTopLevelNullability: true);
+
+ // Generic function types.
+ notEqual("<T extends dynamic>() ->? int", "<T extends Object?>() -> int?");
+ notEqual("<T extends dynamic>() ->? int", "<T extends Object?>() -> int?",
+ equateTopTypes: true);
+ notEqual("<T extends dynamic>() ->? int", "<T extends Object?>() -> int?",
+ ignoreAllNullabilities: true);
+ notEqual("<T extends dynamic>() ->? int", "<T extends Object?>() -> int?",
+ ignoreTopLevelNullability: true);
+ notEqual("<T extends dynamic>() ->? int", "<T extends Object?>() -> int?",
+ equateTopTypes: true, ignoreTopLevelNullability: true);
+ areEqual("<T extends dynamic>() ->? int", "<T extends Object?>() -> int?",
+ equateTopTypes: true, ignoreAllNullabilities: true);
+
+ notEqual("<T extends dynamic>() -> T?", "<S extends Object?>() -> S?");
+ areEqual("<T extends dynamic>() -> T?", "<S extends Object?>() -> S?",
+ equateTopTypes: true);
+
+ notEqual("<T extends dynamic>() -> T", "<S extends FutureOr<void>>() -> S?");
+ notEqual("<T extends dynamic>() -> T", "<S extends FutureOr<void>>() -> S?",
+ equateTopTypes: true);
+ notEqual("<T extends dynamic>() -> T", "<S extends FutureOr<void>>() -> S?",
+ ignoreAllNullabilities: true);
+ areEqual("<T extends dynamic>() -> T", "<S extends FutureOr<void>>() -> S?",
+ equateTopTypes: true, ignoreAllNullabilities: true);
+
+ areEqual("<T>(<S>() -> void) -> void", "<X>(<Y>() -> void) -> void");
+
+ notEqual("<T>(<S extends void>() -> void) -> void",
+ "<X>(<Y extends Object?>() -> void) -> void");
+ notEqual("<T>(<S extends void>() -> void) -> void",
+ "<X>(<Y extends Object?>() -> void) -> void",
+ ignoreAllNullabilities: true);
+
+ // Free type variables.
+ areEqual("T", "T", typeParameters: "T");
+ notEqual("T?", "T", typeParameters: "T");
+
+ notEqual("T", "S",
+ typeParameters: "T, S",
+ equateTopTypes: true,
+ ignoreAllNullabilities: true);
+
+ notEqual("T & int?", "T & int", typeParameters: "T");
+ notEqual("T & int?", "T & int",
+ typeParameters: "T", ignoreTopLevelNullability: true);
+ areEqual("T & int?", "T & int",
+ typeParameters: "T", ignoreAllNullabilities: true);
+
+ // Check that normalization is out of the scope of the equivalence, even with
+ // the most permissive flags.
+ notEqual("Never?", "Null",
+ equateTopTypes: true, ignoreAllNullabilities: true);
+ notEqual("FutureOr<Never>", "Future<Never>",
+ equateTopTypes: true, ignoreAllNullabilities: true);
+ notEqual("FutureOr<Object>", "Object",
+ equateTopTypes: true, ignoreAllNullabilities: true);
+}
+
+areEqual(String type1, String type2,
+ {String typeParameters = '',
+ bool equateTopTypes = false,
+ bool ignoreAllNullabilities = false,
+ bool ignoreTopLevelNullability = false}) {
+ Env env = new Env('')..extendWithTypeParameters(typeParameters);
+ DartType t1 = env.parseType(type1);
+ DartType t2 = env.parseType(type2);
+
+ List<String> flagNamesForDebug = [
+ if (equateTopTypes) "equateTopTypes",
+ if (ignoreAllNullabilities) "ignoreAllNullabilities",
+ if (ignoreTopLevelNullability) "ignoreTopLevelNullability",
+ ];
+
+ print("areEqual(${type1}, ${type2}"
+ "${flagNamesForDebug.map((f) => ", $f").join()})");
+ Expect.isTrue(
+ new DartTypeEquivalence(env.coreTypes,
+ equateTopTypes: equateTopTypes,
+ ignoreAllNullabilities: ignoreAllNullabilities,
+ ignoreTopLevelNullability: ignoreTopLevelNullability)
+ .areEqual(t1, t2),
+ "Expected '${type1}' and '${type2}' to be equal "
+ "with flags ${flagNamesForDebug.map((f) => "'$f'").join(", ")}.");
+
+ print("areEqual(${type2}, ${type1}"
+ "${flagNamesForDebug.map((f) => ", $f").join()})");
+ Expect.isTrue(
+ new DartTypeEquivalence(env.coreTypes,
+ equateTopTypes: equateTopTypes,
+ ignoreAllNullabilities: ignoreAllNullabilities,
+ ignoreTopLevelNullability: ignoreTopLevelNullability)
+ .areEqual(t2, t1),
+ "Expected '${type2}' and '${type1}' to be equal "
+ "with flags ${flagNamesForDebug.map((f) => "'$f'").join(", ")}.");
+}
+
+notEqual(String type1, String type2,
+ {String typeParameters = '',
+ bool equateTopTypes = false,
+ bool ignoreAllNullabilities = false,
+ bool ignoreTopLevelNullability = false}) {
+ Env env = new Env('')..extendWithTypeParameters(typeParameters);
+ DartType t1 = env.parseType(type1);
+ DartType t2 = env.parseType(type2);
+
+ List<String> flagNamesForDebug = [
+ if (equateTopTypes) "equateTopTypes",
+ if (ignoreAllNullabilities) "ignoreAllNullabilities",
+ if (ignoreTopLevelNullability) "ignoreTopLevelNullability",
+ ];
+
+ print("notEqual(${type1}, ${type2}"
+ "${flagNamesForDebug.map((f) => ", $f").join()})");
+ Expect.isFalse(
+ new DartTypeEquivalence(env.coreTypes,
+ equateTopTypes: equateTopTypes,
+ ignoreAllNullabilities: ignoreAllNullabilities,
+ ignoreTopLevelNullability: ignoreTopLevelNullability)
+ .areEqual(t1, t2),
+ "Expected '${type1}' and '${type2}' to be not equal "
+ "with flags ${flagNamesForDebug.map((f) => "'$f'").join(", ")}.");
+
+ print("notEqual(${type2}, ${type1}"
+ "${flagNamesForDebug.map((f) => ", $f").join()})");
+ Expect.isFalse(
+ new DartTypeEquivalence(env.coreTypes,
+ equateTopTypes: equateTopTypes,
+ ignoreAllNullabilities: ignoreAllNullabilities,
+ ignoreTopLevelNullability: ignoreTopLevelNullability)
+ .areEqual(t2, t1),
+ "Expected '${type2}' and '${type1}' to be not equal "
+ "with flags ${flagNamesForDebug.map((f) => "'$f'").join(", ")}.");
+}
+
+main() => run();