diff --git a/src/main/java/ch/njol/skript/doc/Documentation.java b/src/main/java/ch/njol/skript/doc/Documentation.java index 850f56b0bb6..787af40e330 100644 --- a/src/main/java/ch/njol/skript/doc/Documentation.java +++ b/src/main/java/ch/njol/skript/doc/Documentation.java @@ -9,7 +9,6 @@ import org.skriptlang.skript.common.function.DefaultFunction; import ch.njol.skript.lang.function.Functions; import ch.njol.skript.lang.function.JavaFunction; -import ch.njol.skript.lang.function.Parameter; import ch.njol.skript.registrations.Classes; import ch.njol.skript.util.Utils; import ch.njol.util.NonNullPair; @@ -18,6 +17,7 @@ import ch.njol.util.coll.iterator.IteratorIterable; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.common.function.Parameter; import java.io.*; import java.util.ArrayList; @@ -389,8 +389,8 @@ private static void insertFunction(PrintWriter pw, ch.njol.skript.lang.function. } StringBuilder params = new StringBuilder(); - for (Parameter p : func.getParameters()) { - if (params.length() != 0) + for (Parameter p : func.getSignature().parameters().values()) { + if (!params.isEmpty()) params.append(", "); params.append(p.toString()); } diff --git a/src/main/java/ch/njol/skript/lang/SkriptParser.java b/src/main/java/ch/njol/skript/lang/SkriptParser.java index a8426bd55d1..26b2ad528b2 100644 --- a/src/main/java/ch/njol/skript/lang/SkriptParser.java +++ b/src/main/java/ch/njol/skript/lang/SkriptParser.java @@ -15,7 +15,6 @@ import ch.njol.skript.lang.function.FunctionReference; import ch.njol.skript.lang.function.FunctionRegistry; import ch.njol.skript.lang.function.Functions; -import ch.njol.skript.lang.function.Signature; import ch.njol.skript.lang.parser.DefaultValueData; import ch.njol.skript.lang.parser.ParseStackOverflowException; import ch.njol.skript.lang.parser.ParserInstance; @@ -46,26 +45,18 @@ import org.bukkit.plugin.java.JavaPlugin; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.common.function.FunctionReferenceParser; import org.skriptlang.skript.lang.converter.Converters; import org.skriptlang.skript.lang.experiment.ExperimentSet; import org.skriptlang.skript.lang.experiment.ExperimentalSyntax; -import org.skriptlang.skript.lang.script.Script; import org.skriptlang.skript.lang.script.ScriptWarning; import org.skriptlang.skript.registration.SyntaxInfo; import org.skriptlang.skript.registration.SyntaxRegistry; import java.lang.reflect.Array; -import java.util.ArrayList; -import java.util.Deque; -import java.util.EnumMap; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; -import java.util.Locale; -import java.util.Map; +import java.util.*; import java.util.Map.Entry; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Supplier; import java.util.regex.MatchResult; import java.util.regex.Matcher; @@ -491,11 +482,10 @@ private static boolean checkExperimentalSyntax(T eleme log.printError(); return null; } - FunctionReference functionReference = parseFunction(types); + org.skriptlang.skript.common.function.FunctionReference functionReference = parseFunctionReference(); if (functionReference != null) { log.printLog(); - //noinspection rawtypes - return new ExprFunctionCall(functionReference); + return new ExprFunctionCall<>(functionReference, types); } else if (log.hasError()) { log.printError(); return null; @@ -647,10 +637,9 @@ private static boolean checkExperimentalSyntax(T eleme } // If it wasn't variable, do same for function call - FunctionReference functionReference = parseFunction(types); + org.skriptlang.skript.common.function.FunctionReference functionReference = parseFunctionReference(); if (functionReference != null) { - - if (onlySingular && !functionReference.isSingle()) { + if (onlySingular && !functionReference.single()) { Skript.error("'" + expr + "' can only be a single " + Classes.toString(Stream.of(exprInfo.classes).map(classInfo -> classInfo.getName().toString()).toArray(), false) + ", not more."); @@ -659,7 +648,7 @@ private static boolean checkExperimentalSyntax(T eleme } log.printLog(); - return new ExprFunctionCall<>(functionReference); + return new ExprFunctionCall<>(functionReference, types); } else if (log.hasError()) { log.printError(); return null; @@ -879,12 +868,7 @@ private boolean checkAcceptedType(Class clazz, Class ... types) { private final static String MULTIPLE_AND_OR = "List has multiple 'and' or 'or', will default to 'and'. Use brackets if you want to define multiple lists."; private final static String MISSING_AND_OR = "List is missing 'and' or 'or', defaulting to 'and'"; - private boolean suppressMissingAndOrWarnings = SkriptConfig.disableMissingAndOrWarnings.value(); - - private SkriptParser suppressMissingAndOrWarnings() { - suppressMissingAndOrWarnings = true; - return this; - } + private final boolean suppressMissingAndOrWarnings = SkriptConfig.disableMissingAndOrWarnings.value(); @SafeVarargs public final @Nullable Expression parseExpression(Class... types) { @@ -1157,11 +1141,28 @@ private record OrderedExprInfo(ExprInfo[] infos) { } * Function parsing */ + + /** + * Attempts to parse {@link SkriptParser#expr} as a function reference. + * + * @param The return type of the function. + * @return A {@link FunctionReference} if a function is found, or {@code null} if none is found. + */ + public org.skriptlang.skript.common.function.FunctionReference parseFunctionReference() { + if (context != ParseContext.DEFAULT && context != ParseContext.EVENT) { + return null; + } + + return new FunctionReferenceParser(context, flags).parseFunctionReference(expr); + } + private final static Pattern FUNCTION_CALL_PATTERN = Pattern.compile("(" + Functions.functionNamePattern + ")\\((.*)\\)"); /** - * @param types The required return type or null if it is not used (e.g. when calling a void function) - * @return The parsed function, or null if the given expression is not a function call or is an invalid function call (check for an error to differentiate these two) + * Attempts to parse {@link SkriptParser#expr} as a function reference. + * + * @param The return type of the function. + * @return A {@link FunctionReference} if a function is found, or {@code null} if none is found. */ @SuppressWarnings("unchecked") public @Nullable FunctionReference parseFunction(@Nullable Class... types) { @@ -1192,8 +1193,7 @@ private record OrderedExprInfo(ExprInfo[] infos) { } return null; } - SkriptParser skriptParser = new SkriptParser(args, flags | PARSE_LITERALS, context) - .suppressMissingAndOrWarnings(); + SkriptParser skriptParser = new SkriptParser(args, flags | PARSE_LITERALS, context); Expression[] params = args.isEmpty() ? new Expression[0] : null; String namespace = null; @@ -1235,7 +1235,7 @@ record SignatureData(ClassInfo classInfo, boolean plural) { } boolean trySinglePlural = false; for (var signature : signatures) { trySingle |= signature.getMinParameters() == 1 || signature.getMaxParameters() == 1; - trySinglePlural |= trySingle && !signature.getParameter(0).isSingleValue(); + trySinglePlural |= trySingle && !signature.getParameter(0).single(); for (int i = 0; i < signature.getMaxParameters(); i++) { if (signatureDatas.size() <= i) { signatureDatas.add(new ArrayList<>()); @@ -1564,22 +1564,24 @@ public static int next(String expr, int startIndex, ParseContext context) { return startIndex + 1; int index; - switch (expr.charAt(startIndex)) { - case '"': + return switch (expr.charAt(startIndex)) { + case '"' -> { index = nextQuote(expr, startIndex + 1); - return index < 0 ? -1 : index + 1; - case '{': + yield index < 0 ? -1 : index + 1; + } + case '{' -> { index = VariableString.nextVariableBracket(expr, startIndex + 1); - return index < 0 ? -1 : index + 1; - case '(': + yield index < 0 ? -1 : index + 1; + } + case '(' -> { for (index = startIndex + 1; index >= 0 && index < exprLength; index = next(expr, index, context)) { if (expr.charAt(index) == ')') - return index + 1; + yield index + 1; } - return -1; - default: - return startIndex + 1; - } + yield -1; + } + default -> startIndex + 1; + }; } /** @@ -1784,18 +1786,4 @@ private static ParserInstance getParser() { ParserInstance.registerData(DefaultValueData.class, DefaultValueData::new); } - /** - * @deprecated due to bad naming conventions, - * use {@link #LIST_SPLIT_PATTERN} instead. - */ - @Deprecated(since = "2.7.0", forRemoval = true) - public final static Pattern listSplitPattern = LIST_SPLIT_PATTERN; - - /** - * @deprecated due to bad naming conventions, - * use {@link #WILDCARD} instead. - */ - @Deprecated(since = "2.8.0", forRemoval = true) - public final static String wildcard = WILDCARD; - } diff --git a/src/main/java/ch/njol/skript/lang/function/DynamicFunctionReference.java b/src/main/java/ch/njol/skript/lang/function/DynamicFunctionReference.java index 4fc4d553c2f..cbd4ded6d84 100644 --- a/src/main/java/ch/njol/skript/lang/function/DynamicFunctionReference.java +++ b/src/main/java/ch/njol/skript/lang/function/DynamicFunctionReference.java @@ -10,6 +10,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.UnknownNullability; +import org.skriptlang.skript.common.function.Parameter; import org.skriptlang.skript.lang.script.Script; import org.skriptlang.skript.util.Executable; import org.skriptlang.skript.util.Validated; @@ -44,7 +45,7 @@ public DynamicFunctionReference(Function function) { this.function = new WeakReference<>(function); this.name = function.getName(); this.signature = function.getSignature(); - @Nullable File file = ScriptLoader.getScriptFromName(signature.script); + @Nullable File file = ScriptLoader.getScriptFromName(signature.namespace()); this.source = file != null ? ScriptLoader.getScript(file) : null; } @@ -69,7 +70,7 @@ public DynamicFunctionReference(@NotNull String name, @Nullable Script source) { this.function = new WeakReference<>(function); if (resolved) { this.signature = function.getSignature(); - @Nullable File file = ScriptLoader.getScriptFromName(signature.script); + @Nullable File file = ScriptLoader.getScriptFromName(signature.namespace()); this.source = file != null ? ScriptLoader.getScript(file) : null; } else { this.signature = null; @@ -90,8 +91,8 @@ public DynamicFunctionReference(@NotNull String name, @Nullable Script source) { public boolean isSingle(Expression... arguments) { if (!resolved) return true; - return signature.contract != null - ? signature.contract.isSingle(arguments) + return signature.getContract() != null + ? signature.getContract().isSingle(arguments) : signature.isSingle(); } @@ -99,8 +100,8 @@ public boolean isSingle(Expression... arguments) { public @Nullable Class getReturnType(Expression... arguments) { if (!resolved) return Object.class; - if (signature.contract != null) - return signature.contract.getReturnType(arguments); + if (signature.getContract() != null) + return signature.getContract().getReturnType(arguments); Function function = this.function.get(); if (function != null && function.getReturnType() != null) return function.getReturnType().getC(); @@ -162,7 +163,7 @@ public String toString() { this.checkedInputs.put(input, null); // failure case if (signature == null) return null; - boolean varArgs = signature.getMaxParameters() == 1 && !signature.getParameter(0).single; + boolean varArgs = signature.getMaxParameters() == 1 && !signature.parameters().firstEntry().getValue().single(); Expression[] parameters = input.parameters(); // Too many parameters if (parameters.length > signature.getMaxParameters() && !varArgs) @@ -174,12 +175,19 @@ else if (parameters.length < signature.getMinParameters()) // Check parameter types for (int i = 0; i < parameters.length; i++) { - Parameter parameter = signature.parameters[varArgs ? 0 : i]; - //noinspection unchecked - Expression expression = parameters[i].getConvertedExpression(parameter.type()); + Parameter parameter = signature.parameters().values().toArray(new Parameter[0])[varArgs ? 0 : i]; + + Class target; + if (parameter.type().isArray()) { + target = parameter.type().componentType(); + } else { + target = parameter.type(); + } + + Expression expression = parameters[i].getConvertedExpression(target); if (expression == null) { return null; - } else if (parameter.single && !expression.isSingle()) { + } else if (parameter.single() && !expression.isSingle()) { return null; } checked[i] = expression; diff --git a/src/main/java/ch/njol/skript/lang/function/EffFunctionCall.java b/src/main/java/ch/njol/skript/lang/function/EffFunctionCall.java index 10dd821b017..0a4c2939e2a 100644 --- a/src/main/java/ch/njol/skript/lang/function/EffFunctionCall.java +++ b/src/main/java/ch/njol/skript/lang/function/EffFunctionCall.java @@ -1,49 +1,46 @@ package ch.njol.skript.lang.function; -import org.bukkit.event.Event; -import org.jetbrains.annotations.Nullable; - import ch.njol.skript.lang.Effect; import ch.njol.skript.lang.Expression; import ch.njol.skript.lang.ParseContext; import ch.njol.skript.lang.SkriptParser; import ch.njol.skript.lang.SkriptParser.ParseResult; import ch.njol.util.Kleenean; +import org.bukkit.event.Event; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.common.function.FunctionReference; -/** - * @author Peter Güttinger - */ public class EffFunctionCall extends Effect { - - private final FunctionReference function; - - public EffFunctionCall(final FunctionReference function) { - this.function = function; + + private final FunctionReference reference; + + public EffFunctionCall(FunctionReference reference) { + this.reference = reference; } - - @Nullable + public static EffFunctionCall parse(final String line) { - final FunctionReference function = new SkriptParser(line, SkriptParser.ALL_FLAGS, ParseContext.DEFAULT).parseFunction((Class[]) null); + FunctionReference function = new SkriptParser(line, SkriptParser.ALL_FLAGS, ParseContext.DEFAULT).parseFunctionReference(); if (function != null) return new EffFunctionCall(function); return null; } - + @Override protected void execute(final Event event) { - function.execute(event); - function.resetReturnValue(); // Function might have return value that we're ignoring + reference.execute(event); + if (reference.function() != null) + reference.function().resetReturnValue(); // Function might have return value that we're ignoring } - + @Override - public String toString(@Nullable final Event event, final boolean debug) { - return function.toString(event, debug); + public String toString(@Nullable Event event, boolean debug) { + return reference.toString(event, debug); } - + @Override - public boolean init(final Expression[] exprs, final int matchedPattern, final Kleenean isDelayed, final ParseResult parseResult) { + public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { assert false; return false; } - + } diff --git a/src/main/java/ch/njol/skript/lang/function/ExprFunctionCall.java b/src/main/java/ch/njol/skript/lang/function/ExprFunctionCall.java index 9ba3c82798e..05fe30f8c5b 100644 --- a/src/main/java/ch/njol/skript/lang/function/ExprFunctionCall.java +++ b/src/main/java/ch/njol/skript/lang/function/ExprFunctionCall.java @@ -2,8 +2,6 @@ import ch.njol.skript.lang.Expression; import ch.njol.skript.lang.KeyProviderExpression; -import ch.njol.skript.lang.KeyedValue; -import ch.njol.skript.lang.KeyedValue.UnzippedKeyValues; import ch.njol.skript.lang.SkriptParser.ParseResult; import ch.njol.skript.lang.util.SimpleExpression; import ch.njol.skript.util.Utils; @@ -13,31 +11,37 @@ import org.bukkit.event.Event; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.common.function.FunctionReference; import org.skriptlang.skript.lang.converter.Converters; import java.lang.reflect.Array; -import java.util.*; +import java.util.Arrays; +import java.util.Map; +import java.util.WeakHashMap; public class ExprFunctionCall extends SimpleExpression implements KeyProviderExpression { - private final FunctionReference function; + private final FunctionReference reference; private final Class[] returnTypes; private final Class returnType; private final Map cache = new WeakHashMap<>(); - public ExprFunctionCall(FunctionReference function) { - this(function, function.returnTypes); - } - @SuppressWarnings("unchecked") - public ExprFunctionCall(FunctionReference function, Class[] expectedReturnTypes) { - this.function = function; - Class functionReturnType = function.getReturnType(); - assert functionReturnType != null; - if (CollectionUtils.containsSuperclass(expectedReturnTypes, functionReturnType)) { + public ExprFunctionCall(FunctionReference reference, Class[] expectedReturnTypes) { + this.reference = reference; + + Class functionReturnType = reference.signature().returnType(); + Class returnType; + if (functionReturnType != null && functionReturnType.isArray()) { + returnType = functionReturnType.componentType(); + } else { + returnType = functionReturnType; + } + + if (CollectionUtils.containsSuperclass(expectedReturnTypes, returnType)) { // Function returns expected type already - this.returnTypes = new Class[] {functionReturnType}; - this.returnType = (Class) functionReturnType; + this.returnTypes = new Class[] {returnType}; + this.returnType = (Class) returnType; } else { // Return value needs to be converted this.returnTypes = expectedReturnTypes; @@ -47,9 +51,15 @@ public ExprFunctionCall(FunctionReference function, Class[] expe @Override protected T @Nullable [] get(Event event) { - Object[] values = function.execute(event); - String[] keys = function.returnedKeys(); - function.resetReturnValue(); + Object[] values; + if (reference.single()) { + values = new Object[] { reference.execute(event) }; + } else { + values = (Object[]) reference.execute(event); + } + + String[] keys = reference.function().returnedKeys(); + reference.function().resetReturnValue(); //noinspection unchecked T[] convertedValues = (T[]) Array.newInstance(returnType, values != null ? values.length : 0); @@ -90,15 +100,23 @@ public boolean areKeysRecommended() { public @Nullable Expression getConvertedExpression(Class... to) { if (CollectionUtils.containsSuperclass(to, getReturnType())) return (Expression) this; - assert function.getReturnType() != null; - if (Converters.converterExists(function.getReturnType(), to)) - return new ExprFunctionCall<>(function, to); + + Class returns = reference.signature().returnType(); + Class converterType; + if (returns != null && returns.isArray()) { + converterType = returns.componentType(); + } else { + converterType = returns; + } + + if (Converters.converterExists(converterType, to)) + return new ExprFunctionCall<>(reference, to); return null; } @Override public boolean isSingle() { - return function.isSingle(); + return reference.single(); } @Override @@ -118,7 +136,7 @@ public boolean isLoopOf(String input) { @Override public String toString(@Nullable Event event, boolean debug) { - return function.toString(event, debug); + return reference.toString(event, debug); } @Override diff --git a/src/main/java/ch/njol/skript/lang/function/Function.java b/src/main/java/ch/njol/skript/lang/function/Function.java index 3734f1e925e..ffdb0dcc419 100644 --- a/src/main/java/ch/njol/skript/lang/function/Function.java +++ b/src/main/java/ch/njol/skript/lang/function/Function.java @@ -2,6 +2,7 @@ import ch.njol.skript.SkriptConfig; import ch.njol.skript.classes.ClassInfo; +import ch.njol.skript.lang.Expression; import ch.njol.skript.lang.KeyProviderExpression; import ch.njol.skript.lang.KeyedValue; import ch.njol.util.coll.CollectionUtils; @@ -9,9 +10,13 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.skriptlang.skript.common.function.DefaultFunction; +import org.skriptlang.skript.common.function.FunctionArguments; import org.skriptlang.skript.common.function.Parameter.Modifier; +import org.skriptlang.skript.common.function.ScriptParameter; import java.util.Arrays; +import java.util.Objects; +import java.util.SequencedMap; /** * Functions can be called using arguments. @@ -47,32 +52,47 @@ public String getName() { return sign.getName(); } - public Parameter[] getParameters() { + /** + * @deprecated Use {@link Signature#parameters()} instead. + */ + @Deprecated(forRemoval = true, since = "INSERT VERSION") + public org.skriptlang.skript.common.function.Parameter[] getParameters() { return sign.getParameters(); } - @SuppressWarnings("null") - public Parameter getParameter(int index) { - return getParameters()[index]; + /** + * @deprecated Use {@link Signature#getParameter(String)}} instead. + */ + @Deprecated(forRemoval = true, since = "INSERT VERSION") + public org.skriptlang.skript.common.function.Parameter getParameter(int index) { + return sign.getParameter(index); } public boolean isSingle() { return sign.isSingle(); } + /** + * @deprecated Use {@link #type()} instead. + */ + @Deprecated(forRemoval = true, since = "INSERT VERSION") public @Nullable ClassInfo getReturnType() { return sign.getReturnType(); } + /** + * @return The return type of this signature. Returns null for no return type. + */ + public Class type() { + return sign.returnType(); + } + // FIXME what happens with a delay in a function? /** - * Executes this function with given parameter. - * @param params Function parameters. Must contain at least - * {@link Signature#getMinParameters()} elements and at most - * {@link Signature#getMaxParameters()} elements. - * @return The result(s) of this function + * @deprecated Use {@link #execute(FunctionEvent, FunctionArguments)} instead. */ + @Deprecated(forRemoval = true, since = "INSERT VERSION") public final T @Nullable [] execute(Object[][] params) { FunctionEvent event = new FunctionEvent<>(this); @@ -82,38 +102,44 @@ public boolean isSingle() { Bukkit.getPluginManager().callEvent(event); // Parameters taken by the function. - Parameter[] parameters = sign.getParameters(); + SequencedMap> parameters = sign.parameters(); - if (params.length > parameters.length) { + if (params.length > parameters.size()) { // Too many parameters, should have failed to parse assert false : params.length; return null; } // If given less that max amount of parameters, pad remaining with nulls - Object[][] parameterValues = params.length < parameters.length ? Arrays.copyOf(params, parameters.length) : params; + Object[][] parameterValues = params.length < parameters.size() ? Arrays.copyOf(params, parameters.size()) : params; + int i = 0; // Execute parameters or default value expressions - for (int i = 0; i < parameters.length; i++) { - Parameter parameter = parameters[i]; + for (org.skriptlang.skript.common.function.Parameter parameter : parameters.values()) { Object[] parameterValue = parameter.hasModifier(Modifier.KEYED) ? convertToKeyed(parameterValues[i]) : parameterValues[i]; + Expression def; + if (parameter instanceof Parameter script) { + def = script.def; + } else if (parameter instanceof ScriptParameter script) { + def = script.defaultValue(); + } else { + def = null; + } + // see https://github.com/SkriptLang/Skript/pull/8135 - if ((parameterValues[i] == null || parameterValues[i].length == 0) - && parameter.keyed - && parameter.def != null - ) { - Object[] defaultValue = parameter.def.getArray(event); + if ((parameterValues[i] == null || parameterValues[i].length == 0) && parameter.hasModifier(Modifier.KEYED) && def != null) { + Object[] defaultValue = def.getArray(event); if (defaultValue.length == 1) { parameterValue = KeyedValue.zip(defaultValue, null); } else { parameterValue = defaultValue; } - } else if (!(this instanceof DefaultFunction) && parameterValue == null) { // Go for default value - assert parameter.def != null; // Should've been parse error - Object[] defaultValue = parameter.def.getArray(event); - if (parameter.hasModifier(Modifier.KEYED) && KeyProviderExpression.areKeysRecommended(parameter.def)) { - String[] keys = ((KeyProviderExpression) parameter.def).getArrayKeys(event); + } else if (parameterValue == null) { // Go for default value + assert def != null; // Should've been parse error + Object[] defaultValue = def.getArray(event); + if (parameter.hasModifier(Modifier.KEYED) && KeyProviderExpression.areKeysRecommended(def)) { + String[] keys = ((KeyProviderExpression) def).getArrayKeys(event); parameterValue = KeyedValue.zip(defaultValue, keys); } else { parameterValue = defaultValue; @@ -126,9 +152,10 @@ public boolean isSingle() { * really have a concept of nulls, it was changed. The config * option may be removed in future. */ - if (!(this instanceof DefaultFunction) && !executeWithNulls && parameterValue.length == 0) + if (!(this instanceof DefaultFunction) && !executeWithNulls && parameterValue != null && parameterValue.length == 0) return null; parameterValues[i] = parameterValue; + i++; } // Execute function contents @@ -157,15 +184,9 @@ public boolean isSingle() { } /** - * Executes this function with given parameters. Usually, using - * {@link #execute(Object[][])} is better; it handles optional and keyed arguments - * and function event creation automatically. - * @param event Associated function event. This is usually created by Skript. - * @param params Function parameters. - * There must be {@link Signature#getMaxParameters()} amount of them, and - * you need to manually handle default values. - * @return Function return value(s). + * @deprecated Use {@link #execute(FunctionEvent, FunctionArguments)} instead. */ + @Deprecated(since = "INSERT VERSION", forRemoval = true) public abstract T @Nullable [] execute(FunctionEvent event, Object[][] params); /** @@ -185,7 +206,7 @@ public boolean isSingle() { @Override public String toString() { - return (sign.local ? "local " : "") + "function " + sign.getName(); + return (sign.isLocal() ? "local " : "") + "function " + sign.getName(); } } diff --git a/src/main/java/ch/njol/skript/lang/function/FunctionEvent.java b/src/main/java/ch/njol/skript/lang/function/FunctionEvent.java index 6e17b1410d1..a4de260d8d4 100644 --- a/src/main/java/ch/njol/skript/lang/function/FunctionEvent.java +++ b/src/main/java/ch/njol/skript/lang/function/FunctionEvent.java @@ -13,7 +13,11 @@ public final class FunctionEvent extends Event { public FunctionEvent(Function function) { this.function = function; } - + + public FunctionEvent(org.skriptlang.skript.common.function.Function function) { + this.function = (Function) function; + } + public Function getFunction() { return function; } diff --git a/src/main/java/ch/njol/skript/lang/function/FunctionReference.java b/src/main/java/ch/njol/skript/lang/function/FunctionReference.java index 478d2cd0bac..631f18afb33 100644 --- a/src/main/java/ch/njol/skript/lang/function/FunctionReference.java +++ b/src/main/java/ch/njol/skript/lang/function/FunctionReference.java @@ -4,10 +4,12 @@ import ch.njol.skript.SkriptAPIException; import ch.njol.skript.classes.ClassInfo; import ch.njol.skript.config.Node; -import ch.njol.skript.lang.*; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.KeyProviderExpression; +import ch.njol.skript.lang.KeyedValue; +import ch.njol.skript.lang.SkriptParser; import ch.njol.skript.lang.function.FunctionRegistry.Retrieval; import ch.njol.skript.lang.function.FunctionRegistry.RetrievalResult; -import ch.njol.skript.lang.parser.ParserInstance; import ch.njol.skript.log.RetainingLogHandler; import ch.njol.skript.log.SkriptLogger; import ch.njol.skript.registrations.Classes; @@ -16,16 +18,20 @@ import ch.njol.util.StringUtils; import org.bukkit.event.Event; import org.jetbrains.annotations.Nullable; -import org.skriptlang.skript.lang.converter.Converters; +import org.skriptlang.skript.common.function.FunctionReference.Argument; +import org.skriptlang.skript.common.function.FunctionReference.ArgumentType; +import org.skriptlang.skript.common.function.Parameter; import org.skriptlang.skript.common.function.Parameter.Modifier; +import org.skriptlang.skript.lang.converter.Converters; import org.skriptlang.skript.util.Executable; import java.util.*; import java.util.stream.Collectors; /** - * Reference to a {@link Function Skript function}. + * @deprecated Use {@link org.skriptlang.skript.common.function.FunctionReference} instead. */ +@Deprecated(forRemoval = true, since = "INSERT VERSION") public class FunctionReference implements Contract, Executable { private static final String AMBIGUOUS_ERROR = @@ -160,8 +166,8 @@ public boolean validateFunction(boolean first) { // Validate that return types are what caller expects they are Class[] returnTypes = this.returnTypes; if (returnTypes != null) { - ClassInfo rt = sign.returnType; - if (rt == null) { + Class returnType = sign.returnType(); + if (returnType == null) { if (first) { Skript.error("The function '" + stringified + "' doesn't return any value."); } else { @@ -171,9 +177,10 @@ public boolean validateFunction(boolean first) { } return false; } - if (!Converters.converterExists(rt.getC(), returnTypes)) { + + if (!Converters.converterExists(returnType, returnTypes)) { if (first) { - Skript.error("The returned value of the function '" + stringified + "', " + sign.returnType + ", is " + SkriptParser.notOfType(returnTypes) + "."); + Skript.error("The returned value of the function '" + stringified + "', " + returnType + ", is " + SkriptParser.notOfType(returnTypes) + "."); } else { Skript.error("The function '" + stringified + "' was redefined with a different, incompatible return type, but is still used in other script(s)." + " These will continue to use the old version of the function until Skript restarts."); @@ -182,8 +189,8 @@ public boolean validateFunction(boolean first) { return false; } if (first) { - single = sign.single; - } else if (single && !sign.single) { + single = sign.isSingle(); + } else if (single && !sign.isSingle()) { Skript.error("The function '" + functionName + "' was redefined with a different, incompatible return type, but is still used in other script(s)." + " These will continue to use the old version of the function until Skript restarts."); function = previousFunction; @@ -192,7 +199,7 @@ public boolean validateFunction(boolean first) { } // Validate parameter count - singleListParam = sign.getMaxParameters() == 1 && !sign.getParameter(0).single; + singleListParam = sign.getMaxParameters() == 1 && !sign.parameters().entrySet().iterator().next().getValue().single(); if (!singleListParam) { // Check that parameter count is within allowed range // Too many parameters if (parameters.length > sign.getMaxParameters()) { @@ -229,17 +236,24 @@ public boolean validateFunction(boolean first) { // Check parameter types for (int i = 0; i < parameters.length; i++) { - Parameter p = sign.parameters[singleListParam ? 0 : i]; + Parameter parameter = sign.parameters().values().toArray(new Parameter[0])[singleListParam ? 0 : i]; RetainingLogHandler log = SkriptLogger.startRetainingLog(); try { + Class target; + if (parameter.type().isArray()) { + target = parameter.type().componentType(); + } else { + target = parameter.type(); + } + //noinspection unchecked - Expression e = parameters[i].getConvertedExpression(p.type()); - if (e == null) { + Expression expr = parameters[i].getConvertedExpression(target); + if (expr == null) { if (first) { if (LiteralUtils.hasUnparsedLiteral(parameters[i])) { Skript.error("Can't understand this expression: " + parameters[i].toString()); } else { - String type = Classes.toString(getClassInfo(p.type())); + String type = Classes.toString(getClassInfo(target)); Skript.error("The " + StringUtils.fancyOrderNumber(i + 1) + " argument given to the function '" + stringified + "' is not of the required type " + type + "." + " Check the correct order of the arguments and put lists into parentheses if appropriate (e.g. 'give(player, (iron ore and gold ore))')." @@ -251,7 +265,7 @@ public boolean validateFunction(boolean first) { function = previousFunction; } return false; - } else if (p.single && !e.isSingle()) { + } else if (parameter.single() && !expr.isSingle()) { if (first) { Skript.error("The " + StringUtils.fancyOrderNumber(i + 1) + " argument given to the function '" + functionName + "' is plural, " + "but a single argument was expected"); @@ -262,7 +276,7 @@ public boolean validateFunction(boolean first) { } return false; } - parameters[i] = e; + parameters[i] = expr; } finally { log.printLog(); } @@ -270,7 +284,13 @@ public boolean validateFunction(boolean first) { //noinspection unchecked signature = (Signature) sign; - sign.calls.add(this); + + //noinspection unchecked + Argument>[] stream = (Argument>[]) Arrays.stream(parameters) + .map(it -> new Argument<>(ArgumentType.UNNAMED, null, it)) + .toArray(Argument[]::new); + + sign.calls().add(new org.skriptlang.skript.common.function.FunctionReference<>(script, functionName, signature, stream)); Contract contract = sign.getContract(); if (contract != null) @@ -387,10 +407,14 @@ public boolean resetReturnValue() { // Prepare parameter values for calling Object[][] params = new Object[singleListParam ? 1 : parameters.length][]; if (singleListParam && parameters.length > 1) { // All parameters to one list - params[0] = evaluateSingleListParameter(parameters, event, function.getParameter(0).hasModifier(Modifier.KEYED)); + params[0] = evaluateSingleListParameter(parameters, event, function.getSignature().parameters() + .entrySet().iterator().next().getValue().modifiers().contains(Modifier.KEYED)); } else { // Use parameters in normal way + Parameter[] values = function.getSignature().parameters() + .values().toArray(new Parameter[0]); + for (int i = 0; i < parameters.length; i++) - params[i] = evaluateParameter(parameters[i], event, function.getParameter(i).hasModifier(Modifier.KEYED)); + params[i] = evaluateParameter(parameters[i], event, values[i].modifiers().contains(Modifier.KEYED)); } // Execute the function @@ -467,8 +491,7 @@ public boolean isSingle(Expression... arguments) { if (signature == null) throw new SkriptAPIException("Signature of function is null when return type is asked!"); - ClassInfo ret = signature.returnType; - return ret == null ? null : ret.getC(); + return signature.returnType(); } /** diff --git a/src/main/java/ch/njol/skript/lang/function/FunctionRegistry.java b/src/main/java/ch/njol/skript/lang/function/FunctionRegistry.java index 0a5296577fb..7d2d7e4cad8 100644 --- a/src/main/java/ch/njol/skript/lang/function/FunctionRegistry.java +++ b/src/main/java/ch/njol/skript/lang/function/FunctionRegistry.java @@ -4,16 +4,18 @@ import ch.njol.skript.SkriptAPIException; import com.google.common.base.MoreObjects; import com.google.common.base.Preconditions; -import com.google.common.collect.ImmutableSet; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Unmodifiable; +import org.skriptlang.skript.common.function.Parameter; +import org.skriptlang.skript.common.function.Parameter.Modifier; import org.skriptlang.skript.lang.converter.Converters; import org.skriptlang.skript.util.Registry; import java.util.*; import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Pattern; import java.util.stream.Collectors; /** @@ -40,7 +42,7 @@ public static FunctionRegistry getRegistry() { * The pattern for a valid function name. * Functions must start with a letter or underscore and can only contain letters, numbers, and underscores. */ - final static String FUNCTION_NAME_PATTERN = "[\\p{IsAlphabetic}_][\\p{IsAlphabetic}\\d_]*"; + final static Pattern FUNCTION_NAME_PATTERN = Pattern.compile("[A-z_][A-z_0-9]*"); /** * The namespace for registered global functions. @@ -148,7 +150,7 @@ public void register(@Nullable String namespace, @NotNull Function function) Skript.debug("Registering function '%s'", function.getName()); String name = function.getName(); - if (!name.matches(FUNCTION_NAME_PATTERN)) { + if (!FUNCTION_NAME_PATTERN.matcher(name).matches()) { throw new SkriptAPIException("Invalid function name '" + name + "'"); } @@ -212,7 +214,7 @@ private boolean signatureExists(@NotNull NamespaceIdentifier namespace, @NotNull * The result of attempting to retrieve a function. * Depending on the type, a {@link Retrieval} will feature different data. */ - enum RetrievalResult { + public enum RetrievalResult { /** * The specified function or signature has not been registered. @@ -258,7 +260,7 @@ enum RetrievalResult { * @param retrieved The function or signature that was found if {@code result} is {@code EXACT}. * @param conflictingArgs The conflicting arguments if {@code result} is {@code AMBIGUOUS}. */ - record Retrieval( + public record Retrieval( @NotNull RetrievalResult result, T retrieved, Class[][] conflictingArgs @@ -407,31 +409,24 @@ Retrieval> getExactSignature( public @Unmodifiable @NotNull Set> getSignatures(@Nullable String namespace, @NotNull String name) { Preconditions.checkNotNull(name, "name cannot be null"); - ImmutableSet.Builder> setBuilder = ImmutableSet.builder(); - - // obtain all global functions of "name" - Namespace globalNamespace = namespaces.get(GLOBAL_NAMESPACE); - Set globalIdentifiers = globalNamespace.identifiers.get(name); - if (globalIdentifiers != null) { - for (FunctionIdentifier identifier : globalIdentifiers) { - setBuilder.add(globalNamespace.signatures.get(identifier)); - } - } + Map> total = new HashMap<>(); // obtain all local functions of "name" if (namespace != null) { - Namespace localNamespace = namespaces.get(new NamespaceIdentifier(namespace)); - if (localNamespace != null) { - Set localIdentifiers = localNamespace.identifiers.get(name); - if (localIdentifiers != null) { - for (FunctionIdentifier identifier : localIdentifiers) { - setBuilder.add(localNamespace.signatures.get(identifier)); - } - } + Namespace local = namespaces.getOrDefault(new NamespaceIdentifier(namespace), new Namespace()); + + for (FunctionIdentifier identifier : local.identifiers.getOrDefault(name, Collections.emptySet())) { + total.putIfAbsent(identifier, local.signatures.get(identifier)); } } - return setBuilder.build(); + // obtain all global functions of "name" + Namespace global = namespaces.getOrDefault(GLOBAL_NAMESPACE, new Namespace()); + for (FunctionIdentifier identifier : global.identifiers.getOrDefault(name, Collections.emptySet())) { + total.putIfAbsent(identifier, global.signatures.get(identifier)); + } + + return Set.copyOf(total.values()); } /** @@ -505,10 +500,13 @@ private Retrieval> getSignature(@NotNull NamespaceIdentifier namesp && candidate.args.length == 1 && candidate.args[0].isArray()) { // if a function has single list value param, check all types - // make sure all types in the passed array are valid for the array parameter Class arrayType = candidate.args[0].componentType(); for (Class arrayArg : provided.args) { + if (arrayArg.isArray()) { + arrayArg = arrayArg.componentType(); + } + if (!Converters.converterExists(arrayArg, arrayType)) { continue candidates; } @@ -527,6 +525,13 @@ private Retrieval> getSignature(@NotNull NamespaceIdentifier namesp // if the types of the provided arguments do not match the candidate arguments, skip for (int i = 0; i < provided.args.length; i++) { // allows single passed values to still match array type in candidate (e.g. clamp) + Class providedType; + if (provided.args[i].isArray()) { + providedType = provided.args[i].componentType(); + } else { + providedType = provided.args[i]; + } + Class candidateType; if (candidate.args[i].isArray()) { candidateType = candidate.args[i].componentType(); @@ -540,7 +545,7 @@ private Retrieval> getSignature(@NotNull NamespaceIdentifier namesp continue candidates; } } else { - if (!Converters.converterExists(providedArg, candidateType)) { + if (!Converters.converterExists(providedType, candidateType)) { continue candidates; } } @@ -595,7 +600,7 @@ public void remove(@NotNull Signature signature) { Namespace namespace; if (signature.isLocal()) { - namespace = namespaces.get(new NamespaceIdentifier(signature.script)); + namespace = namespaces.get(new NamespaceIdentifier(signature.namespace())); } else { namespace = namespaces.get(GLOBAL_NAMESPACE); } @@ -707,22 +712,17 @@ static FunctionIdentifier of(@NotNull String name, boolean local, @NotNull Class static FunctionIdentifier of(@NotNull Signature signature) { Preconditions.checkNotNull(signature, "signature cannot be null"); - Parameter[] signatureParams = signature.parameters; + Parameter[] signatureParams = signature.parameters().values().toArray(new Parameter[0]); Class[] parameters = new Class[signatureParams.length]; int optionalArgs = 0; for (int i = 0; i < signatureParams.length; i++) { Parameter param = signatureParams[i]; - if (param.isOptional()) { + if (param.modifiers().contains(Modifier.OPTIONAL)) { optionalArgs++; } - Class type = param.type(); - if (param.isSingleValue()) { - parameters[i] = type; - } else { - parameters[i] = type.arrayType(); - } + parameters[i] = param.type(); } return new FunctionIdentifier(signature.getName(), signature.isLocal(), @@ -731,7 +731,7 @@ static FunctionIdentifier of(@NotNull Signature signature) { @Override public int hashCode() { - return Objects.hash(name, local, Arrays.hashCode(args)); + return Objects.hash(name, Arrays.hashCode(args)); } @Override @@ -748,10 +748,6 @@ public boolean equals(Object obj) { return false; } - if (local != other.local) { - return false; - } - for (int i = 0; i < args.length; i++) { if (args[i] != other.args[i]) { return false; diff --git a/src/main/java/ch/njol/skript/lang/function/Functions.java b/src/main/java/ch/njol/skript/lang/function/Functions.java index bcd3570a6e3..88ed4aed612 100644 --- a/src/main/java/ch/njol/skript/lang/function/Functions.java +++ b/src/main/java/ch/njol/skript/lang/function/Functions.java @@ -8,11 +8,12 @@ import ch.njol.skript.lang.function.FunctionRegistry.Retrieval; import ch.njol.skript.lang.function.FunctionRegistry.RetrievalResult; import ch.njol.skript.registrations.Classes; -import ch.njol.skript.util.Utils; -import ch.njol.util.NonNullPair; +import ch.njol.skript.structures.StructFunction; import ch.njol.util.StringUtils; import org.jetbrains.annotations.Nullable; import org.skriptlang.skript.common.function.DefaultFunction; +import org.skriptlang.skript.common.function.FunctionReference; +import org.skriptlang.skript.common.function.Parameter; import org.skriptlang.skript.lang.script.Script; import java.util.*; @@ -52,7 +53,7 @@ private Functions() {} */ private static final Map globalFunctions = new HashMap<>(); - static boolean callFunctionEvents = false; + public static boolean callFunctionEvents = false; /** * Registers a {@link DefaultFunction}. @@ -94,7 +95,7 @@ public static JavaFunction registerFunction(JavaFunction function) { return function; } - public final static String functionNamePattern = "[\\p{IsAlphabetic}_][\\p{IsAlphabetic}\\p{IsDigit}_]*"; + public final static String functionNamePattern = "[\\p{IsAlphabetic}_][\\p{IsAlphabetic}\\d_]*"; /** * Loads a script function from given node. @@ -105,7 +106,7 @@ public static JavaFunction registerFunction(JavaFunction function) { * @return Script function, or null if something went wrong. */ public static @Nullable Function loadFunction(Script script, SectionNode node, Signature signature) { - String name = signature.name; + String name = signature.getName(); Namespace namespace = getScriptNamespace(script.getConfig().getFileName()); if (namespace == null) { namespace = globalFunctions.get(name); @@ -113,12 +114,20 @@ public static JavaFunction registerFunction(JavaFunction function) { return null; // Probably duplicate signature; reported before } - Parameter[] params = signature.parameters; - ClassInfo c = signature.returnType; + Parameter[] params = signature.parameters().values().toArray(new Parameter[0]); - if (Skript.debug() || node.debug()) - Skript.debug((signature.local ? "local " : "") + "function " + name + "(" + StringUtils.join(params, ", ") + ")" + if (Skript.debug() || node.debug()) { + Class returnType = signature.returnType(); + ClassInfo c; + if (returnType != null && returnType.isArray()) { + c = Classes.getExactClassInfo(returnType.componentType()); + } else { + c = Classes.getExactClassInfo(returnType); + } + + Skript.debug((signature.isLocal() ? "local " : "") + "function " + name + "(" + StringUtils.join(params, ", ") + ")" + (c != null ? " :: " + (signature.isSingle() ? c.getName().getSingular() : c.getName().getPlural()) : "") + ":"); + } Function function; try { @@ -132,7 +141,7 @@ public static JavaFunction registerFunction(JavaFunction function) { return null; } - if (namespace.getFunction(signature.name) == null) { + if (namespace.getFunction(signature.getName()) == null) { namespace.addFunction(function); } @@ -145,41 +154,12 @@ public static JavaFunction registerFunction(JavaFunction function) { return function; } - /** - * Parses the signature from the given arguments. - * @param script Script file name (might be used for some checks). - * @param name The name of the function. - * @param args The parameters of the function. See {@link Parameter#parse(String)} - * @param returnType The return type of the function - * @param local If the signature of function is local. - * @return Parsed signature or null if something went wrong. - * @see Functions#registerSignature(Signature) + * @deprecated Use {@link StructFunction.FunctionParser#parse(String, String, String, String, boolean)} instead. */ + @Deprecated(forRemoval = true, since = "INSERT VERSION") public static @Nullable Signature parseSignature(String script, String name, String args, @Nullable String returnType, boolean local) { - List> parameters = Parameter.parse(args); - if (parameters == null) - return null; - - // Parse return type if one exists - ClassInfo returnClass; - boolean singleReturn; - if (returnType == null) { - returnClass = null; - singleReturn = false; // Ignored, nothing is returned - } else { - returnClass = Classes.getClassInfoFromUserInput(returnType); - NonNullPair p = Utils.getEnglishPlural(returnType); - singleReturn = !p.getSecond(); - if (returnClass == null) - returnClass = Classes.getClassInfoFromUserInput(p.getFirst()); - if (returnClass == null) { - Skript.error("Cannot recognise the type '" + returnType + "'"); - return null; - } - } - //noinspection unchecked - return new Signature<>(script, name, parameters.toArray(new Parameter[0]), local, (ClassInfo) returnClass, singleReturn, null); + return StructFunction.FunctionParser.parse(script, name, args, returnType, local); } /** @@ -190,21 +170,17 @@ public static JavaFunction registerFunction(JavaFunction function) { */ public static @Nullable Signature registerSignature(Signature signature) { Retrieval> existing; - Parameter[] parameters = signature.parameters; + Parameter[] parameters = signature.parameters().values().toArray(new Parameter[0]); - if (parameters.length == 1 && !parameters[0].isSingleValue()) { - existing = FunctionRegistry.getRegistry().getExactSignature(signature.script, signature.getName(), parameters[0].type().arrayType()); + if (parameters.length == 1 && !parameters[0].single()) { + existing = FunctionRegistry.getRegistry().getExactSignature(signature.namespace(), signature.getName(), parameters[0].type().arrayType()); } else { Class[] types = new Class[parameters.length]; for (int i = 0; i < parameters.length; i++) { - if (parameters[i].isSingleValue()) { - types[i] = parameters[i].type(); - } else { - types[i] = parameters[i].type().arrayType(); - } + types[i] = parameters[i].type(); } - existing = FunctionRegistry.getRegistry().getExactSignature(signature.script, signature.getName(), types); + existing = FunctionRegistry.getRegistry().getExactSignature(signature.namespace(), signature.getName(), types); } // if this function has already been registered, only allow it if one function is local and one is global. @@ -217,9 +193,9 @@ public static JavaFunction registerFunction(JavaFunction function) { } else { error.append("Function "); } - error.append("'%s' with the same argument types already exists".formatted(signature.getName())); - if (existing.retrieved().script != null) { - error.append(" in script '%s'.".formatted(existing.retrieved().script)); + error.append("%s with the same argument types already exists".formatted(signature.getName())); + if (existing.retrieved().namespace() != null) { + error.append(" in script '%s'.".formatted(existing.retrieved().namespace())); } else { error.append("."); } @@ -229,21 +205,21 @@ public static JavaFunction registerFunction(JavaFunction function) { return null; } - Namespace.Key namespaceKey = new Namespace.Key(Namespace.Origin.SCRIPT, signature.script); + Namespace.Key namespaceKey = new Namespace.Key(Namespace.Origin.SCRIPT, signature.namespace()); Namespace namespace = namespaces.computeIfAbsent(namespaceKey, k -> new Namespace()); - if (namespace.getSignature(signature.name) == null) { + if (namespace.getSignature(signature.getName()) == null) { namespace.addSignature(signature); } - if (!signature.local) - globalFunctions.put(signature.name, namespace); + if (!signature.isLocal()) + globalFunctions.put(signature.getName(), namespace); - if (signature.local) { - FunctionRegistry.getRegistry().register(signature.script, signature); + if (signature.isLocal()) { + FunctionRegistry.getRegistry().register(signature.namespace(), signature); } else { FunctionRegistry.getRegistry().register(null, signature); } - Skript.debug("Registered function signature: " + signature.name); + Skript.debug("Registered function signature: " + signature.getName()); return signature; } @@ -392,8 +368,8 @@ public static int clearFunctions(String script) { // Queue references to signatures we have for revalidation // Can't validate here, because other scripts might be loaded soon for (Signature sign : namespace.getSignatures()) { - for (FunctionReference ref : sign.calls) { - if (!script.equals(ref.script)) { + for (FunctionReference ref : sign.calls()) { + if (!script.equals(ref.namespace())) { toValidate.add(ref); } } @@ -408,7 +384,7 @@ public static void unregisterFunction(Signature signature) { while (namespaceIterator.hasNext()) { Namespace namespace = namespaceIterator.next(); if (namespace.removeSignature(signature)) { - if (!signature.local) + if (!signature.isLocal()) globalFunctions.remove(signature.getName()); // remove the namespace if it is empty @@ -419,15 +395,15 @@ public static void unregisterFunction(Signature signature) { } } - for (FunctionReference ref : signature.calls) { - if (signature.script != null && !signature.script.equals(ref.script)) + for (FunctionReference ref : signature.calls()) { + if (signature.namespace() != null && !signature.namespace().equals(ref.namespace())) toValidate.add(ref); } } public static void validateFunctions() { for (FunctionReference c : toValidate) - c.validateFunction(false); + c.validate(); toValidate.clear(); } diff --git a/src/main/java/ch/njol/skript/lang/function/JavaFunction.java b/src/main/java/ch/njol/skript/lang/function/JavaFunction.java index 89cb0c4297c..ae40f0ab273 100644 --- a/src/main/java/ch/njol/skript/lang/function/JavaFunction.java +++ b/src/main/java/ch/njol/skript/lang/function/JavaFunction.java @@ -2,14 +2,15 @@ import ch.njol.skript.classes.ClassInfo; import ch.njol.skript.doc.Documentable; +import ch.njol.skript.lang.Expression; import ch.njol.skript.util.Contract; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Unmodifiable; import org.skriptlang.skript.common.function.DefaultFunction; +import org.skriptlang.skript.common.function.FunctionArguments; -import java.util.Collection; import java.util.Collections; import java.util.List; @@ -46,6 +47,45 @@ public JavaFunction(String name, Parameter[] parameters, ClassInfo returnT @Override public abstract T @Nullable [] execute(FunctionEvent event, Object[][] params); + @Override + public final T execute(FunctionEvent event, FunctionArguments arguments) { + List> parameters = getSignature().parameters().values().stream().toList(); + + Object[][] params = new Object[parameters.size()][]; + for (int i = 0; i < parameters.size(); i++) { + Parameter parameter = (Parameter) parameters.get(i); + Object object = arguments.get(parameter.name()); + + if (object != null && object.getClass().isArray()) { + params[i] = (Object[]) object; + } else if (object == null) { + Expression defaultExpression = parameter.getDefaultExpression(); + + if (defaultExpression == null) { + return null; + } + + if (parameter.single()) { + params[i] = new Object[] { defaultExpression.getSingle(event) }; + } else { + params[i] = defaultExpression.getArray(event); + } + } else { + params[i] = new Object[] { object }; + } + } + + T[] execute = execute(event, params); + if (execute == null || execute.length == 0) { + return null; + } else if (execute.length == 1) { + return execute[0]; + } else { + //noinspection unchecked + return (T) execute; + } + } + @Override public @NotNull String @Nullable [] returnedKeys() { return returnedKeys; diff --git a/src/main/java/ch/njol/skript/lang/function/Namespace.java b/src/main/java/ch/njol/skript/lang/function/Namespace.java index 123428c7ce3..53ace1f0794 100644 --- a/src/main/java/ch/njol/skript/lang/function/Namespace.java +++ b/src/main/java/ch/njol/skript/lang/function/Namespace.java @@ -147,14 +147,14 @@ public Namespace() { } public void addSignature(Signature sign) { - Info info = new Info(sign.getName(), sign.local); + Info info = new Info(sign.getName(), sign.isLocal()); if (signatures.containsKey(info)) throw new IllegalArgumentException("function name already used"); signatures.put(info, sign); } public boolean removeSignature(Signature sign) { - Info info = new Info(sign.getName(), sign.local); + Info info = new Info(sign.getName(), sign.isLocal()); if (signatures.get(info) != sign) return false; signatures.remove(info); @@ -176,7 +176,7 @@ public Collection> getSignatures() { } public void addFunction(Function func) { - Info info = new Info(func.getName(), func.getSignature().local); + Info info = new Info(func.getName(), func.getSignature().isLocal()); assert signatures.containsKey(info) : "missing signature for function"; functions.put(info, func); } diff --git a/src/main/java/ch/njol/skript/lang/function/Parameter.java b/src/main/java/ch/njol/skript/lang/function/Parameter.java index 46ee53e6409..2d4d5ffa582 100644 --- a/src/main/java/ch/njol/skript/lang/function/Parameter.java +++ b/src/main/java/ch/njol/skript/lang/function/Parameter.java @@ -18,11 +18,17 @@ import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Unmodifiable; import org.skriptlang.skript.common.function.DefaultFunction; +import org.skriptlang.skript.common.function.ScriptParameter; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; +/** + * @deprecated Use {@link ScriptParameter} + * or {@link DefaultFunction.Builder#parameter(String, Class, Modifier...)} instead. + */ +@Deprecated(forRemoval = true, since = "INSERT VERSION") public final class Parameter implements org.skriptlang.skript.common.function.Parameter { public final static Pattern PARAM_PATTERN = Pattern.compile("^\\s*([^:(){}\",]+?)\\s*:\\s*([a-zA-Z ]+?)\\s*(?:\\s*=\\s*(.+))?\\s*$"); @@ -51,16 +57,15 @@ public final class Parameter implements org.skriptlang.skript.common.function */ final boolean single; + private final Set modifiers; + /** - * Whether this parameter takes in key-value pairs. - *
- * If this is true, a {@link ch.njol.skript.lang.KeyedValue} array containing key-value pairs will be passed to - * {@link Function#execute(FunctionEvent, Object[][])} rather than a value-only object array. + * @deprecated Use {@link org.skriptlang.skript.common.function.Parameter} + * or {@link DefaultFunction.Builder#parameter(String, Class, Modifier...)} + * instead. */ final boolean keyed; - private final Set modifiers; - /** * @deprecated Use {@link DefaultFunction.Builder#parameter(String, Class, Modifier...)} instead. */ @@ -70,7 +75,9 @@ public Parameter(String name, ClassInfo type, boolean single, @Nullable Expre } /** - * @deprecated Use {@link DefaultFunction.Builder#parameter(String, Class, Modifier...)} instead. + * @deprecated Use {@link org.skriptlang.skript.common.function.Parameter} + * or {@link DefaultFunction.Builder#parameter(String, Class, Modifier...)} + * instead. */ @Deprecated(since = "INSERT VERSION", forRemoval = true) public Parameter(String name, ClassInfo type, boolean single, @Nullable Expression def, boolean keyed) { @@ -90,7 +97,9 @@ public Parameter(String name, ClassInfo type, boolean single, @Nullable Expre } /** - * @deprecated Use {@link DefaultFunction.Builder#parameter(String, Class, Modifier...)} instead. + * @deprecated Use {@link org.skriptlang.skript.common.function.Parameter} + * or {@link DefaultFunction.Builder#parameter(String, Class, Modifier...)} + * instead. */ @Deprecated(since = "INSERT VERSION", forRemoval = true) public Parameter(String name, ClassInfo type, boolean single, @Nullable Expression def, boolean keyed, boolean optional) { @@ -135,12 +144,17 @@ public boolean isOptional() { } /** - * @return The type of this parameter as a {@link ClassInfo}. + * @deprecated Use {@link #type()} instead. */ + @Deprecated(forRemoval = true, since = "INSERT VERSION") public ClassInfo getType() { return type; } + /** + * @deprecated Use {@link ScriptParameter#parse(String, Class, String)}} instead. + */ + @Deprecated(forRemoval = true, since = "INSERT VERSION") public static @Nullable Parameter newInstance(String name, ClassInfo type, boolean single, @Nullable String def) { if (!Variable.isValidVariableName(name, true, false)) { Skript.error("A parameter's name must be a valid variable name."); @@ -177,11 +191,9 @@ public ClassInfo getType() { } /** - * Parses function parameters from a string. The string should look something like this: - *
"something: string, something else: number = 12"
- * @param args The string to parse. - * @return The parsed parameters + * @deprecated Use {@link ch.njol.skript.structures.StructFunction.FunctionParser#parse(String, String, String, String, boolean)} instead. */ + @Deprecated(forRemoval = true, since = "INSERT VERSION") public static @Nullable List> parse(String args) { List> params = new ArrayList<>(); boolean caseInsensitive = SkriptConfig.caseInsensitiveVariables.value(); @@ -238,10 +250,6 @@ public ClassInfo getType() { return params; } - /** - * @deprecated Use {@link #name()} instead. - */ - @Deprecated(forRemoval = true, since = "INSERT VERSION") public String getName() { return name; } @@ -291,7 +299,12 @@ public String toString(boolean debug) { @Override public @NotNull Class type() { - return type.getC(); + if (single) { + return type.getC(); + } else { + //noinspection unchecked + return (Class) type.getC().arrayType(); + } } @Override @@ -299,4 +312,8 @@ public String toString(boolean debug) { return Collections.unmodifiableSet(modifiers); } + @Override + public boolean single() { + return single; + } } diff --git a/src/main/java/ch/njol/skript/lang/function/ScriptFunction.java b/src/main/java/ch/njol/skript/lang/function/ScriptFunction.java index 0ac8328eda4..8626809110c 100644 --- a/src/main/java/ch/njol/skript/lang/function/ScriptFunction.java +++ b/src/main/java/ch/njol/skript/lang/function/ScriptFunction.java @@ -1,6 +1,5 @@ package ch.njol.skript.lang.function; -import ch.njol.skript.classes.ClassInfo; import ch.njol.skript.config.SectionNode; import ch.njol.skript.lang.*; import ch.njol.skript.lang.parser.ParserInstance; @@ -8,10 +7,14 @@ import ch.njol.skript.variables.HintManager; import ch.njol.skript.variables.Variables; import org.bukkit.event.Event; -import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import org.skriptlang.skript.lang.script.Script; +import org.skriptlang.skript.common.function.FunctionArguments; +import org.skriptlang.skript.common.function.Parameter; + +import java.util.LinkedHashMap; +import java.util.Map.Entry; +import java.util.SequencedMap; public class ScriptFunction extends Function implements ReturnHandler { @@ -21,14 +24,6 @@ public class ScriptFunction extends Function implements ReturnHandler { private T @Nullable [] returnValues; private String @Nullable [] returnKeys; - /** - * @deprecated use {@link ScriptFunction#ScriptFunction(Signature, SectionNode)} instead. - */ - @Deprecated(since = "2.9.0", forRemoval = true) - public ScriptFunction(Signature sign, Script script, SectionNode node) { - this(sign, node); - } - public ScriptFunction(Signature sign, SectionNode node) { super(sign); @@ -36,12 +31,16 @@ public ScriptFunction(Signature sign, SectionNode node) { HintManager hintManager = ParserInstance.get().getHintManager(); try { hintManager.enterScope(false); - for (Parameter parameter : sign.getParameters()) { + for (Parameter parameter : sign.parameters().values()) { String hintName = parameter.name(); - if (!parameter.isSingleValue()) { + if (!parameter.single()) { hintName += Variable.SEPARATOR + "*"; + assert parameter.type().isArray(); + hintManager.set(hintName, parameter.type().componentType()); + } else { + assert !parameter.type().isArray(); + hintManager.set(hintName, parameter.type()); } - hintManager.set(hintName, parameter.type()); } trigger = loadReturnableTrigger(node, "function " + sign.getName(), new SimpleEvent()); } finally { @@ -55,11 +54,14 @@ public ScriptFunction(Signature sign, SectionNode node) { // REM: use patterns, e.g. {_a%b%} is like "a.*", and thus subsequent {_axyz} may be set and of that type. @Override public T @Nullable [] execute(FunctionEvent event, Object[][] params) { - Parameter[] parameters = getSignature().getParameters(); - for (int i = 0; i < parameters.length; i++) { - Parameter parameter = parameters[i]; + SequencedMap> parameters = getSignature().parameters(); + + int i = 0; + for (Entry> entry : parameters.entrySet()) { + Parameter parameter = entry.getValue(); + Object[] val = params[i]; - if (parameter.single && val.length > 0) { + if (parameter.single() && val.length > 0) { Variables.setVariable(parameter.name(), val[0], event, true); } else { for (Object value : val) { @@ -67,27 +69,60 @@ public ScriptFunction(Signature sign, SectionNode node) { Variables.setVariable(parameter.name() + "::" + keyedValue.key(), keyedValue.value(), event, true); } } + i++; } trigger.execute(event); - ClassInfo returnType = getReturnType(); - return returnType != null ? returnValues : null; + return type() != null ? returnValues : null; } @Override - public @NotNull String @Nullable [] returnedKeys() { - return returnKeys; + public T execute(FunctionEvent event, FunctionArguments arguments) { + SequencedMap> parameters = getSignature().parameters(); + FunctionEvent newEvent = new FunctionEvent<>(this); + + for (String name : arguments.names()) { + Parameter parameter = parameters.get(name); + Object value = arguments.get(name); + + if (value == null) { + continue; + } + + if (parameter.single()) { + Variables.setVariable(name, value, newEvent, true); + } else { + if (value instanceof KeyedValue[] keyedValues) { + for (KeyedValue keyedValue : keyedValues) { + Variables.setVariable(name + "::" + keyedValue.key(), keyedValue.value(), newEvent, true); + } + } else { + int i = 0; + for (Object o : (Object[]) value) { + Variables.setVariable(name + "::" + i, o, newEvent, true); + i++; + } + } + } + } + + trigger.execute(newEvent); + + if (type() == null || returnValues == null || returnValues.length == 0) { + return null; + } + + if (returnValues.length == 1) { + return returnValues[0]; + } else { + //noinspection unchecked + return (T) returnValues; + } } - /** - * @deprecated Use {@link ScriptFunction#returnValues(Event, Expression)} instead. - */ - @Deprecated(since = "2.9.0", forRemoval = true) - @ApiStatus.Internal - public final void setReturnValue(@Nullable T[] values) { - assert !returnValueSet; - returnValueSet = true; - this.returnValues = values; + @Override + public @NotNull String @Nullable [] returnedKeys() { + return returnKeys; } @Override @@ -114,7 +149,16 @@ public final boolean isSingleReturnValue() { @Override public final @Nullable Class returnValueType() { - return getReturnType() != null ? getReturnType().getC() : null; + if (type() == null) { + return null; + } + + if (type().isArray()) { + //noinspection unchecked + return (Class) type().componentType(); + } else { + return type(); + } } } diff --git a/src/main/java/ch/njol/skript/lang/function/Signature.java b/src/main/java/ch/njol/skript/lang/function/Signature.java index 430a383da79..9774cec627a 100644 --- a/src/main/java/ch/njol/skript/lang/function/Signature.java +++ b/src/main/java/ch/njol/skript/lang/function/Signature.java @@ -3,14 +3,18 @@ import ch.njol.skript.Skript; import ch.njol.skript.classes.ClassInfo; import ch.njol.skript.registrations.Classes; -import ch.njol.skript.util.Utils; import ch.njol.skript.util.Contract; +import ch.njol.skript.util.Utils; +import ch.njol.util.StringUtils; +import com.google.common.base.Preconditions; +import com.google.common.collect.Lists; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Unmodifiable; +import org.skriptlang.skript.common.function.FunctionReference; import org.skriptlang.skript.common.function.Parameter.Modifier; -import java.util.Collection; -import java.util.Collections; -import java.util.WeakHashMap; +import java.util.*; /** * Function signature: name, parameter types and a return type. @@ -30,7 +34,7 @@ public class Signature implements org.skriptlang.skript.common.function.Signa /** * Parameters taken by this function, in order. */ - final Parameter[] parameters; + private final SequencedMap> parameters; /** * Whether this function is only accessible in the script it was declared in @@ -38,11 +42,10 @@ public class Signature implements org.skriptlang.skript.common.function.Signa final boolean local; /** - * Return type of this function. For functions that return nothing, this - * is null. void is never used as return type, because it is not registered - * to Skript's type system. + * The return type. */ final @Nullable ClassInfo returnType; + final Class returns; /** * Whether this function returns a single value, or multiple ones. @@ -55,163 +58,215 @@ public class Signature implements org.skriptlang.skript.common.function.Signa */ final Collection> calls; - /** - * The class path for the origin of this signature. - */ - final @Nullable String originClassPath; - /** * An overriding contract for this function (e.g. to base its return on its arguments). */ final @Nullable Contract contract; - public Signature(@Nullable String script, - String name, - Parameter[] parameters, boolean local, - @Nullable ClassInfo returnType, - boolean single, - @Nullable String originClassPath, - @Nullable Contract contract) { + public Signature(@Nullable String script, String name, Parameter[] parameters, boolean local, @Nullable ClassInfo returnType, boolean single, @Nullable Contract contract) { this.script = script; this.name = name; - this.parameters = parameters; + this.parameters = initParameters(parameters); this.local = local; this.returnType = returnType; + if (returnType == null) { + this.returns = null; + } else { + if (single) { + this.returns = returnType.getC(); + } else { + this.returns = returnType.getC().arrayType(); + } + } this.single = single; - this.originClassPath = originClassPath; this.contract = contract; + this.calls = Collections.newSetFromMap(new WeakHashMap<>()); + } - calls = Collections.newSetFromMap(new WeakHashMap<>()); + public Signature(@Nullable String script, String name, Parameter[] parameters, boolean local, @Nullable ClassInfo returnType, boolean single, String stacktrace) { + this(script, name, parameters, local, returnType, single, (Contract) null); } - /** - * Creates a new signature. - * - * @param script The script of this signature. - * @param name The name of the function. - * @param parameters The parameters. - * @param returnType The return type class. - * @param contract A {@link Contract} that may belong to this signature. - */ - public Signature(@Nullable String script, - String name, - org.skriptlang.skript.common.function.Parameter[] parameters, - @Nullable Class returnType, - boolean single, - @Nullable Contract contract) { - this.parameters = new Parameter[parameters.length]; - for (int i = 0; i < parameters.length; i++) { - org.skriptlang.skript.common.function.Parameter parameter = parameters[i]; - this.parameters[i] = new Parameter<>(parameter.name(), - getClassInfo(parameter.type()), parameter.single(), - null, - parameter.modifiers().toArray(new Modifier[0])); - } + public Signature(String script, String name, Parameter[] parameters, boolean local, ClassInfo returnType, boolean single, String stacktrace, @Nullable Contract contract) { + this(script, name, parameters, local, returnType, single, contract); + } + public Signature(@Nullable String script, String name, SequencedMap> parameters, Class returnType, boolean local) { this.script = script; this.name = name; - this.local = script != null; - if (returnType != null) { + this.parameters = parameters; + this.local = local; + this.returns = returnType; + this.single = single(); + if (returnType != null && returnType.isArray()) { //noinspection unchecked - this.returnType = (ClassInfo) getClassInfo(returnType); + this.returnType = (ClassInfo) Classes.getExactClassInfo(returnType.componentType()); } else { - this.returnType = null; + this.returnType = Classes.getExactClassInfo(returnType); } - this.single = single; - this.contract = contract; - this.originClassPath = ""; + this.contract = null; + this.calls = Collections.newSetFromMap(new WeakHashMap<>()); + } - calls = Collections.newSetFromMap(new WeakHashMap<>()); + public Signature(String namespace, String name, org.skriptlang.skript.common.function.Parameter[] parameters, Class returnType, boolean single, @Nullable Contract contract) { + this(namespace, name, initParameters(parameters), returnType, false); + } + + private static SequencedMap> initParameters(org.skriptlang.skript.common.function.Parameter[] params) { + SequencedMap> map = new LinkedHashMap<>(); + for (org.skriptlang.skript.common.function.Parameter parameter : params) { + map.put(parameter.name(), parameter); + } + return map; } /** - * Returns the {@link ClassInfo} of the non-array type of {@code cls}. - * - * @param cls The class. - * @param The type of class. - * @return The non-array {@link ClassInfo} of {@code cls}. + * Converts a {@link org.skriptlang.skript.common.function.Parameter} to a {@link Parameter}. + * @param parameter The parameter to use to convert. + * @return The converted parameter. */ - private static ClassInfo getClassInfo(Class cls) { - ClassInfo classInfo; - if (cls.isArray()) { - //noinspection unchecked - classInfo = (ClassInfo) Classes.getExactClassInfo(cls.componentType()); + private static Parameter toParameter(org.skriptlang.skript.common.function.Parameter parameter) { + if (parameter == null) { + return null; + } + + ClassInfo classInfo; + if (parameter.type().isArray()) { + classInfo = Classes.getExactClassInfo(parameter.type().componentType()); } else { - classInfo = Classes.getExactClassInfo(cls); + classInfo = Classes.getExactClassInfo(parameter.type()); } - return classInfo; + + return new Parameter<>(parameter.name(), classInfo, !parameter.type().isArray(), null, parameter.modifiers().toArray(new Modifier[0])); } - public Signature(String script, - String name, - Parameter[] parameters, boolean local, - @Nullable ClassInfo returnType, - boolean single, - @Nullable String originClassPath) { - this(script, name, parameters, local, returnType, single, originClassPath, null); + /** + * @deprecated Use {@link #getParameter(String)} or {@link #parameters()} instead. + */ + @Deprecated(forRemoval = true, since = "INSERT VERSION") + public Parameter getParameter(int index) { + return parameters.values() + .stream().map(Signature::toParameter) + .toList().get(index); } - public Signature(String script, String name, Parameter[] parameters, boolean local, @Nullable ClassInfo returnType, boolean single) { - this(script, name, parameters, local, returnType, single, null); + /** + * @deprecated Use {@link #parameters()} instead. + */ + @Deprecated(forRemoval = true, since = "INSERT VERSION") + public Parameter[] getParameters() { + return parameters.values().stream().map(Signature::toParameter) + .toList() + .toArray(new Parameter[0]); } - public String getName() { - return name; + @Override + public Class returnType() { + if (returns == null) { + return null; + } + //noinspection unchecked + return (Class) returns; } - @SuppressWarnings("null") - public Parameter getParameter(int index) { - return parameters[index]; + /** + * @return A {@link SequencedMap} containing all parameters. + */ + @Override + public @NotNull SequencedMap> parameters() { + return Collections.unmodifiableSequencedMap(parameters); } - public Parameter[] getParameters() { - return parameters; + @Override + public Contract contract() { + return contract; + } + + @Override + public void addCall(FunctionReference reference) { + calls.add(reference); + } + + /** + * @param name The parameter name. + * @return The parameter with the specified name, or null if none is found. + */ + public org.skriptlang.skript.common.function.Parameter getParameter(@NotNull String name) { + Preconditions.checkNotNull(name, "name cannot be null"); + + return parameters.get(name); + } + + public String getName() { + return name; } public boolean isLocal() { return local; } + /** + * @return The namespace of this signature. + */ + String namespace() { + return script; + } + public @Nullable ClassInfo getReturnType() { return returnType; } + /** + * @return Whether this signature returns a single or multiple values. + */ public boolean isSingle() { return single; } /** - * @deprecated Unused and unsafe. + * @deprecated Unused. */ @Deprecated(forRemoval = true, since = "INSERT VERSION") public String getOriginClassPath() { - return originClassPath; + return ""; } public @Nullable Contract getContract() { return contract; } + public Collection> calls() { + return calls; + } + /** * Gets maximum number of parameters that the function described by this * signature is able to take. + * * @return Maximum number of parameters. */ public int getMaxParameters() { - return parameters.length; + return parameters.size(); } /** * Gets minimum number of parameters that the function described by this * signature is able to take. Parameters that have default values and do * not have any parameters that are mandatory after them, are optional. + * * @return Minimum number of parameters required. */ public int getMinParameters() { - for (int i = parameters.length - 1; i >= 0; i--) { - if (!parameters[i].isOptional()) + List> params = new LinkedList<>(parameters.values()); + + int i = parameters.size() - 1; + for (org.skriptlang.skript.common.function.Parameter parameter : Lists.reverse(params)) { + if (!parameter.modifiers().contains(Modifier.OPTIONAL)) { return i + 1; + } + i--; } + return 0; // No-args function } @@ -232,21 +287,47 @@ public String toString(boolean includeReturnType, boolean debug) { signatureBuilder.append("local "); signatureBuilder.append(name); - signatureBuilder.append('('); - int lastParameterIndex = parameters.length - 1; - for (int i = 0; i < parameters.length; i++) { - signatureBuilder.append(parameters[i].toString(debug)); - if (i != lastParameterIndex) - signatureBuilder.append(", "); - } - signatureBuilder.append(')'); + signatureBuilder.append('(') + .append(StringUtils.join(parameters.values(), ", ")) + .append(')'); - if (includeReturnType && returnType != null) { + if (includeReturnType && returns != null) { signatureBuilder.append(" :: "); - signatureBuilder.append(Utils.toEnglishPlural(returnType.getCodeName(), !single)); + + signatureBuilder.append(Utils.toEnglishPlural(returnType.getCodeName(), returns.isArray())); } return signatureBuilder.toString(); } + @Override + public @NotNull String name() { + return name; + } + + @Override + public @Unmodifiable @NotNull List description() { + return List.of(); + } + + @Override + public @Unmodifiable @NotNull List since() { + return List.of(); + } + + @Override + public @Unmodifiable @NotNull List examples() { + return List.of(); + } + + @Override + public @Unmodifiable @NotNull List keywords() { + return List.of(); + } + + @Override + public @Unmodifiable @NotNull List requires() { + return List.of(); + } + } diff --git a/src/main/java/ch/njol/skript/structures/StructFunction.java b/src/main/java/ch/njol/skript/structures/StructFunction.java index 1a8ea11c478..98b94d2643a 100644 --- a/src/main/java/ch/njol/skript/structures/StructFunction.java +++ b/src/main/java/ch/njol/skript/structures/StructFunction.java @@ -2,22 +2,36 @@ import ch.njol.skript.ScriptLoader; import ch.njol.skript.Skript; +import ch.njol.skript.SkriptConfig; +import ch.njol.skript.classes.ClassInfo; import ch.njol.skript.config.SectionNode; import ch.njol.skript.doc.Description; import ch.njol.skript.doc.Examples; import ch.njol.skript.doc.Name; import ch.njol.skript.doc.Since; import ch.njol.skript.lang.Literal; +import ch.njol.skript.lang.ParseContext; +import ch.njol.skript.lang.SkriptParser; import ch.njol.skript.lang.SkriptParser.ParseResult; import ch.njol.skript.lang.function.FunctionEvent; import ch.njol.skript.lang.function.Functions; import ch.njol.skript.lang.function.Signature; import ch.njol.skript.lang.parser.ParserInstance; +import ch.njol.skript.registrations.Classes; +import ch.njol.skript.util.Contract; +import ch.njol.skript.util.Utils; +import ch.njol.skript.util.Utils.PluralResult; +import ch.njol.util.StringUtils; import org.bukkit.event.Event; import org.jetbrains.annotations.Nullable; import org.skriptlang.skript.lang.entry.EntryContainer; +import org.skriptlang.skript.common.function.Parameter; +import org.skriptlang.skript.common.function.ScriptParameter; import org.skriptlang.skript.lang.structure.Structure; +import java.util.LinkedHashMap; +import java.util.Locale; +import java.util.SequencedMap; import java.util.concurrent.atomic.AtomicBoolean; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -43,8 +57,30 @@ public class StructFunction extends Structure { public static final Priority PRIORITY = new Priority(400); + /** + * Represents a function signature pattern. + *

+ * Name + * The name may start with any Unicode alphabetic character or an underscore. + * Any character following it should be any Unicode alphabetic character, an underscore, or a number. + *

+ *

+ * Args + * The arguments that can be passed to this function. + *

+ *

+ * Returns + * The type that this function returns, if any. + * Acceptable return type prefixes are as follows. + *

    + *
  • {@code returns}
  • + *
  • {@code ->}
  • + *
  • {@code ::}
  • + *
+ *

+ */ private static final Pattern SIGNATURE_PATTERN = - Pattern.compile("^(?:local )?function (" + Functions.functionNamePattern + ")\\((.*?)\\)(?:\\s*(?:::| returns )\\s*(.+))?$"); + Pattern.compile("^(?:local )?function (?" + Functions.functionNamePattern + ")\\((?.*?)\\)(?:\\s*(?:->|::| returns )\\s*(?.+))?$"); private static final AtomicBoolean VALIDATE_FUNCTIONS = new AtomicBoolean(); static { @@ -53,7 +89,6 @@ public class StructFunction extends Structure { ); } - @SuppressWarnings("NotNullFieldNotInitialized") private SectionNode source; @Nullable private Signature signature; @@ -82,9 +117,9 @@ public boolean preLoad() { // parse signature getParser().setCurrentEvent((local ? "local " : "") + "function", FunctionEvent.class); - signature = Functions.parseSignature( + signature = FunctionParser.parse( getParser().getCurrentScript().getConfig().getFileName(), - matcher.group(1), matcher.group(2), matcher.group(3), local + matcher.group("name"), matcher.group("args"), matcher.group("returns"), local ); getParser().deleteCurrentEvent(); @@ -134,4 +169,139 @@ public String toString(@Nullable Event event, boolean debug) { return (local ? "local " : "") + "function"; } + public static class FunctionParser { + + /** + * Parses the signature from the given arguments. + * + * @param script Script file name (might be used for some checks). + * @param name The name of the function. + * @param args The parameters of the function. + * @param returns The return type of the function + * @param local If the signature of function is local. + * @return Parsed signature or null if something went wrong. + * @see Functions#registerSignature(Signature) + */ + public static @Nullable Signature parse(String script, String name, String args, @Nullable String returns, boolean local) { + SequencedMap> parameters = parseParameters(args); + if (parameters == null) + return null; + + // Parse return type if one exists + ClassInfo returnClass; + Class returnType = null; + + if (returns != null) { + returnClass = Classes.getClassInfoFromUserInput(returns); + PluralResult result = Utils.isPlural(returns); + + if (returnClass == null) + returnClass = Classes.getClassInfoFromUserInput(result.updated()); + + if (returnClass == null) { + Skript.error("Cannot recognise the type '" + returns + "'"); + return null; + } + + if (result.plural()) { + returnType = returnClass.getC().arrayType(); + } else { + returnType = returnClass.getC(); + } + + return new Signature<>(script, name, parameters, returnType, local); + } + + return new Signature<>(script, name, parameters, returnType, local); + } + + /** + * Represents the pattern used for the parameter definition in a script function declaration. + *

+ * The first group specifies the name of the parameter. The name may contain any characters + * but a colon, parenthesis, curly braces, double quotes, or a comma. Then, after a colon, + * the type is specified in the {@code type} group. If a default value is present, this is specified + * with the least amount of tokens as possible. + *

+ */ + private final static Pattern SCRIPT_PARAMETER_PATTERN = + Pattern.compile("^\\s*(?[^:(){}\",]+?)\\s*:\\s*(?[a-zA-Z ]+?)\\s*(?:\\s*=\\s*(?.+))?\\s*$"); + + private static SequencedMap> parseParameters(String args) { + SequencedMap> params = new LinkedHashMap<>(); + + boolean caseInsensitive = SkriptConfig.caseInsensitiveVariables.value(); + + if (args.isEmpty()) // Zero-argument function + return params; + + int j = 0; + for (int i = 0; i <= args.length(); i = SkriptParser.next(args, i, ParseContext.DEFAULT)) { + if (i == -1) { + Skript.error("Invalid text/variables/parentheses in the arguments of this function"); + return null; + } + + if (i != args.length() && args.charAt(i) != ',') { + continue; + } + + String arg = args.substring(j, i); + + // One or more arguments for this function + Matcher n = SCRIPT_PARAMETER_PATTERN.matcher(arg); + if (!n.matches()) { + Skript.error("The " + StringUtils.fancyOrderNumber(params.size() + 1) + " argument's definition is invalid. It should look like 'name: type' or 'name: type = default value'."); + return null; + } + + String paramName = n.group("name"); + // for comparing without affecting the original name, in case the config option for case insensitivity changes. + String lowerParamName = paramName.toLowerCase(Locale.ENGLISH); + for (String otherName : params.keySet()) { + // only force lowercase if we don't care about case in variables + otherName = caseInsensitive ? otherName.toLowerCase(Locale.ENGLISH) : otherName; + if (otherName.equals(caseInsensitive ? lowerParamName : paramName)) { + Skript.error("Each argument's name must be unique, but the name '" + paramName + "' occurs at least twice."); + return null; + } + } + + ClassInfo c = Classes.getClassInfoFromUserInput(n.group("type")); + PluralResult result = Utils.isPlural(n.group("type")); + + if (c == null) + c = Classes.getClassInfoFromUserInput(result.updated()); + + if (c == null) { + Skript.error("Cannot recognise the type '%s'", n.group("type")); + return null; + } + + String variableName = paramName.endsWith("*") ? paramName.substring(0, paramName.length() - 3) + + (!result.plural() ? "::1" : "") : paramName; + + Class type; + if (result.plural()) { + type = c.getC().arrayType(); + } else { + type = c.getC(); + } + + Parameter parameter = ScriptParameter.parse(variableName, type, n.group("def")); + + if (parameter == null) + return null; + + params.put(variableName, parameter); + + j = i + 1; + if (i == args.length()) + break; + } + return params; + } + + } + } diff --git a/src/main/java/ch/njol/skript/util/LiteralUtils.java b/src/main/java/ch/njol/skript/util/LiteralUtils.java index d2ba137886a..e53963f7819 100644 --- a/src/main/java/ch/njol/skript/util/LiteralUtils.java +++ b/src/main/java/ch/njol/skript/util/LiteralUtils.java @@ -68,8 +68,8 @@ public static boolean hasUnparsedLiteral(Expression expr) { * @return Whether or not the passed expressions contain {@link UnparsedLiteral} objects */ public static boolean canInitSafely(Expression... expressions) { - for (int i = 0; i < expressions.length; i++) { - if (expressions[i] == null || hasUnparsedLiteral(expressions[i])) { + for (Expression expression : expressions) { + if (expression == null || hasUnparsedLiteral(expression)) { return false; } } diff --git a/src/main/java/ch/njol/skript/util/Utils.java b/src/main/java/ch/njol/skript/util/Utils.java index 1bbb8e5626a..2ff288d0bb9 100644 --- a/src/main/java/ch/njol/skript/util/Utils.java +++ b/src/main/java/ch/njol/skript/util/Utils.java @@ -8,6 +8,7 @@ import ch.njol.util.Pair; import ch.njol.util.StringUtils; import ch.njol.util.coll.CollectionUtils; +import com.google.common.base.Preconditions; import com.google.common.collect.Iterables; import com.google.common.io.ByteArrayDataInput; import com.google.common.io.ByteArrayDataOutput; @@ -243,9 +244,9 @@ public static File getFile(Plugin plugin) { } /** - * @param word trimmed string - * @return Pair of singular string + boolean whether it was plural + * @deprecated Use {@link #isPlural(String)} instead. */ + @Deprecated(forRemoval = true, since = "INSERT VERSION") public static NonNullPair getEnglishPlural(String word) { assert word != null; if (word.isEmpty()) @@ -273,10 +274,60 @@ public static NonNullPair getEnglishPlural(String word) { return new NonNullPair<>(word, false); } + public record PluralResult(String updated, boolean plural) { + + } + + /** + * Returns whether a word is plural. If it is, {@code updated} contains the single variant of the word. + * Otherwise, {@code updated == word}. + * + * @param word The word to check. + * @return A pair with the updated word and a boolean indicating whether it was plural. + */ + public static PluralResult isPlural(String word) { + Preconditions.checkNotNull(word, "word cannot be null"); + + if (word.isEmpty()) { + return new PluralResult("", false); + } + + if (couldBeSingular(word)) { + return new PluralResult(word, false); + } + + for (WordEnding ending : plurals) { + if (ending.isCompleteWord()) { + // Complete words shouldn't be used as partial pieces + if (word.length() != ending.plural().length()) { + continue; + } + } + + if (word.endsWith(ending.plural())) { + return new PluralResult( + word.substring(0, word.length() - ending.plural().length()) + ending.singular(), + true + ); + } + + if (word.endsWith(ending.plural().toUpperCase(Locale.ENGLISH))) { + return new PluralResult( + word.substring(0, word.length() - ending.plural().length()) + + ending.singular().toUpperCase(Locale.ENGLISH), + true + ); + } + } + + return new PluralResult(word, false); + } + private static boolean couldBeSingular(String word) { - for (final WordEnding ending : plurals) { + for (WordEnding ending : plurals) { if (ending.singular().isBlank()) continue; + if (ending.isCompleteWord() && ending.singular().length() != word.length()) continue; // Skip complete words @@ -724,8 +775,8 @@ public static Class highestDenominator(Class< assert classes.length > 0; Class chosen = classes[0]; outer: - for (final Class checking : classes) { - assert checking != null && !checking.isArray() && !checking.isPrimitive() : checking; + for (Class checking : classes) { + assert !checking.isArray() && !checking.isPrimitive() : "%s has no super".formatted(checking.getSimpleName()); if (chosen.isAssignableFrom(checking)) continue; Class superType = checking; diff --git a/src/main/java/org/skriptlang/skript/common/function/DefaultFunctionImpl.java b/src/main/java/org/skriptlang/skript/common/function/DefaultFunctionImpl.java index 2a89f4bd0e2..585daf80802 100644 --- a/src/main/java/org/skriptlang/skript/common/function/DefaultFunctionImpl.java +++ b/src/main/java/org/skriptlang/skript/common/function/DefaultFunctionImpl.java @@ -3,6 +3,7 @@ import ch.njol.skript.lang.function.FunctionEvent; import ch.njol.skript.lang.function.Signature; import com.google.common.base.Preconditions; +import org.enginehub.piston.converter.MapArgumentConverter; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Unmodifiable; @@ -16,7 +17,7 @@ final class DefaultFunctionImpl extends ch.njol.skript.lang.function.Function implements DefaultFunction { private final SkriptAddon source; - private final Parameter[] parameters; + private final SequencedMap> parameters; private final Function execute; private final List description; @@ -27,14 +28,15 @@ final class DefaultFunctionImpl extends ch.njol.skript.lang.function.Function DefaultFunctionImpl( SkriptAddon source, - String name, Parameter[] parameters, + String name, + SequencedMap> parameters, Class returnType, boolean single, @Nullable ch.njol.skript.util.Contract contract, Function execute, String[] description, String[] since, String[] examples, String[] keywords, String[] requires ) { - super(new Signature<>(null, name, parameters, returnType, single, contract)); + super(new Signature<>(null, name, parameters.values().toArray(new Parameter[0]), returnType, single, contract)); Preconditions.checkNotNull(source, "source cannot be null"); Preconditions.checkNotNull(name, "name cannot be null"); @@ -56,10 +58,11 @@ final class DefaultFunctionImpl extends ch.njol.skript.lang.function.Function public T @Nullable [] execute(FunctionEvent event, Object[][] params) { Map args = new LinkedHashMap<>(); - int length = Math.min(parameters.length, params.length); + int length = Math.min(parameters.size(), params.length); + Parameter[] arrayParams = parameters.values().toArray(new Parameter[0]); for (int i = 0; i < length; i++) { Object[] arg = params[i]; - Parameter parameter = parameters[i]; + Parameter parameter = arrayParams[i]; if (arg == null || arg.length == 0) { if (parameter.hasModifier(Modifier.OPTIONAL)) { @@ -100,6 +103,17 @@ final class DefaultFunctionImpl extends ch.njol.skript.lang.function.Function } } + @Override + public T execute(FunctionEvent event, FunctionArguments arguments) { + for (String name : arguments.names()) { + if (arguments.get(name) == null && !parameters.get(name).hasModifier(Modifier.OPTIONAL)) { + return null; + } + } + + return execute.apply(arguments); + } + @Override public boolean resetReturnValue() { return true; @@ -150,7 +164,7 @@ static class BuilderImpl implements DefaultFunctionImpl.Builder { private final SkriptAddon source; private final String name; private final Class returnType; - private final Map> parameters = new LinkedHashMap<>(); + private final SequencedMap> parameters = new LinkedHashMap<>(); private ch.njol.skript.util.Contract contract = null; @@ -236,7 +250,7 @@ public Builder parameter(@NotNull String name, @NotNull Class type, Modifi public DefaultFunction build(@NotNull Function execute) { Preconditions.checkNotNull(execute, "execute cannot be null"); - return new DefaultFunctionImpl<>(source, name, parameters.values().toArray(new Parameter[0]), + return new DefaultFunctionImpl<>(source, name, parameters, returnType, !returnType.isArray(), contract, execute, description, since, examples, keywords, requires); } diff --git a/src/main/java/org/skriptlang/skript/common/function/Function.java b/src/main/java/org/skriptlang/skript/common/function/Function.java index 4b121d92943..a7f56afb48f 100644 --- a/src/main/java/org/skriptlang/skript/common/function/Function.java +++ b/src/main/java/org/skriptlang/skript/common/function/Function.java @@ -1,9 +1,11 @@ package org.skriptlang.skript.common.function; +import ch.njol.skript.lang.function.FunctionEvent; import org.jetbrains.annotations.ApiStatus.Experimental; import org.jetbrains.annotations.ApiStatus.Internal; import org.jetbrains.annotations.ApiStatus.NonExtendable; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; /** * Represents a function implementation. @@ -16,9 +18,22 @@ @Experimental public interface Function { + /** + * Executes this function with the given parameters. + * + * @param event The event that is associated with this function execution. + * @param arguments The arguments to execute the function with. + * @return The return value. + */ + T execute(FunctionEvent event, FunctionArguments arguments); + /** * @return The signature belonging to this function. */ @NotNull Signature signature(); + boolean resetReturnValue(); + + @NotNull String @Nullable [] returnedKeys(); + } diff --git a/src/main/java/org/skriptlang/skript/common/function/FunctionArgumentParser.java b/src/main/java/org/skriptlang/skript/common/function/FunctionArgumentParser.java new file mode 100644 index 00000000000..4d884235d9d --- /dev/null +++ b/src/main/java/org/skriptlang/skript/common/function/FunctionArgumentParser.java @@ -0,0 +1,239 @@ +package org.skriptlang.skript.common.function; + +import org.skriptlang.skript.common.function.FunctionReference.Argument; +import org.skriptlang.skript.common.function.FunctionReference.ArgumentType; + +import java.util.ArrayList; +import java.util.List; + +/** + * Parses the arguments of a function reference. + */ +final class FunctionArgumentParser { + + /** + * The input string. + */ + private final String args; + + /** + * The list of arguments that have been found so far. + */ + private final List> arguments = new ArrayList<>(); + + /** + * Constructs a new function argument parser based on the + * input string and instantly calculates the result. + * + * @param args The input string. + */ + public FunctionArgumentParser(String args) { + this.args = args; + + parse(); + } + + /** + * The char index. + */ + private int index = 0; + + /** + * The current character. + */ + private char c; + + /** + * Whether the current argument being parsed starts with a name declaration. + */ + private boolean nameFound = false; + + /** + * A builder which keeps track of the name part of an argument. + *

+ * This builder may contain a part of the expression at the start of parsing an argument, + * when it is unclear whether we are currently parsing a name or not. On realization that + * this argument does not have a name, its contents are cleared. + *

+ */ + private final StringBuilder namePart = new StringBuilder(); + + /** + * A builder which keeps track of the expression part of an argument. + *

+ * This builder may contain a part of the name at the start of parsing an argument, + * when it is unclear whether we are currently parsing a name or not. On realization that + * this argument has a name, its contents are cleared. + *

+ */ + private final StringBuilder exprPart = new StringBuilder(); + + /** + * Whether we are currently in a string or not. + *

+ * To avoid parsing a comma in a string as the start of a new argument, we keep track of whether we're + * in a string or not to ignore commas found in strings. + * A new argument can only start when {@code nesting == 0 && !inString}. + *

+ */ + private boolean inString = false; + + /** + * The level of nesting we are currently in. + *

+ * The nesting level is increased when entering special expressions which may contain commas, + * thereby avoiding incorrectly parsing a comma in variables or parentheses as the start of a new argument. + * A new argument can only start when {@code nesting == 0 && !inString}. + *

+ */ + private int nesting = 0; + + /** + * Parses the input string into arguments. + *

+ * For every argument, during the parsing of the first few characters, one of the following things occurs. + *

    + *
  • A legal parameter name character is encountered. The character is added to {@link #namePart} and + * {@link #exprPart}.
  • + *
  • An illegal parameter name character is encountered. This means that the previous data added to {@link #namePart} + * cannot be a name. {@link #namePart} is cleared and the rest of the argument is parsed as the expression.
  • + *
  • A colon {@code :} is encountered. When all previous characters for this argument match the requirements + * for a parameter name, the name is stored in {@link #namePart} and the rest of the argument is parsed as the expression.
  • + *
  • A comma {@code ,} is encountered. This means that the end of the argument has been reached. If no name was found, + * the entire argument is parsed as {@link #exprPart}. If a name was found, {@link #exprPart} gets stored alongside {@link #namePart}.
  • + *
+ *

+ */ + private void parse() { + // if we have no args to parse, give up instantly + if (args.isEmpty()) { + return; + } + + while (index < args.length()) { + c = args.charAt(index); + + // first try to compile the name + if (!nameFound) { + // if a name matches the legal characters, update name part + if (c == '_' || Character.isLetterOrDigit(c)) { + namePart.append(c); + exprPart.append(c); + index++; + continue; + } + + // then if we have a name, start parsing the second part + if (nesting == 0 && c == ':' && !namePart.isEmpty()) { + exprPart.setLength(0); + index++; + nameFound = true; + continue; + } + + if (isSpecialCharacter(ArgumentType.UNNAMED)) { + continue; + } + + // given that the character did not match the legal name chars, reset name + namePart.setLength(0); + nextExpr(); + continue; + } + + if (isSpecialCharacter(ArgumentType.NAMED)) { + continue; + } + + nextExpr(); // add to expression + } + + // make sure to save the last argument + if (nameFound) { + save(ArgumentType.NAMED); + } else { + save(ArgumentType.UNNAMED); + } + } + + /** + * Manages special character handling by updating the {@link #nesting} and {@link #inString} variables. + * + * @param type The type of argument that is currently being parsed. + * @return True when {@link #c} is a special character, false if not. + */ + private boolean isSpecialCharacter(ArgumentType type) { + // for strings + if (!inString && c == '"') { + nesting++; + inString = true; + nextExpr(); + return true; + } + + if (inString && c == '"' + && index < args.length() - 1 && args.charAt(index + 1) != '"') { // allow double string char in strings + nesting--; + inString = false; + nextExpr(); + return true; + } + + if (c == '(' || c == '{') { + nesting++; + nextExpr(); + return true; + } + + if (c == ')' || c == '}') { + nesting--; + nextExpr(); + return true; + } + + if (nesting == 0 && c == ',') { + save(type); + return true; + } + + return false; + } + + /** + * Moves the parser to the next part of the expression that is being parsed. + */ + private void nextExpr() { + exprPart.append(c); + index++; + } + + /** + * Saves the string parts stored in {@link #exprPart} (and optionally {@link #namePart}) as a new argument in + * {@link #arguments}. Then, all data for the current argument is cleared. + * + * @param type The type of argument to save as. + */ + private void save(ArgumentType type) { + if (type == ArgumentType.UNNAMED) { + arguments.add(new Argument<>(ArgumentType.UNNAMED, null, exprPart.toString().trim())); + } else { + arguments.add(new Argument<>(ArgumentType.NAMED, namePart.toString().trim(), exprPart.toString().trim())); + } + + namePart.setLength(0); + exprPart.setLength(0); + index++; + nameFound = false; + } + + /** + * Returns all arguments. + * + * @return All arguments. + */ + public Argument[] getArguments() { + //noinspection unchecked + return (Argument[]) arguments.toArray(new Argument[0]); + } + +} diff --git a/src/main/java/org/skriptlang/skript/common/function/FunctionReference.java b/src/main/java/org/skriptlang/skript/common/function/FunctionReference.java new file mode 100644 index 00000000000..4b8b5ab110e --- /dev/null +++ b/src/main/java/org/skriptlang/skript/common/function/FunctionReference.java @@ -0,0 +1,351 @@ +package org.skriptlang.skript.common.function; + +import ch.njol.skript.Skript; +import ch.njol.skript.classes.ClassInfo; +import ch.njol.skript.expressions.ExprBlockSound.SoundType; +import ch.njol.skript.expressions.ExprKeyed; +import ch.njol.skript.lang.*; +import ch.njol.skript.lang.function.*; +import ch.njol.skript.lang.function.FunctionRegistry.Retrieval; +import ch.njol.skript.lang.function.FunctionRegistry.RetrievalResult; +import ch.njol.skript.localization.Language; +import ch.njol.skript.registrations.Classes; +import ch.njol.skript.util.LiteralUtils; +import com.google.common.base.Preconditions; +import org.bukkit.Bukkit; +import org.bukkit.event.Event; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.common.function.Parameter.Modifier; + +import java.util.*; +import java.util.Map.Entry; +import java.util.stream.Collectors; + +/** + * A reference to a {@link Function} found in a script. + * + * @param The return type of this reference. + */ +public final class FunctionReference implements Debuggable { + + private final String namespace; + private final String name; + private final Signature signature; + private final Argument>[] arguments; + + private Function cachedFunction; + private LinkedHashMap cachedArguments; + + private record ArgInfo(Expression expression, Class type, Set modifiers) { + + } + + public FunctionReference(@Nullable String namespace, + @NotNull String name, + @NotNull Signature signature, + @NotNull Argument>[] arguments) { + Preconditions.checkNotNull(name, "name cannot be null"); + Preconditions.checkNotNull(signature, "signature cannot be null"); + Preconditions.checkNotNull(arguments, "arguments cannot be null"); + + this.namespace = namespace; + this.name = name; + this.signature = signature; + this.arguments = arguments; + } + + /** + * Validates this function reference. + * + * @return True if this is a valid function reference, false if not. + */ + public boolean validate() { + if (signature == null) { + return false; + } + + if (cachedArguments == null) { + cachedArguments = new LinkedHashMap<>(); + + // mixing arguments is only allowed when the order of arguments matches param order + boolean mix = Arrays.stream(arguments) + .map(it -> it.type) + .collect(Collectors.toSet()).size() == ArgumentType.values().length; + + // get the target params of the function + SequencedMap> targetParameters = new LinkedHashMap<>(signature.parameters()); + + for (Argument> argument : arguments) { + Parameter target; + if (argument.type == ArgumentType.NAMED) { + target = targetParameters.get(argument.name); + } else { + Entry> first = targetParameters.entrySet().iterator().next(); + + if (first == null) { + return false; + } + + target = first.getValue(); + } + + if (target == null) { + return false; + } + + // try to parse value in the argument + Class conversionTarget; + if (target.type().isArray()) { + conversionTarget = target.type().componentType(); + } else { + conversionTarget = target.type(); + } + + //noinspection unchecked + Expression converted = argument.value.getConvertedExpression(conversionTarget); + + // failed to parse value + if (!validateArgument(target, argument.value, converted)) { + return false; + } + + if (mix && !targetParameters.entrySet().iterator().next().getKey().equals(target.name())) { + Skript.error(Language.get("functions.mixing named and unnamed arguments")); + return false; + } + + // all good + cachedArguments.put(target.name(), new ArgInfo(converted, target.type(), target.modifiers())); + targetParameters.remove(target.name()); + } + } + + signature.addCall(this); + + return true; + } + + private boolean validateArgument(Parameter target, Expression original, Expression converted) { + if (converted == null) { + if (LiteralUtils.hasUnparsedLiteral(original)) { + Skript.error("Can't understand this expression: %s", original); + } else { + Skript.error("Expected type %s for argument '%s', but %s is of type %s.", + getName(target.type(), target.single()), target.name(), original, getName(original.getReturnType(), original.isSingle())); + } + return false; + } + + if (target.single() && !converted.isSingle()) { + Skript.error("Expected type %s for argument '%s', but %s is of type %s.", + getName(target.type(), target.single()), target.name(), converted, getName(converted.getReturnType(), converted.isSingle())); + return false; + } + + return true; + } + + private String getName(Class clazz, boolean single) { + if (single) { + return Classes.getSuperClassInfo(clazz).getName().getSingular(); + } else { + if (clazz.isArray()) { + return Classes.getSuperClassInfo(clazz.componentType()).getName().getPlural(); + } + return Classes.getSuperClassInfo(clazz).getName().getPlural(); + } + } + + /** + * Executes the function referred to by this reference. + * + * @param event The event to use for execution. + * @return The return value of the function. + */ + public T execute(Event event) { + if (!validate()) { + Skript.error("Failed to verify function %s before execution.", name); + return null; + } + + LinkedHashMap args = new LinkedHashMap<>(); + cachedArguments.forEach((k, v) -> { + if (v.modifiers().contains(Modifier.KEYED)) { + args.put(k, evaluateKeyed(v.expression(), event)); + return; + } + + if (!v.type().isArray()) { + args.put(k, v.expression().getSingle(event)); + } else { + args.put(k, v.expression().getArray(event)); + } + }); + + Function function = function(); + FunctionEvent fnEvent = new FunctionEvent<>(function); + + if (Functions.callFunctionEvents) + Bukkit.getPluginManager().callEvent(fnEvent); + + return function.execute(fnEvent, new FunctionArgumentsImpl(args)); + } + + private KeyedValue[] evaluateKeyed(Expression expression, Event event) { + if (expression instanceof ExpressionList list) { + return evaluateSingleListParameter(list.getExpressions(), event); + } + return evaluateParameter(expression, event); + } + + private KeyedValue[] evaluateSingleListParameter(Expression[] parameters, Event event) { + List values = new ArrayList<>(); + Set keys = new LinkedHashSet<>(); + int keyIndex = 1; + for (Expression parameter : parameters) { + Object[] valuesArray = parameter.getArray(event); + String[] keysArray = KeyProviderExpression.areKeysRecommended(parameter) + ? ((KeyProviderExpression) parameter).getArrayKeys(event) + : null; + + // Don't allow mutating across function boundary; same hack is applied to variables + for (Object value : valuesArray) + values.add(Classes.clone(value)); + + if (keysArray != null) { + keys.addAll(Arrays.asList(keysArray)); + continue; + } + + for (int i = 0; i < valuesArray.length; i++) { + while (keys.contains(String.valueOf(keyIndex))) + keyIndex++; + keys.add(String.valueOf(keyIndex++)); + } + } + return KeyedValue.zip(values.toArray(), keys.toArray(new String[0])); + } + + private KeyedValue[] evaluateParameter(Expression parameter, Event event) { + Object[] values = parameter.getArray(event); + + // Don't allow mutating across function boundary; same hack is applied to variables + for (int i = 0; i < values.length; i++) + values[i] = Classes.clone(values[i]); + + String[] keys = KeyProviderExpression.areKeysRecommended(parameter) + ? ((KeyProviderExpression) parameter).getArrayKeys(event) + : null; + return KeyedValue.zip(values, keys); + } + + /** + * @return The function referred to by this reference. + */ + public Function function() { + if (cachedFunction == null) { + Class[] parameters = signature.parameters().values().stream().map(Parameter::type).toArray(Class[]::new); + + Retrieval> retrieval = FunctionRegistry.getRegistry().getFunction(namespace, name, parameters); + + if (retrieval.result() == RetrievalResult.EXACT) { + //noinspection unchecked + cachedFunction = (Function) retrieval.retrieved(); + } + } + + return cachedFunction; + } + + /** + * @return The signature belonging to this reference. + */ + public Signature signature() { + return signature; + } + + /** + * @return The namespace that this reference is in. + */ + public String namespace() { + return namespace; + } + + /** + * @return The name of the function being referenced. + */ + public @NotNull String name() { + return name; + } + + /** + * @return The passed arguments. + */ + public @NotNull Argument>[] arguments() { + return arguments; + } + + /** + * @return Whether this reference returns a single or multiple values. + */ + public boolean single() { + if (signature.contract() != null) { + Expression[] args = Arrays.stream(arguments) + .map(it -> it.value) + .toArray(Expression[]::new); + + return signature.contract().isSingle(args); + } else { + return signature.single(); + } + } + + @Override + public String toString(@Nullable Event event, boolean debug) { + StringBuilder builder = new StringBuilder(); + + builder.append(name); + builder.append("("); + + StringJoiner args = new StringJoiner(", "); + for (Argument> argument : arguments) { + args.add("%s: %s".formatted(argument.name, argument.value.toString(event, debug))); + } + + builder.append(args); + builder.append(")"); + return builder.toString(); + } + + /** + * An argument. + * + * @param type The type of the argument. + * @param name The name of the argument, possibly null. + * @param value The value of the argument. + */ + public record Argument( + ArgumentType type, + String name, + T value + ) { + + } + + /** + * The type of argument. + */ + public enum ArgumentType { + /** + * Whether this argument has a name. + */ + NAMED, + + /** + * Whether this argument does not have a name. + */ + UNNAMED + } + +} diff --git a/src/main/java/org/skriptlang/skript/common/function/FunctionReferenceParser.java b/src/main/java/org/skriptlang/skript/common/function/FunctionReferenceParser.java new file mode 100644 index 00000000000..828bbf12f73 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/common/function/FunctionReferenceParser.java @@ -0,0 +1,469 @@ +package org.skriptlang.skript.common.function; + +import ch.njol.skript.Skript; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.ExpressionList; +import ch.njol.skript.lang.ParseContext; +import ch.njol.skript.lang.SkriptParser; +import ch.njol.skript.lang.function.FunctionRegistry; +import ch.njol.skript.lang.function.Signature; +import ch.njol.skript.lang.parser.ParserInstance; +import ch.njol.skript.localization.Language; +import ch.njol.skript.log.ParseLogHandler; +import ch.njol.skript.log.SkriptLogger; +import ch.njol.skript.registrations.Classes; +import ch.njol.skript.util.LiteralUtils; +import ch.njol.util.StringUtils; +import ch.njol.util.coll.CollectionUtils; +import org.skriptlang.skript.common.function.FunctionReference.Argument; +import org.skriptlang.skript.common.function.FunctionReference.ArgumentType; + +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +/** + * A class containing the methods to parse an expression to a {@link FunctionReference}. + * + * @param context The context of parsing. + * @param flags The active parsing flags. + */ +public record FunctionReferenceParser(ParseContext context, int flags) { + + private final static Pattern FUNCTION_CALL_PATTERN = + Pattern.compile("(?[\\p{IsAlphabetic}_][\\p{IsAlphabetic}\\d_]*)\\((?.*)\\)"); + + /** + * Attempts to parse {@code expr} as a function reference. + * + * @param The return type of the function. + * @return A {@link FunctionReference} if a function is found, or {@code null} if none is found. + */ + public FunctionReference parseFunctionReference(String expr) { + try (ParseLogHandler log = SkriptLogger.startParseLogHandler()) { + if (!expr.endsWith(")")) { + log.printLog(); + return null; + } + + Matcher matcher = FUNCTION_CALL_PATTERN.matcher(expr); + if (!matcher.matches()) { + log.printLog(); + return null; + } + + String functionName = matcher.group("name"); + String args = matcher.group("args"); + + // Check for incorrect quotes, e.g. "myFunction() + otherFunction()" being parsed as one function + // See https://github.com/SkriptLang/Skript/issues/1532 + for (int i = 0; i < args.length(); i = SkriptParser.next(args, i, context)) { + if (i != -1) { + continue; + } + log.printLog(); + return null; + } + + if ((flags & SkriptParser.PARSE_EXPRESSIONS) == 0) { + Skript.error("Functions cannot be used here (or there is a problem with your arguments)."); + log.printError(); + return null; + } + + FunctionReference.Argument[] arguments = new FunctionArgumentParser(args).getArguments(); + return parseFunctionReference(functionName, arguments, log); + } + } + + /** + * Attempts to parse a function reference. + * + * @param name The function name. + * @param arguments The passed arguments to the function as an array of {@link Argument Arguments}, + * usually parsed with a {@link FunctionArgumentParser}. + * @param log The log handler. + * @param The return type of the function. + * @return A {@link FunctionReference} if a function is found, or {@code null} if none is found. + */ + public FunctionReference parseFunctionReference(String name, FunctionReference.Argument[] arguments, ParseLogHandler log) { + // avoid assigning values to a parameter multiple times + Set named = new HashSet<>(); + for (Argument argument : arguments) { + if (argument.type() != ArgumentType.NAMED) { + continue; + } + + boolean added = named.add(argument.name()); + if (added) { + continue; + } + + Skript.error(Language.get("functions.already assigned value to parameter"), argument.name()); + log.printError(); + return null; + } + + String namespace; + if (ParserInstance.get().isActive()) { + namespace = ParserInstance.get().getCurrentScript().getConfig().getFileName(); + } else { + namespace = null; + } + + // try to find a matching signature to get which types to parse args with + Set> options = FunctionRegistry.getRegistry().getSignatures(namespace, name); + + if (options.isEmpty()) { + doesNotExist(name, arguments); + log.printError(); + return null; + } + + // all signatures that have no single list param + // example: function add(x: int, y: int) + Set> exacts = new HashSet<>(); + // all signatures with only single list params + // these are functions that accept any number of arguments given a specific type + // example: function sum(ns: numbers) + Set> lists = new HashSet<>(); + + // first, sort into types + for (Signature option : options) { + if (option.parameters().size() == 1 && !option.parameters().firstEntry().getValue().single()) { + lists.add(option); + } else { + exacts.add(option); + } + } + + // second, try to match any exact functions + Set> exactReferences = getExactReferences(namespace, name, exacts, arguments); + if (exactReferences == null) { // a list error, so quit parsing + log.printError(); + return null; + } + + // if we found an exact one, return first to avoid conflict with list references + if (exactReferences.size() == 1) { + return exactReferences.stream().findAny().orElse(null); + } + + // last, find single list functions + Set> listReferences = getListReferences(namespace, name, lists, arguments); + if (listReferences == null) { // a list error, so quit parsing + log.printError(); + return null; + } + + exactReferences.addAll(listReferences); + + if (exactReferences.isEmpty()) { + doesNotExist(name, arguments); + log.printError(); + return null; + } else if (exactReferences.size() == 1) { + return exactReferences.stream().findAny().orElse(null); + } else { + ambiguousError(name, exactReferences); + log.printError(); + return null; + } + } + + /** + * Returns all possible {@link FunctionReference FunctionReferences} given the list of signatures + * which do not contain a single list parameter. + * + * @param namespace The current namespace. + * @param name The name of the function. + * @param signatures The possible signatures. + * @param arguments The passed arguments. + * @param The return type of the references. + * @return All possible exact {@link FunctionReference FunctionReferences}. + */ + private Set> getExactReferences( + String namespace, String name, + Set> signatures, Argument[] arguments + ) { + Set> exactReferences = new HashSet<>(); + + signatures: + for (Signature signature : signatures) { + // if arguments arent possible, skip + if (arguments.length > signature.getMaxParameters() || arguments.length < signature.getMinParameters()) { + continue; + } + + // all remaining arguments to parse + // if a passed argument is named it bypasses the regular argument order of unnamed arguments + LinkedHashSet remaining = new LinkedHashSet<>(signature.parameters().keySet()); + + Class[] targets = new Class[arguments.length]; + for (int i = 0; i < arguments.length; i++) { + Argument argument = arguments[i]; + + if (remaining.isEmpty()) { + break; + } + + Parameter parameter; + if (argument.type() == ArgumentType.NAMED) { + parameter = signature.getParameter(argument.name()); + } else { + parameter = signature.getParameter(remaining.getFirst()); + } + + if (parameter == null) { + continue signatures; + } + + if (parameter.type().isArray()) { + targets[i] = parameter.type().componentType(); + } else { + targets[i] = parameter.type(); + } + + remaining.remove(parameter.name()); + } + + //noinspection DuplicatedCode + FunctionArgumentParseResult result = parseFunctionArguments(arguments, targets); + + if (result.type() == FunctionArgumentParseResultType.LIST_ERROR) { + return null; + } + + if (result.type() == FunctionArgumentParseResultType.OK) { + //noinspection unchecked + FunctionReference reference = new FunctionReference<>(namespace, name, (Signature) signature, result.parsed()); + + if (!reference.validate()) { + continue; + } + + exactReferences.add(reference); + } + } + return exactReferences; + } + + /** + * Returns all possible {@link FunctionReference FunctionReferences} given the list of signatures + * which only contain a single list parameter. + * + * @param namespace The current namespace. + * @param name The name of the function. + * @param signatures The possible signatures. + * @param arguments The passed arguments. + * @param The return type of the references. + * @return All possible {@link FunctionReference FunctionReferences} which contain a single list parameter. + */ + private Set> getListReferences( + String namespace, String name, + Set> signatures, Argument[] arguments + ) { + // disallow naming any arguments other than the first + if (arguments.length > 1) { + for (Argument argument : arguments) { + if (argument.type() != ArgumentType.NAMED) { + continue; + } + + doesNotExist(name, arguments); + return null; + } + } + + Set> references = new HashSet<>(); + + signatures: + for (Signature signature : signatures) { + Parameter parameter = signature.parameters().firstEntry().getValue(); + + Class target = parameter.type().componentType(); + Class[] targets = new Class[]{target}; + + if (arguments.length == 1 && arguments[0].type() == ArgumentType.NAMED) { + if (!arguments[0].name().equals(parameter.name())) { + doesNotExist(name, arguments); + continue; + } + } + + // join all args to a single arg + String joined = Arrays.stream(arguments).map(Argument::value) + .collect(Collectors.joining(", ")); + Argument argument = new Argument<>(ArgumentType.NAMED, parameter.name(), joined); + Argument[] array = CollectionUtils.array(argument); + + //noinspection DuplicatedCode + FunctionArgumentParseResult result = parseFunctionArguments(array, targets); + + if (result.type() == FunctionArgumentParseResultType.LIST_ERROR) { + return null; + } + + if (result.type() == FunctionArgumentParseResultType.OK) { + // avoid allowing lists inside lists + if (result.parsed.length == 1 && result.parsed[0].value() instanceof ExpressionList list) { + for (Expression expression : list.getExpressions()) { + if (expression instanceof ExpressionList) { + doesNotExist(name, arguments); + continue signatures; + } + } + } + + //noinspection unchecked + FunctionReference reference = new FunctionReference<>(namespace, name, (Signature) signature, result.parsed()); + + if (!reference.validate()) { + continue; + } + + references.add(reference); + } + } + + return references; + } + + /** + * Prints the error for when multiple function references have been matched. + * + * @param name The function name. + * @param references The possible references. + * @param The return types of the references. + */ + private void ambiguousError(String name, Set> references) { + List parts = new ArrayList<>(); + + for (FunctionReference reference : references) { + String builder = reference.name() + + "(" + + reference.signature().parameters().values().stream() + .map(it -> { + if (it.type().isArray()) { + return Classes.getSuperClassInfo(it.type().componentType()).getName().getPlural(); + } else { + return Classes.getSuperClassInfo(it.type()).getName().getSingular(); + } + }) + .collect(Collectors.joining(", ")) + + ")"; + + parts.add(builder); + } + + Skript.error(Language.get("functions.ambiguous function call"), + name, StringUtils.join(parts, ", ", " or ")); + } + + /** + * Prints the error for when a function does not exist. + * + * @param name The function name. + * @param arguments The passed arguments to the function call. + */ + private void doesNotExist(String name, FunctionReference.Argument[] arguments) { + StringJoiner joiner = new StringJoiner(", "); + + for (FunctionReference.Argument argument : arguments) { + SkriptParser parser = new SkriptParser(argument.value(), flags | SkriptParser.PARSE_LITERALS, context); + + Expression expression = LiteralUtils.defendExpression(parser.parseExpression(Object.class)); + + String argumentName; + if (argument.type() == ArgumentType.NAMED) { + argumentName = argument.name() + ": "; + } else { + argumentName = ""; + } + + if (!LiteralUtils.canInitSafely(expression)) { + joiner.add(argumentName + "?"); + continue; + } + + if (expression.isSingle()) { + joiner.add(argumentName + Classes.getSuperClassInfo(expression.getReturnType()).getName().getSingular()); + } else { + joiner.add(argumentName + Classes.getSuperClassInfo(expression.getReturnType()).getName().getPlural()); + } + } + + Skript.error("The function %s(%s) does not exist.", name, joiner); + } + + /** + * The type of result from attempting to parse function arguments. + */ + private enum FunctionArgumentParseResultType { + + /** + * All arguments were successfully parsed to the specified type. + */ + OK, + + /** + * An argument failed to parse to the specified target type. + */ + PARSE_FAIL, + + /** + * An expression list contained "or", thus parsing should be stopped. + */ + LIST_ERROR + + } + + /** + * The results of attempting to parse function arguments. + * + * @param type The type of result. + * @param parsed The resulting parsed arguments, or null if parsing was not successful. + */ + private record FunctionArgumentParseResult(FunctionArgumentParseResultType type, + FunctionReference.Argument>[] parsed) { + + } + + /** + * Attempts to parse every argument in {@code arguments} as the specified type in {@code targets}. + * + * @param arguments The arguments to parse. + * @param targets The target classes to parse. + * @return A {@link FunctionArgumentParseResult} with the results. + */ + private FunctionArgumentParseResult parseFunctionArguments(FunctionReference.Argument[] arguments, Class[] targets) { + assert arguments.length == targets.length; + assert Arrays.stream(targets).noneMatch(Class::isArray); + + //noinspection unchecked + FunctionReference.Argument>[] parsed = (FunctionReference.Argument>[]) + new FunctionReference.Argument[arguments.length]; + + for (int i = 0; i < arguments.length; i++) { + FunctionReference.Argument argument = arguments[i]; + + SkriptParser parser = new SkriptParser(argument.value(), flags | SkriptParser.PARSE_LITERALS, context); + + Expression expression = parser.parseExpression(targets[i]); + + if (expression == null) { + return new FunctionArgumentParseResult(FunctionArgumentParseResultType.PARSE_FAIL, null); + } + + if (expression instanceof ExpressionList list && !list.getAnd()) { + Skript.error(Language.get("functions.or in arguments")); + return new FunctionArgumentParseResult(FunctionArgumentParseResultType.LIST_ERROR, null); + } + + parsed[i] = new FunctionReference.Argument<>(argument.type(), argument.name(), expression); + } + + return new FunctionArgumentParseResult(FunctionArgumentParseResultType.OK, parsed); + } +} diff --git a/src/main/java/org/skriptlang/skript/common/function/Parameter.java b/src/main/java/org/skriptlang/skript/common/function/Parameter.java index ccf790745dd..2acbad834c9 100644 --- a/src/main/java/org/skriptlang/skript/common/function/Parameter.java +++ b/src/main/java/org/skriptlang/skript/common/function/Parameter.java @@ -1,5 +1,6 @@ package org.skriptlang.skript.common.function; +import org.jetbrains.annotations.ApiStatus.NonExtendable; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Unmodifiable; import org.skriptlang.skript.common.function.DefaultFunction.Builder; @@ -11,6 +12,7 @@ * * @param The type of the function parameter. */ +@NonExtendable public interface Parameter { /** @@ -46,7 +48,7 @@ default boolean single() { /** * Represents a modifier that can be applied to a parameter - * when constructing one using {@link Builder#parameter(String, Class, Modifier[])}}. + * when constructing one using {@link Builder#parameter(String, Class, Modifier[])}. */ interface Modifier { diff --git a/src/main/java/org/skriptlang/skript/common/function/ScriptParameter.java b/src/main/java/org/skriptlang/skript/common/function/ScriptParameter.java new file mode 100644 index 00000000000..6cb41490a8b --- /dev/null +++ b/src/main/java/org/skriptlang/skript/common/function/ScriptParameter.java @@ -0,0 +1,88 @@ +package org.skriptlang.skript.common.function; + +import ch.njol.skript.Skript; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.ParseContext; +import ch.njol.skript.lang.SkriptParser; +import ch.njol.skript.lang.Variable; +import ch.njol.skript.log.RetainingLogHandler; +import ch.njol.skript.log.SkriptLogger; +import ch.njol.skript.util.LiteralUtils; +import com.google.common.base.Preconditions; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.HashSet; +import java.util.Set; + +/** + * A parameter for a {@link DefaultFunction}. + * + * @param name The name. + * @param type The type's class. + * @param modifiers The modifiers. + * @param defaultValue The default value, or null if there is no default value. + * @param The type. + */ +public record ScriptParameter(String name, Class type, Set modifiers, Expression defaultValue) + implements Parameter { + + public ScriptParameter(String name, Class type, Modifier... modifiers) { + this(name, type, Set.of(modifiers), null); + } + + public ScriptParameter(String name, Class type, Expression defaultValue, Modifier... modifiers) { + this(name, type, Set.of(modifiers), defaultValue); + } + + /** + * Parses a {@link ScriptParameter} from a script. + * + * @param name The name. + * @param type The class of the parameter. + * @param def The default value, if present. + * @return A parsed parameter {@link ScriptParameter}, or null if parsing failed. + */ + public static Parameter parse(@NotNull String name, @NotNull Class type, @Nullable String def) { + Preconditions.checkNotNull(name, "name cannot be null"); + Preconditions.checkNotNull(type, "type cannot be null"); + + if (!Variable.isValidVariableName(name, true, false)) { + Skript.error("Invalid parameter name: %s", name); + return null; + } + + Expression defaultValue = null; + if (def != null) { + Class target; + if (type.isArray()) { + target = type.componentType(); + } else { + target = type; + } + + // Parse the default value expression + try (RetainingLogHandler log = SkriptLogger.startRetainingLog()) { + defaultValue = new SkriptParser(def, SkriptParser.ALL_FLAGS, ParseContext.DEFAULT).parseExpression(target); + if (defaultValue == null || LiteralUtils.hasUnparsedLiteral(defaultValue)) { + log.printErrors("Can't understand this expression: " + def); + log.stop(); + return null; + } + log.printLog(); + log.stop(); + } + } + + Set modifiers = new HashSet<>(); + if (defaultValue != null) { + modifiers.add(Modifier.OPTIONAL); + } + if (type.isArray()) { + modifiers.add(Modifier.KEYED); + } + + return new ScriptParameter<>(name, type, defaultValue, modifiers.toArray(new Modifier[0])); + } + +} diff --git a/src/main/java/org/skriptlang/skript/common/function/Signature.java b/src/main/java/org/skriptlang/skript/common/function/Signature.java index 90c064aab08..d3df504b318 100644 --- a/src/main/java/org/skriptlang/skript/common/function/Signature.java +++ b/src/main/java/org/skriptlang/skript/common/function/Signature.java @@ -1,8 +1,14 @@ package org.skriptlang.skript.common.function; +import ch.njol.skript.doc.Documentable; +import ch.njol.skript.util.Contract; import org.jetbrains.annotations.ApiStatus.Experimental; import org.jetbrains.annotations.ApiStatus.Internal; import org.jetbrains.annotations.ApiStatus.NonExtendable; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Unmodifiable; + +import java.util.SequencedMap; /** * Represents a function signature. @@ -13,6 +19,30 @@ @NonExtendable @Internal @Experimental -public interface Signature { +public interface Signature extends Documentable { + + /** + * @return The type of this parameter. + */ + Class returnType(); + + /** + * @return An unmodifiable view of all the parameters that this signature has. + */ + @Unmodifiable @NotNull SequencedMap> parameters(); + + Contract contract(); + + void addCall(FunctionReference reference); + + /** + * @return Whether this signature returns single values. + */ + default boolean single() { + if (returnType() == null) { + return false; + } + return !returnType().isArray(); + } } diff --git a/src/main/java/org/skriptlang/skript/common/function/SignatureImpl.java b/src/main/java/org/skriptlang/skript/common/function/SignatureImpl.java new file mode 100644 index 00000000000..4ea9ff5200f --- /dev/null +++ b/src/main/java/org/skriptlang/skript/common/function/SignatureImpl.java @@ -0,0 +1,61 @@ +package org.skriptlang.skript.common.function; + +import ch.njol.skript.util.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Unmodifiable; + +import java.util.List; +import java.util.SequencedMap; + +final class SignatureImpl implements Signature { + + @Override + public @NotNull String name() { + return ""; + } + + @Override + public @Unmodifiable @NotNull List description() { + return List.of(); + } + + @Override + public @Unmodifiable @NotNull List since() { + return List.of(); + } + + @Override + public @Unmodifiable @NotNull List examples() { + return List.of(); + } + + @Override + public @Unmodifiable @NotNull List keywords() { + return List.of(); + } + + @Override + public @Unmodifiable @NotNull List requires() { + return List.of(); + } + + @Override + public @NotNull Class returnType() { + return null; + } + + @Override + public @Unmodifiable @NotNull SequencedMap> parameters() { + return null; + } + + @Override + public Contract contract() { + return null; + } + + @Override + public void addCall(FunctionReference reference) { + + } +} diff --git a/src/main/resources/lang/english.lang b/src/main/resources/lang/english.lang index 224a1da7d6e..8671122405d 100644 --- a/src/main/resources/lang/english.lang +++ b/src/main/resources/lang/english.lang @@ -226,3 +226,10 @@ time: io exceptions: unknownhostexception: Cannot connect to %s accessdeniedexception: Access denied for %s + +# -- Functions -- +functions: + or in arguments: Function arguments cannot be seperated by an 'or'. Put the 'or' into a second set of parentheses if you want to make it a single parameter, e.g. 'give(player, (sword or axe))' + ambiguous function call: Cannot determine which function named %s to call: %s. Try clarifying the type of the arguments using the 'value within' expression. + already assigned value to parameter: A value has already been assigned to parameter %s. + mixing named and unnamed arguments: Mixing named and unnamed arguments is not allowed unless the order of the arguments matches the order of the parameters. diff --git a/src/test/java/org/skriptlang/skript/common/function/FunctionArgumentParserTest.java b/src/test/java/org/skriptlang/skript/common/function/FunctionArgumentParserTest.java new file mode 100644 index 00000000000..9675bdab15f --- /dev/null +++ b/src/test/java/org/skriptlang/skript/common/function/FunctionArgumentParserTest.java @@ -0,0 +1,60 @@ +package org.skriptlang.skript.common.function; + +import org.junit.Test; +import org.skriptlang.skript.common.function.FunctionArgumentParser; +import org.skriptlang.skript.common.function.FunctionReference.Argument; +import org.skriptlang.skript.common.function.FunctionReference.ArgumentType; + +import static org.junit.Assert.assertEquals; + +public class FunctionArgumentParserTest { + + @Test + public void testUnnamedArgs() { + Argument[] arguments = new FunctionArgumentParser("1, 2, \"hey:, gi:rl\", ({forza, real::*}, {_x::2}, 2)").getArguments(); + + assertEquals(new Argument<>(ArgumentType.UNNAMED, null, "1"), arguments[0]); + assertEquals(new Argument<>(ArgumentType.UNNAMED, null, "2"), arguments[1]); + assertEquals(new Argument<>(ArgumentType.UNNAMED, null, "\"hey:, gi:rl\""), arguments[2]); + assertEquals(new Argument<>(ArgumentType.UNNAMED, null, "({forza, real::*}, {_x::2}, 2)"), arguments[3]); + + arguments = new FunctionArgumentParser("1, 2, \"hey, girl\", ({forza, real}, 2)").getArguments(); + + assertEquals(new Argument<>(ArgumentType.UNNAMED, null, "1"), arguments[0]); + assertEquals(new Argument<>(ArgumentType.UNNAMED, null, "2"), arguments[1]); + assertEquals(new Argument<>(ArgumentType.UNNAMED, null, "\"hey, girl\""), arguments[2]); + assertEquals(new Argument<>(ArgumentType.UNNAMED, null, "({forza, real}, 2)"), arguments[3]); + } + + @Test + public void testNamedArgs() { + Argument[] arguments = new FunctionArgumentParser("a_rg: 1, 2, womp: \"hey:, gi:rl\", list: ({forza, real::*}, {_x::2}, 2)").getArguments(); + + assertEquals(new Argument<>(ArgumentType.NAMED, "a_rg", "1"), arguments[0]); + assertEquals(new Argument<>(ArgumentType.UNNAMED, null, "2"), arguments[1]); + assertEquals(new Argument<>(ArgumentType.NAMED, "womp", "\"hey:, gi:rl\""), arguments[2]); + assertEquals(new Argument<>(ArgumentType.NAMED, "list", "({forza, real::*}, {_x::2}, 2)"), arguments[3]); + + arguments = new FunctionArgumentParser("2: 1, 2, 3_60: \"hey, girl\", 1list: ({forza, real}, 2)").getArguments(); + + assertEquals(new Argument<>(ArgumentType.NAMED, "2", "1"), arguments[0]); + assertEquals(new Argument<>(ArgumentType.UNNAMED, null, "2"), arguments[1]); + assertEquals(new Argument<>(ArgumentType.NAMED, "3_60", "\"hey, girl\""), arguments[2]); + assertEquals(new Argument<>(ArgumentType.NAMED, "1list", "({forza, real}, 2)"), arguments[3]); + } + + @Test + public void testSingleNamedList() { + Argument[] arguments = new FunctionArgumentParser("1: (2, 3, 4)").getArguments(); + + assertEquals(new Argument<>(ArgumentType.NAMED, "1", "(2, 3, 4)"), arguments[0]); + } + + @Test + public void testStringEscape() { + Argument[] arguments = new FunctionArgumentParser("1: \"hello \"\" %{x,y::%player's car, or not!%::*} there\"\"\"").getArguments(); + + assertEquals(new Argument<>(ArgumentType.NAMED, "1", "\"hello \"\" %{x,y::%player's car, or not!%::*} there\"\"\""), arguments[0]); + } + +} diff --git a/src/test/skript/tests/misc/function overloading.sk b/src/test/skript/tests/misc/function overloading.sk index 38b344d8dc0..7baaa3c529a 100644 --- a/src/test/skript/tests/misc/function overloading.sk +++ b/src/test/skript/tests/misc/function overloading.sk @@ -13,7 +13,7 @@ test "function overloading with 1 parameter": parse: overloaded1({_x}) - assert first element of last parse logs contains "Skript cannot determine which function named 'overloaded1' to call." + assert first element of last parse logs contains "Cannot determine which function named overloaded1 to call" set {_x} to 1 @@ -49,7 +49,7 @@ test "function overloading with 2 parameters": parse: overloaded2({_y}, {_x}) - assert first element of last parse logs contains "Skript cannot determine which function named 'overloaded2' to call." + assert first element of last parse logs contains "Cannot determine which function named overloaded2 to call" function overloaded3() :: int: return 1 @@ -62,7 +62,7 @@ parse: test "function overloading with different return types": assert size of {FunctionOverloading3::parse::*} = 1 - assert {FunctionOverloading3::parse::1} contains "Function 'overloaded3' with the same argument types already exists in script" + assert {FunctionOverloading3::parse::1} contains "Function overloaded3 with the same argument types already exists in script" function overloading_supertype1(p: offlineplayer): stop diff --git a/src/test/skript/tests/misc/named function arguments.sk b/src/test/skript/tests/misc/named function arguments.sk new file mode 100644 index 00000000000..46eba435de5 --- /dev/null +++ b/src/test/skript/tests/misc/named function arguments.sk @@ -0,0 +1,84 @@ +local function nfa(a: int, b: int, c: int) :: int: + return {_a} + {_b} - {_c} + +local function nfa(a: string, b: string) :: int: + return 3 + +test "named function arguments": + assert nfa(8, 2, 3) = 7 + + assert nfa(a: 8, 2, 3) = 7 + assert nfa(a: 8, b: 2, 3) = 7 + assert nfa(a: 8, b: 2, c: 3) = 7 + + assert nfa(c: 3, a: 8, b: 2) = 7 + assert nfa(c: 3, b: 2, a: 8) = 7 + assert nfa(a: 8, c: 3, b: 2) = 7 + + assert nfa(a: 8, b: 2, 3) = 7 + assert nfa(8, b: 2, c: 3) = 7 + + set {_x} to 8 + assert nfa(a: {_x}, b: 2, 3) = 7 + + parse: + assert nfa(a: 8, c: adghadhaherta, b: 2) = 7 + assert first element of last parse logs contains "The function nfa(a: integer, c: ?, b: integer) does not exist" + + parse: + assert nfa(8, c: 3, b: 2) = 7 + assert first element of last parse logs contains "Mixing named and unnamed arguments is not allowed" + + parse: + assert nfa(c: 3, 2, a: 8) = 7 + assert first element of last parse logs contains "Mixing named and unnamed arguments is not allowed" + + parse: + assert nfa(c: 3, a: 8, 2) = 7 + assert first element of last parse logs contains "Mixing named and unnamed arguments is not allowed" + + parse: + assert nfa(b: 2, 3, a: 8) = 7 + assert first element of last parse logs contains "Mixing named and unnamed arguments is not allowed" + + assert nfa("gonna be, gonna be, ""'golden'""", "fire %{_, }%") = 3 + + parse: + nfa(a: 1, b: 2, d: 2) + assert first element of last parse logs contains "The function nfa(a: integer, b: integer, d: integer) does not exist" + + parse: + nfa(a: 1, d: 2, b: 2) + assert first element of last parse logs contains "The function nfa(a: integer, d: integer, b: integer) does not exist" + + parse: + nfa(a: 1, a: 2, c: 2) + assert first element of last parse logs contains "A value has already been assigned to parameter a" + +local function nfa_incorrect_list(ns: numbers): + stop + +test "named function arguments with single list params": + parse: + nfa_incorrect_list(ns: (1, 2, 3)) + assert last parse logs are not set + + parse: + nfa_incorrect_list(ns: 1, n: 2, 3) + assert first element of last parse logs contains "The function nfa_incorrect_list(ns: integer, n: integer, integer) does not exist" + + parse: + nfa_incorrect_list(wrong: (1, 2, 3)) + assert first element of last parse logs contains "The function nfa_incorrect_list(wrong: integers) does not exist" + + parse: + nfa_incorrect_list(ns: (1, (2, 3))) + assert first element of last parse logs contains "The function nfa_incorrect_list(ns: integers) does not exist" + + parse: + nfa_incorrect_list(ns: 1, 2, 3) + assert first element of last parse logs contains "The function nfa_incorrect_list(ns: integer, integer, integer) does not exist" + + parse: + nfa_incorrect_list(ns: 1, ns: 2, ns: 3) + assert first element of last parse logs contains "A value has already been assigned to parameter ns." diff --git a/src/test/skript/tests/syntaxes/structures/StructFunction.sk b/src/test/skript/tests/syntaxes/structures/StructFunction.sk index 1fa8f21a682..4a236bf5450 100644 --- a/src/test/skript/tests/syntaxes/structures/StructFunction.sk +++ b/src/test/skript/tests/syntaxes/structures/StructFunction.sk @@ -45,27 +45,62 @@ test "function structure type hints": set {_number} to true parse: type_hint_argument_test({_number}) - assert the first element of the last parse logs is set # contains "The function 'type_hint_argument_test(boolean)' does not exist." (NEEDS FIXING) + assert the first element of the last parse logs contains "The function 'type_hint_argument_test(boolean)' does not exist." -local function list_argument_single_default(x: texts="hey") :: texts: +local function list_argument_single_default(x: texts="hey") -> texts: return {_x::*} -test "function list argument with a single default": +test "function list argument with a single default value": assert list_argument_single_default() = "hey" +local function argument_single_default(x: text="hey") -> text: + return {_x} + +test "function argument with a default value": + assert argument_single_default() = "hey" + local function literal_test(x: entity type): stop + local function literal_test_projectile(x: entity): stop test "literal type parsing": parse: literal_test(firework) - assert last parse logs is not set + assert last parse logs are not set spawn an arrow at test-location: set {_entity} to entity parse: literal_test_projectile(projectile) + assert last parse logs are not set clear entity within {_entity} + +local function argument_test(x: int, y: int): + stop + +local function argument_test_list(xs: ints): + stop + +test "function structure arguments": + parse: + argument_test(1) + assert last parse logs contain "The function argument_test(integer) does not exist" + + parse: + argument_test(1, 2) + assert last parse logs are not set + + parse: + argument_test(1, 2, 3) + assert last parse logs contain "The function argument_test(integer, integer, integer) does not exist" + + parse: + argument_test_list((1, 2, 3)) + assert last parse logs are not set + + parse: + argument_test_list(1, (2, 3)) + assert last parse logs contain "The function argument_test_list(integer, integers) does not exist"