//////////////////////////////////////////////////////////////////////////////// // checkstyle: Checks Java source code for adherence to a set of rules. // Copyright (C) 2001-2017 the original author or authors. // // This library is free software; you can redistribute it and/or // modify it under the terms of the GNU Lesser General Public // License as published by the Free Software Foundation; either // version 2.1 of the License, or (at your option) any later version. // // This library is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public // License along with this library; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA //////////////////////////////////////////////////////////////////////////////// package com.github.sevntu.checkstyle.checks.coding; import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedList; import java.util.List; import java.util.regex.Pattern; import com.github.sevntu.checkstyle.Utils; import com.puppycrawl.tools.checkstyle.api.AbstractCheck; import com.puppycrawl.tools.checkstyle.api.DetailAST; import com.puppycrawl.tools.checkstyle.api.FullIdent; import com.puppycrawl.tools.checkstyle.api.TokenTypes; /** * <p> * Either log the exception, or throw it, but never do both. Logging and * throwing results in multiple log messages for a single problem in the code, * and makes problems for the support engineer who is trying to dig through the * logs. This is one of the most annoying error-handling antipatterns. All of * these examples are equally wrong. * </p> * <p> * <b>Examples:</b> * * <pre> * catch (NoSuchMethodException e) { * LOG.error("Message", e); * throw e; * } * </pre> * * <b>or</b> * * <pre> * catch (NoSuchMethodException e) { * LOG.error("Message", e); * throw new MyServiceException("AnotherMessage", e); * } * </pre> * * <b>or</b> * * <pre> * catch (NoSuchMethodException e) { * e.printStackTrace(); * throw new MyServiceException("Message", e); * } * </pre> * * <p> * <b>What check can detect:</b> <br> * <b>Loggers</b> * <ul> * <li>logger is declared as class field</li> * <li>logger is declared as method's local variable</li> * <li>logger is declared as local variable in <code>catch</code> block</li> * <li>logger is passed through method's parameters</li> * </ul> * <b>Exceptions</b> * <ul> * <li>logger logs <code>catch</code> parameter exception or it's message</li> * <li>throw <code>catch</code> parameter exception</li> * <li>throw another exception which is based on <code>catch</code> parameter * exception</li> * <li>printStackTrace was called on <code>catch</code> parameter exception</li> * </ul> * <p> * <b>What check can not detect:</b> <br> * <ul> * <li>loggers that is used like method's return value. Example: * * <pre> * getLogger().error("message", e) * </pre> * * </li> * <li>loggers that is used like static fields from another classes: * * <pre> * MyAnotherClass.LOGGER.error("message", e); * </pre> * </li> * </ul> * <p> * Default parameters are: * <ul> * <li><b>loggerFullyQualifiedClassName</b> - fully qualified class name of * logger type. Default value is <i>"org.slf4j.Logger"</i>.</li> * <li><b>loggingMethodNames</b> - comma separated names of logging methods. * Default value is <i>"error, warn, info, debug"</i>.</li> * </ul> * <p> * Note that check works with only one logger type. If you have multiple * different loggers, then create another instance of this check. * </p> * @author <a href="mailto:barataliba@gmail.com">Baratali Izmailov</a> */ public class EitherLogOrThrowCheck extends AbstractCheck { /** * Key for error message. */ public static final String MSG_KEY = "either.log.or.throw"; /** * Regexp of printStackTrace method. */ private static final Pattern PRINT_STACK_TRACE_METHOD_PATTERN = Pattern .compile(".+\\.printStackTrace"); /** * Logger fully qualified class name. */ private String loggerFullyQualifiedClassName = "org.slf4j.Logger"; /** * Logger class name. */ private String loggerSimpleClassName = "Logger"; /** * Logger method names. */ private List<String> loggingMethodNames = Arrays.asList("error", "warn", "info", "debug"); /** * Variables names of logger variables. */ private List<String> loggerFieldNames = new LinkedList<>(); /** * Logger class is in imports. */ private boolean hasLoggerClassInImports; /** * Considered class definition. */ private DetailAST currentClassDefAst; /** * Considered method definition. */ private DetailAST currentMethodDefAst; /** * Current local variable names of logger type. It can be method's parameter * or method's local variable. */ private List<String> currentLocalLoggerVariableNames = new ArrayList<>(); /** * Set logger full class name and logger simple class name. * @param loggerFullyQualifiedClassName * Logger full class name. Example: org.slf4j.Logger. */ public void setLoggerFullyQualifiedClassName( String loggerFullyQualifiedClassName) { this.loggerFullyQualifiedClassName = loggerFullyQualifiedClassName; loggerSimpleClassName = loggerFullyQualifiedClassName; final int lastDotIndex = this.loggerFullyQualifiedClassName.lastIndexOf('.'); if (lastDotIndex != -1) { loggerSimpleClassName = this.loggerFullyQualifiedClassName .substring(lastDotIndex + 1); } } /** * Set logging method names. * @param loggingMethodNames Logger method names. */ public void setLoggingMethodNames(String[] loggingMethodNames) { this.loggingMethodNames = Arrays.asList(loggingMethodNames); } @Override public int[] getDefaultTokens() { return new int[] { TokenTypes.IMPORT, TokenTypes.CLASS_DEF, TokenTypes.LITERAL_CATCH, TokenTypes.VARIABLE_DEF, TokenTypes.METHOD_DEF, }; } @Override public void visitToken(final DetailAST ast) { switch (ast.getType()) { case TokenTypes.IMPORT: if (!hasLoggerClassInImports && isLoggerImport(ast)) { hasLoggerClassInImports = true; } break; case TokenTypes.CLASS_DEF: if (!isInnerClass(ast)) { currentClassDefAst = ast; collectLoggerFieldNames(ast); } break; case TokenTypes.METHOD_DEF: if (isMethodOfCurrentClass(ast)) { currentMethodDefAst = ast; currentLocalLoggerVariableNames.clear(); final DetailAST parametersAst = currentMethodDefAst .findFirstToken(TokenTypes.PARAMETERS); collectLoggersFromParameters(parametersAst); } break; case TokenTypes.VARIABLE_DEF: final DetailAST methodDefAst = ast.getParent().getParent(); if (methodDefAst == currentMethodDefAst && methodDefAst.getType() == TokenTypes.METHOD_DEF && isLoggerVariableDefinition(ast)) { currentLocalLoggerVariableNames.add(getIdentifier(ast)); } break; case TokenTypes.LITERAL_CATCH: processCatchNode(ast); break; default: Utils.reportInvalidToken(ast.getType()); break; } } /** * @param importAst * DetailAST of import statement. * @return true if import equals logger full class name. */ private boolean isLoggerImport(final DetailAST importAst) { final String importIdentifier = FullIdent.createFullIdent(importAst.getFirstChild()).getText(); return loggerFullyQualifiedClassName.equals(importIdentifier); } /** * Verify that class is inner. * @param classDefAst * DetailAST of class definition. * @return true if class is inner, false otherwise. */ private boolean isInnerClass(final DetailAST classDefAst) { boolean result = false; DetailAST parentAst = classDefAst.getParent(); while (parentAst != null) { if (parentAst == currentClassDefAst) { result = true; break; } parentAst = parentAst.getParent(); } return result; } /** * Save names of parameters which have logger type. * @param parametersAst * DetailAST of parameters. */ private void collectLoggersFromParameters(final DetailAST parametersAst) { DetailAST currentParameterAst = parametersAst .findFirstToken(TokenTypes.PARAMETER_DEF); while (currentParameterAst != null) { final DetailAST parameterTypeAst = currentParameterAst .findFirstToken(TokenTypes.TYPE); final String className = getIdentifier(parameterTypeAst); if (className != null && isLoggerClassName(className)) { currentLocalLoggerVariableNames .add(getIdentifier(currentParameterAst)); } currentParameterAst = currentParameterAst.getNextSibling(); } } /** * Verify that method's parent is class, stored in mCurrentClassDefAst. * @param methodDefAst DetailAST of METHOD_DEF. * @return true if method's parent is class, stored in mCurrentClassDefAst. */ private boolean isMethodOfCurrentClass(final DetailAST methodDefAst) { final DetailAST classDefAst = methodDefAst.getParent().getParent(); return classDefAst == currentClassDefAst; } /** * Find all logger fields in aClassDefAst and save them. * @param classDefAst * DetailAST of class definition. */ private void collectLoggerFieldNames(final DetailAST classDefAst) { final DetailAST objBlockAst = classDefAst.findFirstToken(TokenTypes.OBJBLOCK); DetailAST variableDefAst = objBlockAst.findFirstToken(TokenTypes.VARIABLE_DEF); while (variableDefAst != null) { if (variableDefAst.getType() == TokenTypes.VARIABLE_DEF && isLoggerVariableDefinition(variableDefAst)) { loggerFieldNames.add(getIdentifier(variableDefAst)); } variableDefAst = variableDefAst.getNextSibling(); } } /** * Look at the each statement of catch block to find logging and throwing. * If same exception is being logged and throwed, then prints warning * message. * @param catchAst * DetailAST of catch block. */ private void processCatchNode(final DetailAST catchAst) { boolean isLoggingExceptionFound = false; int loggingExceptionLineNumber = 0; final List<String> exceptionVariableNames = new LinkedList<>(); final String catchParameterName = getCatchParameterName(catchAst); final DetailAST statementsAst = catchAst.findFirstToken(TokenTypes.SLIST); DetailAST currentStatementAst = statementsAst.getFirstChild(); while (currentStatementAst != null) { switch (currentStatementAst.getType()) { // local logger or exception variable definition case TokenTypes.VARIABLE_DEF: if (isLoggerVariableDefinition(currentStatementAst)) { currentLocalLoggerVariableNames .add(getIdentifier(currentStatementAst)); } else { final DetailAST assignAst = currentStatementAst .findFirstToken(TokenTypes.ASSIGN); if (assignAst != null && isInstanceCreationBasedOnException( assignAst.getFirstChild(), catchParameterName)) { exceptionVariableNames .add(getIdentifier(currentStatementAst)); } } break; // logging exception or printStackTrace case TokenTypes.EXPR: if (!isLoggingExceptionFound && (isLoggingExceptionArgument(currentStatementAst, catchParameterName) || isPrintStackTrace(currentStatementAst, catchParameterName))) { isLoggingExceptionFound = true; loggingExceptionLineNumber = currentStatementAst.getLineNo(); } break; // throw exception case TokenTypes.LITERAL_THROW: if (isLoggingExceptionFound) { exceptionVariableNames.add(catchParameterName); final DetailAST thrownExceptionAst = currentStatementAst .getFirstChild(); if (exceptionVariableNames.contains(getIdentifier(thrownExceptionAst)) || isInstanceCreationBasedOnException( thrownExceptionAst, catchParameterName)) { log(loggingExceptionLineNumber, MSG_KEY); break; } } break; default: // rest tokens shall be skipped break; } currentStatementAst = currentStatementAst.getNextSibling(); } } /** * Verify that aVariableDefAst is variable of logger type. * @param variableDefAst * DetailAST of variable definition. * @return true if variable is of logger type. */ private boolean isLoggerVariableDefinition(final DetailAST variableDefAst) { final DetailAST variableTypeAst = variableDefAst.findFirstToken(TokenTypes.TYPE).getFirstChild(); final String variableTypeName = FullIdent.createFullIdent(variableTypeAst).getText(); return isLoggerClassName(variableTypeName); } /** * Verify that aClassName is class name of logger type. * @param className name of checked class. * @return true aClassName is class name of logger type. */ private boolean isLoggerClassName(String className) { return (hasLoggerClassInImports && className.equals(loggerSimpleClassName)) || className.equals(loggerFullyQualifiedClassName); } /** * Get parameter name of catch block. * @param catchAst * DetailAST of catch block. * @return name of parameter. */ private static String getCatchParameterName(final DetailAST catchAst) { final DetailAST parameterDefAst = catchAst.findFirstToken(TokenTypes.PARAMETER_DEF); return getIdentifier(parameterDefAst); } /** * Get identifier of AST. These can be names of types, subpackages, fields, * methods, parameters, and local variables. * @param ast * DetailAST instance * @return identifier of AST, null if AST does not have name. */ private static String getIdentifier(final DetailAST ast) { String result = null; if (ast != null) { final DetailAST identAst = ast.findFirstToken(TokenTypes.IDENT); if (identAst != null) { result = identAst.getText(); } } return result; } /** * Verify that expression is creating instance. And this instance is created * with exception argument. Example: new MyException("message", exception). * @param expressionAst * DetailAST of expression. * @param exceptionArgumentName * Exception argument name. * @return true if given expression is creating new exception based on * another exception object named aExeceptionParameterName. */ private static boolean isInstanceCreationBasedOnException( final DetailAST expressionAst, final String exceptionArgumentName) { boolean result = false; final DetailAST literalNewAst = expressionAst.findFirstToken(TokenTypes.LITERAL_NEW); if (literalNewAst != null) { final DetailAST parametersAst = literalNewAst .findFirstToken(TokenTypes.ELIST); if (parametersAst != null) { result = containsExceptionParameter(parametersAst, exceptionArgumentName); } } return result; } /** * Verify that expression is logging exception. * @param expressionAst DetailAST of expression(EXPR). * @param exceptionVariableName name of exception variable. * @return true if expression is logging exception. */ private boolean isLoggingExceptionArgument( final DetailAST expressionAst, final String exceptionVariableName) { boolean result = false; if (isLoggingExpression(expressionAst)) { final DetailAST loggingMethodCallAst = expressionAst.getFirstChild(); final DetailAST loggerParametersAst = loggingMethodCallAst.findFirstToken(TokenTypes.ELIST); result = containsExceptionParameter( loggerParametersAst, exceptionVariableName); } return result; } /** * Verify that aExpressionAst is a logging expression. * @param expressionAst * DetailAST of expression. * @return true if aExpressionAst is a logging expression. */ private boolean isLoggingExpression(final DetailAST expressionAst) { boolean result = false; final DetailAST methodCallAst = expressionAst.getFirstChild(); if (methodCallAst.getType() == TokenTypes.METHOD_CALL && hasChildToken(methodCallAst, TokenTypes.DOT)) { final DetailAST dotAst = methodCallAst.getFirstChild(); final DetailAST loggerObjectAst = dotAst.getFirstChild(); final DetailAST invokedMethodAst = loggerObjectAst.getNextSibling(); final String loggerObjectIdentifier = FullIdent.createFullIdent(loggerObjectAst).getText(); final String invokedMethodIdentifier = invokedMethodAst.getText(); result = (currentLocalLoggerVariableNames .contains(loggerObjectIdentifier) || loggerFieldNames.contains(loggerObjectIdentifier)) && loggingMethodNames.contains(invokedMethodIdentifier); } return result; } /** * Verify that aExceptionVariableName is in aParametersAst. * @param parametersAst * DetailAST of expression list(ELIST). * @param exceptionVariableName * name of exception. * @return true if aExceptionVariableName is in aParametersAst. */ private static boolean containsExceptionParameter( final DetailAST parametersAst, final String exceptionVariableName) { boolean result = false; DetailAST parameterAst = parametersAst.getFirstChild(); while (parameterAst != null) { if (exceptionVariableName.equals(getIdentifier(parameterAst)) || isInstanceMethodCall(exceptionVariableName, parameterAst.getFirstChild())) { result = true; parameterAst = null; } else { parameterAst = parameterAst.getNextSibling(); } } return result; } /** * Verify that expression is call of exception's printStackTrace method. * @param expressionAst * DetailAST of expression. * @param exceptionVariableName * name of exception variable. * @return true if expression is call of exception's printStackTrace method. */ private static boolean isPrintStackTrace(final DetailAST expressionAst, final String exceptionVariableName) { boolean result = false; final DetailAST methodCallAst = expressionAst.getFirstChild(); if (isInstanceMethodCall(exceptionVariableName, methodCallAst)) { final String methodCallStr = FullIdent.createFullIdentBelow(methodCallAst).getText(); if (PRINT_STACK_TRACE_METHOD_PATTERN.matcher(methodCallStr).matches()) { result = true; } } return result; } /** * Verify that method is invoked on aUsedInstanceName. * @param usedInstanseName name of instance. * @param methodCallAst DetailAST of METHOD_CALL. * @return true if method is invoked on aUsedInstanceName. */ private static boolean isInstanceMethodCall(final String usedInstanseName, final DetailAST methodCallAst) { boolean result = false; if (methodCallAst != null && methodCallAst.getType() == TokenTypes.METHOD_CALL) { final String methodCallIdent = FullIdent.createFullIdentBelow(methodCallAst).getText(); final int firstDotIndex = methodCallIdent.indexOf('.'); if (firstDotIndex != -1) { final String usedObjectName = methodCallIdent.substring(0, firstDotIndex); if (usedObjectName.equals(usedInstanseName)) { result = true; } } } return result; } /** * Return true if aAST has token of aTokenType type. * @param ast * DetailAST instance. * @param tokenType * one of TokenTypes * @return true if aAST has token of given type, or false otherwise. */ private static boolean hasChildToken(final DetailAST ast, int tokenType) { return ast.findFirstToken(tokenType) != null; } }