Version 2.11.0-239.0.dev

Merge commit 'f1843b0abed6b43eb2e737b0edd52671c9c31eda' into 'dev'
diff --git a/pkg/analyzer/lib/src/test_utilities/mock_sdk.dart b/pkg/analyzer/lib/src/test_utilities/mock_sdk.dart
index e71bee4..6b23228 100644
--- a/pkg/analyzer/lib/src/test_utilities/mock_sdk.dart
+++ b/pkg/analyzer/lib/src/test_utilities/mock_sdk.dart
@@ -381,8 +381,12 @@
 
   void forEach(void f(E element));
 
+  E lastWhere(bool test(E element), {E orElse()?});
+
   Iterable<R> map<R>(R f(E e));
 
+  E singleWhere(bool test(E element), {E orElse()?});
+
   List<E> toList({bool growable = true});
 
   Set<E> toSet();
diff --git a/pkg/nnbd_migration/lib/instrumentation.dart b/pkg/nnbd_migration/lib/instrumentation.dart
index 387d666..a89962a 100644
--- a/pkg/nnbd_migration/lib/instrumentation.dart
+++ b/pkg/nnbd_migration/lib/instrumentation.dart
@@ -277,6 +277,7 @@
   instantiateToBounds,
   isCheckComponentType,
   isCheckMainType,
+  iteratorMethodReturn,
   listLengthConstructor,
   literal,
   namedParameterNotSupplied,
diff --git a/pkg/nnbd_migration/lib/nnbd_migration.dart b/pkg/nnbd_migration/lib/nnbd_migration.dart
index b222f91..64a6c49 100644
--- a/pkg/nnbd_migration/lib/nnbd_migration.dart
+++ b/pkg/nnbd_migration/lib/nnbd_migration.dart
@@ -291,12 +291,18 @@
   /// Optional parameter [warnOnWeakCode] indicates whether weak-only code
   /// should be warned about or removed (in the way specified by
   /// [removeViaComments]).
+  ///
+  /// Optional parameter [transformWhereOrNull] indicates whether Iterable
+  /// methods should be transformed to their "OrNull" equivalents when possible.
+  /// This feature is a work in progress, so by default they are not
+  /// transformed.
   factory NullabilityMigration(NullabilityMigrationListener listener,
       LineInfo Function(String) getLineInfo,
       {bool permissive,
       NullabilityMigrationInstrumentation instrumentation,
       bool removeViaComments,
-      bool warnOnWeakCode}) = NullabilityMigrationImpl;
+      bool warnOnWeakCode,
+      bool transformWhereOrNull}) = NullabilityMigrationImpl;
 
   /// Check if this migration is being run permissively.
   bool get isPermissive;
diff --git a/pkg/nnbd_migration/lib/src/edge_builder.dart b/pkg/nnbd_migration/lib/src/edge_builder.dart
index 962dc5e..282b622 100644
--- a/pkg/nnbd_migration/lib/src/edge_builder.dart
+++ b/pkg/nnbd_migration/lib/src/edge_builder.dart
@@ -32,6 +32,7 @@
 import 'package:nnbd_migration/src/utilities/permissive_mode.dart';
 import 'package:nnbd_migration/src/utilities/resolution_utils.dart';
 import 'package:nnbd_migration/src/utilities/scoped_set.dart';
+import 'package:nnbd_migration/src/utilities/where_or_null_transformer.dart';
 import 'package:nnbd_migration/src/variables.dart';
 
 import 'decorated_type_operations.dart';
@@ -213,10 +214,29 @@
 
   final Map<Token, HintComment> _nullCheckHints = {};
 
-  EdgeBuilder(this.typeProvider, this._typeSystem, this._variables, this._graph,
-      this.source, this.listener, this._decoratedClassHierarchy,
+  /// Helper that assists us in transforming Iterable methods to their "OrNull"
+  /// equivalents, or `null` if we are not doing such transformations.
+  final WhereOrNullTransformer _whereOrNullTransformer;
+
+  /// Deferred processing that should be performed once we have finished
+  /// evaluating the decorated type of a method invocation.
+  final Map<MethodInvocation, DecoratedType Function(DecoratedType)>
+      _deferredMethodInvocationProcessing = {};
+
+  EdgeBuilder(
+      this.typeProvider,
+      this._typeSystem,
+      this._variables,
+      this._graph,
+      this.source,
+      this.listener,
+      this._decoratedClassHierarchy,
+      bool transformWhereOrNull,
       {this.instrumentation})
-      : _inheritanceManager = InheritanceManager3();
+      : _inheritanceManager = InheritanceManager3(),
+        _whereOrNullTransformer = transformWhereOrNull
+            ? WhereOrNullTransformer(typeProvider, _typeSystem)
+            : null;
 
   /// Gets the decorated type of [element] from [_variables], performing any
   /// necessary substitutions.
@@ -880,6 +900,7 @@
       _postDominatedLocals.doScoped(
           elements: node.declaredElement.parameters,
           action: () => _dispatch(node.body));
+      _variables.recordDecoratedExpressionType(node, _currentFunctionType);
       return _currentFunctionType;
     } finally {
       if (node.parent is! FunctionDeclaration) {
@@ -1228,11 +1249,16 @@
           calleeType,
           null,
           invokeType: node.staticInvokeType);
+      // Do any deferred processing for this method invocation.
+      var deferredProcessing = _deferredMethodInvocationProcessing.remove(node);
+      if (deferredProcessing != null) {
+        expressionType = deferredProcessing(expressionType);
+      }
       if (isNullAware) {
         expressionType = expressionType.withNode(
             NullabilityNode.forLUB(targetType.node, expressionType.node));
-        _variables.recordDecoratedExpressionType(node, expressionType);
       }
+      _variables.recordDecoratedExpressionType(node, expressionType);
     }
     _handleArgumentErrorCheckNotNull(node);
     _handleQuiverCheckNotNull(node);
@@ -2260,18 +2286,40 @@
           sourceType = _makeNullableDynamicType(compoundOperatorInfo);
         }
       } else {
-        var unwrappedExpression = expression.unParenthesized;
-        var hard = (questionAssignNode == null &&
-                _postDominatedLocals.isReferenceInScope(expression)) ||
-            // An edge from a cast should be hard, so that the cast type
-            // annotation is appropriately made nullable according to the
-            // destination type.
-            unwrappedExpression is AsExpression;
-        _checkAssignment(edgeOrigin, FixReasonTarget.root,
-            source: sourceType,
-            destination: destinationType,
-            hard: hard,
-            sourceIsFunctionLiteral: expression is FunctionExpression);
+        var transformationInfo =
+            _whereOrNullTransformer?.tryTransformOrElseArgument(expression);
+        if (transformationInfo != null) {
+          // Don't build any edges for this argument; if necessary we'll transform
+          // it rather than make things nullable.  But do save the nullability of
+          // the return value of the `orElse` method, so that we can later connect
+          // it to the nullability of the value returned from the method
+          // invocation.
+          var extraNullability = sourceType.returnType.node;
+          _deferredMethodInvocationProcessing[
+              transformationInfo.methodInvocation] = (methodInvocationType) {
+            var newNode = NullabilityNode.forInferredType(
+                NullabilityNodeTarget.text(
+                    'return value from ${transformationInfo.originalName}'));
+            var origin = IteratorMethodReturnOrigin(
+                source, transformationInfo.methodInvocation);
+            _graph.connect(methodInvocationType.node, newNode, origin);
+            _graph.connect(extraNullability, newNode, origin);
+            return methodInvocationType.withNode(newNode);
+          };
+        } else {
+          var unwrappedExpression = expression.unParenthesized;
+          var hard = (questionAssignNode == null &&
+                  _postDominatedLocals.isReferenceInScope(expression)) ||
+              // An edge from a cast should be hard, so that the cast type
+              // annotation is appropriately made nullable according to the
+              // destination type.
+              unwrappedExpression is AsExpression;
+          _checkAssignment(edgeOrigin, FixReasonTarget.root,
+              source: sourceType,
+              destination: destinationType,
+              hard: hard,
+              sourceIsFunctionLiteral: expression is FunctionExpression);
+        }
       }
       if (destinationLocalVariable != null) {
         _flowAnalysis.write(destinationLocalVariable, sourceType);
diff --git a/pkg/nnbd_migration/lib/src/edge_origin.dart b/pkg/nnbd_migration/lib/src/edge_origin.dart
index 61c6837..81bdf76 100644
--- a/pkg/nnbd_migration/lib/src/edge_origin.dart
+++ b/pkg/nnbd_migration/lib/src/edge_origin.dart
@@ -371,6 +371,19 @@
   EdgeOriginKind get kind => EdgeOriginKind.isCheckMainType;
 }
 
