diff --git a/pkg/_fe_analyzer_shared/benchmark/exhaustiveness/large_fields_call_counts.dart b/pkg/_fe_analyzer_shared/benchmark/exhaustiveness/large_fields_call_counts.dart new file mode 100644 index 00000000000..ec1695f3b50 --- /dev/null +++ b/pkg/_fe_analyzer_shared/benchmark/exhaustiveness/large_fields_call_counts.dart @@ -0,0 +1,116 @@ +// Copyright (c) 2022, 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:_fe_analyzer_shared/src/exhaustiveness/exhaustive.dart'; +import 'package:_fe_analyzer_shared/src/exhaustiveness/profile.dart' as profile; +import 'package:_fe_analyzer_shared/src/exhaustiveness/space.dart'; +import 'package:_fe_analyzer_shared/src/exhaustiveness/static_type.dart'; + +import '../../test/exhaustiveness/utils.dart'; + +void main() { + profile.enabled = true; + + // (A) + // /|\ + // B C D + var a = StaticType('A', isSealed: true); + var b = StaticType('B', inherits: [a]); + var c = StaticType('C', inherits: [a]); + var d = StaticType('D', inherits: [a]); + var t = StaticType('T', fields: {'w': a, 'x': a, 'y': a, 'z': a}); + + expectExhaustiveOnlyAll(t, [ + {'w': b, 'x': b, 'y': b, 'z': b}, + {'w': b, 'x': b, 'y': b, 'z': c}, + {'w': b, 'x': b, 'y': b, 'z': d}, + {'w': b, 'x': b, 'y': c, 'z': b}, + {'w': b, 'x': b, 'y': c, 'z': c}, + {'w': b, 'x': b, 'y': c, 'z': d}, + {'w': b, 'x': b, 'y': d, 'z': b}, + {'w': b, 'x': b, 'y': d, 'z': c}, + {'w': b, 'x': b, 'y': d, 'z': d}, + {'w': b, 'x': c, 'y': b, 'z': b}, + {'w': b, 'x': c, 'y': b, 'z': c}, + {'w': b, 'x': c, 'y': b, 'z': d}, + {'w': b, 'x': c, 'y': c, 'z': b}, + {'w': b, 'x': c, 'y': c, 'z': c}, + {'w': b, 'x': c, 'y': c, 'z': d}, + {'w': b, 'x': c, 'y': d, 'z': b}, + {'w': b, 'x': c, 'y': d, 'z': c}, + {'w': b, 'x': c, 'y': d, 'z': d}, + {'w': b, 'x': d, 'y': b, 'z': b}, + {'w': b, 'x': d, 'y': b, 'z': c}, + {'w': b, 'x': d, 'y': b, 'z': d}, + {'w': b, 'x': d, 'y': c, 'z': b}, + {'w': b, 'x': d, 'y': c, 'z': c}, + {'w': b, 'x': d, 'y': c, 'z': d}, + {'w': b, 'x': d, 'y': d, 'z': b}, + {'w': b, 'x': d, 'y': d, 'z': c}, + {'w': b, 'x': d, 'y': d, 'z': d}, + {'w': c, 'x': b, 'y': b, 'z': b}, + {'w': c, 'x': b, 'y': b, 'z': c}, + {'w': c, 'x': b, 'y': b, 'z': d}, + {'w': c, 'x': b, 'y': c, 'z': b}, + {'w': c, 'x': b, 'y': c, 'z': c}, + {'w': c, 'x': b, 'y': c, 'z': d}, + {'w': c, 'x': b, 'y': d, 'z': b}, + {'w': c, 'x': b, 'y': d, 'z': c}, + {'w': c, 'x': b, 'y': d, 'z': d}, + {'w': c, 'x': c, 'y': b, 'z': b}, + {'w': c, 'x': c, 'y': b, 'z': c}, + {'w': c, 'x': c, 'y': b, 'z': d}, + {'w': c, 'x': c, 'y': c, 'z': b}, + {'w': c, 'x': c, 'y': c, 'z': c}, + {'w': c, 'x': c, 'y': c, 'z': d}, + {'w': c, 'x': c, 'y': d, 'z': b}, + {'w': c, 'x': c, 'y': d, 'z': c}, + {'w': c, 'x': c, 'y': d, 'z': d}, + {'w': c, 'x': d, 'y': b, 'z': b}, + {'w': c, 'x': d, 'y': b, 'z': c}, + {'w': c, 'x': d, 'y': b, 'z': d}, + {'w': c, 'x': d, 'y': c, 'z': b}, + {'w': c, 'x': d, 'y': c, 'z': c}, + {'w': c, 'x': d, 'y': c, 'z': d}, + {'w': c, 'x': d, 'y': d, 'z': b}, + {'w': c, 'x': d, 'y': d, 'z': c}, + {'w': c, 'x': d, 'y': d, 'z': d}, + {'w': d, 'x': b, 'y': b, 'z': b}, + {'w': d, 'x': b, 'y': b, 'z': c}, + {'w': d, 'x': b, 'y': b, 'z': d}, + {'w': d, 'x': b, 'y': c, 'z': b}, + {'w': d, 'x': b, 'y': c, 'z': c}, + {'w': d, 'x': b, 'y': c, 'z': d}, + {'w': d, 'x': b, 'y': d, 'z': b}, + {'w': d, 'x': b, 'y': d, 'z': c}, + {'w': d, 'x': b, 'y': d, 'z': d}, + {'w': d, 'x': c, 'y': b, 'z': b}, + {'w': d, 'x': c, 'y': b, 'z': c}, + {'w': d, 'x': c, 'y': b, 'z': d}, + {'w': d, 'x': c, 'y': c, 'z': b}, + {'w': d, 'x': c, 'y': c, 'z': c}, + {'w': d, 'x': c, 'y': c, 'z': d}, + {'w': d, 'x': c, 'y': d, 'z': b}, + {'w': d, 'x': c, 'y': d, 'z': c}, + {'w': d, 'x': c, 'y': d, 'z': d}, + {'w': d, 'x': d, 'y': b, 'z': b}, + {'w': d, 'x': d, 'y': b, 'z': c}, + {'w': d, 'x': d, 'y': b, 'z': d}, + {'w': d, 'x': d, 'y': c, 'z': b}, + {'w': d, 'x': d, 'y': c, 'z': c}, + {'w': d, 'x': d, 'y': c, 'z': d}, + {'w': d, 'x': d, 'y': d, 'z': b}, + {'w': d, 'x': d, 'y': d, 'z': c}, + {'w': d, 'x': d, 'y': d, 'z': d}, + ]); +} + +/// Test that [cases] are exhaustive over [type] if and only if all cases are +/// included and that all subsets of the cases are not exhaustive. +void expectExhaustiveOnlyAll(StaticType type, List cases) { + var spaces = parseSpaces(cases); + profile.reset(); + print(isExhaustive(Space(type), spaces)); + profile.log(); +} diff --git a/pkg/_fe_analyzer_shared/benchmark/exhaustiveness/large_fields_timed.dart b/pkg/_fe_analyzer_shared/benchmark/exhaustiveness/large_fields_timed.dart new file mode 100644 index 00000000000..ae2b28277eb --- /dev/null +++ b/pkg/_fe_analyzer_shared/benchmark/exhaustiveness/large_fields_timed.dart @@ -0,0 +1,128 @@ +// Copyright (c) 2022, 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 'dart:math'; + +import 'package:_fe_analyzer_shared/src/exhaustiveness/exhaustive.dart'; +import 'package:_fe_analyzer_shared/src/exhaustiveness/space.dart'; +import 'package:_fe_analyzer_shared/src/exhaustiveness/static_type.dart'; + +import '../../test/exhaustiveness/utils.dart'; + +void main() { + // (A) + // /|\ + // B C D + var a = StaticType('A', isSealed: true); + var b = StaticType('B', inherits: [a]); + var c = StaticType('C', inherits: [a]); + var d = StaticType('D', inherits: [a]); + var t = StaticType('T', fields: {'w': a, 'x': a, 'y': a, 'z': a}); + + expectExhaustiveOnlyAll(t, [ + {'w': b, 'x': b, 'y': b, 'z': b}, + {'w': b, 'x': b, 'y': b, 'z': c}, + {'w': b, 'x': b, 'y': b, 'z': d}, + {'w': b, 'x': b, 'y': c, 'z': b}, + {'w': b, 'x': b, 'y': c, 'z': c}, + {'w': b, 'x': b, 'y': c, 'z': d}, + {'w': b, 'x': b, 'y': d, 'z': b}, + {'w': b, 'x': b, 'y': d, 'z': c}, + {'w': b, 'x': b, 'y': d, 'z': d}, + {'w': b, 'x': c, 'y': b, 'z': b}, + {'w': b, 'x': c, 'y': b, 'z': c}, + {'w': b, 'x': c, 'y': b, 'z': d}, + {'w': b, 'x': c, 'y': c, 'z': b}, + {'w': b, 'x': c, 'y': c, 'z': c}, + {'w': b, 'x': c, 'y': c, 'z': d}, + {'w': b, 'x': c, 'y': d, 'z': b}, + {'w': b, 'x': c, 'y': d, 'z': c}, + {'w': b, 'x': c, 'y': d, 'z': d}, + {'w': b, 'x': d, 'y': b, 'z': b}, + {'w': b, 'x': d, 'y': b, 'z': c}, + {'w': b, 'x': d, 'y': b, 'z': d}, + {'w': b, 'x': d, 'y': c, 'z': b}, + {'w': b, 'x': d, 'y': c, 'z': c}, + {'w': b, 'x': d, 'y': c, 'z': d}, + {'w': b, 'x': d, 'y': d, 'z': b}, + {'w': b, 'x': d, 'y': d, 'z': c}, + {'w': b, 'x': d, 'y': d, 'z': d}, + {'w': c, 'x': b, 'y': b, 'z': b}, + {'w': c, 'x': b, 'y': b, 'z': c}, + {'w': c, 'x': b, 'y': b, 'z': d}, + {'w': c, 'x': b, 'y': c, 'z': b}, + {'w': c, 'x': b, 'y': c, 'z': c}, + {'w': c, 'x': b, 'y': c, 'z': d}, + {'w': c, 'x': b, 'y': d, 'z': b}, + {'w': c, 'x': b, 'y': d, 'z': c}, + {'w': c, 'x': b, 'y': d, 'z': d}, + {'w': c, 'x': c, 'y': b, 'z': b}, + {'w': c, 'x': c, 'y': b, 'z': c}, + {'w': c, 'x': c, 'y': b, 'z': d}, + {'w': c, 'x': c, 'y': c, 'z': b}, + {'w': c, 'x': c, 'y': c, 'z': c}, + {'w': c, 'x': c, 'y': c, 'z': d}, + {'w': c, 'x': c, 'y': d, 'z': b}, + {'w': c, 'x': c, 'y': d, 'z': c}, + {'w': c, 'x': c, 'y': d, 'z': d}, + {'w': c, 'x': d, 'y': b, 'z': b}, + {'w': c, 'x': d, 'y': b, 'z': c}, + {'w': c, 'x': d, 'y': b, 'z': d}, + {'w': c, 'x': d, 'y': c, 'z': b}, + {'w': c, 'x': d, 'y': c, 'z': c}, + {'w': c, 'x': d, 'y': c, 'z': d}, + {'w': c, 'x': d, 'y': d, 'z': b}, + {'w': c, 'x': d, 'y': d, 'z': c}, + {'w': c, 'x': d, 'y': d, 'z': d}, + {'w': d, 'x': b, 'y': b, 'z': b}, + {'w': d, 'x': b, 'y': b, 'z': c}, + {'w': d, 'x': b, 'y': b, 'z': d}, + {'w': d, 'x': b, 'y': c, 'z': b}, + {'w': d, 'x': b, 'y': c, 'z': c}, + {'w': d, 'x': b, 'y': c, 'z': d}, + {'w': d, 'x': b, 'y': d, 'z': b}, + {'w': d, 'x': b, 'y': d, 'z': c}, + {'w': d, 'x': b, 'y': d, 'z': d}, + {'w': d, 'x': c, 'y': b, 'z': b}, + {'w': d, 'x': c, 'y': b, 'z': c}, + {'w': d, 'x': c, 'y': b, 'z': d}, + {'w': d, 'x': c, 'y': c, 'z': b}, + {'w': d, 'x': c, 'y': c, 'z': c}, + {'w': d, 'x': c, 'y': c, 'z': d}, + {'w': d, 'x': c, 'y': d, 'z': b}, + {'w': d, 'x': c, 'y': d, 'z': c}, + {'w': d, 'x': c, 'y': d, 'z': d}, + {'w': d, 'x': d, 'y': b, 'z': b}, + {'w': d, 'x': d, 'y': b, 'z': c}, + {'w': d, 'x': d, 'y': b, 'z': d}, + {'w': d, 'x': d, 'y': c, 'z': b}, + {'w': d, 'x': d, 'y': c, 'z': c}, + {'w': d, 'x': d, 'y': c, 'z': d}, + {'w': d, 'x': d, 'y': d, 'z': b}, + {'w': d, 'x': d, 'y': d, 'z': c}, + {'w': d, 'x': d, 'y': d, 'z': d}, + ]); +} + +/// Test that [cases] are exhaustive over [type] if and only if all cases are +/// included and that all subsets of the cases are not exhaustive. +void expectExhaustiveOnlyAll(StaticType type, List cases) { + const trials = 100; + + var best = 9999999; + for (var j = 0; j < 100000; j++) { + var watch = Stopwatch()..start(); + for (var i = 0; i < trials; i++) { + var spaces = parseSpaces(cases); + var actual = isExhaustive(Space(type), spaces); + if (!actual) { + throw 'Expected exhaustive'; + } + } + + var elapsed = watch.elapsedMilliseconds; + best = min(elapsed, best); + print('${elapsed / trials}ms (best ${best / trials}ms)'); + } +} diff --git a/pkg/_fe_analyzer_shared/lib/src/exhaustiveness/equal.dart b/pkg/_fe_analyzer_shared/lib/src/exhaustiveness/equal.dart new file mode 100644 index 00000000000..63f527d9a2d --- /dev/null +++ b/pkg/_fe_analyzer_shared/lib/src/exhaustiveness/equal.dart @@ -0,0 +1,71 @@ +// Copyright (c) 2022, 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 'profile.dart' as profile; +import 'space.dart'; + +/// Returns `true` if [left] and [right] are equivalent spaces. +/// +/// Equality is defined purely structurally/syntactically. +bool equal(Space left, Space right, String reason) { + profile.count('equal', reason); + + if (identical(left, right)) return true; + + // Empty is only equal to itself (and will get caught by the previous check). + if (left == Space.empty) return false; + if (right == Space.empty) return false; + + if (left is UnionSpace && right is UnionSpace) { + return _equalUnions(left, right); + } + + if (left is ExtractSpace && right is ExtractSpace) { + return _equalExtracts(left, right); + } + + // If we get here, one is a union and one is an extract. + return false; +} + +/// Returns `true` if [left] and [right] have the same type and the same fields +/// with equal subspaces. +bool _equalExtracts(ExtractSpace left, ExtractSpace right) { + // Must have the same type. + if (left.type != right.type) return false; + + // And the same fields. + Set fields = {...left.fields.keys, ...right.fields.keys}; + if (left.fields.length != fields.length) return false; + if (right.fields.length != fields.length) return false; + + for (String field in fields) { + if (!equal(left.fields[field]!, right.fields[field]!, 'recurse extract')) { + return false; + } + } + + return true; +} + +/// Returns `true` if [left] and [right] contain equal arms in any order. +/// +/// Assumes that all duplicates have already been removed from each union. +bool _equalUnions(UnionSpace left, UnionSpace right) { + if (left.arms.length != right.arms.length) return false; + + /// For each left arm, should find an equal right arm. + for (Space leftArm in left.arms) { + bool found = false; + for (Space rightArm in right.arms) { + if (equal(leftArm, rightArm, 'recurse union')) { + found = true; + break; + } + } + if (!found) return false; + } + + return true; +} diff --git a/pkg/_fe_analyzer_shared/lib/src/exhaustiveness/exhaustive.dart b/pkg/_fe_analyzer_shared/lib/src/exhaustiveness/exhaustive.dart new file mode 100644 index 00000000000..17fbf9f9310 --- /dev/null +++ b/pkg/_fe_analyzer_shared/lib/src/exhaustiveness/exhaustive.dart @@ -0,0 +1,49 @@ +// Copyright (c) 2022, 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 'space.dart'; +import 'static_type.dart'; +import 'subtract.dart'; + +/// Returns `true` if [cases] exhaustively covers all possible values of +/// [value]. +/// +/// This is defined simply in terms of subtraction and unions: [cases] is a +/// union space, and it's exhaustive if subtracting it from [value] leaves +/// nothing. +bool isExhaustive(Space value, List cases) { + return subtract(value, new Space.union(cases)) == Space.empty; +} + +/// Checks the [cases] representing a series of switch cases to see if they +/// exhaustively cover all possible values of the matched [valueType]. Also +/// checks to see if any case can't be matched because it's covered by previous +/// cases. +/// +/// Returns a string containing any unreachable case or non-exhaustive match +/// errors. Returns an empty string if all cases are reachable and the cases +/// are exhaustive. +String reportErrors(StaticType valueType, List cases) { + List errors = []; + + Space remaining = new Space(valueType); + for (int i = 0; i < cases.length; i++) { + // See if this case is covered by previous ones. + if (i > 0) { + Space previous = new Space.union(cases.sublist(0, i)); + if (subtract(cases[i], previous) == Space.empty) { + errors.add('Case #${i + 1} ${cases[i]} is covered by $previous.'); + } + } + + remaining = subtract(remaining, cases[i]); + } + + if (remaining != Space.empty) { + errors.add( + '$valueType is not exhaustively matched by ${new Space.union(cases)}.'); + } + + return errors.join('\n'); +} diff --git a/pkg/_fe_analyzer_shared/lib/src/exhaustiveness/intersect.dart b/pkg/_fe_analyzer_shared/lib/src/exhaustiveness/intersect.dart new file mode 100644 index 00000000000..e710c609736 --- /dev/null +++ b/pkg/_fe_analyzer_shared/lib/src/exhaustiveness/intersect.dart @@ -0,0 +1,107 @@ +// Copyright (c) 2022, 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 'profile.dart' as profile; +import 'space.dart'; +import 'static_type.dart'; + +/// Calculates the intersection of [left] and [right]. +/// +/// This is used to tell if two field spaces on a pair of spaces being +/// subtracted have no common values. +Space intersect(Space left, Space right) { + profile.count('intersect'); + + // The intersection with an empty space is always empty. + if (left == Space.empty) return Space.empty; + if (right == Space.empty) return Space.empty; + + // The intersection of a union is the union of the intersections of its arms. + if (left is UnionSpace) { + return new Space.union( + left.arms.map((arm) => intersect(arm, right)).toList()); + } + + if (right is UnionSpace) { + return new Space.union( + right.arms.map((arm) => intersect(left, arm)).toList()); + } + + // Otherwise, we're intersecting two [ExtractSpaces]. + return _intersectExtracts(left as ExtractSpace, right as ExtractSpace); +} + +/// Returns the intersection of two static types [left] and [right]. +/// +/// Returns `null` if the intersection is empty. +StaticType? intersectTypes(StaticType left, StaticType right) { + // If one type is a subtype, the subtype is the intersection. + if (left.isSubtypeOf(right)) return left; + if (right.isSubtypeOf(left)) return right; + + if (left.isNullable) { + if (right.isNullable) { + StaticType? intersection = + intersectTypes(left.underlying, right.underlying); + if (intersection == null) return null; + return intersection.nullable; + } else { + return intersectTypes(left.underlying, right); + } + } else if (right.isNullable) { + return intersectTypes(left, right.underlying); + } + + // If we allow sealed types to share subtypes, then this will need to be more + // sophisticated. Here: + // + // (A) (B) + // / \ / \ + // C D E + // + // The intersection of A and B should be D. Here: + // + // (A) (B) + // | \ / | + // |\ \/ /| + // | \/\/ | + // C D E F + // + // It should be D, E. + + // Unrelated types. + return null; +} + +/// Returns the interaction of extract spaces [left] and [right]. +Space _intersectExtracts(ExtractSpace left, ExtractSpace right) { + StaticType? type = intersectTypes(left.type, right.type); + + // If the types are disjoint, the intersection is empty. + if (type == null) return Space.empty; + + // Recursively intersect the fields. + List fieldNames = + {...left.fields.keys, ...right.fields.keys}.toList(); + + // Sorting isn't needed for correctness, just to make the tests less brittle. + fieldNames.sort(); + + Map fields = {}; + for (String name in fieldNames) { + Space field = _intersectFields(left.fields[name], right.fields[name]); + + // If the fields are disjoint, then the entire space will have no values. + if (field == Space.empty) return Space.empty; + fields[name] = field; + } + + return new Space(type, fields); +} + +Space _intersectFields(Space? left, Space? right) { + if (left == null) return right!; + if (right == null) return left; + return intersect(left, right); +} diff --git a/pkg/_fe_analyzer_shared/lib/src/exhaustiveness/intersect_empty.dart b/pkg/_fe_analyzer_shared/lib/src/exhaustiveness/intersect_empty.dart new file mode 100644 index 00000000000..2baa029fb4f --- /dev/null +++ b/pkg/_fe_analyzer_shared/lib/src/exhaustiveness/intersect_empty.dart @@ -0,0 +1,60 @@ +// Copyright (c) 2022, 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 'space.dart'; +import 'static_type.dart'; + +/// Calculates whether the intersection of [left] and [right] is empty. +/// +/// This is used to tell if two field spaces on a pair of spaces being +/// subtracted have no common values. +bool intersectEmpty(Space left, Space right) { + // The intersection with an empty space is always empty. + if (left == Space.empty) return true; + if (right == Space.empty) return true; + + // The intersection of a union is empty if all of the arms are. + if (left is UnionSpace) { + return left.arms.every((arm) => intersectEmpty(arm, right)); + } + + if (right is UnionSpace) { + return right.arms.every((arm) => intersectEmpty(left, arm)); + } + + // Otherwise, we're intersecting two [ExtractSpaces]. + return _intersectExtracts(left as ExtractSpace, right as ExtractSpace); +} + +/// Returns true if the intersection of two static types [left] and [right] is +/// empty. +bool intersectTypes(StaticType left, StaticType right) { + // If one type is a subtype, the subtype is the intersection. + if (left.isSubtypeOf(right)) return false; + if (right.isSubtypeOf(left)) return false; + + // Unrelated types. + return true; +} + +/// Returns the interaction of extract spaces [left] and [right]. +bool _intersectExtracts(ExtractSpace left, ExtractSpace right) { + if (intersectTypes(left.type, right.type)) return true; + + // Recursively intersect the fields. + List fieldNames = + {...left.fields.keys, ...right.fields.keys}.toList(); + for (String name in fieldNames) { + // If the fields are disjoint, then the entire space will have no values. + if (_intersectFields(left.fields[name], right.fields[name])) return true; + } + + return false; +} + +bool _intersectFields(Space? left, Space? right) { + if (left == null) return right! == Space.empty; + if (right == null) return left == Space.empty; + return intersectEmpty(left, right); +} diff --git a/pkg/_fe_analyzer_shared/lib/src/exhaustiveness/profile.dart b/pkg/_fe_analyzer_shared/lib/src/exhaustiveness/profile.dart new file mode 100644 index 00000000000..694b568e2a9 --- /dev/null +++ b/pkg/_fe_analyzer_shared/lib/src/exhaustiveness/profile.dart @@ -0,0 +1,47 @@ +// Copyright (c) 2022, 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 'dart:math'; + +bool enabled = false; + +final _counts = {}; + +void count(String name, [String? subname]) { + if (!enabled) return; + _counts.putIfAbsent(name, () => 0); + _counts[name] = _counts[name]! + 1; + + if (subname != null) { + count('$name/$subname'); + } +} + +void log() { + List names = _counts.keys.toList(); + names.sort(); + int nameLength = + names.fold(0, (length, name) => max(length, name.length)); + int countLength = _counts.values + .fold(0, (length, count) => max(length, count.toString().length)); + + for (String name in names) { + print('${name.padRight(nameLength)} = ' + '${_counts[name].toString().padLeft(countLength)}'); + } +} + +void reset() { + _counts.clear(); +} + +void run(void Function() callback) { + reset(); + try { + callback(); + } finally { + log(); + reset(); + } +} diff --git a/pkg/_fe_analyzer_shared/lib/src/exhaustiveness/space.dart b/pkg/_fe_analyzer_shared/lib/src/exhaustiveness/space.dart new file mode 100644 index 00000000000..fe146569eec --- /dev/null +++ b/pkg/_fe_analyzer_shared/lib/src/exhaustiveness/space.dart @@ -0,0 +1,150 @@ +// Copyright (c) 2022, 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 'equal.dart'; +import 'static_type.dart'; + +/// The main space for matching types and destructuring. +/// +/// It has a type which determines the type of values it contains. The type may +/// be [StaticType.top] to indicate that it doesn't filter by type. +/// +/// It may also contain zero or more named fields. The space then only contains +/// values where the field values are contained by the corresponding field +/// spaces. +class ExtractSpace extends Space { + /// The type of values the space matches. + final StaticType type; + + /// Any field subspaces the space matches. + final Map fields; + + ExtractSpace._(this.type, [this.fields = const {}]) : super._(); + + /// An [ExtractSpace] with no type and no fields contains all values. + @override + bool get isTop => type == StaticType.top && fields.isEmpty; + + @override + String toString() { + if (isTop) return '()'; + + // If there are no fields, just show the type. + if (fields.isEmpty) return type.name; + + StringBuffer buffer = new StringBuffer(); + + // We model a bare record pattern by treating it like an extractor on top. + if (type != StaticType.top) buffer.write(type.name); + + buffer.write('('); + bool first = true; + + // Positional fields have stringified number names. + for (int i = 0;; i++) { + Space? pattern = fields[i.toString()]; + if (pattern == null) break; + + if (!first) buffer.write(', '); + buffer.write(pattern); + first = false; + } + + fields.forEach((name, pattern) { + // Skip positional fields. + if (int.tryParse(name) != null) return; + + if (!first) buffer.write(', '); + buffer.write('$name: $pattern'); + first = false; + }); + + buffer.write(')'); + + return buffer.toString(); + } +} + +// TODO(paulberry, rnystrom): List spaces. + +abstract class Space { + static final _EmptySpace empty = new _EmptySpace._(); + static final Space top = new Space(StaticType.top); + + factory Space(StaticType type, [Map fields = const {}]) => + new ExtractSpace._(type, fields); + + factory Space.record([Map fields = const {}]) => + new Space(StaticType.top, fields); + + factory Space.union(List arms) { + // Simplify the arms if possible. + List allArms = []; + + void addSpace(Space space) { + // Discard duplicate arms. Duplicates can appear when working through a + // series of cases that destructure multiple fields with different types. + // Discarding the duplicates isn't necessary for correctness (a union with + // redundant arms contains the same set of values), but improves + // performance greatly. In the "sealed subtypes large T with all cases" + // test, you end up with a union containing 2520 arms, 2488 are + // duplicates. With this check, the largest union has only 5 arms. + // + // This is O(n^2) since we define only equality on spaces, but a real + // implementation would likely define hash code too and then simply + // create a hash set to merge duplicates in O(n) time. + for (Space existing in allArms) { + if (equal(existing, space, 'dedupe union')) return; + } + + allArms.add(space); + } + + for (Space space in arms) { + // Discard empty arms. + if (space == empty) continue; + + // Flatten unions. We don't need to flatten recursively since we always + // go through this constructor to create unions. A UnionSpace will never + // contain UnionSpaces. + if (space is UnionSpace) { + for (Space arm in space.arms) { + addSpace(arm); + } + } else { + addSpace(space); + } + } + + if (allArms.isEmpty) return empty; + if (allArms.length == 1) return allArms.first; + return new UnionSpace._(allArms); + } + + Space._(); + + /// An untyped record space with no fields matches all values and thus isn't + /// very useful. + bool get isTop => false; +} + +/// A union of spaces. The space A|B contains all of the values of A and B. +class UnionSpace extends Space { + final List arms; + + UnionSpace._(this.arms) : super._() { + assert(arms.length > 1); + } + + @override + String toString() => arms.join('|'); +} + +/// The uninhabited space. +class _EmptySpace extends Space { + _EmptySpace._() : super._(); + + @override + String toString() => '∅'; +} diff --git a/pkg/_fe_analyzer_shared/lib/src/exhaustiveness/static_type.dart b/pkg/_fe_analyzer_shared/lib/src/exhaustiveness/static_type.dart new file mode 100644 index 00000000000..647e36d0881 --- /dev/null +++ b/pkg/_fe_analyzer_shared/lib/src/exhaustiveness/static_type.dart @@ -0,0 +1,139 @@ +// Copyright (c) 2022, 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. + +// TODO(paulberry,rnystrom): Generics. + +/// A static type in the type system. +class StaticType { + /// Built-in top type that all types are a subtype of. + static final StaticType top = new StaticType('top', inherits: []); + + static final StaticType nullType = new StaticType('Null'); + + final String name; + + late final StaticType nullable = new StaticType._nullable(this); + + /// If this type is a nullable type, then this is the underlying type. + /// + /// Otherwise `null`. + final StaticType? _underlying; + + /// Whether this type is sealed. A sealed type is implicitly abstract and has + /// a closed set of known subtypes. This means that every instance of the + /// type must be an instance of one of those subtypes. Conversely, if an + /// instance is *not* an instance of one of those subtypes, that it must not + /// be an instance of this type. + /// + /// Note that subtypes of a sealed type do not themselves have to be sealed. + /// Consider: + /// + /// (A) + /// / \ + /// B C + /// + /// Here, A is sealed and B and C are not. There may be many unknown + /// subclasses of B and C, or classes implementing their interfaces. That + /// doesn't interfere with exhaustiveness checking because it's still the + /// case that any instance of A must be either a B or C *or some subtype of + /// one of those two types*. + final bool isSealed; + + final Map _fields; + + final List _supertypes = []; + + final List _subtypes = []; + + StaticType(this.name, + {this.isSealed = false, + List? inherits, + Map fields = const {}}) + : _underlying = null, + _fields = fields { + if (inherits != null) { + for (StaticType type in inherits) { + _supertypes.add(type); + type._subtypes.add(this); + } + } else { + _supertypes.add(top); + } + + int sealed = 0; + for (StaticType supertype in _supertypes) { + if (supertype.isSealed) sealed++; + } + + // We don't allow a sealed type's subtypes to be shared with some other + // sibling supertype, as in D here: + // + // (A) (B) + // / \ / \ + // C D E + // + // We could remove this restriction but doing so will require + // expandTypes() to be more complex. In the example here, if we subtract + // E from A, the result should be C|D. That requires knowing that B should + // be expanded, which expandTypes() doesn't currently handle. + if (sealed > 1) { + throw new ArgumentError('Can only have one sealed supertype.'); + } + } + + StaticType._nullable(StaticType underlying) + : name = '${underlying.name}?', + _underlying = underlying, + isSealed = true, + // No fields because it may match null which doesn't have them. + _fields = {} {} + + /// The static types of the fields this type exposes for record destructuring. + /// + /// Includes inherited fields. + Map get fields { + return { + for (StaticType supertype in _supertypes) ...supertype.fields, + ..._fields + }; + } + + bool get isNullable => _underlying != null; + + /// The immediate subtypes of this type. + Iterable get subtypes => _subtypes; + + /// The underlying type of this nullable type. It's an error to call this on + /// a non-nullable type. + StaticType get underlying => _underlying!; + + bool isSubtypeOf(StaticType other) { + if (this == other) return true; + + // Null is a subtype of all nullable types. + if (this == nullType && other._underlying != null) return true; + + // A nullable type is a subtype if the underlying type and Null both are. + StaticType? underlying = _underlying; + if (underlying != null) { + return underlying.isSubtypeOf(other) && nullType.isSubtypeOf(other); + } + + // A non-nullable type is a subtype of the underlying type of a nullable + // type. + StaticType? otherUnderlying = other._underlying; + if (otherUnderlying != null) { + return isSubtypeOf(otherUnderlying); + } + + for (StaticType supertype in _supertypes) { + if (supertype.isSubtypeOf(other)) return true; + } + + return false; + } + + @override + String toString() => name; +} diff --git a/pkg/_fe_analyzer_shared/lib/src/exhaustiveness/subtract.dart b/pkg/_fe_analyzer_shared/lib/src/exhaustiveness/subtract.dart new file mode 100644 index 00000000000..2da21576cf9 --- /dev/null +++ b/pkg/_fe_analyzer_shared/lib/src/exhaustiveness/subtract.dart @@ -0,0 +1,182 @@ +// Copyright (c) 2022, 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 'intersect_empty.dart'; +import 'profile.dart' as profile; +import 'space.dart'; +import 'static_type.dart'; + +/// Recursively replaces [left] with a union of its sealed subtypes as long as +/// doing so enables it to more precisely match against [right]. +List expandType(StaticType left, StaticType right) { + // If [left] is nullable and right is null or non-nullable, then expand the + // nullable type. + if (left.isNullable && (right == StaticType.nullType || !right.isNullable)) { + return [...expandType(left.underlying, right), StaticType.nullType]; + } + + // If [right] is nullable, then expand using its underlying type. + if (right.isNullable) { + return expandType(left, right.underlying); + } + + // If [left] is a sealed supertype and [right] is in its subtype hierarchy, + // then expand out the subtypes (recursively) to more precisely match [right]. + if (left.isSealed && left != right && right.isSubtypeOf(left)) { + return { + for (StaticType subtype in left.subtypes) ...expandType(subtype, right), + }.toList(); + } + + return [left]; +} + +/// Returns a new [Space] that contains all of the values of [left] that are +/// not also in [right]. +Space subtract(Space left, Space right) { + profile.count('subtract'); + + // Subtracting from empty is still empty. + if (left == Space.empty) return Space.empty; + + // Subtracting nothing leaves it unchanged. + if (right == Space.empty) return left; + + // Distribute a union on the left. + // A|B - x => A-x | B-x + if (left is UnionSpace) { + return new Space.union( + left.arms.map((arm) => subtract(arm, right)).toList()); + } + + // Distribute a union on the right. + // x - A|B => x - A - B + if (right is UnionSpace) { + Space result = left; + for (Space arm in right.arms) { + result = subtract(result, arm); + } + return result; + } + + // Otherwise, it must be two extract spaces. + return _subtractExtract(left as ExtractSpace, right as ExtractSpace); +} + +/// Returns `true` if every field in [leftFields] is covered by the +/// corresponding field in [rightFields]. +bool _isLeftSubspace(StaticType leftType, List fieldNames, + Map leftFields, Map rightFields) { + for (String name in fieldNames) { + if (subtract(leftFields[name]!, rightFields[name]!) != Space.empty) { + return false; + } + } + + // If we get here, every field covered. + return true; +} + +/// Subtract [right] from [left]. +Space _subtractExtract(ExtractSpace left, ExtractSpace right) { + List fieldNames = + {...left.fields.keys, ...right.fields.keys}.toList(); + + List spaces = []; + + // If the left type is in a sealed hierarchy, expanding it to its subtypes + // might let us calculate the subtraction more precisely. + List subtypes = expandType(left.type, right.type); + for (StaticType subtype in subtypes) { + spaces.addAll(_subtractExtractAtType(subtype, left, right, fieldNames)); + } + + return new Space.union(spaces); +} + +/// Subtract [right] from [left], but using [type] for left's type, which may +/// be a more specific subtype of [left]'s own type is a sealed supertype. +List _subtractExtractAtType(StaticType type, ExtractSpace left, + ExtractSpace right, List fieldNames) { + // If the right type doesn't cover the left (even after expanding sealed + // types), then we can't do anything with the fields since they may not + // even come into play for all values. Subtract nothing from this subtype + // and keep all of the current fields. + if (!type.isSubtypeOf(right.type)) return [new Space(type, left.fields)]; + + // Infer any fields that appear in one space and not the other. + Map leftFields = {}; + Map rightFields = {}; + for (String name in fieldNames) { + // If the right space matches on a field that the left doesn't have, infer + // it from the static type of the field. That contains the same set of + // values as having no field at all. + leftFields[name] = left.fields[name] ?? new Space(type.fields[name]!); + + // If the left matches on a field that the right doesn't have, infer top + // for the right field since the right will accept any of left's values for + // that field. + rightFields[name] = right.fields[name] ?? Space.top; + } + + // If any pair of fields have no overlapping values, then no overall value + // that matches the left space will also match the right space. So the right + // space doesn't subtract anything and we keep the left space as-is. + for (String name in fieldNames) { + if (intersectEmpty(leftFields[name]!, rightFields[name]!)) { + return [new Space(type, left.fields)]; + } + } + + // If all the right's fields strictly cover all of the left's, then the + // right completely subtracts this type and nothing remains. + if (_isLeftSubspace(type, fieldNames, leftFields, rightFields)) { + return const []; + } + + // The right side is a supertype but its fields don't totally cover, so + // handle each of them individually. + + // Walk the fields and see which ones are modified by the right-hand fields. + Map fixed = {}; + Map changedDifference = {}; + for (String name in fieldNames) { + Space difference = subtract(leftFields[name]!, rightFields[name]!); + if (difference == Space.empty) { + // The right field accepts all the values that the left field accepts, so + // keep the left field as it is. + fixed[name] = leftFields[name]!; + } else if (difference.isTop) { + // If the resulting field matches everything, simply discard it since + // it's equivalent to omitting the field. + } else { + changedDifference[name] = difference; + } + } + + // If no fields are affected by the subtraction, just return a single arm + // with all of the fields. + if (changedDifference.isEmpty) return [new Space(type, fixed)]; + + // For each field whose `left - right` is different, include an arm that + // includes that one difference. + List changedFields = changedDifference.keys.toList(); + List spaces = []; + for (int i = 0; i < changedFields.length; i++) { + Map fields = {...fixed}; + + for (int j = 0; j < changedFields.length; j++) { + String name = changedFields[j]; + if (i == j) { + fields[name] = changedDifference[name]!; + } else { + fields[name] = leftFields[name]!; + } + } + + spaces.add(new Space(type, fields)); + } + + return spaces; +} diff --git a/pkg/_fe_analyzer_shared/test/exhaustiveness/expand_type_test.dart b/pkg/_fe_analyzer_shared/test/exhaustiveness/expand_type_test.dart new file mode 100644 index 00000000000..0b0adb793de --- /dev/null +++ b/pkg/_fe_analyzer_shared/test/exhaustiveness/expand_type_test.dart @@ -0,0 +1,232 @@ +// Copyright (c) 2022, 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:_fe_analyzer_shared/src/exhaustiveness/static_type.dart'; +import 'package:_fe_analyzer_shared/src/exhaustiveness/subtract.dart'; +import 'package:test/test.dart'; + +void main() { + test('sealed', () { + // (A) + // /|\ + // B C(D) + // / \ + // E F + var a = StaticType('A', isSealed: true); + var b = StaticType('B', inherits: [a]); + var c = StaticType('C', inherits: [a]); + var d = StaticType('D', isSealed: true, inherits: [a]); + var e = StaticType('E', inherits: [d]); + var f = StaticType('F', inherits: [d]); + + expectExpand(a, a, 'A'); + expectExpand(a, b, 'B|C|D'); + expectExpand(a, c, 'B|C|D'); + expectExpand(a, d, 'B|C|D'); + expectExpand(a, e, 'B|C|E|F'); + expectExpand(a, f, 'B|C|E|F'); + + expectExpand(d, a, 'D'); + expectExpand(d, b, 'D'); + expectExpand(d, c, 'D'); + expectExpand(d, d, 'D'); + expectExpand(d, e, 'E|F'); + expectExpand(d, f, 'E|F'); + }); + + test('unsealed', () { + // A + // /|\ + // B C D + // / \ + // E F + var a = StaticType('A'); + var b = StaticType('B', inherits: [a]); + var c = StaticType('C', inherits: [a]); + var d = StaticType('D', inherits: [a]); + var e = StaticType('E', inherits: [d]); + var f = StaticType('F', inherits: [d]); + + expectExpand(a, a, 'A'); + expectExpand(a, b, 'A'); + expectExpand(a, c, 'A'); + expectExpand(a, d, 'A'); + expectExpand(a, e, 'A'); + expectExpand(a, f, 'A'); + + expectExpand(d, a, 'D'); + expectExpand(d, b, 'D'); + expectExpand(d, c, 'D'); + expectExpand(d, d, 'D'); + expectExpand(d, e, 'D'); + expectExpand(d, f, 'D'); + }); + + test('unsealed in middle', () { + // (A) + // / \ + // B C + // / \ + // D (E) + // / \ + // F G + var a = StaticType('A', isSealed: true); + var b = StaticType('B', inherits: [a]); + var c = StaticType('C', inherits: [a]); + var d = StaticType('D', inherits: [c]); + var e = StaticType('E', isSealed: true, inherits: [c]); + var f = StaticType('F', inherits: [e]); + var g = StaticType('G', inherits: [e]); + + expectExpand(a, a, 'A'); + expectExpand(a, b, 'B|C'); + expectExpand(a, c, 'B|C'); + expectExpand(a, d, 'B|C'); + expectExpand(a, e, 'B|C'); + expectExpand(a, f, 'B|C'); + + expectExpand(c, a, 'C'); + expectExpand(c, b, 'C'); + expectExpand(c, c, 'C'); + expectExpand(c, d, 'C'); + expectExpand(c, e, 'C'); + expectExpand(c, f, 'C'); + + expectExpand(e, a, 'E'); + expectExpand(e, b, 'E'); + expectExpand(e, c, 'E'); + expectExpand(e, e, 'E'); + expectExpand(e, f, 'F|G'); + expectExpand(e, g, 'F|G'); + }); + + test('transitive sealed family', () { + // (A) + // / \ + // (B) (C) + // / | | \ + // D E F G + // \ / + // H + var a = StaticType('A', isSealed: true); + var b = StaticType('B', isSealed: true, inherits: [a]); + var c = StaticType('C', isSealed: true, inherits: [a]); + var d = StaticType('D', inherits: [b]); + var e = StaticType('E', inherits: [b]); + var f = StaticType('F', inherits: [c]); + var g = StaticType('G', inherits: [c]); + var h = StaticType('H', inherits: [e, f]); + + expectExpand(a, a, 'A'); + expectExpand(a, b, 'B|C'); + expectExpand(a, c, 'B|C'); + expectExpand(a, d, 'D|E|C'); + expectExpand(a, e, 'D|E|C'); + expectExpand(a, f, 'B|F|G'); + expectExpand(a, g, 'B|F|G'); + expectExpand(a, h, 'D|E|F|G'); + + expectExpand(b, a, 'B'); + expectExpand(b, b, 'B'); + expectExpand(b, c, 'B'); + expectExpand(b, d, 'D|E'); + expectExpand(b, e, 'D|E'); + expectExpand(b, f, 'B'); + expectExpand(b, h, 'D|E'); + + expectExpand(d, a, 'D'); + expectExpand(d, b, 'D'); + expectExpand(d, c, 'D'); + expectExpand(d, d, 'D'); + expectExpand(d, e, 'D'); + expectExpand(d, f, 'D'); + + expectExpand(e, a, 'E'); + expectExpand(e, b, 'E'); + expectExpand(e, c, 'E'); + expectExpand(e, d, 'E'); + expectExpand(e, e, 'E'); + expectExpand(e, f, 'E'); + expectExpand(e, h, 'E'); + + expectExpand(h, a, 'H'); + expectExpand(h, b, 'H'); + expectExpand(h, d, 'H'); + expectExpand(h, e, 'H'); + expectExpand(h, h, 'H'); + }); + + test('sealed with multiple paths', () { + // (A) + // / \ + // (B) C + // / \ / + // D E + var a = StaticType('A', isSealed: true); + var b = StaticType('B', isSealed: true, inherits: [a]); + var c = StaticType('C', inherits: [a]); + var d = StaticType('D', inherits: [b]); + var e = StaticType('E', inherits: [b, c]); + + expectExpand(a, a, 'A'); + expectExpand(a, b, 'B|C'); + expectExpand(a, c, 'B|C'); + expectExpand(a, d, 'D|E|C'); + expectExpand(a, e, 'D|E|C'); + + expectExpand(b, a, 'B'); + expectExpand(b, b, 'B'); + expectExpand(b, c, 'B'); + expectExpand(b, d, 'D|E'); + expectExpand(b, e, 'D|E'); + + expectExpand(c, a, 'C'); + expectExpand(c, b, 'C'); + expectExpand(c, c, 'C'); + expectExpand(c, d, 'C'); + expectExpand(c, e, 'C'); + + expectExpand(d, a, 'D'); + expectExpand(d, b, 'D'); + expectExpand(d, c, 'D'); + expectExpand(d, d, 'D'); + expectExpand(d, e, 'D'); + + expectExpand(e, a, 'E'); + expectExpand(e, b, 'E'); + expectExpand(e, c, 'E'); + expectExpand(e, d, 'E'); + expectExpand(e, e, 'E'); + }); + + test('nullable', () { + // (A) + // / \ + // B C + // / \ + // D E + var a = StaticType('A', isSealed: true); + var b = StaticType('B', inherits: [a]); + var c = StaticType('C', inherits: [a]); + var d = StaticType('D', inherits: [c]); + + expectExpand(a.nullable, a, 'A|Null'); + expectExpand(a, a.nullable, 'A'); + expectExpand(a.nullable, a.nullable, 'A|Null'); + + // Sealed subtype. + expectExpand(a.nullable, b, 'B|C|Null'); + expectExpand(a, b.nullable, 'B|C'); + expectExpand(a.nullable, b.nullable, 'B|C|Null'); + + // Unsealed subtype. + expectExpand(c.nullable, d, 'C|Null'); + expectExpand(c, d.nullable, 'C'); + expectExpand(c.nullable, d.nullable, 'C|Null'); + }); +} + +void expectExpand(StaticType left, StaticType right, String expected) { + expect(expandType(left, right).join('|'), expected); +} diff --git a/pkg/_fe_analyzer_shared/test/exhaustiveness/intersect_empty_test.dart b/pkg/_fe_analyzer_shared/test/exhaustiveness/intersect_empty_test.dart new file mode 100644 index 00000000000..d25df88e035 --- /dev/null +++ b/pkg/_fe_analyzer_shared/test/exhaustiveness/intersect_empty_test.dart @@ -0,0 +1,77 @@ +// Copyright (c) 2022, 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:_fe_analyzer_shared/src/exhaustiveness/intersect_empty.dart'; +import 'package:_fe_analyzer_shared/src/exhaustiveness/space.dart'; +import 'package:_fe_analyzer_shared/src/exhaustiveness/static_type.dart'; +import 'package:test/test.dart'; + +import 'utils.dart'; + +void main() { + // (A) + // / \ + // B C + var a = StaticType('A', isSealed: true); + var b = StaticType('B', inherits: [a]); + var c = StaticType('C', inherits: [a]); + + Space A({StaticType? x, StaticType? y, StaticType? z}) => ty( + a, {if (x != null) 'x': x, if (y != null) 'y': y, if (z != null) 'z': z}); + Space B({StaticType? x, StaticType? y, StaticType? z}) => ty( + b, {if (x != null) 'x': x, if (y != null) 'y': y, if (z != null) 'z': z}); + Space C({StaticType? x, StaticType? y, StaticType? z}) => ty( + c, {if (x != null) 'x': x, if (y != null) 'y': y, if (z != null) 'z': z}); + + test('records', () { + expectIntersectEmpty(rec(x: a, y: a), rec(x: a, y: a), isFalse); + expectIntersectEmpty(rec(x: a, y: a), rec(x: a), isFalse); + expectIntersectEmpty(rec(w: a, x: a), rec(y: a, z: a), isFalse); + expectIntersectEmpty(rec(w: a, x: a, y: a), rec(x: a, y: a, z: a), isFalse); + }); + + test('types', () { + // Note: More comprehensive tests under intersect_types_test.dart. + expectIntersectEmpty(a, a, isFalse); + expectIntersectEmpty(a, b, isFalse); + expectIntersectEmpty(a, c, isFalse); + expectIntersectEmpty(b, c, isTrue); + }); + + test('field types', () { + expectIntersectEmpty(rec(x: a, y: b), rec(x: b, y: a), isFalse); + expectIntersectEmpty(rec(x: b), rec(x: c), isTrue); + }); + + test('types and fields', () { + expectIntersectEmpty(A(x: a), A(x: a), isFalse); + expectIntersectEmpty(A(x: a), A(x: b), isFalse); + expectIntersectEmpty(A(x: b), A(x: c), isTrue); + + expectIntersectEmpty(A(x: a), B(x: a), isFalse); + expectIntersectEmpty(A(x: a), B(x: b), isFalse); + expectIntersectEmpty(A(x: b), B(x: c), isTrue); + + expectIntersectEmpty(B(x: a), A(x: a), isFalse); + expectIntersectEmpty(B(x: a), A(x: b), isFalse); + expectIntersectEmpty(B(x: b), A(x: c), isTrue); + + expectIntersectEmpty(B(x: a), B(x: a), isFalse); + expectIntersectEmpty(B(x: a), B(x: b), isFalse); + expectIntersectEmpty(B(x: b), B(x: c), isTrue); + + expectIntersectEmpty(B(x: a), C(x: a), isTrue); + expectIntersectEmpty(B(x: a), C(x: b), isTrue); + expectIntersectEmpty(B(x: b), C(x: c), isTrue); + }); +} + +void expectIntersectEmpty(Object left, Object right, Matcher expected) { + var leftSpace = parseSpace(left); + var rightSpace = parseSpace(right); + + // Intersection is symmetric so try both directions. + expect(intersectEmpty(leftSpace, rightSpace), expected); + expect(intersectEmpty(rightSpace, leftSpace), expected); +} diff --git a/pkg/_fe_analyzer_shared/test/exhaustiveness/intersect_test.dart b/pkg/_fe_analyzer_shared/test/exhaustiveness/intersect_test.dart new file mode 100644 index 00000000000..f90fa5a65aa --- /dev/null +++ b/pkg/_fe_analyzer_shared/test/exhaustiveness/intersect_test.dart @@ -0,0 +1,80 @@ +// Copyright (c) 2022, 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:_fe_analyzer_shared/src/exhaustiveness/intersect.dart'; +import 'package:_fe_analyzer_shared/src/exhaustiveness/space.dart'; +import 'package:_fe_analyzer_shared/src/exhaustiveness/static_type.dart'; +import 'package:test/test.dart'; + +import 'utils.dart'; + +void main() { + // (A) + // / \ + // B C + var a = StaticType('A', isSealed: true); + var b = StaticType('B', inherits: [a]); + var c = StaticType('C', inherits: [a]); + + Space A({StaticType? x, StaticType? y, StaticType? z}) => ty( + a, {if (x != null) 'x': x, if (y != null) 'y': y, if (z != null) 'z': z}); + Space B({StaticType? x, StaticType? y, StaticType? z}) => ty( + b, {if (x != null) 'x': x, if (y != null) 'y': y, if (z != null) 'z': z}); + Space C({StaticType? x, StaticType? y, StaticType? z}) => ty( + c, {if (x != null) 'x': x, if (y != null) 'y': y, if (z != null) 'z': z}); + + test('records', () { + expectIntersect(rec(x: a, y: a), rec(x: a, y: a), rec(x: a, y: a)); + expectIntersect(rec(x: a, y: a), rec(x: a), rec(x: a, y: a)); + expectIntersect( + rec(w: a, x: a), rec(y: a, z: a), rec(w: a, x: a, y: a, z: a)); + expectIntersect(rec(w: a, x: a, y: a), rec(x: a, y: a, z: a), + rec(w: a, x: a, y: a, z: a)); + }); + + test('types', () { + // Note: More comprehensive tests under intersect_types_test.dart. + expectIntersect(a, a, a); + expectIntersect(a, b, b); + expectIntersect(a, c, c); + expectIntersect(b, c, '∅'); + }); + + test('field types', () { + expectIntersect(rec(x: a, y: b), rec(x: b, y: a), rec(x: b, y: b)); + expectIntersect(rec(x: b), rec(x: c), '∅'); + }); + + test('types and fields', () { + expectIntersect(A(x: a), A(x: a), A(x: a)); + expectIntersect(A(x: a), A(x: b), A(x: b)); + expectIntersect(A(x: b), A(x: c), '∅'); + + expectIntersect(A(x: a), B(x: a), B(x: a)); + expectIntersect(A(x: a), B(x: b), B(x: b)); + expectIntersect(A(x: b), B(x: c), '∅'); + + expectIntersect(B(x: a), A(x: a), B(x: a)); + expectIntersect(B(x: a), A(x: b), B(x: b)); + expectIntersect(B(x: b), A(x: c), '∅'); + + expectIntersect(B(x: a), B(x: a), B(x: a)); + expectIntersect(B(x: a), B(x: b), B(x: b)); + expectIntersect(B(x: b), B(x: c), '∅'); + + expectIntersect(B(x: a), C(x: a), '∅'); + expectIntersect(B(x: a), C(x: b), '∅'); + expectIntersect(B(x: b), C(x: c), '∅'); + }); +} + +void expectIntersect(Object left, Object right, Object expected) { + var leftSpace = parseSpace(left); + var rightSpace = parseSpace(right); + var expectedText = parseSpace(expected).toString(); + + // Intersection is symmetric so try both directions. + expect(intersect(leftSpace, rightSpace).toString(), expectedText); + expect(intersect(rightSpace, leftSpace).toString(), expectedText); +} diff --git a/pkg/_fe_analyzer_shared/test/exhaustiveness/intersect_types_test.dart b/pkg/_fe_analyzer_shared/test/exhaustiveness/intersect_types_test.dart new file mode 100644 index 00000000000..704e9fd4d35 --- /dev/null +++ b/pkg/_fe_analyzer_shared/test/exhaustiveness/intersect_types_test.dart @@ -0,0 +1,99 @@ +// Copyright (c) 2022, 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:_fe_analyzer_shared/src/exhaustiveness/intersect.dart'; +import 'package:_fe_analyzer_shared/src/exhaustiveness/static_type.dart'; +import 'package:test/test.dart'; + +void main() { + test('hierarchy', () { + // (A) + // /|\ + // B C(D) + // / \ + // E F + var a = StaticType('A', isSealed: true); + var b = StaticType('B', inherits: [a]); + var c = StaticType('C', inherits: [a]); + var d = StaticType('D', isSealed: true, inherits: [a]); + var e = StaticType('E', inherits: [d]); + var f = StaticType('F', inherits: [d]); + + expectIntersect(a, a, a); + expectIntersect(a, b, b); + expectIntersect(a, c, c); + expectIntersect(a, d, d); + expectIntersect(a, e, e); + expectIntersect(a, f, f); + + expectIntersect(b, b, b); + expectIntersect(b, c, null); + expectIntersect(b, d, null); + expectIntersect(b, e, null); + expectIntersect(b, f, null); + + expectIntersect(c, c, c); + expectIntersect(c, d, null); + expectIntersect(c, e, null); + expectIntersect(c, f, null); + + expectIntersect(d, d, d); + expectIntersect(d, e, e); + expectIntersect(d, f, f); + + expectIntersect(e, e, e); + expectIntersect(e, f, null); + }); + + test('sealed with multiple paths', () { + // (A) + // / \ + // (B) C + // / \ / + // D E + var a = StaticType('A', isSealed: true); + var b = StaticType('B', isSealed: true, inherits: [a]); + var c = StaticType('C', inherits: [a]); + var d = StaticType('D', inherits: [b]); + var e = StaticType('E', inherits: [b, c]); + + expectIntersect(a, a, a); + expectIntersect(a, b, b); + expectIntersect(a, c, c); + expectIntersect(a, d, d); + expectIntersect(a, e, e); + expectIntersect(b, b, b); + expectIntersect(b, c, null); + expectIntersect(b, d, d); + expectIntersect(b, e, e); + expectIntersect(c, c, c); + expectIntersect(c, d, null); + expectIntersect(c, e, e); + expectIntersect(d, d, d); + expectIntersect(d, e, null); + expectIntersect(e, e, e); + }); + + test('nullable', () { + // A + // | + // B + var a = StaticType('A'); + var b = StaticType('B', inherits: [a]); + + expectIntersect(a, a.nullable, a); + expectIntersect(a, StaticType.nullType, null); + expectIntersect(a.nullable, StaticType.nullType, StaticType.nullType); + + expectIntersect(a, b.nullable, b); + expectIntersect(a.nullable, b, b); + expectIntersect(a.nullable, b.nullable, b.nullable); + }); +} + +void expectIntersect(StaticType left, StaticType right, StaticType? expected) { + // Intersection is symmetric so try both directions. + expect(intersectTypes(left, right), expected); + expect(intersectTypes(right, left), expected); +} diff --git a/pkg/_fe_analyzer_shared/test/exhaustiveness/is_exhaustive_field_test.dart b/pkg/_fe_analyzer_shared/test/exhaustiveness/is_exhaustive_field_test.dart new file mode 100644 index 00000000000..e127979c20f --- /dev/null +++ b/pkg/_fe_analyzer_shared/test/exhaustiveness/is_exhaustive_field_test.dart @@ -0,0 +1,259 @@ +// Copyright (c) 2022, 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:_fe_analyzer_shared/src/exhaustiveness/static_type.dart'; +import 'package:test/test.dart'; + +import 'utils.dart'; + +void main() { + group('sealed subtypes', () { + // (A) + // / \ + // B C + var a = StaticType('A', isSealed: true); + var b = StaticType('B', inherits: [a]); + var c = StaticType('C', inherits: [a]); + var t = StaticType('T', fields: {'x': a, 'y': a}); + + expectExhaustiveOnlyAll(t, [ + {'x': b, 'y': b}, + {'x': b, 'y': c}, + {'x': c, 'y': b}, + {'x': c, 'y': c}, + ]); + }); + + group('sealed subtypes medium', () { + // (A) + // /|\ + // B C D + var a = StaticType('A', isSealed: true); + var b = StaticType('B', inherits: [a]); + var c = StaticType('C', inherits: [a]); + var d = StaticType('D', inherits: [a]); + var t = StaticType('T', fields: {'y': a, 'z': a}); + + expectExhaustiveOnlyAll(t, [ + {'y': b, 'z': b}, + {'y': b, 'z': c}, + {'y': b, 'z': d}, + {'y': c, 'z': b}, + {'y': c, 'z': c}, + {'y': c, 'z': d}, + {'y': d, 'z': b}, + {'y': d, 'z': c}, + {'y': d, 'z': d}, + ]); + }); + + group('sealed subtypes large', () { + // (A) + // /|\ + // B C D + var a = StaticType('A', isSealed: true); + var b = StaticType('B', inherits: [a]); + var c = StaticType('C', inherits: [a]); + var d = StaticType('D', inherits: [a]); + var t = StaticType('T', fields: {'w': a, 'x': a, 'y': a, 'z': a}); + + expectExhaustiveOnlyAll(t, [ + {'w': b, 'x': b, 'y': b, 'z': b}, + {'w': b, 'x': b, 'y': b, 'z': c}, + {'w': b, 'x': b, 'y': b, 'z': d}, + {'w': b, 'x': b, 'y': c, 'z': b}, + {'w': b, 'x': b, 'y': c, 'z': c}, + {'w': b, 'x': b, 'y': c, 'z': d}, + {'w': b, 'x': b, 'y': d, 'z': b}, + {'w': b, 'x': b, 'y': d, 'z': c}, + {'w': b, 'x': b, 'y': d, 'z': d}, + {'w': b, 'x': c, 'y': b, 'z': b}, + {'w': b, 'x': c, 'y': b, 'z': c}, + {'w': b, 'x': c, 'y': b, 'z': d}, + {'w': b, 'x': c, 'y': c, 'z': b}, + {'w': b, 'x': c, 'y': c, 'z': c}, + {'w': b, 'x': c, 'y': c, 'z': d}, + {'w': b, 'x': c, 'y': d, 'z': b}, + {'w': b, 'x': c, 'y': d, 'z': c}, + {'w': b, 'x': c, 'y': d, 'z': d}, + {'w': b, 'x': d, 'y': b, 'z': b}, + {'w': b, 'x': d, 'y': b, 'z': c}, + {'w': b, 'x': d, 'y': b, 'z': d}, + {'w': b, 'x': d, 'y': c, 'z': b}, + {'w': b, 'x': d, 'y': c, 'z': c}, + {'w': b, 'x': d, 'y': c, 'z': d}, + {'w': b, 'x': d, 'y': d, 'z': b}, + {'w': b, 'x': d, 'y': d, 'z': c}, + {'w': b, 'x': d, 'y': d, 'z': d}, + {'w': c, 'x': b, 'y': b, 'z': b}, + {'w': c, 'x': b, 'y': b, 'z': c}, + {'w': c, 'x': b, 'y': b, 'z': d}, + {'w': c, 'x': b, 'y': c, 'z': b}, + {'w': c, 'x': b, 'y': c, 'z': c}, + {'w': c, 'x': b, 'y': c, 'z': d}, + {'w': c, 'x': b, 'y': d, 'z': b}, + {'w': c, 'x': b, 'y': d, 'z': c}, + {'w': c, 'x': b, 'y': d, 'z': d}, + {'w': c, 'x': c, 'y': b, 'z': b}, + {'w': c, 'x': c, 'y': b, 'z': c}, + {'w': c, 'x': c, 'y': b, 'z': d}, + {'w': c, 'x': c, 'y': c, 'z': b}, + {'w': c, 'x': c, 'y': c, 'z': c}, + {'w': c, 'x': c, 'y': c, 'z': d}, + {'w': c, 'x': c, 'y': d, 'z': b}, + {'w': c, 'x': c, 'y': d, 'z': c}, + {'w': c, 'x': c, 'y': d, 'z': d}, + {'w': c, 'x': d, 'y': b, 'z': b}, + {'w': c, 'x': d, 'y': b, 'z': c}, + {'w': c, 'x': d, 'y': b, 'z': d}, + {'w': c, 'x': d, 'y': c, 'z': b}, + {'w': c, 'x': d, 'y': c, 'z': c}, + {'w': c, 'x': d, 'y': c, 'z': d}, + {'w': c, 'x': d, 'y': d, 'z': b}, + {'w': c, 'x': d, 'y': d, 'z': c}, + {'w': c, 'x': d, 'y': d, 'z': d}, + {'w': d, 'x': b, 'y': b, 'z': b}, + {'w': d, 'x': b, 'y': b, 'z': c}, + {'w': d, 'x': b, 'y': b, 'z': d}, + {'w': d, 'x': b, 'y': c, 'z': b}, + {'w': d, 'x': b, 'y': c, 'z': c}, + {'w': d, 'x': b, 'y': c, 'z': d}, + {'w': d, 'x': b, 'y': d, 'z': b}, + {'w': d, 'x': b, 'y': d, 'z': c}, + {'w': d, 'x': b, 'y': d, 'z': d}, + {'w': d, 'x': c, 'y': b, 'z': b}, + {'w': d, 'x': c, 'y': b, 'z': c}, + {'w': d, 'x': c, 'y': b, 'z': d}, + {'w': d, 'x': c, 'y': c, 'z': b}, + {'w': d, 'x': c, 'y': c, 'z': c}, + {'w': d, 'x': c, 'y': c, 'z': d}, + {'w': d, 'x': c, 'y': d, 'z': b}, + {'w': d, 'x': c, 'y': d, 'z': c}, + {'w': d, 'x': c, 'y': d, 'z': d}, + {'w': d, 'x': d, 'y': b, 'z': b}, + {'w': d, 'x': d, 'y': b, 'z': c}, + {'w': d, 'x': d, 'y': b, 'z': d}, + {'w': d, 'x': d, 'y': c, 'z': b}, + {'w': d, 'x': d, 'y': c, 'z': c}, + {'w': d, 'x': d, 'y': c, 'z': d}, + {'w': d, 'x': d, 'y': d, 'z': b}, + {'w': d, 'x': d, 'y': d, 'z': c}, + {'w': d, 'x': d, 'y': d, 'z': d}, + ]); + }); + + group('sealed transitive subtypes', () { + // (A) + // / \ + // (B) (C) + // / \ \ + // D E F + var a = StaticType('A', isSealed: true); + var b = StaticType('B', isSealed: true, inherits: [a]); + var c = StaticType('C', isSealed: true, inherits: [a]); + var d = StaticType('D', inherits: [b]); + var e = StaticType('E', inherits: [b]); + var f = StaticType('F', inherits: [c]); + + var t = StaticType('T', fields: {'x': a, 'y': a}); + expectExhaustiveOnlyAll(t, [ + {'x': a, 'y': a}, + ]); + + expectExhaustiveOnlyAll(t, [ + {'x': b, 'y': b}, + {'x': b, 'y': c}, + {'x': c, 'y': b}, + {'x': c, 'y': c}, + ]); + + expectExhaustiveOnlyAll(t, [ + {'x': b, 'y': d}, + {'x': b, 'y': e}, + {'x': b, 'y': f}, + {'x': c, 'y': d}, + {'x': c, 'y': e}, + {'x': c, 'y': f}, + ]); + }); + + group('unsealed subtypes', () { + // A + // / \ + // B C + var a = StaticType('A'); + var b = StaticType('B', inherits: [a]); + var c = StaticType('C', inherits: [a]); + + // Not exhaustive even when known subtypes covered. + var t = StaticType('T', fields: {'x': a, 'y': a}); + expectNeverExhaustive(t, [ + {'x': b, 'y': b}, + {'x': b, 'y': c}, + {'x': c, 'y': b}, + {'x': c, 'y': c}, + ]); + + // Exhaustive if field static type is a covered subtype. + var u = StaticType('T', fields: {'x': b, 'y': c}); + expectExhaustiveOnlyAll(u, [ + {'x': b, 'y': c}, + ]); + }); + + group('different fields', () { + // (A) + // / \ + // B C + var a = StaticType('A', isSealed: true); + var b = StaticType('B', inherits: [a]); + var c = StaticType('C', inherits: [a]); + var t = StaticType('T', fields: {'x': a, 'y': a, 'z': a}); + + expectNeverExhaustive(t, [ + {'x': b}, + {'y': b}, + {'z': b}, + ]); + + expectExhaustiveOnlyAll(t, [ + {'x': b, 'y': a}, + {'x': c, 'z': a}, + ]); + + expectExhaustiveOnlyAll(t, [ + {'x': b, 'y': b}, + {'x': b, 'y': c}, + {'x': c, 'y': b}, + {'x': c, 'y': c}, + ]); + }); + + group('field types', () { + // (A) + // / \ + // B C + var a = StaticType('A', isSealed: true); + var b = StaticType('B', inherits: [a]); + var c = StaticType('C', inherits: [a]); + var t = StaticType('T', fields: {'x': a, 'y': b, 'z': c}); + + expectExhaustiveOnlyAll(t, [ + {'x': a, 'y': b, 'z': c}, + ]); + + expectExhaustiveOnlyAll(t, [ + {'x': b}, + {'x': c}, + ]); + + expectExhaustiveOnlyAll(t, [ + {'y': b}, + ]); + + expectExhaustiveOnlyAll(t, [ + {'z': c}, + ]); + }); +} diff --git a/pkg/_fe_analyzer_shared/test/exhaustiveness/is_exhaustive_nested_test.dart b/pkg/_fe_analyzer_shared/test/exhaustiveness/is_exhaustive_nested_test.dart new file mode 100644 index 00000000000..d236651dbae --- /dev/null +++ b/pkg/_fe_analyzer_shared/test/exhaustiveness/is_exhaustive_nested_test.dart @@ -0,0 +1,57 @@ +// Copyright (c) 2022, 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:_fe_analyzer_shared/src/exhaustiveness/static_type.dart'; +import 'package:test/test.dart'; + +import 'utils.dart'; + +void main() { + group('nested records', () { + // (A) + // / \ + // B C + var a = StaticType('A', isSealed: true); + var b = StaticType('B', inherits: [a]); + var c = StaticType('C', inherits: [a]); + var t = StaticType('T', fields: {'x': a, 'y': b}); + var u = StaticType('U', fields: {'w': t, 'z': t}); + + expectExhaustiveOnlyAll(u, [ + rec(w: rec(x: a), z: t), + ]); + + expectExhaustiveOnlyAll(u, [ + rec(w: rec(x: a, y: a), z: rec(x: a, y: a)), + ]); + + expectExhaustiveOnlyAll(u, [ + rec(w: rec(x: a, y: b), z: rec(x: a, y: b)), + ]); + + expectExhaustiveOnlyAll(u, [ + rec(w: rec(x: b), z: t), + rec(w: rec(x: c), z: t), + ]); + + expectExhaustiveOnlyAll(u, [ + rec(w: rec(x: b, y: b), z: rec(x: b, y: b)), + rec(w: rec(x: b, y: b), z: rec(x: c, y: b)), + rec(w: rec(x: c, y: b), z: rec(x: b, y: b)), + rec(w: rec(x: c, y: b), z: rec(x: c, y: b)), + ]); + }); + + group('nested with different fields of same name', () { + // A B C D + var a = StaticType('A'); + var b = StaticType('B', fields: {'x': a}); + var c = StaticType('C', fields: {'x': b}); + var d = StaticType('D', fields: {'x': c}); + + expectExhaustiveOnlyAll(d, [ + rec(x: rec(x: rec(x: a))), + ]); + }); +} diff --git a/pkg/_fe_analyzer_shared/test/exhaustiveness/is_exhaustive_type_test.dart b/pkg/_fe_analyzer_shared/test/exhaustiveness/is_exhaustive_type_test.dart new file mode 100644 index 00000000000..77c37ed159b --- /dev/null +++ b/pkg/_fe_analyzer_shared/test/exhaustiveness/is_exhaustive_type_test.dart @@ -0,0 +1,195 @@ +// Copyright (c) 2022, 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:_fe_analyzer_shared/src/exhaustiveness/space.dart'; +import 'package:_fe_analyzer_shared/src/exhaustiveness/static_type.dart'; +import 'package:test/test.dart'; + +import 'utils.dart'; + +/// Test `subtract()` on combinations of types. +void main() { + // Note: In the class diagrams, "(_)" means "sealed". A bare name is unsealed. + group('sealed family', () { + // (A) + // / \ + // (B) (C) + // / \ \ + // D E F + var a = StaticType('A', isSealed: true); + var b = StaticType('B', isSealed: true, inherits: [a]); + var c = StaticType('C', isSealed: true, inherits: [a]); + var d = StaticType('D', inherits: [b]); + var e = StaticType('E', inherits: [b]); + var f = StaticType('F', inherits: [c]); + + var checkExhaustive = _makeTestFunction([a, b, c, d, e, f]); + checkExhaustive([a], 'ABCDEF'); + checkExhaustive([b], 'BDE'); + checkExhaustive([c], 'CF'); + checkExhaustive([d], 'D'); + checkExhaustive([f], 'CF'); + + checkExhaustive([a, b], 'ABCDEF'); + checkExhaustive([a, c], 'ABCDEF'); + checkExhaustive([a, d], 'ABCDEF'); + checkExhaustive([a, f], 'ABCDEF'); + + checkExhaustive([b, c], 'ABCDEF'); + checkExhaustive([b, d], 'BDE'); + checkExhaustive([b, f], 'ABCDEF'); + + checkExhaustive([c, d], 'CDF'); + checkExhaustive([c, e], 'CEF'); + checkExhaustive([c, f], 'CF'); + + checkExhaustive([d, e], 'BDE'); // Covers B because both cases covered. + checkExhaustive([d, f], 'CDF'); + checkExhaustive([e, f], 'CEF'); + + checkExhaustive([d, e, f], 'ABCDEF'); // All cases covered. + }); + + group('sealed with many subtypes', () { + // (A) + // //|\\ + // / /|\ \ + // B C D E F + var a = StaticType('A', isSealed: true); + var b = StaticType('B', inherits: [a]); + var c = StaticType('C', inherits: [a]); + var d = StaticType('D', inherits: [a]); + var e = StaticType('E', inherits: [a]); + var f = StaticType('F', inherits: [a]); + + var checkExhaustive = _makeTestFunction([a, b, c, d, e, f]); + checkExhaustive([a], 'ABCDEF'); + checkExhaustive([b], 'B'); + checkExhaustive([c, e], 'CE'); + checkExhaustive([b, d, f], 'BDF'); + checkExhaustive([b, c, e, f], 'BCEF'); + checkExhaustive([b, c, d, e, f], 'ABCDEF'); // Covers A. + }); + + group('sealed with multiple paths', () { + // (A) + // / \ + // (B) C + // / \ / + // D E + var a = StaticType('A', isSealed: true); + var b = StaticType('B', isSealed: true, inherits: [a]); + var c = StaticType('C', inherits: [a]); + var d = StaticType('D', inherits: [b]); + var e = StaticType('E', inherits: [b, c]); + + var checkExhaustive = _makeTestFunction([a, b, c, d, e]); + checkExhaustive([a], 'ABCDE'); + checkExhaustive([b], 'BDE'); + checkExhaustive([c], 'CE'); + checkExhaustive([d], 'D'); + checkExhaustive([e], 'E'); + + checkExhaustive([b, c], 'ABCDE'); + checkExhaustive([b, d], 'BDE'); + checkExhaustive([b, e], 'BDE'); + checkExhaustive([c, d], 'CDE'); + checkExhaustive([d, e], 'BDE'); + }); + + group('sealed with unsealed supertype', () { + // A + // | + // (B) + // / \ + // C D + var a = StaticType('A'); + var b = StaticType('B', isSealed: true, inherits: [a]); + var c = StaticType('C', inherits: [b]); + var d = StaticType('D', inherits: [b]); + + var checkExhaustive = _makeTestFunction([a, b, c, d]); + checkExhaustive([a], 'ABCD'); + checkExhaustive([b], 'BCD'); + checkExhaustive([c], 'C'); + checkExhaustive([d], 'D'); + checkExhaustive([c, d], 'BCD'); + }); + + group('sealed with single subclass', () { + // (A) + // | + // (B) + // | + // C + var a = StaticType('A', isSealed: true); + var b = StaticType('B', isSealed: true, inherits: [a]); + var c = StaticType('C', inherits: [b]); + + var checkExhaustive = _makeTestFunction([a, b, c]); + checkExhaustive([a], 'ABC'); + checkExhaustive([b], 'ABC'); // Every A must be a B, so A is covered. + checkExhaustive([c], 'ABC'); // Every C must be a B, which must be an A. + checkExhaustive([a, b], 'ABC'); + checkExhaustive([a, c], 'ABC'); + checkExhaustive([b, c], 'ABC'); + checkExhaustive([a, b, c], 'ABC'); + }); + + group('unsealed', () { + // A + // / \ + // B C + // / \ / \ + // D E F + var a = StaticType('A'); + var b = StaticType('B', inherits: [a]); + var c = StaticType('C', inherits: [a]); + var d = StaticType('D', inherits: [b]); + var e = StaticType('E', inherits: [b, c]); + var f = StaticType('F', inherits: [c]); + + var checkExhaustive = _makeTestFunction([a, b, c, d, e, f]); + checkExhaustive([a], 'ABCDEF'); + checkExhaustive([b], 'BDE'); + checkExhaustive([d], 'D'); + checkExhaustive([a, b], 'ABCDEF'); // Same as A. + checkExhaustive([a, d], 'ABCDEF'); // Same as A. + checkExhaustive([d, e], 'DE'); // Doesn't cover B because unsealed. + checkExhaustive([d, f], 'DF'); + checkExhaustive([e, f], 'EF'); // Doesn't cover C because unsealed. + checkExhaustive([b, f], 'BDEF'); + checkExhaustive([c, d], 'CDEF'); + checkExhaustive([d, e, f], 'DEF'); + }); +} + +/// Returns a function that takes a list of `types` and a string containing a +/// list of type letters that map to the types in [allTypes]. +/// +/// The function checks that the list of types exhaustively covers every type +/// whose name appears in the string. So: +/// +/// checkExhaustive([d, e], 'bde'); +/// +/// Means that the union of D|E should be exhaustive over B, D, and E and not +/// exhaustive over the other types in [allTypes]. +Function(List, String) _makeTestFunction( + List allTypes) { + assert(allTypes.length <= 6, 'Only supports up to six types.'); + var letters = 'ABCDEF'; + + return (types, covered) { + var spaces = types.map((type) => Space(type)).toList(); + + for (var i = 0; i < allTypes.length; i++) { + var value = Space(allTypes[i]); + if (covered.contains(letters[i])) { + expectExhaustive(value, spaces); + } else { + expectNotExhaustive(value, spaces); + } + } + }; +} diff --git a/pkg/_fe_analyzer_shared/test/exhaustiveness/report_errors_test.dart b/pkg/_fe_analyzer_shared/test/exhaustiveness/report_errors_test.dart new file mode 100644 index 00000000000..62da5130edd --- /dev/null +++ b/pkg/_fe_analyzer_shared/test/exhaustiveness/report_errors_test.dart @@ -0,0 +1,129 @@ +// Copyright (c) 2022, 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:_fe_analyzer_shared/src/exhaustiveness/exhaustive.dart'; +import 'package:_fe_analyzer_shared/src/exhaustiveness/static_type.dart'; +import 'package:test/test.dart'; + +import 'utils.dart'; + +void main() { + // Here, "(_)" means "sealed". A bare name is unsealed. + // + // (A) + // / \ + // (B) (C) + // / \ \ + // D E F + // / \ + // G H + var a = StaticType('A', isSealed: true); + var b = StaticType('B', isSealed: true, inherits: [a]); + var c = StaticType('C', isSealed: true, inherits: [a]); + var d = StaticType('D', inherits: [b]); + var e = StaticType('E', inherits: [b]); + var f = StaticType('F', inherits: [c]); + var g = StaticType('G', inherits: [f]); + var h = StaticType('H', inherits: [f]); + + test('exhaustiveness', () { + // Case matching top type covers all subtypes. + expectReportErrors(a, [a]); + expectReportErrors(b, [a]); + expectReportErrors(d, [a]); + + // Case matching subtype doesn't cover supertype. + expectReportErrors(a, [b], 'A is not exhaustively matched by B.'); + expectReportErrors(b, [b]); + expectReportErrors(d, [b]); + expectReportErrors(e, [b]); + + // Matching subtypes of sealed type is exhaustive. + expectReportErrors(a, [b, c]); + expectReportErrors(a, [d, e, f]); + expectReportErrors(a, [b, f]); + expectReportErrors(a, [c, d], 'A is not exhaustively matched by C|D.'); + expectReportErrors(f, [g, h], 'F is not exhaustively matched by G|H.'); + }); + + test('unreachable case', () { + // Same type. + expectReportErrors(b, [b, b], 'Case #2 B is covered by B.'); + + // Previous case is supertype. + expectReportErrors(b, [a, b], 'Case #2 B is covered by A.'); + + // Previous subtype cases cover sealed supertype. + expectReportErrors(a, [b, c, a], 'Case #3 A is covered by B|C.'); + expectReportErrors(a, [d, e, f, a], 'Case #4 A is covered by D|E|F.'); + expectReportErrors(a, [b, f, a], 'Case #3 A is covered by B|F.'); + expectReportErrors(a, [c, d, a]); + + // Previous subtype cases do not cover unsealed supertype. + expectReportErrors(f, [g, h, f]); + }); + + test('covered record destructuring', () { + var r = StaticType('R', fields: {'x': a, 'y': a, 'z': a}); + + // Wider field is not covered. + expectReportErrors(r, [ + {'x': b}, + {'x': a} + ]); + + // Narrower field is covered. + expectReportErrors( + r, + [ + {'x': a}, + {'x': b} + ], + 'Case #2 (x: B) is covered by (x: A).'); + }); + + test('nullable sealed', () { + // (A) + // / \ + // B (C) + // / \ + // D E + var a = StaticType('A', isSealed: true); + var b = StaticType('B', inherits: [a]); + var c = StaticType('C', isSealed: true, inherits: [a]); + var d = StaticType('D', inherits: [c]); + var e = StaticType('E', inherits: [c]); + + // Must cover null. + expectReportErrors( + a.nullable, [b, d, e], 'A? is not exhaustively matched by B|D|E.'); + + // Can cover null with any nullable subtype. + expectReportErrors(a.nullable, [b.nullable, c]); + expectReportErrors(a.nullable, [b, c.nullable]); + expectReportErrors(a.nullable, [b, d.nullable, e]); + expectReportErrors(a.nullable, [b, d, e.nullable]); + + // Can cover null with a null space. + expectReportErrors(a.nullable, [b, c, StaticType.nullType]); + expectReportErrors(a.nullable, [b, d, e, StaticType.nullType]); + + // Nullable covers the non-null. + expectReportErrors( + a.nullable, [a.nullable, a], 'Case #2 A is covered by A?.'); + expectReportErrors( + b.nullable, [a.nullable, b], 'Case #2 B is covered by A?.'); + + // Nullable covers null. + expectReportErrors(a.nullable, [a.nullable, StaticType.nullType], + 'Case #2 Null is covered by A?.'); + expectReportErrors(b.nullable, [a.nullable, StaticType.nullType], + 'Case #2 Null is covered by A?.'); + }); +} + +void expectReportErrors(StaticType valueType, List cases, + [String errors = '']) { + expect(reportErrors(valueType, parseSpaces(cases)), errors); +} diff --git a/pkg/_fe_analyzer_shared/test/exhaustiveness/static_type_test.dart b/pkg/_fe_analyzer_shared/test/exhaustiveness/static_type_test.dart new file mode 100644 index 00000000000..54df2c6f287 --- /dev/null +++ b/pkg/_fe_analyzer_shared/test/exhaustiveness/static_type_test.dart @@ -0,0 +1,172 @@ +// Copyright (c) 2022, 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:_fe_analyzer_shared/src/exhaustiveness/static_type.dart'; +import 'package:test/test.dart'; + +void main() { + group('isSubtypeOf()', () { + var a = StaticType('A'); + var b = StaticType('B', inherits: [a]); + var b2 = StaticType('B2', inherits: [a]); + var c = StaticType('C', inherits: [b]); + var d = StaticType('D', inherits: [c]); + + test('subtype includes self', () { + expect(a.isSubtypeOf(a), isTrue); + expect(b.isSubtypeOf(b), isTrue); + expect(c.isSubtypeOf(c), isTrue); + expect(d.isSubtypeOf(d), isTrue); + }); + + test('immediate', () { + expect(a.isSubtypeOf(b), isFalse); + expect(b.isSubtypeOf(a), isTrue); + + expect(b.isSubtypeOf(c), isFalse); + expect(c.isSubtypeOf(b), isTrue); + + expect(c.isSubtypeOf(d), isFalse); + expect(d.isSubtypeOf(c), isTrue); + }); + + test('transitive', () { + expect(a.isSubtypeOf(c), isFalse); + expect(c.isSubtypeOf(a), isTrue); + + expect(b.isSubtypeOf(d), isFalse); + expect(d.isSubtypeOf(b), isTrue); + + expect(a.isSubtypeOf(d), isFalse); + expect(d.isSubtypeOf(a), isTrue); + }); + + test('unrelated', () { + expect(b.isSubtypeOf(b2), isFalse); + expect(b2.isSubtypeOf(b), isFalse); + + expect(c.isSubtypeOf(b2), isFalse); + expect(b2.isSubtypeOf(c), isFalse); + + expect(d.isSubtypeOf(b2), isFalse); + expect(b2.isSubtypeOf(d), isFalse); + }); + + test('multiple supertypes', () { + // I1 I2 I3 + // \ / \ / + // I12 I23 + // \/ + // I123 + var i1 = StaticType('I1'); + var i2 = StaticType('I2'); + var i3 = StaticType('I3'); + var i12 = StaticType('I12', inherits: [i1, i2]); + var i23 = StaticType('I12', inherits: [i2, i3]); + var i123 = StaticType('I12', inherits: [i12, i23]); + + expect(i1.isSubtypeOf(i2), isFalse); + expect(i2.isSubtypeOf(i1), isFalse); + expect(i2.isSubtypeOf(i3), isFalse); + expect(i3.isSubtypeOf(i2), isFalse); + expect(i1.isSubtypeOf(i3), isFalse); + expect(i3.isSubtypeOf(i1), isFalse); + + expect(i1.isSubtypeOf(i12), isFalse); + expect(i12.isSubtypeOf(i1), isTrue); + expect(i2.isSubtypeOf(i12), isFalse); + expect(i12.isSubtypeOf(i2), isTrue); + expect(i3.isSubtypeOf(i12), isFalse); + expect(i12.isSubtypeOf(i3), isFalse); + + expect(i1.isSubtypeOf(i23), isFalse); + expect(i23.isSubtypeOf(i1), isFalse); + expect(i2.isSubtypeOf(i23), isFalse); + expect(i23.isSubtypeOf(i2), isTrue); + expect(i3.isSubtypeOf(i23), isFalse); + expect(i23.isSubtypeOf(i3), isTrue); + + expect(i1.isSubtypeOf(i123), isFalse); + expect(i123.isSubtypeOf(i1), isTrue); + expect(i2.isSubtypeOf(i123), isFalse); + expect(i123.isSubtypeOf(i2), isTrue); + expect(i3.isSubtypeOf(i123), isFalse); + expect(i123.isSubtypeOf(i3), isTrue); + expect(i12.isSubtypeOf(i123), isFalse); + expect(i123.isSubtypeOf(i12), isTrue); + expect(i23.isSubtypeOf(i123), isFalse); + expect(i123.isSubtypeOf(i23), isTrue); + }); + + test('nullable', () { + var a = StaticType('A'); + var b = StaticType('B', inherits: [a]); + + expect(StaticType.nullType.isSubtypeOf(a), isFalse); + expect(StaticType.nullType.isSubtypeOf(b), isFalse); + expect(StaticType.nullType.isSubtypeOf(a.nullable), isTrue); + expect(StaticType.nullType.isSubtypeOf(b.nullable), isTrue); + + expect(a.isSubtypeOf(StaticType.nullType), isFalse); + expect(b.isSubtypeOf(StaticType.nullType), isFalse); + expect(a.nullable.isSubtypeOf(StaticType.nullType), isFalse); + expect(b.nullable.isSubtypeOf(StaticType.nullType), isFalse); + + expect(a.isSubtypeOf(a.nullable), isTrue); + expect(a.nullable.isSubtypeOf(a), isFalse); + expect(a.nullable.isSubtypeOf(a.nullable), isTrue); + + expect(a.isSubtypeOf(b.nullable), isFalse); + expect(a.nullable.isSubtypeOf(b), isFalse); + expect(a.nullable.isSubtypeOf(b.nullable), isFalse); + + expect(b.isSubtypeOf(a.nullable), isTrue); + expect(b.nullable.isSubtypeOf(a), isFalse); + expect(b.nullable.isSubtypeOf(a.nullable), isTrue); + }); + }); + + test('fields', () { + var a = StaticType('A'); + var b = StaticType('B'); + var c = StaticType('C', fields: {'x': a, 'y': b}); + var d = StaticType('D', fields: {'w': a}); + var e = StaticType('E', inherits: [c, d], fields: {'z': b}); + + expect(a.fields, isEmpty); + expect(b.fields, isEmpty); + + expect(c.fields, hasLength(2)); + expect(c.fields['x'], a); + expect(c.fields['y'], b); + + // Fields are inherited. + expect(e.fields, hasLength(4)); + expect(e.fields['x'], a); + expect(e.fields['y'], b); + expect(e.fields['w'], a); + expect(e.fields['z'], b); + + // Overridden field types win. + var f = StaticType('F', fields: {'x': a}); + var g = StaticType('G', inherits: [f], fields: {'x': b}); + expect(g.fields, hasLength(1)); + expect(g.fields['x'], b); + }); + + test('subtypes', () { + var a = StaticType('A', isSealed: true); + var b = StaticType('B', inherits: [a]); + var c = StaticType('C', inherits: [a]); + var d = StaticType('D', inherits: [c]); + + // Gets subtypes for sealed type. + var aSubtypes = a.subtypes.toList(); + expect(aSubtypes, unorderedEquals([b, c])); + + // And unsealed. + var cSubtypes = c.subtypes.toList(); + expect(cSubtypes, unorderedEquals([d])); + }); +} diff --git a/pkg/_fe_analyzer_shared/test/exhaustiveness/subtract_test.dart b/pkg/_fe_analyzer_shared/test/exhaustiveness/subtract_test.dart new file mode 100644 index 00000000000..5a092175c93 --- /dev/null +++ b/pkg/_fe_analyzer_shared/test/exhaustiveness/subtract_test.dart @@ -0,0 +1,253 @@ +// Copyright (c) 2022, 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:_fe_analyzer_shared/src/exhaustiveness/space.dart'; +import 'package:_fe_analyzer_shared/src/exhaustiveness/static_type.dart'; +import 'package:_fe_analyzer_shared/src/exhaustiveness/subtract.dart'; +import 'package:test/test.dart'; + +import 'utils.dart'; + +void main() { + group('empty', () { + var a = StaticType('A'); + var b = StaticType('B', inherits: [a], fields: {'x': a, 'y': a}); + var c = StaticType('C', inherits: [a]); + + expectSubtract('∅', '∅', '∅'); + + // Subtracting from empty stays empty. + expectSubtract('∅', a, '∅'); + expectSubtract('∅', [b, c], '∅'); + expectSubtract('∅', {'x': a}, '∅'); + expectSubtract('∅', ty(b, {'x': a}), '∅'); + + // Subtracting empty leaves unchanged. + expectSubtract(a, '∅', 'A'); + expectSubtract([b, c], '∅', 'B|C'); + expectSubtract({'x': a}, '∅', '(x: A)'); + expectSubtract(ty(b, {'x': a}), '∅', 'B(x: A)'); + }); + + group('union with sealed types', () { + // (A) + // /|\ + // B C(D) + // / \ + // E F + var a = StaticType('A', isSealed: true); + var b = StaticType('B', inherits: [a]); + var c = StaticType('C', inherits: [a]); + var d = StaticType('D', isSealed: true, inherits: [a]); + var e = StaticType('E', inherits: [d]); + var f = StaticType('F', inherits: [d]); + + expectSubtract(a, b, 'C|D'); + expectSubtract(a, c, 'B|D'); + expectSubtract(a, d, 'B|C'); + expectSubtract(a, e, 'B|C|F'); + expectSubtract(a, f, 'B|C|E'); + expectSubtract(a, [b, c], 'D'); + expectSubtract(a, [b, d], 'C'); + expectSubtract(a, [b, e], 'C|F'); + + expectSubtract([b, c], b, 'C'); + expectSubtract([b, c], c, 'B'); + expectSubtract([b, c, d], b, 'C|D'); + expectSubtract([b, c, d], c, 'B|D'); + expectSubtract([b, c, d], d, 'B|C'); + + expectSubtract([b, c], [b, c], '∅'); + expectSubtract([b, c], [b, d], 'C'); + expectSubtract([b, d], [b, c], 'D'); + expectSubtract([b, c, d], [b, c], 'D'); + + expectSubtract([b, d], e, 'B|F'); + expectSubtract([b, e], d, 'B'); + }); + + group('unsealed subtype', () { + // A B + // / \ / + // C D + // \ / + // E + var a = StaticType('A'); + var b = StaticType('B'); + var c = StaticType('C', inherits: [a]); + var d = StaticType('D', inherits: [a, b]); + var e = StaticType('E', inherits: [c, d]); + + expectSubtract(a, a, '∅'); + expectSubtract(a, b, 'A'); + expectSubtract(a, c, 'A'); + expectSubtract(a, d, 'A'); + expectSubtract(a, e, 'A'); + + expectSubtract(b, a, 'B'); + expectSubtract(b, b, '∅'); + expectSubtract(b, c, 'B'); + expectSubtract(b, d, 'B'); + expectSubtract(b, e, 'B'); + + expectSubtract(c, a, '∅'); + expectSubtract(c, b, 'C'); + expectSubtract(c, c, '∅'); + expectSubtract(c, d, 'C'); + expectSubtract(c, e, 'C'); + + expectSubtract(d, a, '∅'); + expectSubtract(d, b, '∅'); + expectSubtract(d, c, 'D'); + expectSubtract(d, d, '∅'); + expectSubtract(d, e, 'D'); + + expectSubtract(e, a, '∅'); + expectSubtract(e, b, '∅'); + expectSubtract(e, c, '∅'); + expectSubtract(e, d, '∅'); + expectSubtract(e, e, '∅'); + }); + + group('unsealed subtype and field', () { + // X A + // | | + // Y B + var x = StaticType('X'); + var y = StaticType('Y', inherits: [x]); + var a = StaticType('A', fields: {'x': StaticType.top}); + var b = StaticType('B', inherits: [a]); + + Space A({required StaticType x}) => ty(a, {'x': x}); + Space B({required StaticType x}) => ty(b, {'x': x}); + + expectSubtract(A(x: x), A(x: x), '∅'); + expectSubtract(A(x: x), B(x: x), 'A(x: X)'); + expectSubtract(B(x: x), A(x: x), '∅'); + expectSubtract(B(x: x), B(x: x), '∅'); + + expectSubtract(A(x: x), A(x: y), 'A(x: X)'); + expectSubtract(A(x: x), B(x: y), 'A(x: X)'); + expectSubtract(B(x: x), A(x: y), 'B(x: X)'); + expectSubtract(B(x: x), B(x: y), 'B(x: X)'); + + expectSubtract(A(x: y), A(x: x), '∅'); + expectSubtract(A(x: y), B(x: x), 'A(x: Y)'); + expectSubtract(B(x: y), A(x: x), '∅'); + expectSubtract(B(x: y), B(x: x), '∅'); + + expectSubtract(A(x: y), A(x: y), '∅'); + expectSubtract(A(x: y), B(x: y), 'A(x: Y)'); + expectSubtract(B(x: y), A(x: y), '∅'); + expectSubtract(B(x: y), B(x: y), '∅'); + + expectSubtract(A(x: x), a, '∅'); + expectSubtract(A(x: x), b, 'A(x: X)'); + expectSubtract(B(x: x), a, '∅'); + expectSubtract(B(x: x), b, '∅'); + expectSubtract(a, A(x: x), 'A'); + expectSubtract(b, A(x: x), 'B'); + expectSubtract(a, B(x: x), 'A'); + expectSubtract(b, B(x: x), 'B'); + }); + + group('sealed subtype and field', () { + // (X) (A) + // /|\ /|\ + // W Y Z B C D + var x = StaticType('X', isSealed: true); + StaticType('W', inherits: [x]); + var y = StaticType('Y', inherits: [x]); + StaticType('Z', inherits: [x]); + var a = StaticType('A', isSealed: true, fields: {'x': StaticType.top}); + var b = StaticType('B', inherits: [a]); + StaticType('C', inherits: [a]); + StaticType('D', inherits: [a]); + + Space A({required StaticType x}) => ty(a, {'x': x}); + Space B({required StaticType x}) => ty(b, {'x': x}); + + expectSubtract(A(x: x), A(x: x), '∅'); + expectSubtract(A(x: x), B(x: x), 'C(x: X)|D(x: X)'); + expectSubtract(B(x: x), A(x: x), '∅'); + expectSubtract(B(x: x), B(x: x), '∅'); + + expectSubtract(A(x: x), A(x: y), 'A(x: W|Z)'); + expectSubtract(A(x: x), B(x: y), 'B(x: W|Z)|C(x: X)|D(x: X)'); + expectSubtract(B(x: x), A(x: y), 'B(x: W|Z)'); + expectSubtract(B(x: x), B(x: y), 'B(x: W|Z)'); + + expectSubtract(A(x: y), A(x: x), '∅'); + expectSubtract(A(x: y), B(x: x), 'C(x: Y)|D(x: Y)'); + expectSubtract(B(x: y), A(x: x), '∅'); + expectSubtract(B(x: y), B(x: x), '∅'); + + expectSubtract(A(x: y), A(x: y), '∅'); + expectSubtract(A(x: y), B(x: y), 'C(x: Y)|D(x: Y)'); + expectSubtract(B(x: y), A(x: y), '∅'); + expectSubtract(B(x: y), B(x: y), '∅'); + + expectSubtract(A(x: x), a, '∅'); + expectSubtract(A(x: x), b, 'C(x: X)|D(x: X)'); + expectSubtract(B(x: x), a, '∅'); + expectSubtract(B(x: x), b, '∅'); + + // Note that these don't specialize x to "W|Z" because it's declared type + // is top, which isn't sealed. + expectSubtract(a, A(x: x), 'A'); + expectSubtract(b, A(x: x), 'B'); + expectSubtract(a, B(x: x), 'B|C|D'); + expectSubtract(b, B(x: x), 'B'); + }); + + group('sealed subtype and field', () { + // (X) (A) + // /|\ /|\ + // W Y Z B C D + var x = StaticType('X', isSealed: true); + var w = StaticType('W', inherits: [x]); + var y = StaticType('Y', inherits: [x]); + var z = StaticType('Z', inherits: [x]); + var a = StaticType('A', isSealed: true, fields: {'x': x, 'y': x, 'z': x}); + var b = StaticType('B', inherits: [a]); + var c = StaticType('C', inherits: [a]); + StaticType('D', inherits: [a]); + + Space A({StaticType? x, StaticType? y, StaticType? z}) => ty(a, + {if (x != null) 'x': x, if (y != null) 'y': y, if (z != null) 'z': z}); + Space B({StaticType? x, StaticType? y, StaticType? z}) => ty(b, + {if (x != null) 'x': x, if (y != null) 'y': y, if (z != null) 'z': z}); + + // Fields only on left. + expectSubtract(A(x: x, y: y), a, '∅'); + expectSubtract(A(x: x, y: y), b, 'C(x: X, y: Y)|D(x: X, y: Y)'); + expectSubtract(B(x: x, y: y), a, '∅'); + expectSubtract(B(x: x, y: y), c, 'B(x: X, y: Y)'); + + expectSubtract(A(x: w, y: z), a, '∅'); + expectSubtract(A(x: w, y: z), b, 'C(x: W, y: Z)|D(x: W, y: Z)'); + expectSubtract(B(x: w, y: z), a, '∅'); + expectSubtract(B(x: w, y: z), c, 'B(x: W, y: Z)'); + + // Fields only on right. + expectSubtract(a, A(x: x, y: y), 'A(x: X, y: W|Z)'); + expectSubtract(b, A(x: x, y: y), 'B(x: X, y: W|Z)'); + expectSubtract(a, B(x: x, y: y), 'B(x: X, y: W|Z)|C|D'); + expectSubtract(c, B(x: x, y: y), 'C'); + + expectSubtract(a, A(x: w, y: z), 'A(x: Y|Z, y: X)|A(x: X, y: W|Y)'); + expectSubtract(b, A(x: w, y: z), 'B(x: Y|Z, y: X)|B(x: X, y: W|Y)'); + expectSubtract(a, B(x: w, y: z), 'B(x: Y|Z, y: X)|B(x: X, y: W|Y)|C|D'); + expectSubtract(c, B(x: w, y: z), 'C'); + }); +} + +void expectSubtract(Object left, Object right, String expected) { + var leftSpace = parseSpace(left); + var rightSpace = parseSpace(right); + test('$leftSpace - $rightSpace', () { + var result = subtract(leftSpace, rightSpace); + expect(result.toString(), expected); + }); +} diff --git a/pkg/_fe_analyzer_shared/test/exhaustiveness/utils.dart b/pkg/_fe_analyzer_shared/test/exhaustiveness/utils.dart new file mode 100644 index 00000000000..10cc5be362f --- /dev/null +++ b/pkg/_fe_analyzer_shared/test/exhaustiveness/utils.dart @@ -0,0 +1,102 @@ +// Copyright (c) 2022, 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:_fe_analyzer_shared/src/exhaustiveness/exhaustive.dart'; +import 'package:_fe_analyzer_shared/src/exhaustiveness/space.dart'; +import 'package:_fe_analyzer_shared/src/exhaustiveness/static_type.dart'; +import 'package:test/test.dart'; + +/// Test that [spaces] is exhaustive over [value]. +void expectExhaustive(Space value, List spaces) { + _expectExhaustive(value, spaces, true); +} + +/// Test that [cases] are exhaustive over [type] if and only if all cases are +/// included and that all subsets of the cases are not exhaustive. +void expectExhaustiveOnlyAll(StaticType type, List cases) { + _testCases(type, cases, true); +} + +/// Test that [cases] are not exhaustive over [type]. Also test that omitting +/// each case is still not exhaustive. +void expectNeverExhaustive(StaticType type, List cases) { + _testCases(type, cases, false); +} + +/// Test that [spaces] is not exhaustive over [value]. +void expectNotExhaustive(Space value, List spaces) { + _expectExhaustive(value, spaces, false); +} + +Map fieldsToSpace(Map fields) => + fields.map((key, value) => MapEntry(key, parseSpace(value))); + +Space parseSpace(Object object) { + if (object is Space) return object; + if (object == '∅') return Space.empty; + if (object is StaticType) return Space(object); + if (object is List) { + return Space.union(object.map(parseSpace).toList()); + } + if (object is Map) { + return Space.record(fieldsToSpace(object)); + } + + throw ArgumentError('Invalid space $object'); +} + +/// Parse a list of spaces using [parseSpace]. +List parseSpaces(List objects) => + objects.map(parseSpace).toList(); + +/// Make a record space with the given fields. +Space rec({Object? w, Object? x, Object? y, Object? z}) => Space.record({ + if (w != null) 'w': parseSpace(w), + if (x != null) 'x': parseSpace(x), + if (y != null) 'y': parseSpace(y), + if (z != null) 'z': parseSpace(z) + }); + +/// Make a [Space] with [type] and [fields]. +Space ty(StaticType type, Map fields) => + Space(type, fieldsToSpace(fields)); + +void _checkExhaustive(Space value, List spaces, bool expectation) { + var actual = isExhaustive(value, spaces); + if (expectation != actual) { + if (expectation) { + fail('Expected $spaces to cover $value but did not.'); + } else { + fail('Expected $spaces to not cover $value but did.'); + } + } +} + +void _expectExhaustive(Space value, List spaces, bool expectation) { + test( + '$value - ${spaces.join(' - ')} ${expectation ? 'is' : 'is not'} ' + 'exhaustive', () { + _checkExhaustive(value, spaces, expectation); + }); +} + +/// Test that [cases] are not exhaustive over [type]. +void _testCases(StaticType type, List cases, bool expectation) { + var valueSpace = Space(type); + var spaces = parseSpaces(cases); + + test('$type with all cases', () { + _checkExhaustive(valueSpace, spaces, expectation); + }); + + // With any single case removed, should also not be exhaustive. + for (var i = 0; i < spaces.length; i++) { + var filtered = spaces.toList(); + filtered.removeAt(i); + + test('$type without case ${spaces[i]}', () { + _checkExhaustive(valueSpace, filtered, false); + }); + } +} diff --git a/pkg/front_end/test/spell_checking_list_code.txt b/pkg/front_end/test/spell_checking_list_code.txt index 268a80e0fbc..6311cd62762 100644 --- a/pkg/front_end/test/spell_checking_list_code.txt +++ b/pkg/front_end/test/spell_checking_list_code.txt @@ -60,6 +60,8 @@ args arise arising arity +arm +arms artifact artifacts artificial @@ -259,6 +261,7 @@ continuations contra contribute convention +conversely coordinated coordinating core @@ -310,6 +313,7 @@ dec decl decoupled decreases +dedupe deduplication deemed deepest @@ -343,6 +347,8 @@ destination destinations destroy destructive +destructure +destructuring deterministic dev device @@ -438,9 +444,12 @@ execute executor executors exhausted +exhaustively +exhaustiveness existence existentially exp +expanded expando expense experimentation @@ -607,6 +616,7 @@ implying importantly imprecise improperly +improves inapplicable inc incomparable @@ -619,6 +629,7 @@ indexer indexing indirection individual +individually inequality inequivalence inequivalences @@ -643,6 +654,7 @@ integrate intends intentionally interested +interfere interim interior interleaved @@ -651,6 +663,7 @@ internet interop interpolations interrupted +intersections intersects interval intervals @@ -718,6 +731,7 @@ lc ld leafp leeway +left's len lets letting @@ -861,6 +875,7 @@ occurences offending ogham oked +omitting op opens opt @@ -881,6 +896,7 @@ outlined outputs outputting overlap +overlapping overloader overlooked overshadowed @@ -969,6 +985,7 @@ processes processor producers product +profile progresses promo proof @@ -1117,6 +1134,7 @@ rewrote rf rhs ri +right's rightmost risk rn @@ -1159,6 +1177,7 @@ serializable serializables serializer serializers +series serve server serves @@ -1265,6 +1284,7 @@ strict stringified stringify structs +structurally structures stub stubs @@ -1277,15 +1297,22 @@ subexpression subexpression's subexpressions subgraph +subname subnode subnodes subscription subsection subsections +subspace +subspaces subst substed substitutes substitutor +subtracted +subtracting +subtraction +subtracts suffixing suggests suite @@ -1386,6 +1413,7 @@ ts tty tuple twos +type's typeref u ufeff @@ -1423,6 +1451,7 @@ unification unifier unifies unify +uninhabited uninstantiable uninstantiated unintentionally