/*
* Copyright 2015 Nokia Solutions and Networks
* Licensed under the Apache License, Version 2.0,
* see license.txt file for details.
*/
package org.robotframework.ide.eclipse.main.plugin.project.build.validation;
import static com.google.common.collect.Iterables.filter;
import static com.google.common.collect.Iterables.tryFind;
import static com.google.common.collect.Lists.newArrayList;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.IProgressMonitor;
import org.rf.ide.core.testdata.model.table.exec.descs.VariableExtractor;
import org.rf.ide.core.testdata.model.table.exec.descs.ast.mapping.MappingResult;
import org.rf.ide.core.testdata.text.read.IRobotTokenType;
import org.rf.ide.core.testdata.text.read.recognizer.RobotToken;
import org.rf.ide.core.testdata.text.read.recognizer.RobotTokenType;
import org.robotframework.ide.eclipse.main.plugin.project.build.ProblemsReportingStrategy;
import org.robotframework.ide.eclipse.main.plugin.project.build.RobotArtifactsValidator.ModelUnitValidator;
import org.robotframework.ide.eclipse.main.plugin.project.build.RobotProblem;
import org.robotframework.ide.eclipse.main.plugin.project.build.causes.ArgumentProblem;
import org.robotframework.ide.eclipse.main.plugin.project.library.ArgumentsDescriptor;
import org.robotframework.ide.eclipse.main.plugin.project.library.ArgumentsDescriptor.Argument;
import com.google.common.base.Joiner;
import com.google.common.base.Predicate;
import com.google.common.base.Splitter;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Range;
/**
* @author Michal Anglart
*
*/
class KeywordCallArgumentsValidator implements ModelUnitValidator {
private final IFile file;
private final RobotToken definingToken;
private final ProblemsReportingStrategy reporter;
private final ArgumentsDescriptor descriptor;
private final List<RobotToken> arguments;
KeywordCallArgumentsValidator(final IFile file, final RobotToken definingToken,
final ProblemsReportingStrategy reporter, final ArgumentsDescriptor descriptor,
final List<RobotToken> arguments) {
this.file = file;
this.definingToken = definingToken;
this.reporter = reporter;
this.descriptor = descriptor;
this.arguments = arguments;
}
@Override
public void validate(final IProgressMonitor monitor) {
boolean shallContinue = validateNumberOfArguments();
if (!shallContinue) {
return;
}
final Map<String, Argument> namesToArgs = namesToArgsMapping();
shallContinue = validatePositionalAndNamedArgsOrder(namesToArgs.keySet());
if (!shallContinue) {
return;
}
final ArgumentsBinding<Argument, RobotToken> argsMapping = mapDescriptorArgumentsToTokens(namesToArgs);
validateArgumentsBinding(namesToArgs, argsMapping);
}
private boolean validateNumberOfArguments() {
final Range<Integer> expectedArgsNumber = descriptor.getPossibleNumberOfArguments();
final int actual = arguments.size();
if (!expectedArgsNumber.contains(actual)) {
if (!listIsPassed() && !dictIsPassed()) {
final String additional = String.format("Keyword '%s' expects " + getRangesInfo(expectedArgsNumber)
+ ", but %d " + toBeInProperForm(actual) + " provided", definingToken.getText(), actual);
final RobotProblem problem = RobotProblem.causedBy(ArgumentProblem.INVALID_NUMBER_OF_PARAMETERS)
.formatMessageWith(additional);
reporter.handleProblem(problem, file, definingToken);
return false;
}
}
return true;
}
private boolean dictIsPassed() {
return hasTokenOfType(RobotTokenType.VARIABLES_DICTIONARY_DECLARATION);
}
private boolean listIsPassed() {
return hasTokenOfType(RobotTokenType.VARIABLES_LIST_DECLARATION);
}
private boolean hasTokenOfType(final RobotTokenType type) {
return tryFind(arguments, new Predicate<RobotToken>() {
@Override
public boolean apply(final RobotToken argToken) {
return argToken.getTypes().contains(type);
}
}).isPresent();
}
private Map<String, Argument> namesToArgsMapping() {
final Map<String, Argument> argumentsWithNames = new HashMap<>();
for (final Argument argument : descriptor) {
argumentsWithNames.put(argument.getName(), argument);
}
return argumentsWithNames;
}
private boolean validatePositionalAndNamedArgsOrder(final Collection<String> argumentNames) {
boolean thereWasNamedArgumentAlready = false;
boolean thereIsAMessInOrder = false;
for (final RobotToken arg : arguments) {
if (isNamed(arg, argumentNames)) {
thereWasNamedArgumentAlready = true;
} else if (thereWasNamedArgumentAlready) {
final String additionalMsg;
if (arg.getText().contains("=")) {
final String argName = Splitter.on('=').limit(2).splitToList(arg.getText()).get(0).trim();
additionalMsg = ". Although this argument looks like named one, it isn't because there is no '"
+ argName + "' argument in the keyword definition";
} else {
additionalMsg = "";
}
final RobotProblem problem = RobotProblem.causedBy(ArgumentProblem.POSITIONAL_ARGUMENT_AFTER_NAMED)
.formatMessageWith(additionalMsg);
reporter.handleProblem(problem, file, arg);
thereIsAMessInOrder = true;
}
}
return !thereIsAMessInOrder;
}
private ArgumentsBinding<Argument, RobotToken> mapDescriptorArgumentsToTokens(
final Map<String, Argument> namesToArgs) {
final List<RobotToken> positional = new ArrayList<>();
final List<RobotToken> named = new ArrayList<>();
for (final RobotToken arg : arguments) {
if (isPositional(arg, namesToArgs.keySet())) {
positional.add(arg);
} else {
named.add(arg);
}
}
final ArgumentsBinding<Argument, RobotToken> mapping = new ArgumentsBinding<>();
// map positional arguments
int i = 0, j = 0;
while (i < descriptor.size() && j < positional.size()) {
final Argument definingArg = descriptor.get(i);
final RobotToken currentToken = positional.get(j);
mapping.bind(definingArg, currentToken);
if (definingArg.isRequired() || definingArg.isDefault()) {
i++;
}
final List<IRobotTokenType> tokenTypes = currentToken.getTypes();
if (!(tokenTypes.contains(RobotTokenType.VARIABLES_LIST_DECLARATION) && !isNonCollectionVar(currentToken)
&& !definingArg.isVarArg())) {
j++;
}
}
for (final RobotToken argToken : named) {
final String name = getName(argToken);
final Argument potentialArgument = namesToArgs.get(name);
if (potentialArgument != null) {
mapping.bind(potentialArgument, argToken);
} else if (descriptor.supportsKwargs()) {
mapping.bind(descriptor.getKwargArgument().get(), argToken);
} else if (i < descriptor.size()) {
mapping.bind(descriptor.get(i), argToken);
i++;
}
}
while (i < descriptor.size()) {
if (!named.isEmpty() && named.get(named.size() - 1)
.getTypes()
.contains(RobotTokenType.VARIABLES_DICTIONARY_DECLARATION)) {
mapping.bind(descriptor.get(i), named.get(named.size() - 1));
}
i++;
}
return mapping;
}
private void validateArgumentsBinding(final Map<String, Argument> namesToArgs,
final ArgumentsBinding<Argument, RobotToken> argsMapping) {
for (final Argument arg : descriptor) {
final List<RobotToken> values = argsMapping.getDefinitionsMapping(arg);
if (arg.isRequired() && values.isEmpty()) {
final RobotProblem problem = RobotProblem.causedBy(ArgumentProblem.NO_VALUE_PROVIDED_FOR_REQUIRED_ARG)
.formatMessageWith(definingToken.getText(), arg.getName());
reporter.handleProblem(problem, file, definingToken);
} else if ((arg.isRequired() || arg.isDefault()) && values.size() > 1) {
final String firstValue = values.get(0).getText();
for (int i = 1; i < values.size(); i++) {
final RobotToken argToken = values.get(i);
final RobotProblem problem = RobotProblem.causedBy(ArgumentProblem.MULTIPLE_MATCH_TO_SINGLE_ARG)
.formatMessageWith(arg.getName(), firstValue);
reporter.handleProblem(problem, file, argToken);
}
} else if (arg.isKwArg()) {
for (final RobotToken argToken : values) {
if (isPositional(argToken, namesToArgs.keySet())) {
final RobotProblem problem = RobotProblem.causedBy(ArgumentProblem.MISMATCHING_ARGUMENT)
.formatMessageWith(argToken.getText(), definingToken.getText(), arg.getName());
reporter.handleProblem(problem, file, argToken);
}
}
}
}
for (final RobotToken useSiteArg : arguments) {
final List<Argument> defs = argsMapping.getUsageMapping(useSiteArg);
if ((useSiteArg.getTypes().contains(RobotTokenType.VARIABLES_LIST_DECLARATION)
|| useSiteArg.getTypes().contains(RobotTokenType.VARIABLES_DICTIONARY_DECLARATION))
&& !isNonCollectionVar(useSiteArg)) {
if (!defs.isEmpty()) {
final List<Argument> required = newArrayList(filter(defs, onlyRequired()));
if (!required.isEmpty()) {
final ArgumentProblem cause = useSiteArg.getTypes()
.contains(RobotTokenType.VARIABLES_LIST_DECLARATION)
? ArgumentProblem.LIST_ARGUMENT_SHOULD_PROVIDE_ARGS
: ArgumentProblem.DICT_ARGUMENT_SHOULD_PROVIDE_ARGS;
final int noOfRequiredArgs = required.size();
final RobotProblem problem = RobotProblem.causedBy(cause).formatMessageWith(
useSiteArg.getText(), noOfRequiredArgs + toPluralIfNeeded(" value", noOfRequiredArgs),
"[" + Joiner.on(", ").join(required) + "]");
reporter.handleProblem(problem, file, useSiteArg);
}
} else {
final ArgumentProblem cause = useSiteArg.getTypes()
.contains(RobotTokenType.VARIABLES_LIST_DECLARATION)
? ArgumentProblem.LIST_ARGUMENT_SHOULD_PROVIDE_ARGS
: ArgumentProblem.DICT_ARGUMENT_SHOULD_PROVIDE_ARGS;
final RobotProblem problem = RobotProblem.causedBy(cause).formatMessageWith(useSiteArg.getText(),
"0 values", "[]");
reporter.handleProblem(problem, file, useSiteArg);
}
}
}
}
private static Predicate<Argument> onlyRequired() {
return new Predicate<Argument>() {
@Override
public boolean apply(final Argument arg) {
return arg.isRequired();
}
};
}
private boolean isNamed(final RobotToken arg, final Collection<String> argumentNames) {
return !isPositional(arg, argumentNames);
}
private boolean isPositional(final RobotToken arg, final Collection<String> argumentNames) {
final String argument = arg.getText();
if (argument.contains("=")) {
final String name = Splitter.on('=').limit(2).splitToList(argument).get(0);
return !descriptor.supportsKwargs() && !argumentNames.contains(name);
} else if (arg.getTypes().contains(RobotTokenType.VARIABLES_DICTIONARY_DECLARATION)) {
return isNonCollectionVar(arg);
} else {
return true;
}
}
private boolean isNonCollectionVar(final RobotToken arg) {
final MappingResult extractedVars = new VariableExtractor().extract(arg, null);
return !extractedVars.isOnlyPossibleCollectionVariable();
}
private String getName(final RobotToken robotToken) {
return Splitter.on('=').limit(2).splitToList(robotToken.getText()).get(0);
}
private static String getRangesInfo(final Range<Integer> range) {
final int minArgs = range.lowerEndpoint();
if (!range.hasUpperBound()) {
return "at least " + minArgs + " " + toPluralIfNeeded("argument", minArgs);
} else if (range.lowerEndpoint().equals(range.upperEndpoint())) {
return minArgs + " " + toPluralIfNeeded("argument", minArgs);
} else {
final int maxArgs = range.upperEndpoint();
return "from " + minArgs + " to " + maxArgs + " arguments";
}
}
private static String toBeInProperForm(final int amount) {
return amount == 1 ? "is" : "are";
}
private static String toPluralIfNeeded(final String noun, final int amount) {
return amount == 1 ? noun : noun + "s";
}
private static class ArgumentsBinding<D, U> {
public void bind(final D key, final U val) {
defToUsageMapping.put(key, val);
usageToDefMapping.put(val, key);
}
public List<U> getDefinitionsMapping(final D arg) {
return defToUsageMapping.get(arg);
}
public List<D> getUsageMapping(final U arg) {
return usageToDefMapping.get(arg);
}
private final ArrayListMultimap<D, U> defToUsageMapping = ArrayListMultimap.create();
private final ArrayListMultimap<U, D> usageToDefMapping = ArrayListMultimap.create();
}
}