NoNullForCollectionReturnCheck.java
////////////////////////////////////////////////////////////////////////////////
// checkstyle: Checks Java source code for adherence to a set of rules.
// Copyright (C) 2001-2019 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.SevntuUtil;
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>
* @since 1.9.0
*/
public class NoNullForCollectionReturnCheck extends AbstractCheck {
/**
* Warning message key.
*/
public static final String MSG_KEY = "no.null.for.collections";
/**
* Default list of collection implementing classes.
*/
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";
/**
* List of collection, that will be check.
*/
private final 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 final List<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 int[] getAcceptableTokens() {
return getDefaultTokens();
}
@Override
public int[] getRequiredTokens() {
return getDefaultTokens();
}
@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.add(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, MSG_KEY);
}
}
break;
default:
SevntuUtil.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 List<DetailAST> subblocks = getAllSubblocks(methodDef);
subblocks.add(0, 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 List<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.findFirstToken(TokenTypes.SLIST) != null) {
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) {
result = true;
}
else {
variableValue = variableValue.findFirstToken(TokenTypes.EXPR);
result = variableValue.getFirstChild().getType() == TokenTypes.LITERAL_NULL;
}
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;
}
}