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..420a321fd6a 100644 --- a/src/main/java/ch/njol/skript/lang/function/FunctionRegistry.java +++ b/src/main/java/ch/njol/skript/lang/function/FunctionRegistry.java @@ -4,6 +4,7 @@ import ch.njol.skript.SkriptAPIException; import com.google.common.base.MoreObjects; import com.google.common.base.Preconditions; +import org.jetbrains.annotations.*; import com.google.common.collect.ImmutableSet; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -14,6 +15,7 @@ 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()); } /** @@ -509,6 +504,9 @@ private Retrieval> getSignature(@NotNull NamespaceIdentifier namesp // 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; } @@ -534,13 +532,20 @@ private Retrieval> getSignature(@NotNull NamespaceIdentifier namesp candidateType = candidate.args[i]; } + Class providedType; + if (provided.args[i].isArray()) { + providedType = provided.args[i].componentType(); + } else { + providedType = provided.args[i]; + } + Class providedArg = provided.args[i]; if (exact) { if (providedArg != candidateType) { continue candidates; } } else { - if (!Converters.converterExists(providedArg, candidateType)) { + if (!Converters.converterExists(providedType, candidateType)) { continue candidates; } } diff --git a/src/main/java/ch/njol/skript/sections/ExprSecFunction.java b/src/main/java/ch/njol/skript/sections/ExprSecFunction.java new file mode 100644 index 00000000000..bffb6ad0c6c --- /dev/null +++ b/src/main/java/ch/njol/skript/sections/ExprSecFunction.java @@ -0,0 +1,251 @@ +package ch.njol.skript.sections; + +import ch.njol.skript.Skript; +import ch.njol.skript.config.Node; +import ch.njol.skript.config.SectionNode; +import ch.njol.skript.config.SimpleNode; +import ch.njol.skript.doc.Description; +import ch.njol.skript.doc.Example; +import ch.njol.skript.doc.Name; +import ch.njol.skript.doc.Since; +import ch.njol.skript.expressions.base.SectionExpression; +import ch.njol.skript.lang.*; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.skript.lang.function.Function; +import ch.njol.skript.lang.function.FunctionRegistry; +import ch.njol.skript.lang.function.FunctionRegistry.Retrieval; +import ch.njol.skript.lang.function.FunctionRegistry.RetrievalResult; +import ch.njol.skript.lang.function.Parameter; +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.LiteralUtils; +import ch.njol.util.Kleenean; +import org.bukkit.event.Event; +import org.jetbrains.annotations.Nullable; + +import java.util.*; +import java.util.Map.Entry; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +@Name("Function Section") +@Description(""" + Runs a function with the specified arguments. + """) +@Example(""" + local function multiply(x: number, y: number) returns number: + return {_x} * {_y} + + set {_x} to function multiply with arguments: + x as 2 + y as 3 + + broadcast "%{_x}%" # returns 6 + """) +@Since("INSERT VERSION") +public class ExprSecFunction extends SectionExpression { + + /** + * The pattern for a valid function name. + * Functions must start with a letter or underscore and can only contain letters, numbers, and underscores. + */ + private final static Pattern FUNCTION_NAME_PATTERN = Pattern.compile("[A-z_][A-z_0-9]*"); + + /** + * The pattern for an argument that can be passed in the children of this section. + */ + private static final Pattern ARGUMENT_PATTERN = Pattern.compile("(?:(?:the )?argument )?(?%s) set to (?.+)".formatted(FUNCTION_NAME_PATTERN.toString())); + + static { + Skript.registerExpression(ExprSecFunction.class, Object.class, ExpressionType.SIMPLE, "[the] function <.+> with [the] arg[ument][s]"); + } + + private Function function; + private LinkedHashMap> arguments = null; + + @Override + public boolean init(Expression[] expressions, int pattern, Kleenean delayed, ParseResult result, + @Nullable SectionNode node, @Nullable List triggerItems) { + assert node != null; + + if (node.isEmpty()) { + Skript.error("A function section must contain arguments."); + return false; + } + + LinkedHashMap args = new LinkedHashMap<>(); + for (Node n : node) { + if (!(n instanceof SimpleNode) || n.getKey() == null) { + Skript.error("Invalid argument declaration for a function section: ", n.getKey()); + return false; + } + + Matcher matcher = ARGUMENT_PATTERN.matcher(n.getKey()); + if (!matcher.matches()) { + Skript.error("Invalid argument declaration for a function section: ", n.getKey()); + return false; + } + + args.put(matcher.group("name"), matcher.group("value")); + } + + String namespace = ParserInstance.get().getCurrentScript().getConfig().getFileName(); + String name = result.regexes.get(0).group(); + + if (!FUNCTION_NAME_PATTERN.matcher(name).matches()) { + Skript.error("The function %s does not exist.", name); + return false; + } + + // todo use FunctionParser + function = findFunction(namespace, name, args); + + if (function == null || arguments == null || arguments.isEmpty()) { + doesNotExist(name, args); + return false; + } + + if (function.getReturnType() == null) { + Skript.error("The function %s does not return anything.", name); + return false; + } + + return true; + } + + /** + * Attempts to find the function to execute given the arguments. + * + * @param namespace The current script. + * @param name The name of the function. + * @param args The passed arguments. + * @return The function given the arguments, or null if no function is found. + */ + private Function findFunction(String namespace, String name, LinkedHashMap args) { + signatures: + for (Signature signature : FunctionRegistry.getRegistry().getSignatures(namespace, name)) { + LinkedHashMap> arguments = new LinkedHashMap<>(); + + LinkedHashMap> parameters = Arrays.stream(signature.getParameters()) + .collect(Collectors.toMap(Parameter::getName, p -> p, (a, b) -> b, LinkedHashMap::new)); + for (Entry entry : args.entrySet()) { + Parameter parameter = parameters.get(entry.getKey()); + + if (parameter == null) { + continue signatures; + } + + //noinspection unchecked + Expression expression = LiteralUtils.defendExpression( + new SkriptParser(entry.getValue(), SkriptParser.ALL_FLAGS, ParseContext.DEFAULT) + .parseExpression(parameter.getType().getC())); + + if (expression == null || LiteralUtils.hasUnparsedLiteral(expression)) { + continue signatures; + } + + arguments.put(entry.getKey(), expression); + } + + Class[] signatureArgs = Arrays.stream(signature.getParameters()) + .map(it -> { + if (it.isSingleValue()) { + return it.getType().getC(); + } else { + return it.getType().getC().arrayType(); + } + }) + .toArray(Class[]::new); + + Retrieval> retrieval = FunctionRegistry.getRegistry().getFunction(namespace, name, signatureArgs); + if (retrieval.result() == RetrievalResult.EXACT) { + this.arguments = arguments; + return retrieval.retrieved(); + } + } + + return null; + } + + /** + * 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, LinkedHashMap arguments) { + StringJoiner joiner = new StringJoiner(", "); + + for (Map.Entry entry : arguments.entrySet()) { + SkriptParser parser = new SkriptParser(entry.getValue(), SkriptParser.ALL_FLAGS, ParseContext.DEFAULT); + + Expression expression = LiteralUtils.defendExpression(parser.parseExpression(Object.class)); + + if (expression == null || LiteralUtils.hasUnparsedLiteral(expression)) { + joiner.add(entry.getKey() + ": ?"); + continue; + } + + if (expression.isSingle()) { + joiner.add(entry.getKey() + ": " + Classes.getSuperClassInfo(expression.getReturnType()).getName().getSingular()); + } else { + joiner.add(entry.getKey() + ": " + Classes.getSuperClassInfo(expression.getReturnType()).getName().getPlural()); + } + } + + Skript.error("The function %s(%s) does not exist.", name, joiner); + } + + @Override + protected Object @Nullable [] get(Event event) { + if (function == null) { + return null; + } + + Object[][] args = new Object[function.getParameters().length][]; + int i = 0; + for (Parameter value : function.getParameters()) { + Expression expression = arguments.get(value.getName()); + + if (expression == null) { + return null; + } + + args[i] = expression.getArray(event); + i++; + } + + try { + return function.execute(args); + } finally { + function.resetReturnValue(); + } + } + + @Override + public boolean isSingle() { + return function.isSingle(); + } + + @Override + public boolean isSectionOnly() { + return true; + } + + @Override + public Class getReturnType() { + return function.getReturnType() != null ? function.getReturnType().getC() : null; + } + + @Override + public String toString(@Nullable Event event, boolean debug) { + return new SyntaxStringBuilder(event, debug) + .append("run function") + .append(function.getName()) + .append("with arguments") + .toString(); + } + +} diff --git a/src/test/skript/tests/syntaxes/sections/ExprSecFunction.sk b/src/test/skript/tests/syntaxes/sections/ExprSecFunction.sk new file mode 100644 index 00000000000..4a971e8de7a --- /dev/null +++ b/src/test/skript/tests/syntaxes/sections/ExprSecFunction.sk @@ -0,0 +1,66 @@ +local function esf(x: int):: int: + return 1 + +#local function esf(x: string):: int: +# return 2 + +#local function esf(x: objects):: int: +# return 3 + +local function esf_two(x: int, y: int):: int: + return 4 + +local function esf_void(x: int): + stop + +test "function section": + set {_x} to function esf with arguments: + x set to 1 + assert {_x} = 1 + + #set {_x} to function esf with arguments: + # x set to "hey" + #assert {_x} = 2 + + #set {_x} to function esf with arguments: + # x set to 1 and 2 + #assert {_x} = 3 + + #set {_x} to function esf with arguments: + # x set to 1, 2, 3, {_a}, {_b::*} + #assert {_x} = 3 + + #parse: + # set {_x} to function esf with arguments: + # x set to {_y} + #assert first element of last parse logs is set + + set {_y} to 3 + set {_x} to function esf with arguments: + x set to {_y} + assert {_x} = 1 + + parse: + set {_x} to function esf with arguments: + x set to firework + assert first element of last parse logs contains "The function esf(x: item type) does not exist" + + parse: + set {_x} to function esf with arguments: + x set to agadsfg + assert first element of last parse logs contains "The function esf(x: ?) does not exist" + + set {_x} to function esf_two with arguments: + x set to 1 + y set to 2 + assert {_x} = 4 + + set {_x} to function esf_two with arguments: + y set to 2 + x set to 1 + assert {_x} = 4 + + parse: + set {_x} to function esf_void with arguments: + x set to 1 + assert first element of last parse logs contains "The function esf_void does not return anything"