| // Copyright (c) 2015, 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:analyzer/dart/ast/ast.dart'; |
| import 'package:analyzer/dart/ast/standard_resolution_map.dart'; |
| import 'package:analyzer/dart/ast/token.dart' show TokenType; |
| import 'package:analyzer/dart/ast/visitor.dart' show RecursiveAstVisitor; |
| import 'package:analyzer/dart/element/element.dart'; |
| import 'package:analyzer/dart/element/type.dart'; |
| import 'element_helpers.dart' show getStaticType, isInlineJS, findAnnotation; |
| import 'js_interop.dart' show isNotNullAnnotation, isNullCheckAnnotation; |
| import 'js_typerep.dart'; |
| import 'property_model.dart'; |
| |
| /// An inference engine for nullable types. |
| /// |
| /// This can answer questions about whether expressions are nullable |
| /// (see [isNullable]). Given a set of compilation units in a library, it will |
| /// determine if locals can be null using flow-insensitive analysis. |
| /// |
| /// The analysis for null expressions is conservative and incomplete, but it can |
| /// optimize some patterns. |
| // TODO(vsm): Revisit whether we really need this when we get |
| // better non-nullability in the type system. |
| abstract class NullableTypeInference { |
| LibraryElement get coreLibrary; |
| VirtualFieldModel get virtualFields; |
| |
| JSTypeRep get jsTypeRep; |
| bool isObjectMember(String name); |
| |
| /// Known non-null local variables. |
| HashSet<LocalVariableElement> _notNullLocals; |
| |
| void inferNullableTypes(AstNode node) { |
| var visitor = _NullableLocalInference(this); |
| node.accept(visitor); |
| _notNullLocals = visitor.computeNotNullLocals(); |
| } |
| |
| /// Adds a new variable, typically a compiler generated temporary, and record |
| /// whether its type is nullable. |
| void addTemporaryVariable(LocalVariableElement local, |
| {bool nullable = true}) { |
| if (!nullable) _notNullLocals.add(local); |
| } |
| |
| /// Returns true if [expr] can be null. |
| bool isNullable(Expression expr) => _isNullable(expr); |
| |
| bool _isNonNullMethodInvocation(MethodInvocation expr) { |
| // TODO(vsm): This logic overlaps with the resolver. |
| // Where is the best place to put this? |
| var e = resolutionMap.staticElementForIdentifier(expr.methodName); |
| if (e == null) return false; |
| if (isInlineJS(e)) { |
| // Fix types for JS builtin calls. |
| // |
| // This code was taken from analyzer. It's not super sophisticated: |
| // only looks for the type name in dart:core, so we just copy it here. |
| // |
| // TODO(jmesserly): we'll likely need something that can handle a wider |
| // variety of types, especially when we get to JS interop. |
| var args = expr.argumentList.arguments; |
| var first = args.isNotEmpty ? args.first : null; |
| if (first is SimpleStringLiteral) { |
| var types = first.stringValue; |
| if (types != '' && |
| types != 'var' && |
| !types.split('|').contains('Null')) { |
| return true; |
| } |
| } |
| } |
| |
| if (e.name == 'identical' && identical(e.library, coreLibrary)) { |
| return true; |
| } |
| // If this is a method call, check to see whether it is to a final |
| // type for which we have a known implementation type (i.e. int, bool, |
| // double, and String), and if so use the element for the implementation |
| // type instead. |
| if (e is MethodElement) { |
| Element container = e.enclosingElement; |
| if (container is ClassElement) { |
| DartType targetType = container.type; |
| InterfaceType implType = jsTypeRep.getImplementationType(targetType); |
| if (implType != null) { |
| MethodElement method = implType.lookUpMethod(e.name, coreLibrary); |
| if (method != null) e = method; |
| } |
| } |
| } |
| // If the method or function is annotated as returning a non-null value |
| // then the result of the call is non-null. |
| return (e is MethodElement || e is FunctionElement) && _assertedNotNull(e); |
| } |
| |
| bool _isNonNullProperty(Element element, String name) { |
| if (element is! PropertyInducingElement && |
| element is! PropertyAccessorElement) { |
| return false; |
| } |
| // If this is a reference to an element of a type for which |
| // we have a known implementation type (i.e. int, double, |
| // bool, String), then use the element for the implementation |
| // type. |
| Element container = element.enclosingElement; |
| if (container is ClassElement) { |
| var targetType = container.type; |
| var implType = jsTypeRep.getImplementationType(targetType); |
| if (implType != null) { |
| var getter = implType.lookUpGetter(name, coreLibrary); |
| if (getter != null) element = getter; |
| } |
| } |
| // If the getter is a synthetic element, then any annotations will |
| // be on the variable, so use those instead. |
| if (element is PropertyAccessorElement && element.isSynthetic) { |
| return _assertedNotNull(element.variable); |
| } |
| // Return true if the element is annotated as returning a non-null value. |
| return _assertedNotNull(element); |
| } |
| |
| /// Returns true if [expr] can be null, optionally using [localIsNullable] |
| /// for locals. |
| /// |
| /// If [localIsNullable] is not supplied, this will use the known list of |
| /// [_notNullLocals]. |
| bool _isNullable(Expression expr, |
| [bool localIsNullable(LocalVariableElement e)]) { |
| // TODO(jmesserly): we do recursive calls in a few places. This could |
| // leads to O(depth) cost for calling this function. We could store the |
| // resulting value if that becomes an issue, so we maintain the invariant |
| // that each node is visited once. |
| Element element = null; |
| String name = null; |
| if (expr is PropertyAccess && |
| expr.operator?.type != TokenType.QUESTION_PERIOD) { |
| element = expr.propertyName.staticElement; |
| name = expr.propertyName.name; |
| } else if (expr is PrefixedIdentifier) { |
| element = expr.staticElement; |
| name = expr.identifier.name; |
| } else if (expr is Identifier) { |
| element = expr.staticElement; |
| name = expr.name; |
| } |
| if (element != null) { |
| if (_isNonNullProperty(element, name)) return false; |
| |
| // Type literals are not null. |
| if (element is ClassElement || element is FunctionTypeAliasElement) { |
| return false; |
| } |
| |
| if (element is LocalVariableElement) { |
| if (localIsNullable != null) { |
| return localIsNullable(element); |
| } |
| return !_notNullLocals.contains(element); |
| } |
| |
| if (element is ParameterElement && _assertedNotNull(element)) { |
| return false; |
| } |
| |
| if (element is FunctionElement || element is MethodElement) { |
| // A function or method. This can't be null. |
| return false; |
| } |
| |
| if (element is PropertyAccessorElement && element.isGetter) { |
| PropertyInducingElement variable = element.variable; |
| if (variable is FieldElement && virtualFields.isVirtual(variable)) { |
| return true; |
| } |
| var value = variable.computeConstantValue(); |
| return value == null || value.isNull || !value.hasKnownValue; |
| } |
| |
| // Other types of identifiers are nullable (parameters, fields). |
| return true; |
| } |
| |
| if (expr is Literal) return expr is NullLiteral; |
| if (expr is IsExpression) return false; |
| if (expr is FunctionExpression) return false; |
| if (expr is ThisExpression) return false; |
| if (expr is SuperExpression) return false; |
| if (expr is CascadeExpression) { |
| // Cascades normally can't return `null`, because if the target is null, |
| // they will throw noSuchMethod. |
| // The only properties/methods on `null` are those on Object itself. |
| for (var section in expr.cascadeSections) { |
| Element e = null; |
| if (section is PropertyAccess) { |
| e = section.propertyName.staticElement; |
| } else if (section is MethodInvocation) { |
| e = section.methodName.staticElement; |
| } else if (section is IndexExpression) { |
| // Object does not have operator []=. |
| return false; |
| } |
| // We encountered a non-Object method/property. |
| if (e != null && !isObjectMember(e.name)) { |
| return false; |
| } |
| } |
| return _isNullable(expr.target, localIsNullable); |
| } |
| if (expr is ConditionalExpression) { |
| return _isNullable(expr.thenExpression, localIsNullable) || |
| _isNullable(expr.elseExpression, localIsNullable); |
| } |
| if (expr is ParenthesizedExpression) { |
| return _isNullable(expr.expression, localIsNullable); |
| } |
| if (expr is InstanceCreationExpression) { |
| var e = resolutionMap.staticElementForConstructorReference(expr); |
| if (e == null) return true; |
| |
| // Follow redirects. |
| while (e.redirectedConstructor != null) { |
| e = e.redirectedConstructor; |
| } |
| |
| // Generative constructors are not nullable. |
| if (!e.isFactory) return false; |
| |
| // Factory constructors are nullable. However it is a bad pattern and |
| // our own SDK will never do this. |
| // TODO(jmesserly): we could enforce this for user-defined constructors. |
| if (e.library.source.isInSystemLibrary) return false; |
| |
| return true; |
| } |
| if (expr is MethodInvocation && |
| (expr.operator?.lexeme != '?.' || |
| !_isNullable(expr.target, localIsNullable)) && |
| _isNonNullMethodInvocation(expr)) { |
| return false; |
| } |
| |
| Expression operand, rightOperand; |
| String op; |
| if (expr is AssignmentExpression) { |
| op = expr.operator.lexeme; |
| assert(op.endsWith('=')); |
| if (op == '=') { |
| return _isNullable(expr.rightHandSide, localIsNullable); |
| } |
| // op assignment like +=, remove trailing '=' |
| op = op.substring(0, op.length - 1); |
| operand = expr.leftHandSide; |
| rightOperand = expr.rightHandSide; |
| } else if (expr is BinaryExpression) { |
| operand = expr.leftOperand; |
| rightOperand = expr.rightOperand; |
| op = expr.operator.lexeme; |
| } else if (expr is PrefixExpression) { |
| operand = expr.operand; |
| op = expr.operator.lexeme; |
| } else if (expr is PostfixExpression) { |
| operand = expr.operand; |
| op = expr.operator.lexeme; |
| } |
| switch (op) { |
| case '==': |
| case '!=': |
| case '&&': |
| case '||': |
| case '!': |
| return false; |
| case '??': |
| return _isNullable(operand, localIsNullable) && |
| _isNullable(rightOperand, localIsNullable); |
| } |
| if (operand != null && jsTypeRep.isPrimitive(getStaticType(operand))) { |
| return false; |
| } |
| |
| // TODO(ochafik,jmesserly): handle other cases such as: refs to top-level |
| // finals that have been assigned non-nullable values. |
| |
| // Failed to recognize a non-nullable case: assume it can be null. |
| return true; |
| } |
| } |
| |
| /// A visitor that determines which local variables are non-nullable. |
| /// |
| /// This will consider all assignments to local variables using |
| /// flow-insensitive inference. That information is used to determine which |
| /// variables are nullable in the given scope. |
| // TODO(ochafik): Introduce flow analysis (a variable may be nullable in |
| // some places and not in others). |
| class _NullableLocalInference extends RecursiveAstVisitor { |
| final NullableTypeInference _nullInference; |
| |
| /// Known local variables. |
| final _locals = HashSet<LocalVariableElement>.identity(); |
| |
| /// Variables that are known to be nullable. |
| final _nullableLocals = HashSet<LocalVariableElement>.identity(); |
| |
| /// Given a variable, tracks all other variables that it is assigned to. |
| final _assignments = |
| HashMap<LocalVariableElement, Set<LocalVariableElement>>.identity(); |
| |
| _NullableLocalInference(this._nullInference); |
| |
| /// After visiting nodes, this can be called to compute the set of not-null |
| /// locals. |
| /// |
| /// This method must only be called once. After it is called, the visitor |
| /// should be discarded. |
| HashSet<LocalVariableElement> computeNotNullLocals() { |
| // Given a set of variables that are nullable, remove them from our list of |
| // local variables. The end result of this process is a list of variables |
| // known to be not null. |
| visitNullableLocal(LocalVariableElement e) { |
| _locals.remove(e); |
| |
| // Visit all other locals that this one is assigned to, and record that |
| // they are nullable too. |
| _assignments.remove(e)?.forEach(visitNullableLocal); |
| } |
| |
| _nullableLocals.forEach(visitNullableLocal); |
| |
| // Any remaining locals are non-null. |
| return _locals; |
| } |
| |
| @override |
| visitVariableDeclaration(VariableDeclaration node) { |
| var element = node.declaredElement; |
| var initializer = node.initializer; |
| if (element is LocalVariableElement) { |
| _locals.add(element); |
| if (initializer != null) { |
| _visitAssignment(node.name, initializer); |
| } else if (!_assertedNotNull(element)) { |
| _nullableLocals.add(element); |
| } |
| } |
| super.visitVariableDeclaration(node); |
| } |
| |
| @override |
| visitForEachStatement(ForEachStatement node) { |
| if (node.identifier == null) { |
| var declaration = node.loopVariable; |
| var element = declaration.declaredElement; |
| _locals.add(element); |
| if (!_assertedNotNull(element)) { |
| _nullableLocals.add(element); |
| } |
| } else { |
| var element = node.identifier.staticElement; |
| if (element is LocalVariableElement && !_assertedNotNull(element)) { |
| _nullableLocals.add(element); |
| } |
| } |
| super.visitForEachStatement(node); |
| } |
| |
| @override |
| visitCatchClause(CatchClause node) { |
| var e = node.exceptionParameter?.staticElement; |
| if (e is LocalVariableElement) { |
| _locals.add(e); |
| // TODO(jmesserly): we allow throwing of `null`, for better or worse. |
| _nullableLocals.add(e); |
| } |
| |
| e = node.stackTraceParameter?.staticElement; |
| if (e is LocalVariableElement) _locals.add(e); |
| |
| super.visitCatchClause(node); |
| } |
| |
| @override |
| visitAssignmentExpression(AssignmentExpression node) { |
| if (node.operator.lexeme == '=') { |
| _visitAssignment(node.leftHandSide, node.rightHandSide); |
| } else { |
| _visitAssignment(node.leftHandSide, node); |
| } |
| super.visitAssignmentExpression(node); |
| } |
| |
| @override |
| visitPostfixExpression(PostfixExpression node) { |
| var op = node.operator.type; |
| if (op.isIncrementOperator) { |
| _visitAssignment(node.operand, node); |
| } |
| super.visitPostfixExpression(node); |
| } |
| |
| @override |
| visitPrefixExpression(PrefixExpression node) { |
| var op = node.operator.type; |
| if (op.isIncrementOperator) { |
| _visitAssignment(node.operand, node); |
| } |
| super.visitPrefixExpression(node); |
| } |
| |
| void _visitAssignment(Expression left, Expression right) { |
| if (left is SimpleIdentifier) { |
| var element = left.staticElement; |
| if (element is LocalVariableElement && !_assertedNotNull(element)) { |
| bool visitLocal(LocalVariableElement otherLocal) { |
| // Record the assignment. |
| _assignments |
| .putIfAbsent(otherLocal, () => HashSet.identity()) |
| .add(element); |
| // Optimistically assume this local is not null. |
| // We will validate this assumption later. |
| return false; |
| } |
| |
| if (_nullInference._isNullable(right, visitLocal)) { |
| _nullableLocals.add(element); |
| } |
| } |
| } |
| } |
| } |
| |
| bool _assertedNotNull(Element e) => |
| findAnnotation(e, isNotNullAnnotation) != null || |
| findAnnotation(e, isNullCheckAnnotation) != null; |