package org.xtest.validation;
import static com.google.common.collect.Maps.newHashMap;
import static org.eclipse.xtext.xbase.validation.IssueCodes.INCOMPATIBLE_RETURN_TYPE;
import static org.eclipse.xtext.xbase.validation.IssueCodes.INVALID_USE_OF_TYPE;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.common.util.TreeIterator;
import org.eclipse.emf.ecore.EAttribute;
import org.eclipse.emf.ecore.EClass;
import org.eclipse.emf.ecore.EClassifier;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.xtend.core.validation.IssueCodes;
import org.eclipse.xtend.core.validation.TypeErasedSignature;
import org.eclipse.xtend.core.xtend.XtendImport;
import org.eclipse.xtend.core.xtend.XtendPackage;
import org.eclipse.xtend.core.xtend.XtendParameter;
import org.eclipse.xtext.CrossReference;
import org.eclipse.xtext.common.types.JvmField;
import org.eclipse.xtext.common.types.JvmGenericType;
import org.eclipse.xtext.common.types.JvmIdentifiableElement;
import org.eclipse.xtext.common.types.JvmOperation;
import org.eclipse.xtext.common.types.JvmType;
import org.eclipse.xtext.common.types.JvmTypeReference;
import org.eclipse.xtext.common.types.TypesPackage;
import org.eclipse.xtext.common.types.util.TypeConformanceComputer;
import org.eclipse.xtext.common.types.util.TypeReferences;
import org.eclipse.xtext.naming.QualifiedName;
import org.eclipse.xtext.nodemodel.ICompositeNode;
import org.eclipse.xtext.nodemodel.INode;
import org.eclipse.xtext.nodemodel.util.NodeModelUtils;
import org.eclipse.xtext.resource.IEObjectDescription;
import org.eclipse.xtext.scoping.IScope;
import org.eclipse.xtext.util.CancelIndicator;
import org.eclipse.xtext.validation.CancelableDiagnostician;
import org.eclipse.xtext.validation.Check;
import org.eclipse.xtext.validation.CheckType;
import org.eclipse.xtext.validation.ValidationMessageAcceptor;
import org.eclipse.xtext.xbase.XAssignment;
import org.eclipse.xtext.xbase.XBlockExpression;
import org.eclipse.xtext.xbase.XExpression;
import org.eclipse.xtext.xbase.XbasePackage;
import org.eclipse.xtext.xbase.typing.ITypeProvider;
import org.xtest.RunType;
import org.xtest.XTestAssertionFailure;
import org.xtest.XTestEvaluationException;
import org.xtest.XTestRunner;
import org.xtest.XTestRunner.DontRunCheck;
import org.xtest.jvmmodel.XtestJvmModelAssociator;
import org.xtest.preferences.PerFilePreferenceProvider;
import org.xtest.preferences.RuntimePref;
import org.xtest.results.XTestResult;
import org.xtest.xTest.Body;
import org.xtest.xTest.XAssertExpression;
import org.xtest.xTest.XMethodDef;
import org.xtest.xTest.XTestPackage;
import com.google.common.base.Function;
import com.google.common.base.Strings;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multiset;
import com.google.common.collect.TreeMultiset;
import com.google.inject.Inject;
import com.google.inject.Singleton;
/**
* Validator for xtest expression models. Validates that:
* <ul>
* <li>assert expressions have boolean return type
* <li>assert/throws types are subclasses are throwable
* <li><b>All unit tests pass</b> </ol>
*
* @author Michael Barry
*/
@Singleton
@SuppressWarnings("restriction")
public class XTestJavaValidator extends AbstractXTestJavaValidator {
private static final int TEST_RUN_FAILURE_INDEX = Integer.MAX_VALUE;
@Inject
private XtestJvmModelAssociator associations;
private final ThreadLocal<CancelIndicator> cancelIndicators = new ThreadLocal<CancelIndicator>();
@Inject
private PerFilePreferenceProvider preferenceProvider;
@Inject
private XTestRunner runner;
@Inject
private TypeErasedSignature.Provider signatureProvider;
@Inject
private TypeConformanceComputer typeConformanceComputer;
@Inject
private ITypeProvider typeProvider;
@Inject
private TypeReferences typeReferences;
private final XtendValidations xtendUtils;
/**
* FOR GUICE USE ONLY
*
* @param xtendUtil
* Xtend validator delegate to hook up to this validators message acceptor
*/
@Inject
public XTestJavaValidator(XtendValidations xtendUtil) {
this.xtendUtils = xtendUtil;
xtendUtils.setDelagate(this);
}
/**
* Verifies that the "throws" type is a subclass of throwable or that the assert expression has
* boolean return type.
*
* @param assertExpression
* The assert expression to check
*/
@Check
public void checkAssertExpression(XAssertExpression assertExpression) {
JvmTypeReference throws1 = assertExpression.getThrows();
if (throws1 != null) {
JvmTypeReference expected = typeReferences.getTypeForName(Throwable.class,
assertExpression);
if (!typeConformanceComputer.isConformant(expected, throws1)) {
error("Throws expression must be a subclass of Throwable",
XTestPackage.Literals.XASSERT_EXPRESSION__THROWS);
}
} else {
XExpression actual = assertExpression.getActual();
if (actual != null) {
JvmTypeReference returnType = typeProvider.getCommonReturnType(actual, true);
JvmTypeReference expected = typeReferences.getTypeForName(Boolean.class,
assertExpression);
if (!typeConformanceComputer.isConformant(expected, returnType)) {
// AssertionMessageBuilder will add more detailed message
warning("Assert expression must return a boolean, is "
+ returnType.getQualifiedName() + " instead",
XTestPackage.Literals.XASSERT_EXPRESSION__ACTUAL);
}
}
}
}
@Override
@Check
public void checkAssignment(XAssignment assignment) {
JvmIdentifiableElement assignmentFeature = assignment.getFeature();
if (!(assignmentFeature instanceof JvmField && ((JvmField) assignmentFeature).isFinal())) {
super.checkAssignment(assignment);
}
}
/**
* Validate imports for an Xtest file
*
* From Xtend 2.3
*
* @param file
* The Xtest file
*/
@Check
public void checkImports(Body file) {
final Map<JvmType, XtendImport> imports = newHashMap();
final Map<JvmType, XtendImport> staticImports = newHashMap();
final Map<String, JvmType> importedNames = newHashMap();
for (XtendImport imp : file.getImports()) {
if (imp.getImportedNamespace() != null) {
warning("The use of wildcard imports is deprecated.", imp, null,
IssueCodes.IMPORT_WILDCARD_DEPRECATED);
} else {
JvmType importedType = imp.getImportedType();
if (importedType != null && !importedType.eIsProxy()) {
Map<JvmType, XtendImport> map = imp.isStatic() ? staticImports : imports;
if (map.containsKey(importedType)) {
warning("Duplicate import of '" + importedType.getSimpleName() + "'.", imp,
null, IssueCodes.IMPORT_DUPLICATE);
} else {
map.put(importedType, imp);
if (!imp.isStatic()) {
JvmType currentType = importedType;
String currentSuffix = currentType.getSimpleName();
JvmType collidingImport = importedNames
.put(currentSuffix, importedType);
if (collidingImport != null) {
error("The import '" + importedType.getIdentifier()
+ "' collides with the import '"
+ collidingImport.getIdentifier() + "'.", imp, null,
IssueCodes.IMPORT_COLLISION);
}
while (currentType.eContainer() instanceof JvmType) {
currentType = (JvmType) currentType.eContainer();
currentSuffix = currentType.getSimpleName() + "$" + currentSuffix;
JvmType collidingImport2 = importedNames.put(currentSuffix,
importedType);
if (collidingImport2 != null) {
error("The import '" + importedType.getIdentifier()
+ "' collides with the import '"
+ collidingImport2.getIdentifier() + "'.", imp, null,
IssueCodes.IMPORT_COLLISION);
}
}
}
}
}
}
}
EList<XExpression> expressions = file.getExpressions();
for (XExpression expression : expressions) {
ICompositeNode node = NodeModelUtils.findActualNodeFor(expression);
if (node != null) {
for (INode n : node.getAsTreeIterable()) {
if (n.getGrammarElement() instanceof CrossReference) {
EClassifier classifier = ((CrossReference) n.getGrammarElement()).getType()
.getClassifier();
if (classifier instanceof EClass
&& (TypesPackage.Literals.JVM_TYPE
.isSuperTypeOf((EClass) classifier) || TypesPackage.Literals.JVM_CONSTRUCTOR
.isSuperTypeOf((EClass) classifier))) {
String simpleName = n.getText().trim();
// handle StaticQualifier Workaround (see Xbase grammar)
if (simpleName.endsWith("::")) {
simpleName = simpleName.substring(0, simpleName.length() - 2);
}
if (importedNames.containsKey(simpleName)) {
JvmType type = importedNames.remove(simpleName);
imports.remove(type);
} else {
while (simpleName.contains("$")) {
simpleName = simpleName.substring(0,
simpleName.lastIndexOf('$'));
if (importedNames.containsKey(simpleName)) {
imports.remove(importedNames.remove(simpleName));
break;
}
}
}
}
}
}
}
}
for (XtendImport imp : imports.values()) {
warning("The import '" + imp.getImportedType().getQualifiedName() + "' is never used.",
imp, null, IssueCodes.IMPORT_UNUSED);
}
}
/**
* Checks that method parameter names don't collide with eachother or other local variables
*
* @param def
* The method def
*/
@Check
public void checkMethodNameDoesntShadowVariable(XMethodDef def) {
if (!def.isStatic()) {
checkDeclaredVariableName(def, def, XtendPackage.Literals.XTEND_FUNCTION__NAME);
}
}
/**
* Checks that method parameter names don't collide with eachother or other local variables
*
* @param def
* The method def
*/
@Check
public void checkMethodParametersUnique(XMethodDef def) {
if (!def.isStatic()) {
for (XtendParameter parameter : def.getParameters()) {
checkDeclaredVariableName(def, parameter,
XtendPackage.Literals.XTEND_PARAMETER__NAME);
}
}
checkParameterNames(def.getParameters(), XtendPackage.Literals.XTEND_PARAMETER__NAME);
}
/**
* Checks that method parameter types are not void
*
* @param param
* The method def parameter
*/
@Check
public void checkMethodParemeterNotVoid(XtendParameter param) {
if (typeReferences.is(param.getParameterType(), Void.TYPE)) {
error("Argument type cannot be void", param.getParameterType(), null,
INVALID_USE_OF_TYPE);
}
}
/**
* Checks that the declared return type of a method matches the actual return type, if specified
*
* @param def
* The method def
*/
@Check
public void checkMethodReturnType(XMethodDef def) {
JvmTypeReference declaredReturnType = def.getReturnType();
if (declaredReturnType != null && !typeReferences.is(declaredReturnType, Void.TYPE)) {
JvmTypeReference commonReturnType = typeProvider.getCommonReturnType(
def.getExpression(), true);
if (!typeConformanceComputer.isConformant(declaredReturnType, commonReturnType)) {
error("Incompatible return type. Declared " + getNameOfTypes(declaredReturnType)
+ " but was " + canonicalName(commonReturnType), def,
XtendPackage.Literals.XTEND_FUNCTION__EXPRESSION,
ValidationMessageAcceptor.INSIGNIFICANT_INDEX, INCOMPATIBLE_RETURN_TYPE);
}
}
}
/**
* Checks that method def type parameter names are unique
*
* @param def
* The method def
*/
@Check
public void checkMethodTypeParametersUnique(XMethodDef def) {
checkParameterNames(def.getTypeParameters(), TypesPackage.Literals.JVM_TYPE_PARAMETER__NAME);
}
/**
* Checks that static method def signatures are unique.
*
* Don't need to check local methods. Like javascript functions they can shadow each other based
* on scoping rules.
*
* @param body
* The body of the test file
*/
@Check
public void checkStaticMethodSignaturesUnique(Body body) {
final TreeIterator<EObject> allContents = body.eResource().getAllContents();
Iterable<EObject> objects = new Iterable<EObject>() {
@Override
public java.util.Iterator<EObject> iterator() {
return allContents;
};
};
Iterable<XMethodDef> filter = Iterables.filter(objects, XMethodDef.class);
Multimap<Object, JvmOperation> signatureToOperation = HashMultimap.create();
for (XMethodDef def : filter) {
if (def.isStatic()) {
for (JvmOperation operation : associations.getJvmOperations(def)) {
TypeErasedSignature signature = signatureProvider.get(operation);
signatureToOperation.put(signature, operation);
}
}
}
JvmGenericType inferredType = associations.getInferredType(body);
if (inferredType != null) {
xtendUtils.doCheckDuplicateExecutables(inferredType, signatureToOperation);
}
}
@Check
@Override
public void checkTypeReferenceIsNotVoid(XExpression expression) {
// only type reference is return type, that can be void
if (!(expression instanceof XMethodDef)) {
super.checkTypeReferenceIsNotVoid(expression);
}
}
/**
* Checks that var-arg parameter is last, if present
*
* @param def
* The method def
*/
@Check
public void checkVarArgIsLast(XMethodDef def) {
EList<XtendParameter> parameters = def.getParameters();
List<XtendParameter> reversedParams = Lists.reverse(parameters);
boolean first = true;
for (XtendParameter parameter : reversedParams) {
if (first) {
first = false;
} else if (parameter.isVarArg()) {
error("Only last paremeter can be var arg", parameter, null, 0);
}
}
}
/**
* Runs the unit test as long as the {@link CheckType} is not {@link DontRunCheck} and marks any
* failed expressions.
*
* @param main
* The xtest expression model to run.
*/
@Check(CheckType.EXPENSIVE)
public void doMagic(Body main) {
RunType weight = getCheckMode().shouldCheck(CheckType.FAST) ? RunType.LIGHTWEIGHT
: RunType.HEAVYWEIGHT;
if (!(getCheckMode() instanceof XTestRunner.DontRunCheck)) {
CancelIndicator indicator = cancelIndicators.get();
if (indicator == null) {
indicator = CancelIndicator.NullImpl;
}
XTestResult result = runner.run(main, weight, indicator);
markErrorsFromTest(result);
if (preferenceProvider.get(main, RuntimePref.MARK_UNEXECUTED)) {
Set<XExpression> unexecutedExpressions = runner.getUnexecutedExpressions(main);
markUnexecuted(main, unexecutedExpressions);
}
getContext().put(XTestResult.KEY, result);
}
}
@Override
protected void checkDeclaredVariableName(EObject nameDeclarator, EObject attributeHolder,
EAttribute attr) {
super.checkDeclaredVariableName(nameDeclarator, attributeHolder, attr);
if (nameDeclarator.eContainer() != null
&& attr.getEContainingClass().isInstance(attributeHolder)) {
String name = (String) attributeHolder.eGet(attr);
if (name != null) {
int idx = 0;
if (nameDeclarator.eContainer() instanceof XBlockExpression) {
idx = ((XBlockExpression) nameDeclarator.eContainer()).getExpressions()
.indexOf(nameDeclarator);
}
IScope scope = getScopeProvider().createSimpleFeatureCallScope(
nameDeclarator.eContainer(),
XbasePackage.Literals.XABSTRACT_FEATURE_CALL__FEATURE,
nameDeclarator.eResource(), true, idx);
Iterable<IEObjectDescription> elements = scope.getElements(QualifiedName
.create(name));
for (IEObjectDescription desc : elements) {
EObject eObjectOrProxy = desc.getEObjectOrProxy();
if (eObjectOrProxy != nameDeclarator && eObjectOrProxy instanceof JvmOperation
&& !(nameDeclarator instanceof XMethodDef)
&& !((JvmOperation) eObjectOrProxy).isStatic()) {
error("Local variable '" + name + "' shadows local method",
attributeHolder,
attr,
org.eclipse.xtext.xbase.validation.IssueCodes.VARIABLE_NAME_SHADOWING);
}
}
}
}
}
/**
* Checks that a parameter name is only used once
*
* @param typeParameters
* List of parameters
* @param nameAttr
* Attribute containing the name
*/
protected void checkParameterNames(EList<? extends EObject> typeParameters,
final EAttribute nameAttr) {
Multiset<String> typeParamNames = TreeMultiset.create(Iterables.transform(typeParameters,
new Function<EObject, String>() {
@Override
public String apply(EObject input) {
return (String) input.eGet(nameAttr);
}
}));
for (EObject typeParam : typeParameters) {
String name = (String) typeParam.eGet(nameAttr);
if (typeParamNames.count(name) > 1) {
error("Duplicate type parameter name '" + name + "'", typeParam, nameAttr,
org.eclipse.xtext.xbase.validation.IssueCodes.VARIABLE_NAME_SHADOWING);
}
}
}
@Override
protected boolean isResponsible(Map<Object, Object> context, EObject eObject) {
cancelIndicators.set((CancelIndicator) context
.get(CancelableDiagnostician.CANCEL_INDICATOR));
return super.isResponsible(context, eObject);
}
@Override
protected boolean isValueExpectedRecursive(XExpression expr) {
EObject eContainer = expr.eContainer();
return eContainer != null && eContainer instanceof XAssertExpression
|| super.isValueExpectedRecursive(expr);
}
@Override
protected boolean supportsCheckedExceptions() {
// No need to check exceptions in xtest, they will be flagged as errors if thrown
return false;
}
/**
* Marks the errors from the test
*
* @param run
* The test result
*/
private void markErrorsFromTest(XTestResult run) {
if (run != null) {
for (String error : run.getErrorMessages()) {
error(run.getQualifiedName() + ": " + error, run.getEObject(), null,
TEST_RUN_FAILURE_INDEX);
}
markEvaluationExceptions(run);
for (XTestResult test : run.getSubTests()) {
markErrorsFromTest(test);
}
}
}
/**
* Marks the evaluation exception on the line that generated it
*
* @param run
* The test that failed
*/
private void markEvaluationExceptions(XTestResult run) {
Collection<XTestEvaluationException> exceptions = run.getEvaluationException();
for (XTestEvaluationException exception : exceptions) {
Throwable cause = exception.getCause();
XExpression expression = exception.getExpression();
String name = run.getQualifiedName();
StringBuilder builder = new StringBuilder(name);
if (!Strings.isNullOrEmpty(name)) {
builder.append(": ");
}
if (cause instanceof XTestAssertionFailure) {
builder.append(cause.getMessage());
} else {
builder.append(cause.toString());
}
for (StackTraceElement trace : cause.getStackTrace()) {
builder.append("\n");
builder.append(trace.toString());
}
error(builder.toString(), expression, null, TEST_RUN_FAILURE_INDEX);
}
}
/**
* Finds all expressions in {@code main} that are not contained in {@code executedExpressions}
*
* @param main
* The top-level expression object
* @param executedExpressions
* The set of evaluated expressions
*/
private void markUnexecuted(Body main, Set<XExpression> unexecuted) {
for (XExpression expression : unexecuted) {
warning("Expression never reached", expression, null, 10);
}
}
}