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