mirror of
https://github.com/dart-lang/sdk
synced 2024-09-06 00:59:09 +00:00
![Lasse R.H. Nielsen](/assets/img/avatar_default.png)
Also adds `firstOrNull`, `lastOrNull`, `singleOrNull`, and `elementAtOrNull`. I chose `nonNulls` instead of `whereNotNull()`. The latter should have been a getter, but isn't. Making it a getter in the platform libraries would be confusing, and make migrating from `package:collection`'s function harder, so instead I gave it a completely new name. The alternative would be to retain the non-idiomatic `whereNotNull()` for familiarity. But then it would be tool late to fix. Fixes #49928 BUG= https://github.com/dart-lang/sdk/issues/49928 CoreLibraryReviewExempt: Everybody's on vacation, everybody everywhere. Change-Id: If464e3bd6bc97cbeefc3e5084b4cbaadac3f1e95 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/290760 Reviewed-by: Aske Simon Christensen <askesc@google.com> Reviewed-by: Nate Bosch <nbosch@google.com> Commit-Queue: Lasse Nielsen <lrn@google.com>
366 lines
12 KiB
Dart
366 lines
12 KiB
Dart
// Copyright (c) 2023, 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:async" show FutureOr;
|
|
import "package:expect/expect.dart";
|
|
import "../language/static_type_helper.dart";
|
|
// All extensions are exported by `dart:core`.
|
|
|
|
void main() {
|
|
// Null-related extensions.
|
|
testNonNulls();
|
|
testFirstOrNull();
|
|
testLastOrNull();
|
|
testSingleOrNull();
|
|
testElementAtOrNull();
|
|
|
|
// Record-related extensions.
|
|
testIndexed();
|
|
}
|
|
|
|
void testNonNulls() {
|
|
// Static behavior.
|
|
{
|
|
// Removes nullability from element type.
|
|
Iterable<int?> target = [];
|
|
var result = target.nonNulls;
|
|
result.expectStaticType<Exactly<Iterable<int>>>();
|
|
}
|
|
{
|
|
// Works on subtypes of iterable.
|
|
List<int?> target = [];
|
|
var result = target.nonNulls;
|
|
result.expectStaticType<Exactly<Iterable<int>>>();
|
|
}
|
|
{
|
|
// Works on non-nullable types too (wish it didn't).
|
|
Iterable<int> target = [];
|
|
var result = target.nonNulls;
|
|
result.expectStaticType<Exactly<Iterable<int>>>();
|
|
}
|
|
{
|
|
// Removes nullability from `Never?`, giving `Never`.
|
|
// (Cannot remove nullability from `Null`, so doesn't match
|
|
// `Iterable<Null>`.)
|
|
Iterable<Never?> target = [];
|
|
var result = target.nonNulls;
|
|
result.expectStaticType<Exactly<Iterable<Never>>>();
|
|
}
|
|
|
|
// Dynamic behavior.
|
|
|
|
void test<T extends Object>(
|
|
String name, Iterable<T?> input, List<T> expectedResults) {
|
|
var actualResults = input.nonNulls;
|
|
Expect.type<Iterable<T>>(actualResults, "$name type");
|
|
Expect.listEquals(expectedResults, [...actualResults], "$name result");
|
|
}
|
|
|
|
test<int>("empty iterable", Iterable.empty(), []);
|
|
test<int>("empty list", [], []);
|
|
test<int>("all non-null", numbers(5), [1, 2, 3, 4, 5]);
|
|
test<int>("all null", numbers(5, where: none), []);
|
|
test<int>("one non-null", numbers(5, where: only(3)), [3]);
|
|
test<int>("some null", numbers(5, where: even), [2, 4]);
|
|
test<int>("some null, list", [null, 2, null, 4, null], [2, 4]);
|
|
|
|
// Is lazy.
|
|
var nonNulls = numbers(5, where: even, throwAt: 3).nonNulls;
|
|
var it = nonNulls.iterator;
|
|
Expect.isTrue(it.moveNext());
|
|
Expect.equals(2, it.current);
|
|
Expect.throws<UnimplementedError>(it.moveNext);
|
|
}
|
|
|
|
void testFirstOrNull() {
|
|
// Static behavior.
|
|
// (Tested to ensure extension captures the correct type,
|
|
// and the correct extension member is applied).
|
|
{
|
|
Iterable<int> target = [];
|
|
var result = target.firstOrNull;
|
|
result.expectStaticType<Exactly<int?>>();
|
|
}
|
|
{
|
|
Iterable<int?> target = [];
|
|
var result = target.firstOrNull;
|
|
result.expectStaticType<Exactly<int?>>();
|
|
}
|
|
{
|
|
Iterable<Never> target = [];
|
|
var result = target.firstOrNull;
|
|
result.expectStaticType<Exactly<Null>>();
|
|
}
|
|
// Dynamic behavior.
|
|
void test<T extends Object>(
|
|
String name, Iterable<T?> source, T? expectedResult) {
|
|
var actualResult = source.firstOrNull;
|
|
Expect.equals(expectedResult, actualResult, "firstOrNull $name");
|
|
}
|
|
|
|
test<int>("Empty iterable", Iterable.empty(), null);
|
|
test<Never>("Empty iterable", Iterable.empty(), null);
|
|
test<int>("Empty list", [], null);
|
|
test<int>("Single value", numbers(1), 1);
|
|
test<int>("Multiple values", numbers(3), 1);
|
|
test<int>("Nullable values", numbers(3, where: even), null);
|
|
test<int>("Stops after first", numbers(3, throwAt: 2), 1);
|
|
Expect.throws<UnimplementedError>(() => numbers(3, throwAt: 1).firstOrNull,
|
|
null, "firstOrNull first throws");
|
|
}
|
|
|
|
void testLastOrNull() {
|
|
// Static behavior.
|
|
{
|
|
Iterable<int> target = [];
|
|
var result = target.lastOrNull;
|
|
result.expectStaticType<Exactly<int?>>();
|
|
}
|
|
{
|
|
Iterable<int?> target = [];
|
|
var result = target.lastOrNull;
|
|
result.expectStaticType<Exactly<int?>>();
|
|
}
|
|
{
|
|
Iterable<Never> target = [];
|
|
var result = target.lastOrNull;
|
|
result.expectStaticType<Exactly<Null>>();
|
|
}
|
|
// Dynamic behavior.
|
|
void test<T>(String name, Iterable<T> source, T? expectedResult) {
|
|
var actualResult = source.lastOrNull;
|
|
Expect.equals(expectedResult, actualResult, "lastOrNull $name");
|
|
}
|
|
|
|
test<int>("Empty iterable", Iterable.empty(), null);
|
|
test<Never>("Empty iterable", Iterable.empty(), null);
|
|
test<int>("Empty list", [], null);
|
|
test<int?>("Single value", numbers(1), 1);
|
|
test<int?>("Multiple values", numbers(3), 3);
|
|
test<int?>("Nullable values", numbers(3, where: even), null);
|
|
Expect.throws<UnimplementedError>(
|
|
() => numbers(3, throwAt: 1).lastOrNull, null, "lastOrNull first throws");
|
|
Expect.throws<UnimplementedError>(
|
|
() => numbers(3, throwAt: 3).lastOrNull, null, "lastOrNull last throws");
|
|
Expect.throws<UnimplementedError>(
|
|
() => CurrentThrowIterable<int?>(numbers(3), 2).lastOrNull,
|
|
null,
|
|
"lastOrNull throw on current");
|
|
}
|
|
|
|
void testSingleOrNull() {
|
|
// Static behavior.
|
|
{
|
|
Iterable<int> target = [];
|
|
var result = target.singleOrNull;
|
|
result.expectStaticType<Exactly<int?>>();
|
|
}
|
|
{
|
|
Iterable<int?> target = [];
|
|
var result = target.singleOrNull;
|
|
result.expectStaticType<Exactly<int?>>();
|
|
}
|
|
{
|
|
Iterable<Never> target = [];
|
|
var result = target.singleOrNull;
|
|
result.expectStaticType<Exactly<Null>>();
|
|
}
|
|
// Dynamic behavior.
|
|
void test<T>(String name, Iterable<T> source, T? expectedResult) {
|
|
var actualResult = source.singleOrNull;
|
|
Expect.equals(expectedResult, actualResult, "singleOrNull $name");
|
|
}
|
|
|
|
test<int>("Empty iterable", Iterable.empty(), null);
|
|
test<Never>("Empty iterable", Iterable.empty(), null);
|
|
test<int>("Empty list", [], null);
|
|
test<int?>("Single value", numbers(1), 1);
|
|
test<int?>("Multiple values", numbers(3), null);
|
|
test<int?>("Nullable values", numbers(3, where: even), null);
|
|
Expect.throws<UnimplementedError>(() => numbers(3, throwAt: 1).singleOrNull,
|
|
null, "singleOrNull first throws");
|
|
Expect.throws<UnimplementedError>(() => numbers(3, throwAt: 2).singleOrNull,
|
|
null, "singleOrNull second throws");
|
|
test<int?>("Throws after two", numbers(3, throwAt: 3), null);
|
|
}
|
|
|
|
void testElementAtOrNull() {
|
|
// Static behavior.
|
|
{
|
|
Iterable<int> target = [];
|
|
var result = target.elementAtOrNull(0);
|
|
result.expectStaticType<Exactly<int?>>();
|
|
}
|
|
{
|
|
Iterable<int?> target = [];
|
|
var result = target.elementAtOrNull(0);
|
|
result.expectStaticType<Exactly<int?>>();
|
|
}
|
|
{
|
|
Iterable<Never> target = [];
|
|
var result = target.elementAtOrNull(0);
|
|
result.expectStaticType<Exactly<Null>>();
|
|
}
|
|
// Dynamic behavior.
|
|
void test<T>(String name, Iterable<T> source, int index, T? expectedResult) {
|
|
var actualResult = source.elementAtOrNull(index);
|
|
Expect.equals(
|
|
expectedResult, actualResult, "elementAtOrNull($index) $name");
|
|
}
|
|
|
|
Expect.throwsArgumentError(() => numbers(3).elementAtOrNull(-1),
|
|
"elementAtOrNull(negative) first throws");
|
|
|
|
test<int>("Empty iterable", Iterable<int>.empty(), 0, null);
|
|
test<int>("Empty iterable", Iterable<int>.empty(), 1000000, null);
|
|
test<Never>("Empty iterable", Iterable<Never>.empty(), 0, null);
|
|
test<int>("Empty list", [], 0, null);
|
|
test<int?>("Single value", numbers(1), 0, 1);
|
|
test<int?>("Single value", numbers(1), 1, null);
|
|
test<int?>("Multiple values first", numbers(3), 0, 1);
|
|
test<int?>("Multiple values mid", numbers(3), 1, 2);
|
|
test<int?>("Multiple values last", numbers(3), 2, 3);
|
|
test<int?>("Multiple values overshoot", numbers(3), 3, null);
|
|
test<int?>("Nullable values found", numbers(3, where: even), 2, null);
|
|
test<int?>("Nullable values not found", numbers(3, where: even), 3, null);
|
|
Expect.throws<UnimplementedError>(
|
|
() => numbers(3, throwAt: 1).elementAtOrNull(1),
|
|
null,
|
|
"elementAtOrNull(1) first throws");
|
|
Expect.throws<UnimplementedError>(
|
|
() => numbers(3, throwAt: 2).elementAtOrNull(2),
|
|
null,
|
|
"elementAtOrNull(2) second throws");
|
|
test<int?>("Throws after two", numbers(3, throwAt: 3), 1, 2);
|
|
|
|
var currentThrow2 = CurrentThrowIterable<int?>(numbers(3), 2);
|
|
test<int?>("Throws current middle", currentThrow2, 0, 1);
|
|
test<int?>("Throws current middle", currentThrow2, 2, 3);
|
|
Expect.throws<UnimplementedError>(() => currentThrow2.elementAt(1));
|
|
}
|
|
|
|
void testIndexed() {
|
|
void test<T>(String name, Iterable<T> elements) {
|
|
var values = elements.toList();
|
|
var indexed = elements.indexed;
|
|
testRec(name, values, indexed, 0, false);
|
|
}
|
|
|
|
// Non-efficient-length iterables.
|
|
test<int?>("NELI empty", numbers(0));
|
|
test<int?>("NELI single", numbers(1));
|
|
test<int?>("NELI two", numbers(2));
|
|
test<int?>("NELI more", numbers(10));
|
|
|
|
// Efficient-length iterables (a list's `map` has efficient length).
|
|
test<int?>("ELI empty", numbers(0).toList().map((x) => x));
|
|
test<int?>("ELI single", numbers(1).toList().map((x) => x));
|
|
test<int?>("ELI two", numbers(2).toList().map((x) => x));
|
|
test<int?>("ELI more", numbers(10).toList().map((x) => x));
|
|
}
|
|
|
|
// Helper function for `testIndexed`. Top-level because dart2js crashes
|
|
// on recursive generic local functions.
|
|
//
|
|
// If [rec] is true, we're doing a recursive test on skip/take/both,
|
|
// and `start` the number of leading elements skipped.
|
|
void testRec<T>(String name, List<T> values, Iterable<(int, T)> indexed,
|
|
int start, bool rec) {
|
|
var length = values.length;
|
|
Expect.equals(length, indexed.length, "$values length");
|
|
Expect.equals(values.isEmpty, indexed.isEmpty);
|
|
Expect.equals(values.isNotEmpty, indexed.isNotEmpty);
|
|
Expect.listEquals([for (var i = 0; i < length; i++) (start + i, values[i])],
|
|
indexed.toList());
|
|
|
|
int index = 0;
|
|
indexed.forEach((pair) {
|
|
Expect.equals(start + index, pair.$1);
|
|
Expect.equals(values[index], pair.$2);
|
|
index++;
|
|
});
|
|
Expect.equals(length, index);
|
|
|
|
Expect.isFalse(indexed.contains(0));
|
|
Expect.isFalse(indexed.contains((start - 1, 0)));
|
|
Expect.isFalse(indexed.contains((start + length, 0)));
|
|
|
|
if (values.isNotEmpty) {
|
|
Expect.isFalse(indexed.contains(values.first));
|
|
Expect.equals((start, values.first), indexed.first);
|
|
Expect.equals((start + length - 1, values.last), indexed.last);
|
|
for (var i = 0; i < length; i++) {
|
|
Expect.equals((start + i, values[i]), indexed.elementAt(i));
|
|
Expect.isTrue(indexed.contains((start + i, values[i])));
|
|
}
|
|
Expect.isFalse(indexed.contains((start - 1, values.first)));
|
|
Expect.isFalse(indexed.contains((start + length, values.last)));
|
|
if (length == 1) {
|
|
Expect.equals((start, values.single), indexed.single);
|
|
} else if (!rec) {
|
|
Expect.throws<StateError>(() => indexed.single);
|
|
// More than one element, so test skip/take.
|
|
testRec("$name.skip(1)", values.sublist(1), indexed.skip(1), 1, true);
|
|
testRec("$name.take(l-1)", values.sublist(0, length - 1),
|
|
indexed.take(length - 1), 0, true);
|
|
if (length > 2) {
|
|
testRec("$name.skip(1).take(l-2)", values.sublist(1, length - 1),
|
|
indexed.skip(1).take(length - 2), 1, true);
|
|
testRec("$name.take(l-1).skip(1)", values.sublist(1, length - 1),
|
|
indexed.take(length - 1).skip(1), 1, true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Generates an iterable with [length] elements.
|
|
///
|
|
/// The elements are 1, ..., [length], except that if `isValue` returns
|
|
/// `false` for a number, it's replaced by `null`.
|
|
/// If [throwAt] is provided, the iterable throws an `UnimplementedError`
|
|
/// (which shouldn't conflict with an actual error) instead of emitting
|
|
/// a value at the [throwAt] index.
|
|
Iterable<int?> numbers(int length,
|
|
{bool Function(int) where = all, int? throwAt}) sync* {
|
|
for (var i = 1; i <= length; i++) {
|
|
if (i == throwAt) throw UnimplementedError("Error");
|
|
yield where(i) ? i : null;
|
|
}
|
|
}
|
|
|
|
/// Iterable which throws only when accessing [current].
|
|
///
|
|
/// Used to test that operations that don't need the value,
|
|
/// also don't read it.
|
|
///
|
|
/// (Could also be achieved with
|
|
/// ```
|
|
/// _source.toList().map((x) => x == _throwAt ? throw ... : x));
|
|
/// ```
|
|
/// but that assumes optimization behavior that is not necessarily tested.)
|
|
class CurrentThrowIterable<T> extends Iterable<T> {
|
|
final T _throwAt;
|
|
final Iterable<T> _source;
|
|
CurrentThrowIterable(this._source, this._throwAt);
|
|
Iterator<T> get iterator =>
|
|
CurrentThrowIterator<T>(_source.iterator, _throwAt);
|
|
}
|
|
|
|
class CurrentThrowIterator<T> implements Iterator<T> {
|
|
final T _throwAt;
|
|
Iterator<T> _source;
|
|
CurrentThrowIterator(this._source, this._throwAt);
|
|
bool moveNext() => _source.moveNext();
|
|
T get current {
|
|
var result = _source.current;
|
|
if (result == _throwAt) throw UnimplementedError("Error");
|
|
return result;
|
|
}
|
|
}
|
|
|
|
bool none(_) => false;
|
|
bool all(_) => true;
|
|
bool even(int n) => n.isEven;
|
|
bool Function(int) only(int n1) => (int n2) => n1 == n2;
|