| // 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:analysis_server/src/nullability/conditional_discard.dart'; |
| import 'package:analysis_server/src/nullability/constraint_gatherer.dart'; |
| import 'package:analysis_server/src/nullability/constraint_variable_gatherer.dart'; |
| import 'package:analysis_server/src/nullability/decorated_type.dart'; |
| import 'package:analysis_server/src/nullability/expression_checks.dart'; |
| import 'package:analysis_server/src/nullability/unit_propagation.dart'; |
| import 'package:analyzer/dart/ast/ast.dart'; |
| import 'package:analyzer/dart/element/element.dart'; |
| import 'package:analyzer/src/generated/resolver.dart'; |
| import 'package:analyzer/src/generated/source.dart'; |
| import 'package:analyzer_plugin/protocol/protocol_common.dart' show SourceEdit; |
| |
| /// Type of a [ConstraintVariable] representing the addition of a null check. |
| class CheckExpression extends ConstraintVariable { |
| final int offset; |
| |
| CheckExpression(Expression expression) : offset = expression.end; |
| |
| @override |
| toString() => 'checkNotNull($offset)'; |
| } |
| |
| /// Records information about how a conditional expression or statement might |
| /// need to be modified. |
| class ConditionalModification extends PotentialModification { |
| final int offset; |
| |
| final int end; |
| |
| final bool isStatement; |
| |
| final ConditionalDiscard discard; |
| |
| final _KeepNode condition; |
| |
| final _KeepNode thenStatement; |
| |
| final _KeepNode elseStatement; |
| |
| factory ConditionalModification(AstNode node, ConditionalDiscard discard) { |
| if (node is IfStatement) { |
| return ConditionalModification._( |
| node.offset, |
| node.end, |
| node is Statement, |
| discard, |
| _KeepNode(node.condition), |
| _KeepNode(node.thenStatement), |
| _KeepNode(node.elseStatement)); |
| } else { |
| throw new UnimplementedError('TODO(paulberry)'); |
| } |
| } |
| |
| ConditionalModification._(this.offset, this.end, this.isStatement, |
| this.discard, this.condition, this.thenStatement, this.elseStatement); |
| |
| @override |
| bool get isEmpty => discard.keepTrue.value && discard.keepFalse.value; |
| |
| @override |
| Iterable<SourceEdit> get modifications { |
| if (isEmpty) return const []; |
| // TODO(paulberry): move the following logic into DartEditBuilder (see |
| // dartbug.com/35872). |
| var result = <SourceEdit>[]; |
| var keepNodes = <_KeepNode>[]; |
| if (!discard.pureCondition) { |
| keepNodes.add(condition); // TODO(paulberry): test |
| } |
| if (discard.keepTrue.value) { |
| keepNodes.add(thenStatement); // TODO(paulberry): test |
| } |
| if (discard.keepFalse.value) { |
| keepNodes.add(elseStatement); // TODO(paulberry): test |
| } |
| // TODO(paulberry): test thoroughly |
| for (int i = 0; i < keepNodes.length; i++) { |
| var keepNode = keepNodes[i]; |
| if (i == 0 && keepNode.offset != offset) { |
| result.add(SourceEdit(offset, 0, '/* ')); |
| } |
| if (i != 0 || keepNode.offset != offset) { |
| result.add(SourceEdit(keepNode.offset, 0, '*/ ')); |
| } |
| if (i != keepNodes.length - 1 || keepNode.end != end) { |
| result.add(SourceEdit(keepNode.end, 0, |
| keepNode.isExpression && isStatement ? '; /*' : ' /*')); |
| } |
| if (i == keepNodes.length - 1 && keepNode.end != end) { |
| result.add(SourceEdit(end, 0, ' */')); |
| } |
| } |
| return result; |
| } |
| } |
| |
| /// Enum encapsulating the various options proposed at |
| /// https://github.com/dart-lang/language/issues/156#issuecomment-460525075 |
| enum DefaultParameterHandling { |
| /// Option 2: Add required named parameters |
| /// |
| /// - `{int x}` implicitly means `x` is required |
| /// - required-ness goes into the function type: |
| /// `int Function({required int x})` |
| /// - `{required int? x}` is allowed |
| /// - means that something must be passed |
| /// - passing null is allowed |
| /// - `{int x = 3}` is allowed |
| /// - `x` is optional |
| /// - passing null to it is an error |
| /// - passing nothing to it results in it getting the default value |
| /// - `[int x]` is an error |
| /// - `[int x = 3]` is allowed |
| option2_addRequiredNamedParameters, |
| } |
| |
| /// Enum representing the possible heuristics for handling named parameters with |
| /// no default value. |
| enum NamedNoDefaultParameterHeuristic { |
| /// Assume that the parameter should be considered nullable, unless the user |
| /// has explicitly marked it as `@required`. |
| assumeNullable, |
| |
| /// Assume that the parameter should be considered required, unless the user |
| /// has explicitly marked it as nullable. |
| assumeRequired, |
| } |
| |
| /// Transitional migration API. |
| /// |
| /// Usage: pass each input source file to [prepareInput]. Then pass each input |
| /// source file to [processInput]. Then call [finish] to obtain the |
| /// modifications that need to be made to each source file. |
| /// |
| /// TODO(paulberry): this implementation keeps a lot of CompilationUnit objects |
| /// around. Can we do better? |
| class NullabilityMigration { |
| final bool _permissive; |
| |
| final NullabilityMigrationAssumptions assumptions; |
| |
| final _variables = Variables(); |
| |
| final _constraints = Solver(); |
| |
| /// Prepares to perform nullability migration. |
| /// |
| /// If [permissive] is `true`, exception handling logic will try to proceed |
| /// as far as possible even though the migration algorithm is not yet |
| /// complete. TODO(paulberry): remove this mode once the migration algorithm |
| /// is fully implemented. |
| NullabilityMigration( |
| {bool permissive: false, |
| this.assumptions: const NullabilityMigrationAssumptions()}) |
| : _permissive = permissive; |
| |
| Map<Source, List<PotentialModification>> finish() { |
| _constraints.applyHeuristics(); |
| return _variables.getPotentialModifications(); |
| } |
| |
| void prepareInput(CompilationUnit unit) { |
| unit.accept(ConstraintVariableGatherer( |
| _variables, unit.declaredElement.source, _permissive, assumptions)); |
| } |
| |
| void processInput(CompilationUnit unit, TypeProvider typeProvider) { |
| unit.accept(ConstraintGatherer(typeProvider, _variables, _constraints, |
| unit.declaredElement.source, _permissive, assumptions)); |
| } |
| } |
| |
| /// Assumptions affecting the behavior of the nullability migration tool. |
| /// |
| /// These options generally reflect design decisions that have not yet been |
| /// made. They don't reflect behavioral differences we would want to expose to |
| /// the user. |
| /// |
| /// TODO(paulberry): hardcode these assumptions once decisions have been made. |
| class NullabilityMigrationAssumptions { |
| /// Handling of default parameters. |
| final DefaultParameterHandling defaultParameterHandling; |
| |
| /// Heuristic for handling named parameters with no default value. |
| final NamedNoDefaultParameterHeuristic namedNoDefaultParameterHeuristic; |
| |
| const NullabilityMigrationAssumptions( |
| {this.defaultParameterHandling: |
| DefaultParameterHandling.option2_addRequiredNamedParameters, |
| this.namedNoDefaultParameterHeuristic: |
| NamedNoDefaultParameterHeuristic.assumeNullable}); |
| } |
| |
| /// Records information about the possible addition of a `@required` annotation |
| /// to the source code. |
| class PotentiallyAddRequired extends PotentialModification { |
| final ConstraintVariable _optionalVariable; |
| |
| final int _offset; |
| |
| PotentiallyAddRequired( |
| DefaultFormalParameter parameter, this._optionalVariable) |
| : _offset = parameter.offset; |
| |
| @override |
| bool get isEmpty => _optionalVariable.value; |
| |
| @override |
| Iterable<SourceEdit> get modifications => |
| isEmpty ? const [] : [SourceEdit(_offset, 0, '@required ')]; |
| } |
| |
| /// Interface used by data structures representing potential modifications to |
| /// the code being migrated. |
| abstract class PotentialModification { |
| bool get isEmpty; |
| |
| /// Gets the individual migrations that need to be done, considering the |
| /// solution to the constraint equations. |
| Iterable<SourceEdit> get modifications; |
| } |
| |
| class Variables implements VariableRecorder, VariableRepository { |
| final _decoratedElementTypes = <Element, DecoratedType>{}; |
| |
| final _potentialModifications = <Source, List<PotentialModification>>{}; |
| |
| @override |
| DecoratedType decoratedElementType(Element element, {bool create: false}) => |
| _decoratedElementTypes[element] ??= create |
| ? DecoratedType.forElement(element) |
| : throw StateError('No element found'); |
| |
| Map<Source, List<PotentialModification>> getPotentialModifications() => |
| _potentialModifications; |
| |
| @override |
| void recordConditionalDiscard( |
| Source source, AstNode node, ConditionalDiscard conditionalDiscard) { |
| _addPotentialModification( |
| source, ConditionalModification(node, conditionalDiscard)); |
| } |
| |
| void recordDecoratedElementType(Element element, DecoratedType type) { |
| _decoratedElementTypes[element] = type; |
| } |
| |
| void recordDecoratedExpressionType(Expression node, DecoratedType type) {} |
| |
| void recordDecoratedTypeAnnotation( |
| Source source, TypeAnnotation node, DecoratedTypeAnnotation type) { |
| _addPotentialModification(source, type); |
| } |
| |
| @override |
| void recordExpressionChecks( |
| Source source, Expression expression, ExpressionChecks checks) { |
| _addPotentialModification(source, checks); |
| } |
| |
| @override |
| void recordPossiblyOptional(Source source, DefaultFormalParameter parameter, |
| ConstraintVariable variable) { |
| _addPotentialModification( |
| source, PotentiallyAddRequired(parameter, variable)); |
| } |
| |
| void _addPotentialModification( |
| Source source, PotentialModification potentialModification) { |
| (_potentialModifications[source] ??= []).add(potentialModification); |
| } |
| } |
| |
| /// Helper object used by [ConditionalModification] to keep track of AST nodes |
| /// within the conditional expression. |
| class _KeepNode { |
| final int offset; |
| |
| final int end; |
| |
| final bool isExpression; |
| |
| factory _KeepNode(AstNode node) { |
| int offset = node.offset; |
| int end = node.end; |
| if (node is Block && node.statements.isNotEmpty) { |
| offset = node.statements.beginToken.offset; |
| end = node.statements.endToken.end; |
| } |
| return _KeepNode._(offset, end, node is Expression); |
| } |
| |
| _KeepNode._(this.offset, this.end, this.isExpression); |
| } |