UniformEnumConstantNameCheck.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.naming;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Collection;
import java.util.List;
import java.util.regex.Pattern;
import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
import com.puppycrawl.tools.checkstyle.api.DetailAST;
import com.puppycrawl.tools.checkstyle.api.TokenTypes;
/**
* Check forces enum constants to match one of the specified patterns and forces
* all the values to follow only one of the specified patterns.
*
* <p>By default both CamelCase and UPPER_CASE are allowed, so check validates,
* whether all the values conform the either of them.
*
* <p>For example, both enums are allowed by the check:<pre>
* public enum EnumOne {
* FirstElement, SecondElement, ThirdElement;
* }
* public enum EnumTwo {
* FIRST_ELEMENT, SECOND_ELEMENT, THIRD_ELEMENT;
* }</pre> But the following enum, is violated, because values conform
* different notations: <pre>
* public enum EnumThree {
* FirstElement, SECOND_ELEMENT, ThirdElement;
* }
* </pre>
*
* <p>To use only CamelCase, use the following configuration:
*
* <pre>
* <module name="UniformEnumConstantNameCheck">
* <property name="format" value="^[A-Z][a-zA-Z0-9]*$"/>
* </module>
* </pre>
*
* <p>If both CamelCase and UPPER_CASE are allowed, use the following configuration
* (this is the default):
*
* <pre>
* <module name="UniformEnumConstantNameCheck">
* <property name="format" value="^[A-Z][a-zA-Z0-9]*$,^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$"/>
* </module>
* </pre>
*
* <p>Only first violation is reported for each enumeration because of the nature
* of the check: it's impossible to determine which specific pattern user should
* follow for this certain enumeration, as multiple patterns have been
* specified. The only thing that this check reports is whether there is at
* least one pattern (among specified in the configuration), which all the enum
* constant conform or there is no.
*
* @author Pavel Baranchikov
* @since 1.21.0
*/
public class UniformEnumConstantNameCheck extends AbstractCheck {
/**
* Message code for format violations. Used, when more than one format
* violated.
*/
public static final String MSG_NOT_VALID_MULTI = "enum.name.formats.violated";
/**
* Message code for format violations. Used, when exactly one format
* violated.
*/
public static final String MSG_NOT_VALID_SINGLE = "enum.name.format.violated";
/**
* Camel notation regular expression.
*/
public static final String CAMEL_PATTERN = "^[A-Z][a-zA-Z0-9]*$";
/**
* Upper case notation regular expression.
*/
public static final String UPPERCASE_PATTERN = "^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$";
/**
* Default pattern for enumeration values.
*/
public static final String[] DEFAULT_PATTERN = {
CAMEL_PATTERN,
UPPERCASE_PATTERN,
};
/**
* Regular expression list to test Enumeration names against.
*/
private List<Pattern> patterns;
/**
* Number of patterns specified for {@code patterns} field. This field is
* always the size of {@link #patterns}.
*/
private int patternCount;
/**
* Constructs check with the default pattern.
*/
public UniformEnumConstantNameCheck() {
setFormats(DEFAULT_PATTERN);
}
/**
* Method sets format to match Class Enumeration names.
* @param regexps format to check against
*/
public final void setFormats(String... regexps) {
this.patterns = new ArrayList<>(regexps.length);
for (final String regexp: regexps) {
final Pattern pattern = Pattern.compile(regexp, 0);
patterns.add(pattern);
}
patternCount = regexps.length;
}
@Override
public int[] getDefaultTokens() {
return getAcceptableTokens();
}
@Override
public int[] getAcceptableTokens() {
return new int[] {
TokenTypes.ENUM_DEF,
};
}
@Override
public int[] getRequiredTokens() {
return getAcceptableTokens();
}
@Override
public void visitToken(DetailAST ast) {
final DetailAST objBlock = ast.findFirstToken(TokenTypes.OBJBLOCK);
final BitSet conformedPatterns = new BitSet(patternCount);
conformedPatterns.set(0, patternCount);
for (DetailAST member = objBlock.getFirstChild(); member != null
&& !conformedPatterns.isEmpty();
member = member.getNextSibling()) {
if (member.getType() != TokenTypes.ENUM_CONSTANT_DEF) {
continue;
}
final String enumName = member.findFirstToken(TokenTypes.IDENT).getText();
final BitSet matched = match(enumName, conformedPatterns);
if (matched.isEmpty()) {
logViolation(member, enumName, conformedPatterns);
}
conformedPatterns.and(matched);
}
}
/**
* Logs violation for the specified token, representing the specified enum
* value wich violates the specified patterns.
*
* @param member
* token, which violates the check
* @param enumName
* enum value name for this token
* @param violated
* bit set of violated patterns
*/
private void logViolation(DetailAST member, String enumName, BitSet violated) {
final String patternsString;
final String msgKey;
if (violated.cardinality() == 1) {
msgKey = MSG_NOT_VALID_SINGLE;
patternsString = patterns.get(violated.nextSetBit(0))
.toString();
}
else {
msgKey = MSG_NOT_VALID_MULTI;
final Collection<Pattern> violatedPatterns = new ArrayList<>(
violated.cardinality());
int index = violated.nextSetBit(0);
while (index >= 0) {
violatedPatterns.add(patterns.get(index));
index = violated.nextSetBit(index + 1);
}
patternsString = violatedPatterns.toString();
}
log(member, msgKey, enumName,
patternsString);
}
/**
* Matches the specified enum name against the patterns, specified by
* {@code conformedPatterns}.
*
* @param name
* name to validate
* @param conformedPatterns
* bit set of patterns, which the method should match against.
* @return bit set of matched patterns. Returned value is always a subset of
* {@code conformedPatterns}
*/
private BitSet match(String name, BitSet conformedPatterns) {
final BitSet result = new BitSet(patternCount);
for (int i = 0; i < patterns.size(); i++) {
if (conformedPatterns.get(i)) {
final Pattern pattern = patterns.get(i);
if (pattern.matcher(name).find()) {
result.set(i);
}
}
}
return result;
}
}