////////////////////////////////////////////////////////////////////////////////
// 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.naming;
import java.util.Collection;
import java.util.List;
import java.util.regex.Pattern;
import com.google.common.collect.Lists;
import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
import com.puppycrawl.tools.checkstyle.api.DetailAST;
import com.puppycrawl.tools.checkstyle.api.TokenTypes;
/**
* Check forces enum values to match the specific pattern. According to
* "Java Coding Style" by Achut Reddy p 3.3 constants include
* "all static final object reference types that are never followed by "
* ." (dot).", i.e. enums, which are followed by dot while used in the code are
* to be treated as static object references, while enums, that are not used
* with following dot, should be treated as constants.
* <p>
* Enums are defined to be used as class have some own methods. This condition
* is used to distinguish between Values Enumeration and Class Enumeration.
* Values Enumeration looks like the following: <code>
* enum SimpleErrorEnum
* {
* FIRST_SIMPLE, SECOND_SIMPLE, THIRD_SIMPLE;
* }
* </code>
* <p>
* While Class Enumeration has some methods, for example: <code>
* enum SimpleErrorEnum
* {
* FIRST_SIMPLE, SECOND_SIMPLE, THIRD_SIMPLE;
*
* public String toString() {
* return Integer.toString(ordinal() + 10);
* }
* }
* </code>
* <p>
* Name format for Class Enumeration values is specified with
* {@link #setObjFormat(String)} , while format for enum constants - with
* {@link #setConstFormat(String)}
* <p>
* To avoid assuming enum as static object reference, while using some specific
* methods, {@link #setExcludes(String[])} can be used. For example to make enum in
* the previous example a constant set Excludes property to a value
* <code>toString</code>
* <p>
* By default <code>toString</code> is used as an exclusion.
*
* @author Pavel Baranchikov
*
* @see <a href="http://www.scribd.com/doc/15884743/Java-Coding-Style-by-Achut-Reddy">
* Java Coding Style</a>
*/
public class EnumValueNameCheck extends AbstractCheck {
/**
* A key is pointing to the warning message text in "messages.properties"
* file.
*/
public static final String MSG_CONST = "enum.name.const.invalidPattern";
/**
* A key is pointing to the warning message text in "messages.properties"
* file.
*/
public static final String MSG_OBJ = "enum.name.obj.invalidPattern";
/**
* Default pattern for Values Enumeration names.
*/
public static final String DEFAULT_CONST_PATTERN =
"^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$";
/**
* Default pattern for Class Enumeration names.
*/
public static final String DEFAULT_OBJ_PATTERN = "^[A-Z][a-zA-Z0-9]*$";
/**
* Default exclusions value.
*/
private static final String[] DEFAULT_EXCLUSION = {
"toString",
};
/**
* Regular expression to test Class Enumeration names against.
*/
private Pattern objRegexp;
/**
* Format for Class Enumeration names to check for. Compiled to
* {@link #objRegexp}
*/
private String objFormat;
/**
* Regular expression to test Values Enumeration names against.
*/
private Pattern constRegexp;
/**
* Format for Values Enumeration names to check for. Compiled to
* {@link #constRegexp}
*/
private String constFormat;
/**
* Method and field names to exclude from check.
*/
private final List<Pattern> excludes;
/**
* Constructs check with the default pattern.compile.
*/
public EnumValueNameCheck() {
setConstFormat(DEFAULT_CONST_PATTERN);
setObjFormat(DEFAULT_OBJ_PATTERN);
excludes = Lists.newArrayList();
setExcludes(DEFAULT_EXCLUSION);
}
/**
* Method sets format to match Class Enumeration names.
* @param newConstRegexp format to check against
*/
public final void setConstFormat(String newConstRegexp) {
this.constRegexp = Pattern.compile(newConstRegexp, 0);
constFormat = newConstRegexp;
}
/**
* Method sets format to match Values Enumeration names.
* @param objectRegexp format to check against
*/
public final void setObjFormat(String objectRegexp) {
objRegexp = Pattern.compile(objectRegexp, 0);
objFormat = objectRegexp;
}
/**
* Method sets method and field name exclusions.
* @param excludes
* comma separated list or regular expressions
*/
public final void setExcludes(String[] excludes) {
this.excludes.clear();
for (String exclude: excludes) {
this.excludes.add(Pattern.compile(exclude));
}
}
@Override
public int[] getDefaultTokens() {
return new int[] {
TokenTypes.ENUM_CONSTANT_DEF,
};
}
@Override
public void visitToken(DetailAST ast) {
final DetailAST nameAST = ast.findFirstToken(TokenTypes.IDENT);
final boolean enumIsClass = isClassEnumeration(ast);
final Pattern pattern;
if (enumIsClass) {
pattern = objRegexp;
}
else {
pattern = constRegexp;
}
if (!pattern.matcher(nameAST.getText()).find()) {
final String format;
if (enumIsClass) {
format = objFormat;
}
else {
format = constFormat;
}
final String msg;
if (enumIsClass) {
msg = MSG_OBJ;
}
else {
msg = MSG_CONST;
}
log(nameAST.getLineNo(),
nameAST.getColumnNo(),
msg,
nameAST.getText(),
format);
}
}
/**
* Method determines, whether the Enum, specified as parameter has any
* members. Method uses {@link #excludes} while looking though the tree
* nodes
*
* @param ast
* ast to check
* @return <code>true</code> if enum is a class enumeration
*/
private boolean isClassEnumeration(DetailAST ast) {
return hasMembers(ast, excludes);
}
/**
* Method determines whether the specified enum is a constant or is an
* object.
*
* @param ast
* token of a enum value definition
* @param excludes
* list of ignored member names
* @return <code>true</code> if enum is a class enumeration
*/
private static boolean
hasMembers(DetailAST ast, List<Pattern> excludes) {
final DetailAST objBlock = ast.getParent();
boolean memberFound = false;
for (DetailAST member = objBlock.getFirstChild(); member != null; member = member
.getNextSibling()) {
if (member.getType() == TokenTypes.METHOD_DEF
|| member.getType() == TokenTypes.VARIABLE_DEF) {
final DetailAST memberIdent = member
.findFirstToken(TokenTypes.IDENT);
final String identifierStr = memberIdent.getText();
if (!isAnyMatched(excludes, identifierStr)) {
memberFound = true;
break;
}
}
}
return memberFound;
}
/**
* Returns whether at least one of patterns are successfully matched arainst
* the specified string value
* @param patterns
* pattern list to match against
* @param value
* value to match
* @return <code>true</code> if at least one pattern have been successfully
* matched.
*/
private static boolean
isAnyMatched(Collection<Pattern> patterns, String value) {
for (Pattern pattern : patterns) {
if (pattern.matcher(value).find()) {
return true;
}
}
return false;
}
}