+/// An edge origin used for the return type of an iterator method that might be
+/// changed into an extension method from package:collection.
+class IteratorMethodReturnOrigin extends EdgeOrigin {
+  IteratorMethodReturnOrigin(Source source, AstNode node) : super(source, node);
+
+  @override
+  String get description =>
+      'Call to iterator method with orElse that returns null';
+
+  @override
+  EdgeOriginKind get kind => EdgeOriginKind.iteratorMethodReturn;
+}
+
 /// An edge origin used for the type argument of a list constructor that
 /// specified an initial length, because that type argument must be nullable.
 class ListLengthConstructorOrigin extends EdgeOrigin {
diff --git a/pkg/nnbd_migration/lib/src/fix_aggregator.dart b/pkg/nnbd_migration/lib/src/fix_aggregator.dart
index cc0a510..bf7ff82 100644
--- a/pkg/nnbd_migration/lib/src/fix_aggregator.dart
+++ b/pkg/nnbd_migration/lib/src/fix_aggregator.dart
@@ -9,6 +9,7 @@
 import 'package:analyzer/dart/element/nullability_suffix.dart';
 import 'package:analyzer/dart/element/type.dart';
 import 'package:analyzer/src/dart/ast/ast.dart';
+import 'package:meta/meta.dart';
 import 'package:nnbd_migration/fix_reason_target.dart';
 import 'package:nnbd_migration/instrumentation.dart';
 import 'package:nnbd_migration/nnbd_migration.dart';
@@ -278,6 +279,46 @@
   }
 }
 
+/// Implementation of [NodeChange] specialized for operating on [ArgumentList]
+/// nodes.
+class NodeChangeForArgumentList extends NodeChange<ArgumentList> {
+  /// The set of arguments that should be dropped from this argument list, or
+  /// the empty set if no arguments should be dropped.
+  final Set<Expression> _argumentsToDrop = {};
+
+  NodeChangeForArgumentList() : super._();
+
+  /// Queries the set of arguments that should be dropped from this argument
+  /// list, or the empty set if no arguments should be dropped.
+  @visibleForTesting
+  Iterable<Expression> get argumentsToDrop => _argumentsToDrop;
+
+  @override
+  Iterable<String> get _toStringParts => [
+        if (_argumentsToDrop.isNotEmpty)
+          'argumentsToDrop: {${_argumentsToDrop.join(', ')}}'
+      ];
+
+  /// Updates `this` so that the given [argument] will be dropped.
+  void dropArgument(Expression argument) {
+    _argumentsToDrop.add(argument);
+  }
+
+  @override
+  EditPlan _apply(ArgumentList node, FixAggregator aggregator) {
+    assert(_argumentsToDrop.every((e) => identical(e.parent, node)));
+    List<EditPlan> innerPlans = [];
+    for (var argument in node.arguments) {
+      if (_argumentsToDrop.contains(argument)) {
+        innerPlans.add(aggregator.planner.removeNode(argument));
+      } else {
+        innerPlans.add(aggregator.planForNode(argument));
+      }
+    }
+    return aggregator.planner.passThrough(node, innerPlans: innerPlans);
+  }
+}
+
 /// Implementation of [NodeChange] specialized for operating on [AsExpression]
 /// nodes.
 class NodeChangeForAsExpression extends NodeChangeForExpression<AsExpression> {
@@ -393,13 +434,35 @@
 /// Implementation of [NodeChange] specialized for operating on
 /// [CompilationUnit] nodes.
 class NodeChangeForCompilationUnit extends NodeChange<CompilationUnit> {
+  /// A map of the imports that should be added, or the empty map if no imports
+  /// should be added.
+  ///
+  /// Each import is expressed as a map entry whose key is the URI to import and
+  /// whose value is the set of symbols to show.
+  final Map<String, Set<String>> _addImports = {};
+
   bool removeLanguageVersionComment = false;
 
   NodeChangeForCompilationUnit() : super._();
 
+  /// Queries a map of the imports that should be added, or the empty map if no
+  /// imports should be added.
+  ///
+  /// Each import is expressed as a map entry whose key is the URI to import and
+  /// whose value is the set of symbols to show.
+  @visibleForTesting
+  Map<String, Set<String>> get addImports => _addImports;
+
   @override
-  Iterable<String> get _toStringParts =>
-      [if (removeLanguageVersionComment) 'removeLanguageVersionComment'];
+  Iterable<String> get _toStringParts => [
+        if (_addImports.isNotEmpty) 'addImports: $_addImports',
+        if (removeLanguageVersionComment) 'removeLanguageVersionComment'
+      ];
+
+  /// Updates `this` so that an import of [uri] will be added, showing [name].
+  void addImport(String uri, String name) {
+    (_addImports[uri] ??= {}).add(name);
+  }
 
   @override
   EditPlan _apply(CompilationUnit node, FixAggregator aggregator) {
@@ -412,9 +475,67 @@
               NullabilityFixDescription.removeLanguageVersionComment,
               const {})));
     }
-    innerPlans.addAll(aggregator.innerPlansForNode(node));
+    _processDirectives(node, aggregator, innerPlans);
+    for (var declaration in node.declarations) {
+      innerPlans.add(aggregator.planForNode(declaration));
+    }
     return aggregator.planner.passThrough(node, innerPlans: innerPlans);
   }
+
+  /// Adds the necessary inner plans to [innerPlans] for the directives part of
+  /// [node].  This solely involves adding imports.
+  void _processDirectives(CompilationUnit node, FixAggregator aggregator,
+      List<EditPlan> innerPlans) {
+    List<MapEntry<String, Set<String>>> importsToAdd =
+        _addImports.entries.toList();
+    importsToAdd.sort((x, y) => x.key.compareTo(y.key));
+
+    void insertImport(int offset, MapEntry<String, Set<String>> importToAdd,
+        {String prefix = '', String suffix = '\n'}) {
+      var shownNames = importToAdd.value.toList();
+      shownNames.sort();
+      innerPlans.add(aggregator.planner.insertText(node, offset, [
+        if (prefix.isNotEmpty) AtomicEdit.insert(prefix),
+        AtomicEdit.insert(
+            "import '${importToAdd.key}' show ${shownNames.join(', ')};"),
+        if (suffix.isNotEmpty) AtomicEdit.insert(suffix)
+      ]));
+    }
+
+    if (node.directives.every((d) => d is LibraryDirective)) {
+      while (importsToAdd.isNotEmpty) {
+        insertImport(
+            node.declarations.beginToken.offset, importsToAdd.removeAt(0),
+            suffix: importsToAdd.isEmpty ? '\n\n' : '\n');
+      }
+    } else {
+      for (var directive in node.directives) {
+        while (importsToAdd.isNotEmpty &&
+            _shouldImportGoBefore(importsToAdd.first.key, directive)) {
+          insertImport(directive.offset, importsToAdd.removeAt(0));
+        }
+        innerPlans.add(aggregator.planForNode(directive));
+      }
+      while (importsToAdd.isNotEmpty) {
+        insertImport(node.directives.last.end, importsToAdd.removeAt(0),
+            prefix: '\n', suffix: '');
+      }
+    }
+  }
+
+  /// Determines whether a new import of [newImportUri] should be sorted before
+  /// an existing [directive].
+  bool _shouldImportGoBefore(String newImportUri, Directive directive) {
+    if (directive is ImportDirective) {
+      return newImportUri.compareTo(directive.uriContent) < 0;
+    } else if (directive is LibraryDirective) {
+      // Library directives must come before imports.
+      return false;
+    } else {
+      // Everything else tends to come after imports.
+      return true;
+    }
+  }
 }
 
 /// Common infrastructure used by [NodeChange] objects that operate on AST nodes
@@ -749,6 +870,29 @@
   }
 }
 
+/// Implementation of [NodeChange] specialized for operating on
+/// [SimpleIdentifier] nodes that represent a method name.
+class NodeChangeForMethodName extends NodeChange<SimpleIdentifier> {
+  /// The name the method name should be changed to, or `null` if no change
+  /// should be made.
+  String replacement;
+
+  NodeChangeForMethodName() : super._();
+
+  @override
+  Iterable<String> get _toStringParts =>
+      [if (replacement != null) 'replacement: $replacement'];
+
+  @override
+  EditPlan _apply(SimpleIdentifier node, FixAggregator aggregator) {
+    if (replacement != null) {
+      return aggregator.planner.replace(node, [AtomicEdit.insert(replacement)]);
+    } else {
+      return aggregator.innerPlanForNode(node);
+    }
+  }
+}
+
 /// Common infrastructure used by [NodeChange] objects that operate on AST nodes
 /// with that can be null-aware (method invocations and propety accesses).
 mixin NodeChangeForNullAware<N extends Expression> on NodeChange<N> {
@@ -831,6 +975,60 @@
   }
 }
 
