com.puppycrawl.tools.checkstyle.checks.coding.MultipleStringLiteralsCheck Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of checkstyle Show documentation
Show all versions of checkstyle Show documentation
Checkstyle is a development tool to help programmers write Java code
that adheres to a coding standard
////////////////////////////////////////////////////////////////////////////////
// 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.puppycrawl.tools.checkstyle.checks.coding;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
import com.puppycrawl.tools.checkstyle.api.DetailAST;
import com.puppycrawl.tools.checkstyle.api.TokenTypes;
import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
/**
*
* Checks for multiple occurrences of the same string literal within a single file.
*
*
* Rationale: Code duplication makes maintenance more difficult, so it can be better
* to replace the multiple occurrences with a constant.
*
*
* -
* Property {@code allowedDuplicates} - Specify the maximum number of occurrences
* to allow without generating a warning.
* Default value is {@code 1}.
*
* -
* Property {@code ignoreStringsRegexp} - Specify RegExp for ignored strings (with quotation marks).
* Default value is {@code "^""$"}.
*
* -
* Property {@code ignoreOccurrenceContext} - Specify token type names where duplicate
* strings are ignored even if they don't match ignoredStringsRegexp. This allows you to
* exclude syntactical contexts like annotations or static initializers from the check.
* Default value is {@code ANNOTATION}.
*
*
*
* To configure the check:
*
*
* <module name="MultipleStringLiterals"/>
*
*
* To configure the check so that it allows two occurrences of each string:
*
*
* <module name="MultipleStringLiterals">
* <property name="allowedDuplicates" value="2"/>
* </module>
*
*
* To configure the check so that it ignores ", " and empty strings:
*
*
* <module name="MultipleStringLiterals">
* <property name="ignoreStringsRegexp"
* value='^(("")|(", "))$'/>
* </module>
*
*
* To configure the check so that it flags duplicate strings in all syntactical contexts,
* even in annotations like {@code @SuppressWarnings("unchecked")}:
*
*
* <module name="MultipleStringLiterals">
* <property name="ignoreOccurrenceContext" value=""/>
* </module>
*
*
* @since 3.5
*/
@FileStatefulCheck
public class MultipleStringLiteralsCheck extends AbstractCheck {
/**
* A key is pointing to the warning message text in "messages.properties"
* file.
*/
public static final String MSG_KEY = "multiple.string.literal";
/**
* The found strings and their tokens.
*/
private final Map> stringMap = new HashMap<>();
/**
* Specify token type names where duplicate strings are ignored even if they
* don't match ignoredStringsRegexp. This allows you to exclude syntactical
* contexts like annotations or static initializers from the check.
*/
private final BitSet ignoreOccurrenceContext = new BitSet();
/**
* Specify the maximum number of occurrences to allow without generating a warning.
*/
private int allowedDuplicates = 1;
/**
* Specify RegExp for ignored strings (with quotation marks).
*/
private Pattern ignoreStringsRegexp;
/**
* Construct an instance with default values.
*/
public MultipleStringLiteralsCheck() {
setIgnoreStringsRegexp(Pattern.compile("^\"\"$"));
ignoreOccurrenceContext.set(TokenTypes.ANNOTATION);
}
/**
* Setter to specify the maximum number of occurrences to allow without generating a warning.
* @param allowedDuplicates The maximum number of duplicates.
*/
public void setAllowedDuplicates(int allowedDuplicates) {
this.allowedDuplicates = allowedDuplicates;
}
/**
* Setter to specify RegExp for ignored strings (with quotation marks).
* @param ignoreStringsRegexp
* regular expression pattern for ignored strings
* @noinspection WeakerAccess
*/
public final void setIgnoreStringsRegexp(Pattern ignoreStringsRegexp) {
if (ignoreStringsRegexp == null || ignoreStringsRegexp.pattern().isEmpty()) {
this.ignoreStringsRegexp = null;
}
else {
this.ignoreStringsRegexp = ignoreStringsRegexp;
}
}
/**
* Setter to specify token type names where duplicate strings are ignored even
* if they don't match ignoredStringsRegexp. This allows you to exclude
* syntactical contexts like annotations or static initializers from the check.
* @param strRep the string representation of the tokens interested in
*/
public final void setIgnoreOccurrenceContext(String... strRep) {
ignoreOccurrenceContext.clear();
for (final String s : strRep) {
final int type = TokenUtil.getTokenId(s);
ignoreOccurrenceContext.set(type);
}
}
@Override
public int[] getDefaultTokens() {
return getRequiredTokens();
}
@Override
public int[] getAcceptableTokens() {
return getRequiredTokens();
}
@Override
public int[] getRequiredTokens() {
return new int[] {TokenTypes.STRING_LITERAL};
}
@Override
public void visitToken(DetailAST ast) {
if (!isInIgnoreOccurrenceContext(ast)) {
final String currentString = ast.getText();
if (ignoreStringsRegexp == null || !ignoreStringsRegexp.matcher(currentString).find()) {
stringMap.computeIfAbsent(currentString, key -> new ArrayList<>()).add(ast);
}
}
}
/**
* Analyses the path from the AST root to a given AST for occurrences
* of the token types in {@link #ignoreOccurrenceContext}.
*
* @param ast the node from where to start searching towards the root node
* @return whether the path from the root node to ast contains one of the
* token type in {@link #ignoreOccurrenceContext}.
*/
private boolean isInIgnoreOccurrenceContext(DetailAST ast) {
boolean isInIgnoreOccurrenceContext = false;
for (DetailAST token = ast;
token.getParent() != null;
token = token.getParent()) {
final int type = token.getType();
if (ignoreOccurrenceContext.get(type)) {
isInIgnoreOccurrenceContext = true;
break;
}
}
return isInIgnoreOccurrenceContext;
}
@Override
public void beginTree(DetailAST rootAST) {
stringMap.clear();
}
@Override
public void finishTree(DetailAST rootAST) {
for (Map.Entry> stringListEntry : stringMap.entrySet()) {
final List hits = stringListEntry.getValue();
if (hits.size() > allowedDuplicates) {
final DetailAST firstFinding = hits.get(0);
log(firstFinding, MSG_KEY, stringListEntry.getKey(), hits.size());
}
}
}
}