diff --git a/src/main/java/ch/njol/skript/expressions/ExprCaughtErrors.java b/src/main/java/ch/njol/skript/expressions/ExprCaughtErrors.java new file mode 100644 index 00000000000..927ba943c38 --- /dev/null +++ b/src/main/java/ch/njol/skript/expressions/ExprCaughtErrors.java @@ -0,0 +1,59 @@ +package ch.njol.skript.expressions; + +import ch.njol.skript.Skript; +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.lang.Expression; +import ch.njol.skript.lang.ExpressionType; +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; + +@Name("Last Caught Errors") +@Description("Gets the last caught runtime errors from a 'catch runtime errors' section.") +@Example(""" + catch runtime errors: + set worldborder center of {_border} to location(0, 0, NaN value) + if last caught runtime errors contains "Your location can't have a NaN value as one of its components": + set worldborder center of {_border} to location(0, 0, 0) + """) +@Since("INSERT VERSION") +public class ExprCaughtErrors extends SimpleExpression { + + static { + Skript.registerExpression(ExprCaughtErrors.class, String.class, ExpressionType.SIMPLE, + "last caught [run[ ]time] errors"); + } + + public static String[] lastErrors; + + @Override + public boolean init(Expression[] expressions, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { + return true; + } + + @Override + protected String @Nullable [] get(Event event) { + return lastErrors; + } + + @Override + public boolean isSingle() { + return false; + } + + @Override + public Class getReturnType() { + return String.class; + } + + @Override + public String toString(@Nullable Event event, boolean debug) { + return "last caught runtime errors"; + } + +} diff --git a/src/main/java/ch/njol/skript/sections/SecCatchErrors.java b/src/main/java/ch/njol/skript/sections/SecCatchErrors.java new file mode 100644 index 00000000000..9f65a8a41ae --- /dev/null +++ b/src/main/java/ch/njol/skript/sections/SecCatchErrors.java @@ -0,0 +1,75 @@ +package ch.njol.skript.sections; + +import ch.njol.skript.Skript; +import ch.njol.skript.config.SectionNode; +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.ExprCaughtErrors; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.Section; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.skript.lang.Trigger; +import ch.njol.skript.lang.TriggerItem; +import ch.njol.util.Kleenean; +import com.google.common.collect.Iterables; +import org.bukkit.event.Event; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.log.runtime.RuntimeError; +import org.skriptlang.skript.log.runtime.RuntimeErrorCatcher; + +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +@Name("Catch Runtime Errors") +@Description("Catch any runtime errors produced by code within the section. This is an in progress feature") +@Example(""" + catch runtime errors: + set worldborder center of {_border} to location(0, 0, NaN value) + if last caught runtime errors contains "Your location can't have a NaN value as one of its components": + set worldborder center of {_border} to location(0, 0, 0) + """) +@Since("INSERT VERSION") +public class SecCatchErrors extends Section { + + static { + Skript.registerSection(SecCatchErrors.class, "catch [run[ ]time] error[s]"); + } + + private Trigger trigger; + + @Override + public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult, SectionNode sectionNode, List triggerItems) { + if (Iterables.size(sectionNode) == 0) { + Skript.error("A catch errors section must contain code."); + return false; + } + + AtomicBoolean delayed = new AtomicBoolean(false); + Runnable afterLoading = () -> delayed.set(!getParser().getHasDelayBefore().isFalse()); + trigger = loadCode(sectionNode, "runtime", afterLoading, Event.class); + if (delayed.get()) { + Skript.error("Delays can't be used within a catch errors section."); + return false; + } + return true; + } + + @Override + protected @Nullable TriggerItem walk(Event event) { + RuntimeErrorCatcher catcher = new RuntimeErrorCatcher().start(); + TriggerItem.walk(trigger, event); + ExprCaughtErrors.lastErrors = catcher.getCachedErrors().stream().map(RuntimeError::error).toArray(String[]::new); + catcher.clearCachedErrors() + .clearCachedFrames() + .stop(); + return walk(event, false); + } + + @Override + public String toString(@Nullable Event event, boolean debug) { + return "catch runtime errors"; + } + +} diff --git a/src/main/java/org/skriptlang/skript/log/runtime/RuntimeErrorCatcher.java b/src/main/java/org/skriptlang/skript/log/runtime/RuntimeErrorCatcher.java new file mode 100644 index 00000000000..dbf3e34e940 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/log/runtime/RuntimeErrorCatcher.java @@ -0,0 +1,119 @@ +package org.skriptlang.skript.log.runtime; + +import ch.njol.skript.log.SkriptLogger; +import org.jetbrains.annotations.Unmodifiable; +import org.skriptlang.skript.log.runtime.Frame.FrameOutput; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map.Entry; +import java.util.logging.Level; + +/** + * A {@link RuntimeErrorConsumer} to be used in {@link RuntimeErrorManager} to catch {@link RuntimeError}s. + * This should always be used with {@link #start()} and {@link #stop()}. + */ +public class RuntimeErrorCatcher implements RuntimeErrorConsumer { + + private List storedConsumers = new ArrayList<>(); + + private final List cachedErrors = new ArrayList<>(); + + private final List> cachedFrames = new ArrayList<>(); + + public RuntimeErrorCatcher() {} + + /** + * Gets the {@link RuntimeErrorManager}. + */ + private RuntimeErrorManager getManager() { + return RuntimeErrorManager.getInstance(); + } + + /** + * Starts this {@link RuntimeErrorCatcher}, removing all {@link RuntimeErrorConsumer}s from {@link RuntimeErrorManager} + * and storing them in {@link #storedConsumers}. + * Makes this {@link RuntimeErrorCatcher} the only {@link RuntimeErrorConsumer} in {@link RuntimeErrorManager} + * to catch {@link RuntimeError}s. + * @return This {@link RuntimeErrorCatcher} + */ + public RuntimeErrorCatcher start() { + storedConsumers = getManager().removeAllConsumers(); + getManager().addConsumer(this); + return this; + } + + /** + * Stops this {@link RuntimeErrorCatcher}, removing from {@link RuntimeErrorManager} and restoring the previous + * {@link RuntimeErrorConsumer}s from {@link #storedConsumers}. + * Prints all cached {@link RuntimeError}s, {@link #cachedErrors}, and cached {@link FrameOutput}s, {@link #cachedFrames}. + */ + public void stop() { + if (!getManager().removeConsumer(this)) { + SkriptLogger.LOGGER.severe("[Skript] A 'RuntimeErrorCatcher' was stopped incorrectly."); + return; + } + getManager().addConsumers(storedConsumers.toArray(RuntimeErrorConsumer[]::new)); + for (RuntimeError runtimeError : cachedErrors) + storedConsumers.forEach(consumer -> consumer.printError(runtimeError)); + for (Entry entry : cachedFrames) + storedConsumers.forEach(consumer -> consumer.printFrameOutput(entry.getKey(), entry.getValue())); + } + + /** + * Gets all the cached {@link RuntimeError}s. + */ + public @Unmodifiable List getCachedErrors() { + return Collections.unmodifiableList(cachedErrors); + } + + /** + * Gets all cached {@link FrameOutput}s stored with its corresponding {@link Level} in an {@link Entry} + */ + public @Unmodifiable List> getCachedFrames() { + return Collections.unmodifiableList(cachedFrames); + } + + /** + * Clear all cached {@link RuntimeError}s. + */ + public RuntimeErrorCatcher clearCachedErrors() { + cachedErrors.clear(); + return this; + } + + /** + * Clears all cached {@link FrameOutput}s. + */ + public RuntimeErrorCatcher clearCachedFrames() { + cachedFrames.clear(); + return this; + } + + @Override + public void printError(RuntimeError error) { + cachedErrors.add(error); + } + + @Override + public void printFrameOutput(FrameOutput output, Level level) { + cachedFrames.add(new Entry() { + @Override + public FrameOutput getKey() { + return output; + } + + @Override + public Level getValue() { + return level; + } + + @Override + public Level setValue(Level value) { + return null; + } + }); + } + +} diff --git a/src/main/java/org/skriptlang/skript/log/runtime/RuntimeErrorManager.java b/src/main/java/org/skriptlang/skript/log/runtime/RuntimeErrorManager.java index b44688da1c0..e40aa34be87 100644 --- a/src/main/java/org/skriptlang/skript/log/runtime/RuntimeErrorManager.java +++ b/src/main/java/org/skriptlang/skript/log/runtime/RuntimeErrorManager.java @@ -10,6 +10,7 @@ import java.io.Closeable; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.logging.Level; @@ -132,13 +133,36 @@ public void addConsumer(RuntimeErrorConsumer consumer) { } } + /** + * Adds multiple {@link RuntimeErrorConsumer}s that will receive the emitted errors and frame output data. + * Consumers will be maintained when the manager is refreshed. + * @param newConsumers The {@link RuntimeErrorConsumer}s to add. + */ + public void addConsumers(RuntimeErrorConsumer ... newConsumers) { + synchronized (consumers) { + consumers.addAll(Arrays.asList(newConsumers)); + } + } + /** * Removes a {@link RuntimeErrorConsumer} from the tracked list. * @param consumer The consumer to remove. */ - public void removeConsumer(RuntimeErrorConsumer consumer) { + public boolean removeConsumer(RuntimeErrorConsumer consumer) { + synchronized (consumers) { + return consumers.remove(consumer); + } + } + + /** + * Removes all {@link RuntimeErrorConsumer}s that receive emitted errors and frame output data. + * @return All {@link RuntimeErrorConsumer}s removed. + */ + public List removeAllConsumers() { synchronized (consumers) { - consumers.remove(consumer); + List currentConsumers = List.copyOf(consumers); + consumers.clear(); + return currentConsumers; } } diff --git a/src/test/skript/tests/syntaxes/effects/EffWorldBorderExpand.sk b/src/test/skript/tests/syntaxes/effects/EffWorldBorderExpand.sk index 0541de7c500..bd8ded1606f 100644 --- a/src/test/skript/tests/syntaxes/effects/EffWorldBorderExpand.sk +++ b/src/test/skript/tests/syntaxes/effects/EffWorldBorderExpand.sk @@ -35,9 +35,13 @@ test "worldborder expand": assert worldborder radius of {_border} is 100 with "Growing border by None changed the radius" shrink {_border} to {_None} assert worldborder radius of {_border} is 100 with "Shrinking border to None changed the radius" - shrink {_border} by NaN value + catch runtime errors: + shrink {_border} by NaN value + assert last caught runtime errors contains "You can't shrink a world border by NaN." with "Shrinking by NaN did not throw error" assert worldborder radius of {_border} is 100 with "Shrinking border by NaN changed the radius" - grow {_border} to NaN value + catch runtime errors: + grow {_border} to NaN value + assert last caught runtime errors contains "You can't grow a world border to NaN." with "Growing by NaN did not throw error" assert worldborder radius of {_border} is 100 with "Growing border to NaN changed the radius" # infinite values diff --git a/src/test/skript/tests/syntaxes/expressions/ExprWorldBorderCenter.sk b/src/test/skript/tests/syntaxes/expressions/ExprWorldBorderCenter.sk index b3dd5ab27c0..4bc88e1210e 100644 --- a/src/test/skript/tests/syntaxes/expressions/ExprWorldBorderCenter.sk +++ b/src/test/skript/tests/syntaxes/expressions/ExprWorldBorderCenter.sk @@ -19,9 +19,13 @@ test "worldborder center": assert worldborder center of {_border} is location(-4.5, 0, -34.2, "world") with "setting location of border to None moved border" set worldborder center of {_border} to location({_None}, 0, {_None}) assert worldborder center of {_border} is location(-4.5, 0, -34.2, "world") with "using a location with None components changed the border center" - set worldborder center of {_border} to location(NaN value, 0, 1) + catch runtime errors: + set worldborder center of {_border} to location(NaN value, 0, 1) + assert last caught runtime errors contains "Your location can't have a NaN value as one of its components" with "x-coord NaN value did not throw error" assert worldborder center of {_border} is location(-4.5, 0, -34.2, "world") with "using a location with x-coord NaN changed the border center" - set worldborder center of {_border} to location(2, 0, NaN value) + catch runtime errors: + set worldborder center of {_border} to location(2, 0, NaN value) + assert last caught runtime errors contains "Your location can't have a NaN value as one of its components" with "z-coord NaN value did not throw error" assert worldborder center of {_border} is location(-4.5, 0, -34.2, "world") with "using a location with z-coord NaN changed the border center" set worldborder center of {_border} to location(infinity value, 0, infinity value) assert worldborder center of {_border} is location(29999984, 0, 29999984, "world") with "border center coords were not rounded correctly when using +infinity" diff --git a/src/test/skript/tests/syntaxes/expressions/ExprWorldBorderDamageAmount.sk b/src/test/skript/tests/syntaxes/expressions/ExprWorldBorderDamageAmount.sk index 443604d6d4b..c9b48d24146 100644 --- a/src/test/skript/tests/syntaxes/expressions/ExprWorldBorderDamageAmount.sk +++ b/src/test/skript/tests/syntaxes/expressions/ExprWorldBorderDamageAmount.sk @@ -9,11 +9,17 @@ test "worldborder damage amount": assert worldborder damage amount of {_border} is 1.5 with "Failed to set worldborder damage amount to a float" set worldborder damage amount of {_border} to {_None} assert worldborder damage amount of {_border} is 1.5 with "Setting worldborder damage amount to None changed the damage amount" - set worldborder damage amount of {_border} to NaN value + catch runtime errors: + set worldborder damage amount of {_border} to NaN value + assert last caught runtime errors contains "NaN is not a valid world border damage amount" with "NaN damage value did not throw error" assert worldborder damage amount of {_border} is 1.5 with "Setting worldborder damage amount to NaN value changed the damage amount" - set worldborder damage amount of {_border} to infinity value + catch runtime errors: + set worldborder damage amount of {_border} to infinity value + assert last caught runtime errors contains "World border damage amount cannot be infinite" with "Infinity damage value did not throw error" assert worldborder damage amount of {_border} is 1.5 with "Setting worldborder damage amount to infinity changed the damage amount" - set worldborder damage amount of {_border} to -infinity value + catch runtime errors: + set worldborder damage amount of {_border} to -infinity value + assert last caught runtime errors contains "World border damage amount cannot be infinite" with "Negative infinity damage value did not throw error" assert worldborder damage amount of {_border} is 1.5 with "Setting worldborder damage amount to -infinity changed the damage amount" # add tests @@ -27,7 +33,9 @@ test "worldborder damage amount": assert worldborder damage amount of {_border} is 1.5 with "Failed adding negative integer to damage amount" add {_None} to worldborder damage amount of {_border} assert worldborder damage amount of {_border} is 1.5 with "Adding None to worldborder damage amount changed the damage amount" - add NaN value to worldborder damage amount of {_border} + catch runtime errors: + add NaN value to worldborder damage amount of {_border} + assert last caught runtime errors contains "NaN is not a valid world border damage amount" with "Adding NaN damage value did not throw error" assert worldborder damage amount of {_border} is 1.5 with "Adding NaN value to worldborder damage amount changed the damage amount" add infinity value to worldborder damage amount of {_border} assert worldborder damage amount of {_border} is 1.5 with "Adding infinity to worldborder damage amount changed the damage amount"