+/// Implementation of [NodeChange] specialized for operating on [ShowCombinator]
+/// nodes.
+class NodeChangeForShowCombinator extends NodeChange<ShowCombinator> {
+  /// A set of the names that should be added, or the empty set if no names
+  /// should be added.
+  final Set<String> _addNames = {};
+
+  NodeChangeForShowCombinator() : super._();
+
+  /// Queries the set of names that should be added, or the empty set if no
+  /// names should be added.
+  @visibleForTesting
+  Iterable<String> get addNames => _addNames;
+
+  @override
+  Iterable<String> get _toStringParts => [
+        if (_addNames.isNotEmpty) 'addNames: $_addNames',
+      ];
+
+  /// Updates `this` so that [name] will be added.
+  void addName(String name) {
+    _addNames.add(name);
+  }
+
+  @override
+  EditPlan _apply(ShowCombinator node, FixAggregator aggregator) {
+    List<EditPlan> innerPlans = [];
+    List<String> namesToAdd = _addNames.toList();
+    namesToAdd.sort();
+
+    void insertName(int offset, String nameToAdd,
+        {String prefix = '', String suffix = ', '}) {
+      innerPlans.add(aggregator.planner.insertText(node, offset, [
+        if (prefix.isNotEmpty) AtomicEdit.insert(prefix),
+        AtomicEdit.insert(nameToAdd),
+        if (suffix.isNotEmpty) AtomicEdit.insert(suffix)
+      ]));
+    }
+
+    for (var shownName in node.shownNames) {
+      while (namesToAdd.isNotEmpty &&
+          namesToAdd.first.compareTo(shownName.name) < 0) {
+        insertName(shownName.offset, namesToAdd.removeAt(0));
+      }
+      innerPlans.add(aggregator.planForNode(shownName));
+    }
+    while (namesToAdd.isNotEmpty) {
+      insertName(node.shownNames.last.end, namesToAdd.removeAt(0),
+          prefix: ', ', suffix: '');
+    }
+    return aggregator.planner.passThrough(node, innerPlans: innerPlans);
+  }
+}
+
 /// Implementation of [NodeChange] specialized for operating on
 /// [SimpleFormalParameter] nodes.
 class NodeChangeForSimpleFormalParameter
@@ -1036,6 +1234,10 @@
   NodeChange visitAnnotation(Annotation node) => NodeChangeForAnnotation();
 
   @override
+  NodeChange visitArgumentList(ArgumentList node) =>
+      NodeChangeForArgumentList();
+
+  @override
   NodeChange visitAsExpression(AsExpression node) =>
       NodeChangeForAsExpression();
 
@@ -1098,10 +1300,24 @@
       NodeChangeForPropertyAccess();
 
   @override
+  NodeChange visitShowCombinator(ShowCombinator node) =>
+      NodeChangeForShowCombinator();
+
+  @override
   NodeChange visitSimpleFormalParameter(SimpleFormalParameter node) =>
       NodeChangeForSimpleFormalParameter();
 
   @override
+  NodeChange visitSimpleIdentifier(SimpleIdentifier node) {
+    var parent = node.parent;
+    if (parent is MethodInvocation && identical(node, parent.methodName)) {
+      return NodeChangeForMethodName();
+    } else {
+      return super.visitSimpleIdentifier(node);
+    }
+  }
+
+  @override
   NodeChange visitTypeName(TypeName node) => NodeChangeForTypeAnnotation();
 
   @override
diff --git a/pkg/nnbd_migration/lib/src/fix_builder.dart b/pkg/nnbd_migration/lib/src/fix_builder.dart
index 9e5e486..55b7a16 100644
--- a/pkg/nnbd_migration/lib/src/fix_builder.dart
+++ b/pkg/nnbd_migration/lib/src/fix_builder.dart
@@ -26,6 +26,7 @@
 import 'package:analyzer/src/generated/resolver.dart';
 import 'package:analyzer/src/generated/source.dart';
 import 'package:analyzer/src/generated/utilities_dart.dart';
+import 'package:meta/meta.dart';
 import 'package:nnbd_migration/fix_reason_target.dart';
 import 'package:nnbd_migration/instrumentation.dart';
 import 'package:nnbd_migration/nnbd_migration.dart';
@@ -37,6 +38,7 @@
 import 'package:nnbd_migration/src/utilities/hint_utils.dart';
 import 'package:nnbd_migration/src/utilities/permissive_mode.dart';
 import 'package:nnbd_migration/src/utilities/resolution_utils.dart';
+import 'package:nnbd_migration/src/utilities/where_or_null_transformer.dart';
 import 'package:nnbd_migration/src/variables.dart';
 
 bool _isIncrementOrDecrementOperator(TokenType tokenType) {
@@ -118,6 +120,15 @@
 
   final NullabilityGraph _graph;
 
+  /// Helper that assists us in transforming Iterable methods to their "OrNull"
+  /// equivalents, or `null` if we are not doing such transformations.
+  final WhereOrNullTransformer _whereOrNullTransformer;
+
+  /// Indicates whether an import of package:collection's `IterableExtension`
+  /// will need to be added.
+  @visibleForTesting
+  bool needsIterableExtension = false;
+
   factory FixBuilder(
       Source source,
       DecoratedClassHierarchy decoratedClassHierarchy,
@@ -128,7 +139,8 @@
       NullabilityMigrationListener listener,
       CompilationUnit unit,
       bool warnOnWeakCode,
-      NullabilityGraph graph) {
+      NullabilityGraph graph,
+      bool transformWhereOrNull) {
     var migrationResolutionHooks = MigrationResolutionHooksImpl();
     return FixBuilder._(
         decoratedClassHierarchy,
@@ -143,7 +155,8 @@
         unit,
         migrationResolutionHooks,
         warnOnWeakCode,
-        graph);
+        graph,
+        transformWhereOrNull);
   }
 
   FixBuilder._(
@@ -156,8 +169,12 @@
       this.unit,
       this.migrationResolutionHooks,
       this.warnOnWeakCode,
-      this._graph)
-      : typeProvider = _typeSystem.typeProvider {
+      this._graph,
+      bool transformWhereOrNull)
+      : typeProvider = _typeSystem.typeProvider,
+        _whereOrNullTransformer = transformWhereOrNull
+            ? WhereOrNullTransformer(_typeSystem.typeProvider, _typeSystem)
+            : null {
     migrationResolutionHooks._fixBuilder = this;
     assert(_typeSystem.isNonNullableByDefault);
     assert((typeProvider as TypeProviderImpl).isNonNullableByDefault);
@@ -308,6 +325,11 @@
   FlowAnalysis<AstNode, Statement, Expression, PromotableElement, DartType>
       _flowAnalysis;
 
+  /// Deferred processing that should be performed once we have finished
+  /// evaluating the type of a method invocation.
+  final Map<MethodInvocation, DartType Function(DartType)>
+      _deferredMethodInvocationProcessing = {};
+
   TypeProvider get typeProvider => _fixBuilder.typeProvider;
 
   @override
@@ -584,6 +606,12 @@
 
   DartType _modifyRValueType(Expression node, DartType type,
       {DartType context}) {
+    if (node is MethodInvocation) {
+      var deferredProcessing = _deferredMethodInvocationProcessing.remove(node);
+      if (deferredProcessing != null) {
+        type = deferredProcessing(type);
+      }
+    }
     var hint =
         _fixBuilder._variables.getNullCheckHint(_fixBuilder.source, node);
     if (hint != null) {
@@ -602,6 +630,24 @@
     context ??=
         InferenceContext.getContext(ancestor) ?? DynamicTypeImpl.instance;
     if (!_fixBuilder._typeSystem.isSubtypeOf(type, context)) {
+      var transformationInfo =
+          _fixBuilder._whereOrNullTransformer?.tryTransformOrElseArgument(node);
+      if (transformationInfo != null) {
+        // We can fix this by dropping the node and changing the method call.
+        _fixBuilder.needsIterableExtension = true;
+        (_fixBuilder._getChange(transformationInfo.methodInvocation.methodName)
+                as NodeChangeForMethodName)
+            .replacement = transformationInfo.replacementName;
+        (_fixBuilder._getChange(
+                    transformationInfo.methodInvocation.argumentList)
+                as NodeChangeForArgumentList)
+            .dropArgument(transformationInfo.orElseArgument);
+        _deferredMethodInvocationProcessing[
+                transformationInfo.methodInvocation] =
+            (methodInvocationType) => _fixBuilder._typeSystem
+                .makeNullable(methodInvocationType as TypeImpl);
+        return type;
+      }
       // Either a cast or a null check is needed.  We prefer to do a null
       // check if we can.
       var nonNullType = _fixBuilder._typeSystem.promoteToNonNull(type);
@@ -855,6 +901,21 @@
       (_fixBuilder._getChange(node) as NodeChangeForCompilationUnit)
           .removeLanguageVersionComment = true;
     }
+    if (_fixBuilder.needsIterableExtension) {
+      var packageCollectionImport =
+          _findImportDirective(node, 'package:collection/collection.dart');
+      if (packageCollectionImport != null) {
+        for (var combinator in packageCollectionImport.combinators) {
+          if (combinator is ShowCombinator) {
+            _ensureShows(combinator, 'IterableExtension');
+          }
+        }
+      } else {
+        (_fixBuilder._getChange(node) as NodeChangeForCompilationUnit)
+            .addImport(
+                'package:collection/collection.dart', 'IterableExtension');
+      }
+    }
     super.visitCompilationUnit(node);
   }
 
@@ -930,6 +991,28 @@
     }
     super.visitVariableDeclarationList(node);
   }
