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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user