ChildBlockLengthCheck.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.design;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
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;
import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
/**
* This check detects the child blocks, which length is more then 80% of parent
* block length. <br>
* <p>
* Supported keywords are used to detect blocks: <br>
* "if", "else", "for", "switch", "do", "while", "try", "catch".
* </p>
* <p>
* <i>Rationale:</i>
* </p>
* <p>
* Length of child block that is more then 80% of parent block is usually hard
* to read in case child block is long(few display screens). Such child blocks
* should be refactored or moved to separate method.
* </p>
* @author <a href="mailto:Daniil.Yaroslavtsev@gmail.com"> Daniil
* Yaroslavtsev</a>
* @since 1.8.0
*/
public class ChildBlockLengthCheck extends AbstractCheck {
/**
* A key is pointing to the warning message text in "messages.properties"
* file.
*/
public static final String MSG_KEY = "child.block.length";
/**
* The constant is used in percentage arithmetic operations. Represents
* '100%'
*/
private static final double PERCENTS_FACTOR = 100.0;
/**
* Default value of maximum percentage ratio between the child and the
* parent block.
*/
private static final double DEFAULT_MAX_CHILD_BLOCK_PERCENTAGE = 80.0;
/**
* Default number of lines of which block body may consist to be skipped by
* check.
*/
private static final int DEFAULT_IGNORE_BLOCK_LINESCOUNT = 50;
/**
* Array contains all allowed block types to be checked. Supported block
* types: LITERAL_IF, LITERAL_SWITCH, LITERAL_FOR, LITERAL_DO,
* LITERAL_WHILE, LITERAL_TRY, LITERAL_ELSE, LITERAL_CATCH.
*/
private int[] blockTypes;
/**
* Maximum percentage ratio between the child block and the parent block.
*/
private double maxChildBlockPercentage = DEFAULT_MAX_CHILD_BLOCK_PERCENTAGE;
/**
* Maximum number of lines of which block body may consist to be skipped by
* check.
*/
private int ignoreBlockLinesCount = DEFAULT_IGNORE_BLOCK_LINESCOUNT;
/**
* Sets allowed types of blocks to be checked. Supported block types:
* LITERAL_IF, LITERAL_SWITCH, LITERAL_FOR, LITERAL_DO, LITERAL_WHILE,
* LITERAL_TRY, LITERAL_ELSE, LITERAL_CATCH.
* @param blockTypes
* - DetailAST tokenTypes that are related to the types which are
* allowed by user in check preferences.
**/
public void setBlockTypes(final String... blockTypes) {
this.blockTypes = new int[blockTypes.length];
for (int i = 0; i < blockTypes.length; i++) {
this.blockTypes[i] = TokenUtil.getTokenId(blockTypes[i]);
}
}
/**
* Sets the maximum percentage ratio between child and parent block. (sets
* "maxChildBlockPercentage" option value)
* @param maxChildBlockPercentage
* the new "maxChildBlockPercentage" option value.
*/
public void setMaxChildBlockPercentage(int maxChildBlockPercentage) {
this.maxChildBlockPercentage = maxChildBlockPercentage;
}
/**
* Sets the maximum linelength of blocks that will be ignored by check.
* @param ignoreBlockLinesCount
* the maximum linelength of the block to be ignored.
*/
public void setIgnoreBlockLinesCount(int ignoreBlockLinesCount) {
this.ignoreBlockLinesCount = ignoreBlockLinesCount;
}
// -@cs[SimpleAccessorNameNotation] Overrides method from the base class.
// Issue: https://github.com/sevntu-checkstyle/sevntu.checkstyle/issues/166
@Override
public int[] getDefaultTokens() {
final int[] result;
if (blockTypes == null) {
result = null;
}
else {
result = Arrays.copyOf(blockTypes, blockTypes.length);
}
return result;
}
@Override
public int[] getAcceptableTokens() {
return getDefaultTokens();
}
@Override
public int[] getRequiredTokens() {
return getDefaultTokens();
}
@Override
public void visitToken(DetailAST ast) {
final DetailAST aOpeningBrace = openingBrace(ast);
// if the block has braces at all
if (aOpeningBrace != null) {
final DetailAST aClosingBrace = closingBrace(ast);
final int parentBlockSize =
linesCount(aOpeningBrace, aClosingBrace);
if (parentBlockSize > ignoreBlockLinesCount) {
final List<DetailAST> childBlocks = getChildBlocks(
aOpeningBrace, aClosingBrace);
final List<DetailAST> badChildBlocks = getBadChildBlocks(
childBlocks, parentBlockSize);
if (badChildBlocks.isEmpty()) {
for (DetailAST childBlock : childBlocks) {
visitToken(childBlock);
}
}
else {
for (DetailAST badBlock : badChildBlocks) {
final int blockSize = linesCount(badBlock);
final double allowedBlockSize = (int) (parentBlockSize
* maxChildBlockPercentage / PERCENTS_FACTOR);
log(badBlock, MSG_KEY, blockSize, allowedBlockSize);
}
}
}
}
}
/**
* Gets all the child blocks for given parent block. Uses an iterative
* algorithm.
* @param blockOpeningBrace
* a DetailAST node that points to the current method`s opening
* brace.
* @param blockClosingBrace
* the a block closing brace
* @return all child blocks that have braces.
*/
private List<DetailAST> getChildBlocks(DetailAST blockOpeningBrace,
DetailAST blockClosingBrace) {
final List<DetailAST> childBlocks = Lists.newLinkedList();
DetailAST curNode = blockOpeningBrace;
while (curNode != blockClosingBrace) {
if (isAllowedBlockType(curNode.getType())) {
childBlocks.add(curNode);
}
// before node leaving
DetailAST nextNode = curNode.getFirstChild();
final int type = curNode.getType();
// skip anonymous classes and nested methods
if (type == TokenTypes.METHOD_DEF
|| type == TokenTypes.CLASS_DEF) {
nextNode = curNode.getNextSibling();
}
while (nextNode == null) {
// leave the visited node
nextNode = curNode.getNextSibling();
if (nextNode == null) {
curNode = curNode.getParent();
}
}
curNode = nextNode;
}
return childBlocks;
}
/**
* Checks that given child block type is allowed.
* @param blockType
* the token type ID for the given block.
* @return true, if the given child block type is allowed.
*/
private boolean isAllowedBlockType(int blockType) {
boolean result = false;
for (int type : blockTypes) {
if (type == blockType) {
result = true;
break;
}
}
return result;
}
/**
* Gets the child blocks which occupies too much size (in percentage) of
* given parent block size.
* @param blocksList
* the blocks
* @param parentBlockSize
* the a parent block size
* @return the wrong child blocks
*/
private List<DetailAST> getBadChildBlocks(List<DetailAST> blocksList,
int parentBlockSize) {
final List<DetailAST> result = new LinkedList<>();
for (DetailAST block : blocksList) {
if (isChildBlockBad(block, parentBlockSize)) {
result.add(block);
}
}
return result;
}
/**
* Checks if the child block size percentage from parent block is greater
* than.
* @param childBlock
* the a child block node
* @param parentBlockSize
* the a parent block size
* @return true, if is child block wrong
*/
private boolean isChildBlockBad(DetailAST childBlock,
int parentBlockSize) {
boolean result = false;
final DetailAST openingBrace = openingBrace(childBlock);
if (openingBrace != null) {
final DetailAST closingBrace = closingBrace(childBlock);
final int childBlockSize = linesCount(openingBrace, closingBrace);
result = getPercentage(parentBlockSize, childBlockSize);
}
return result;
}
/**
* Gets the percentage which the child block occupies inside the parent
* block.
* @param parentBlockSize
* the parent block size in lines
* @param childBlockSize
* the child block size in lines
* @return the percentage value.
*/
private boolean getPercentage(int parentBlockSize,
final int childBlockSize) {
final double percentage =
((double) childBlockSize / (double) parentBlockSize) * 100.0;
return percentage > maxChildBlockPercentage;
}
/**
* Gets the opening brace for the given block.
* @param parentBlock
* the DetailAST node is related to the given parent block.
* @return the DetailAST node is related to the given block opening brace
*/
private static DetailAST openingBrace(final DetailAST parentBlock) {
final int searchType;
if (parentBlock.getType() == TokenTypes.LITERAL_SWITCH) {
searchType = TokenTypes.LCURLY;
}
else {
searchType = TokenTypes.SLIST;
}
return parentBlock.findFirstToken(searchType);
}
/**
* Gets the closing brace for the given block.
* @param parentBlockNode
* the DetailAST node is related to the given parent block.
* @return the DetailAST node is related to the given block closing brace
*/
private static DetailAST closingBrace(DetailAST parentBlockNode) {
final int aParentBlockType = parentBlockNode.getType();
final DetailAST result;
if (aParentBlockType == TokenTypes.LITERAL_SWITCH) {
result = parentBlockNode.getLastChild();
}
else {
result = openingBrace(parentBlockNode).getLastChild();
}
return result;
}
/**
* Gets the lines count between braces of the given block.
* @param blockAst
* - the DetailAST node is related to the given block (the block
* should have braces!).
* @return the lines count between the given block braces.
*/
private static int linesCount(DetailAST blockAst) {
return linesCount(openingBrace(blockAst), closingBrace(blockAst));
}
/**
* Gets the lines count between the given block opening and closing braces.
* @param openingBrace
* the block opening brace DetailAST (LCURLY or SLIST type)
* @param closingBrace
* the block closing brace DetailAST (RCURLY type)
* @return the lines count between the given block braces.
*/
private static int linesCount(DetailAST openingBrace,
DetailAST closingBrace) {
int result = closingBrace.getLineNo() - openingBrace.getLineNo();
if (result != 0) {
result--;
}
return result;
}
}