+
+  /// Creates the necessary changes to ensure that [combinator] shows [name].
+  void _ensureShows(ShowCombinator combinator, String name) {
+    if (combinator.shownNames.any((shownName) => shownName.name == name)) {
+      return;
+    }
+    (_fixBuilder._getChange(combinator) as NodeChangeForShowCombinator)
+        .addName(name);
+  }
+
+  /// Searches [unit] for an unprefixed import directive whose URI matches
+  /// [uri], returning it if found, or `null` if not found.
+  ImportDirective _findImportDirective(CompilationUnit unit, String uri) {
+    for (var directive in unit.directives) {
+      if (directive is ImportDirective &&
+          directive.prefix == null &&
+          directive.uriContent == uri) {
+        return directive;
+      }
+    }
+    return null;
+  }
 }
 
 /// Visitor that computes additional migrations on behalf of [FixBuilder] that
diff --git a/pkg/nnbd_migration/lib/src/nullability_migration_impl.dart b/pkg/nnbd_migration/lib/src/nullability_migration_impl.dart
index 8054089..101e5ea 100644
--- a/pkg/nnbd_migration/lib/src/nullability_migration_impl.dart
+++ b/pkg/nnbd_migration/lib/src/nullability_migration_impl.dart
@@ -59,6 +59,10 @@
 
   final LineInfo Function(String) _getLineInfo;
 
+  /// Indicates whether we should transform iterable methods taking an "orElse"
+  /// parameter into their "OrNull" equivalents if possible.
+  final bool transformWhereOrNull;
+
   /// Prepares to perform nullability migration.
   ///
   /// If [permissive] is `true`, exception handling logic will try to proceed
@@ -72,12 +76,18 @@
   /// Optional parameter [warnOnWeakCode] indicates whether weak-only code
   /// should be warned about or removed (in the way specified by
   /// [removeViaComments]).
+  ///
+  /// Optional parameter [transformWhereOrNull] indicates whether Iterable
+  /// methods should be transformed to their "OrNull" equivalents when possible.
+  /// This feature is a work in progress, so by default they are not
+  /// transformed.
   NullabilityMigrationImpl(NullabilityMigrationListener listener,
       LineInfo Function(String) getLineInfo,
       {bool permissive = false,
       NullabilityMigrationInstrumentation instrumentation,
       bool removeViaComments = false,
-      bool warnOnWeakCode = true})
+      bool warnOnWeakCode = true,
+      bool transformWhereOrNull = false})
       : this._(
             listener,
             NullabilityGraph(instrumentation: instrumentation),
@@ -85,7 +95,8 @@
             instrumentation,
             removeViaComments,
             warnOnWeakCode,
-            getLineInfo);
+            getLineInfo,
+            transformWhereOrNull);
 
   NullabilityMigrationImpl._(
       this.listener,
@@ -94,7 +105,8 @@
       this._instrumentation,
       this.removeViaComments,
       this.warnOnWeakCode,
-      this._getLineInfo) {
+      this._getLineInfo,
+      this.transformWhereOrNull) {
     _instrumentation?.immutableNodes(_graph.never, _graph.always);
     _postmortemFileWriter?.graph = _graph;
   }
@@ -131,7 +143,8 @@
         _permissive ? listener : null,
         unit,
         warnOnWeakCode,
