Skip to content

Commit

Permalink
Pull #565: added new check MoveVariableInsideIfCheck
Browse files Browse the repository at this point in the history
  • Loading branch information
rnveach committed May 9, 2017
1 parent 40bac21 commit 6680782
Show file tree
Hide file tree
Showing 11 changed files with 575 additions and 0 deletions.
Expand Up @@ -177,3 +177,6 @@ EitherLogOrThrowCheck.loggingMethodNames = Logging method names separated with c
WhitespaceBeforeArrayInitializerCheck.name = Whitespace Before Array Initializer
WhitespaceBeforeArrayInitializerCheck.desc = This checks enforces whitespace before array initializer.
MoveVariableInsideIfCheck.name = Move Variable Inside If Check
MoveVariableInsideIfCheck.desc = Checks if a variable is only used inside if statements and asks for it's declaration to be moved there too.
Expand Up @@ -529,5 +529,12 @@
<message-key key="whitespace.before.array.initializer"/>
</rule-metadata>

<rule-metadata name="%MoveVariableInsideIfCheck.name" internal-name="MoveVariableInsideIfCheck" parent="TreeWalker">
<alternative-name internal-name="com.github.sevntu.checkstyle.checks.coding.MoveVariableInsideIfCheck"/>
<description>%MoveVariableInsideIfCheck.desc</description>

<message-key key="move.variable.inside"/>
</rule-metadata>

</rule-group-metadata>
</checkstyle-metadata>
2 changes: 2 additions & 0 deletions sevntu-checks/pom.xml
Expand Up @@ -165,6 +165,8 @@
<regex><pattern>.*.checks.coding.EitherLogOrThrowCheck</pattern><branchRate>88</branchRate><lineRate>99</lineRate></regex>
<regex><pattern>.*.checks.coding.ForbidThrowAnonymousExceptionsCheck</pattern><branchRate>81</branchRate><lineRate>97</lineRate></regex>
<regex><pattern>.*.checks.coding.MapIterationInForEachLoopCheck</pattern><branchRate>90</branchRate><lineRate>98</lineRate></regex>
<regex><pattern>.*.checks.coding.MoveVariableInsideIfCheck</pattern><branchRate>98</branchRate><lineRate>100</lineRate></regex>
<regex><pattern>.*.checks.coding.MoveVariableInsideIfCheck.Holder</pattern><branchRate>89</branchRate><lineRate>100</lineRate></regex>
<regex><pattern>.*.checks.coding.NoNullForCollectionReturnCheck</pattern><branchRate>85</branchRate><lineRate>96</lineRate></regex>
<regex><pattern>.*.checks.coding.OverridableMethodInConstructorCheck</pattern><branchRate>88</branchRate><lineRate>99</lineRate></regex>
<regex><pattern>.*.checks.design.HideUtilityClassConstructorCheck</pattern><branchRate>94</branchRate><lineRate>100</lineRate></regex>
Expand Down
2 changes: 2 additions & 0 deletions sevntu-checks/sevntu-checks.xml
Expand Up @@ -212,6 +212,8 @@
<property name="ignoreMethod" value="false"/>
</module>

<module name="com.github.sevntu.checkstyle.checks.coding.MoveVariableInsideIf" />

<!-- moved to checkstyle project since 1.21.0 -->
<!--
<module name="com.github.sevntu.checkstyle.checks.whitespace.SingleSpaceSeparator">
Expand Down
@@ -0,0 +1,275 @@
////////////////////////////////////////////////////////////////////////////////
// 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 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.ScopeUtils;

