blob: 481a4e8b847b6ef7ebb78818e55a147c8e272f87 [file] [log] [blame]
// Copyright (c) 2018, 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 'dart:collection';
import 'package:kernel/core_types.dart';
import 'package:kernel/kernel.dart';
import 'package:kernel/type_environment.dart';
import 'js_typerep.dart';
import 'kernel_helpers.dart';
/// Determines whether a given expression [isNullable].
///
/// This class can also analyze the nullability of local variables, if
/// [enterFunction] and [exitFunction] are used.
class NullableInference extends ExpressionVisitor<bool> {
final TypeEnvironment types;
final JSTypeRep jsTypeRep;
final CoreTypes coreTypes;
/// Whether `@notNull` and `@nullCheck` declarations are honored.
///
/// This is set when compiling the SDK and in tests. Even when set to `false`,
/// non-SDK code will still benefit from `@notNull` annotations declared on
/// SDK APIs.
///
/// Since `@notNull` cannot be imported by user code, this flag is only
/// a performance optimization, so we don't spend time looking for
/// annotations that cannot exist in user code.
bool allowNotNullDeclarations = false;
/// Allows `@notNull` and `@nullCheck` to be declared in `package:meta`
/// instead of requiring a private SDK library.
///
/// This is currently used by tests, in conjunction with
/// [allowNotNullDeclarations].
bool allowPackageMetaAnnotations = false;
final _variableInference = _NullableVariableInference();
NullableInference(this.jsTypeRep)
: types = jsTypeRep.types,
coreTypes = jsTypeRep.coreTypes {
_variableInference._nullInference = this;
}
/// Call when entering a function to enable [isNullable] to recognize local
/// variables that cannot be null.
void enterFunction(FunctionNode fn) => _variableInference.enterFunction(fn);
/// Call when exiting a function to clear out the information recorded by
/// [enterFunction].
void exitFunction(FunctionNode fn) => _variableInference.exitFunction(fn);
/// Returns true if [expr] can be null.
bool isNullable(Expression expr) =>
expr != null ? expr.accept(this) as bool : false;
@override
defaultExpression(Expression node) => true;
@override
defaultBasicLiteral(BasicLiteral node) => false;
@override
visitNullLiteral(NullLiteral node) => true;
@override
visitVariableGet(VariableGet node) {
return _variableInference.variableIsNullable(node.variable);
}
@override
visitVariableSet(VariableSet node) => isNullable(node.value);
@override
visitPropertyGet(PropertyGet node) => _getterIsNullable(node.interfaceTarget);
@override
visitPropertySet(PropertySet node) => isNullable(node.value);
@override
visitDirectPropertyGet(DirectPropertyGet node) =>
_getterIsNullable(node.target);
@override
visitDirectPropertySet(DirectPropertySet node) => isNullable(node.value);
@override
visitSuperPropertyGet(SuperPropertyGet node) =>
_getterIsNullable(node.interfaceTarget);
@override
visitSuperPropertySet(SuperPropertySet node) => isNullable(node.value);
@override
visitStaticGet(StaticGet node) => _getterIsNullable(node.target);
@override
visitStaticSet(StaticSet node) => isNullable(node.value);
@override
visitMethodInvocation(MethodInvocation node) => _invocationIsNullable(
node.interfaceTarget, node.name.name, node.receiver);
@override
visitDirectMethodInvocation(DirectMethodInvocation node) =>
_invocationIsNullable(node.target, node.name.name, node.receiver);
@override
visitSuperMethodInvocation(SuperMethodInvocation node) =>
_invocationIsNullable(node.interfaceTarget, node.name.name);
bool _invocationIsNullable(Member target, String name,
[Expression receiver]) {
// TODO(jmesserly): this is not a valid assumption for user-defined equality
// but it is added to match the behavior of the Analyzer backend.
// https://github.com/dart-lang/sdk/issues/31854
if (name == '==') return false;
if (target == null) return true; // dynamic call
if (target.name.name == 'toString' &&
receiver != null &&
receiver.getStaticType(types) == coreTypes.stringClass.rawType) {
// TODO(jmesserly): `class String` in dart:core does not explicitly
// declare `toString`, which results in a target of `Object.toString` even
// when the reciever type is known to be `String`. So we work around it.
// (The Analyzer backend of DDC probably has the same issue.)
return false;
}
return _returnValueIsNullable(target);
}
bool _getterIsNullable(Member target) {
if (target == null) return true;
// tear-offs are not null
if (target is Procedure && !target.isAccessor) return false;
return _returnValueIsNullable(target);
}
bool _returnValueIsNullable(Member target) {
var targetClass = target.enclosingClass;
if (targetClass != null) {
// Convert `int` `double` `num` `String` and `bool` to their corresponding
// implementation class in dart:_interceptors, for example `JSString`.
//
// This allows us to find the `@notNull` annotation if it exists.
var implClass = jsTypeRep.getImplementationClass(targetClass.rawType);
if (implClass != null) {
var member =
jsTypeRep.hierarchy.getDispatchTarget(implClass, target.name);
if (member != null) target = member;
}
}
// If the method or function is annotated as returning a non-null value
// then the result of the call is non-null.
var annotations = target.annotations;
if (annotations.isNotEmpty && annotations.any(isNotNullAnnotation)) {
return false;
}
return true;
}
@override
visitStaticInvocation(StaticInvocation node) {
var target = node.target;
if (target == types.coreTypes.identicalProcedure) return false;
if (isInlineJS(target)) {
var args = node.arguments.positional;
var first = args.isNotEmpty ? args.first : null;
if (first is StringLiteral) {
var typeString = first.value;
return typeString == '' ||
typeString == 'var' ||
typeString.split('|').contains('Null');
}
}
return _invocationIsNullable(target, node.name.name);
}
@override
visitConstructorInvocation(ConstructorInvocation node) => false;
@override
visitNot(Not node) => false;
@override
visitLogicalExpression(LogicalExpression node) => false;
@override
visitConditionalExpression(ConditionalExpression node) =>
isNullable(node.then) || isNullable(node.otherwise);
@override
visitStringConcatenation(StringConcatenation node) => false;
@override
visitIsExpression(IsExpression node) => false;
@override
visitAsExpression(AsExpression node) => isNullable(node.operand);
@override
visitSymbolLiteral(SymbolLiteral node) => false;
@override
visitTypeLiteral(TypeLiteral node) => false;
@override
visitThisExpression(ThisExpression node) => false;
@override
visitRethrow(Rethrow node) => false;
@override
visitThrow(Throw node) => false;
@override
visitListLiteral(ListLiteral node) => false;
@override
visitMapLiteral(MapLiteral node) => false;
@override
visitAwaitExpression(AwaitExpression node) => true;
@override
visitFunctionExpression(FunctionExpression node) => false;
@override
visitConstantExpression(ConstantExpression node) {
var c = node.constant;
if (c is UnevaluatedConstant) return c.expression.accept(this) as bool;
if (c is PrimitiveConstant) return c.value == null;
return false;
}
@override
visitLet(Let node) => isNullable(node.body);
@override
visitInstantiation(Instantiation node) => false;
bool isNotNullAnnotation(Expression value) =>
_isInternalAnnotationField(value, 'notNull', '_NotNull');
bool isNullCheckAnnotation(Expression value) =>
_isInternalAnnotationField(value, 'nullCheck', '_NullCheck');
bool _isInternalAnnotationField(
Expression node, String fieldName, String className) {
node = unwrapUnevaluatedConstant(node);
if (node is ConstantExpression) {
var constant = node.constant;
return constant is InstanceConstant &&
constant.classNode.name == className &&
_isInternalSdkAnnotation(constant.classNode.enclosingLibrary);
}
if (node is StaticGet) {
var t = node.target;
return t is Field &&
t.name.name == fieldName &&
_isInternalSdkAnnotation(t.enclosingLibrary);
}
return false;
}
bool _isInternalSdkAnnotation(Library library) {
var uri = library.importUri;
return uri.scheme == 'dart' && uri.pathSegments[0] == '_js_helper' ||
allowPackageMetaAnnotations &&
uri.scheme == 'package' &&
uri.pathSegments[0] == 'meta';
}
}
/// A visitor that determines which local variables cannot be null using
/// flow-insensitive inference.
///
/// The entire body of a function (including any local functions) is visited
/// recursively, and we collect variables that are tentatively believed to be
/// not-null. If the assumption turns out to be incorrect (based on a later
/// assignment of a nullable value), we remove it from the not-null set, along
/// with any variable it was assigned to.
///
/// This does not track nullable locals, so we can avoid doing work on any
/// variables that have already been determined to be nullable.
///
// TODO(jmesserly): Introduce flow analysis.
class _NullableVariableInference extends RecursiveVisitor<void> {
NullableInference _nullInference;
/// Variables that are currently believed to be not-null.
final _notNullLocals = HashSet<VariableDeclaration>.identity();
/// For each variable currently believed to be not-null ([_notNullLocals]),
/// this collects variables that it is assigned to, so we update them if we
/// later determine that the variable can be null.
final _assignedTo =
HashMap<VariableDeclaration, List<VariableDeclaration>>.identity();
/// All functions that have been analyzed with [analyzeFunction].
///
/// In practice this will include the outermost function (typically a
/// [Procedure]) as well as an local functions it contains.
final _functions = HashSet<FunctionNode>.identity();
/// The current variable we are setting/initializing, so we can track if it
/// is [_assignedTo] from another variable.
VariableDeclaration _variableAssignedTo;
void enterFunction(FunctionNode node) {
if (_functions.contains(node)) return; // local function already analyzed.
visitFunctionNode(node);
_assignedTo.clear();
}
void exitFunction(FunctionNode node) {
var removed = _functions.remove(node);
assert(removed);
if (_functions.isEmpty) _notNullLocals.clear();
}
@override
visitFunctionDeclaration(FunctionDeclaration node) {
_notNullLocals.add(node.variable);
node.function?.accept(this);
}
@override
visitFunctionNode(FunctionNode node) {
_functions.add(node);
if (_nullInference.allowNotNullDeclarations) {
visitList(node.positionalParameters, this);
visitList(node.namedParameters, this);
}
node.body?.accept(this);
}
@override
visitCatch(Catch node) {
// The stack trace variable is not nullable, but the exception can be.
var stackTrace = node.stackTrace;
if (stackTrace != null) _notNullLocals.add(stackTrace);
super.visitCatch(node);
}
@override
visitVariableDeclaration(VariableDeclaration node) {
if (_nullInference.allowNotNullDeclarations) {
var annotations = node.annotations;
if (annotations.isNotEmpty &&
(annotations.any(_nullInference.isNotNullAnnotation) ||
annotations.any(_nullInference.isNullCheckAnnotation))) {
_notNullLocals.add(node);
}
}
var initializer = node.initializer;
if (initializer != null) {
var savedVariable = _variableAssignedTo;
_variableAssignedTo = node;
if (!_nullInference.isNullable(initializer)) _notNullLocals.add(node);
_variableAssignedTo = savedVariable;
}
initializer?.accept(this);
}
@override
visitVariableGet(VariableGet node) {}
@override
visitVariableSet(VariableSet node) {
var variable = node.variable;
var value = node.value;
if (_notNullLocals.contains(variable)) {
var savedVariable = _variableAssignedTo;
_variableAssignedTo = variable;
if (_nullInference.isNullable(node.value)) {
markNullable(VariableDeclaration v) {
_notNullLocals.remove(v);
_assignedTo.remove(v)?.forEach(markNullable);
}
markNullable(variable);
}
_variableAssignedTo = savedVariable;
}
value.accept(this);
}
bool variableIsNullable(VariableDeclaration variable) {
if (_notNullLocals.contains(variable)) {
if (_variableAssignedTo != null) {
_assignedTo.putIfAbsent(variable, () => []).add(_variableAssignedTo);
}
return false;
}
return true;
}
}