/* * Copyright (c) 2013, the Dart project authors. * * Licensed under the Eclipse Public License v1.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.eclipse.org/legal/epl-v10.html * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package com.google.dart.engine.internal.hint; import com.google.dart.engine.ast.ArgumentList; import com.google.dart.engine.ast.AsExpression; import com.google.dart.engine.ast.AssignmentExpression; import com.google.dart.engine.ast.AstNode; import com.google.dart.engine.ast.BinaryExpression; import com.google.dart.engine.ast.BlockFunctionBody; import com.google.dart.engine.ast.ClassDeclaration; import com.google.dart.engine.ast.ConstructorName; import com.google.dart.engine.ast.ExportDirective; import com.google.dart.engine.ast.Expression; import com.google.dart.engine.ast.FunctionBody; import com.google.dart.engine.ast.FunctionDeclaration; import com.google.dart.engine.ast.HideCombinator; import com.google.dart.engine.ast.ImportDirective; import com.google.dart.engine.ast.IndexExpression; import com.google.dart.engine.ast.InstanceCreationExpression; import com.google.dart.engine.ast.IsExpression; import com.google.dart.engine.ast.MethodDeclaration; import com.google.dart.engine.ast.MethodInvocation; import com.google.dart.engine.ast.NullLiteral; import com.google.dart.engine.ast.ParenthesizedExpression; import com.google.dart.engine.ast.PostfixExpression; import com.google.dart.engine.ast.PrefixExpression; import com.google.dart.engine.ast.RedirectingConstructorInvocation; import com.google.dart.engine.ast.SimpleIdentifier; import com.google.dart.engine.ast.SuperConstructorInvocation; import com.google.dart.engine.ast.TypeName; import com.google.dart.engine.ast.VariableDeclaration; import com.google.dart.engine.ast.visitor.RecursiveAstVisitor; import com.google.dart.engine.element.ClassElement; import com.google.dart.engine.element.ConstructorElement; import com.google.dart.engine.element.Element; import com.google.dart.engine.element.ImportElement; import com.google.dart.engine.element.LibraryElement; import com.google.dart.engine.element.MethodElement; import com.google.dart.engine.element.ParameterElement; import com.google.dart.engine.element.PropertyAccessorElement; import com.google.dart.engine.element.VariableElement; import com.google.dart.engine.error.CompileTimeErrorCode; import com.google.dart.engine.error.ErrorCode; import com.google.dart.engine.error.HintCode; import com.google.dart.engine.internal.error.ErrorReporter; import com.google.dart.engine.internal.type.VoidTypeImpl; import com.google.dart.engine.internal.verifier.ErrorVerifier; import com.google.dart.engine.scanner.Keyword; import com.google.dart.engine.scanner.TokenType; import com.google.dart.engine.type.Type; import com.google.dart.engine.type.TypeParameterType; /** * Instances of the class {@code BestPracticesVerifier} traverse an AST structure looking for * violations of Dart best practices. * * @coverage dart.engine.resolver */ public class BestPracticesVerifier extends RecursiveAstVisitor<Void> { private static final String HASHCODE_GETTER_NAME = "hashCode"; private static final String NULL_TYPE_NAME = "Null"; private static final String TO_INT_METHOD_NAME = "toInt"; /** * Given a parenthesized expression, this returns the parent (or recursively grand-parent) of the * expression that is a parenthesized expression, but whose parent is not a parenthesized * expression. * <p> * For example given the code {@code (((e)))}: {@code (e) -> (((e)))}. * * @param parenthesizedExpression some expression whose parent is a parenthesized expression * @return the first parent or grand-parent that is a parenthesized expression, that does not have * a parenthesized expression parent */ private static ParenthesizedExpression wrapParenthesizedExpression( ParenthesizedExpression parenthesizedExpression) { if (parenthesizedExpression.getParent() instanceof ParenthesizedExpression) { return wrapParenthesizedExpression((ParenthesizedExpression) parenthesizedExpression.getParent()); } return parenthesizedExpression; } /** * The class containing the AST nodes being visited, or {@code null} if we are not in the scope of * a class. */ private ClassElement enclosingClass; /** * The error reporter by which errors will be reported. */ private ErrorReporter errorReporter; /** * Create a new instance of the {@link BestPracticesVerifier}. * * @param errorReporter the error reporter */ public BestPracticesVerifier(ErrorReporter errorReporter) { this.errorReporter = errorReporter; } @Override public Void visitArgumentList(ArgumentList node) { checkForArgumentTypesNotAssignableInList(node); return super.visitArgumentList(node); } @Override public Void visitAsExpression(AsExpression node) { checkForUnnecessaryCast(node); return super.visitAsExpression(node); } @Override public Void visitAssignmentExpression(AssignmentExpression node) { TokenType operatorType = node.getOperator().getType(); if (operatorType == TokenType.EQ) { checkForUseOfVoidResult(node.getRightHandSide()); checkForInvalidAssignment(node.getLeftHandSide(), node.getRightHandSide()); } else { checkForDeprecatedMemberUse(node.getBestElement(), node); } return super.visitAssignmentExpression(node); } @Override public Void visitBinaryExpression(BinaryExpression node) { checkForDivisionOptimizationHint(node); checkForDeprecatedMemberUse(node.getBestElement(), node); return super.visitBinaryExpression(node); } @Override public Void visitClassDeclaration(ClassDeclaration node) { ClassElement outerClass = enclosingClass; try { enclosingClass = node.getElement(); // Commented out until we decide that we want this hint in the analyzer // checkForOverrideEqualsButNotHashCode(node); return super.visitClassDeclaration(node); } finally { enclosingClass = outerClass; } } @Override public Void visitExportDirective(ExportDirective node) { checkForDeprecatedMemberUse(node.getUriElement(), node); return super.visitExportDirective(node); } @Override public Void visitFunctionDeclaration(FunctionDeclaration node) { checkForMissingReturn(node.getReturnType(), node.getFunctionExpression().getBody()); return super.visitFunctionDeclaration(node); } @Override public Void visitImportDirective(ImportDirective node) { checkForDeprecatedMemberUse(node.getUriElement(), node); ImportElement importElement = node.getElement(); if (importElement != null) { if (importElement.isDeferred()) { checkForLoadLibraryFunction(node, importElement); } } return super.visitImportDirective(node); } @Override public Void visitIndexExpression(IndexExpression node) { checkForDeprecatedMemberUse(node.getBestElement(), node); return super.visitIndexExpression(node); } @Override public Void visitInstanceCreationExpression(InstanceCreationExpression node) { checkForDeprecatedMemberUse(node.getStaticElement(), node); return super.visitInstanceCreationExpression(node); } @Override public Void visitIsExpression(IsExpression node) { checkAllTypeChecks(node); return super.visitIsExpression(node); } @Override public Void visitMethodDeclaration(MethodDeclaration node) { // This was determined to not be a good hint, see: dartbug.com/16029 //checkForOverridingPrivateMember(node); checkForMissingReturn(node.getReturnType(), node.getBody()); return super.visitMethodDeclaration(node); } @Override public Void visitPostfixExpression(PostfixExpression node) { checkForDeprecatedMemberUse(node.getBestElement(), node); return super.visitPostfixExpression(node); } @Override public Void visitPrefixExpression(PrefixExpression node) { checkForDeprecatedMemberUse(node.getBestElement(), node); return super.visitPrefixExpression(node); } @Override public Void visitRedirectingConstructorInvocation(RedirectingConstructorInvocation node) { checkForDeprecatedMemberUse(node.getStaticElement(), node); return super.visitRedirectingConstructorInvocation(node); } @Override public Void visitSimpleIdentifier(SimpleIdentifier node) { checkForDeprecatedMemberUseAtIdentifier(node); return super.visitSimpleIdentifier(node); } @Override public Void visitSuperConstructorInvocation(SuperConstructorInvocation node) { checkForDeprecatedMemberUse(node.getStaticElement(), node); return super.visitSuperConstructorInvocation(node); } @Override public Void visitVariableDeclaration(VariableDeclaration node) { checkForUseOfVoidResult(node.getInitializer()); checkForInvalidAssignment(node.getName(), node.getInitializer()); return super.visitVariableDeclaration(node); } /** * Check for the passed is expression for the unnecessary type check hint codes as well as null * checks expressed using an is expression. * * @param node the is expression to check * @return {@code true} if and only if a hint code is generated on the passed node * @see HintCode#TYPE_CHECK_IS_NOT_NULL * @see HintCode#TYPE_CHECK_IS_NULL * @see HintCode#UNNECESSARY_TYPE_CHECK_TRUE * @see HintCode#UNNECESSARY_TYPE_CHECK_FALSE */ private boolean checkAllTypeChecks(IsExpression node) { Expression expression = node.getExpression(); TypeName typeName = node.getType(); Type lhsType = expression.getStaticType(); Type rhsType = typeName.getType(); if (lhsType == null || rhsType == null) { return false; } String rhsNameStr = typeName.getName().getName(); // if x is dynamic if ((rhsType.isDynamic() && rhsNameStr.equals(Keyword.DYNAMIC.getSyntax()))) { if (node.getNotOperator() == null) { // the is case errorReporter.reportErrorForNode(HintCode.UNNECESSARY_TYPE_CHECK_TRUE, node); } else { // the is not case errorReporter.reportErrorForNode(HintCode.UNNECESSARY_TYPE_CHECK_FALSE, node); } return true; } Element rhsElement = rhsType.getElement(); LibraryElement libraryElement = rhsElement != null ? rhsElement.getLibrary() : null; if (libraryElement != null && libraryElement.isDartCore()) { // if x is Object or null is Null if (rhsType.isObject() || (expression instanceof NullLiteral && rhsNameStr.equals(NULL_TYPE_NAME))) { if (node.getNotOperator() == null) { // the is case errorReporter.reportErrorForNode(HintCode.UNNECESSARY_TYPE_CHECK_TRUE, node); } else { // the is not case errorReporter.reportErrorForNode(HintCode.UNNECESSARY_TYPE_CHECK_FALSE, node); } return true; } else if (rhsNameStr.equals(NULL_TYPE_NAME)) { if (node.getNotOperator() == null) { // the is case errorReporter.reportErrorForNode(HintCode.TYPE_CHECK_IS_NULL, node); } else { // the is not case errorReporter.reportErrorForNode(HintCode.TYPE_CHECK_IS_NOT_NULL, node); } return true; } } return false; } /** * This verifies that the passed expression can be assigned to its corresponding parameters. * <p> * This method corresponds to ErrorVerifier.checkForArgumentTypeNotAssignable. * <p> * TODO (jwren) In the ErrorVerifier there are other warnings that we could have a corresponding * hint for: see other callers of ErrorVerifier.checkForArgumentTypeNotAssignable(..). * * @param expression the expression to evaluate * @param expectedStaticType the expected static type of the parameter * @param actualStaticType the actual static type of the argument * @param expectedPropagatedType the expected propagated type of the parameter, may be * {@code null} * @param actualPropagatedType the expected propagated type of the parameter, may be {@code null} * @return {@code true} if and only if an hint code is generated on the passed node * @see HintCode#ARGUMENT_TYPE_NOT_ASSIGNABLE */ private boolean checkForArgumentTypeNotAssignable(Expression expression, Type expectedStaticType, Type actualStaticType, Type expectedPropagatedType, Type actualPropagatedType, ErrorCode hintCode) { // // Warning case: test static type information // if (actualStaticType != null && expectedStaticType != null) { if (!actualStaticType.isAssignableTo(expectedStaticType)) { // A warning was created in the ErrorVerifier, return false, don't create a hint when a // warning has already been created. return false; } } // // Hint case: test propagated type information // // Compute the best types to use. Type expectedBestType = expectedPropagatedType != null ? expectedPropagatedType : expectedStaticType; Type actualBestType = actualPropagatedType != null ? actualPropagatedType : actualStaticType; if (actualBestType != null && expectedBestType != null) { if (!actualBestType.isAssignableTo(expectedBestType)) { errorReporter.reportTypeErrorForNode(hintCode, expression, actualBestType, expectedBestType); return true; } } return false; } /** * This verifies that the passed argument can be assigned to its corresponding parameter. * <p> * This method corresponds to ErrorCode.checkForArgumentTypeNotAssignableForArgument. * * @param argument the argument to evaluate * @return {@code true} if and only if an hint code is generated on the passed node * @see HintCode#ARGUMENT_TYPE_NOT_ASSIGNABLE */ private boolean checkForArgumentTypeNotAssignableForArgument(Expression argument) { if (argument == null) { return false; } ParameterElement staticParameterElement = argument.getStaticParameterElement(); Type staticParameterType = staticParameterElement == null ? null : staticParameterElement.getType(); ParameterElement propagatedParameterElement = argument.getPropagatedParameterElement(); Type propagatedParameterType = propagatedParameterElement == null ? null : propagatedParameterElement.getType(); return checkForArgumentTypeNotAssignableWithExpectedTypes( argument, staticParameterType, propagatedParameterType, HintCode.ARGUMENT_TYPE_NOT_ASSIGNABLE); } /** * This verifies that the passed expression can be assigned to its corresponding parameters. * <p> * This method corresponds to ErrorCode.checkForArgumentTypeNotAssignableWithExpectedTypes. * * @param expression the expression to evaluate * @param expectedStaticType the expected static type * @param expectedPropagatedType the expected propagated type, may be {@code null} * @return {@code true} if and only if an hint code is generated on the passed node * @see HintCode#ARGUMENT_TYPE_NOT_ASSIGNABLE */ private boolean checkForArgumentTypeNotAssignableWithExpectedTypes(Expression expression, Type expectedStaticType, Type expectedPropagatedType, ErrorCode errorCode) { return checkForArgumentTypeNotAssignable( expression, expectedStaticType, expression.getStaticType(), expectedPropagatedType, expression.getPropagatedType(), errorCode); } /** * This verifies that the passed arguments can be assigned to their corresponding parameters. * <p> * This method corresponds to ErrorCode.checkForArgumentTypesNotAssignableInList. * * @param node the arguments to evaluate * @return {@code true} if and only if an hint code is generated on the passed node * @see HintCode#ARGUMENT_TYPE_NOT_ASSIGNABLE */ private boolean checkForArgumentTypesNotAssignableInList(ArgumentList argumentList) { if (argumentList == null) { return false; } boolean problemReported = false; for (Expression argument : argumentList.getArguments()) { problemReported |= checkForArgumentTypeNotAssignableForArgument(argument); } return problemReported; } /** * Given some {@link Element}, look at the associated metadata and report the use of the member if * it is declared as deprecated. * * @param element some element to check for deprecated use of * @param node the node use for the location of the error * @return {@code true} if and only if a hint code is generated on the passed node * @see HintCode#DEPRECATED_MEMBER_USE */ private boolean checkForDeprecatedMemberUse(Element element, AstNode node) { if (element != null && element.isDeprecated()) { String displayName = element.getDisplayName(); if (element instanceof ConstructorElement) { // TODO(jwren) We should modify ConstructorElement.getDisplayName(), or have the logic // centralized elsewhere, instead of doing this logic here. ConstructorElement constructorElement = (ConstructorElement) element; displayName = constructorElement.getEnclosingElement().getDisplayName(); if (!constructorElement.getDisplayName().isEmpty()) { displayName = displayName + '.' + constructorElement.getDisplayName(); } } errorReporter.reportErrorForNode(HintCode.DEPRECATED_MEMBER_USE, node, displayName); return true; } return false; } /** * For {@link SimpleIdentifier}s, only call {@link #checkForDeprecatedMemberUse(Element, AstNode)} * if the node is not in a declaration context. * <p> * Also, if the identifier is a constructor name in a constructor invocation, then calls to the * deprecated constructor will be caught by * {@link #visitInstanceCreationExpression(InstanceCreationExpression)} and * {@link #visitSuperConstructorInvocation(SuperConstructorInvocation)}, and can be ignored by * this visit method. * * @param identifier some simple identifier to check for deprecated use of * @return {@code true} if and only if a hint code is generated on the passed node * @see HintCode#DEPRECATED_MEMBER_USE */ private boolean checkForDeprecatedMemberUseAtIdentifier(SimpleIdentifier identifier) { if (identifier.inDeclarationContext()) { return false; } AstNode parent = identifier.getParent(); if ((parent instanceof ConstructorName && identifier == ((ConstructorName) parent).getName()) || (parent instanceof SuperConstructorInvocation && identifier == ((SuperConstructorInvocation) parent).getConstructorName()) || parent instanceof HideCombinator) { return false; } return checkForDeprecatedMemberUse(identifier.getBestElement(), identifier); } /** * Check for the passed binary expression for the {@link HintCode#DIVISION_OPTIMIZATION}. * * @param node the binary expression to check * @return {@code true} if and only if a hint code is generated on the passed node * @see HintCode#DIVISION_OPTIMIZATION */ private boolean checkForDivisionOptimizationHint(BinaryExpression node) { // Return if the operator is not '/' if (!node.getOperator().getType().equals(TokenType.SLASH)) { return false; } // Return if the '/' operator is not defined in core, or if we don't know its static or propagated type MethodElement methodElement = node.getBestElement(); if (methodElement == null) { return false; } LibraryElement libraryElement = methodElement.getLibrary(); if (libraryElement != null && !libraryElement.isDartCore()) { return false; } // Report error if the (x/y) has toInt() invoked on it if (node.getParent() instanceof ParenthesizedExpression) { ParenthesizedExpression parenthesizedExpression = wrapParenthesizedExpression((ParenthesizedExpression) node.getParent()); if (parenthesizedExpression.getParent() instanceof MethodInvocation) { MethodInvocation methodInvocation = (MethodInvocation) parenthesizedExpression.getParent(); if (TO_INT_METHOD_NAME.equals(methodInvocation.getMethodName().getName()) && methodInvocation.getArgumentList().getArguments().isEmpty()) { errorReporter.reportErrorForNode(HintCode.DIVISION_OPTIMIZATION, methodInvocation); return true; } } } return false; } /** * This verifies that the passed left hand side and right hand side represent a valid assignment. * <p> * This method corresponds to ErrorVerifier.checkForInvalidAssignment. * * @param lhs the left hand side expression * @param rhs the right hand side expression * @return {@code true} if and only if an error code is generated on the passed node * @see HintCode#INVALID_ASSIGNMENT */ private boolean checkForInvalidAssignment(Expression lhs, Expression rhs) { if (lhs == null || rhs == null) { return false; } VariableElement leftVariableElement = ErrorVerifier.getVariableElement(lhs); Type leftType = (leftVariableElement == null) ? ErrorVerifier.getStaticType(lhs) : leftVariableElement.getType(); Type staticRightType = ErrorVerifier.getStaticType(rhs); if (!staticRightType.isAssignableTo(leftType)) { // The warning was generated on this rhs return false; } // Test for, and then generate the hint Type bestRightType = rhs.getBestType(); if (leftType != null && bestRightType != null) { if (!bestRightType.isAssignableTo(leftType)) { errorReporter.reportTypeErrorForNode( HintCode.INVALID_ASSIGNMENT, rhs, bestRightType, leftType); return true; } } return false; } /** * Check that the imported library does not define a loadLibrary function. The import has already * been determined to be deferred when this is called. * * @param node the import directive to evaluate * @param importElement the {@link ImportElement} retrieved from the node * @return {@code true} if and only if an error code is generated on the passed node * @see CompileTimeErrorCode#IMPORT_DEFERRED_LIBRARY_WITH_LOAD_FUNCTION */ private boolean checkForLoadLibraryFunction(ImportDirective node, ImportElement importElement) { LibraryElement importedLibrary = importElement.getImportedLibrary(); if (importedLibrary == null) { return false; } if (importedLibrary.hasLoadLibraryFunction()) { errorReporter.reportErrorForNode( HintCode.IMPORT_DEFERRED_LIBRARY_WITH_LOAD_FUNCTION, node, importedLibrary.getName()); return true; } return false; } /** * Generate a hint for functions or methods that have a return type, but do not have a return * statement on all branches. At the end of blocks with no return, Dart implicitly returns * {@code null}, avoiding these implicit returns is considered a best practice. Note: for async * functions/methods, this hint only applies when the function has a return type that * Future<Null> is not assignable to. * * @param node the binary expression to check * @param body the function body * @return {@code true} if and only if a hint code is generated on the passed node * @see HintCode#MISSING_RETURN */ private boolean checkForMissingReturn(TypeName returnType, FunctionBody body) { // Check that the method or function has a return type, and a function body if (returnType == null || body == null) { return false; } // Check that the body is a BlockFunctionBody if (!(body instanceof BlockFunctionBody)) { return false; } // Generators are never required to have a return statement. if (body.isGenerator()) { return false; } // Check that the type is resolvable, and is not "void" Type returnTypeType = returnType.getType(); if (returnTypeType == null || returnTypeType.isVoid()) { return false; } // For async, give no hint if Future<Null> is assignable to the return type. // TODO(paulberry): we can't check if Future<Null> is assignable to the return type because the // Future type isn't available in the type provider. So to avoid bogus hints, just suppress // the hint for asynchronous methods. if (body.isAsynchronous()) { return false; } // Check the block for a return statement, if not, create the hint BlockFunctionBody blockFunctionBody = (BlockFunctionBody) body; if (!blockFunctionBody.accept(new ExitDetector())) { errorReporter.reportErrorForNode( HintCode.MISSING_RETURN, returnType, returnTypeType.getDisplayName()); return true; } return false; } /** * Check for the passed class declaration for the * {@link HintCode#OVERRIDE_EQUALS_BUT_NOT_HASH_CODE} hint code. * * @param node the class declaration to check * @return {@code true} if and only if a hint code is generated on the passed node * @see HintCode#OVERRIDE_EQUALS_BUT_NOT_HASH_CODE */ @SuppressWarnings("unused") private boolean checkForOverrideEqualsButNotHashCode(ClassDeclaration node) { ClassElement classElement = node.getElement(); if (classElement == null) { return false; } MethodElement equalsOperatorMethodElement = classElement.getMethod(TokenType.EQ_EQ.getLexeme()); if (equalsOperatorMethodElement != null) { PropertyAccessorElement hashCodeElement = classElement.getGetter(HASHCODE_GETTER_NAME); if (hashCodeElement == null) { errorReporter.reportErrorForNode( HintCode.OVERRIDE_EQUALS_BUT_NOT_HASH_CODE, node.getName(), classElement.getDisplayName()); return true; } } return false; } /** * Check for the passed as expression for the {@link HintCode#UNNECESSARY_CAST} hint code. * * @param node the as expression to check * @return {@code true} if and only if a hint code is generated on the passed node * @see HintCode#UNNECESSARY_CAST */ private boolean checkForUnnecessaryCast(AsExpression node) { Expression expression = node.getExpression(); TypeName typeName = node.getType(); Type lhsType = expression.getStaticType(); Type rhsType = typeName.getType(); // TODO(jwren) After dartbug.com/13732, revisit this, we should be able to remove the // !(x instanceof TypeParameterType) checks. if (lhsType != null && rhsType != null && !lhsType.isDynamic() && !rhsType.isDynamic() && !(lhsType instanceof TypeParameterType) && !(rhsType instanceof TypeParameterType) && lhsType.isMoreSpecificThan(rhsType)) { errorReporter.reportErrorForNode(HintCode.UNNECESSARY_CAST, node); return true; } return false; } /** * Check for situations where the result of a method or function is used, when it returns 'void'. * <p> * TODO(jwren) Many other situations of use could be covered. We currently cover the cases var x = * m() and x = m(), but we could also cover cases such as m().x, m()[k], a + m(), f(m()), return * m(). * * @param node expression on the RHS of some assignment * @return {@code true} if and only if a hint code is generated on the passed node * @see HintCode#USE_OF_VOID_RESULT */ private boolean checkForUseOfVoidResult(Expression expression) { if (expression == null || !(expression instanceof MethodInvocation)) { return false; } MethodInvocation methodInvocation = (MethodInvocation) expression; if (methodInvocation.getStaticType() == VoidTypeImpl.getInstance()) { SimpleIdentifier methodName = methodInvocation.getMethodName(); errorReporter.reportErrorForNode( HintCode.USE_OF_VOID_RESULT, methodName, methodName.getName()); return true; } return false; } }