diff --git a/src/main/java/ch/njol/skript/effects/EffReturn.java b/src/main/java/ch/njol/skript/effects/EffReturn.java index d0b4d220514..21ce3d4d475 100644 --- a/src/main/java/ch/njol/skript/effects/EffReturn.java +++ b/src/main/java/ch/njol/skript/effects/EffReturn.java @@ -26,21 +26,22 @@ import ch.njol.skript.doc.Since; import ch.njol.skript.lang.Effect; import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.ReturnHandler; +import ch.njol.skript.lang.ReturnHandler.ReturnHandlerStack; import ch.njol.skript.lang.SectionExitHandler; import ch.njol.skript.lang.SkriptParser.ParseResult; import ch.njol.skript.lang.TriggerItem; import ch.njol.skript.lang.TriggerSection; -import ch.njol.skript.lang.function.FunctionEvent; -import ch.njol.skript.lang.function.Functions; -import ch.njol.skript.lang.function.ScriptFunction; +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; import ch.njol.util.Kleenean; import org.bukkit.event.Event; import org.eclipse.jdt.annotation.Nullable; @Name("Return") -@Description("Makes a function return a value") +@Description("Makes a trigger (e.g. a function) return a value") @Examples({ "function double(i: number) :: number:", "\treturn 2 * {_i}", @@ -50,90 +51,89 @@ }) @Since("2.2, 2.8.0 (returns aliases)") public class EffReturn extends Effect { - + static { Skript.registerEffect(EffReturn.class, "return %objects%"); + ParserInstance.registerData(ReturnHandlerStack.class, ReturnHandlerStack::new); } - + @SuppressWarnings("NotNullFieldNotInitialized") - private ScriptFunction<?> function; - + private ReturnHandler<?> handler; @SuppressWarnings("NotNullFieldNotInitialized") private Expression<?> value; - - @SuppressWarnings("unchecked") + @Override + @SuppressWarnings("unchecked") public boolean init(Expression<?>[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { - ScriptFunction<?> f = Functions.currentFunction; - if (f == null) { - Skript.error("The return statement can only be used in a function"); + handler = getParser().getData(ReturnHandlerStack.class).getCurrentHandler(); + if (handler == null) { + Skript.error("The return statement cannot be used here"); return false; } - + if (!isDelayed.isFalse()) { Skript.error("A return statement after a delay is useless, as the calling trigger will resume when the delay starts (and won't get any returned value)"); return false; } - - function = f; - ClassInfo<?> returnType = function.getReturnType(); + + Class<?> returnType = handler.returnValueType(); if (returnType == null) { - Skript.error("This function doesn't return any value. Please use 'stop' or 'exit' if you want to stop the function."); + Skript.error(handler + " doesn't return any value. Please use 'stop' or 'exit' if you want to stop the trigger."); return false; } - + RetainingLogHandler log = SkriptLogger.startRetainingLog(); Expression<?> convertedExpr; try { - convertedExpr = exprs[0].getConvertedExpression(returnType.getC()); + convertedExpr = exprs[0].getConvertedExpression(returnType); if (convertedExpr == null) { - log.printErrors("This function is declared to return " + returnType.getName().withIndefiniteArticle() + ", but " + exprs[0].toString(null, false) + " is not of that type."); + String typeName = Classes.getSuperClassInfo(returnType).getName().withIndefiniteArticle(); + log.printErrors(handler + " is declared to return " + typeName + ", but " + exprs[0].toString(null, false) + " is not of that type."); return false; } log.printLog(); } finally { log.stop(); } - - if (f.isSingle() && !convertedExpr.isSingle()) { - Skript.error("This function is defined to only return a single " + returnType.toString() + ", but this return statement can return multiple values."); + + if (handler.isSingleReturnValue() && !convertedExpr.isSingle()) { + Skript.error(handler + " is defined to only return a single " + returnType + ", but this return statement can return multiple values."); return false; } value = convertedExpr; - + return true; } - + @Override @Nullable - @SuppressWarnings({"unchecked", "rawtypes"}) protected TriggerItem walk(Event event) { debug(event, false); - if (event instanceof FunctionEvent) { - ((ScriptFunction) function).setReturnValue(value.getArray(event)); - } else { - assert false : event; - } + //noinspection rawtypes,unchecked + ((ReturnHandler) handler).returnValues(value.getArray(event)); TriggerSection parent = getParent(); - while (parent != null) { + while (parent != null && parent != handler) { if (parent instanceof SectionExitHandler) ((SectionExitHandler) parent).exit(event); parent = parent.getParent(); } + if (handler instanceof SectionExitHandler) + ((SectionExitHandler) handler).exit(event); + return null; } - + @Override protected void execute(Event event) { assert false; } - + @Override public String toString(@Nullable Event event, boolean debug) { return "return " + value.toString(event, debug); } - + } diff --git a/src/main/java/ch/njol/skript/lang/ReturnHandler.java b/src/main/java/ch/njol/skript/lang/ReturnHandler.java new file mode 100644 index 00000000000..f908fc68c51 --- /dev/null +++ b/src/main/java/ch/njol/skript/lang/ReturnHandler.java @@ -0,0 +1,194 @@ +/** + * This file is part of Skript. + * + * Skript is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Skript is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Skript. If not, see <http://www.gnu.org/licenses/>. + * + * Copyright Peter Güttinger, SkriptLang team and contributors + */ +package ch.njol.skript.lang; + +import ch.njol.skript.ScriptLoader; +import ch.njol.skript.SkriptAPIException; +import ch.njol.skript.config.SectionNode; +import ch.njol.skript.lang.parser.ParserInstance; +import org.bukkit.event.Event; +import org.jetbrains.annotations.ApiStatus.NonExtendable; +import org.jetbrains.annotations.Nullable; + +import java.util.Deque; +import java.util.LinkedList; + +public interface ReturnHandler<T> { + + /** + * Loads the code in the given {@link SectionNode} using the same logic as + * {@link Section#loadCode(SectionNode)} and pushes the section onto the + * return handler stack + * <br> + * <b>This method may only be called by a {@link Section}</b> + * @throws SkriptAPIException if this return handler is not a {@link Section} + */ + @NonExtendable + default void loadReturnableSectionCode(SectionNode node) { + if (!(this instanceof Section)) + throw new SkriptAPIException("loadReturnableSectionCode called on a non-section object"); + ParserInstance parser = ParserInstance.get(); + ReturnHandlerStack stack = parser.getData(ReturnHandlerStack.class); + stack.push(this); + Section section = (Section) this; + try { + section.loadCode(node); + } finally { + stack.pop(); + } + } + + /** + * Loads the code in the given {@link SectionNode} using the same logic as + * {@link Section#loadCode(SectionNode, String, Class[])} and pushes the section onto the + * return handler stack + * <br> + * <b>This method may only be called by a {@link Section}</b> + * @param node the section node + * @param name the name of the event(s) being used + * @param events the event(s) during the section's execution + * @return a returnable trigger containing the loaded section. + * This should be stored and used to run the section one or more times + * @throws SkriptAPIException if this return handler is not a {@link Section} + */ + @NonExtendable + default ReturnableTrigger<T> loadReturnableSectionCode(SectionNode node, String name, Class<? extends Event>[] events) { + if (!(this instanceof Section)) + throw new SkriptAPIException("loadReturnableSectionCode called on a non-section object"); + ParserInstance parser = ParserInstance.get(); + ParserInstance.Backup parserBackup = parser.backup(); + parser.reset(); + + parser.setCurrentEvent(name, events); + SkriptEvent skriptEvent = new SectionSkriptEvent(name, (Section) this); + parser.setCurrentStructure(skriptEvent); + ReturnHandlerStack stack = parser.getData(ReturnHandlerStack.class); + + try { + return new ReturnableTrigger<>( + this, + parser.getCurrentScript(), + name, + skriptEvent, + trigger -> { + stack.push(trigger); + return ScriptLoader.loadItems(node); + } + ); + } finally { + stack.pop(); + parser.restoreBackup(parserBackup); + } + } + + /** + * Loads the code in the given {@link SectionNode} into a {@link ReturnableTrigger}. + * <br> + * This is a general method to load a section node without extra logic + * done to the {@link ParserInstance}. + * The calling code is expected to manage the {@code ParserInstance} accordingly, which may vary depending on + * where the code being loaded is located and what state the {@code ParserInstance} is in. + * @param node the section node to load + * @param name the name of the trigger + * @param event the {@link SkriptEvent} of the trigger + * @return a returnable trigger containing the loaded section node + */ + @NonExtendable + default ReturnableTrigger<T> loadReturnableTrigger(SectionNode node, String name, SkriptEvent event) { + ParserInstance parser = ParserInstance.get(); + ReturnHandlerStack stack = parser.getData(ReturnHandlerStack.class); + try { + return new ReturnableTrigger<T>( + this, + parser.getCurrentScript(), + name, + event, + trigger -> { + stack.push(trigger); + return ScriptLoader.loadItems(node); + } + ); + } finally { + stack.pop(); + } + } + + /** + * @param values the values to return + */ + void returnValues(T @Nullable [] values); + + /** + * @return whether this return handler may accept multiple return values + */ + boolean isSingleReturnValue(); + + /** + * The return type of this return handler, or null if it can't + * accept return values in this context (e.g. a function without a return type). + * + * @return the return type + */ + @Nullable Class<? extends T> returnValueType(); + + class ReturnHandlerStack extends ParserInstance.Data { + + private final Deque<ReturnHandler<?>> stack = new LinkedList<>(); + + public ReturnHandlerStack(ParserInstance parserInstance) { + super(parserInstance); + } + + public Deque<ReturnHandler<?>> getStack() { + return stack; + } + + /** + * Retrieves the current {@link ReturnHandler} + * @return the return data + */ + public @Nullable ReturnHandler<?> getCurrentHandler() { + return stack.peek(); + } + + /** + * Pushes the current return handler onto the return stack. + * <br> + * <b>Note: After the trigger finished loading, + * {@link ReturnHandlerStack#pop()} <u>MUST</u> be called</b> + * @param handler the return handler + * @see ReturnHandlerStack#pop() + */ + public void push(ReturnHandler<?> handler) { + stack.push(handler); + } + + /** + * Pops the current handler off the return stack. + * Should be called after the trigger has finished loading. + * @return the popped return data + * @see ReturnHandlerStack#push(ReturnHandler) + */ + public ReturnHandler<?> pop() { + return stack.pop(); + } + + } + +} diff --git a/src/main/java/ch/njol/skript/lang/ReturnableTrigger.java b/src/main/java/ch/njol/skript/lang/ReturnableTrigger.java new file mode 100644 index 00000000000..51c025ef70c --- /dev/null +++ b/src/main/java/ch/njol/skript/lang/ReturnableTrigger.java @@ -0,0 +1,53 @@ +/** + * This file is part of Skript. + * + * Skript is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Skript is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Skript. If not, see <http://www.gnu.org/licenses/>. + * + * Copyright Peter Güttinger, SkriptLang team and contributors + */ +package ch.njol.skript.lang; + +import org.eclipse.jdt.annotation.Nullable; +import org.skriptlang.skript.lang.script.Script; + +import java.util.Collections; +import java.util.List; +import java.util.function.Function; + +public class ReturnableTrigger<T> extends Trigger implements ReturnHandler<T> { + + private final ReturnHandler<T> handler; + + public ReturnableTrigger(ReturnHandler<T> handler, @Nullable Script script, String name, SkriptEvent event, Function<ReturnHandler<T>, List<TriggerItem>> loadItems) { + super(script, name, event, Collections.emptyList()); + this.handler = handler; + setTriggerItems(loadItems.apply(this)); + } + + @Override + public void returnValues(T @Nullable [] values) { + handler.returnValues(values); + } + + @Override + public boolean isSingleReturnValue() { + return handler.isSingleReturnValue(); + } + + @Override + public @Nullable Class<? extends T> returnValueType() { + return handler.returnValueType(); + } + +} 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 bc19f2bbf72..2eee7ea6c18 100644 --- a/src/main/java/ch/njol/skript/lang/function/Functions.java +++ b/src/main/java/ch/njol/skript/lang/function/Functions.java @@ -121,7 +121,7 @@ public static Function<?> loadFunction(Script script, SectionNode node, Signatur Skript.debug((signature.local ? "local " : "") + "function " + name + "(" + StringUtils.join(params, ", ") + ")" + (c != null ? " :: " + (signature.isSingle() ? c.getName().getSingular() : c.getName().getPlural()) : "") + ":"); - Function<?> f = new ScriptFunction<>(signature, script, node); + Function<?> f = new ScriptFunction<>(signature, node); // Register the function for signature namespace.addFunction(f); 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 259c8bcad85..d3a1e85117c 100644 --- a/src/main/java/ch/njol/skript/lang/function/ScriptFunction.java +++ b/src/main/java/ch/njol/skript/lang/function/ScriptFunction.java @@ -18,58 +18,49 @@ */ package ch.njol.skript.lang.function; +import ch.njol.skript.classes.ClassInfo; +import ch.njol.skript.lang.ReturnHandler; +import org.jetbrains.annotations.ApiStatus; import org.skriptlang.skript.lang.script.Script; import org.eclipse.jdt.annotation.Nullable; -import ch.njol.skript.ScriptLoader; import ch.njol.skript.config.SectionNode; import ch.njol.skript.effects.EffReturn; import ch.njol.skript.lang.Trigger; import ch.njol.skript.lang.util.SimpleEvent; import ch.njol.skript.variables.Variables; -/** - * @author Peter Güttinger - */ -public class ScriptFunction<T> extends Function<T> { - +public class ScriptFunction<T> extends Function<T> implements ReturnHandler<T> { + private final Trigger trigger; - + + private boolean returnValueSet; + private T @Nullable [] returnValues; + + /** + * @deprecated use {@link ScriptFunction#ScriptFunction(Signature, SectionNode)} + */ + @Deprecated public ScriptFunction(Signature<T> sign, Script script, SectionNode node) { + this(sign, node); + } + + public ScriptFunction(Signature<T> sign, SectionNode node) { super(sign); - + Functions.currentFunction = this; try { - trigger = new Trigger( - script, - "function " + sign.getName(), - new SimpleEvent(), - ScriptLoader.loadItems(node) - ); - trigger.setLineNumber(node.getLine()); + trigger = loadReturnableTrigger(node, "function " + sign.getName(), new SimpleEvent()); } finally { Functions.currentFunction = null; } + trigger.setLineNumber(node.getLine()); } - - private boolean returnValueSet = false; - @Nullable - private T[] returnValue = null; - - /** - * Should only be called by {@link EffReturn}. - */ - public final void setReturnValue(final @Nullable T[] value) { - assert !returnValueSet; - returnValueSet = true; - returnValue = value; - } - + // REMIND track possible types of local variables (including undefined variables) (consider functions, commands, and EffChange) - maybe make a general interface for this purpose // REM: use patterns, e.g. {_a%b%} is like "a.*", and thus subsequent {_axyz} may be set and of that type. @Override - @Nullable - public T[] execute(final FunctionEvent<?> e, final Object[][] params) { + public T @Nullable [] execute(final FunctionEvent<?> e, final Object[][] params) { Parameter<?>[] parameters = getSignature().getParameters(); for (int i = 0; i < parameters.length; i++) { Parameter<?> p = parameters[i]; @@ -84,14 +75,42 @@ public T[] execute(final FunctionEvent<?> e, final Object[][] params) { } trigger.execute(e); - return returnValue; + ClassInfo<T> returnType = getReturnType(); + return returnType != null ? returnValues : null; + } + + /** + * Should only be called by {@link EffReturn}. + * @deprecated Use {@link ScriptFunction#returnValues(Object[])} + */ + @Deprecated + @ApiStatus.Internal + public final void setReturnValue(@Nullable T[] values) { + returnValues(values); } @Override public boolean resetReturnValue() { - returnValue = null; returnValueSet = false; + returnValues = null; return true; } + @Override + public final void returnValues(T @Nullable [] values) { + assert !returnValueSet; + returnValueSet = true; + this.returnValues = values; + } + + @Override + public final boolean isSingleReturnValue() { + return isSingle(); + } + + @Override + public final @Nullable Class<? extends T> returnValueType() { + return getReturnType() != null ? getReturnType().getC() : null; + } + } diff --git a/src/main/java/ch/njol/skript/test/runner/SecReturnable.java b/src/main/java/ch/njol/skript/test/runner/SecReturnable.java new file mode 100644 index 00000000000..436b6d282fb --- /dev/null +++ b/src/main/java/ch/njol/skript/test/runner/SecReturnable.java @@ -0,0 +1,114 @@ +/** + * This file is part of Skript. + * + * Skript is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Skript is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Skript. If not, see <http://www.gnu.org/licenses/>. + * + * Copyright Peter Güttinger, SkriptLang team and contributors + */ +package ch.njol.skript.test.runner; + +import ch.njol.skript.Skript; +import ch.njol.skript.classes.ClassInfo; +import ch.njol.skript.config.SectionNode; +import ch.njol.skript.doc.NoDoc; +import ch.njol.skript.lang.*; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.skript.lang.util.SimpleExpression; +import ch.njol.util.Kleenean; +import org.bukkit.event.Event; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +@NoDoc +public class SecReturnable extends Section implements ReturnHandler<Object> { + + static { + Skript.registerSection(SecReturnable.class, "returnable [:plural] %*classinfo% section"); + } + + private ClassInfo<?> returnValueType; + private boolean singleReturnValue; + private static Object @Nullable [] returnedValues; + + @Override + public boolean init(Expression<?>[] expressions, int matchedPattern, Kleenean isDelayed, ParseResult parseResult, SectionNode sectionNode, List<TriggerItem> triggerItems) { + returnValueType = ((Literal<ClassInfo<?>>) expressions[0]).getSingle(); + singleReturnValue = !parseResult.hasTag("plural"); + loadReturnableSectionCode(sectionNode); + return true; + } + + @Override + protected @Nullable TriggerItem walk(Event event) { + return walk(event, true); + } + + @Override + public void returnValues(Object @Nullable [] values) { + returnedValues = values; + } + + @Override + public boolean isSingleReturnValue() { + return singleReturnValue; + } + + @Override + public @Nullable Class<?> returnValueType() { + return returnValueType.getC(); + } + + @Override + public String toString(@Nullable Event event, boolean debug) { + return "returnable " + (singleReturnValue ? "" : "plural ") + returnValueType.toString(event, debug) + " section"; + } + + @NoDoc + public static class ExprLastReturnValues extends SimpleExpression<Object> { + + static { + Skript.registerExpression(ExprLastReturnValues.class, Object.class, ExpressionType.SIMPLE, "[the] last return[ed] value[s]"); + } + + @Override + public boolean init(Expression<?>[] expressions, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { + return true; + } + + @Override + public @Nullable Object[] get(Event event) { + Object[] returnedValues = SecReturnable.returnedValues; + SecReturnable.returnedValues = null; + return returnedValues; + } + + @Override + public boolean isSingle() { + return false; + } + + @Override + public Class<?> getReturnType() { + return Object.class; + } + + @Override + public String toString(@Nullable Event event, boolean debug) { + return "last returned values"; + } + + } + +} diff --git a/src/test/skript/tests/misc/returns.sk b/src/test/skript/tests/misc/returns.sk new file mode 100644 index 00000000000..56e2937f7c5 --- /dev/null +++ b/src/test/skript/tests/misc/returns.sk @@ -0,0 +1,23 @@ +test "returns": + returnable string section: + return "hello" + assert last return value is "hello" with "failed single return value" + + returnable plural number section: + return (3 and 5) + assert last return values is 3 and 5 with "failed multiple return values" + + returnable integer section: + return 2.5 + assert last return value is 2 with "failed converted return value" + +test "returns (parsing)": + parse: + returnable string section: + return 1 + assert last parse logs is set with "skript shouldn't be able to return a different type" + + parse: + returnable string section: + return ("foo" and "bar") + assert last parse logs is set with "skript shouldn't be able to return multiple values for a single return trigger"