/**
* <p>
* Checks if a variable is only used inside if statements and asks for it's
* declaration to be moved there too.
* </p>
*
* @author Richard Veach
*/
public class MoveVariableInsideIfCheck extends AbstractCheck {
/**
* A key is pointing to the warning message text in "messages.properties"
* file.
*/
public static final String MSG_KEY = "move.variable.inside";

@Override
public int[] getDefaultTokens() {
return new int[] {TokenTypes.VARIABLE_DEF};
}

@Override
public int[] getRequiredTokens() {
return getDefaultTokens();
}

@Override
public void visitToken(DetailAST ast) {
if (ScopeUtils.isLocalVariableDef(ast)) {
validateLocalVariable(ast);
}
}

/**
* Examines the local variable for violations to be moved inside an nest if
* statement.
*
* @param ast The local variable to examine.
*/
private void validateLocalVariable(DetailAST ast) {
final Holder holder = new Holder(ast);

for (DetailAST child = ast.getNextSibling(); (!holder.exit) && (child != null);
child = child.getNextSibling()) {
switch (child.getType()) {
case TokenTypes.LITERAL_IF:
validateIf(holder, child);
break;
default:
validateOther(holder, child);
break;
}
}

if (holder.blockNode != null) {
log(ast, MSG_KEY, holder.variableName, holder.blockNode.getLineNo());
}
}

/**
* Examines an if statement to see how many times the specified variable
* identifier was used inside it.
*
* @param holder The object holder with the specified variable to check and
* it's current state.
* @param ifNodeGiven The current if node to examine.
*/
private static void validateIf(Holder holder, DetailAST ifNodeGiven) {
DetailAST ifNode = ifNodeGiven;

// -@cs[SingleBreakOrContinue] Too complex to break apart
while (true) {
// check condition
final DetailAST rparen = ifNode.findFirstToken(TokenTypes.RPAREN);
final boolean usedInCondition = holder.hasIdent(
ifNode.findFirstToken(TokenTypes.LPAREN), rparen);

if (usedInCondition) {
holder.setExit();
break;
}

final DetailAST elseNode = ifNode.getLastChild();

// check body of if
final DetailAST body = rparen.getNextSibling();
final DetailAST bodyEnd;

if (body.getType() == TokenTypes.SLIST) {
bodyEnd = body.getLastChild();
}
else {
bodyEnd = elseNode;
}

final boolean used = holder.hasIdent(body, bodyEnd);

if (used) {
holder.setBlockNode(ifNode);

if (holder.exit) {
break;
}
}

if (elseNode.getType() != TokenTypes.LITERAL_ELSE) {
break;
}

ifNode = elseNode.getFirstChild();

if (ifNode.getType() != TokenTypes.LITERAL_IF) {
// check body of else

validateElseOfIf(holder, ifNode, elseNode);
break;
}
}
}

/**
* Examines the else of an if statement to see how many times the specified
* variable identifier was used inside it.
*
* @param holder The object holder with the specified variable to check and
* it's current state.
* @param ifNode The if node of the specified else.
* @param elseNode The current else node to examine.
*/
private static void validateElseOfIf(Holder holder, DetailAST ifNode, DetailAST elseNode) {
final boolean used;

if (ifNode.getType() == TokenTypes.SLIST) {
used = holder.hasIdent(ifNode.getFirstChild(), ifNode.getLastChild());
}
else {
used = holder.hasIdent(ifNode, elseNode.getLastChild());
}

if (used) {
holder.setBlockNode(elseNode);
}
}

/**
* Examines other nodes to see how many times a variable was used inside it.
* If the variable is used, no violations are reported for it.
*
* @param holder The object holder with the specified variable to check and
* it's current state.
* @param child The current node to examine.
*/
private static void validateOther(Holder holder, DetailAST child) {
final boolean used = holder.hasIdent(child, child.getNextSibling());

if (used) {
holder.setExit();
}
}

/**
* The holder of information for the specified variable.
*
* @author Richard Veach
*/
private static class Holder {
/** The name of the variable being examined. */
private String variableName;
/** Switch to trigger ending examining more nodes. */
private boolean exit;
/** The node to report violations on. */
private DetailAST blockNode;

/**
* Default constructor for the class.
*
* @param ast The variable the holder is for.
*/
Holder(DetailAST ast) {
variableName = ast.findFirstToken(TokenTypes.IDENT).getText();
}

/**
* Sets the specified node that is to be reported for the violation for
* the block. If there is already a node being reported, then no nodes
* are reported.
*
* @param blockNode The given block node to report for.
*/
public void setBlockNode(DetailAST blockNode) {
if (this.blockNode != null) {
setExit();
}
else {
this.blockNode = blockNode;
}
}

/** Sets the state to exit examining further nodes. */
public void setExit() {
blockNode = null;
exit = true;
}

/**
* Checks if any of the nodes between the given start and end are an
* identifier with the name of the variable.
*
* @param start The node to start examining from.
* @param end The last node to stop examining once reached. If null,
* then the last node is when we leave the start node.
* @return true if the identifier has been found, otherwise false.
*/
public boolean hasIdent(DetailAST start, DetailAST end) {
boolean found = false;
DetailAST curNode = start;

// -@cs[SingleBreakOrContinue] Too complex to break apart
while (curNode != null) {
if ((curNode.getType() == TokenTypes.IDENT)
&& (variableName.equals(curNode.getText()))) {
found = true;
break;
}

if (curNode == end) {
break;
}

DetailAST toVisit = curNode.getFirstChild();

while (curNode != null && toVisit == null) {
toVisit = curNode.getNextSibling();

if (toVisit == null) {
if ((end == null) && (curNode == start)) {
break;
}

curNode = curNode.getParent();
}
}

curNode = toVisit;
}

return found;
}
}
}
Expand Up @@ -30,6 +30,7 @@ logic.condition.need.optimization=Condition with {0} at line {1} position {2} ne
map.iteration.entrySet=You are using both keys and values for this map. It is better to use entrySet() instead of keySet() + get().
map.iteration.keySet=It is better to use keySet() method to iterate over this map because you aren`t using values.
map.iteration.values=You are using only values of this map. It is better to use values() to iterate this map.
move.variable.inside=Variable ''{0}'' can be moved inside the block at line ''{1}'' to restrict runtime creation.
multiple.string.literal=The String {0} appears {1} times in the file.
multiple.variable.declarations=Only one variable definition per line allowed.
multiple.variable.declarations.comma=Each variable declaration must be in its own statement.
Expand Down
@@ -0,0 +1,62 @@
////////////////////////////////////////////////////////////////////////////////
// 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 static com.github.sevntu.checkstyle.checks.coding.MoveVariableInsideIfCheck.MSG_KEY;

import org.junit.Test;

import com.github.sevntu.checkstyle.BaseCheckTestSupport;
import com.puppycrawl.tools.checkstyle.DefaultConfiguration;
import com.puppycrawl.tools.checkstyle.utils.CommonUtils;

public class MoveVariableInsideIfCheckTest extends BaseCheckTestSupport {
@Test
public final void testNoViolations() throws Exception {
final DefaultConfiguration checkConfig = createCheckConfig(MoveVariableInsideIfCheck.class);
verify(checkConfig, getPath("InputMoveVariableInsideIfCheckNoViolations.java"),
CommonUtils.EMPTY_STRING_ARRAY);
}

@Test
public final void testViolations() throws Exception {
final DefaultConfiguration checkConfig = createCheckConfig(MoveVariableInsideIfCheck.class);
final String[] expected = {
"5:9: " + getCheckMessage(MSG_KEY, "variable", "7"),
"13:9: " + getCheckMessage(MSG_KEY, "variable", "15"),
"24:9: " + getCheckMessage(MSG_KEY, "variable", "26"),
"33:9: " + getCheckMessage(MSG_KEY, "variable", "38"),
"44:9: " + getCheckMessage(MSG_KEY, "variable", "48"),
};
verify(checkConfig, getPath("InputMoveVariableInsideIfCheckViolations.java"), expected);
}

@Test
public final void testFalsePositives() throws Exception {
final DefaultConfiguration checkConfig = createCheckConfig(MoveVariableInsideIfCheck.class);
final String[] expected = {
"9:9: " + getCheckMessage(MSG_KEY, "variable", "12"),
"18:9: " + getCheckMessage(MSG_KEY, "variable", "24"),
"30:9: " + getCheckMessage(MSG_KEY, "variable", "34"),
};
verify(checkConfig, getPath("InputMoveVariableInsideIfCheckFalsePositives.java"),
expected);
}
}

0 comments on commit 6680782

Please sign in to comment.