blob: 7002575dad0d1cad1ae822a1215da569424c66a9 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2015 Obeo.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Obeo - initial API and implementation
*******************************************************************************/
package org.eclipse.acceleo.query.parser;
import com.google.common.collect.Sets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import org.eclipse.acceleo.query.ast.BooleanLiteral;
import org.eclipse.acceleo.query.ast.Call;
import org.eclipse.acceleo.query.ast.CollectionTypeLiteral;
import org.eclipse.acceleo.query.ast.Conditional;
import org.eclipse.acceleo.query.ast.EnumLiteral;
import org.eclipse.acceleo.query.ast.Error;
import org.eclipse.acceleo.query.ast.ErrorCollectionCall;
import org.eclipse.acceleo.query.ast.ErrorExpression;
import org.eclipse.acceleo.query.ast.ErrorFeatureAccessOrCall;
import org.eclipse.acceleo.query.ast.ErrorTypeLiteral;
import org.eclipse.acceleo.query.ast.Expression;
import org.eclipse.acceleo.query.ast.FeatureAccess;
import org.eclipse.acceleo.query.ast.IntegerLiteral;
import org.eclipse.acceleo.query.ast.Lambda;
import org.eclipse.acceleo.query.ast.NullLiteral;
import org.eclipse.acceleo.query.ast.RealLiteral;
import org.eclipse.acceleo.query.ast.SequenceInExtensionLiteral;
import org.eclipse.acceleo.query.ast.SetInExtensionLiteral;
import org.eclipse.acceleo.query.ast.StringLiteral;
import org.eclipse.acceleo.query.ast.TypeLiteral;
import org.eclipse.acceleo.query.ast.VarRef;
import org.eclipse.acceleo.query.ast.VariableDeclaration;
import org.eclipse.acceleo.query.ast.util.AstSwitch;
import org.eclipse.acceleo.query.runtime.IQueryBuilderEngine.AstResult;
import org.eclipse.acceleo.query.runtime.IQueryEnvironment;
import org.eclipse.acceleo.query.runtime.IValidationResult;
import org.eclipse.acceleo.query.runtime.ValidationMessageLevel;
import org.eclipse.acceleo.query.runtime.impl.ValidationMessage;
import org.eclipse.acceleo.query.runtime.impl.ValidationResult;
import org.eclipse.acceleo.query.runtime.impl.ValidationServices;
import org.eclipse.acceleo.query.validation.type.ClassType;
import org.eclipse.acceleo.query.validation.type.EClassifierLiteralType;
import org.eclipse.acceleo.query.validation.type.EClassifierType;
import org.eclipse.acceleo.query.validation.type.ICollectionType;
import org.eclipse.acceleo.query.validation.type.IType;
import org.eclipse.acceleo.query.validation.type.LambdaType;
import org.eclipse.acceleo.query.validation.type.NothingType;
import org.eclipse.acceleo.query.validation.type.SequenceType;
import org.eclipse.acceleo.query.validation.type.SetType;
import org.eclipse.emf.ecore.EClassifier;
/**
* Validates an {@link org.eclipse.acceleo.query.ast.Expression Expression}.
*
* @author <a href="mailto:yvan.lussaud@obeo.fr">Yvan Lussaud</a>
*/
public class AstValidator extends AstSwitch<Set<IType>> {
/**
* Message used when a variable overrides an existing value.
*/
private static final String VARIABLE_OVERRIDES_AN_EXISTING_VALUE = "Variable %s overrides an existing value.";
/**
* Should never happen message.
*/
private static final String SHOULD_NEVER_HAPPEN = "should never happen";
/**
* The {@link ValidationResult}.
*/
protected ValidationResult validationResult;
/**
* Basic language validation services implementation.
*/
private final ValidationServices services;
/**
* Local variable types usable during validation.
*/
private final Stack<Map<String, Set<IType>>> variableTypesStack = new Stack<Map<String, Set<IType>>>();
/**
* Constructor.
*
* @param environment
* the {@link IQueryEnvironment} used to validate
* @param variableTypes
* the set of defined variables.
*/
public AstValidator(IQueryEnvironment environment, Map<String, Set<IType>> variableTypes) {
this.variableTypesStack.push(variableTypes);
this.services = new ValidationServices(environment);
}
/**
* Checks warnings and errors from {@link NothingType} removing them from the resulting {@link Set} of
* {@link IType}.
*
* @param expression
* the {@link Expression} associated with {@link IType}
* @param types
* the {@link Set} of {@link IType} to check
* @return a {@link Set} of {@link IType} filtered from {@link NothingType}
*/
private Set<IType> checkWarningsAndErrors(Expression expression, Set<IType> types) {
final Set<IType> result = new LinkedHashSet<IType>();
final List<ValidationMessage> msgs = new ArrayList<ValidationMessage>();
for (IType type : types) {
if (type instanceof NothingType) {
final AstResult astResult = validationResult.getAstResult();
final int startPostion;
if (expression instanceof Call) {
startPostion = astResult.getEndPosition(((Call)expression).getArguments().get(0));
} else if (expression instanceof FeatureAccess) {
startPostion = astResult.getEndPosition(((FeatureAccess)expression).getTarget());
} else {
startPostion = astResult.getStartPosition(expression);
}
final int endPosition = astResult.getEndPosition(expression);
msgs.add(new ValidationMessage(ValidationMessageLevel.WARNING, ((NothingType)type)
.getMessage(), startPostion, endPosition));
} else {
result.add(type);
}
}
if (result.size() == 0) {
for (ValidationMessage message : msgs) {
message.setLevel(ValidationMessageLevel.ERROR);
}
}
validationResult.getMessages().addAll(msgs);
validationResult.addTypes(expression, result);
return result;
}
/**
* {@inheritDoc}
*
* @see org.eclipse.acceleo.query.ast.util.AstSwitch#caseBooleanLiteral(org.eclipse.acceleo.query.ast.BooleanLiteral)
*/
@Override
public Set<IType> caseBooleanLiteral(BooleanLiteral object) {
final Set<IType> possibleTypes = services.getIType(java.lang.Boolean.class);
return checkWarningsAndErrors(object, possibleTypes);
}
/**
* {@inheritDoc}
*
* @see org.eclipse.acceleo.query.ast.util.AstSwitch#caseCall(org.eclipse.acceleo.query.ast.Call)
*/
@Override
public Set<IType> caseCall(Call object) {
final Set<IType> possibleTypes;
final List<Set<IType>> argTypes = new ArrayList<Set<IType>>();
for (Expression arg : object.getArguments()) {
argTypes.add(doSwitch(arg));
}
final String serviceName = object.getServiceName();
switch (object.getType()) {
case CALLSERVICE:
possibleTypes = services.callType(serviceName, argTypes);
break;
case CALLORAPPLY:
possibleTypes = services.callOrApplyTypes(serviceName, argTypes);
break;
case COLLECTIONCALL:
possibleTypes = services.collectionServiceCallTypes(serviceName, argTypes);
break;
default:
throw new UnsupportedOperationException(SHOULD_NEVER_HAPPEN);
}
return checkWarningsAndErrors(object, possibleTypes);
}
/**
* {@inheritDoc}
*
* @see org.eclipse.acceleo.query.ast.util.AstSwitch#caseCollectionTypeLiteral(org.eclipse.acceleo.query.ast.CollectionTypeLiteral)
*/
@Override
public Set<IType> caseCollectionTypeLiteral(CollectionTypeLiteral object) {
final Set<IType> possibleTypes = new LinkedHashSet<IType>();
for (IType type : doSwitch(object.getElementType())) {
if (object.getValue() == List.class) {
possibleTypes.add(new SequenceType(services.getQueryEnvironment(), type));
} else if (object.getValue() == Set.class) {
possibleTypes.add(new SetType(services.getQueryEnvironment(), type));
} else {
throw new UnsupportedOperationException(SHOULD_NEVER_HAPPEN);
}
}
return checkWarningsAndErrors(object, possibleTypes);
}
/**
* {@inheritDoc}
*
* @see org.eclipse.acceleo.query.ast.util.AstSwitch#caseEnumLiteral(org.eclipse.acceleo.query.ast.EnumLiteral)
*/
@Override
public Set<IType> caseEnumLiteral(EnumLiteral object) {
final Set<IType> possibleTypes = new LinkedHashSet<IType>();
possibleTypes
.add(new EClassifierType(services.getQueryEnvironment(), object.getLiteral().getEEnum()));
return checkWarningsAndErrors(object, possibleTypes);
}
/**
* {@inheritDoc}
*
* @see org.eclipse.acceleo.query.ast.util.AstSwitch#caseFeatureAccess(org.eclipse.acceleo.query.ast.FeatureAccess)
*/
@Override
public Set<IType> caseFeatureAccess(FeatureAccess object) {
final Set<IType> reveiverTypes = doSwitch(object.getTarget());
final String featureName = object.getFeatureName();
final Set<IType> flattened = services.featureAccessTypes(reveiverTypes, featureName);
return checkWarningsAndErrors(object, flattened);
}
/**
* {@inheritDoc}
*
* @see org.eclipse.acceleo.query.ast.util.AstSwitch#caseIntegerLiteral(org.eclipse.acceleo.query.ast.IntegerLiteral)
*/
@Override
public Set<IType> caseIntegerLiteral(IntegerLiteral object) {
final Set<IType> possibleTypes = services.getIType(java.lang.Integer.class);
return checkWarningsAndErrors(object, possibleTypes);
}
/**
* {@inheritDoc}
*
* @see org.eclipse.acceleo.query.ast.util.AstSwitch#caseLambda(org.eclipse.acceleo.query.ast.Lambda)
*/
@Override
public Set<IType> caseLambda(Lambda object) {
final Set<IType> lambdaExpressionTypes = new LinkedHashSet<IType>();
final Map<String, Set<IType>> newVariableTypes = new HashMap<String, Set<IType>>(variableTypesStack
.peek());
for (VariableDeclaration variableDeclaration : object.getParameters()) {
final Set<IType> types;
if (variableDeclaration.getType() == null || variableDeclaration.getType() instanceof Error) {
final Set<IType> variableTypes = doSwitch(variableDeclaration.getExpression());
types = new LinkedHashSet<IType>();
for (IType type : variableTypes) {
if (type instanceof ICollectionType) {
types.add(((ICollectionType)type).getCollectionType());
} else {
types.add(type);
}
}
} else {
types = doSwitch(variableDeclaration.getType());
}
if (newVariableTypes.containsKey(variableDeclaration.getName())) {
lambdaExpressionTypes.add(services.nothing(VARIABLE_OVERRIDES_AN_EXISTING_VALUE,
variableDeclaration.getName()));
}
newVariableTypes.put(variableDeclaration.getName(), types);
}
variableTypesStack.push(newVariableTypes);
final Set<IType> lambdaExpressionPossibleTypes = doSwitch(object.getExpression());
final Set<IType> lambdaEvaluatorPossibleTypes = newVariableTypes.get(object.getParameters().get(0)
.getName());
for (IType lambdaEvaluatorPossibleType : lambdaEvaluatorPossibleTypes) {
for (IType lambdaExpressionType : lambdaExpressionPossibleTypes) {
lambdaExpressionTypes.add(new LambdaType(services.getQueryEnvironment(),
lambdaEvaluatorPossibleType, lambdaExpressionType));
}
}
variableTypesStack.pop();
return lambdaExpressionTypes;
}
/**
* {@inheritDoc}
*
* @see org.eclipse.acceleo.query.ast.util.AstSwitch#caseRealLiteral(org.eclipse.acceleo.query.ast.RealLiteral)
*/
@Override
public Set<IType> caseRealLiteral(RealLiteral object) {
final Set<IType> possibleTypes = services.getIType(java.lang.Double.class);
return checkWarningsAndErrors(object, possibleTypes);
}
/**
* {@inheritDoc}
*
* @see org.eclipse.acceleo.query.ast.util.AstSwitch#caseStringLiteral(org.eclipse.acceleo.query.ast.StringLiteral)
*/
@Override
public Set<IType> caseStringLiteral(StringLiteral object) {
final Set<IType> possibleTypes = services.getIType(java.lang.String.class);
return checkWarningsAndErrors(object, possibleTypes);
}
/**
* {@inheritDoc}
*
* @see org.eclipse.acceleo.query.ast.util.AstSwitch#caseVarRef(org.eclipse.acceleo.query.ast.VarRef)
*/
@Override
public Set<IType> caseVarRef(VarRef object) {
final Set<IType> variableTypes = services.getVariableTypes(variableTypesStack.peek(), object
.getVariableName());
return checkWarningsAndErrors(object, variableTypes);
}
/**
* Validates the given {@link AstResult}.
*
* @param astResult
* the {@link AstResult}
* @return the {@link IValidationResult}
*/
public IValidationResult validate(AstResult astResult) {
validationResult = new ValidationResult(astResult);
doSwitch(astResult.getAst());
return validationResult;
}
/**
* {@inheritDoc}
*
* @see org.eclipse.acceleo.query.ast.util.AstSwitch#caseTypeLiteral(org.eclipse.acceleo.query.ast.TypeLiteral)
*/
@Override
public Set<IType> caseTypeLiteral(TypeLiteral object) {
final Set<IType> possibleTypes;
if (object.getValue() instanceof EClassifier) {
possibleTypes = new LinkedHashSet<IType>();
possibleTypes.add(new EClassifierLiteralType(services.getQueryEnvironment(), (EClassifier)object
.getValue()));
} else if (object.getValue() instanceof Class<?>) {
possibleTypes = services.getIType((Class<?>)object.getValue());
} else {
throw new UnsupportedOperationException(SHOULD_NEVER_HAPPEN);
}
return checkWarningsAndErrors(object, possibleTypes);
}
/**
* {@inheritDoc}
*
* @see org.eclipse.acceleo.query.ast.util.AstSwitch#caseErrorCollectionCall(org.eclipse.acceleo.query.ast.ErrorCollectionCall)
*/
@Override
public Set<IType> caseErrorCollectionCall(ErrorCollectionCall object) {
doSwitch(object.getTarget());
return checkWarningsAndErrors(object, services.getErrorTypes(object));
}
/**
* {@inheritDoc}
*
* @see org.eclipse.acceleo.query.ast.util.AstSwitch#caseErrorExpression(org.eclipse.acceleo.query.ast.ErrorExpression)
*/
@Override
public Set<IType> caseErrorExpression(ErrorExpression object) {
return checkWarningsAndErrors(object, services.getErrorTypes(object));
}
/**
* {@inheritDoc}
*
* @see org.eclipse.acceleo.query.ast.util.AstSwitch#caseErrorFeatureAccessOrCall(org.eclipse.acceleo.query.ast.ErrorFeatureAccessOrCall)
*/
@Override
public Set<IType> caseErrorFeatureAccessOrCall(ErrorFeatureAccessOrCall object) {
doSwitch(object.getTarget());
return checkWarningsAndErrors(object, services.getErrorTypes(object));
}
/**
* {@inheritDoc}
*
* @see org.eclipse.acceleo.query.ast.util.AstSwitch#caseErrorTypeLiteral(org.eclipse.acceleo.query.ast.ErrorTypeLiteral)
*/
@Override
public Set<IType> caseErrorTypeLiteral(ErrorTypeLiteral object) {
return checkWarningsAndErrors(object, services.getErrorTypes(object));
}
/**
* {@inheritDoc}
*
* @see org.eclipse.acceleo.query.ast.util.AstSwitch#caseNullLiteral(org.eclipse.acceleo.query.ast.NullLiteral)
*/
@Override
public Set<IType> caseNullLiteral(NullLiteral object) {
final Set<IType> possibleTypes = new LinkedHashSet<IType>();
possibleTypes.add(new ClassType(services.getQueryEnvironment(), null));
return checkWarningsAndErrors(object, possibleTypes);
}
/**
* {@inheritDoc}
*
* @see org.eclipse.acceleo.query.ast.util.AstSwitch#caseSetInExtensionLiteral(org.eclipse.acceleo.query.ast.SetInExtensionLiteral)
*/
@Override
public Set<IType> caseSetInExtensionLiteral(SetInExtensionLiteral object) {
final Set<IType> possibleTypes = new LinkedHashSet<IType>();
for (Expression expression : object.getValues()) {
for (IType type : doSwitch(expression)) {
possibleTypes.add(new SetType(services.getQueryEnvironment(), type));
}
}
return checkWarningsAndErrors(object, possibleTypes);
}
/**
* {@inheritDoc}
*
* @see org.eclipse.acceleo.query.ast.util.AstSwitch#caseSequenceInExtensionLiteral(org.eclipse.acceleo.query.ast.SequenceInExtensionLiteral)
*/
@Override
public Set<IType> caseSequenceInExtensionLiteral(SequenceInExtensionLiteral object) {
final Set<IType> possibleTypes = new LinkedHashSet<IType>();
for (Expression expression : object.getValues()) {
for (IType type : doSwitch(expression)) {
possibleTypes.add(new SequenceType(services.getQueryEnvironment(), type));
}
}
return checkWarningsAndErrors(object, possibleTypes);
}
/**
* {@inheritDoc}
*
* @see org.eclipse.acceleo.query.ast.util.AstSwitch#caseVariableDeclaration(org.eclipse.acceleo.query.ast.VariableDeclaration)
*/
@Override
public Set<IType> caseVariableDeclaration(VariableDeclaration object) {
doSwitch(object.getExpression());
return null;
}
/**
* Returns <code>true</code> iff the specified set contains the boolean type.
*
* @param types
* the set of types that should contain the boolean type.
* @return <code>true</code> if types contains the boolean type.
*/
private boolean containsBooleanType(Set<IType> types) {
for (IType type : types) {
if (Boolean.class.equals(type.getType())) {
return true;
}
}
return false;
}
/**
* {@inheritDoc}
*
* @see org.eclipse.acceleo.query.ast.util.AstSwitch#caseConditional(org.eclipse.acceleo.query.ast.Conditional)
*/
@Override
public Set<IType> caseConditional(Conditional object) {
Set<IType> result = Sets.newLinkedHashSet();
Set<IType> predicate = doSwitch(object.getPredicate());
if (containsBooleanType(predicate)) {
if (predicate.size() > 1) {
result.add(services.nothing("The predicate may evaluate to a value that is not a boolean."));
result.addAll(doSwitch(object.getTrueBranch()));
result.addAll(doSwitch(object.getFalseBranch()));
}
} else {
result.add(services.nothing("The predicate never evaluates to a boolean type."));
}
return result;
}
}