PublicReferenceToPrivateTypeCheck.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.design;
import java.util.ArrayList;
import java.util.HashSet;
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;
/**
* This Check warns on propagation of inner private types to outer classes:<br>
* - Externally accessible method if it returns private inner type.<br>
* - Externally accessible field if it's type is a private inner type.<br>
* These types could be <a href=
* 'http://docs.oracle.com/javase/tutorial/java/javaOO/nested.html'>private
* inner classes</a>, interfaces or enumerations.<br>
* <br>
* Examples:
* <pre>
* class OuterClass {
* public InnerClass innerFromMain = new InnerClass(); //WARNING
* private class InnerClass { ... }
* public InnerClass getValue() { //WARNING
* return new InnerClass();
* }
* <br>
* private interface InnerInterface { ... }
* public Set<InnerInterface> getValue() { //WARNING
* return new TreeSet<InnerInterface>
* }
* <br>
* private Enum Fruit {Apple, Pear}
* public Fruit getValue() { //WARNING
* return Fruit.Apple;
* }
* <br>
* public someMethod(InnerClass innerClass) { ... } //WARNING
* <br>
* }
* </pre>
* <b>Rationale:</b> it is possible to return<br>
* private inner type or use it as the parameter of non-private method, but it
* is impossible<br>
* to use it in other classes (besides inner classes)<br>
* unless it extends or implements at least one <u>non-private</u> class or
* interface.<br>
* Such situation usually happens after bulk refactoring and usually means
* dead/useless code<br>
* <br>
* @author <a href="mailto:nesterenko-aleksey@list.ru">Aleksey Nesterenko</a>
* @since 1.12.0
*/
public class PublicReferenceToPrivateTypeCheck extends AbstractCheck {
/**
* Check message key for private types.
*/
public static final String MSG_KEY = "public.reference.to.private.type";
/**
* List containing names of private types (classes, interfaces or enums).
*/
private final Set<DetailAST> privateTypes = new HashSet<>();
/**
* List containing the names of types returned by public methods or fields.
*/
private final Set<DetailAST> externallyReferencedTypes = new HashSet<>();
@Override
public int[] getDefaultTokens() {
return new int[] {
TokenTypes.CLASS_DEF,
TokenTypes.METHOD_DEF,
TokenTypes.INTERFACE_DEF,
TokenTypes.ENUM_DEF,
TokenTypes.VARIABLE_DEF,
};
}
@Override
public int[] getAcceptableTokens() {
return getDefaultTokens();
}
@Override
public int[] getRequiredTokens() {
return getDefaultTokens();
}
@Override
public void beginTree(DetailAST rootAST) {
privateTypes.clear();
externallyReferencedTypes.clear();
}
@Override
public void visitToken(DetailAST defAst) {
switch (defAst.getType()) {
case TokenTypes.CLASS_DEF:
case TokenTypes.INTERFACE_DEF:
case TokenTypes.ENUM_DEF:
if (hasModifier(TokenTypes.LITERAL_PRIVATE, defAst)) {
addPrivateTypes(defAst);
}
break;
case TokenTypes.METHOD_DEF:
if (isDefinedInTopLevelClass(defAst)
&& !hasModifier(TokenTypes.LITERAL_PRIVATE, defAst)) {
addExternallyAccessibleMethodTypes(defAst);
}
break;
case TokenTypes.VARIABLE_DEF:
if (isDefinedInTopLevelClass(defAst)
&& !hasModifier(TokenTypes.LITERAL_PRIVATE, defAst)) {
addExternallyAccessibleFieldTypes(defAst);
}
break;
default:
SevntuUtil.reportInvalidToken(defAst.getType());
break;
}
}
@Override
public void finishTree(DetailAST rootAst) {
for (DetailAST privateType : privateTypes) {
for (DetailAST outReturnedType : externallyReferencedTypes) {
if (privateType.getText().equals(
outReturnedType.getText())
&& !isExtendsOrImplementsSmth(privateType
.getParent())) {
log(outReturnedType, MSG_KEY,
outReturnedType.getText());
}
}
}
}
/**
* Adds type to the list of private types.
* @param classOrInterfaceOrEnumDefAst
* AST subtree that represent inner private type definition.
*/
private void addPrivateTypes(DetailAST classOrInterfaceOrEnumDefAst) {
final DetailAST definitionAst = classOrInterfaceOrEnumDefAst
.findFirstToken(TokenTypes.IDENT);
privateTypes.add(definitionAst);
}
/**
* Appends non-private, defined in top-level class method's returned or
* parameter type, or field's type to general list of out referenced types.
* @param methodDefAst
* AST subtree that represent method definition.
*/
private void addExternallyAccessibleMethodTypes(DetailAST methodDefAst) {
final DetailAST typeDefAst = methodDefAst
.findFirstToken(TokenTypes.TYPE);
final DetailAST parametersDefAst = methodDefAst
.findFirstToken(TokenTypes.PARAMETERS);
externallyReferencedTypes.addAll(getMethodOrFieldReferencedTypes(typeDefAst));
externallyReferencedTypes.addAll(getMethodParameterTypes(parametersDefAst));
}
/**
* Appends non-private, defined in top-level class field's type to general
* list of out referenced types.
* @param fieldDefAst
* AST subtree that represent field definition.
*/
private void addExternallyAccessibleFieldTypes(DetailAST fieldDefAst) {
final DetailAST typeDefAst = fieldDefAst.findFirstToken(TokenTypes.TYPE);
externallyReferencedTypes.addAll(getMethodOrFieldReferencedTypes(typeDefAst));
}
/**
* Gets the return type of method or field type.
* @param typeAst
* AST subtree to process.
* @return the return types of the token.
*/
private static List<DetailAST>
getMethodOrFieldReferencedTypes(DetailAST typeAst) {
DetailAST returnedType = null;
final List<DetailAST> returnedTypes = new ArrayList<>();
DetailAST currentNode = typeAst;
while (currentNode != null) {
if (currentNode.getType() == TokenTypes.IDENT) {
returnedType = currentNode;
returnedTypes.add(returnedType);
}
currentNode = SevntuUtil.getNextSubTreeNode(currentNode, typeAst);
}
return returnedTypes;
}
/**
* Gets method's parameters types.
* @param parametersDefAst The token to examine.
* @return The parameter types of the method.
*/
private static List<DetailAST>
getMethodParameterTypes(DetailAST parametersDefAst) {
final List<DetailAST> parameterTypes = new ArrayList<>();
if (parametersDefAst.getFirstChild() != null) {
DetailAST currentNode = parametersDefAst;
DetailAST parameterType = null;
while (currentNode != null) {
if (currentNode.getType() == TokenTypes.PARAMETER_DEF) {
parameterType = currentNode;
while (parameterType != null) {
parameterType = SevntuUtil.getNextSubTreeNode(parameterType,
currentNode);
if (parameterType != null
&& parameterType.getType() == TokenTypes.IDENT) {
parameterTypes.add(parameterType);
}
}
}
currentNode = SevntuUtil.getNextSubTreeNode(currentNode, parametersDefAst);
}
}
return parameterTypes;
}
/**
* Checks if defined type or interface extends or implements any
* <u>non-private type</u>.
* @param classOrInterfaceDefAst The token to examine.
* @return Method returns true if class extends or implements something.
*/
private boolean isExtendsOrImplementsSmth(DetailAST classOrInterfaceDefAst) {
return (classOrInterfaceDefAst
.findFirstToken(TokenTypes.EXTENDS_CLAUSE) != null
|| classOrInterfaceDefAst
.findFirstToken(TokenTypes.IMPLEMENTS_CLAUSE) != null)
&& !isExtendsOrImplementsPrivate(classOrInterfaceDefAst);
}
/**
* Checks if inner class or interface extends or implements <u>inner private
* type</u>.
* @param classOrInterfaceDefAst The token to examine.
* @return true if extending or implementing type is in collection of inner
* private types
*/
private boolean
isExtendsOrImplementsPrivate(DetailAST classOrInterfaceDefAst) {
boolean result = false;
final Set<String> inheritedTypesNamesSet = new HashSet<>();
DetailAST currentNode = classOrInterfaceDefAst;
while (currentNode != null) {
if (currentNode.getType() == TokenTypes.EXTENDS_CLAUSE
|| currentNode.getType() == TokenTypes.IMPLEMENTS_CLAUSE) {
DetailAST implementingOrExtendingAst = currentNode;
while (implementingOrExtendingAst != null) {
implementingOrExtendingAst = SevntuUtil.getNextSubTreeNode(
implementingOrExtendingAst, currentNode);
if (implementingOrExtendingAst != null
&& implementingOrExtendingAst.getType() == TokenTypes.IDENT) {
inheritedTypesNamesSet
.add(implementingOrExtendingAst.getText());
}
}
}
currentNode = SevntuUtil.getNextSubTreeNode(currentNode, classOrInterfaceDefAst);
}
final Set<String> existingPrivateTypes = new HashSet<>();
for (DetailAST privateType : privateTypes) {
existingPrivateTypes.add(privateType.getText());
}
if (existingPrivateTypes.containsAll(inheritedTypesNamesSet)) {
result = true;
}
return result;
}
/**
* Checks if class, interface, enumeration, method or field definition has an
* access modifier of specified type.
* @param modifierType modifier type
* @param defAst definition ast (METHOD_DEF, FIELD_DEF, etc.)
* @return true if class, interface, enumeration, method or field definition has an
* access modifier of specified type
*/
public static boolean
hasModifier(int modifierType, DetailAST defAst) {
final DetailAST modifiersToken = defAst.getFirstChild();
return modifiersToken.findFirstToken(modifierType) != null;
}
/**
* Checks if method or field is defined in top-level class.
* @param methodOrFieldDefAst The token to examine.
* @return true if method is defined in top-level class
*/
private static boolean
isDefinedInTopLevelClass(DetailAST methodOrFieldDefAst) {
return methodOrFieldDefAst.getParent().getParent().getParent() == null;
}
}