diff --git a/src/main/java/ch/njol/skript/expressions/ExprResult.java b/src/main/java/ch/njol/skript/expressions/ExprResult.java deleted file mode 100644 index 58545049769..00000000000 --- a/src/main/java/ch/njol/skript/expressions/ExprResult.java +++ /dev/null @@ -1,111 +0,0 @@ -package ch.njol.skript.expressions; - -import ch.njol.skript.Skript; -import ch.njol.skript.classes.Changer.ChangeMode; -import ch.njol.skript.doc.*; -import ch.njol.skript.expressions.base.PropertyExpression; -import ch.njol.skript.lang.Expression; -import ch.njol.skript.lang.ExpressionList; -import ch.njol.skript.lang.ExpressionType; -import ch.njol.skript.lang.SkriptParser.ParseResult; -import ch.njol.skript.lang.function.DynamicFunctionReference; -import ch.njol.skript.util.LiteralUtils; -import ch.njol.util.Kleenean; -import org.bukkit.event.Event; -import org.jetbrains.annotations.Nullable; -import ch.njol.skript.registrations.experiments.ReflectionExperimentSyntax; -import org.skriptlang.skript.util.Executable; - -@Name("Result") -@Description({ - "Runs something (like a function) and returns its result.", - "If the thing is expected to return multiple values, use 'results' instead of 'result'." -}) -@Example("set {_function} to the function named \"myFunction\"") -@Example("set {_result} to the result of {_function}") -@Example("set {_list::*} to the results of {_function}") -@Example("set {_result} to the result of {_function} with arguments 13 and true") -@Since("2.10") -@Keywords({"run", "result", "execute", "function", "reflection"}) -public class ExprResult extends PropertyExpression, Object> implements ReflectionExperimentSyntax { - - static { - Skript.registerExpression(ExprResult.class, Object.class, ExpressionType.COMBINED, - "[the] result[plural:s] of [running|executing] %executable% [arguments:with arg[ument]s %-objects%]"); - } - - private Expression arguments; - private boolean hasArguments, isPlural; - private DynamicFunctionReference.Input input; - - @Override - public boolean init(Expression[] expressions, int matchedPattern, Kleenean isDelayed, ParseResult result) { - //noinspection unchecked - this.setExpr((Expression>) expressions[0]); - this.hasArguments = result.hasTag("arguments"); - this.isPlural = result.hasTag("plural"); - if (hasArguments) { - this.arguments = LiteralUtils.defendExpression(expressions[1]); - Expression[] arguments; - if (this.arguments instanceof ExpressionList list) { - arguments = list.getExpressions(); - } else { - arguments = new Expression[] {this.arguments}; - } - this.input = new DynamicFunctionReference.Input(arguments); - return LiteralUtils.canInitSafely(this.arguments); - } else { - this.input = new DynamicFunctionReference.Input(); - } - return true; - } - - @Override - protected Object[] get(Event event, Executable[] source) { - for (Executable task : source) { - Object[] arguments; - //noinspection rawtypes - if (task instanceof DynamicFunctionReference reference) { - Expression validated = reference.validate(input); - if (validated == null) - return new Object[0]; - arguments = validated.getArray(event); - } else if (hasArguments) { - arguments = this.arguments.getArray(event); - } else { - arguments = new Object[0]; - } - Object execute = task.execute(event, arguments); - if (execute instanceof Object[] results) - return results; - return new Object[] {execute}; - } - return new Object[0]; - } - - @Override - public Class @Nullable [] acceptChange(ChangeMode mode) { - return null; - } - - @Override - public Class getReturnType() { - return Object.class; - } - - @Override - public boolean isSingle() { - return !isPlural; - } - - @Override - public String toString(@Nullable Event event, final boolean debug) { - String text = "the result" + (isPlural ? "s" : "") + " of " + getExpr().toString(event, debug); - if (hasArguments) - text += " with arguments " + arguments.toString(event, debug); - return text; - } - - - -} diff --git a/src/main/java/org/skriptlang/skript/common/CommonModule.java b/src/main/java/org/skriptlang/skript/common/CommonModule.java index 762ec04c158..7b224a40931 100644 --- a/src/main/java/org/skriptlang/skript/common/CommonModule.java +++ b/src/main/java/org/skriptlang/skript/common/CommonModule.java @@ -4,9 +4,15 @@ import org.skriptlang.skript.addon.AddonModule; import org.skriptlang.skript.addon.HierarchicalAddonModule; import org.skriptlang.skript.addon.SkriptAddon; -import org.skriptlang.skript.common.elements.expressions.*; +import org.skriptlang.skript.common.elements.expressions.ExprColorFromHexCode; +import org.skriptlang.skript.common.elements.expressions.ExprHexCode; +import org.skriptlang.skript.common.elements.expressions.ExprRecursiveSize; +import org.skriptlang.skript.common.elements.expressions.ExprReplace; +import org.skriptlang.skript.common.elements.sections.ExprSecFunction; import org.skriptlang.skript.common.properties.PropertiesModule; -import org.skriptlang.skript.common.types.*; +import org.skriptlang.skript.common.types.QuaternionClassInfo; +import org.skriptlang.skript.common.types.QueueClassInfo; +import org.skriptlang.skript.common.types.ScriptClassInfo; import java.util.List; @@ -15,7 +21,7 @@ public class CommonModule extends HierarchicalAddonModule { @Override public Iterable children() { return List.of( - new PropertiesModule(this) + new PropertiesModule(this) ); } @@ -33,7 +39,8 @@ protected void loadSelf(SkriptAddon addon) { ExprColorFromHexCode::register, ExprHexCode::register, ExprRecursiveSize::register, - ExprReplace::register + ExprReplace::register, + ExprSecFunction::register ); } diff --git a/src/main/java/org/skriptlang/skript/common/elements/sections/ExprSecFunction.java b/src/main/java/org/skriptlang/skript/common/elements/sections/ExprSecFunction.java new file mode 100644 index 00000000000..e637d6f0b54 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/common/elements/sections/ExprSecFunction.java @@ -0,0 +1,279 @@ +package org.skriptlang.skript.common.elements.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.Functions; +import ch.njol.skript.localization.Language; +import ch.njol.skript.localization.Noun; +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.skript.util.Utils; +import ch.njol.util.Kleenean; +import org.bukkit.event.Event; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.common.function.FunctionReference; +import org.skriptlang.skript.common.function.FunctionReference.Argument; +import org.skriptlang.skript.common.function.FunctionReference.ArgumentType; +import org.skriptlang.skript.common.function.FunctionReferenceParser; +import org.skriptlang.skript.registration.SyntaxInfo; +import org.skriptlang.skript.registration.SyntaxRegistry; +import org.skriptlang.skript.util.Executable; + +import java.util.ArrayList; +import java.util.List; +import java.util.StringJoiner; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +@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 result of function multiply with arguments: + x set to 2 + y set to 3 + + broadcast "%{_x}%" # returns 6 + """) +@Example(""" + set {_function} to the function named "myFunction" + set {_result} to the result of {_function} + set {_list::*} to the results of {_function} + set {_result} to the result of {_function} with arguments 13 and true + """) +@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(Functions.functionNamePattern); + + /** + * The pattern for an argument that can be passed in the children of this section. + */ + private static final Pattern ARGUMENT_PATTERN = Pattern.compile("(?:argument )?(?%s) set to (?.+)".formatted(FUNCTION_NAME_PATTERN.toString())); + + public static void register(SyntaxRegistry syntaxRegistry) { + syntaxRegistry.register(SyntaxRegistry.EXPRESSION, SyntaxInfo.Expression.builder(ExprSecFunction.class, Object.class) + .supplier(ExprSecFunction::new) + .addPattern("[the] result[plural:s] of [running|executing] %executable% [arguments:with arg[ument]s %-objects%]") + .addPattern("[the] result[s] of [running|executing] function <.+> with [the] arg[ument][s]") + .build()); + } + + private boolean usesExecutable; + private Expression> executable; + private Expression[] executableArguments = null; + + private FunctionReference reference; + private final List> arguments = new ArrayList<>(); + + @Override + public boolean init( + Expression[] expressions, int pattern, Kleenean delayed, ParseResult result, + @Nullable SectionNode node, @Nullable List triggerItems + ) { + usesExecutable = pattern == 0; + + if (usesExecutable) { + //noinspection unchecked + executable = (Expression>) expressions[0]; + if (result.hasTag("arguments")) { + Expression expression = LiteralUtils.defendExpression(expressions[1]); + if (expression instanceof ExpressionList list) { + executableArguments = list.getExpressions(); + } else { + executableArguments = new Expression[]{expression}; + } + + return LiteralUtils.canInitSafely(executableArguments); + } + + return true; + } + + assert node != null; + + if (node.isEmpty()) { + Skript.error("A function section must contain arguments."); + return false; + } + + for (Node child : node) { + if (!(child instanceof SimpleNode) || child.getKey() == null) { + Skript.error(Language.get("functions.invalid argument in section"), child.getKey()); + return false; + } + + Matcher matcher = ARGUMENT_PATTERN.matcher(child.getKey()); + if (!matcher.matches()) { + Skript.error(Language.get("functions.invalid argument in section"), child.getKey()); + return false; + } + + arguments.add(new Argument<>(ArgumentType.NAMED, matcher.group("name"), matcher.group("value"))); + } + + String name = result.regexes.getFirst().group(); + if (!FUNCTION_NAME_PATTERN.matcher(name).matches()) { + Skript.error(Language.get("functions.does not exist"), name); + return false; + } + + FunctionReferenceParser parser = new FunctionReferenceParser(ParseContext.DEFAULT, SkriptParser.PARSE_EXPRESSIONS); + //noinspection unchecked + Argument[] array = (Argument[]) arguments.toArray(new Argument[0]); + try (ParseLogHandler log = SkriptLogger.startParseLogHandler()) { + reference = parser.parseFunctionReference(name, array, log); + } + + if (reference == null || this.arguments.isEmpty()) { + doesNotExist(name); + return false; + } + + if (reference.signature().returnType() == null) { + Skript.error(Language.get("functions.does not return"), name); + return false; + } + + return true; + } + + /** + * Prints the error for when a function does not exist. + * + * @param name The function name. + */ + private void doesNotExist(String name) { + StringJoiner joiner = new StringJoiner(", "); + + for (Argument argument : arguments) { + SkriptParser parser = new SkriptParser(argument.value(), SkriptParser.ALL_FLAGS, ParseContext.DEFAULT); + + Expression expression = LiteralUtils.defendExpression(parser.parseExpression(Object.class)); + if (!LiteralUtils.canInitSafely(expression)) { + joiner.add(argument.name() + ": ?"); + continue; + } + + Noun className = Classes.getSuperClassInfo(expression.getReturnType()).getName(); + if (expression.isSingle()) { + joiner.add(argument.name() + ": " + className.getSingular()); + } else { + joiner.add(argument.name() + ": " + className.getPlural()); + } + } + + Skript.error(Language.get("functions.does not exist"), "%s(%s)".formatted(name, joiner)); + } + + @Override + protected Object @Nullable [] get(Event event) { + if (usesExecutable) { + Executable executable = this.executable.getSingle(event); + if (executable == null) { + return null; + } + + Object result; + if (executableArguments == null) { + result = executable.execute(event); + } else { + Object[] arguments = new Object[executableArguments.length]; + for (int i = 0; i < arguments.length; i++) { + arguments[i] = executableArguments[i].getArray(event); + } + + result = executable.execute(event, arguments); + } + if (result instanceof Object[] results) { + return results; + } + return new Object[]{result}; + } + + if (reference == null) { + return null; + } + + Class returnType = reference.signature().returnType(); + if (returnType == null) { + return null; + } + + Object result = reference.execute(event); + if (result == null) { + return null; + } + + reference.function().resetReturnValue(); + + if (result.getClass().isArray()) { + return (Object[]) result; + } else { + return new Object[]{result}; + } + } + + @Override + public boolean isSingle() { + return usesExecutable ? executable.isSingle() : reference.isSingle(); + } + + @Override + public Class getReturnType() { + return usesExecutable ? executable.getReturnType() : (reference.signature().returnType() != null ? Utils.getComponentType(reference.signature().returnType()) : null); + } + + @Override + public String toString(@Nullable Event event, boolean debug) { + SyntaxStringBuilder builder = new SyntaxStringBuilder(event, debug) + .append("the result of function"); + + if (usesExecutable) { + builder.append(executable.toString(event, debug)); + } else { + builder.append(reference.name()); + } + + if (arguments.size() > 1) { + builder.append("with arguments"); + } else if (arguments.size() == 1) { + builder.append("with argument"); + } else { + return builder.toString(); + } + + if (usesExecutable) { + StringJoiner joiner = new StringJoiner(", "); + for (Expression argument : executableArguments) { + joiner.add(argument.toString(event, debug)); + } + + builder.append(joiner); + } else { + arguments.forEach(argument -> builder.append(argument.name() + ": " + argument.value() + ", ")); + } + + return builder.toString(); + } + +} diff --git a/src/main/java/org/skriptlang/skript/common/function/FunctionReferenceParser.java b/src/main/java/org/skriptlang/skript/common/function/FunctionReferenceParser.java index f5d93fd7ef1..5af390b12f5 100644 --- a/src/main/java/org/skriptlang/skript/common/function/FunctionReferenceParser.java +++ b/src/main/java/org/skriptlang/skript/common/function/FunctionReferenceParser.java @@ -44,7 +44,7 @@ public record FunctionReferenceParser(ParseContext context, int flags) { private static final ArgsMessage UNEXPECTED_ARGUMENT = new ArgsMessage("functions.unexpected argument"); private static final ArgsMessage INVALID_ARGUMENT = new ArgsMessage("functions.invalid argument"); - private static final ArgsMessage UNKNOWN_FUNCTION = new ArgsMessage("functions.unknown function"); + private static final ArgsMessage DOES_NOT_EXIST = new ArgsMessage("functions.does not exist"); private static final ArgsMessage POTENTIAL_SIGNATURE = new ArgsMessage("functions.potential signature"); /** @@ -539,7 +539,7 @@ private void doesNotExist(String name, FunctionReference.Argument[] argu if (intended.isPresent()) { possibleMatch = " " + POTENTIAL_SIGNATURE.toString(intended.get().toString(false, false)); } - Skript.error(UNKNOWN_FUNCTION.toString(name, joiner) + possibleMatch); + Skript.error(DOES_NOT_EXIST.toString("%s(%s)".formatted(name, joiner)) + possibleMatch); } /** diff --git a/src/main/resources/lang/english.lang b/src/main/resources/lang/english.lang index 9b29be974d7..c52ab48a8a6 100644 --- a/src/main/resources/lang/english.lang +++ b/src/main/resources/lang/english.lang @@ -238,7 +238,9 @@ functions: 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. + does not exist: The function '%s' does not exist. + does not return: The function '%s' does not return anything. + invalid argument in section: Invalid argument declaration for a function section: %s invalid argument: Can't understand the argument for the parameter '%s' with type '%s': %s. unexpected argument: The argument named '%s' is unexpected. - unknown function: The function %s(%s) does not exist. potential signature: Did you mean to use the function '%s'? diff --git a/src/test/skript/tests/syntaxes/expressions/ExprResult.sk b/src/test/skript/tests/syntaxes/sections/ExprSecFunction.sk similarity index 58% rename from src/test/skript/tests/syntaxes/expressions/ExprResult.sk rename to src/test/skript/tests/syntaxes/sections/ExprSecFunction.sk index 8eb8ad6ca8a..45d7ce7e411 100644 --- a/src/test/skript/tests/syntaxes/expressions/ExprResult.sk +++ b/src/test/skript/tests/syntaxes/sections/ExprSecFunction.sk @@ -1,3 +1,70 @@ +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 result": + set {_x} to result of function esf with arguments: + x set to 1 + assert {_x} = 1 + + set {_x} to result of function esf with arguments: + x set to "hey" + assert {_x} = 2 + + set {_x} to result of function esf with arguments: + x set to 1 and 2 + assert {_x} = 3 + + set {_x} to result of function esf with arguments: + x set to 1, 2, 3, {_a}, {_b::*} + assert {_x} = 3 + + parse: + set {_x} to result of function esf with arguments: + x set to {_y} + assert first element of last parse logs is set + + set {_y} to 3 + set {_x} to result of function esf with arguments: + x set to integer within {_y} + assert {_x} = 1 + + parse: + set {_x} to result of function esf_two with arguments: + x set to firework + assert first element of last parse logs contains "The function 'esf_two(x: item type)' does not exist" + + parse: + set {_x} to result of function esf with arguments: + x set to agasgasfgadsfg + assert first element of last parse logs contains "Can't understand the argument for the parameter" + + set {_x} to result of function esf_two with arguments: + x set to 1 + y set to 2 + assert {_x} = 4 + + set {_x} to result of function esf_two with arguments: + y set to 2 + x set to 1 + assert {_x} = 4 + + parse: + set {_x} to result of 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" + options: path: "../../../../../../src/test/skript/tests/syntaxes/expressions/" misc: "../../../../../../src/test/skript/tests/misc/" diff --git a/src/test/skript/tests/syntaxes/structures/StructFunction.sk b/src/test/skript/tests/syntaxes/structures/StructFunction.sk index 3d3eee94c9b..5ee1e6b86bf 100644 --- a/src/test/skript/tests/syntaxes/structures/StructFunction.sk +++ b/src/test/skript/tests/syntaxes/structures/StructFunction.sk @@ -65,7 +65,7 @@ local function argument_test_list(xs: ints): test "function structure arguments": parse: argument_test(1) - assert last parse logs contain "The function argument_test(integer) does not exist" + assert last parse logs contain "The function 'argument_test(integer)' does not exist" parse: argument_test(1, 2) @@ -73,7 +73,7 @@ test "function structure arguments": parse: argument_test(1, 2, 3) - assert last parse logs contain "The function argument_test(integer, integer, integer) does not exist" + assert last parse logs contain "The function 'argument_test(integer, integer, integer)' does not exist" parse: argument_test_list((1, 2, 3)) @@ -81,11 +81,11 @@ test "function structure arguments": parse: argument_test_list(1, (2, 3)) - assert last parse logs contain "The function argument_test_list(integer, integers) does not exist" + assert last parse logs contain "The function 'argument_test_list(integer, integers)' does not exist" parse: argument_test(1 if {_x} is 1, else 2, 3) - assert last parse logs is "The function argument_test(?, ?, integer) does not exist. Did you mean to use the function 'local argument_test(x: integer, y: integer)'?" + assert first element of last parse logs contains "The function 'argument_test(?, ?, integer)' does not exist" parse: argument_test((1 if {_x} is 1, else 2), 3) @@ -186,19 +186,19 @@ test "named function arguments with single list params": 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" + 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" + 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" + 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" + 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)