| // Copyright (c) 2019, 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/ast/token.dart'; |
| import 'package:analyzer/dart/ast/visitor.dart'; |
| import 'package:analyzer/dart/element/element.dart'; |
| 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'; |
| import 'package:nnbd_migration/src/decorated_type.dart'; |
| import 'package:nnbd_migration/src/edit_plan.dart'; |
| import 'package:nnbd_migration/src/fix_builder.dart'; |
| import 'package:nnbd_migration/src/utilities/hint_utils.dart'; |
| |
| /// Base class representing a change that might need to be made to an |
| /// expression. |
| abstract class ExpressionChange { |
| /// The type of the expression after the change is applied. |
| final DartType resultType; |
| |
| ExpressionChange(this.resultType); |
| |
| /// Description of the change. |
| NullabilityFixDescription get description; |
| |
| /// Creates a [NodeProducingEditPlan] that applies the change to [innerPlan]. |
| NodeProducingEditPlan applyExpression(FixAggregator aggregator, |
| NodeProducingEditPlan innerPlan, AtomicEditInfo? info); |
| |
| /// Creates a string that applies the change to the [inner] text string. |
| String applyText(FixAggregator aggregator, String inner); |
| |
| /// Describes the change, for use in debugging. |
| String describe(); |
| } |
| |
| /// Visitor that combines together the changes produced by [FixBuilder] into a |
| /// concrete set of source code edits using the infrastructure of [EditPlan]. |
| class FixAggregator extends UnifyingAstVisitor<void> { |
| /// Map from the [AstNode]s that need to have changes made, to the changes |
| /// that need to be applied to them. |
| final Map<AstNode?, NodeChange> _changes; |
| |
| /// The set of [EditPlan]s being accumulated. |
| List<EditPlan> _plans = []; |
| |
| final EditPlanner planner; |
| |
| /// Map from library to the prefix we should use when inserting code that |
| /// refers to it. |
| final Map<LibraryElement?, String?> _importPrefixes = {}; |
| |
| final bool? _warnOnWeakCode; |
| |
| FixAggregator._(this.planner, this._changes, this._warnOnWeakCode, |
| CompilationUnitElement compilationUnitElement) { |
| for (var importElement in compilationUnitElement.library.imports) { |
| // TODO(paulberry): the `??=` should ensure that if there are two imports, |
| // one prefixed and one not, we prefer the prefix. Test this. |
| _importPrefixes[importElement.importedLibrary] ??= |
| importElement.prefix?.name; |
| } |
| } |
| |
| /// Creates the necessary Dart code to refer to the given element, using an |
| /// import prefix if necessary. |
| /// |
| /// TODO(paulberry): if the element is not currently imported, we should |
| /// update or add an import statement as necessary. |
| String? elementToCode(Element element) { |
| var name = element.name; |
| var library = element.library; |
| var prefix = _importPrefixes[library]; |
| if (prefix != null) { |
| return '$prefix.$name'; |
| } else { |
| return name; |
| } |
| } |
| |
| /// Gathers all the changes to nodes descended from [node] into a single |
| /// [EditPlan]. |
| NodeProducingEditPlan innerPlanForNode(AstNode node) { |
| var innerPlans = innerPlansForNode(node); |
| return planner.passThrough(node, innerPlans: innerPlans); |
| } |
| |
| /// Gathers all the changes to nodes descended from [node] into a list of |
| /// [EditPlan]s, one for each change. |
| List<EditPlan> innerPlansForNode(AstNode node) { |
| var previousPlans = _plans; |
| try { |
| _plans = []; |
| node.visitChildren(this); |
| return _plans; |
| } finally { |
| _plans = previousPlans; |
| } |
| } |
| |
| /// Gathers all the changes to [node] and its descendants into a single |
| /// [EditPlan]. |
| EditPlan planForNode(AstNode? node) { |
| var change = _changes[node]; |
| if (change != null) { |
| return change._apply(node!, this); |
| } else { |
| return planner.passThrough(node, innerPlans: innerPlansForNode(node!)); |
| } |
| } |
| |
| /// Creates a string representation of the given type parameter element, |
| /// suitable for inserting into the user's source code. |
| String typeFormalToCode(TypeParameterElement formal) { |
| var bound = formal.bound; |
| if (bound == null || |
| bound.isDynamic || |
| (bound.isDartCoreObject && |
| bound.nullabilitySuffix != NullabilitySuffix.none)) { |
| return formal.name; |
| } |
| return '${formal.name} extends ${typeToCode(bound)}'; |
| } |
| |
| String typeToCode(DartType type) { |
| // TODO(paulberry): is it possible to share code with DartType.toString() |
| // somehow? |
| String suffix = |
| type.nullabilitySuffix == NullabilitySuffix.question ? '?' : ''; |
| if (type is InterfaceType) { |
| var name = elementToCode(type.element); |
| var typeArguments = type.typeArguments; |
| if (typeArguments.isEmpty) { |
| return '$name$suffix'; |
| } else { |
| var args = [for (var arg in typeArguments) typeToCode(arg)].join(', '); |
| return '$name<$args>$suffix'; |
| } |
| } else if (type is FunctionType) { |
| var buffer = StringBuffer(); |
| buffer.write(typeToCode(type.returnType)); |
| buffer.write(' Function'); |
| var typeFormals = type.typeFormals; |
| if (typeFormals.isNotEmpty) { |
| var formals = [for (var formal in typeFormals) typeFormalToCode(formal)] |
| .join(', '); |
| buffer.write('<$formals>'); |
| } |
| buffer.write('('); |
| String optionalOrNamedCloser = ''; |
| bool commaNeeded = false; |
| for (var parameter in type.parameters) { |
| if (commaNeeded) { |
| buffer.write(', '); |
| } else { |
| commaNeeded = true; |
| } |
| if (optionalOrNamedCloser.isEmpty && !parameter.isRequiredPositional) { |
| if (parameter.isPositional) { |
| buffer.write('['); |
| optionalOrNamedCloser = ']'; |
| } else { |
| buffer.write('{'); |
| optionalOrNamedCloser = '}'; |
| } |
| } |
| buffer.write(typeToCode(parameter.type)); |
| if (parameter.isNamed) { |
| buffer.write(' ${parameter.name}'); |
| } |
| } |
| buffer.write(optionalOrNamedCloser); |
| buffer.write(')'); |
| buffer.write(suffix); |
| return buffer.toString(); |
| } else { |
| return type.toString(); |
| } |
| } |
| |
| @override |
| void visitNode(AstNode node) { |
| var change = _changes[node]; |
| if (change != null) { |
| var innerPlan = change._apply(node, this); |
| _plans.add(innerPlan); |
| } else { |
| node.visitChildren(this); |
| } |
| } |
| |
| /// Runs the [FixAggregator] on a [unit] and returns the resulting edits. |
| static Map<int?, List<AtomicEdit>>? run(CompilationUnit unit, |
| String? sourceText, Map<AstNode?, NodeChange> changes, |
| {bool? removeViaComments = false, bool? warnOnWeakCode = false}) { |
| var planner = EditPlanner(unit.lineInfo, sourceText, |
| removeViaComments: removeViaComments); |
| var aggregator = FixAggregator._( |
| planner, changes, warnOnWeakCode, unit.declaredElement!); |
| unit.accept(aggregator); |
| if (aggregator._plans.isEmpty) return {}; |
| EditPlan plan; |
| if (aggregator._plans.length == 1) { |
| plan = aggregator._plans[0]; |
| } else { |
| plan = planner.passThrough(unit, innerPlans: aggregator._plans); |
| } |
| return planner.finalize(plan); |
| } |
| } |
| |
| /// [ExpressionChange] describing the addition of an `as` cast to an expression. |
| class IntroduceAsChange extends ExpressionChange { |
| /// The type being cast to. |
| final DartType type; |
| |
| /// Indicates whether this is a downcast. |
| final bool isDowncast; |
| |
| IntroduceAsChange(this.type, {required this.isDowncast}) : super(type); |
| |
| @override |
| NullabilityFixDescription get description => isDowncast |
| ? NullabilityFixDescription.downcastExpression |
| : NullabilityFixDescription.otherCastExpression; |
| |
| @override |
| NodeProducingEditPlan applyExpression(FixAggregator aggregator, |
| NodeProducingEditPlan innerPlan, AtomicEditInfo? info) => |
| aggregator.planner.addBinaryPostfix( |
| innerPlan, TokenType.AS, aggregator.typeToCode(type), |
| info: info); |
| |
| @override |
| String applyText(FixAggregator aggregator, String inner) => |
| '$inner as ${aggregator.typeToCode(type)}'; |
| |
| @override |
| String describe() => 'IntroduceAsChange($type)'; |
| } |
| |
| /// [ExpressionChange] describing the addition of an `as` cast to an expression |
| /// having a Future type. |
| class IntroduceThenChange extends ExpressionChange { |
| /// The change that should be made to the value the future completes with. |
| final ExpressionChange innerChange; |
| |
| IntroduceThenChange(DartType resultType, this.innerChange) |
| : super(resultType); |
| |
| @override |
| NullabilityFixDescription get description => |
| NullabilityFixDescription.addThen; |
| |
| @override |
| NodeProducingEditPlan applyExpression(FixAggregator aggregator, |
| NodeProducingEditPlan innerPlan, AtomicEditInfo? info) => |
| aggregator.planner.addPostfix(innerPlan, |
| '.then((value) => ${innerChange.applyText(aggregator, 'value')})', |
| info: info); |
| |
| @override |
| String applyText(FixAggregator aggregator, String inner) => |
| '$inner.then((value) => ${innerChange.applyText(aggregator, 'value')})'; |
| |
| @override |
| String describe() => 'IntroduceThenChange($innerChange)'; |
| } |
| |
| /// Reasons that a variable declaration is to be made late. |
| enum LateAdditionReason { |
| /// It was inferred that the associated variable declaration is to be made |
| /// late through the late-inferring algorithm. |
| inference, |
| |
| /// It was inferred that the associated variable declaration is to be made |
| /// late, because it is a test variable which is assigned during setup. |
| testVariableInference, |
| } |
| |
| /// Base class representing a kind of change that [FixAggregator] might make to |
| /// a particular AST node. |
| abstract class NodeChange<N extends AstNode> { |
| NodeChange._(); |
| |
| /// Indicates whether this node exists solely to provide informative |
| /// information. |
| bool get isInformative => false; |
| |
| Iterable<String> get _toStringParts => const []; |
| |
| @override |
| String toString() => '$runtimeType(${_toStringParts.join(', ')})'; |
| |
| /// Applies this change to the given [node], producing an [EditPlan]. The |
| /// [aggregator] may be used to gather up any edits to the node's descendants |
| /// into their own [EditPlan]s. |
| /// |
| /// Note: the reason the caller can't just gather up the edits and pass them |
| /// in is that some changes don't preserve all of the structure of the nodes |
| /// below them (e.g. dropping an unnecessary cast), so those changes need to |
| /// be able to call the appropriate [aggregator] methods just on the nodes |
| /// they need. |
| /// |
| /// May return `null` if no changes need to be made. |
| EditPlan _apply(N node, FixAggregator aggregator); |
| |
| /// Creates the appropriate specialized kind of [NodeChange] appropriate for |
| /// the given [node]. |
| static NodeChange<AstNode>? create(AstNode node) => |
| node.accept(_NodeChangeVisitor._instance); |
| } |
| |
| /// Implementation of [NodeChange] specialized for operating on [Annotation] |
| /// nodes. |
| class NodeChangeForAnnotation extends NodeChange<Annotation> { |
| /// Indicates whether the node should be changed into a `required` keyword. |
| bool changeToRequiredKeyword = false; |
| |
| /// If [changeToRequiredKeyword] is `true`, the information that should be |
| /// contained in the edit. |
| AtomicEditInfo? changeToRequiredKeywordInfo; |
| |
| NodeChangeForAnnotation() : super._(); |
| |
| @override |
| Iterable<String> get _toStringParts => |
| [if (changeToRequiredKeyword) 'changeToRequiredKeyword']; |
| |
| @override |
| EditPlan _apply(Annotation node, FixAggregator aggregator) { |
| if (!changeToRequiredKeyword) { |
| return aggregator.innerPlanForNode(node); |
| } |
| var name = node.name; |
| if (name is PrefixedIdentifier) { |
| name = name.identifier; |
| } |
| if (aggregator.planner.sourceText!.substring(name.offset, name.end) == |
| 'required') { |
| // The text `required` already exists in the annotation; we can just |
| // extract it. |
| return aggregator.planner.extract( |
| node, aggregator.planForNode(name) as NodeProducingEditPlan, |
| infoBefore: changeToRequiredKeywordInfo, alwaysDelete: true); |
| } else { |
| return aggregator.planner.replace(node, |
| [AtomicEdit.insert('required', info: changeToRequiredKeywordInfo)]); |
| } |
| } |
| } |
| |
| /// Implementation of [NodeChange] specialized for operating on [ArgumentList] |
| /// nodes. |
| class NodeChangeForArgumentList extends NodeChange<ArgumentList> { |
| /// Map whose keys are the arguments that should be dropped from this argument |
| /// list, if any. Values are info about why the arguments are being dropped. |
| final Map<Expression, AtomicEditInfo?> _argumentsToDrop = {}; |
| |
| NodeChangeForArgumentList() : super._(); |
| |
| /// Queries the map whose keys are the arguments that should be dropped from |
| /// this argument list, if any. Values are info about why the arguments are |
| /// being dropped. |
| @visibleForTesting |
| Map<Expression, AtomicEditInfo?> get argumentsToDrop => _argumentsToDrop; |
| |
| @override |
| Iterable<String> get _toStringParts => |
| [if (_argumentsToDrop.isNotEmpty) 'argumentsToDrop: $_argumentsToDrop']; |
| |
| /// Updates `this` so that the given [argument] will be dropped, using [info] |
| /// to annotate the reason why it is being dropped. |
| void dropArgument(Expression argument, AtomicEditInfo? info) { |
| assert(!_argumentsToDrop.containsKey(argument)); |
| _argumentsToDrop[argument] = info; |
| } |
| |
| @override |
| EditPlan _apply(ArgumentList node, FixAggregator aggregator) { |
| assert(_argumentsToDrop.keys.every((e) => identical(e.parent, node))); |
| List<EditPlan> innerPlans = []; |
| for (var argument in node.arguments) { |
| if (_argumentsToDrop.containsKey(argument)) { |
| innerPlans.add(aggregator.planner |
| .removeNode(argument, info: _argumentsToDrop[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> { |
| /// Indicates whether the cast should be removed. |
| bool removeAs = false; |
| |
| @override |
| Iterable<String> get _toStringParts => |
| [...super._toStringParts, if (removeAs) 'removeAs']; |
| |
| @override |
| EditPlan _apply(AsExpression node, FixAggregator aggregator) { |
| if (removeAs) { |
| return aggregator.planner.extract(node, |
| aggregator.planForNode(node.expression) as NodeProducingEditPlan, |
| infoAfter: |
| AtomicEditInfo(NullabilityFixDescription.removeAs, const {})); |
| } else { |
| return super._apply(node, aggregator); |
| } |
| } |
| } |
| |
| /// Implementation of [NodeChange] specialized for operating on |
| /// [AssignmentExpression] nodes. |
| class NodeChangeForAssignment |
| extends NodeChangeForExpression<AssignmentExpression> |
| with NodeChangeForAssignmentLike { |
| /// Indicates whether the user should be warned that the assignment is a |
| /// null-aware assignment that will have no effect when strong checking is |
| /// enabled. |
| bool isWeakNullAware = false; |
| |
| @override |
| Iterable<String> get _toStringParts => |
| [...super._toStringParts, if (isWeakNullAware) 'isWeakNullAware']; |
| |
| @override |
| NodeProducingEditPlan _apply( |
| AssignmentExpression node, FixAggregator aggregator) { |
| var lhsPlan = aggregator.planForNode(node.leftHandSide); |
| if (isWeakNullAware && !aggregator._warnOnWeakCode!) { |
| // Just keep the LHS |
| return aggregator.planner.extract(node, lhsPlan as NodeProducingEditPlan, |
| infoAfter: AtomicEditInfo( |
| NullabilityFixDescription.removeNullAwareAssignment, const {})); |
| } |
| var operatorPlan = _makeOperatorPlan(aggregator, node, node.operator); |
| var rhsPlan = aggregator.planForNode(node.rightHandSide); |
| var innerPlans = <EditPlan>[ |
| lhsPlan, |
| if (operatorPlan != null) operatorPlan, |
| rhsPlan |
| ]; |
| return _applyExpression(aggregator, |
| aggregator.planner.passThrough(node, innerPlans: innerPlans)); |
| } |
| |
| EditPlan? _makeOperatorPlan( |
| FixAggregator aggregator, AssignmentExpression node, Token operator) { |
| var operatorPlan = super._makeOperatorPlan(aggregator, node, operator); |
| if (operatorPlan != null) return operatorPlan; |
| if (isWeakNullAware) { |
| assert(aggregator._warnOnWeakCode!); |
| return aggregator.planner.informativeMessageForToken(node, operator, |
| info: AtomicEditInfo( |
| NullabilityFixDescription |
| .nullAwareAssignmentUnnecessaryInStrongMode, |
| const {})); |
| } else { |
| return null; |
| } |
| } |
| } |
| |
| /// Common behaviors for expressions that can represent an assignment (possibly |
| /// through desugaring). |
| mixin NodeChangeForAssignmentLike<N extends Expression> |
| on NodeChangeForExpression<N> { |
| /// Indicates whether the user should be warned that the assignment has a |
| /// bad combined type (the return type of the combiner isn't assignable to the |
| /// write type of the target). |
| bool hasBadCombinedType = false; |
| |
| /// Indicates whether the user should be warned that the assignment has a |
| /// nullable source type. |
| bool hasNullableSource = false; |
| |
| @override |
| Iterable<String> get _toStringParts => [ |
| ...super._toStringParts, |
| if (hasBadCombinedType) 'hasBadCombinedType', |
| if (hasNullableSource) 'hasNullableSource' |
| ]; |
| |
| EditPlan? _makeOperatorPlan( |
| FixAggregator aggregator, N node, Token operator) { |
| if (hasNullableSource) { |
| return aggregator.planner.informativeMessageForToken(node, operator, |
| info: AtomicEditInfo( |
| NullabilityFixDescription.compoundAssignmentHasNullableSource, |
| const {})); |
| } else if (hasBadCombinedType) { |
| return aggregator.planner.informativeMessageForToken(node, operator, |
| info: AtomicEditInfo( |
| NullabilityFixDescription.compoundAssignmentHasBadCombinedType, |
| const {})); |
| } else { |
| return null; |
| } |
| } |
| } |
| |
| /// 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 (_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) { |
| List<EditPlan> innerPlans = []; |
| if (removeLanguageVersionComment) { |
| final comment = (node as CompilationUnitImpl).languageVersionToken!; |
| innerPlans.add(aggregator.planner.replaceToken(node, comment, '', |
| info: AtomicEditInfo( |
| NullabilityFixDescription.removeLanguageVersionComment, |
| const {}))); |
| } |
| _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(', ')};", |
| info: AtomicEditInfo(NullabilityFixDescription.addImport, {})), |
| 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 |
| /// with conditional behavior (if statements, if elements, and conditional |
| /// expressions). |
| mixin NodeChangeForConditional<N extends AstNode> on NodeChange<N> { |
| /// If not `null`, indicates that the condition expression is known to |
| /// evaluate to either `true` or `false`, so the other branch of the |
| /// conditional is dead code and should be eliminated. |
| bool? conditionValue; |
| |
| /// If [conditionValue] is not `null`, the reason that should be included in |
| /// the [AtomicEditInfo] for the edit that removes the dead code. |
| FixReasonInfo? conditionReason; |
| |
| @override |
| Iterable<String> get _toStringParts => |
| [...super._toStringParts, if (conditionValue != null) 'conditionValue']; |
| |
| /// If dead code removal is warranted for [node], returns an [EditPlan] that |
| /// removes the dead code (and performs appropriate updates within any |
| /// descendant AST nodes that remain). Otherwise returns `null`. |
| EditPlan? _applyConditional(N node, FixAggregator aggregator, |
| AstNode conditionNode, AstNode thenNode, AstNode? elseNode) { |
| if (conditionValue == null) return null; |
| if (aggregator._warnOnWeakCode!) { |
| var conditionPlan = aggregator.innerPlanForNode(conditionNode); |
| var info = AtomicEditInfo( |
| conditionValue! |
| ? NullabilityFixDescription.conditionTrueInStrongMode |
| : NullabilityFixDescription.conditionFalseInStrongMode, |
| {FixReasonTarget.root: conditionReason}); |
| var commentedConditionPlan = aggregator.planner.addCommentPostfix( |
| conditionPlan, '/* == $conditionValue */', |
| info: info, isInformative: true); |
| return aggregator.planner.passThrough(node, innerPlans: [ |
| commentedConditionPlan, |
| aggregator.planForNode(thenNode), |
| if (elseNode != null) aggregator.planForNode(elseNode) |
| ]); |
| } |
| AstNode? nodeToKeep; |
| NullabilityFixDescription descriptionBefore, descriptionAfter; |
| if (conditionValue!) { |
| nodeToKeep = thenNode; |
| descriptionBefore = NullabilityFixDescription.discardCondition; |
| if (elseNode == null) { |
| descriptionAfter = descriptionBefore; |
| } else { |
| descriptionAfter = NullabilityFixDescription.discardElse; |
| } |
| } else { |
| nodeToKeep = elseNode; |
| descriptionBefore = |
| descriptionAfter = NullabilityFixDescription.discardThen; |
| } |
| if (nodeToKeep == null || |
| nodeToKeep is Block && nodeToKeep.statements.isEmpty) { |
| // The conditional node collapses to a no-op, so try to remove it |
| // entirely. |
| var info = AtomicEditInfo(NullabilityFixDescription.discardIf, |
| {FixReasonTarget.root: conditionReason}); |
| var removeNode = aggregator.planner.tryRemoveNode(node, info: info); |
| if (removeNode != null) { |
| return removeNode; |
| } else { |
| // We can't remove the node because it's not inside a sequence, so we |
| // have to create a suitable replacement. |
| if (node is IfStatement) { |
| return aggregator.planner |
| .replace(node, [AtomicEdit.insert('{}', info: info)], info: info); |
| } else if (node is IfElement) { |
| return aggregator.planner.replace( |
| node, [AtomicEdit.insert('...{}', info: info)], |
| info: info); |
| } else { |
| // We should never get here; the only types of conditional nodes that |
| // can wind up collapsing to a no-op are if statements and if |
| // elements. |
| throw StateError( |
| 'Unexpected node type collapses to no-op: ${node.runtimeType}'); |
| } |
| } |
| } |
| var infoBefore = AtomicEditInfo( |
| descriptionBefore, {FixReasonTarget.root: conditionReason}); |
| var infoAfter = AtomicEditInfo( |
| descriptionAfter, {FixReasonTarget.root: conditionReason}); |
| if (nodeToKeep is Block && nodeToKeep.statements.length == 1) { |
| var singleStatement = nodeToKeep.statements[0]; |
| if (singleStatement is VariableDeclarationStatement) { |
| // It's not safe to eliminate the {} because it increases the scope of |
| // the variable declarations |
| } else { |
| nodeToKeep = singleStatement; |
| } |
| } |
| return aggregator.planner.extract( |
| node, aggregator.planForNode(nodeToKeep) as NodeProducingEditPlan, |
| infoBefore: infoBefore, infoAfter: infoAfter); |
| } |
| } |
| |
| /// Implementation of [NodeChange] specialized for operating on |
| /// [ConditionalExpression] nodes. |
| class NodeChangeForConditionalExpression |
| extends NodeChangeForExpression<ConditionalExpression> |
| with NodeChangeForConditional { |
| @override |
| EditPlan _apply(ConditionalExpression node, FixAggregator aggregator) { |
| return _applyConditional(node, aggregator, node.condition, |
| node.thenExpression, node.elseExpression) ?? |
| super._apply(node, aggregator); |
| } |
| } |
| |
| /// Implementation of [NodeChange] specialized for operating on |
| /// [DefaultFormalParameter] nodes. |
| class NodeChangeForDefaultFormalParameter |
| extends NodeChange<DefaultFormalParameter> { |
| /// Indicates whether a `required` keyword should be added to this node. |
| bool addRequiredKeyword = false; |
| |
| /// If [addRequiredKeyword] is `true`, the information that should be |
| /// contained in the edit. |
| AtomicEditInfo? addRequiredKeywordInfo; |
| |
| /// If [addRequiredKeyword] is `true`, and there is a `/*required*/` hint, |
| /// the hint comment that should be converted into a simple `required` string. |
| HintComment? requiredHint; |
| |
| /// If non-null, indicates a `@required` annotation which should be removed |
| /// from this node. |
| Annotation? annotationToRemove; |
| |
| /// If [annotationToRemove] is non-null, the information that should be |
| /// contained in the edit. |
| AtomicEditInfo? removeAnnotationInfo; |
| |
| NodeChangeForDefaultFormalParameter() : super._(); |
| |
| @override |
| Iterable<String> get _toStringParts => |
| [if (addRequiredKeyword) 'addRequiredKeyword']; |
| |
| @override |
| EditPlan _apply(DefaultFormalParameter node, FixAggregator aggregator) { |
| if (!addRequiredKeyword) return aggregator.innerPlanForNode(node); |
| |
| if (requiredHint != null) { |
| var innerPlan = aggregator.innerPlanForNode(node); |
| return aggregator.planner.acceptPrefixHint(innerPlan, requiredHint!, |
| info: addRequiredKeywordInfo); |
| } |
| |
| var offset = node.firstTokenAfterCommentAndMetadata!.offset; |
| return aggregator.planner.passThrough(node, innerPlans: [ |
| aggregator.planner.insertText(node, offset, [ |
| AtomicEdit.insert('required ', info: addRequiredKeywordInfo), |
| ]), |
| if (annotationToRemove != null) |
| aggregator.planner |
| .removeNode(annotationToRemove!, info: removeAnnotationInfo), |
| ...aggregator.innerPlansForNode(node), |
| ]); |
| } |
| } |
| |
| /// Implementation of [NodeChange] specialized for operating on [Expression] |
| /// nodes. |
| class NodeChangeForExpression<N extends Expression> extends NodeChange<N> { |
| /// The list of [ExpressionChange] objects that should be applied to the |
| /// expression, in the order they should be applied. |
| final List<ExpressionChange> expressionChanges = []; |
| |
| /// The list of [AtomicEditInfo] objects corresponding to each change in |
| /// [expressionChanges]. |
| final List<AtomicEditInfo?> expressionChangeInfos = []; |
| |
| NodeChangeForExpression() : super._(); |
| |
| @override |
| Iterable<String> get _toStringParts => [ |
| for (var expressionChange in expressionChanges) |
| expressionChange.describe() |
| ]; |
| |
| void addExpressionChange(ExpressionChange change, AtomicEditInfo? info) { |
| expressionChanges.add(change); |
| expressionChangeInfos.add(info); |
| } |
| |
| @override |
| EditPlan _apply(N node, FixAggregator aggregator) { |
| var innerPlan = aggregator.innerPlanForNode(node); |
| return _applyExpression(aggregator, innerPlan); |
| } |
| |
| /// If the expression needs to be wrapped in another expression (e.g. a null |
| /// check), wraps the given [innerPlan] to produce appropriate result. |
| /// Otherwise returns [innerPlan] unchanged. |
| NodeProducingEditPlan _applyExpression( |
| FixAggregator aggregator, NodeProducingEditPlan innerPlan) { |
| var plan = innerPlan; |
| for (int i = 0; i < expressionChanges.length; i++) { |
| plan = expressionChanges[i] |
| .applyExpression(aggregator, plan, expressionChangeInfos[i]); |
| } |
| return plan; |
| } |
| } |
| |
| /// Implementation of [NodeChange] specialized for operating on |
| /// [FieldFormalParameter] nodes. |
| class NodeChangeForFieldFormalParameter |
| extends NodeChangeForType<FieldFormalParameter> { |
| /// If not `null`, an explicit type annotation that should be added to the |
| /// parameter. |
| DartType? addExplicitType; |
| |
| NodeChangeForFieldFormalParameter() : super._(); |
| |
| @override |
| Iterable<String> get _toStringParts => |
| [if (addExplicitType != null) 'addExplicitType']; |
| |
| @override |
| EditPlan _apply(FieldFormalParameter node, FixAggregator aggregator) { |
| if (addExplicitType != null) { |
| var typeText = aggregator.typeToCode(addExplicitType!); |
| // Even a field formal parameter can use `var`, `final`. |
| if (node.keyword?.keyword == Keyword.VAR) { |
| // TODO(srawlins): Test instrumentation info. |
| var info = |
| AtomicEditInfo(NullabilityFixDescription.replaceVar(typeText), {}); |
| return aggregator.planner.passThrough(node, innerPlans: [ |
| aggregator.planner |
| .replaceToken(node, node.keyword!, typeText, info: info), |
| ...aggregator.innerPlansForNode(node), |
| ]); |
| } else { |
| // TODO(srawlins): Test instrumentation info. |
| var info = |
| AtomicEditInfo(NullabilityFixDescription.addType(typeText), {}); |
| var offset = node.thisKeyword.offset; |
| return aggregator.planner.passThrough(node, innerPlans: [ |
| aggregator.planner.insertText(node, offset, [ |
| AtomicEdit.insert(typeText, info: info), |
| AtomicEdit.insert(' ') |
| ]), |
| ...aggregator.innerPlansForNode(node), |
| ]); |
| } |
| } else { |
| return super._apply(node, aggregator); |
| } |
| } |
| } |
| |
| /// Implementation of [NodeChange] specialized for operating on |
| /// [FunctionTypedFormalParameter] nodes. |
| class NodeChangeForFunctionTypedFormalParameter |
| extends NodeChangeForType<FunctionTypedFormalParameter> { |
| NodeChangeForFunctionTypedFormalParameter() : super._(); |
| } |
| |
| /// Implementation of [NodeChange] specialized for operating on [IfElement] |
| /// nodes. |
| class NodeChangeForIfElement extends NodeChange<IfElement> |
| with NodeChangeForConditional { |
| NodeChangeForIfElement() : super._(); |
| |
| @override |
| EditPlan _apply(IfElement node, FixAggregator aggregator) { |
| return _applyConditional(node, aggregator, node.condition, node.thenElement, |
| node.elseElement) ?? |
| aggregator.innerPlanForNode(node); |
| } |
| } |
| |
| /// Implementation of [NodeChange] specialized for operating on [IfStatement] |
| /// nodes. |
| class NodeChangeForIfStatement extends NodeChange<IfStatement> |
| with NodeChangeForConditional { |
| NodeChangeForIfStatement() : super._(); |
| |
| @override |
| EditPlan _apply(IfStatement node, FixAggregator aggregator) { |
| return _applyConditional(node, aggregator, node.condition, |
| node.thenStatement, node.elseStatement) ?? |
| aggregator.innerPlanForNode(node); |
| } |
| } |
| |
| /// Implementation of [NodeChange] specialized for operating on |
| /// [MethodDeclaration] nodes. |
| class NodeChangeForMethodDeclaration extends NodeChange<MethodDeclaration> { |
| /// If non-null, indicates a `@nullable` annotation which should be removed |
| /// from this node. |
| Annotation? annotationToRemove; |
| |
| /// If [annotationToRemove] is non-null, the information that should be |
| /// contained in the edit. |
| AtomicEditInfo? removeAnnotationInfo; |
| |
| NodeChangeForMethodDeclaration() : super._(); |
| |
| @override |
| Iterable<String> get _toStringParts => |
| [if (annotationToRemove != null) 'annotationToRemove']; |
| |
| @override |
| EditPlan _apply(MethodDeclaration node, FixAggregator aggregator) { |
| return aggregator.planner.passThrough(node, innerPlans: [ |
| if (annotationToRemove != null) |
| aggregator.planner |
| .removeNode(annotationToRemove!, info: removeAnnotationInfo), |
| ...aggregator.innerPlansForNode(node), |
| ]); |
| } |
| } |
| |
| /// Implementation of [NodeChange] specialized for operating on |
| /// [MethodInvocation] nodes. |
| class NodeChangeForMethodInvocation |
| extends NodeChangeForExpression<MethodInvocation> |
| with NodeChangeForNullAware { |
| @override |
| NodeProducingEditPlan _apply( |
| MethodInvocation node, FixAggregator aggregator) { |
| var target = node.target; |
| var targetPlan = target == null ? null : aggregator.planForNode(target); |
| var nullAwarePlan = _applyNullAware(node, aggregator); |
| var methodNamePlan = aggregator.planForNode(node.methodName); |
| var typeArguments = node.typeArguments; |
| var typeArgumentsPlan = |
| typeArguments == null ? null : aggregator.planForNode(typeArguments); |
| var argumentListPlan = aggregator.planForNode(node.argumentList); |
| var innerPlans = [ |
| if (targetPlan != null) targetPlan, |
| if (nullAwarePlan != null) nullAwarePlan, |
| methodNamePlan, |
| if (typeArgumentsPlan != null) typeArgumentsPlan, |
| argumentListPlan |
| ]; |
| return _applyExpression(aggregator, |
| aggregator.planner.passThrough(node, innerPlans: innerPlans)); |
| } |
| } |
| |
| /// 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; |
| |
| /// Info object associated with the replacement. |
| AtomicEditInfo? _replacementInfo; |
| |
| NodeChangeForMethodName() : super._(); |
| |
| /// Queries the name the method name should be changed to, or `null` if no |
| /// change should be made. |
| @visibleForTesting |
| String? get replacement => _replacement; |
| |
| /// Queries the info object associated with the replacement. |
| @visibleForTesting |
| AtomicEditInfo? get replacementInfo => _replacementInfo; |
| |
| @override |
| Iterable<String> get _toStringParts => |
| [if (replacement != null) 'replacement: $replacement']; |
| |
| /// Updates `this` so that the method name will be changed to [replacement], |
| /// using [info] to annotate the reason for the change. |
| void replaceWith(String replacement, AtomicEditInfo? info) { |
| assert(_replacement == null); |
| _replacement = replacement; |
| _replacementInfo = info; |
| } |
| |
| @override |
| EditPlan _apply(SimpleIdentifier node, FixAggregator aggregator) { |
| if (replacement != null) { |
| return aggregator.planner.replace( |
| node, [AtomicEdit.insert(replacement!, info: replacementInfo)], |
| info: replacementInfo); |
| } 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> { |
| /// Indicates whether null-awareness should be removed. |
| bool removeNullAwareness = false; |
| |
| @override |
| Iterable<String> get _toStringParts => |
| [...super._toStringParts, if (removeNullAwareness) 'removeNullAwareness']; |
| |
| /// Returns an [EditPlan] that removes null awareness, if appropriate. |
| /// Otherwise returns `null`. |
| EditPlan? _applyNullAware(N node, FixAggregator aggregator) { |
| if (!removeNullAwareness) return null; |
| var description = aggregator._warnOnWeakCode! |
| ? NullabilityFixDescription.nullAwarenessUnnecessaryInStrongMode |
| : NullabilityFixDescription.removeNullAwareness; |
| return aggregator.planner.removeNullAwareness(node, |
| info: AtomicEditInfo(description, const {}), |
| isInformative: aggregator._warnOnWeakCode!); |
| } |
| } |
| |
| /// Implementation of [NodeChange] specialized for operating on |
| /// [PostfixExpression] nodes. |
| class NodeChangeForPostfixExpression |
| extends NodeChangeForExpression<PostfixExpression> |
| with NodeChangeForAssignmentLike { |
| @override |
| NodeProducingEditPlan _apply( |
| PostfixExpression node, FixAggregator aggregator) { |
| var operandPlan = aggregator.planForNode(node.operand); |
| var operatorPlan = _makeOperatorPlan(aggregator, node, node.operator); |
| var innerPlans = <EditPlan>[ |
| operandPlan, |
| if (operatorPlan != null) operatorPlan |
| ]; |
| return _applyExpression(aggregator, |
| aggregator.planner.passThrough(node, innerPlans: innerPlans)); |
| } |
| } |
| |
| /// Implementation of [NodeChange] specialized for operating on |
| /// [PrefixExpression] nodes. |
| class NodeChangeForPrefixExpression |
| extends NodeChangeForExpression<PrefixExpression> |
| with NodeChangeForAssignmentLike { |
| @override |
| NodeProducingEditPlan _apply( |
| PrefixExpression node, FixAggregator aggregator) { |
| var operatorPlan = _makeOperatorPlan(aggregator, node, node.operator); |
| var operandPlan = aggregator.planForNode(node.operand); |
| var innerPlans = <EditPlan>[ |
| if (operatorPlan != null) operatorPlan, |
| operandPlan |
| ]; |
| return _applyExpression(aggregator, |
| aggregator.planner.passThrough(node, innerPlans: innerPlans)); |
| } |
| } |
| |
| /// Implementation of [NodeChange] specialized for operating on [PropertyAccess] |
| /// nodes. |
| class NodeChangeForPropertyAccess |
| extends NodeChangeForExpression<PropertyAccess> |
| with NodeChangeForNullAware { |
| @override |
| NodeProducingEditPlan _apply(PropertyAccess node, FixAggregator aggregator) { |
| var targetPlan = |
| node.target == null ? null : aggregator.planForNode(node.target); |
| var nullAwarePlan = _applyNullAware(node, aggregator); |
| var propertyNamePlan = aggregator.planForNode(node.propertyName); |
| var innerPlans = [ |
| if (targetPlan != null) targetPlan, |
| if (nullAwarePlan != null) nullAwarePlan, |
| propertyNamePlan |
| ]; |
| return _applyExpression(aggregator, |
| aggregator.planner.passThrough(node, innerPlans: innerPlans)); |
| } |
| } |
| |
| /// 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 |
| extends NodeChange<SimpleFormalParameter> { |
| /// If not `null`, an explicit type annotation that should be added to the |
| /// parameter. |
| DartType? addExplicitType; |
| |
| NodeChangeForSimpleFormalParameter() : super._(); |
| |
| @override |
| Iterable<String> get _toStringParts => |
| [if (addExplicitType != null) 'addExplicitType']; |
| |
| @override |
| EditPlan _apply(SimpleFormalParameter node, FixAggregator aggregator) { |
| var innerPlan = aggregator.innerPlanForNode(node); |
| if (addExplicitType == null) return innerPlan; |
| var typeText = aggregator.typeToCode(addExplicitType!); |
| if (node.keyword?.keyword == Keyword.VAR) { |
| // TODO(srawlins): Test instrumentation info. |
| var info = |
| AtomicEditInfo(NullabilityFixDescription.replaceVar(typeText), {}); |
| return aggregator.planner.passThrough(node, innerPlans: [ |
| aggregator.planner |
| .replaceToken(node, node.keyword!, typeText, info: info), |
| ...aggregator.innerPlansForNode(node), |
| ]); |
| } else { |
| // TODO(srawlins): Test instrumentation info. |
| var info = |
| AtomicEditInfo(NullabilityFixDescription.addType(typeText), {}); |
| // Skip past the offset of any metadata, a potential `final` keyword, and |
| // a potential `covariant` keyword. |
| var offset = node.type?.offset ?? node.identifier!.offset; |
| return aggregator.planner.passThrough(node, innerPlans: [ |
| aggregator.planner.insertText(node, offset, |
| [AtomicEdit.insert(typeText, info: info), AtomicEdit.insert(' ')]), |
| ...aggregator.innerPlansForNode(node), |
| ]); |
| } |
| } |
| } |
| |
| /// Implementation of [NodeChange] specialized for operating on nodes which |
| /// represent a type, and can be made and hinted nullable and non-nullable. |
| abstract class NodeChangeForType<N extends AstNode> extends NodeChange<N> { |
| bool _makeNullable = false; |
| |
| HintComment? _nullabilityHint; |
| |
| /// The decorated type of the type annotation, or `null` if there is no |
| /// decorated type info of interest. If [makeNullable] is `true`, the node |
| /// from this type will be attached to the edit that adds the `?`. If |
| /// [_makeNullable] is `false`, the node from this type will be attached to |
| /// the information about why the node wasn't made nullable. |
| DecoratedType? _decoratedType; |
| |
| NodeChangeForType._() : super._(); |
| |
| @override |
| bool get isInformative => !_makeNullable; |
| |
| /// Indicates whether the type should be made nullable by adding a `?`. |
| bool get makeNullable => _makeNullable; |
| |
| /// If we are making the type nullable due to a hint, the comment that caused |
| /// it. |
| HintComment? get nullabilityHint => _nullabilityHint; |
| |
| @override |
| Iterable<String> get _toStringParts => [ |
| if (_makeNullable) 'makeNullable', |
| if (_nullabilityHint != null) 'nullabilityHint' |
| ]; |
| |
| void recordNullability(DecoratedType decoratedType, bool makeNullable, |
| {HintComment? nullabilityHint}) { |
| _decoratedType = decoratedType; |
| _makeNullable = makeNullable; |
| _nullabilityHint = nullabilityHint; |
| } |
| |
| @override |
| EditPlan _apply(N node, FixAggregator aggregator) { |
| var innerPlan = aggregator.innerPlanForNode(node); |
| if (_decoratedType == null) return innerPlan; |
| var typeName = |
| _decoratedType!.type!.getDisplayString(withNullability: false); |
| var fixReasons = {FixReasonTarget.root: _decoratedType!.node}; |
| if (_makeNullable) { |
| var hint = _nullabilityHint; |
| if (hint != null) { |
| return aggregator.planner.acceptSuffixHint(innerPlan, hint, |
| info: AtomicEditInfo( |
| NullabilityFixDescription.makeTypeNullableDueToHint(typeName), |
| fixReasons, |
| hintComment: hint)); |
| } else { |
| return aggregator.planner.makeNullable(innerPlan, |
| info: AtomicEditInfo( |
| NullabilityFixDescription.makeTypeNullable(typeName), |
| fixReasons)); |
| } |
| } else { |
| var hint = _nullabilityHint; |
| if (hint != null) { |
| return aggregator.planner.dropNullabilityHint(innerPlan, hint, |
| info: AtomicEditInfo( |
| NullabilityFixDescription.typeNotMadeNullableDueToHint( |
| typeName), |
| fixReasons, |
| hintComment: hint)); |
| } else { |
| return aggregator.planner.explainNonNullable(innerPlan, |
| info: AtomicEditInfo( |
| NullabilityFixDescription.typeNotMadeNullable(typeName), |
| fixReasons)); |
| } |
| } |
| } |
| } |
| |
| /// Implementation of [NodeChange] specialized for operating on [TypeAnnotation] |
| /// nodes. |
| class NodeChangeForTypeAnnotation extends NodeChangeForType<TypeAnnotation> { |
| NodeChangeForTypeAnnotation() : super._(); |
| } |
| |
| /// Implementation of [NodeChange] specialized for operating on |
| /// [VariableDeclarationList] nodes. |
| class NodeChangeForVariableDeclarationList |
| extends NodeChange<VariableDeclarationList> { |
| /// If an explicit type should be added to this variable declaration, the type |
| /// that should be added. Otherwise `null`. |
| DartType? addExplicitType; |
| |
| /// Indicates whether a "late" annotation should be added to this variable |
| /// declaration, caused by inference. |
| LateAdditionReason? lateAdditionReason; |
| |
| /// If a "late" annotation should be added to this variable declaration, and |
| /// the cause is a "late" hint, the hint that caused it. Otherwise `null`. |
| HintComment? lateHint; |
| |
| NodeChangeForVariableDeclarationList() : super._(); |
| |
| @override |
| Iterable<String> get _toStringParts => [ |
| if (addExplicitType != null) 'addExplicitType', |
| if (lateAdditionReason != null) 'lateAdditionReason', |
| if (lateHint != null) 'lateHint' |
| ]; |
| |
| @override |
| EditPlan _apply(VariableDeclarationList node, FixAggregator aggregator) { |
| List<EditPlan> innerPlans = []; |
| if (lateAdditionReason != null) { |
| var description = lateAdditionReason == LateAdditionReason.inference |
| ? NullabilityFixDescription.addLate |
| : NullabilityFixDescription.addLateDueToTestSetup; |
| var info = AtomicEditInfo(description, {}); |
| innerPlans.add(aggregator.planner.insertText( |
| node, |
| node.firstTokenAfterCommentAndMetadata.offset, |
| [AtomicEdit.insert('late', info: info), AtomicEdit.insert(' ')])); |
| } |
| if (addExplicitType != null) { |
| var typeText = aggregator.typeToCode(addExplicitType!); |
| if (node.keyword?.keyword == Keyword.VAR) { |
| var info = |
| AtomicEditInfo(NullabilityFixDescription.replaceVar(typeText), {}); |
| innerPlans.add(aggregator.planner |
| .replaceToken(node, node.keyword!, typeText, info: info)); |
| } else { |
| var info = |
| AtomicEditInfo(NullabilityFixDescription.addType(typeText), {}); |
| innerPlans.add(aggregator.planner.insertText( |
| node, |
| node.variables.first.offset, |
| [AtomicEdit.insert(typeText, info: info), AtomicEdit.insert(' ')])); |
| } |
| } |
| innerPlans.addAll(aggregator.innerPlansForNode(node)); |
| var plan = aggregator.planner.passThrough(node, innerPlans: innerPlans); |
| if (lateHint != null) { |
| var description = lateHint!.kind == HintCommentKind.late_ |
| ? NullabilityFixDescription.addLateDueToHint |
| : NullabilityFixDescription.addLateFinalDueToHint; |
| plan = aggregator.planner.acceptPrefixHint(plan, lateHint!, |
| info: AtomicEditInfo(description, {}, hintComment: lateHint)); |
| } |
| return plan; |
| } |
| } |
| |
| /// [ExpressionChange] describing the addition of a comment explaining that a |
| /// literal `null` could not be migrated. |
| class NoValidMigrationChange extends ExpressionChange { |
| NoValidMigrationChange(DartType resultType) : super(resultType); |
| |
| @override |
| NullabilityFixDescription get description => |
| NullabilityFixDescription.noValidMigrationForNull; |
| |
| @override |
| NodeProducingEditPlan applyExpression(FixAggregator aggregator, |
| NodeProducingEditPlan innerPlan, AtomicEditInfo? info) => |
| aggregator.planner.addCommentPostfix( |
| innerPlan, '/* no valid migration */', |
| info: info, isInformative: true); |
| |
| @override |
| String applyText(FixAggregator aggregator, String inner) => |
| '$inner /* no valid migration */'; |
| |
| @override |
| String describe() => 'NoValidMigrationChange'; |
| } |
| |
| /// [ExpressionChange] describing the addition of an `!` after an expression. |
| class NullCheckChange extends ExpressionChange { |
| /// The hint that is causing this `!` to be added, if any. |
| final HintComment? hint; |
| |
| NullCheckChange(DartType resultType, {this.hint}) : super(resultType); |
| |
| @override |
| NullabilityFixDescription get description => |
| NullabilityFixDescription.checkExpression; |
| |
| @override |
| NodeProducingEditPlan applyExpression(FixAggregator aggregator, |
| NodeProducingEditPlan innerPlan, AtomicEditInfo? info) { |
| if (hint != null) { |
| return aggregator.planner.acceptSuffixHint(innerPlan, hint!, info: info); |
| } else { |
| return aggregator.planner |
| .addUnaryPostfix(innerPlan, TokenType.BANG, info: info); |
| } |
| } |
| |
| @override |
| String applyText(FixAggregator aggregator, String inner) => '$inner!'; |
| |
| @override |
| String describe() => 'NullCheckChange'; |
| } |
| |
| /// Visitor that creates an appropriate [NodeChange] object for the node being |
| /// visited. |
| class _NodeChangeVisitor extends GeneralizingAstVisitor<NodeChange<AstNode>> { |
| static final _instance = _NodeChangeVisitor(); |
| |
| @override |
| NodeChange visitAnnotation(Annotation node) => NodeChangeForAnnotation(); |
| |
| @override |
| NodeChange visitArgumentList(ArgumentList node) => |
| NodeChangeForArgumentList(); |
| |
| @override |
| NodeChange visitAsExpression(AsExpression node) => |
| NodeChangeForAsExpression(); |
| |
| @override |
| NodeChange visitAssignmentExpression(AssignmentExpression node) => |
| NodeChangeForAssignment(); |
| |
| @override |
| NodeChange visitCompilationUnit(CompilationUnit node) => |
| NodeChangeForCompilationUnit(); |
| |
| @override |
| NodeChange visitConditionalExpression(ConditionalExpression node) => |
| NodeChangeForConditionalExpression(); |
| |
| @override |
| NodeChange visitDefaultFormalParameter(DefaultFormalParameter node) => |
| NodeChangeForDefaultFormalParameter(); |
| |
| @override |
| NodeChange visitExpression(Expression node) => NodeChangeForExpression(); |
| |
| @override |
| NodeChange visitFieldFormalParameter(FieldFormalParameter node) => |
| NodeChangeForFieldFormalParameter(); |
| |
| @override |
| NodeChange visitFunctionTypedFormalParameter( |
| FunctionTypedFormalParameter node) => |
| NodeChangeForFunctionTypedFormalParameter(); |
| |
| @override |
| NodeChange visitGenericFunctionType(GenericFunctionType node) => |
| NodeChangeForTypeAnnotation(); |
| |
| @override |
| NodeChange visitIfElement(IfElement node) => NodeChangeForIfElement(); |
| |
| @override |
| NodeChange visitIfStatement(IfStatement node) => NodeChangeForIfStatement(); |
| |
| @override |
| NodeChange visitMethodDeclaration(MethodDeclaration node) => |
| NodeChangeForMethodDeclaration(); |
| |
| @override |
| NodeChange visitMethodInvocation(MethodInvocation node) => |
| NodeChangeForMethodInvocation(); |
| |
| @override |
| NodeChange visitNamedType(NamedType node) => NodeChangeForTypeAnnotation(); |
| |
| @override |
| NodeChange visitNode(AstNode node) => |
| throw StateError('Unexpected node type: ${node.runtimeType}'); |
| |
| @override |
| NodeChange visitPostfixExpression(PostfixExpression node) => |
| NodeChangeForPostfixExpression(); |
| |
| @override |
| NodeChange visitPrefixExpression(PrefixExpression node) => |
| NodeChangeForPrefixExpression(); |
| |
| @override |
| NodeChange visitPropertyAccess(PropertyAccess node) => |
| 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 visitVariableDeclarationList(VariableDeclarationList node) => |
| NodeChangeForVariableDeclarationList(); |
| } |