package org.smoothbuild.lang.function.def; import static org.smoothbuild.lang.function.base.Parameter.parametersToString; import static org.smoothbuild.lang.function.base.Parameters.parametersToNames; import static org.smoothbuild.lang.type.Conversions.canConvert; import static org.smoothbuild.lang.type.Types.allTypes; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import javax.inject.Inject; import org.smoothbuild.cli.Console; import org.smoothbuild.lang.expr.DefaultValueExpression; import org.smoothbuild.lang.expr.Expression; import org.smoothbuild.lang.expr.ImplicitConverter; import org.smoothbuild.lang.function.base.Function; import org.smoothbuild.lang.function.base.Parameter; import org.smoothbuild.lang.message.CodeLocation; import org.smoothbuild.lang.type.Type; import org.smoothbuild.lang.type.Types; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMultimap; public class ArgumentExpressionCreator { private final ImplicitConverter implicitConverter; @Inject public ArgumentExpressionCreator(ImplicitConverter implicitConverter) { this.implicitConverter = implicitConverter; } public List<Expression> createArgExprs(CodeLocation codeLocation, Console console, Function function, Collection<Argument> arguments) { ParametersPool parametersPool = new ParametersPool(function.parameters()); ImmutableList<Argument> namedArguments = Argument.filterNamed(arguments); detectDuplicatedAndUnknownArgumentNames(function, console, namedArguments); if (console.isErrorReported()) { return null; } Map<Parameter, Argument> argumentMap = new HashMap<>(); processNamedArguments(parametersPool, console, argumentMap, namedArguments); if (console.isErrorReported()) { return null; } processNamelessArguments(function, arguments, parametersPool, console, argumentMap, codeLocation); if (console.isErrorReported()) { return null; } Set<Parameter> missingRequiredParameters = parametersPool.allRequired(); if (missingRequiredParameters.size() != 0) { console.error(codeLocation, missingRequiredArgsMessage(function, argumentMap, missingRequiredParameters)); return null; } Map<String, Expression> argumentExpressions = convert(argumentMap); for (Parameter parameter : parametersPool.allOptional()) { if (parameter.type() == Types.NOTHING) { console.error(codeLocation, "Parameter '" + parameter.name() + "' has to be " + "assigned explicitly as type 'Nothing' doesn't have default value."); } else { Expression expression = new DefaultValueExpression(parameter.type(), codeLocation); argumentExpressions.put(parameter.name(), expression); } } if (console.isErrorReported()) { return null; } return sortAccordingToParametersOrder(argumentExpressions, function); } private static String missingRequiredArgsMessage(Function function, Map<Parameter, Argument> argumentMap, Set<Parameter> missingRequiredParameters) { return "Not all parameters required by " + function.name() + " function has been specified.\n" + "Missing required parameters:\n" + parametersToString(missingRequiredParameters) + "All correct 'parameters <- arguments' assignments:\n" + argumentMap.toString(); } private List<Expression> sortAccordingToParametersOrder( Map<String, Expression> argumentExpressions, Function function) { ImmutableList.Builder<Expression> builder = ImmutableList.builder(); for (Parameter parameter : function.parameters()) { builder.add(argumentExpressions.get(parameter.name())); } return builder.build(); } private static void detectDuplicatedAndUnknownArgumentNames(Function function, Console console, Collection<Argument> namedArguments) { Set<String> unusedNames = new HashSet<>(parametersToNames(function.parameters())); Set<String> usedNames = new HashSet<>(); for (Argument argument : namedArguments) { if (argument.hasName()) { String name = argument.name(); if (unusedNames.contains(name)) { unusedNames.remove(name); usedNames.add(name); } else if (usedNames.contains(name)) { console.error(argument.codeLocation(), "Argument '" + argument.name() + "' assigned twice."); } else { console.error(argument.codeLocation(), "Function " + function.name() + " has no parameter '" + argument.name() + "'."); } } } } private static void processNamedArguments(ParametersPool parametersPool, Console console, Map<Parameter, Argument> argumentMap, Collection<Argument> namedArguments) { for (Argument argument : namedArguments) { if (argument.hasName()) { String name = argument.name(); Parameter parameter = parametersPool.take(name); Type paramType = parameter.type(); if (!canConvert(argument.type(), paramType)) { console.error(argument.codeLocation(), "Type mismatch, cannot convert argument '" + argument.name() + "' of type '" + argument.type().name() + "' to '" + paramType .name() + "'."); } else { argumentMap.put(parameter, argument); } } } } private static void processNamelessArguments(Function function, Collection<Argument> arguments, ParametersPool parametersPool, Console console, Map<Parameter, Argument> argumentMap, CodeLocation codeLocation) { ImmutableMultimap<Type, Argument> namelessArgs = Argument.filterNameless(arguments); for (Type type : allTypes()) { Collection<Argument> availableArguments = namelessArgs.get(type); int argsSize = availableArguments.size(); if (0 < argsSize) { TypedParametersPool availableTypedParams = parametersPool.assignableFrom(type); if (argsSize == 1 && availableTypedParams.hasCandidate()) { Argument onlyArgument = availableArguments.iterator().next(); Parameter candidateParameter = availableTypedParams.candidate(); argumentMap.put(candidateParameter, onlyArgument); parametersPool.take(candidateParameter); } else { console.error(codeLocation, ambiguousAssignmentErrorMessage(function, argumentMap, availableArguments, availableTypedParams)); return; } } } } private static String ambiguousAssignmentErrorMessage(Function function, Map<Parameter, Argument> argumentMap, Collection<Argument> availableArguments, TypedParametersPool availableTypedParams) { String assignmentList = MapToString.toString(argumentMap); if (availableTypedParams.isEmpty()) { return "Can't find parameter(s) of proper type in " + function.name() + " function for some nameless argument(s):\n" + "List of assignments that were successfully detected so far is following:\n" + assignmentList + "List of arguments for which no parameter could be found is following:\n" + argsToList(availableArguments); } else { return "Can't decide unambiguously to which parameters in " + function.name() + " function some nameless arguments should be assigned:\n" + "List of assignments that were successfully detected is following:\n" + assignmentList + "List of nameless arguments that caused problems:\n" + argsToList(availableArguments) + "List of unassigned parameters of desired type is following:\n" + availableTypedParams.toFormattedString(); } } private static String argsToList(Collection<Argument> availableArguments) { List<Argument> arguments = Argument.NUMBER_ORDERING.sortedCopy(availableArguments); int typeLength = longestArgType(arguments); int nameLength = longestArgName(arguments); int numberLength = longestArgNumber(arguments); StringBuilder builder = new StringBuilder(); for (Argument argument : arguments) { builder.append(" " + argument.toPaddedString(typeLength, nameLength, numberLength) + "\n"); } return builder.toString(); } private static int longestArgType(List<Argument> arguments) { int result = 0; for (Argument argument : arguments) { result = Math.max(result, argument.type().name().length()); } return result; } private static int longestArgName(List<Argument> arguments) { int result = 0; for (Argument argument : arguments) { result = Math.max(result, argument.nameSanitized().length()); } return result; } private static int longestArgNumber(List<Argument> arguments) { int maxNumber = 0; for (Argument argument : arguments) { maxNumber = Math.max(maxNumber, argument.number()); } return Integer.toString(maxNumber).length(); } private Map<String, Expression> convert(Map<Parameter, Argument> paramToArgMap) { Map<String, Expression> map = new HashMap<>(); for (Map.Entry<Parameter, Argument> entry : paramToArgMap.entrySet()) { Parameter parameter = entry.getKey(); Argument argument = entry.getValue(); Expression expression = implicitConverter.apply(parameter.type(), argument.expression()); map.put(parameter.name(), expression); } return map; } }