//////////////////////////////////////////////////////////////////////////////// // 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.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> * * Output for code above is "D", but more obvious would be "BC".<br> * * Check has following properties:<br> * <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> * * When using <b>ignoreIsolatedTernaryOnLine</b> (value = <b>true</b>), even without<br> * <b>ignoreTernaryOperatorsInBraces</b> option Check won't warn on code below:<br> * * <pre> * int a = (d == 5) ? d : f * + * ((d == 6) ? g : k); * </pre> * * @author <a href="mailto:nesterenko-aleksey@list.ru">Aleksey Nesterenko</a> */ 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"; private static final int DEFAULT_MAX_TERNARY_PER_EXPRESSION_COUNT = 1; private int maxTernaryPerExpressionCount = DEFAULT_MAX_TERNARY_PER_EXPRESSION_COUNT; private boolean ignoreTernaryOperatorsInBraces = true; private boolean ignoreIsolatedTernaryOnLine = true; @Override public int[] getDefaultTokens() { return new int[] { TokenTypes.EXPR, }; } /** * 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; } }