-        _graph);
+        _graph,
+        transformWhereOrNull);
     try {
       DecoratedTypeParameterBounds.current = _decoratedTypeParameterBounds;
       fixBuilder.visitAll();
@@ -214,6 +227,7 @@
           unit.declaredElement.source,
           _permissive ? listener : null,
           _decoratedClassHierarchy,
+          transformWhereOrNull,
           instrumentation: _instrumentation));
     } finally {
       DecoratedTypeParameterBounds.current = null;
diff --git a/pkg/nnbd_migration/lib/src/utilities/where_or_null_transformer.dart b/pkg/nnbd_migration/lib/src/utilities/where_or_null_transformer.dart
new file mode 100644
index 0000000..7bc0288
--- /dev/null
+++ b/pkg/nnbd_migration/lib/src/utilities/where_or_null_transformer.dart
@@ -0,0 +1,140 @@
+// Copyright (c) 2020, 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:analyzer/dart/ast/ast.dart';
+import 'package:analyzer/dart/element/element.dart';
+import 'package:analyzer/dart/element/type_provider.dart';
+import 'package:analyzer/dart/element/type_system.dart';
+
+/// Information about a method call that we might want to transform into its
+/// "OrNull" counterpart.  See [WhereOrNullTransformer] for more information.
+class WhereOrNullTransformationInfo {
+  /// AST node of the method invocation.
+  final MethodInvocation methodInvocation;
+
+  /// AST node of the "orElse" argument of the method invocation.
+  final NamedExpression orElseArgument;
+
+  /// Original name of the method being called, prior to transformation.
+  final String originalName;
+
+  /// New method to call, after transformation.
+  final String replacementName;
+
+  WhereOrNullTransformationInfo(this.methodInvocation, this.orElseArgument,
+      this.originalName, this.replacementName);
+}
+
+/// Methods to assist in transforming calls to the `Iterable` methods
+/// `firstWhere`, `lastWhere`, and `singleWhere` into calls to the
+/// `package:collection` methods `firstWhereOrNull`, `lastWhereOrNull`, or
+/// `singleWhereOrNull`, where possible.
+///
+/// An example of the kind of code that can be transformed is:
+///
+///     int firstEven(Iterable<int> x)
+///         => x.firstWhere((x) => x.isEven, orElse: () => null);
+///
+/// We transform this into:
+///
+///     int firstEven(Iterable<int> x)
+///         => x.firstWhereOrNull((x) => x.isEven);
+///
+/// Without this transformation, the migrated result would have been:
+///
+///     int firstEven(Iterable<int?> x)
+///         => x.firstWhere((x) => x.isEven, orElse: () => null);
+///
+/// Which would have placed an otherwise unnecessary nullability requirement on
+/// the type argument of the type of `x`.
+class WhereOrNullTransformer {
+  static const _replacementNames = {
+    'firstWhere': 'firstWhereOrNull',
+    'lastWhere': 'lastWhereOrNull',
+    'singleWhere': 'singleWhereOrNull'
+  };
+
+  final TypeProvider _typeProvider;
+
+  final TypeSystem _typeSystem;
+
+  WhereOrNullTransformer(this._typeProvider, this._typeSystem);
+
+  /// If [expression] is the `orElse` argument of a call that can be
+  /// transformed, returns information about the transformable call; otherwise
+  /// returns `null`.
+  WhereOrNullTransformationInfo tryTransformOrElseArgument(
+          Expression expression) =>
+      _tryTransformMethodInvocation(expression?.parent?.parent?.parent);
+
+  /// Searches [argumentList] for a named argument with the name "orElse".  If
+  /// such an argument is found, and no other named arguments are found, it is
+  /// returned; otherwise `null` is returned.
+  NamedExpression _findOrElseArgument(ArgumentList argumentList) {
+    NamedExpression orElseArgument;
+    for (var argument in argumentList.arguments) {
+      if (argument is NamedExpression) {
+        if (argument.name.label.name == 'orElse') {
+          orElseArgument = argument;
+        } else {
+          // The presence of an unexpected named argument means the user is
+          // calling their own override of the method, and presumably they are
+          // using this named argument to trigger a special behavior of their
+          // override.  So don't try to replace it.
+          return null;
+        }
+      }
+    }
+    return orElseArgument;
+  }
+
+  /// Determines if [element] is a method that can be transformed; if it can,
+  /// the name of the replacement is returned; otherwise, `null` is returned.
+  String _getTransformableMethodReplacementName(Element element) {
+    if (element is MethodElement) {
+      if (element.isStatic) return null;
+      var replacementName = _replacementNames[element.name];
+      if (replacementName == null) return null;
+      var enclosingElement = element.declaration.enclosingElement;
+      if (enclosingElement is ClassElement) {
+        // If the class is `Iterable` or a subtype of it, we consider the user
+        // to be calling a transformable method.
+        if (_typeSystem.isSubtypeOf(
+            enclosingElement.thisType, _typeProvider.iterableDynamicType)) {
+          return replacementName;
+        }
+      }
+    }
+    return null;
+  }
+
+  /// Checks whether [expression] is of the form `() => null`.
+  bool _isClosureReturningNull(Expression expression) {
+    if (expression is FunctionExpression) {
+      if (expression.typeParameters != null) return false;
+      if (expression.parameters.parameters.isNotEmpty) return false;
+      var body = expression.body;
+      if (body is ExpressionFunctionBody) {
+        if (body.expression is NullLiteral) return true;
+      }
+    }
+    return false;
+  }
+
+  /// If [node] is a call that can be transformed, returns information about the
+  /// transformable call; otherwise returns `null`.
+  WhereOrNullTransformationInfo _tryTransformMethodInvocation(AstNode node) {
+    if (node is MethodInvocation) {
+      var replacementName =
+          _getTransformableMethodReplacementName(node.methodName.staticElement);
+      if (replacementName == null) return null;
+      var orElseArgument = _findOrElseArgument(node.argumentList);
+      if (orElseArgument == null) return null;
+      if (!_isClosureReturningNull(orElseArgument.expression)) return null;
+      return WhereOrNullTransformationInfo(
+          node, orElseArgument, node.methodName.name, replacementName);
+    }
+    return null;
+  }
+}
diff --git a/pkg/nnbd_migration/test/api_test.dart b/pkg/nnbd_migration/test/api_test.dart
index 7205cd2..629a6ac 100644
--- a/pkg/nnbd_migration/test/api_test.dart
+++ b/pkg/nnbd_migration/test/api_test.dart
@@ -53,7 +53,8 @@
     var migration = NullabilityMigration(listener, getLineInfo,
         permissive: _usePermissiveMode,
         removeViaComments: removeViaComments,
-        warnOnWeakCode: warnOnWeakCode);
+        warnOnWeakCode: warnOnWeakCode,
+        transformWhereOrNull: true);
     for (var path in input.keys) {
       if (!(session.getFile(path)).isPart) {
         for (var unit in (await session.getResolvedLibrary(path)).units) {
@@ -2749,6 +2750,76 @@
     await _checkSingleFileChanges(content, expected);
   }
 
+  Future<void> test_firstWhere_non_nullable() async {
+    var content = '''
+int firstEven(Iterable<int> x)
+    => x.firstWhere((x) => x.isEven, orElse: () => null);
+''';
+    var expected = '''
+import 'package:collection/collection.dart' show IterableExtension;
+
+int? firstEven(Iterable<int> x)
+    => x.firstWhereOrNull((x) => x.isEven);
+''';
+    await _checkSingleFileChanges(content, expected);
+  }
+
+  Future<void> test_firstWhere_non_nullable_with_cast() async {
+    var content = '''
+int firstNonZero(Iterable<num> x)
+    => x.firstWhere((x) => x != 0, orElse: () => null);
+''';
+    var expected = '''
+import 'package:collection/collection.dart' show IterableExtension;
+
+int? firstNonZero(Iterable<num> x)
+    => x.firstWhereOrNull((x) => x != 0) as int?;
+''';
+    await _checkSingleFileChanges(content, expected);
+  }
+
+  Future<void> test_firstWhere_non_nullable_with_non_null_assertion() async {
+    var content = '''
+int/*!*/ firstEven(Iterable<int> x)
+    => x.firstWhere((x) => x.isEven, orElse: () => null);
+''';
+    var expected = '''
+import 'package:collection/collection.dart' show IterableExtension;
+
+int firstEven(Iterable<int> x)
+    => x.firstWhereOrNull((x) => x.isEven)!;
+''';
+    await _checkSingleFileChanges(content, expected);
+  }
+
+  Future<void> test_firstWhere_nullable() async {
+    var content = '''
+int firstEven(Iterable<int> x)
+    => x.firstWhere((x) => x.isEven, orElse: () => null);
+f() => firstEven([null]);
+''';
+    var expected = '''
+int? firstEven(Iterable<int?> x)
+    => x.firstWhere((x) => x!.isEven, orElse: () => null);
+f() => firstEven([null]);
+''';
+    await _checkSingleFileChanges(content, expected);
+  }
+
+  Future<void> test_firstWhere_nullable_with_cast() async {
+    var content = '''
+int firstNonZero(Iterable<num> x)
+    => x.firstWhere((x) => x != 0, orElse: () => null);
+f() => firstNonZero([null]);
+''';
+    var expected = '''
+int? firstNonZero(Iterable<num?> x)
+    => x.firstWhere((x) => x != 0, orElse: () => null) as int?;
+f() => firstNonZero([null]);
+''';
+    await _checkSingleFileChanges(content, expected);
+  }
+
   Future<void> test_flow_analysis_complex() async {
     var content = '''
 int f(int x) {
@@ -4012,6 +4083,34 @@
     await _checkSingleFileChanges(content, expected);
   }
 
+  Future<void> test_lastWhere_non_nullable() async {
+    var content = '''
+int lastEven(Iterable<int> x)
+    => x.lastWhere((x) => x.isEven, orElse: () => null);
+''';
+    var expected = '''
+import 'package:collection/collection.dart' show IterableExtension;
+
+int? lastEven(Iterable<int> x)
+    => x.lastWhereOrNull((x) => x.isEven);
+''';
+    await _checkSingleFileChanges(content, expected);
+  }
+
+  Future<void> test_lastWhere_nullable() async {
+    var content = '''
+int lastEven(Iterable<int> x)
+    => x.lastWhere((x) => x.isEven, orElse: () => null);
+f() => lastEven([null]);
+''';
+    var expected = '''
+int? lastEven(Iterable<int?> x)
+    => x.lastWhere((x) => x!.isEven, orElse: () => null);
+f() => lastEven([null]);
+''';
+    await _checkSingleFileChanges(content, expected);
+  }
+
   Future<void> test_late_final_hint_instance_field_without_constructor() async {
     var content = '''
 class C {
@@ -6013,6 +6112,34 @@
     await _checkSingleFileChanges(content, expected);
   }
 
+  Future<void> test_singleWhere_non_nullable() async {
+    var content = '''
+int singleEven(Iterable<int> x)
+    => x.singleWhere((x) => x.isEven, orElse: () => null);
+''';
+    var expected = '''
+import 'package:collection/collection.dart' show IterableExtension;
+
+int? singleEven(Iterable<int> x)
+    => x.singleWhereOrNull((x) => x.isEven);
+''';
+    await _checkSingleFileChanges(content, expected);
+  }
+
+  Future<void> test_singleWhere_nullable() async {
+    var content = '''
+int singleEven(Iterable<int> x)
+    => x.singleWhere((x) => x.isEven, orElse: () => null);
+f() => singleEven([null]);
+''';
+    var expected = '''
+int? singleEven(Iterable<int?> x)
+    => x.singleWhere((x) => x!.isEven, orElse: () => null);
+f() => singleEven([null]);
+''';
+    await _checkSingleFileChanges(content, expected);
+  }
+
   @FailingTest(issue: 'https://github.com/dart-lang/sdk/issues/40728')
   Future<void> test_soft_edge_for_assigned_variable() async {
     var content = '''
diff --git a/pkg/nnbd_migration/test/edge_builder_test.dart b/pkg/nnbd_migration/test/edge_builder_test.dart
index 2e9efaa..04aebe5 100644
--- a/pkg/nnbd_migration/test/edge_builder_test.dart
+++ b/pkg/nnbd_migration/test/edge_builder_test.dart
@@ -3126,6 +3126,40 @@
         hard: true);
   }
 
+  Future<void> test_firstWhere_edges() async {
+    await analyze('''
+int firstEven(Iterable<int> x)
+    => x.firstWhere((x) => x.isEven, orElse: () => null);
+''');
+
+    // Normally there would be an edge from the return type of `() => null` to
+    // a substitution node that pointed to the type argument to the type of `x`,
+    // and another substitution node would point from this to the return type of
+    // `firstEven`.  However, since we may replace `firstWhere` with
+    // `firstWhereOrNull` in order to avoid having to make `x`'s type argument
+    // nullable, we need a synthetic edge to ensure that the return type of
+    // `firstEven` is nullable.
+    var closureReturnType = decoratedExpressionType('() => null').returnType;
+    var firstWhereReturnType = variables
+        .decoratedExpressionType(findNode.methodInvocation('firstWhere'));
+    assertEdge(closureReturnType.node, firstWhereReturnType.node, hard: false);
+
+    // There should also be an edge from a substitution node to the return type
+    // of `firstWhere`, to account for the normal data flow (when the element is
+    // found).
+    var typeParameterType = decoratedTypeAnnotation('int>');
+    var firstWhereType = variables.decoratedElementType(findNode
+        .methodInvocation('firstWhere')
+        .methodName
+        .staticElement
+        .declaration);
+    assertEdge(
+        substitutionNode(
+            typeParameterType.node, firstWhereType.returnType.node),
+        firstWhereReturnType.node,
+        hard: false);
+  }
+
   Future<void> test_for_each_element_with_declaration() async {
     await analyze('''
 void f(List<int> l) {
diff --git a/pkg/nnbd_migration/test/fix_aggregator_test.dart b/pkg/nnbd_migration/test/fix_aggregator_test.dart
index 7486bc1..980e9da 100644
--- a/pkg/nnbd_migration/test/fix_aggregator_test.dart
+++ b/pkg/nnbd_migration/test/fix_aggregator_test.dart
@@ -32,6 +32,209 @@
       (testAnalysisResult.typeProvider as TypeProviderImpl)
           .asNonNullableByDefault;
 
+  Future<void> test_addImport_after_library() async {
+    await analyze('''
+library foo;
+
+main() {}
+''');
+    var previewInfo = run({
+      findNode.unit: NodeChangeForCompilationUnit()
+        ..addImport('package:collection/collection.dart', 'IterableExtension')
+    });
+    expect(previewInfo.applyTo(code), '''
+library foo;
+
+import 'package:collection/collection.dart' show IterableExtension;
+
+main() {}
+''');
+  }
+
+  Future<void> test_addImport_after_library_before_other() async {
+    addPackageFile('fixnum', 'fixnum.dart', '');
+    await analyze('''
+library foo;
+
+import 'package:fixnum/fixnum.dart';
+
+main() {}
+''');
+    var previewInfo = run({
+      findNode.unit: NodeChangeForCompilationUnit()
+        ..addImport('package:collection/collection.dart', 'IterableExtension')
+    });
+    expect(previewInfo.applyTo(code), '''
+library foo;
+
+import 'package:collection/collection.dart' show IterableExtension;
+import 'package:fixnum/fixnum.dart';
+
+main() {}
+''');
+  }
+
+  Future<void> test_addImport_atEnd_multiple() async {
+    addPackageFile('args', 'args.dart', '');
+    await analyze('''
+import 'package:args/args.dart';
+
+main() {}
+''');
+    var previewInfo = run({
+      findNode.unit: NodeChangeForCompilationUnit()
+        ..addImport('package:fixnum/fixnum.dart', 'Int32')
+        ..addImport('package:collection/collection.dart', 'IterableExtension')
+    });
+    expect(previewInfo.applyTo(code), '''
+import 'package:args/args.dart';
+import 'package:collection/collection.dart' show IterableExtension;
+import 'package:fixnum/fixnum.dart' show Int32;
+
+main() {}
+''');
+  }
+
+  Future<void> test_addImport_atStart_multiple() async {
+    addPackageFile('fixnum', 'fixnum.dart', '');
+    await analyze('''
+import 'package:fixnum/fixnum.dart';
+
+main() {}
+''');
+    var previewInfo = run({
+      findNode.unit: NodeChangeForCompilationUnit()
+        ..addImport('package:collection/collection.dart', 'IterableExtension')
+        ..addImport('package:args/args.dart', 'ArgParser')
+    });
+    expect(previewInfo.applyTo(code), '''
+import 'package:args/args.dart' show ArgParser;
+import 'package:collection/collection.dart' show IterableExtension;
+import 'package:fixnum/fixnum.dart';
+
+main() {}
+''');
+  }
+
+  Future<void> test_addImport_before_export() async {
+    await analyze('''
+export 'dart:async';
+
+main() {}
+''');
+    var previewInfo = run({
+      findNode.unit: NodeChangeForCompilationUnit()
+        ..addImport('package:collection/collection.dart', 'IterableExtension')
+    });
+    expect(previewInfo.applyTo(code), '''
+import 'package:collection/collection.dart' show IterableExtension;
+export 'dart:async';
+
+main() {}
+''');
+  }
+
+  Future<void> test_addImport_no_previous_imports_multiple() async {
+    await analyze('''
+main() {}
+''');
+    var previewInfo = run({
+      findNode.unit: NodeChangeForCompilationUnit()
+        ..addImport('dart:async', 'Future')
+        ..addImport('dart:math', 'sin')
+    });
+    expect(previewInfo.applyTo(code), '''
+import 'dart:async' show Future;
+import 'dart:math' show sin;
+
+main() {}
+''');
+  }
+
+  Future<void> test_addImport_recursive() async {
+    addPackageFile('args', 'args.dart', '');
+    addPackageFile('fixnum', 'fixnum.dart', 'class Int32 {}');
+    await analyze('''
+import 'package:args/args.dart';
+import 'package:fixnum/fixnum.dart' show Int32;
+
+main() => null;
+''');
+    var previewInfo = run({
+      findNode.unit: NodeChangeForCompilationUnit()
+        ..addImport('package:collection/collection.dart', 'IterableExtension'),
+      findNode.import('package:fixnum').combinators[0]:
+          NodeChangeForShowCombinator()..addName('Int64'),
+      findNode.expression('null'): NodeChangeForExpression()..addNullCheck(null)
+    });
+    expect(previewInfo.applyTo(code), '''
+import 'package:args/args.dart';
+import 'package:collection/collection.dart' show IterableExtension;
+import 'package:fixnum/fixnum.dart' show Int32, Int64;
+
+main() => null!;
+''');
+  }
+
+  Future<void> test_addImport_sort_shown_names() async {
+    await analyze('''
+main() {}
+''');
+    var previewInfo = run({
+      findNode.unit: NodeChangeForCompilationUnit()
+        ..addImport('dart:async', 'Stream')
+        ..addImport('dart:async', 'Future')
+    });
+    expect(previewInfo.applyTo(code), '''
+import 'dart:async' show Future, Stream;
+
+main() {}
+''');
+  }
+
+  Future<void> test_addImport_sorted() async {
+    addPackageFile('args', 'args.dart', '');
+    addPackageFile('fixnum', 'fixnum.dart', '');
+    await analyze('''
+import 'package:args/args.dart';
+import 'package:fixnum/fixnum.dart';
+
+main() {}
+''');
+    var previewInfo = run({
+      findNode.unit: NodeChangeForCompilationUnit()
+        ..addImport('package:collection/collection.dart', 'IterableExtension')
+    });
+    expect(previewInfo.applyTo(code), '''
+import 'package:args/args.dart';
+import 'package:collection/collection.dart' show IterableExtension;
+import 'package:fixnum/fixnum.dart';
+
+main() {}
+''');
+  }
+
+  Future<void> test_addImport_sorted_multiple() async {
+    addPackageFile('collection', 'collection.dart', '');
+    await analyze('''
+import 'package:collection/collection.dart';
+
+main() {}
+''');
+    var previewInfo = run({
+      findNode.unit: NodeChangeForCompilationUnit()
+        ..addImport('package:fixnum/fixnum.dart', 'Int32')
+        ..addImport('package:args/args.dart', 'ArgParser')
+    });
+    expect(previewInfo.applyTo(code), '''
+import 'package:args/args.dart' show ArgParser;
+import 'package:collection/collection.dart';
+import 'package:fixnum/fixnum.dart' show Int32;
+
+main() {}
+''');
+  }
+
   Future<void> test_addRequired() async {
     await analyze('f({int x}) => 0;');
     var previewInfo = run({
@@ -41,6 +244,45 @@
     expect(previewInfo.applyTo(code), 'f({required int x}) => 0;');
   }
 
+  Future<void> test_addShownName_atEnd_multiple() async {
+    await analyze("import 'dart:math' show cos;");
+    var previewInfo = run({
+      findNode.import('dart:math').combinators[0]: NodeChangeForShowCombinator()
+        ..addName('tan')
+        ..addName('sin')
+    });
+    expect(previewInfo.applyTo(code), "import 'dart:math' show cos, sin, tan;");
+  }
+
+  Future<void> test_addShownName_atStart_multiple() async {
+    await analyze("import 'dart:math' show tan;");
+    var previewInfo = run({
+      findNode.import('dart:math').combinators[0]: NodeChangeForShowCombinator()
+        ..addName('sin')
+        ..addName('cos')
+    });
+    expect(previewInfo.applyTo(code), "import 'dart:math' show cos, sin, tan;");
+  }
+
+  Future<void> test_addShownName_sorted() async {
+    await analyze("import 'dart:math' show cos, tan;");
+    var previewInfo = run({
+      findNode.import('dart:math').combinators[0]: NodeChangeForShowCombinator()
+        ..addName('sin')
+    });
+    expect(previewInfo.applyTo(code), "import 'dart:math' show cos, sin, tan;");
+  }
+
+  Future<void> test_addShownName_sorted_multiple() async {
+    await analyze("import 'dart:math' show sin;");
+    var previewInfo = run({
+      findNode.import('dart:math').combinators[0]: NodeChangeForShowCombinator()
+        ..addName('tan')
+        ..addName('cos')
+    });
+    expect(previewInfo.applyTo(code), "import 'dart:math' show cos, sin, tan;");
+  }
+
   Future<void> test_adjacentFixes() async {
     await analyze('f(a, b) => a + b;');
     var aRef = findNode.simple('a +');
@@ -54,6 +296,56 @@
     expect(previewInfo.applyTo(code), 'f(a, b) => (a! + b!)!;');
   }
 
+  Future<void> test_argument_list_drop_all_arguments() async {
+    var content = '''
+f([int x, int y]) => null;
+g(int x, int y) => f(x, y);
+''';
+    await analyze(content);
+    var previewInfo = run({
+      findNode.methodInvocation('f(x').argumentList: NodeChangeForArgumentList()
+        ..dropArgument(findNode.simple('y);'))
+        ..dropArgument(findNode.simple('x, y'))
+    });
+    expect(previewInfo.applyTo(code), '''
+f([int x, int y]) => null;
+g(int x, int y) => f();
+''');
+  }
+
+  Future<void> test_argument_list_drop_one_argument() async {
+    var content = '''
+f([int x, int y]) => null;
+g(int x, int y) => f(x, y);
+''';
+    await analyze(content);
+    var previewInfo = run({
+      findNode.methodInvocation('f(x').argumentList: NodeChangeForArgumentList()
+        ..dropArgument(findNode.simple('y);'))
+    });
+    expect(previewInfo.applyTo(code), '''
+f([int x, int y]) => null;
+g(int x, int y) => f(x);
+''');
+  }
+
+  Future<void> test_argument_list_recursive_changes() async {
+    var content = '''
+f([int x, int y]) => null;
+g(int x, int y) => f(x, y);
+''';
+    await analyze(content);
+    var previewInfo = run({
+      findNode.methodInvocation('f(x').argumentList: NodeChangeForArgumentList()
+        ..dropArgument(findNode.simple('y);')),
+      findNode.simple('x, y'): NodeChangeForExpression()..addNullCheck(null)
+    });
+    expect(previewInfo.applyTo(code), '''
+f([int x, int y]) => null;
+g(int x, int y) => f(x!);
+''');
+  }
+
   Future<void> test_assignment_add_null_check() async {
     var content = 'f(int x, int y) => x += y;';
     await analyze(content);
@@ -802,6 +1094,23 @@
     expect(previewInfo.applyTo(code), 'f(int? x) {}');
   }
 
+  Future<void> test_methodName_change() async {
+    await analyze('f() => f();');
+    var previewInfo = run({
+      findNode.methodInvocation('f();').methodName: NodeChangeForMethodName()
+        ..replacement = 'g'
+    });
+    expect(previewInfo.applyTo(code), 'f() => g();');
+  }
+
+  Future<void> test_methodName_no_change() async {
+    await analyze('f() => f();');
+    var previewInfo = run({
+      findNode.methodInvocation('f();').methodName: NodeChangeForMethodName()
+    });
+    expect(previewInfo, isNull);
+  }
+
   Future<void> test_noChangeToTypeAnnotation() async {
     await analyze('int x = 0;');
     var typeName = findNode.typeName('int');
diff --git a/pkg/nnbd_migration/test/fix_builder_test.dart b/pkg/nnbd_migration/test/fix_builder_test.dart
index 4e3e6e9..0bf7e71 100644
--- a/pkg/nnbd_migration/test/fix_builder_test.dart
+++ b/pkg/nnbd_migration/test/fix_builder_test.dart
@@ -77,6 +77,16 @@
           'removeLanguageVersionComment',
           true);
 
+  static final isAddImportOfIterableExtension =
+      TypeMatcher<NodeChangeForCompilationUnit>()
+          .having((c) => c.addImports, 'addImports', {
+    'package:collection/collection.dart': {'IterableExtension'}
+  });
+
+  static final isAddShowOfIterableExtension =
+      TypeMatcher<NodeChangeForShowCombinator>().having((c) => c.addNames,
+          'addNames', unorderedEquals(['IterableExtension']));
+
   static final isRemoveNullAwareness =
       TypeMatcher<NodeChangeForPropertyAccess>()
           .having((c) => c.removeNullAwareness, 'removeNullAwareness', true);
@@ -106,11 +116,21 @@
     return unit;
   }
 
+  TypeMatcher<NodeChangeForArgumentList> isDropArgument(
+          dynamic argumentsToDrop) =>
+      TypeMatcher<NodeChangeForArgumentList>()
+          .having((c) => c.argumentsToDrop, 'argumentsToDrop', argumentsToDrop);
+
   TypeMatcher<AtomicEditInfo> isInfo(description, fixReasons) =>
       TypeMatcher<AtomicEditInfo>()
           .having((i) => i.description, 'description', description)
           .having((i) => i.fixReasons, 'fixReasons', fixReasons);
 
+  TypeMatcher<NodeChangeForMethodName> isMethodNameChange(
+          dynamic replacement) =>
+      TypeMatcher<NodeChangeForMethodName>()
+          .having((c) => c.replacement, 'replacement', replacement);
+
   Map<AstNode, NodeChange> scopedChanges(
           FixBuilder fixBuilder, AstNode scope) =>
       {
@@ -1342,6 +1362,24 @@
         changes: {findNode.simple('y;'): isNullCheck});
   }
 
+  Future<void> test_firstWhere_transform() async {
+    await analyze('''
+_f(Iterable<int> x) => x.firstWhere((n) => n.isEven, orElse: () => null);
+''');
+    var methodInvocation = findNode.methodInvocation('firstWhere');
+    var functionExpression = findNode.functionExpression('() => null');
+    var fixBuilder = visitSubexpression(methodInvocation, 'int?', changes: {
+      methodInvocation.methodName: isMethodNameChange('firstWhereOrNull'),
+      methodInvocation.argumentList:
+          isDropArgument(unorderedEquals([functionExpression.parent])),
+      // Behavior of the function expression and its subexpression don't matter
+      // because they're being dropped.
+      functionExpression.parent: anything,
+      findNode.nullLiteral('null'): anything
+    });
+    expect(fixBuilder.needsIterableExtension, true);
+  }
+
   Future<void> test_functionExpressionInvocation_dynamic() async {
     await analyze('''
 _f(dynamic d) => d();
@@ -1543,6 +1581,75 @@
     });
   }
 
+  Future<void> test_import_IterableExtension_already_imported_add_show() async {
+    addPackageFile('collection', 'collection.dart', 'class PriorityQueue {}');
+    await analyze('''
+import 'package:collection/collection.dart' show PriorityQueue;
+
+main() {}
+''');
+    visitAll(injectNeedsIterableExtension: true, changes: {
+      findNode.import('package:collection').combinators[0]:
+          isAddShowOfIterableExtension
+    });
+  }
+
+  Future<void> test_import_IterableExtension_already_imported_all() async {
+    addPackageFile('collection', 'collection.dart', '');
+    await analyze('''
+import 'package:collection/collection.dart';
+
+main() {}
+''');
+    visitAll(injectNeedsIterableExtension: true, changes: {});
+  }
+
+  Future<void>
+      test_import_IterableExtension_already_imported_and_shown() async {
+    addPackageFile('collection', 'collection.dart',
+        'extension IterableExtension<T> on Iterable<T> {}');
+    await analyze('''
+import 'package:collection/collection.dart' show IterableExtension;
+
+main() {}
+''');
+    visitAll(injectNeedsIterableExtension: true, changes: {});
+  }
+
+  Future<void> test_import_IterableExtension_already_imported_prefixed() async {
+    addPackageFile('collection', 'collection.dart', '');
+    await analyze('''
+import 'package:collection/collection.dart' as c;
+
+main() {}
+''');
+    visitAll(
+        injectNeedsIterableExtension: true,
+        changes: {findNode.unit: isAddImportOfIterableExtension});
+  }
+
+  Future<void> test_import_IterableExtension_other_import() async {
+    addPackageFile(
+        'foo', 'foo.dart', 'extension IterableExtension<T> on Iterable<T> {}');
+    await analyze('''
+import 'package:foo/foo.dart' show IterableExtension;
+
+main() {}
+''');
+    visitAll(
+        injectNeedsIterableExtension: true,
+        changes: {findNode.unit: isAddImportOfIterableExtension});
+  }
+
+  Future<void> test_import_IterableExtension_simple() async {
+    await analyze('''
+main() {}
+''');
+    visitAll(
+        injectNeedsIterableExtension: true,
+        changes: {findNode.unit: isAddImportOfIterableExtension});
+  }
+
   Future<void> test_indexExpression_dynamic() async {
     await analyze('''
 Object/*!*/ _f(dynamic d, int/*?*/ i) => d[i];
@@ -3295,8 +3402,12 @@
 
   void visitAll(
       {Map<AstNode, Matcher> changes = const <Expression, Matcher>{},
-      Map<AstNode, Set<Problem>> problems = const <AstNode, Set<Problem>>{}}) {
+      Map<AstNode, Set<Problem>> problems = const <AstNode, Set<Problem>>{},
+      bool injectNeedsIterableExtension = false}) {
     var fixBuilder = _createFixBuilder(testUnit);
+    if (injectNeedsIterableExtension) {
+      fixBuilder.needsIterableExtension = true;
+    }
     fixBuilder.visitAll();
     expect(scopedChanges(fixBuilder, testUnit), changes);
     expect(scopedProblems(fixBuilder, testUnit), problems);
@@ -3330,7 +3441,7 @@
     expect(scopedProblems(fixBuilder, node), problems);
   }
 
-  void visitSubexpression(Expression node, String expectedType,
+  FixBuilder visitSubexpression(Expression node, String expectedType,
       {Map<AstNode, Matcher> changes = const <Expression, Matcher>{},
       Map<AstNode, Set<Problem>> problems = const <AstNode, Set<Problem>>{},
       bool warnOnWeakCode = false}) {
@@ -3340,6 +3451,7 @@
     expect(type.getDisplayString(withNullability: true), expectedType);
     expect(scopedChanges(fixBuilder, node), changes);
     expect(scopedProblems(fixBuilder, node), problems);
+    return fixBuilder;
   }
 
   void visitTypeAnnotation(TypeAnnotation node, String expectedType,
@@ -3383,7 +3495,8 @@
         null,
         scope.thisOrAncestorOfType<CompilationUnit>(),
         warnOnWeakCode,
-        graph);
+        graph,
+        true);
   }
 
   bool _isInScope(AstNode node, AstNode scope) {
diff --git a/pkg/nnbd_migration/test/front_end/region_renderer_test.dart b/pkg/nnbd_migration/test/front_end/region_renderer_test.dart
index 7fad2e6..ab9663c 100644
--- a/pkg/nnbd_migration/test/front_end/region_renderer_test.dart
+++ b/pkg/nnbd_migration/test/front_end/region_renderer_test.dart
@@ -108,8 +108,17 @@
     expect(entry.link, isNotNull);
     var sdkCoreLib = convertPath('/sdk/lib/core/core.dart');
     var sdkCoreLibUriPath = resourceProvider.pathContext.toUri(sdkCoreLib).path;
-    expect(entry.link.href,
-        equals('$sdkCoreLibUriPath?offset=3730&line=166&authToken=AUTH_TOKEN'));
+    var coreLibText = resourceProvider.getFile(sdkCoreLib).readAsStringSync();
+    var expectedOffset =
+        'List.from'.allMatches(coreLibText).single.start + 'List.'.length;
+    var expectedLine =
+        '\n'.allMatches(coreLibText.substring(0, expectedOffset)).length + 1;
+    expect(
+        entry.link.href,
+        equals('$sdkCoreLibUriPath?'
+            'offset=$expectedOffset&'
+            'line=$expectedLine&'
+            'authToken=AUTH_TOKEN'));
     // On Windows, the path will simply be the absolute path to the core
     // library, because there is no relative route from C:\ to D:\. On Posix,
     // the path is relative.
diff --git a/pkg/nnbd_migration/test/migration_visitor_test_base.dart b/pkg/nnbd_migration/test/migration_visitor_test_base.dart
index 9fd3eeb..06b0451 100644
--- a/pkg/nnbd_migration/test/migration_visitor_test_base.dart
+++ b/pkg/nnbd_migration/test/migration_visitor_test_base.dart
@@ -149,7 +149,7 @@
     var unit = await super.analyze(code);
     decoratedClassHierarchy = DecoratedClassHierarchy(variables, graph);
     unit.accept(EdgeBuilder(typeProvider, typeSystem, variables, graph,
-        testSource, null, decoratedClassHierarchy));
+        testSource, null, decoratedClassHierarchy, true));
     return unit;
   }
 }
diff --git a/pkg/nnbd_migration/test/utilities/test_all.dart b/pkg/nnbd_migration/test/utilities/test_all.dart
index 8a172cc..dd93187 100644
--- a/pkg/nnbd_migration/test/utilities/test_all.dart
+++ b/pkg/nnbd_migration/test/utilities/test_all.dart
@@ -6,12 +6,17 @@
 
 import 'multi_future_tracker_test.dart' as multi_future_tracker_test;
 import 'scoped_set_test.dart' as scoped_set_test;
+import 'source_edit_diff_formatter_test.dart'
+    as source_edit_diff_formatter_test;
 import 'subprocess_launcher_test.dart' as subprocess_launcher_test;
+import 'where_or_null_transformer_test.dart' as where_or_null_transformer_test;
 
 main() {
   defineReflectiveSuite(() {
     multi_future_tracker_test.main();
     scoped_set_test.main();
+    source_edit_diff_formatter_test.main();
     subprocess_launcher_test.main();
+    where_or_null_transformer_test.main();
   });
 }
diff --git a/pkg/nnbd_migration/test/utilities/where_or_null_transformer_test.dart b/pkg/nnbd_migration/test/utilities/where_or_null_transformer_test.dart
new file mode 100644
index 0000000..27e7033
--- /dev/null
+++ b/pkg/nnbd_migration/test/utilities/where_or_null_transformer_test.dart
@@ -0,0 +1,148 @@
+// Copyright (c) 2020, 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:analyzer/dart/element/type_provider.dart';
+import 'package:analyzer/dart/element/type_system.dart';
+import 'package:nnbd_migration/src/utilities/where_or_null_transformer.dart';
+import 'package:test/test.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+import '../abstract_single_unit.dart';
+
+main() {
+  defineReflectiveSuite(() {
+    defineReflectiveTests(WhereOrNullTransformerTest);
+  });
+}
+
+@reflectiveTest
+class WhereOrNullTransformerTest extends AbstractSingleUnitTest {
+  WhereOrNullTransformer transformer;
+
+  TypeProvider get typeProvider => testAnalysisResult.typeProvider;
+
+  TypeSystem get typeSystem => testAnalysisResult.typeSystem;
+
+  Future<void> analyze(String code) async {
+    await resolveTestUnit(code);
+    transformer = WhereOrNullTransformer(typeProvider, typeSystem);
+  }
+
+  Future<void> test_match() async {
+    await analyze('''
+f(List<int> x) => x.firstWhere((i) => i.isEven, orElse: () => null);
+''');
+    var orElseExpression = findNode.functionExpression('() => null');
+    var transformationInfo =
+        transformer.tryTransformOrElseArgument(orElseExpression);
+    expect(transformationInfo, isNotNull);
+    expect(transformationInfo.methodInvocation,
+        same(findNode.methodInvocation('firstWhere')));
+    expect(transformationInfo.orElseArgument, same(orElseExpression.parent));
+    expect(transformationInfo.originalName, 'firstWhere');
+    expect(transformationInfo.replacementName, 'firstWhereOrNull');
+  }
+
+  Future<void> test_match_extended() async {
+    await analyze('''
+abstract class C implements Iterable<int> {
+  int firstWhere(bool test(int element), {int orElse()}) => null;
+}
+f(C c) => c.firstWhere((i) => i.isEven, orElse: () => null);
+''');
+    var orElseExpression = findNode.functionExpression('() => null');
+    var transformationInfo =
+        transformer.tryTransformOrElseArgument(orElseExpression);
+    expect(transformationInfo, isNotNull);
+    expect(transformationInfo.methodInvocation,
+        same(findNode.methodInvocation('firstWhere((')));
+    expect(transformationInfo.orElseArgument, same(orElseExpression.parent));
+    expect(transformationInfo.originalName, 'firstWhere');
+    expect(transformationInfo.replacementName, 'firstWhereOrNull');
+  }
+
+  Future<void> test_mismatch_misnamed_method() async {
+    await analyze('''
+abstract class C extends Iterable<int> {
+  int fooBar(bool test(int element), {int orElse()});
+}
+f(C c) => c.fooBar((i) => i.isEven, orElse: () => null);
+''');
+    expect(
+        transformer.tryTransformOrElseArgument(
+            findNode.functionExpression('() => null')),
+        isNull);
+  }
+
+  Future<void> test_mismatch_orElse_expression() async {
+    await analyze('''
+f(List<int> x) => x.firstWhere((i) => i.isEven, orElse: () => 0);
+''');
+    expect(
+        transformer
+            .tryTransformOrElseArgument(findNode.functionExpression('() => 0')),
+        isNull);
+  }
+
+  Future<void> test_mismatch_orElse_name() async {
+    await analyze('''
+abstract class C extends Iterable<int> {
+  @override
+  int firstWhere(bool test(int element), {int orElse(), int ifSo()});
+}
+f(C c) => c.firstWhere((i) => i.isEven, ifSo: () => null);
+''');
+    expect(
+        transformer.tryTransformOrElseArgument(
+            findNode.functionExpression('() => null')),
+        isNull);
+  }
+
+  Future<void> test_mismatch_orElse_named_parameter() async {
+    await analyze('''
+f(List<int> x) => x.firstWhere((i) => i.isEven, orElse: ({int x}) => null);
+''');
+    expect(
+        transformer.tryTransformOrElseArgument(
+            findNode.functionExpression(') => null')),
+        isNull);
+  }
+
+  Future<void> test_mismatch_orElse_optional_parameter() async {
+    await analyze('''
+f(List<int> x) => x.firstWhere((i) => i.isEven, orElse: ([int x]) => null);
+''');
+    expect(
+        transformer.tryTransformOrElseArgument(
+            findNode.functionExpression(') => null')),
+        isNull);
+  }
+
+  Future<void> test_mismatch_orElse_presence_of_other_arg() async {
+    await analyze('''
+abstract class C extends Iterable<int> {
+  @override
+  int firstWhere(bool test(int element), {int orElse(), int ifSo()});
+}
+f(C c) => c.firstWhere((i) => i.isEven, orElse: () => null, ifSo: () => null);
+''');
+    expect(
+        transformer.tryTransformOrElseArgument(
+            findNode.functionExpression('() => null,')),
+        isNull);
+  }
+
+  Future<void> test_mismatch_unrelated_type() async {
+    await analyze('''
+abstract class C {
+  int firstWhere(bool test(int element), {int orElse()});
+}
+f(C c) => c.firstWhere((i) => i.isEven, orElse: () => null);
+''');
+    expect(
+        transformer.tryTransformOrElseArgument(
+            findNode.functionExpression('() => null')),
+        isNull);
+  }
+}
diff --git a/tools/VERSION b/tools/VERSION
index f4481ae..1f5a84d 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
 MAJOR 2
 MINOR 11
 PATCH 0
-PRERELEASE 238
+PRERELEASE 239
 PRERELEASE_PATCH 0
\ No newline at end of file