blob: b0d963f052f5095398a3407de22187d80c36e73f [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/decorated_type.dart';
import 'package:analysis_server/src/nullability/nullability_graph.dart';
import 'package:analysis_server/src/nullability/transitional_api.dart';
import 'package:analysis_server/src/nullability/unit_propagation.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:meta/meta.dart';
/// Representation of a single node in the nullability inference graph.
///
/// Initially, this is just a wrapper over constraint variables, and the
/// nullability inference graph is encoded into the wrapped constraint
/// variables. Over time this will be replaced by a first class representation
/// of the nullability inference graph.
abstract class NullabilityNode {
/// [NullabilityNode] used for types that are known a priori to be nullable
/// (e.g. the type of the `null` literal).
static final NullabilityNode always =
_NullabilityNodeSimple(ConstraintVariable.always, 'always');
/// [NullabilityNode] used for types that are known a priori to be
/// non-nullable (e.g. the type of an integer literal).
static final NullabilityNode never = _NullabilityNodeSimple(null, 'never');
static final _debugNamesInUse = Set<String>();
/// [ConstraintVariable] whose value will be set to `true` if this type needs
/// to be nullable.
///
/// If `null`, that means that an external constraint (outside the code being
/// migrated) forces this type to be non-nullable.
final ConstraintVariable _nullable;
ConstraintVariable _nonNullIntent;
bool _isPossiblyOptional = false;
String _debugName;
/// Creates a [NullabilityNode] representing the nullability of a variable
/// whose type is `dynamic` due to type inference.
///
/// TODO(paulberry): this should go away; we should decorate the actual
/// inferred type rather than assuming `dynamic`.
factory NullabilityNode.forInferredDynamicType(
NullabilityGraph graph, Constraints constraints, int offset) {
var node = _NullabilityNodeSimple(
TypeIsNullable(null), 'inferredDynamic($offset)');
constraints.record([], node._nullable);
graph.connect(NullabilityNode.always, node);
return node;
}
/// Creates a [NullabilityNode] representing the nullability of an
/// expression which is nullable iff both [a] and [b] are nullable.
///
/// The constraint variable contained in the new node is created using the
/// [joinNullabilities] callback. TODO(paulberry): this should become
/// unnecessary once constraint solving is performed directly using
/// [NullabilityNode] objects.
factory NullabilityNode.forLUB(
Expression conditionalExpression,
NullabilityNode a,
NullabilityNode b,
NullabilityGraph graph,
ConstraintVariable Function(
Expression, ConstraintVariable, ConstraintVariable)
joinNullabilities) = NullabilityNodeForLUB._;
/// Creates a [NullabilityNode] representing the nullability of a type
/// substitution where [outerNode] is the nullability node for the type
/// variable being eliminated by the substitution, and [innerNode] is the
/// nullability node for the type being substituted in its place.
///
/// [innerNode] may be `null`. TODO(paulberry): when?
///
/// Additional constraints are recorded in [constraints] as necessary to make
/// the new nullability node behave consistently with the old nodes.
/// TODO(paulberry): this should become unnecessary once constraint solving is
/// performed directly using [NullabilityNode] objects.
factory NullabilityNode.forSubstitution(
Constraints constraints,
NullabilityNode innerNode,
NullabilityNode outerNode) = NullabilityNodeForSubstitution._;
/// Creates a [NullabilityNode] representing the nullability of a type
/// annotation appearing explicitly in the user's program.
factory NullabilityNode.forTypeAnnotation(int endOffset,
{@required bool always}) =>
_NullabilityNodeSimple(
always ? ConstraintVariable.always : TypeIsNullable(endOffset),
'type($endOffset)');
NullabilityNode._(this._nullable);
/// Gets a string that can be appended to a type name during debugging to help
/// annotate the nullability of that type.
String get debugSuffix => _nullable == null ? '' : '?($_nullable)';
/// After constraint solving, this getter can be used to query whether the
/// type associated with this node should be considered nullable.
bool get isNullable => _nullable == null ? false : _nullable.value;
/// Indicates whether this node is associated with a named parameter for which
/// nullability migration needs to decide whether it is optional or required.
bool get isPossiblyOptional => _isPossiblyOptional;
/// [ConstraintVariable] whose value will be set to `true` if the usage of
/// this type suggests that it is intended to be non-null (because of the
/// presence of a statement or expression that would unconditionally lead to
/// an exception being thrown in the case of a `null` value at runtime).
ConstraintVariable get nonNullIntent => _nonNullIntent;
String get _debugPrefix;
/// Verifies that the nullability of this node matches [isNullable].
void check(bool isNullable) {
if (isNullable != this.isNullable) {
throw new StateError(
'For $this, new algorithm gives nullability $isNullable; '
'old algorithm gives ${this.isNullable}');
}
}
/// Records the fact that an invocation was made to a function with named
/// parameters, and the named parameter associated with this node was not
/// supplied.
void recordNamedParameterNotSupplied(Constraints constraints,
List<NullabilityNode> guards, NullabilityGraph graph) {
if (isPossiblyOptional) {
_recordConstraints(constraints, guards, const [], _nullable);
graph.connect(NullabilityNode.always, this, guards: guards);
}
}
void recordNonNullIntent(Constraints constraints,
List<NullabilityNode> guards, NullabilityGraph graph) {
_recordConstraints(constraints, guards, const [], nonNullIntent);
graph.connect(this, NullabilityNode.never, unconditional: true);
}
String toString() {
if (_debugName == null) {
var prefix = _debugPrefix;
if (_debugNamesInUse.add(prefix)) {
_debugName = prefix;
} else {
for (int i = 0;; i++) {
var name = '${prefix}_$i';
if (_debugNamesInUse.add(name)) {
_debugName = name;
break;
}
}
}
}
return _debugName;
}
/// Tracks that the possibility that this nullability node might demonstrate
/// non-null intent, based on the fact that it corresponds to a formal
/// parameter declaration at location [offset].
///
/// TODO(paulberry): consider eliminating this method altogether, and simply
/// allowing all nullability nodes to track non-null intent if necessary.
void trackNonNullIntent(int offset) {
assert(_nonNullIntent == null);
_nonNullIntent = NonNullIntent(offset);
}
/// Tracks the possibility that this node is associated with a named parameter
/// for which nullability migration needs to decide whether it is optional or
/// required.
void trackPossiblyOptional() {
_isPossiblyOptional = true;
}
/// Connect the nullability nodes [sourceNode] and [destinationNode]
/// appopriately to account for an assignment in the source code being
/// analyzed. Any constraints generated are recorded in [constraints].
///
/// If [checkNotNull] is non-null, then it tracks the expression that may
/// require null-checking.
///
/// [inConditionalControlFlow] indicates whether the assignment being analyzed
/// is reachable conditionally or unconditionally from the entry point of the
/// function; this affects how non-null intent is back-propagated.
static void recordAssignment(
NullabilityNode sourceNode,
NullabilityNode destinationNode,
CheckExpression checkNotNull,
List<NullabilityNode> guards,
Constraints constraints,
NullabilityGraph graph,
bool inConditionalControlFlow) {
var additionalConditions = <ConstraintVariable>[];
graph.connect(sourceNode, destinationNode,
guards: guards, unconditional: !inConditionalControlFlow);
if (sourceNode._nullable != null) {
additionalConditions.add(sourceNode._nullable);
var destinationNonNullIntent = destinationNode.nonNullIntent;
// nullable_src => nullable_dst | check_expr
_recordConstraints(
constraints,
guards,
additionalConditions,
ConstraintVariable.or(
constraints, destinationNode._nullable, checkNotNull));
if (checkNotNull != null) {
// nullable_src & nonNullIntent_dst => check_expr
if (destinationNonNullIntent != null) {
additionalConditions.add(destinationNonNullIntent);
_recordConstraints(
constraints, guards, additionalConditions, checkNotNull);
}
}
additionalConditions.clear();
var sourceNonNullIntent = sourceNode.nonNullIntent;
if (!inConditionalControlFlow && sourceNonNullIntent != null) {
if (destinationNode._nullable == null) {
// The destination type can never be nullable so this demonstrates
// non-null intent.
_recordConstraints(
constraints, guards, additionalConditions, sourceNonNullIntent);
} else if (destinationNonNullIntent != null) {
// Propagate non-null intent from the destination to the source.
additionalConditions.add(destinationNonNullIntent);
_recordConstraints(
constraints, guards, additionalConditions, sourceNonNullIntent);
}
}
}
}
static void _recordConstraints(
Constraints constraints,
List<NullabilityNode> guards,
List<ConstraintVariable> additionalConditions,
ConstraintVariable consequence) {
var conditions = guards.map((node) => node._nullable).toList();
conditions.addAll(additionalConditions);
constraints.record(conditions, consequence);
}
}
/// Derived class for nullability nodes that arise from the least-upper-bound
/// implied by a conditional expression.
class NullabilityNodeForLUB extends NullabilityNode {
final NullabilityNode left;
final NullabilityNode right;
NullabilityNodeForLUB._(
Expression expression,
this.left,
this.right,
NullabilityGraph graph,
ConstraintVariable Function(
ConditionalExpression, ConstraintVariable, ConstraintVariable)
joinNullabilities)
: super._(
joinNullabilities(expression, left._nullable, right._nullable)) {
graph.connect(left, this);
graph.connect(right, this);
}
@override
String get _debugPrefix => 'LUB($left, $right)';
}
/// Derived class for nullability nodes that arise from type variable
/// substitution.
class NullabilityNodeForSubstitution extends NullabilityNode {
/// Nullability node representing the inner type of the substitution.
///
/// For example, if this NullabilityNode arose from substituting `int*` for
/// `T` in the type `T*`, [innerNode] is the nullability corresponding to the
/// `*` in `int*`.
final NullabilityNode innerNode;
/// Nullability node representing the outer type of the substitution.
///
/// For example, if this NullabilityNode arose from substituting `int*` for
/// `T` in the type `T*`, [innerNode] is the nullability corresponding to the
/// `*` in `T*`.
final NullabilityNode outerNode;
NullabilityNodeForSubstitution._(
Constraints constraints, this.innerNode, this.outerNode)
: super._(ConstraintVariable.or(
constraints, innerNode?._nullable, outerNode._nullable));
@override
String get _debugPrefix => 'Substituted($innerNode, $outerNode)';
}
class _NullabilityNodeSimple extends NullabilityNode {
@override
final String _debugPrefix;
_NullabilityNodeSimple(ConstraintVariable nullable, this._debugPrefix)
: super._(nullable);
}