| // 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; |
| } |
| } |