TernaryPerExpressionCountCheck.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.LinkedList;
import java.util.List;
import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
import com.puppycrawl.tools.checkstyle.api.DetailAST;
import com.puppycrawl.tools.checkstyle.api.TokenTypes;
/**
* Restricts the number of ternary operators in expression to a specific limit.<br><br>
* <b>Rationale:</b> This Check helps to improve code readability by pointing developer on<br>
* expressions which contain more than user-defined count of ternary operators.<br><br>
* It points to complicated ternary
* <a href="http://docs.oracle.com/javase/tutorial/java/nutsandbolts/expressions.html">
* expressions</a>.
* Reason:<br>
* - Complicated ternary expressions are not easy to read.<br>
* - Complicated ternary expressions could lead to ambiguous result if user<br>
* does not know Java's operators priority well, e.g.:<br>
*
* <pre>
* String str = null;
* String x = str != null ? "A" : "B" + str == null ? "C" : "D";
* System.out.println(x);
* </pre>
*
* <p>
* Output for code above is "D", but more obvious would be "BC".
* </p>
* <p>
* Check has following properties:
* </p>
* <ul>
* <li><b>maxTernaryPerExpressionCount</b> - limit of ternary operators per
* expression<br>
* </li>
* <li><b>ignoreTernaryOperatorsInBraces</b> - if true Check will ignore ternary operators<br>
* in braces (braces explicitly set priority level)<br>
* </li>
* <li><b>ignoreIsolatedTernaryOnLine</b> - if true Check will ignore one line ternary operators,
* <br>
* if only it is places in line alone.<br>
* </li>
* </ul>
* Options <b>ignoreTernaryOperatorsInBraces</b> and <b>ignoreIsolatedTernaryOnLine</b> can<br>
* make Check less strict, e.g.:<br>
* Using <b>ignoreTernaryOperatorsInBraces</b> option (value = <b>true</b>)<br>
* does not put violation on code below:<br>
*
* <pre>
* callString = "{? = call " +
* (StringUtils.hasLength(catalogNameToUse)
* ? catalogNameToUse + "." : "") +
* (StringUtils.hasLength(schemaNameToUse)
* ? schemaNameToUse + "." : "") +
* procedureNameToUse + "(";
* </pre>
*
* <p>
* When using <b>ignoreIsolatedTernaryOnLine</b> (value = <b>true</b>), even without<br>
* <b>ignoreTernaryOperatorsInBraces</b> option Check won't warn on code below:
* </p>
*
* <pre>
* int a = (d == 5) ? d : f
* +
* ((d == 6) ? g : k);
* </pre>
*
* @author <a href="mailto:nesterenko-aleksey@list.ru">Aleksey Nesterenko</a>
* @since 1.12.0
*/
public class TernaryPerExpressionCountCheck extends AbstractCheck {
/**
* A key is pointing to the warning message text in "messages.properties"
* file.
*/
public static final String MSG_KEY = "ternary.per.expression.count";
/** Default limit of ternary operators per expression. */
private static final int DEFAULT_MAX_TERNARY_PER_EXPRESSION_COUNT = 1;
/** Limit of ternary operators per expression. */
private int maxTernaryPerExpressionCount = DEFAULT_MAX_TERNARY_PER_EXPRESSION_COUNT;
/**
* If true Check will ignore ternary operators in braces (braces explicitly
* set priority level).
*/
private boolean ignoreTernaryOperatorsInBraces = true;
/**
* If true Check will ignore one line ternary operators, if only it is
* places in line alone.
*/
private boolean ignoreIsolatedTernaryOnLine = true;
@Override
public int[] getDefaultTokens() {
return new int[] {
TokenTypes.EXPR,
};
}
@Override
public int[] getAcceptableTokens() {
return getDefaultTokens();
}
@Override
public int[] getRequiredTokens() {
return getDefaultTokens();
}
/**
* Sets the maximum number of ternary operators, default value = 1.
*
* @param maxTernaryPerExpressionCount
* Number of ternary operators per expression
*/
public void setMaxTernaryPerExpressionCount(int maxTernaryPerExpressionCount) {
if (maxTernaryPerExpressionCount < 0) {
throw new IllegalArgumentException("Value should be 0 or more then 0");
}
this.maxTernaryPerExpressionCount = maxTernaryPerExpressionCount;
}
/**
* Sets parameter to ignore ternary operators in braces, default value =
* true.
*
* @param ignoreTernaryOperatorsInBraces ignore ternary operators in braces
*/
public void setIgnoreTernaryOperatorsInBraces(boolean ignoreTernaryOperatorsInBraces) {
this.ignoreTernaryOperatorsInBraces = ignoreTernaryOperatorsInBraces;
}
/**
* Sets parameter to ignore expressions in case if ternary operator is isolated in line.
*
* @param ignoreIsolatedTernaryOnLine ignore expressions in case if ternary
* operator is isolated in line
*/
public void setIgnoreIsolatedTernaryOnLine(boolean ignoreIsolatedTernaryOnLine) {
this.ignoreIsolatedTernaryOnLine = ignoreIsolatedTernaryOnLine;
}
@Override
public void visitToken(DetailAST expressionNode) {
final List<DetailAST> questionNodes = getQuestionNodes(expressionNode);
if (questionNodes.size() > maxTernaryPerExpressionCount) {
final DetailAST firstQuestionNode = questionNodes.get(0);
log(firstQuestionNode, MSG_KEY, maxTernaryPerExpressionCount);
}
}
/**
* Puts question nodes from current expression node into the list.
* @param expressionNode
* Globally considering expression node
* @return
* List of question nodes
*/
private List<DetailAST> getQuestionNodes(DetailAST expressionNode) {
final List<DetailAST> questionNodes = new LinkedList<>();
DetailAST currentNode = expressionNode;
do {
currentNode = getNextNode(expressionNode, currentNode);
if (currentNode != null
&& currentNode.getType() == TokenTypes.QUESTION
&& !isSkipTernaryOperator(currentNode)) {
questionNodes.add(currentNode);
}
} while (currentNode != null);
return questionNodes;
}
/**
* Checks if options <b>ignoreTernaryInBraces</b> or
* <b>ignoreOneTernaryPerLine</b> were set, hence, count ternary
* operators in current expression or not.
* @param questionAST The token to examine.
* @return true if can skip ternary operator.
*/
private boolean isSkipTernaryOperator(DetailAST questionAST) {
return ignoreTernaryOperatorsInBraces && isTernaryOperatorInBraces(questionAST)
|| ignoreIsolatedTernaryOnLine && isIsolatedTernaryOnLine(questionAST);
}
/**
* Checks ternary operator if it is in braces, which are explicitly setting
* the priority level.
* @param questionAST The token to examine.
* @return true if ternary operator is in braces.
*/
private static boolean isTernaryOperatorInBraces(DetailAST questionAST) {
return questionAST.getPreviousSibling() != null
&& questionAST.getPreviousSibling().getType() == TokenTypes.LPAREN;
}
/**
* Checks if there's one ternary operator per line.
* @param questionAST The token to examine.
* @return true if ternary is isolated on line.
*/
private boolean isIsolatedTernaryOnLine(DetailAST questionAST) {
final int lineNo = questionAST.getLineNo() - 1;
final String line = getFileContents().getText().get(lineNo);
return isSingleTernaryLine(line, lineNo);
}
/**
* Checks line parameter on containing more than 1 ternary operator.
* @param line The line to examine.
* @param lineNo The line number of the line.
* @return true if line is single ternary.
*/
private boolean isSingleTernaryLine(String line, int lineNo) {
int questionsPerLine = 0;
final char[] charArrayFromLine = line.toCharArray();
for (int i = 0; i < line.length(); i++) {
final char currentSymbol = charArrayFromLine[i];
if (currentSymbol == '?' && !getFileContents().hasIntersectionWithComment(lineNo + 1,
i, lineNo + 1, i)) {
questionsPerLine++;
}
if (questionsPerLine > 1) {
break;
}
}
return questionsPerLine == 1;
}
/**
* Gets the next node of a syntactical tree (child of a current node or
* sibling of a current node, or sibling of a parent of a current node).
*
* @param expressionNode
* Globally considering expression node
* @param node
* Current node of syntactical tree
* @return Next node after bypassing
*/
private static DetailAST getNextNode(DetailAST expressionNode,
DetailAST node) {
DetailAST currentNode = node;
DetailAST toVisit = currentNode.getFirstChild();
while (toVisit == null && currentNode != expressionNode) {
toVisit = currentNode.getNextSibling();
if (toVisit == null) {
currentNode = currentNode.getParent();
}
}
return toVisit;
}
}