//////////////////////////////////////////////////////////////////////////////// // 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.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; 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.TokenTypes; /** * <p> * This check report you, when method, that must return array or collection, * return null value instead of empty collection or empty array. * </p> * <p> * Returning Null is error prone as developers forget to review method * implementation or Java doc for nuances of Null meaning. * </p> * <p> * List of collections set by default: * AbstractCollection, AbstractList, AbstractQueue, AbstractSequentialList, AbstractSet, * ArrayBlockingQueue, ArrayDeque, ArrayList, AttributeList, BeanContextServicesSupport, * BeanContextSupport, ConcurrentLinkedDeque, ConcurrentLinkedQueue, ConcurrentSkipListSet, * CopyOnWriteArrayList, CopyOnWriteArraySet, DelayQueue, EnumSet, HashSet, JobStateReasons, * LinkedBlockingDeque, LinkedBlockingQueue, LinkedHashSet, LinkedList, LinkedTransferQueue, * PriorityBlockingQueue, PriorityQueue, RoleList, RoleUnresolvedList, Stack, SynchronousQueue, * TreeSet, Vector, Collection, List, Map, Set. * </p> * @author <a href="mailto:IliaDubinin91@gmail.com">Ilja Dubinin</a> */ public class NoNullForCollectionReturnCheck extends AbstractCheck { /** * Warning message key. */ public static final String MSG_KEY = "no.null.for.collections"; /** * <p> * Default list of collection implementing classes. * <p> */ private static final String DEFAULT_COLLECTIONS = "AbstractCollection AbstractList " + "AbstractQueue AbstractSequentialList AbstractSet ArrayBlockingQueue ArrayDeque " + "ArrayList AttributeList BeanContextServicesSupport BeanContextSupport " + "ConcurrentLinkedDeque ConcurrentLinkedQueue ConcurrentSkipListSet " + "CopyOnWriteArrayList CopyOnWriteArraySet DelayQueue EnumSet HashSet " + "JobStateReasons LinkedBlockingDeque LinkedBlockingQueue LinkedHashSet " + "LinkedList LinkedTransferQueue PriorityBlockingQueue PriorityQueue RoleList " + "RoleUnresolvedList Stack SynchronousQueue TreeSet Vector Collection List Map Set"; /** * <p> * List of collection, that will be check. * </p> */ private Set<String> collectionList = new HashSet<>(); /** * <p> * Search not only in return line. * </p> */ private boolean searchThroughMethodBody; /** * <p> * List of the method definition tokens, that returns collection. * </p> */ private LinkedList<DetailAST> methodDefs = new LinkedList<>(); /** Default constructor. */ public NoNullForCollectionReturnCheck() { setCollectionList(DEFAULT_COLLECTIONS); } /** * <p> * Setter for list of known collections. * </p> * @param collectionList * - line contains all collection names. */ public final void setCollectionList(String collectionList) { this.collectionList.clear(); for (String currentCollection : collectionList.split("\\s+")) { this.collectionList.add(currentCollection); } } /** * <p> * Setter for searching through body of the method. * </p> * @param searchThroughMethodBody * - deep search value. */ public void setSearchThroughMethodBody(boolean searchThroughMethodBody) { this.searchThroughMethodBody = searchThroughMethodBody; } @Override public int[] getDefaultTokens() { return new int[] { TokenTypes.METHOD_DEF, TokenTypes.LITERAL_RETURN, }; } @Override public void beginTree(DetailAST rootAST) { methodDefs.clear(); } @Override public void visitToken(DetailAST detailAST) { switch (detailAST.getType()) { case TokenTypes.METHOD_DEF: if (isReturnCollection(detailAST)) { methodDefs.push(detailAST); } break; case TokenTypes.LITERAL_RETURN: if (!methodDefs.isEmpty()) { final DetailAST currentMethodDef = getMethodDef(detailAST); if (methodDefs.contains(currentMethodDef) && (hasNullLiteralInReturn(detailAST) || (searchThroughMethodBody && isReturnedValueBeNull(detailAST)))) { log(detailAST.getLineNo(), MSG_KEY); } } break; default: Utils.reportInvalidToken(detailAST.getType()); break; } } /** * <p> * Returns true, when method type is a collection or an array. * </p> * @param methodDef * - DetailAST contains method definition node. * @return true, when method return collection. */ private boolean isReturnCollection(DetailAST methodDef) { DetailAST methodType = methodDef.findFirstToken(TokenTypes.TYPE); methodType = methodType.getFirstChild(); return methodType.getType() == TokenTypes.ARRAY_DECLARATOR || collectionList.contains(methodType.getText()); } /** * <p> * Returns true when null literal has in return expression. * </p> * @param returnLit * - DetailAST contains return literal * @return true when null literal has in return expression. */ private static boolean hasNullLiteralInReturn(DetailAST returnLit) { DetailAST returnExpression = returnLit.findFirstToken(TokenTypes.EXPR); boolean result = false; if (returnExpression != null) { final DetailAST ternary = returnExpression.getFirstChild(); if (TokenTypes.QUESTION == ternary.getType()) { returnExpression = ternary; } result = returnExpression.getChildCount(TokenTypes.LITERAL_NULL) > 0; } return result; } /** * <p> * Returns true, when variable in return may be null. * </p> * @param returnLit * - DetailAST contains LITERAL_RETURN * @return true, when variable may be null. */ private static boolean isReturnedValueBeNull(DetailAST returnLit) { boolean result = false; final DetailAST returnedExpression = returnLit.getFirstChild(); if (returnedExpression.getType() != TokenTypes.SEMI) { final DetailAST variable = returnedExpression.findFirstToken(TokenTypes.IDENT); if (variable != null) { final String variableName = variable.getText(); final DetailAST methodDef = getMethodDef(returnLit); final LinkedList<DetailAST> subblocks = getAllSubblocks(methodDef); subblocks.addFirst(methodDef); result = hasNullInDefinition(subblocks, variableName); //searching for not a null value into variable assignment if (result) { for (DetailAST subblock : subblocks) { final List<DetailAST> expressions = getChildren(getBlockBody(subblock), TokenTypes.EXPR); for (DetailAST expression : expressions) { final DetailAST assign = expression.findFirstToken(TokenTypes.ASSIGN); if (assign != null && variableName.equals(assign.findFirstToken(TokenTypes.IDENT) .getText()) && !assign.branchContains(TokenTypes.LITERAL_NULL)) { result = false; break; } } if (!result) { break; } } } } } return result; } /** * <p> * Return all the nested subblocks in block. * </p> * @param blockDef * - node of the block. * @return all the nested subblocks in block. */ private static LinkedList<DetailAST> getAllSubblocks(DetailAST blockDef) { final DetailAST blockBody = getBlockBody(blockDef); final LinkedList<DetailAST> subblocks = new LinkedList<>(); subblocks.addAll(getChildren(blockBody, TokenTypes.LITERAL_IF)); final List<DetailAST> elseBlocks = new LinkedList<>(); for (DetailAST currentIf : subblocks) { if (currentIf.getChildCount(TokenTypes.LITERAL_ELSE) > 0) { elseBlocks.add(currentIf.findFirstToken(TokenTypes.LITERAL_ELSE)); } } if (!elseBlocks.isEmpty()) { subblocks.addAll(elseBlocks); } subblocks.addAll(getChildren(blockBody, TokenTypes.LITERAL_WHILE)); subblocks.addAll(getChildren(blockBody, TokenTypes.LITERAL_DO)); subblocks.addAll(getChildren(blockBody, TokenTypes.LITERAL_FOR)); subblocks.addAll(getChildren(blockBody, TokenTypes.LITERAL_TRY)); final List<DetailAST> nestedSubblocks = new LinkedList<>(); for (DetailAST currentSubblock : subblocks) { if (currentSubblock.branchContains(TokenTypes.SLIST)) { nestedSubblocks.addAll(getAllSubblocks(currentSubblock)); } } subblocks.addAll(nestedSubblocks); return subblocks; } /** * <p> * Return true when variable is null into the variable definition. * </p> * @param subBlocks * - list contains subblocks. * @param variableName * - name of returned variable. * @return true when variable is null into the variable definition. */ private static boolean hasNullInDefinition(List<DetailAST> subBlocks, String variableName) { boolean result = false; for (DetailAST subblock : subBlocks) { final List<DetailAST> variableDefs = getChildren(getBlockBody(subblock), TokenTypes.VARIABLE_DEF); boolean isFinded = false; for (DetailAST currentDef : variableDefs) { final DetailAST variable = currentDef.findFirstToken(TokenTypes.IDENT); if (variableName.equals(variable.getText())) { final DetailAST variableDef = currentDef; DetailAST variableValue = variableDef.findFirstToken(TokenTypes.ASSIGN); if (variableValue != null) { variableValue = variableValue.findFirstToken(TokenTypes.EXPR); result = variableValue.getFirstChild().getType() == TokenTypes.LITERAL_NULL; } else { result = true; } isFinded = true; break; } } if (isFinded) { break; } } return result; } /** * <p> * Returns all children of that have the specified type. * </p> * @param root * - root token of a block * @param type * - type of children * @return all children of that have the specified type. */ private static List<DetailAST> getChildren(DetailAST root, int type) { final List<DetailAST> children = new LinkedList<>(); DetailAST currentChild = root.findFirstToken(type); if (currentChild != null) { children.add(currentChild); } while (children.size() < root.getChildCount(type)) { currentChild = currentChild.getNextSibling(); if (currentChild.getType() == type) { children.add(currentChild); } } return children; } /** * <p> * Return DetailAST that contained method definition. * </p> * @param returnLit * - DetailAST contains LITERAL_RETURN. * @return DetailAST contains METHOD_DEF */ private static DetailAST getMethodDef(DetailAST returnLit) { DetailAST methodDef = returnLit; while (methodDef != null && methodDef.getType() != TokenTypes.METHOD_DEF) { methodDef = methodDef.getParent(); } return methodDef; } /** * <p> * Return body of the block. * </p> * @param blockDef * - block definition node * @return body of the block. */ private static DetailAST getBlockBody(DetailAST blockDef) { DetailAST blockBody = blockDef.findFirstToken(TokenTypes.SLIST); if (blockBody == null) { blockBody = blockDef; } return blockBody; } }