Skip to content

Migration could do a better job of Iterable.firstWhere #42382

Closed
@stereotype441

Description

@stereotype441

Given the following code:

abstract class Foo {
  bool get isLarge;
}
class C {
  final List<Foo> foos;
  C(this.foos);
  Foo get firstLargeFoo => foos.firstWhere(
      (foo) => foo.isLarge, orElse: () => null);
}

Migration produces the result:

abstract class Foo {
  bool get isLarge;
}
class C {
  final List<Foo?> foos;
  C(this.foos);
  Foo? get firstLargeFoo => foos.firstWhere(
      (foo) => foo!.isLarge, orElse: () => null);
}

It's really not obvious why the type of foos gets changed to List<Foo?> (even using the tool's "trace" functionality). The reason is that Iterable<T>.firstWhere's signature is T Function(bool Function(T), {T Function() orElse}), so any value returned by orElse must be suitable for insertion in the list (even though firstWhere won't actually try to insert it in the list). In this example, orElse returns null, so the only way the migration tool can see for that to work is if the list is a List<Foo?>.

It's really unfortunate that the migration tool is changing the type of the list, because that's the sort of thing that can cause nulls to propagate to a large number of other places in the program.

A better migration would be:

abstract class Foo {
  bool get isLarge;
}
class C {
  final List<Foo> foos;
  C(this.foos);
  Foo? get firstLargeFoo => foos.cast<Foo?>().firstWhere(
      (foo) => foo!.isLarge, orElse: () => null);
}

But in order to figure out that this is possible, we have to apply the knowledge that firstWhere won't try to insert the value returned by orElse into the list. (And technically, in order for that to be sound, we'd have to look through the entire program to make sure there isn't an override of firstWhere that does do that). So if we do this at all, it's probably best to do it as a special-case optimization specific to Iterable.firstWhere rather than some general case logic.

(Note that this arose concretely when @srawlins was trying to migrate Mockito)

Metadata

Metadata

Assignees

No one assigned

    Labels

    NNBDIssues related to NNBD ReleaseP2A bug or feature request we're likely to work onarea-migration (deprecated)Deprecated: this label is no longer actively used (was: issues with the `dart migrate` tool).nnbd-migration-correctness-exampleConcrete examples of the migration engine producing an incorrect result on a phase 1 package

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions