ForbidCertainImportsCheck.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.regex.Pattern;
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.FullIdent;
import com.puppycrawl.tools.checkstyle.api.TokenTypes;
/**
* <p>Forbids certain imports usage in certain packages.
* </p>
* You can configure this check using the following parameters:
* <ol>
* <li>Package qualified name regexp;</li>
* <li>Forbidden imports regexp;</li>
* <li>Forbidden imports excludes regexp.</li>
* </ol>
* <p>
* This check loads packages qualified names without
* words "package","import" and semicolons, so, please, do NOT include "package" or
* "import" words (or semicolons) into config regexps.</p>
* <p>
* Real-life example of usage: forbid to use all "*.ui.*" packages in "*.dao.*" packages,
* but ignore all Exception imports (such as
* <b>org.springframework.dao.InvalidDataAccessResourceUsageException</b>).
* For doing that, you should to use the following check parameters:
* </p>
* <ul>
* <li>Package name regexp = ".*ui.*"</li>
* <li>Forbidden imports regexp = ".*dao.*"</li>
* <li>Forbidden imports excludes regexp = "^.+Exception$"</li>
* </ul>
* <p>
* You can cover more sophisticated rules by means of few check instances.
* </p>
* @author <a href="mailto:Daniil.Yaroslavtsev@gmail.com"> Daniil
* Yaroslavtsev</a>
* @since 1.8.0
*/
public class ForbidCertainImportsCheck extends AbstractCheck {
/**
* The key is pointing to the warning message text in "messages.properties"
* file.
*/
public static final String MSG_KEY = "forbid.certain.imports";
/**
* Pattern for matching package fully qualified name
* (sets the scope of affected packages).
*/
private Pattern packageNamesRegexp;
/**
* Pattern for matching forbidden imports.
*/
private Pattern forbiddenImportsRegexp;
/**
* Pattern for excluding imports from checking.
*/
private Pattern forbiddenImportsExcludesRegexp;
/**
* True, if currently processed package fully qualified name
* matches regexp is provided by user.
*/
private boolean packageMatches;
/**
* Sets the regexp for matching package fully qualified name.
* @param packageNameRegexp
* regexp for package fully qualified name matching.
*/
public void setPackageNameRegexp(String packageNameRegexp) {
if (packageNameRegexp != null) {
packageNamesRegexp = Pattern.compile(packageNameRegexp);
}
}
/**
* Gets the regexp is used for matching forbidden imports.
* @return regexp for forbidden imports matching.
*/
public String getForbiddenImportRegexp() {
return forbiddenImportsRegexp.toString();
}
/**
* Sets the regexp for matching forbidden imports.
* @param forbiddenImportsRegexp
* regexp for matching forbidden imports.
*/
public void setForbiddenImportsRegexp(String forbiddenImportsRegexp) {
if (forbiddenImportsRegexp != null) {
this.forbiddenImportsRegexp = Pattern.compile(forbiddenImportsRegexp);
}
}
/**
* Sets the regexp for excluding imports from checking.
* @param forbiddenImportsExcludesRegexp
* String contains a regexp for excluding imports from checking.
*/
public void setForbiddenImportsExcludesRegexp(String
forbiddenImportsExcludesRegexp) {
if (forbiddenImportsExcludesRegexp != null) {
this.forbiddenImportsExcludesRegexp = Pattern
.compile(forbiddenImportsExcludesRegexp);
}
}
@Override
public int[] getDefaultTokens() {
return new int[] {
TokenTypes.PACKAGE_DEF,
TokenTypes.IMPORT,
TokenTypes.LITERAL_NEW,
};
}
@Override
public int[] getAcceptableTokens() {
return getDefaultTokens();
}
@Override
public int[] getRequiredTokens() {
return getDefaultTokens();
}
@Override
public void beginTree(DetailAST rootAST) {
packageMatches = false;
}
@Override
public void visitToken(DetailAST ast) {
switch (ast.getType()) {
case TokenTypes.PACKAGE_DEF:
if (packageNamesRegexp != null) {
final String packageQualifiedName = getText(ast);
packageMatches = packageNamesRegexp.matcher(packageQualifiedName)
.matches();
}
break;
case TokenTypes.IMPORT:
final String importQualifiedText = getText(ast);
if (isImportForbidden(importQualifiedText)) {
log(ast, importQualifiedText);
}
break;
case TokenTypes.LITERAL_NEW:
if (ast.findFirstToken(TokenTypes.DOT) != null) {
final String classQualifiedText = getText(ast);
if (isImportForbidden(classQualifiedText)) {
log(ast, classQualifiedText);
}
}
break;
default:
SevntuUtil.reportInvalidToken(ast.getType());
break;
}
}
/**
* Checks if given import both matches 'include' and not matches 'exclude' patterns.
* @param importText package fully qualified name
* @return true is given import is forbidden in current
* classes package, false otherwise
*/
private boolean isImportForbidden(String importText) {
return packageMatches
&& forbiddenImportsRegexp != null
&& forbiddenImportsRegexp.matcher(importText).matches()
&& (forbiddenImportsExcludesRegexp == null
|| !forbiddenImportsExcludesRegexp.matcher(importText).matches());
}
/**
* Logs message on the part of code.
* @param nodeToWarn
* A DetailAST node is pointing to the part of code to warn on.
* @param importText
* import to be warned.
*/
private void log(DetailAST nodeToWarn, String importText) {
log(nodeToWarn, MSG_KEY,
getForbiddenImportRegexp(), importText);
}
/**
* Gets package/import text representation from node of PACKAGE_DEF or IMPORT type.
* @param packageDefOrImportNode
* - DetailAST node is pointing to package or import definition
* (should be a PACKAGE_DEF or IMPORT type).
* @return The fully qualified name of package or import without
* "package"/"import" words or semicolons.
*/
private static String getText(DetailAST packageDefOrImportNode) {
String result = null;
final DetailAST identNode = packageDefOrImportNode.findFirstToken(TokenTypes.IDENT);
if (identNode == null) {
final DetailAST parentDotAST = packageDefOrImportNode.findFirstToken(TokenTypes.DOT);
final FullIdent dottedPathIdent = FullIdent
.createFullIdentBelow(parentDotAST);
final DetailAST nameAST = parentDotAST.getLastChild();
result = dottedPathIdent.getText() + "." + nameAST.getText();
}
else {
result = identNode.getText();
}
return result;
}
}