/*
* This file is part of the Jikes RVM project (http://jikesrvm.org).
*
* This file is licensed to You under the Eclipse Public License (EPL);
* You may not use this file except in compliance with the License. You
* may obtain a copy of the License at
*
* http://www.opensource.org/licenses/eclipse-1.0.php
*
* See the COPYRIGHT.txt file distributed with this work for information
* regarding copyright ownership.
*/
package org.jikesrvm.tools.checkstyle;
import static com.puppycrawl.tools.checkstyle.api.TokenTypes.CLASS_DEF;
import static com.puppycrawl.tools.checkstyle.api.TokenTypes.DOT;
import static com.puppycrawl.tools.checkstyle.api.TokenTypes.ENUM_DEF;
import static com.puppycrawl.tools.checkstyle.api.TokenTypes.EXPR;
import static com.puppycrawl.tools.checkstyle.api.TokenTypes.IDENT;
import static com.puppycrawl.tools.checkstyle.api.TokenTypes.LAND;
import static com.puppycrawl.tools.checkstyle.api.TokenTypes.LITERAL_ASSERT;
import static com.puppycrawl.tools.checkstyle.api.TokenTypes.LITERAL_FALSE;
import static com.puppycrawl.tools.checkstyle.api.TokenTypes.LITERAL_IF;
import static com.puppycrawl.tools.checkstyle.api.TokenTypes.METHOD_CALL;
import static com.puppycrawl.tools.checkstyle.api.TokenTypes.PACKAGE_DEF;
import static com.puppycrawl.tools.checkstyle.api.TokenTypes.*;
import java.util.Stack;
import com.puppycrawl.tools.checkstyle.api.DetailAST;
import com.puppycrawl.tools.checkstyle.api.FullIdent;
/**
* Implements checking of the Jikes RVM assertion style.<p>
*
* Files that are part of the MMTk test harness are not checked against
* the assertion coding style.<p>
*
* The implementation manually descends the tree (as opposed to relying on the
* visitor for the traversal) from the interesting parts in order to keep the
* set of declared tokens small (see {@link #defaultTokens}).
*/
public final class JikesRVMAssertionStyle extends AbstractJikesRVMPlugin {
private static final String ASSERT_IS_FORBIDDEN_MSG = "The assert keyword must not be used.";
private static final String IMPROPERLY_GUARDED_ASSERTION_MSG = "All " +
"uses of VM._assert or opt_assert must be guarded with one of the following on the left " +
"side of the guard: VM.VerifyAssertions, VM.ExtremeAssertions, " +
"IR.SANITY_CHECK or IR.PARANOID.";
private static final String USE_VM_NOT_REACHED_MSG = "Use VM.NOT_REACHED " +
"instead of false when writing assertions that fail when executed.";
private static final String STRING_CONCATENATION_FORBIDDEN_MSG = "Message for assert must be " +
"a string literal or a variable: String concatenation is not allowed in asserts.";
private static final String MUST_USE_STATIC_IMPORT_FOR_OPT_ASSERT_MSG = "The method " +
"opt_assert must always be used with static imports (i.e. opt_assert(..) instead of " +
"OptimizingCompilerException.opt_assert(..)";
private static final String IR = "IR";
private static final String VM = "VM";
private static final String VERIFY_ASSERTIONS = "VerifyAssertions";
private static final String EXTREME_ASSERTIONS = "ExtremeAssertions";
private static final String PARANOID = "PARANOID";
private static final String SANITY_CHECK = "SANITY_CHECK";
private static final String OCE = "OptimizingCompilerException";
private static final String ASSERT_METHOD = "_assert";
private static final String OPT_ASSERT_METHOD = "opt_assert";
private static final boolean DEBUG = false;
private int classDepth;
private boolean isMMTkHarnessClass;
/**
* Tokens that we're interested in. Checkstyle will only call
* {@link #visitToken(DetailAST)} on AST nodes that have one of those
* types.<p>
*/
private static final int[] defaultTokens = new int[] {PACKAGE_DEF, CLASS_DEF,
ENUM_DEF, LITERAL_IF, LITERAL_ASSERT, METHOD_CALL};
private final Stack<Boolean> assertionGuardsPresent;
public JikesRVMAssertionStyle() {
super();
assertionGuardsPresent = new Stack<Boolean>();
}
@Override
public int[] getDefaultTokens() {
return defaultTokens;
}
@Override
public void visitToken(DetailAST ast) {
int astType = ast.getType();
switch (astType) {
case PACKAGE_DEF:
visitPackageDef(ast);
break;
case CLASS_DEF: // fallthrough
case ENUM_DEF:
visitClassOrEnumDef(ast);
break;
case LITERAL_IF:
visitIf(ast);
break;
case METHOD_CALL:
visitMethodCall(ast);
break;
default:
break;
}
if (isSubjectToAssertionStyleChecks()) {
if (astType == LITERAL_ASSERT) {
log(ast.getLineNo(), ast.getColumnNo(), ASSERT_IS_FORBIDDEN_MSG);
}
}
}
@Override
public void leaveToken(DetailAST ast) {
switch (ast.getType()) {
case LITERAL_IF:
assertionGuardsPresent.pop();
break;
case ENUM_DEF: // fallthrough
case CLASS_DEF:
classDepth--;
int innermostClassIndex = classNameBuilder.lastIndexOf("$");
int newLength = (innermostClassIndex > 0) ? innermostClassIndex : 0;
classNameBuilder.setLength(newLength);
break;
default:
break;
}
}
private void visitIf(DetailAST ast) {
DetailAST expressionOfIf = ast.findFirstToken(EXPR);
DetailAST searchRoot = expressionOfIf;
if (searchRoot != null) {
DetailAST and = expressionOfIf.findFirstToken(LAND);
if (and != null) {
searchRoot = and;
}
DetailAST dot = searchRoot.findFirstToken(DOT);
if (dot != null) {
int dotChildrenCount = dot.getChildCount();
if (dotChildrenCount == 2) {
DetailAST firstIdent = dot.findFirstToken(IDENT);
DetailAST secondIdent = firstIdent.getNextSibling();
boolean isVMAssertionVariable = false;
boolean isIRAssertionVariable = false;
if (secondIdent != null) {
String secondText = secondIdent.getText();
isVMAssertionVariable = secondText.equals(VERIFY_ASSERTIONS) ||
secondText.equals(EXTREME_ASSERTIONS);
isIRAssertionVariable = secondText.equals(PARANOID) ||
secondText.equals(SANITY_CHECK);
}
String firstText = firstIdent.getText();
boolean isVMAssertion = firstText.equals(VM) && isVMAssertionVariable;
boolean isIRAssertion = firstText.equals(IR) && isIRAssertionVariable;
if (isVMAssertion || isIRAssertion) {
assertionGuardsPresent.push(Boolean.TRUE);
return;
}
}
}
}
assertionGuardsPresent.push(Boolean.FALSE);
}
private void visitMethodCall(DetailAST ast) {
DetailAST firstChild = ast.getFirstChild();
int firstChildType = firstChild.getType();
// Examine method calls of the form methodName(args);
boolean calleeIsOptAssert = false;
if (firstChildType == IDENT) {
String calleeName = firstChild.getText();
calleeIsOptAssert = calleeName.equals(OPT_ASSERT_METHOD);
}
// Examine method calls of the form Class.methodName(args);
boolean callerIsVM = false;
boolean callerIsOCE = false;
boolean calleeIsAssert = false;
if (firstChildType == DOT) {
DetailAST callerNameAST = firstChild.getFirstChild();
String callerName = callerNameAST.getText();
if (callerNameAST.getType() != IDENT) {
return;
}
DetailAST calleeNameAST = callerNameAST.getNextSibling();
if (calleeNameAST.getType() != IDENT) {
return;
}
String calleeName = calleeNameAST.getText();
callerIsVM = callerName != null && callerName.equals(VM);
callerIsOCE = callerName != null && callerName.equals(OCE);
calleeIsAssert = calleeName.equals(ASSERT_METHOD);
calleeIsOptAssert = calleeName.equals(OPT_ASSERT_METHOD);
}
if (callerIsOCE && calleeIsOptAssert) {
log(ast.getLineNo(), ast.getColumnNo(), MUST_USE_STATIC_IMPORT_FOR_OPT_ASSERT_MSG);
}
// Complain about improperly guarded asserts
if (calleeIsAssert && callerIsVM && !assertionGuardsPresent.contains(Boolean.TRUE)) {
log(ast.getLineNo(), ast.getColumnNo(), IMPROPERLY_GUARDED_ASSERTION_MSG);
} else if (calleeIsOptAssert && !assertionGuardsPresent.contains(Boolean.TRUE)) {
log(ast.getLineNo(), ast.getColumnNo(), IMPROPERLY_GUARDED_ASSERTION_MSG);
}
// Check for forbidden string concatenation and require use of VM.NOT_REACHED instead of false
if (callerIsVM && calleeIsAssert || calleeIsOptAssert) {
DetailAST parameterList = firstChild.getNextSibling();
DetailAST firstArgumentToAssert = parameterList.findFirstToken(EXPR);
if (firstArgumentToAssert == null) {
log(ast.getLineNo(), ast.getColumnNo(), "null!");
return;
}
DetailAST literalFalse = firstArgumentToAssert.findFirstToken(LITERAL_FALSE);
if (literalFalse != null) {
log(ast.getLineNo(), ast.getColumnNo(), USE_VM_NOT_REACHED_MSG);
}
DetailAST secondArgumentToAssert = getASTForNextParameter(firstArgumentToAssert);
checkForForbiddenStringConcatenation(ast, secondArgumentToAssert);
if (secondArgumentToAssert != null) {
DetailAST thirdArgumentToAssert = getASTForNextParameter(secondArgumentToAssert);
checkForForbiddenStringConcatenation(ast, thirdArgumentToAssert);
}
}
}
private void checkForForbiddenStringConcatenation(DetailAST callAST, DetailAST parameterAST) {
if (parameterAST != null) {
int type = parameterAST.getType();
if (type == EXPR) {
parameterAST = parameterAST.getFirstChild();
if (parameterAST == null) {
log(callAST.getLineNo(), callAST.getColumnNo(), "null");
return;
}
}
type = parameterAST.getType();
if (type == STRING_LITERAL ||
type == IDENT) {
return;
}
log(callAST.getLineNo(), callAST.getColumnNo(), STRING_CONCATENATION_FORBIDDEN_MSG);
}
}
private DetailAST getASTForNextParameter(DetailAST currentParameter) {
DetailAST newParameter = null;
DetailAST tempAST = currentParameter.getNextSibling();
if (tempAST != null) {
newParameter = tempAST.getNextSibling();
}
return newParameter;
}
private boolean isSubjectToAssertionStyleChecks() {
return !isMMTkHarnessClass;
}
private void visitPackageDef(DetailAST ast) {
isMMTkHarnessClass = false;
packageNameBuilder.setLength(0);
DetailAST startForFullIdent = ast.getLastChild().getPreviousSibling();
FullIdent fullIdent = FullIdent.createFullIdent(startForFullIdent);
packageNameBuilder.append(fullIdent.getText());
}
@Override
protected void checkIfClassIsExcluded() {
String packageName = packageNameBuilder.toString();
isMMTkHarnessClass = packageName.endsWith("harness") || packageName.contains(".harness.");
if (DEBUG) {
printIfClassBelongsToMMTkHarness(packageName);
}
}
private void printIfClassBelongsToMMTkHarness(String packageName) {
String state = isMMTkHarnessClass ? "belongs to" : "does NOT belong to";
StringBuilder sb = new StringBuilder();
sb.append("Class ");
sb.append(classNameBuilder.toString());
if (packageNameBuilder.length() == 0) {
sb.append(" from default package ");
} else {
sb.append(" from package ");
sb.append(packageName);
sb.append(" ");
}
sb.append(state);
sb.append(" the MMTk test harness. ");
if (isMMTkHarnessClass) {
sb.append("It will be ignored!");
}
System.out.println(sb.toString());
}
}