mirror of
https://github.com/dart-lang/sdk
synced 2024-09-16 00:19:48 +00:00
[_fe_analyzer_shared] Remove old Space model
This remove the [Space] class from the old algorithm and instead encodes the patterns into the new model. The [Pattern] and [Patterns] classes of the new model have been renamed to [SingleSpace] and [Space], respectively, and a [Path] class is added to track the path property used in the model of the new algorithm. Change-Id: I0c86c738807030be2f9b59f3aefb5bfcf5bbaeee Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/286501 Reviewed-by: Konstantin Shcheglov <scheglov@google.com> Commit-Queue: Johnni Winther <johnniwinther@google.com>
This commit is contained in:
parent
21bf9f4ba6
commit
12ea77b96e
|
@ -3,7 +3,6 @@
|
|||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
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 'package:_fe_analyzer_shared/src/exhaustiveness/witness.dart';
|
||||
|
||||
|
@ -113,6 +112,6 @@ void main() {
|
|||
void expectExhaustiveOnlyAll(StaticType type, List<Map<String, Object>> cases) {
|
||||
var spaces = cases.map((c) => ty(type, c)).toList();
|
||||
profile.reset();
|
||||
print(isExhaustive(Space(type), spaces));
|
||||
print(isExhaustive(Space(const Path.root(), type), spaces));
|
||||
profile.log();
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
|
||||
import 'dart:math';
|
||||
|
||||
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/witness.dart';
|
||||
|
||||
|
@ -110,7 +109,7 @@ void main() {
|
|||
/// 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<Map<String, Object>> cases) {
|
||||
var valueSpace = Space(type);
|
||||
var valueSpace = Space(const Path.root(), type);
|
||||
var caseSpaces = cases.map((c) => ty(type, c)).toList();
|
||||
|
||||
const trials = 100;
|
||||
|
|
|
@ -1,71 +0,0 @@
|
|||
// 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;
|
||||
}
|
|
@ -2,9 +2,8 @@
|
|||
// 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 'witness.dart' as witness;
|
||||
import 'witness.dart';
|
||||
|
||||
/// Indicates whether the "fallback" exhaustiveness algorithm (based on flow
|
||||
/// analysis) should be used instead of full exhaustiveness. This is a
|
||||
|
@ -15,20 +14,6 @@ import 'witness.dart' as witness;
|
|||
/// exhaustiveness algorithm) when it is no longer needed.
|
||||
bool useFallbackExhaustivenessAlgorithm = true;
|
||||
|
||||
/// 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.
|
||||
|
||||
List<ExhaustivenessError> reportErrors(StaticType valueType, List<Space> cases,
|
||||
[List<Space>? remainingSpaces]) {
|
||||
return witness.reportErrors(valueType, cases);
|
||||
}
|
||||
|
||||
class ExhaustivenessError {}
|
||||
|
||||
class NonExhaustiveError implements ExhaustivenessError {
|
||||
|
@ -42,7 +27,7 @@ class NonExhaustiveError implements ExhaustivenessError {
|
|||
|
||||
@override
|
||||
String toString() =>
|
||||
'$valueType is not exhaustively matched by ${new Space.union(cases)}.';
|
||||
'$valueType is not exhaustively matched by ${cases.join('|')}.';
|
||||
}
|
||||
|
||||
class UnreachableCaseError implements ExhaustivenessError {
|
||||
|
|
|
@ -1,156 +0,0 @@
|
|||
// 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.nullableObject] 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.nullableObject && fields.isEmpty;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
if (isTop) return '()';
|
||||
if (this == Space.empty) return '∅';
|
||||
|
||||
if (type.isRecord) {
|
||||
StringBuffer buffer = new StringBuffer();
|
||||
buffer.write('(');
|
||||
bool first = true;
|
||||
type.fields.forEach((String name, StaticType staticType) {
|
||||
if (!first) buffer.write(', ');
|
||||
// TODO(johnniwinther): Ensure using Dart syntax for positional fields.
|
||||
buffer.write('$name: ${fields[name] ?? staticType}');
|
||||
first = false;
|
||||
});
|
||||
|
||||
buffer.write(')');
|
||||
return buffer.toString();
|
||||
} else {
|
||||
// If there are no fields, just show the type.
|
||||
if (fields.isEmpty) return type.name;
|
||||
|
||||
StringBuffer buffer = new StringBuffer();
|
||||
buffer.write(type.name);
|
||||
|
||||
buffer.write('(');
|
||||
bool first = true;
|
||||
|
||||
fields.forEach((String name, Space space) {
|
||||
if (!first) buffer.write(', ');
|
||||
buffer.write('$name: $space');
|
||||
first = false;
|
||||
});
|
||||
|
||||
buffer.write(')');
|
||||
return buffer.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(paulberry, rnystrom): List spaces.
|
||||
|
||||
abstract class Space {
|
||||
/// The uninhabited space.
|
||||
static final Space empty = new Space(StaticType.neverType);
|
||||
|
||||
/// The space containing everything.
|
||||
static final Space top = new Space(StaticType.nullableObject);
|
||||
|
||||
/// The space containing only `null`.
|
||||
static final Space nullSpace = new Space(StaticType.nullType);
|
||||
|
||||
factory Space(StaticType type, [Map<String, Space> fields = const {}]) =>
|
||||
new ExtractSpace._(type, fields);
|
||||
|
||||
factory Space.union(List<Space> arms) {
|
||||
// Simplify the arms if possible.
|
||||
List<ExtractSpace> allArms = <ExtractSpace>[];
|
||||
|
||||
void addSpace(ExtractSpace 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 (ExtractSpace arm in space.arms) {
|
||||
addSpace(arm);
|
||||
}
|
||||
} else {
|
||||
addSpace(space as ExtractSpace);
|
||||
}
|
||||
}
|
||||
|
||||
if (allArms.isEmpty) return empty;
|
||||
if (allArms.length == 1) return allArms.first;
|
||||
if (allArms.length == 2) {
|
||||
if (allArms[0].type == StaticType.nullType &&
|
||||
allArms[0].fields.isEmpty &&
|
||||
allArms[1].fields.isEmpty) {
|
||||
return new Space(allArms[1].type.nullable);
|
||||
} else if (allArms[1].type == StaticType.nullType &&
|
||||
allArms[1].fields.isEmpty &&
|
||||
allArms[0].fields.isEmpty) {
|
||||
return new Space(allArms[0].type.nullable);
|
||||
}
|
||||
}
|
||||
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<ExtractSpace> arms;
|
||||
|
||||
UnionSpace._(this.arms) : super._() {
|
||||
assert(arms.length > 1);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => arms.join('|');
|
||||
}
|
|
@ -3,8 +3,8 @@
|
|||
// 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/witness.dart';
|
||||
|
||||
import 'space.dart';
|
||||
import 'static_type.dart';
|
||||
|
||||
/// Tags used for id-testing of exhaustiveness.
|
||||
|
@ -15,11 +15,10 @@ class Tags {
|
|||
static const String space = 'space';
|
||||
static const String subtypes = 'subtypes';
|
||||
static const String expandedSubtypes = 'expandedSubtypes';
|
||||
static const String remaining = 'remaining';
|
||||
}
|
||||
|
||||
/// Returns a textual representation for [space] used for testing.
|
||||
String spaceToText(Space space) => space.toString();
|
||||
String spacesToText(Space space) => space.toString();
|
||||
|
||||
/// Returns a textual representation for [fields] used for testing.
|
||||
String fieldsToText(Map<String, StaticType> fields) {
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
|
||||
import 'exhaustive.dart';
|
||||
import 'profile.dart' as profile;
|
||||
import 'space.dart';
|
||||
import 'static_type.dart';
|
||||
|
||||
/// Returns `true` if [caseSpaces] exhaustively covers all possible values of
|
||||
|
@ -22,40 +21,35 @@ bool isExhaustive(Space valueSpace, List<Space> caseSpaces) {
|
|||
/// Returns an empty list if all cases are reachable and the cases are
|
||||
/// exhaustive.
|
||||
List<ExhaustivenessError> reportErrors(
|
||||
StaticType valueType, List<Space> caseSpaces) {
|
||||
StaticType valueType, List<Space> cases) {
|
||||
List<ExhaustivenessError> errors = <ExhaustivenessError>[];
|
||||
|
||||
Pattern valuePattern = new Pattern(valueType, {}, []);
|
||||
List<List<Patterns>> cases =
|
||||
caseSpaces.map((space) => [_spaceToPatterns(space)]).toList();
|
||||
Space valuePattern = new Space(const Path.root(), valueType);
|
||||
List<List<Space>> caseRows = cases.map((space) => [space]).toList();
|
||||
|
||||
for (int i = 1; i < cases.length; i++) {
|
||||
for (int i = 1; i < caseRows.length; i++) {
|
||||
// See if this case is covered by previous ones.
|
||||
if (_unmatched(cases.sublist(0, i), cases[i]) == null) {
|
||||
errors.add(new UnreachableCaseError(valueType, caseSpaces, i));
|
||||
if (_unmatched(caseRows.sublist(0, i), caseRows[i]) == null) {
|
||||
errors.add(new UnreachableCaseError(valueType, cases, i));
|
||||
}
|
||||
}
|
||||
|
||||
String? witness = _unmatched(cases, [
|
||||
new Patterns([valuePattern])
|
||||
]);
|
||||
String? witness = _unmatched(caseRows, [valuePattern]);
|
||||
if (witness != null) {
|
||||
errors.add(new NonExhaustiveError(valueType, caseSpaces, witness));
|
||||
errors.add(new NonExhaustiveError(valueType, cases, witness));
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
/// Determines if [caseSpaces] is exhaustive over all values contained by
|
||||
/// Determines if [cases] is exhaustive over all values contained by
|
||||
/// [valueSpace]. If so, returns `null`. Otherwise, returns a string describing
|
||||
/// an example of one value that isn't matched by anything in [caseSpaces].
|
||||
String? checkExhaustiveness(Space valueSpace, List<Space> caseSpaces) {
|
||||
/// an example of one value that isn't matched by anything in [cases].
|
||||
String? checkExhaustiveness(Space valueSpace, List<Space> cases) {
|
||||
// TODO(johnniwinther): Perform reachability checking.
|
||||
Patterns value = _spaceToPatterns(valueSpace);
|
||||
List<List<Patterns>> cases =
|
||||
caseSpaces.map((space) => [_spaceToPatterns(space)]).toList();
|
||||
List<List<Space>> caseRows = cases.map((space) => [space]).toList();
|
||||
|
||||
String? witness = _unmatched(cases, [value]);
|
||||
String? witness = _unmatched(caseRows, [valueSpace]);
|
||||
|
||||
// Uncomment this to have it print out the witness for non-exhaustive matches.
|
||||
// if (witness != null) print(witness);
|
||||
|
@ -63,40 +57,16 @@ String? checkExhaustiveness(Space valueSpace, List<Space> caseSpaces) {
|
|||
return witness;
|
||||
}
|
||||
|
||||
/// Convert the prototype's original [Space] representation to the [Patterns]
|
||||
/// representation used here.
|
||||
///
|
||||
/// This is only a convenience to run the existing test code which creates
|
||||
/// [Space]s using the new algorithm.
|
||||
// TODO(johnniwinther): Encode Dart patterns directly into [Patterns].
|
||||
Patterns _spaceToPatterns(Space space, [List<String> path = const []]) {
|
||||
if (space is UnionSpace) {
|
||||
return new Patterns(
|
||||
space.arms.map((arm) => _extractSpaceToPattern(arm, path)).toList());
|
||||
}
|
||||
return new Patterns([_extractSpaceToPattern(space as ExtractSpace, path)]);
|
||||
}
|
||||
|
||||
/// Convert the prototype's original [ExtractSpace] representation into the
|
||||
/// [Pattern] representation used here.
|
||||
Pattern _extractSpaceToPattern(ExtractSpace space,
|
||||
[List<String> path = const []]) {
|
||||
Map<String, Patterns> fields = {
|
||||
for (String name in space.fields.keys)
|
||||
name: _spaceToPatterns(space.fields[name]!, [...path, name])
|
||||
};
|
||||
return new Pattern(space.type, fields, path);
|
||||
}
|
||||
|
||||
/// Tries to find a pattern containing at least one value matched by
|
||||
/// [valuePatterns] that is not matched by any of the patterns in [caseRows].
|
||||
///
|
||||
/// If found, returns it. This is a witness example showing that [caseRows] is
|
||||
/// not exhaustive over all values in [valuePatterns]. If it returns `null`,
|
||||
/// then [caseRows] exhaustively covers [valuePatterns].
|
||||
String? _unmatched(List<List<Patterns>> caseRows, List<Patterns> valuePatterns,
|
||||
String? _unmatched(List<List<Space>> caseRows, List<Space> valuePatterns,
|
||||
[List<Predicate> witnessPredicates = const []]) {
|
||||
assert(caseRows.every((element) => element.length == valuePatterns.length));
|
||||
assert(caseRows.every((element) => element.length == valuePatterns.length),
|
||||
"Value patterns: $valuePatterns, case rows: $caseRows.");
|
||||
profile.count('_unmatched');
|
||||
// If there are no more columns, then we've tested all the predicates we have
|
||||
// to test.
|
||||
|
@ -112,9 +82,9 @@ String? _unmatched(List<List<Patterns>> caseRows, List<Patterns> valuePatterns,
|
|||
}
|
||||
|
||||
// Look down the first column of tests.
|
||||
Patterns firstValuePatterns = valuePatterns[0];
|
||||
Space firstValuePatterns = valuePatterns[0];
|
||||
|
||||
for (Pattern firstValuePattern in firstValuePatterns.patterns) {
|
||||
for (SingleSpace firstValuePattern in firstValuePatterns.singleSpaces) {
|
||||
// TODO(johnniwinther): Right now, this brute force expands all subtypes of
|
||||
// sealed types and considers them individually. It would be faster to look
|
||||
// at the types of the patterns in the first column of each row and only
|
||||
|
@ -124,7 +94,7 @@ String? _unmatched(List<List<Patterns>> caseRows, List<Patterns> valuePatterns,
|
|||
List<StaticType> subtypes = expandSealedSubtypes(firstValuePattern.type);
|
||||
for (StaticType subtype in subtypes) {
|
||||
String? result = _filterByType(subtype, caseRows, firstValuePattern,
|
||||
valuePatterns, witnessPredicates);
|
||||
valuePatterns, witnessPredicates, firstValuePatterns.path);
|
||||
|
||||
// If we found a witness for a subtype that no rows match, then we can
|
||||
// stop. There may be others but we don't need to find more.
|
||||
|
@ -139,15 +109,16 @@ String? _unmatched(List<List<Patterns>> caseRows, List<Patterns> valuePatterns,
|
|||
|
||||
String? _filterByType(
|
||||
StaticType type,
|
||||
List<List<Patterns>> caseRows,
|
||||
Pattern firstValuePattern,
|
||||
List<Patterns> valuePatterns,
|
||||
List<Predicate> witnessPredicates) {
|
||||
List<List<Space>> caseRows,
|
||||
SingleSpace firstSingleSpaceValue,
|
||||
List<Space> valueSpaces,
|
||||
List<Predicate> witnessPredicates,
|
||||
Path path) {
|
||||
profile.count('_filterByType');
|
||||
// Extend the witness with the type we're matching.
|
||||
List<Predicate> extendedWitness = [
|
||||
...witnessPredicates,
|
||||
new Predicate(firstValuePattern.path, type)
|
||||
new Predicate(path, type)
|
||||
];
|
||||
|
||||
// 1) Discard any rows that might not match because the column's type isn't a
|
||||
|
@ -157,16 +128,16 @@ String? _filterByType(
|
|||
//
|
||||
// 2) Expand any unions in the first column. This can (deliberately) produce
|
||||
// duplicate rows in remainingRows.
|
||||
List<Pattern> remainingRowFirstPatterns = [];
|
||||
List<List<Patterns>> remainingRows = [];
|
||||
for (List<Patterns> row in caseRows) {
|
||||
Patterns firstPatterns = row[0];
|
||||
List<SingleSpace> remainingRowFirstSingleSpaces = [];
|
||||
List<List<Space>> remainingRows = [];
|
||||
for (List<Space> row in caseRows) {
|
||||
Space firstSpace = row[0];
|
||||
|
||||
for (Pattern firstPattern in firstPatterns.patterns) {
|
||||
for (SingleSpace firstSingleSpace in firstSpace.singleSpaces) {
|
||||
// If the row's type is a supertype of the value pattern's type then it
|
||||
// must match.
|
||||
if (type.isSubtypeOf(firstPattern.type)) {
|
||||
remainingRowFirstPatterns.add(firstPattern);
|
||||
if (type.isSubtypeOf(firstSingleSpace.type)) {
|
||||
remainingRowFirstSingleSpaces.add(firstSingleSpace);
|
||||
remainingRows.add(row);
|
||||
}
|
||||
}
|
||||
|
@ -176,8 +147,8 @@ String? _filterByType(
|
|||
// some of those may also have field subpatterns. If so, lift those out so we
|
||||
// can recurse into them.
|
||||
Set<String> fieldNames = {
|
||||
...firstValuePattern.fields.keys,
|
||||
for (Pattern firstPattern in remainingRowFirstPatterns)
|
||||
...firstSingleSpaceValue.fields.keys,
|
||||
for (SingleSpace firstPattern in remainingRowFirstSingleSpaces)
|
||||
...firstPattern.fields.keys
|
||||
};
|
||||
|
||||
|
@ -186,55 +157,47 @@ String? _filterByType(
|
|||
|
||||
// Remove the first column from the value list and replace it with any
|
||||
// expanded fields.
|
||||
valuePatterns = [
|
||||
..._expandFields(sorted, firstValuePattern, type),
|
||||
...valuePatterns.skip(1)
|
||||
valueSpaces = [
|
||||
..._expandFields(sorted, firstSingleSpaceValue, type, path),
|
||||
...valueSpaces.skip(1)
|
||||
];
|
||||
|
||||
// Remove the first column from each row and replace it with any expanded
|
||||
// fields.
|
||||
for (int i = 0; i < remainingRows.length; i++) {
|
||||
remainingRows[i] = [
|
||||
..._expandFields(sorted, remainingRowFirstPatterns[i],
|
||||
remainingRowFirstPatterns[i].type),
|
||||
..._expandFields(sorted, remainingRowFirstSingleSpaces[i],
|
||||
remainingRowFirstSingleSpaces[i].type, path),
|
||||
...remainingRows[i].skip(1)
|
||||
];
|
||||
}
|
||||
|
||||
// Proceed to the next column.
|
||||
return _unmatched(remainingRows, valuePatterns, extendedWitness);
|
||||
return _unmatched(remainingRows, valueSpaces, extendedWitness);
|
||||
}
|
||||
|
||||
/// Given a list of [fieldNames] and a [pattern], generates a list of patterns,
|
||||
/// one for each named field.
|
||||
/// Given a list of [fieldNames] and a [singleSpace], generates a list of
|
||||
/// single spaces, one for each named field.
|
||||
///
|
||||
/// When pattern contains a field with that name, extracts it into the
|
||||
/// resulting list. Otherwise, the pattern doesn't care
|
||||
/// about that field, so inserts a default pattern that matches all values for
|
||||
/// the field.
|
||||
/// When [singleSpace] contains a field with that name, extracts it into the
|
||||
/// resulting list. Otherwise, the [singleSpace] doesn't care about that field,
|
||||
/// so inserts a default [Space] that matches all values for the field.
|
||||
///
|
||||
/// In other words, this unpacks a set of fields so that the main algorithm can
|
||||
/// add them to the worklist.
|
||||
List<Patterns> _expandFields(
|
||||
List<String> fieldNames, Pattern pattern, StaticType type) {
|
||||
List<Space> _expandFields(List<String> fieldNames, SingleSpace singleSpace,
|
||||
StaticType type, Path path) {
|
||||
profile.count('_expandFields');
|
||||
List<Patterns> result = <Patterns>[];
|
||||
List<Space> result = <Space>[];
|
||||
for (String fieldName in fieldNames) {
|
||||
Patterns? field = pattern.fields[fieldName];
|
||||
Space? field = singleSpace.fields[fieldName];
|
||||
if (field != null) {
|
||||
result.add(field);
|
||||
} else {
|
||||
// This pattern doesn't test this field, so add a pattern for the
|
||||
// field that matches all values. This way the columns stay aligned.
|
||||
result.add(
|
||||
new Patterns([
|
||||
new Pattern(
|
||||
type.fields[fieldName] ?? StaticType.nullableObject,
|
||||
{},
|
||||
[...pattern.path, fieldName],
|
||||
)
|
||||
]),
|
||||
);
|
||||
result.add(new Space(path.add(fieldName),
|
||||
type.fields[fieldName] ?? StaticType.nullableObject));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -256,43 +219,143 @@ List<StaticType> expandSealedSubtypes(StaticType type) {
|
|||
/// The main pattern 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.
|
||||
/// be [StaticType.nullableObject] to indicate that it doesn't filter by type.
|
||||
///
|
||||
/// It may also contain zero or more named fields. The pattern then only matches
|
||||
/// values where the field values are matched by the corresponding field
|
||||
/// patterns.
|
||||
// TODO(johnniwinther): Rename this to avoid name clash with Pattern from
|
||||
// dart:core.
|
||||
class Pattern {
|
||||
class SingleSpace {
|
||||
static final SingleSpace empty = new SingleSpace(StaticType.neverType);
|
||||
|
||||
/// The type of values the pattern matches.
|
||||
final StaticType type;
|
||||
|
||||
/// Any field subpatterns the pattern matches.
|
||||
final Map<String, Patterns> fields;
|
||||
final Map<String, Space> fields;
|
||||
|
||||
/// The path of getters that led from the original matched value to value
|
||||
/// matched by this pattern. Used to generate a human-readable witness.
|
||||
final List<String> path;
|
||||
SingleSpace(this.type, {this.fields = const {}});
|
||||
|
||||
Pattern(this.type, this.fields, this.path);
|
||||
@override
|
||||
late final int hashCode = Object.hash(
|
||||
type,
|
||||
Object.hashAllUnordered(fields.keys),
|
||||
Object.hashAllUnordered(fields.values));
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other is! SingleSpace) return false;
|
||||
if (type != other.type) return false;
|
||||
if (fields.length != other.fields.length) return false;
|
||||
if (fields.isNotEmpty) {
|
||||
for (MapEntry<String, Space> entry in fields.entries) {
|
||||
if (entry.value != other.fields[entry.key]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Pattern(type=$type,fields=$fields,path=$path)';
|
||||
if (type == StaticType.nullableObject && fields.isEmpty) return '()';
|
||||
if (this == StaticType.neverType && fields.isEmpty) return '∅';
|
||||
|
||||
if (type.isRecord) {
|
||||
StringBuffer buffer = new StringBuffer();
|
||||
buffer.write('(');
|
||||
bool first = true;
|
||||
type.fields.forEach((String name, StaticType staticType) {
|
||||
if (!first) buffer.write(', ');
|
||||
// TODO(johnniwinther): Ensure using Dart syntax for positional fields.
|
||||
buffer.write('$name: ${fields[name] ?? staticType}');
|
||||
first = false;
|
||||
});
|
||||
|
||||
buffer.write(')');
|
||||
return buffer.toString();
|
||||
} else {
|
||||
// If there are no fields, just show the type.
|
||||
if (fields.isEmpty) return type.name;
|
||||
|
||||
StringBuffer buffer = new StringBuffer();
|
||||
buffer.write(type.name);
|
||||
|
||||
buffer.write('(');
|
||||
bool first = true;
|
||||
|
||||
fields.forEach((String name, Space space) {
|
||||
if (!first) buffer.write(', ');
|
||||
buffer.write('$name: $space');
|
||||
first = false;
|
||||
});
|
||||
|
||||
buffer.write(')');
|
||||
return buffer.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A union of [Pattern]s.
|
||||
/// A set of runtime values encoded as a union of [SingleSpace]s.
|
||||
///
|
||||
/// This is used to support logical-or patterns without having to eagerly
|
||||
/// expand the subpatterns in the parent context.
|
||||
class Patterns {
|
||||
final List<Pattern> patterns;
|
||||
class Space {
|
||||
/// The path of getters that led from the original matched value to value
|
||||
/// matched by this pattern. Used to generate a human-readable witness.
|
||||
final Path path;
|
||||
|
||||
Patterns(this.patterns);
|
||||
final List<SingleSpace> singleSpaces;
|
||||
|
||||
/// Create an empty space.
|
||||
Space.empty(this.path) : singleSpaces = [SingleSpace.empty];
|
||||
|
||||
Space(Path path, StaticType type, {Map<String, Space> fields = const {}})
|
||||
: this._(path, [new SingleSpace(type, fields: fields)]);
|
||||
|
||||
Space._(this.path, this.singleSpaces);
|
||||
|
||||
Space union(Space other) {
|
||||
Set<SingleSpace> singleSpacesSet = {};
|
||||
|
||||
for (SingleSpace singleSpace in singleSpaces) {
|
||||
// Discard empty space.
|
||||
if (singleSpace == SingleSpace.empty) {
|
||||
continue;
|
||||
}
|
||||
|
||||
singleSpacesSet.add(singleSpace);
|
||||
}
|
||||
|
||||
for (SingleSpace singleSpace in other.singleSpaces) {
|
||||
// Discard empty space.
|
||||
if (singleSpace == SingleSpace.empty) {
|
||||
continue;
|
||||
}
|
||||
|
||||
singleSpacesSet.add(singleSpace);
|
||||
}
|
||||
|
||||
List<SingleSpace> singleSpacesList = singleSpacesSet.toList();
|
||||
if (singleSpacesSet.isEmpty) {
|
||||
singleSpacesList.add(SingleSpace.empty);
|
||||
} else if (singleSpacesList.length == 2) {
|
||||
if (singleSpacesList[0].type == StaticType.nullType &&
|
||||
singleSpacesList[0].fields.isEmpty &&
|
||||
singleSpacesList[1].fields.isEmpty) {
|
||||
singleSpacesList = [new SingleSpace(singleSpacesList[1].type.nullable)];
|
||||
} else if (singleSpacesList[1].type == StaticType.nullType &&
|
||||
singleSpacesList[1].fields.isEmpty &&
|
||||
singleSpacesList[0].fields.isEmpty) {
|
||||
singleSpacesList = [new SingleSpace(singleSpacesList[0].type.nullable)];
|
||||
}
|
||||
}
|
||||
return new Space._(path, singleSpacesList);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => 'Patterns($patterns)';
|
||||
String toString() => singleSpaces.join('|');
|
||||
}
|
||||
|
||||
/// Describes a pattern that matches the value or a field accessed from it.
|
||||
|
@ -301,7 +364,7 @@ class Patterns {
|
|||
class Predicate {
|
||||
/// The path of getters that led from the original matched value to the value
|
||||
/// tested by this predicate.
|
||||
final List<String> path;
|
||||
final Path path;
|
||||
|
||||
/// The type this predicate tests.
|
||||
// TODO(johnniwinther): In order to model exhaustiveness on enum types,
|
||||
|
@ -336,7 +399,7 @@ String _witnessString(List<Predicate> predicates) {
|
|||
|
||||
for (Predicate predicate in predicates) {
|
||||
Witness here = witness;
|
||||
for (String field in predicate.path) {
|
||||
for (String field in predicate.path.toList()) {
|
||||
here = here.fields.putIfAbsent(field, () => new Witness());
|
||||
}
|
||||
here.type = predicate.type;
|
||||
|
@ -379,3 +442,82 @@ class Witness {
|
|||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/// A path that describes location of a [SingleSpace] from the root of
|
||||
/// enclosing [Space].
|
||||
abstract class Path {
|
||||
const Path();
|
||||
|
||||
/// Create root path.
|
||||
const factory Path.root() = _Root;
|
||||
|
||||
/// Returns a path that adds a step by the [name] to the current path.
|
||||
Path add(String name) => new _Step(this, name);
|
||||
|
||||
void _toList(List<String> list);
|
||||
|
||||
/// Returns a list of the names from the root to this path.
|
||||
List<String> toList();
|
||||
}
|
||||
|
||||
/// The root path object.
|
||||
class _Root extends Path {
|
||||
const _Root();
|
||||
|
||||
@override
|
||||
void _toList(List<String> list) {}
|
||||
|
||||
@override
|
||||
List<String> toList() => const [];
|
||||
|
||||
@override
|
||||
int get hashCode => 1729;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is _Root;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => '@';
|
||||
}
|
||||
|
||||
/// A single step in a path that holds the [parent] pointer the [name] for the
|
||||
/// step.
|
||||
class _Step extends Path {
|
||||
final Path parent;
|
||||
final String name;
|
||||
|
||||
_Step(this.parent, this.name);
|
||||
|
||||
@override
|
||||
List<String> toList() {
|
||||
List<String> list = [];
|
||||
_toList(list);
|
||||
return list;
|
||||
}
|
||||
|
||||
@override
|
||||
void _toList(List<String> list) {
|
||||
parent._toList(list);
|
||||
list.add(name);
|
||||
}
|
||||
|
||||
@override
|
||||
late final int hashCode = Object.hash(parent, name);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
return other is _Step && name == other.name && parent == other.parent;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
if (parent is _Root) {
|
||||
return name;
|
||||
} else {
|
||||
return '$parent.$name';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
// 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/witness.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import 'env.dart';
|
||||
|
@ -188,10 +188,10 @@ Function(List<StaticType>, String) _makeTestFunction(
|
|||
var letters = 'ABCDEF';
|
||||
|
||||
return (types, covered) {
|
||||
var spaces = types.map((type) => Space(type)).toList();
|
||||
var spaces = types.map((type) => Space(const Path.root(), type)).toList();
|
||||
|
||||
for (var i = 0; i < allTypes.length; i++) {
|
||||
var value = Space(allTypes[i]);
|
||||
var value = Space(const Path.root(), allTypes[i]);
|
||||
if (covered.contains(letters[i])) {
|
||||
expectExhaustive(value, spaces);
|
||||
} else {
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
// 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:_fe_analyzer_shared/src/exhaustiveness/witness.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import 'env.dart';
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
// 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/witness.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
@ -29,15 +28,23 @@ 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)));
|
||||
Map<String, Space> fieldsToSpace(Map<String, Object> fields, Path path) =>
|
||||
fields.map((key, value) => MapEntry(key, parseSpace(value, path.add(key))));
|
||||
|
||||
Space parseSpace(Object object) {
|
||||
Space parseSpace(Object object, [Path path = const Path.root()]) {
|
||||
if (object is Space) return object;
|
||||
if (object == '∅') return Space.empty;
|
||||
if (object is StaticType) return Space(object);
|
||||
if (object == '∅') return Space(path, StaticType.neverType);
|
||||
if (object is StaticType) return Space(path, object);
|
||||
if (object is List<Object>) {
|
||||
return Space.union(object.map(parseSpace).toList());
|
||||
Space? spaces;
|
||||
for (Object element in object) {
|
||||
if (spaces == null) {
|
||||
spaces = parseSpace(element, path);
|
||||
} else {
|
||||
spaces = spaces.union(parseSpace(element, path));
|
||||
}
|
||||
}
|
||||
return spaces ?? new Space.empty(path);
|
||||
}
|
||||
throw ArgumentError('Invalid space $object');
|
||||
}
|
||||
|
@ -47,8 +54,9 @@ List<Space> parseSpaces(List<Object> objects) =>
|
|||
objects.map(parseSpace).toList();
|
||||
|
||||
/// Make a [Space] with [type] and [fields].
|
||||
Space ty(StaticType type, Map<String, Object> fields) =>
|
||||
Space(type, fieldsToSpace(fields));
|
||||
Space ty(StaticType type, Map<String, Object> fields,
|
||||
[Path path = const Path.root()]) =>
|
||||
Space(path, type, fields: fieldsToSpace(fields, path));
|
||||
|
||||
void _checkExhaustive(Space value, List<Space> spaces, bool expectation) {
|
||||
var actual = isExhaustive(value, spaces);
|
||||
|
@ -71,7 +79,7 @@ void _expectExhaustive(Space value, List<Space> spaces, bool expectation) {
|
|||
|
||||
/// Test that [cases] are not exhaustive over [type].
|
||||
void _testCases(StaticType type, List<Object> cases, bool expectation) {
|
||||
var valueSpace = Space(type);
|
||||
var valueSpace = Space(const Path.root(), type);
|
||||
var spaces = parseSpaces(cases);
|
||||
|
||||
test('$type with all cases', () {
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
import 'dart:collection';
|
||||
|
||||
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/witness.dart';
|
||||
import 'package:analyzer/dart/analysis/declared_variables.dart';
|
||||
import 'package:analyzer/dart/analysis/features.dart';
|
||||
import 'package:analyzer/dart/ast/ast.dart';
|
||||
|
@ -784,10 +784,12 @@ class ConstantVerifier extends RecursiveAstVisitor<void> {
|
|||
if (guardedPattern != null) {
|
||||
Space space;
|
||||
if (guardedPattern.whenClause != null) {
|
||||
space = Space(_exhaustivenessCache.getUnknownStaticType());
|
||||
space = Space(
|
||||
const Path.root(), _exhaustivenessCache.getUnknownStaticType());
|
||||
} else {
|
||||
final pattern = guardedPattern.pattern;
|
||||
space = patternConverter.convertPattern(pattern, nonNull: false);
|
||||
space = patternConverter.convertPattern(pattern,
|
||||
nonNull: false, path: const Path.root());
|
||||
}
|
||||
caseNodesWithSpace.add(caseNode);
|
||||
caseSpaces.add(space);
|
||||
|
@ -795,14 +797,10 @@ class ConstantVerifier extends RecursiveAstVisitor<void> {
|
|||
}
|
||||
|
||||
// Prepare for recording data for testing.
|
||||
List<Space>? remainingSpaces;
|
||||
final exhaustivenessDataForTesting = this.exhaustivenessDataForTesting;
|
||||
if (exhaustivenessDataForTesting != null) {
|
||||
remainingSpaces = [];
|
||||
}
|
||||
|
||||
// Compute and report errors.
|
||||
final errors = reportErrors(scrutineeTypeEx, caseSpaces, remainingSpaces);
|
||||
final errors = reportErrors(scrutineeTypeEx, caseSpaces);
|
||||
if (!useFallbackExhaustivenessAlgorithm) {
|
||||
for (final error in errors) {
|
||||
if (error is UnreachableCaseError) {
|
||||
|
@ -832,22 +830,12 @@ class ConstantVerifier extends RecursiveAstVisitor<void> {
|
|||
}
|
||||
|
||||
// Record data for testing.
|
||||
if (exhaustivenessDataForTesting != null && remainingSpaces != null) {
|
||||
assert(remainingSpaces.isEmpty ||
|
||||
remainingSpaces.length == caseSpaces.length + 1);
|
||||
if (exhaustivenessDataForTesting != null) {
|
||||
for (var i = 0; i < caseSpaces.length; i++) {
|
||||
final caseNode = caseNodesWithSpace[i];
|
||||
exhaustivenessDataForTesting.caseSpaces[caseNode] = caseSpaces[i];
|
||||
if (remainingSpaces.isNotEmpty) {
|
||||
exhaustivenessDataForTesting.remainingSpaces[caseNode] =
|
||||
remainingSpaces[i];
|
||||
}
|
||||
}
|
||||
exhaustivenessDataForTesting.switchScrutineeType[node] = scrutineeTypeEx;
|
||||
if (remainingSpaces.isNotEmpty) {
|
||||
exhaustivenessDataForTesting.remainingSpaces[node] =
|
||||
remainingSpaces.last;
|
||||
}
|
||||
for (var error in errors) {
|
||||
if (error is UnreachableCaseError) {
|
||||
exhaustivenessDataForTesting.errors[caseNodesWithSpace[error.index]] =
|
||||
|
|
|
@ -3,9 +3,9 @@
|
|||
// 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:_fe_analyzer_shared/src/exhaustiveness/static_types.dart';
|
||||
import 'package:_fe_analyzer_shared/src/exhaustiveness/witness.dart';
|
||||
import 'package:analyzer/dart/ast/ast.dart';
|
||||
import 'package:analyzer/dart/element/element.dart';
|
||||
import 'package:analyzer/dart/element/nullability_suffix.dart';
|
||||
|
@ -291,10 +291,6 @@ class ExhaustivenessDataForTesting {
|
|||
/// Map from switch case nodes to the space for its pattern/expression.
|
||||
Map<AstNode, Space> caseSpaces = {};
|
||||
|
||||
/// Map from switch case nodes to the remaining space before the case or
|
||||
/// from statement/expression nodes to the remaining space after all cases.
|
||||
Map<AstNode, Space> remainingSpaces = {};
|
||||
|
||||
/// Map from switch statement/expression/case nodes to the error reported
|
||||
/// on the node.
|
||||
Map<AstNode, ExhaustivenessError> errors = {};
|
||||
|
@ -312,11 +308,12 @@ class PatternConverter {
|
|||
Space convertPattern(
|
||||
DartPattern pattern, {
|
||||
required bool nonNull,
|
||||
required Path path,
|
||||
}) {
|
||||
if (pattern is DeclaredVariablePatternImpl) {
|
||||
final type = pattern.declaredElement!.type;
|
||||
final staticType = _asStaticType(type, nonNull: nonNull);
|
||||
return Space(staticType);
|
||||
return Space(path, staticType);
|
||||
} else if (pattern is ObjectPattern) {
|
||||
final fields = <String, Space>{};
|
||||
for (final field in pattern.fields) {
|
||||
|
@ -325,23 +322,24 @@ class PatternConverter {
|
|||
// TODO(johnniwinther): How do we handle error cases?
|
||||
continue;
|
||||
}
|
||||
fields[name] = convertPattern(field.pattern, nonNull: false);
|
||||
fields[name] =
|
||||
convertPattern(field.pattern, nonNull: false, path: path.add(name));
|
||||
}
|
||||
final type = pattern.type.typeOrThrow;
|
||||
final staticType = _asStaticType(type, nonNull: nonNull);
|
||||
return Space(staticType, fields);
|
||||
return Space(path, staticType, fields: fields);
|
||||
} else if (pattern is WildcardPattern) {
|
||||
final typeNode = pattern.type;
|
||||
if (typeNode == null) {
|
||||
if (nonNull) {
|
||||
return Space(StaticType.nonNullableObject);
|
||||
return Space(path, StaticType.nonNullableObject);
|
||||
} else {
|
||||
return Space.top;
|
||||
return Space(path, StaticType.nullableObject);
|
||||
}
|
||||
} else {
|
||||
final type = typeNode.typeOrThrow;
|
||||
final staticType = _asStaticType(type, nonNull: nonNull);
|
||||
return Space(staticType);
|
||||
return Space(path, staticType);
|
||||
}
|
||||
} else if (pattern is RecordPatternImpl) {
|
||||
var index = 1;
|
||||
|
@ -363,26 +361,26 @@ class PatternConverter {
|
|||
continue;
|
||||
}
|
||||
}
|
||||
fields[name] = convertPattern(field.pattern, nonNull: false);
|
||||
fields[name] =
|
||||
convertPattern(field.pattern, nonNull: false, path: path.add(name));
|
||||
}
|
||||
final recordType = RecordType(
|
||||
positional: positional,
|
||||
named: named,
|
||||
nullabilitySuffix: NullabilitySuffix.none,
|
||||
);
|
||||
return Space(cache.getStaticType(recordType), fields);
|
||||
return Space(path, cache.getStaticType(recordType), fields: fields);
|
||||
} else if (pattern is LogicalOrPattern) {
|
||||
return Space.union([
|
||||
convertPattern(pattern.leftOperand, nonNull: nonNull),
|
||||
convertPattern(pattern.rightOperand, nonNull: nonNull)
|
||||
]);
|
||||
return convertPattern(pattern.leftOperand, nonNull: nonNull, path: path)
|
||||
.union(convertPattern(pattern.rightOperand,
|
||||
nonNull: nonNull, path: path));
|
||||
} else if (pattern is NullCheckPattern) {
|
||||
return convertPattern(pattern.pattern, nonNull: true);
|
||||
return convertPattern(pattern.pattern, nonNull: true, path: path);
|
||||
} else if (pattern is ParenthesizedPattern) {
|
||||
return convertPattern(pattern.pattern, nonNull: nonNull);
|
||||
return convertPattern(pattern.pattern, nonNull: nonNull, path: path);
|
||||
} else if (pattern is NullAssertPattern) {
|
||||
Space space = convertPattern(pattern.pattern, nonNull: true);
|
||||
return Space.union([space, Space.nullSpace]);
|
||||
Space space = convertPattern(pattern.pattern, nonNull: true, path: path);
|
||||
return space.union(Space(path, StaticType.nullType));
|
||||
} else if (pattern is CastPattern ||
|
||||
pattern is RelationalPattern ||
|
||||
pattern is LogicalAndPattern) {
|
||||
|
@ -390,23 +388,23 @@ class PatternConverter {
|
|||
// TODO(johnniwinther): Handle `Null` aspect implicitly covered by
|
||||
// [NullAssertPattern] and `as Null`.
|
||||
// TODO(johnniwinther): Handle top in [AndPattern] branches.
|
||||
return Space(cache.getUnknownStaticType());
|
||||
return Space(path, cache.getUnknownStaticType());
|
||||
} else if (pattern is ListPattern || pattern is MapPattern) {
|
||||
// TODO(johnniwinther): Support list and map patterns. This not only
|
||||
// requires a new interpretation of [Space] fields that handles the
|
||||
// relation between concrete lengths, rest patterns with/without
|
||||
// subpattern, and list/map of arbitrary size and content, but also for the
|
||||
// runtime to check for lengths < 0.
|
||||
return Space(cache.getUnknownStaticType());
|
||||
return Space(path, cache.getUnknownStaticType());
|
||||
} else if (pattern is ConstantPattern) {
|
||||
final value = constantPatternValues[pattern];
|
||||
if (value != null) {
|
||||
return _convertConstantValue(value);
|
||||
return _convertConstantValue(value, path);
|
||||
}
|
||||
return Space(cache.getUnknownStaticType());
|
||||
return Space(path, cache.getUnknownStaticType());
|
||||
}
|
||||
assert(false, "Unexpected pattern $pattern (${pattern.runtimeType})");
|
||||
return Space(cache.getUnknownStaticType());
|
||||
return Space(path, cache.getUnknownStaticType());
|
||||
}
|
||||
|
||||
StaticType _asStaticType(DartType type, {required bool nonNull}) {
|
||||
|
@ -414,34 +412,37 @@ class PatternConverter {
|
|||
return nonNull ? staticType.nonNullable : staticType;
|
||||
}
|
||||
|
||||
Space _convertConstantValue(DartObjectImpl value) {
|
||||
Space _convertConstantValue(DartObjectImpl value, Path path) {
|
||||
final state = value.state;
|
||||
if (value.isNull) {
|
||||
return Space.nullSpace;
|
||||
return Space(path, StaticType.nullType);
|
||||
} else if (state is BoolState) {
|
||||
final value = state.value;
|
||||
if (value != null) {
|
||||
return Space(cache.getBoolValueStaticType(value));
|
||||
return Space(path, cache.getBoolValueStaticType(state.value!));
|
||||
}
|
||||
} else if (state is RecordState) {
|
||||
final fields = <String, Space>{};
|
||||
for (var index = 0; index < state.positionalFields.length; index++) {
|
||||
final name = '\$${1 + index}';
|
||||
final value = state.positionalFields[index];
|
||||
fields['\$${1 + index}'] = _convertConstantValue(value);
|
||||
fields[name] = _convertConstantValue(value, path.add(name));
|
||||
}
|
||||
for (final entry in state.namedFields.entries) {
|
||||
fields[entry.key] = _convertConstantValue(entry.value);
|
||||
final name = entry.key;
|
||||
fields[name] = _convertConstantValue(entry.value, path.add(name));
|
||||
}
|
||||
return Space(cache.getStaticType(value.type), fields);
|
||||
return Space(path, cache.getStaticType(value.type), fields: fields);
|
||||
}
|
||||
final type = value.type;
|
||||
if (type is InterfaceType) {
|
||||
final element = type.element;
|
||||
if (element is EnumElement) {
|
||||
return Space(cache.getEnumElementStaticType(element, value));
|
||||
return Space(path, cache.getEnumElementStaticType(element, value));
|
||||
}
|
||||
}
|
||||
return Space(cache.getUniqueStaticType(type, value, value.toString()));
|
||||
return Space(
|
||||
path, cache.getUniqueStaticType(type, value, value.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
import 'dart:io';
|
||||
|
||||
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:_fe_analyzer_shared/src/exhaustiveness/test_helper.dart';
|
||||
import 'package:_fe_analyzer_shared/src/exhaustiveness/witness.dart';
|
||||
|
@ -84,10 +83,6 @@ class _ExhaustivenessDataExtractor extends AstDataExtractor<Features> {
|
|||
}
|
||||
}
|
||||
}
|
||||
Space? remainingSpace = _exhaustivenessData.remainingSpaces[node];
|
||||
if (remainingSpace != null) {
|
||||
features[Tags.remaining] = spaceToText(remainingSpace);
|
||||
}
|
||||
ExhaustivenessError? error = _exhaustivenessData.errors[node];
|
||||
if (error != null) {
|
||||
features[Tags.error] = errorToText(error);
|
||||
|
@ -95,11 +90,7 @@ class _ExhaustivenessDataExtractor extends AstDataExtractor<Features> {
|
|||
} else if (node is SwitchMember || node is SwitchExpressionCase) {
|
||||
Space? caseSpace = _exhaustivenessData.caseSpaces[node];
|
||||
if (caseSpace != null) {
|
||||
features[Tags.space] = spaceToText(caseSpace);
|
||||
}
|
||||
Space? remainingSpace = _exhaustivenessData.remainingSpaces[node];
|
||||
if (remainingSpace != null) {
|
||||
features[Tags.remaining] = spaceToText(remainingSpace);
|
||||
features[Tags.space] = spacesToText(caseSpace);
|
||||
}
|
||||
ExhaustivenessError? error = _exhaustivenessData.errors[node];
|
||||
if (error != null) {
|
||||
|
|
|
@ -32,7 +32,7 @@ import 'package:kernel/type_algebra.dart';
|
|||
import 'package:kernel/type_environment.dart';
|
||||
import 'package:kernel/target/targets.dart';
|
||||
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/witness.dart';
|
||||
import 'package:_fe_analyzer_shared/src/exhaustiveness/static_type.dart';
|
||||
|
||||
import '../../base/nnbd_mode.dart';
|
||||
|
@ -833,12 +833,7 @@ class ConstantsTransformer extends RemovingTransformer {
|
|||
cases.add(caseInfo.createSpace(
|
||||
exhaustivenessCache, constantPatternValues, _staticTypeContext!));
|
||||
}
|
||||
List<Space>? remainingSpaces;
|
||||
if (_exhaustivenessDataForTesting != null) {
|
||||
remainingSpaces = [];
|
||||
}
|
||||
List<ExhaustivenessError> errors =
|
||||
reportErrors(type, cases, remainingSpaces);
|
||||
List<ExhaustivenessError> errors = reportErrors(type, cases);
|
||||
if (!useFallbackExhaustivenessAlgorithm) {
|
||||
for (ExhaustivenessError error in errors) {
|
||||
if (error is UnreachableCaseError) {
|
||||
|
@ -869,7 +864,6 @@ class ConstantsTransformer extends RemovingTransformer {
|
|||
switchInfo.cases
|
||||
.map((SwitchCaseInfo info) => info.fileOffset)
|
||||
.toList(),
|
||||
remainingSpaces!,
|
||||
errors);
|
||||
}
|
||||
|
||||
|
|
|
@ -3,9 +3,9 @@
|
|||
// 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:_fe_analyzer_shared/src/exhaustiveness/static_types.dart';
|
||||
import 'package:_fe_analyzer_shared/src/exhaustiveness/witness.dart';
|
||||
import 'package:front_end/src/fasta/kernel/constant_evaluator.dart';
|
||||
import 'package:kernel/ast.dart';
|
||||
import 'package:kernel/class_hierarchy.dart';
|
||||
|
@ -32,11 +32,10 @@ class ExhaustivenessResult {
|
|||
final StaticType scrutineeType;
|
||||
final List<Space> caseSpaces;
|
||||
final List<int> caseOffsets;
|
||||
final List<Space> remainingSpaces;
|
||||
final List<ExhaustivenessError> errors;
|
||||
|
||||
ExhaustivenessResult(this.scrutineeType, this.caseSpaces, this.caseOffsets,
|
||||
this.remainingSpaces, this.errors);
|
||||
ExhaustivenessResult(
|
||||
this.scrutineeType, this.caseSpaces, this.caseOffsets, this.errors);
|
||||
}
|
||||
|
||||
class CfeTypeOperations implements TypeOperations<DartType> {
|
||||
|
@ -347,7 +346,8 @@ class ExpressionCaseInfo extends SwitchCaseInfo {
|
|||
@override
|
||||
Space createSpace(CfeExhaustivenessCache cache,
|
||||
Map<Node, Constant?> constants, StaticTypeContext context) {
|
||||
return convertExpressionToSpace(cache, expression, constants, context);
|
||||
return convertExpressionToSpace(
|
||||
cache, expression, constants, context, const Path.root());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -365,9 +365,11 @@ class PatternCaseInfo extends SwitchCaseInfo {
|
|||
@override
|
||||
Space createSpace(CfeExhaustivenessCache cache,
|
||||
Map<Node, Constant?> constants, StaticTypeContext context) {
|
||||
if (hasGuard) return new Space(cache.getUnknownStaticType());
|
||||
if (hasGuard) {
|
||||
return new Space(const Path.root(), cache.getUnknownStaticType());
|
||||
}
|
||||
return convertPatternToSpace(cache, pattern, constants, context,
|
||||
nonNull: false);
|
||||
nonNull: false, path: const Path.root());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -400,36 +402,38 @@ Space convertExpressionToSpace(
|
|||
CfeExhaustivenessCache cache,
|
||||
Expression expression,
|
||||
Map<Node, Constant?> constants,
|
||||
StaticTypeContext context) {
|
||||
StaticTypeContext context,
|
||||
Path path) {
|
||||
Constant? constant = constants[expression];
|
||||
return convertConstantToSpace(cache, constant, constants, context);
|
||||
return convertConstantToSpace(cache, constant, constants, context,
|
||||
path: path);
|
||||
}
|
||||
|
||||
Space convertPatternToSpace(CfeExhaustivenessCache cache, Pattern pattern,
|
||||
Map<Node, Constant?> constants, StaticTypeContext context,
|
||||
{required bool nonNull}) {
|
||||
{required bool nonNull, required Path path}) {
|
||||
if (pattern is ObjectPattern) {
|
||||
DartType type = pattern.objectType;
|
||||
Map<String, Space> fields = {};
|
||||
for (NamedPattern field in pattern.fields) {
|
||||
fields[field.name] = convertPatternToSpace(
|
||||
cache, field.pattern, constants, context,
|
||||
nonNull: false);
|
||||
nonNull: false, path: path.add(field.name));
|
||||
}
|
||||
StaticType staticType = cache.getStaticType(type);
|
||||
if (nonNull) {
|
||||
staticType = staticType.nonNullable;
|
||||
}
|
||||
return new Space(staticType, fields);
|
||||
return new Space(path, staticType, fields: fields);
|
||||
} else if (pattern is VariablePattern) {
|
||||
StaticType staticType = cache.getStaticType(pattern.variable.type);
|
||||
if (nonNull) {
|
||||
staticType = staticType.nonNullable;
|
||||
}
|
||||
return new Space(staticType);
|
||||
return new Space(path, staticType);
|
||||
} else if (pattern is ConstantPattern) {
|
||||
return convertExpressionToSpace(
|
||||
cache, pattern.expression, constants, context);
|
||||
cache, pattern.expression, constants, context, path);
|
||||
} else if (pattern is RecordPattern) {
|
||||
int index = 1;
|
||||
Map<String, Space> fields = {};
|
||||
|
@ -445,39 +449,37 @@ Space convertPatternToSpace(CfeExhaustivenessCache cache, Pattern pattern,
|
|||
}
|
||||
fields[name] = convertPatternToSpace(
|
||||
cache, subpattern, constants, context,
|
||||
nonNull: false);
|
||||
nonNull: false, path: path.add(name));
|
||||
}
|
||||
return new Space(cache.getStaticType(pattern.type), fields);
|
||||
return new Space(path, cache.getStaticType(pattern.type), fields: fields);
|
||||
} else if (pattern is WildcardPattern) {
|
||||
final DartType? type = pattern.type;
|
||||
if (type == null) {
|
||||
if (nonNull) {
|
||||
return new Space(StaticType.nonNullableObject);
|
||||
return new Space(path, StaticType.nonNullableObject);
|
||||
} else {
|
||||
return Space.top;
|
||||
return new Space(path, StaticType.nullableObject);
|
||||
}
|
||||
} else {
|
||||
StaticType staticType = cache.getStaticType(type);
|
||||
if (nonNull) {
|
||||
staticType = staticType.nonNullable;
|
||||
}
|
||||
return new Space(staticType);
|
||||
return new Space(path, staticType);
|
||||
}
|
||||
} else if (pattern is OrPattern) {
|
||||
return new Space.union([
|
||||
convertPatternToSpace(cache, pattern.left, constants, context,
|
||||
nonNull: nonNull),
|
||||
convertPatternToSpace(cache, pattern.right, constants, context,
|
||||
nonNull: nonNull)
|
||||
]);
|
||||
return convertPatternToSpace(cache, pattern.left, constants, context,
|
||||
nonNull: nonNull, path: path)
|
||||
.union(convertPatternToSpace(cache, pattern.right, constants, context,
|
||||
nonNull: nonNull, path: path));
|
||||
} else if (pattern is NullCheckPattern) {
|
||||
return convertPatternToSpace(cache, pattern.pattern, constants, context,
|
||||
nonNull: true);
|
||||
nonNull: true, path: path);
|
||||
} else if (pattern is NullAssertPattern) {
|
||||
Space space = convertPatternToSpace(
|
||||
cache, pattern.pattern, constants, context,
|
||||
nonNull: true);
|
||||
return new Space.union([space, Space.nullSpace]);
|
||||
nonNull: true, path: path);
|
||||
return space.union(new Space(path, StaticType.nullType));
|
||||
} else if (pattern is CastPattern ||
|
||||
pattern is InvalidPattern ||
|
||||
pattern is RelationalPattern ||
|
||||
|
@ -486,48 +488,56 @@ Space convertPatternToSpace(CfeExhaustivenessCache cache, Pattern pattern,
|
|||
// TODO(johnniwinther): Handle `Null` aspect implicitly covered by
|
||||
// [NullAssertPattern] and `as Null`.
|
||||
// TODO(johnniwinther): Handle top in [AndPattern] branches.
|
||||
return new Space(cache.getUnknownStaticType());
|
||||
return new Space(path, cache.getUnknownStaticType());
|
||||
} else if (pattern is ListPattern || pattern is MapPattern) {
|
||||
// TODO(johnniwinther): Support list and map patterns. This not only
|
||||
// requires a new interpretation of [Space] fields that handles the
|
||||
// relation between concrete lengths, rest patterns with/without
|
||||
// subpattern, and list/map of arbitrary size and content, but also for the
|
||||
// runtime to check for lengths < 0.
|
||||
return new Space(cache.getUnknownStaticType());
|
||||
return new Space(path, cache.getUnknownStaticType());
|
||||
}
|
||||
assert(false, "Unexpected pattern $pattern (${pattern.runtimeType}).");
|
||||
return new Space(cache.getUnknownStaticType());
|
||||
return new Space(path, cache.getUnknownStaticType());
|
||||
}
|
||||
|
||||
Space convertConstantToSpace(CfeExhaustivenessCache cache, Constant? constant,
|
||||
Map<Node, Constant?> constants, StaticTypeContext context) {
|
||||
Map<Node, Constant?> constants, StaticTypeContext context,
|
||||
{required Path path}) {
|
||||
if (constant != null) {
|
||||
if (constant is NullConstant) {
|
||||
return Space.nullSpace;
|
||||
return new Space(path, StaticType.nullType);
|
||||
} else if (constant is BoolConstant) {
|
||||
return new Space(cache.getBoolValueStaticType(constant.value));
|
||||
return new Space(path, cache.getBoolValueStaticType(constant.value));
|
||||
} else if (constant is InstanceConstant && constant.classNode.isEnum) {
|
||||
return new Space(
|
||||
cache.getEnumElementStaticType(constant.classNode, constant));
|
||||
path, cache.getEnumElementStaticType(constant.classNode, constant));
|
||||
} else if (constant is RecordConstant) {
|
||||
Map<String, Space> fields = {};
|
||||
for (int index = 0; index < constant.positional.length; index++) {
|
||||
fields['\$${index + 1}'] = convertConstantToSpace(
|
||||
cache, constant.positional[index], constants, context);
|
||||
String name = '\$${index + 1}';
|
||||
fields[name] = convertConstantToSpace(
|
||||
cache, constant.positional[index], constants, context,
|
||||
path: path.add(name));
|
||||
}
|
||||
for (MapEntry<String, Constant> entry in constant.named.entries) {
|
||||
fields[entry.key] =
|
||||
convertConstantToSpace(cache, entry.value, constants, context);
|
||||
String name = entry.key;
|
||||
fields[name] = convertConstantToSpace(
|
||||
cache, entry.value, constants, context,
|
||||
path: path.add(name));
|
||||
}
|
||||
return new Space(cache.getStaticType(constant.recordType), fields);
|
||||
return new Space(path, cache.getStaticType(constant.recordType),
|
||||
fields: fields);
|
||||
} else {
|
||||
return new Space(cache.getUniqueStaticType(
|
||||
constant.getType(context), constant, '${constant}'));
|
||||
return new Space(
|
||||
path,
|
||||
cache.getUniqueStaticType(
|
||||
constant.getType(context), constant, '${constant}'));
|
||||
}
|
||||
} else {
|
||||
// TODO(johnniwinther): Assert that constant value is available when the
|
||||
// exhaustiveness checking is complete.
|
||||
return new Space(cache.getUnknownStaticType());
|
||||
return new Space(path, cache.getUnknownStaticType());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -83,9 +83,6 @@ class ExhaustivenessDataExtractor extends CfeDataExtractor<Features> {
|
|||
}
|
||||
features[Tags.scrutineeFields] =
|
||||
fieldsToText(result.scrutineeType.fields);
|
||||
if (result.remainingSpaces.isNotEmpty) {
|
||||
features[Tags.remaining] = spaceToText(result.remainingSpaces.last);
|
||||
}
|
||||
for (ExhaustivenessError error in result.errors) {
|
||||
if (error is NonExhaustiveError) {
|
||||
features[Tags.error] = errorToText(error);
|
||||
|
@ -95,10 +92,7 @@ class ExhaustivenessDataExtractor extends CfeDataExtractor<Features> {
|
|||
for (int i = 0; i < result.caseSpaces.length; i++) {
|
||||
int offset = result.caseOffsets[i];
|
||||
Features caseFeatures = new Features();
|
||||
caseFeatures[Tags.space] = spaceToText(result.caseSpaces[i]);
|
||||
if (result.remainingSpaces.isNotEmpty) {
|
||||
caseFeatures[Tags.remaining] = spaceToText(result.remainingSpaces[i]);
|
||||
}
|
||||
caseFeatures[Tags.space] = spacesToText(result.caseSpaces[i]);
|
||||
for (ExhaustivenessError error in result.errors) {
|
||||
if (error is UnreachableCaseError && error.index == i) {
|
||||
caseFeatures[Tags.error] = errorToText(error);
|
||||
|
|
Loading…
Reference in a new issue