blob: ff2a53f27861db6bcb022896c8f9f5200d823129 [file] [log] [blame]
// 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);
}