Import exhaustiveness checker prototype into _fe_analyzer_shared.

This was developed by rynstrom@google.com as part of the patterns
feature proposal.  Moving it into _fe_analyzer_shared will allow us to
evolve it into the final implementation.

Change-Id: I31bd88fd21e1de60205363c7e6f4c405b8e6c2ab
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/244761
Commit-Queue: Paul Berry <paulberry@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Reviewed-by: Bob Nystrom <rnystrom@google.com>
This commit is contained in:
Paul Berry
2022-05-17 17:46:39 +00:00
committed by Commit Bot
parent 9fb27f9842
commit d9152974ed
22 changed files with 2733 additions and 0 deletions
@@ -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<Object> cases) {
var spaces = parseSpaces(cases);
profile.reset();
print(isExhaustive(Space(type), spaces));
profile.log();
}
@@ -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<Object> 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)');
}
}
@@ -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<String> 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;
}
@@ -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<Space> 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<Space> cases) {
List<String> errors = <String>[];
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');
}
@@ -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<String> fieldNames =
{...left.fields.keys, ...right.fields.keys}.toList();
// Sorting isn't needed for correctness, just to make the tests less brittle.
fieldNames.sort();
Map<String, Space> fields = <String, Space>{};
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);
}
@@ -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<String> 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);
}
@@ -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 = <String, int>{};
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<String> names = _counts.keys.toList();
names.sort();
int nameLength =
names.fold<int>(0, (length, name) => max(length, name.length));
int countLength = _counts.values
.fold<int>(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();
}
}
@@ -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<String, Space> 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<String, Space> fields = const {}]) =>
new ExtractSpace._(type, fields);
factory Space.record([Map<String, Space> fields = const {}]) =>
new Space(StaticType.top, fields);
factory Space.union(List<Space> arms) {
// Simplify the arms if possible.
List<Space> allArms = <Space>[];
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<Space> 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() => '';
}
@@ -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<String, StaticType> _fields;
final List<StaticType> _supertypes = [];
final List<StaticType> _subtypes = [];
StaticType(this.name,
{this.isSealed = false,
List<StaticType>? inherits,
Map<String, StaticType> 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<String, StaticType> get fields {
return {
for (StaticType supertype in _supertypes) ...supertype.fields,
..._fields
};
}
bool get isNullable => _underlying != null;
/// The immediate subtypes of this type.
Iterable<StaticType> 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;
}
@@ -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<StaticType> 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<String> fieldNames,
Map<String, Space> leftFields, Map<String, Space> 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<String> fieldNames =
{...left.fields.keys, ...right.fields.keys}.toList();
List<Space> spaces = <Space>[];
// If the left type is in a sealed hierarchy, expanding it to its subtypes
// might let us calculate the subtraction more precisely.
List<StaticType> 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<Space> _subtractExtractAtType(StaticType type, ExtractSpace left,
ExtractSpace right, List<String> 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<String, Space> leftFields = <String, Space>{};
Map<String, Space> rightFields = <String, Space>{};
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<String, Space> fixed = <String, Space>{};
Map<String, Space> changedDifference = <String, Space>{};
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<String> changedFields = changedDifference.keys.toList();
List<Space> spaces = <Space>[];
for (int i = 0; i < changedFields.length; i++) {
Map<String, Space> 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;
}
@@ -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);
}
@@ -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);
}
@@ -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);
}
@@ -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);
}
@@ -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},
]);
});
}
@@ -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))),
]);
});
}
@@ -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<StaticType>, String) _makeTestFunction(
List<StaticType> 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);
}
}
};
}
@@ -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<Object> cases,
[String errors = '']) {
expect(reportErrors(valueType, parseSpaces(cases)), errors);
}
@@ -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]));
});
}
@@ -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);
});
}
@@ -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<Space> 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<Object> 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<Object> cases) {
_testCases(type, cases, false);
}
/// Test that [spaces] is not exhaustive over [value].
void expectNotExhaustive(Space value, List<Space> spaces) {
_expectExhaustive(value, spaces, false);
}
Map<String, Space> fieldsToSpace(Map<String, Object> 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<Object>) {
return Space.union(object.map(parseSpace).toList());
}
if (object is Map<String, Object>) {
return Space.record(fieldsToSpace(object));
}
throw ArgumentError('Invalid space $object');
}
/// Parse a list of spaces using [parseSpace].
List<Space> parseSpaces(List<Object> 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<String, Object> fields) =>
Space(type, fieldsToSpace(fields));
void _checkExhaustive(Space value, List<Space> 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<Space> 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<Object> 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);
});
}
}
@@ -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