diff --git a/src/main/java/ch/njol/skript/ScriptLoader.java b/src/main/java/ch/njol/skript/ScriptLoader.java index d67e964c227..9d246d9565e 100644 --- a/src/main/java/ch/njol/skript/ScriptLoader.java +++ b/src/main/java/ch/njol/skript/ScriptLoader.java @@ -49,6 +49,7 @@ import org.jetbrains.annotations.Nullable; import org.skriptlang.skript.util.event.EventRegistry; import org.skriptlang.skript.lang.script.Script; +import org.skriptlang.skript.lang.script.ScriptWarning; import org.skriptlang.skript.lang.structure.Structure; import java.io.File; @@ -978,9 +979,10 @@ public static ArrayList loadItems(SectionNode node) { if (Skript.debug()) parser.setIndentation(parser.getIndentation() + " "); - + ArrayList items = new ArrayList<>(); + boolean executionStops = false; for (Node subNode : node) { parser.setNode(subNode); @@ -991,10 +993,11 @@ public static ArrayList loadItems(SectionNode node) { if (!SkriptParser.validateLine(expr)) continue; + TriggerItem item; if (subNode instanceof SimpleNode) { long start = System.currentTimeMillis(); - Statement stmt = Statement.parse(expr, items, "Can't understand this condition/effect: " + expr); - if (stmt == null) + item = Statement.parse(expr, items, "Can't understand this condition/effect: " + expr); + if (item == null) continue; long requiredTime = SkriptConfig.longParseTimeWarningThreshold.value().getMilliSeconds(); if (requiredTime > 0) { @@ -1007,34 +1010,44 @@ public static ArrayList loadItems(SectionNode node) { } if (Skript.debug() || subNode.debug()) - Skript.debug(SkriptColor.replaceColorChar(parser.getIndentation() + stmt.toString(null, true))); + Skript.debug(SkriptColor.replaceColorChar(parser.getIndentation() + item.toString(null, true))); - items.add(stmt); + items.add(item); } else if (subNode instanceof SectionNode) { TypeHints.enterScope(); // Begin conditional type hints - Section section = Section.parse(expr, "Can't understand this section: " + expr, (SectionNode) subNode, items); - if (section == null) + item = Section.parse(expr, "Can't understand this section: " + expr, (SectionNode) subNode, items); + if (item == null) continue; if (Skript.debug() || subNode.debug()) - Skript.debug(SkriptColor.replaceColorChar(parser.getIndentation() + section.toString(null, true))); + Skript.debug(SkriptColor.replaceColorChar(parser.getIndentation() + item.toString(null, true))); - items.add(section); + items.add(item); // Destroy these conditional type hints TypeHints.exitScope(); + } else { + continue; + } + + if (executionStops + && !SkriptConfig.disableUnreachableCodeWarnings.value() + && parser.isActive() + && !parser.getCurrentScript().suppressesWarning(ScriptWarning.UNREACHABLE_CODE)) { + Skript.warning("Unreachable code. The previous statement stops further execution."); } + executionStops = item.executionIntent() != null; } - + for (int i = 0; i < items.size() - 1; i++) items.get(i).setNext(items.get(i + 1)); parser.setNode(node); - + if (Skript.debug()) parser.setIndentation(parser.getIndentation().substring(0, parser.getIndentation().length() - 4)); - + return items; } diff --git a/src/main/java/ch/njol/skript/SkriptConfig.java b/src/main/java/ch/njol/skript/SkriptConfig.java index 436331d1fc6..0c6e0f347e2 100644 --- a/src/main/java/ch/njol/skript/SkriptConfig.java +++ b/src/main/java/ch/njol/skript/SkriptConfig.java @@ -174,6 +174,7 @@ public static String formatDate(final long timestamp) { public static final Option disableMissingAndOrWarnings = new Option<>("disable variable missing and/or warnings", false); public static final Option disableVariableStartingWithExpressionWarnings = new Option<>("disable starting a variable's name with an expression warnings", false); + public static final Option disableUnreachableCodeWarnings = new Option<>("disable unreachable code warnings", false); @Deprecated public static final Option enableScriptCaching = new Option<>("enable script caching", false) diff --git a/src/main/java/ch/njol/skript/effects/EffContinue.java b/src/main/java/ch/njol/skript/effects/EffContinue.java index ec10001435a..9dddbc96a16 100644 --- a/src/main/java/ch/njol/skript/effects/EffContinue.java +++ b/src/main/java/ch/njol/skript/effects/EffContinue.java @@ -19,20 +19,19 @@ package ch.njol.skript.effects; import ch.njol.skript.Skript; +import ch.njol.skript.classes.data.JavaClasses; 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.Effect; -import ch.njol.skript.lang.Expression; -import ch.njol.skript.lang.Literal; -import ch.njol.skript.lang.LoopSection; +import ch.njol.skript.lang.*; import ch.njol.skript.lang.SkriptParser.ParseResult; -import ch.njol.skript.lang.TriggerItem; +import ch.njol.skript.lang.parser.ParserInstance; import ch.njol.util.Kleenean; import ch.njol.util.StringUtils; import org.bukkit.event.Event; import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.UnknownNullability; import java.util.List; @@ -61,39 +60,45 @@ public class EffContinue extends Effect { static { Skript.registerEffect(EffContinue.class, "continue [this loop|[the] [current] loop]", - "continue [the] %*integer%(st|nd|rd|th) loop" + "continue [the] <" + JavaClasses.INTEGER_PATTERN + ">(st|nd|rd|th) loop" ); } - @SuppressWarnings("NotNullFieldNotInitialized") - private LoopSection loop; - @SuppressWarnings("NotNullFieldNotInitialized") - private List innerLoops; + // Used for toString + private int level; + + private @UnknownNullability LoopSection loop; + private @UnknownNullability List sectionsToExit; + private int breakLevels; @Override - @SuppressWarnings("unchecked") - public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { - List currentLoops = getParser().getCurrentSections(LoopSection.class); + public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { + level = matchedPattern == 0 ? 1 : Integer.parseInt(parseResult.regexes.get(0).group()); + if (level < 1) + return false; - int size = currentLoops.size(); - if (size == 0) { + ParserInstance parser = getParser(); + int loops = parser.getCurrentSections(LoopSection.class).size(); + if (loops == 0) { Skript.error("The 'continue' effect may only be used in loops"); return false; } - int level = matchedPattern == 0 ? size : ((Literal) exprs[0]).getSingle(); - if (level < 1) { - Skript.error("Can't continue the " + StringUtils.fancyOrderNumber(level) + " loop"); - return false; - } - if (level > size) { + // Section.getSections counts from the innermost section, so we need to invert the level + int levels = level == -1 ? 1 : loops - level + 1; + if (levels <= 0) { Skript.error("Can't continue the " + StringUtils.fancyOrderNumber(level) + " loop as there " + - (size == 1 ? "is only 1 loop" : "are only " + size + " loops") + " present"); + (loops == 1 ? "is only 1 loop" : "are only " + loops + " loops") + " present"); return false; } - loop = currentLoops.get(level - 1); - innerLoops = currentLoops.subList(level, size); + List innerSections = parser.getSections(levels, LoopSection.class); + breakLevels = innerSections.size(); + loop = (LoopSection) innerSections.remove(0); + sectionsToExit = innerSections.stream() + .filter(SectionExitHandler.class::isInstance) + .map(SectionExitHandler.class::cast) + .toList(); return true; } @@ -103,16 +108,21 @@ protected void execute(Event event) { } @Override - @Nullable - protected TriggerItem walk(Event event) { - for (LoopSection loop : innerLoops) - loop.exit(event); + protected @Nullable TriggerItem walk(Event event) { + debug(event, false); + for (SectionExitHandler section : sectionsToExit) + section.exit(event); return loop; } + @Override + public ExecutionIntent executionIntent() { + return ExecutionIntent.stopSections(breakLevels); + } + @Override public String toString(@Nullable Event event, boolean debug) { - return "continue"; + return "continue" + (level == -1 ? "" : " the " + StringUtils.fancyOrderNumber(level) + " loop"); } } diff --git a/src/main/java/ch/njol/skript/effects/EffExit.java b/src/main/java/ch/njol/skript/effects/EffExit.java index 3ed9948d33a..264f466becb 100644 --- a/src/main/java/ch/njol/skript/effects/EffExit.java +++ b/src/main/java/ch/njol/skript/effects/EffExit.java @@ -19,127 +19,125 @@ package ch.njol.skript.effects; import ch.njol.skript.Skript; +import ch.njol.skript.classes.data.JavaClasses; 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.Effect; -import ch.njol.skript.lang.Expression; -import ch.njol.skript.lang.SectionExitHandler; -import ch.njol.skript.lang.LoopSection; +import ch.njol.skript.lang.*; import ch.njol.skript.lang.SkriptParser.ParseResult; -import ch.njol.skript.lang.TriggerItem; -import ch.njol.skript.lang.TriggerSection; import ch.njol.skript.lang.parser.ParserInstance; -import ch.njol.skript.log.ErrorQuality; import ch.njol.skript.sections.SecConditional; import ch.njol.util.Kleenean; import org.bukkit.event.Event; import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.UnknownNullability; +import java.util.ArrayList; import java.util.List; @Name("Exit") @Description("Exits a given amount of loops and conditionals, or the entire trigger.") @Examples({ "if player has any ore:", - "\tstop", + "\tstop", "message \"%player% has no ores!\"", "loop blocks above the player:", - "\tloop-block is not air:", - "\t\texit 2 sections", - "\tset loop-block to water" + "\tloop-block is not air:", + "\t\texit 2 sections", + "\tset loop-block to water" }) @Since("unknown (before 2.1)") -public class EffExit extends Effect { // TODO [code style] warn user about code after a stop effect +public class EffExit extends Effect { static { Skript.registerEffect(EffExit.class, - "(exit|stop) [trigger]", - "(exit|stop) [(1|a|the|this)] (section|1:loop|2:conditional)", - "(exit|stop) <\\d+> (section|1:loop|2:conditional)s", - "(exit|stop) all (section|1:loop|2:conditional)s"); + "(exit|stop) [trigger]", + "(exit|stop) [1|a|the|this] (section|1:loop|2:conditional)", + "(exit|stop) <" + JavaClasses.INTEGER_PATTERN + "> (section|1:loop|2:conditional)s", + "(exit|stop) all (section|1:loop|2:conditional)s"); } - - private int breakLevels; - - private static final int EVERYTHING = 0; - private static final int LOOPS = 1; - private static final int CONDITIONALS = 2; + + @SuppressWarnings("unchecked") + private static final Class[] types = new Class[]{TriggerSection.class, LoopSection.class, SecConditional.class}; private static final String[] names = {"sections", "loops", "conditionals"}; private int type; - + + private int breakLevels; + private TriggerSection outerSection; + private @UnknownNullability List sectionsToExit; + @Override - public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parser) { + public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { + List innerSections = null; switch (matchedPattern) { - case 0: - breakLevels = getParser().getCurrentSections().size() + 1; - type = EVERYTHING; - break; - case 1: - case 2: - breakLevels = matchedPattern == 1 ? 1 : Integer.parseInt(parser.regexes.get(0).group()); - type = parser.mark; - if (breakLevels > numLevels(type)) { - if (numLevels(type) == 0) - Skript.error("can't stop any " + names[type] + " as there are no " + names[type] + " present", ErrorQuality.SEMANTIC_ERROR); - else - Skript.error("can't stop " + breakLevels + " " + names[type] + " as there are only " + numLevels(type) + " " + names[type] + " present", ErrorQuality.SEMANTIC_ERROR); + case 0 -> { + innerSections = getParser().getCurrentSections(); + breakLevels = innerSections.size() + 1; + } + case 1, 2 -> { + breakLevels = matchedPattern == 1 ? 1 : Integer.parseInt(parseResult.regexes.get(0).group()); + if (breakLevels < 1) + return false; + type = parseResult.mark; + ParserInstance parser = getParser(); + int levels = parser.getCurrentSections(types[type]).size(); + if (breakLevels > levels) { + if (levels == 0) { + Skript.error("Can't stop any " + names[type] + " as there are no " + names[type] + " present"); + } else { + Skript.error("Can't stop " + breakLevels + " " + names[type] + " as there are only " + levels + " " + names[type] + " present"); + } return false; } - break; - case 3: - type = parser.mark; - breakLevels = numLevels(type); - if (breakLevels == 0) { - Skript.error("can't stop any " + names[type] + " as there are no " + names[type] + " present", ErrorQuality.SEMANTIC_ERROR); + innerSections = parser.getSections(breakLevels, types[type]); + outerSection = innerSections.get(0); + } + case 3 -> { + ParserInstance parser = getParser(); + type = parseResult.mark; + List sections = parser.getCurrentSections(types[type]); + if (sections.isEmpty()) { + Skript.error("Can't stop any " + names[type] + " as there are no " + names[type] + " present"); return false; } - break; + outerSection = sections.get(0); + innerSections = parser.getSectionsUntil(outerSection); + innerSections.add(0, outerSection); + breakLevels = innerSections.size(); + } } + assert innerSections != null; + sectionsToExit = innerSections.stream() + .filter(SectionExitHandler.class::isInstance) + .map(SectionExitHandler.class::cast) + .toList(); return true; } - - private static int numLevels(int type) { - List currentSections = ParserInstance.get().getCurrentSections(); - if (type == EVERYTHING) - return currentSections.size(); - int level = 0; - for (TriggerSection section : currentSections) { - if (type == CONDITIONALS ? section instanceof SecConditional : section instanceof LoopSection) - level++; - } - return level; - } - + @Override - @Nullable - protected TriggerItem walk(Event event) { + protected @Nullable TriggerItem walk(Event event) { debug(event, false); - TriggerItem node = this; - for (int i = breakLevels; i > 0;) { - node = node.getParent(); - if (node == null) { - assert false : this; - return null; - } - if (node instanceof SectionExitHandler) - ((SectionExitHandler) node).exit(event); - - if (type == EVERYTHING || type == CONDITIONALS && node instanceof SecConditional || type == LOOPS && (node instanceof LoopSection)) - i--; - } - return node instanceof LoopSection ? ((LoopSection) node).getActualNext() : node.getNext(); + for (SectionExitHandler section : sectionsToExit) + section.exit(event); + if (outerSection == null) + return null; + return outerSection instanceof LoopSection loopSection ? loopSection.getActualNext() : outerSection.getNext(); } - + @Override protected void execute(Event event) { assert false; } - + + @Override + public @Nullable ExecutionIntent executionIntent() { + return ExecutionIntent.stopSections(breakLevels); + } + @Override public String toString(@Nullable Event event, boolean debug) { return "stop " + breakLevels + " " + names[type]; } - + } diff --git a/src/main/java/ch/njol/skript/effects/EffReturn.java b/src/main/java/ch/njol/skript/effects/EffReturn.java index 572cd80730c..cb20b32d5c3 100644 --- a/src/main/java/ch/njol/skript/effects/EffReturn.java +++ b/src/main/java/ch/njol/skript/effects/EffReturn.java @@ -23,14 +23,9 @@ import ch.njol.skript.doc.Examples; import ch.njol.skript.doc.Name; 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.*; 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.parser.ParserInstance; import ch.njol.skript.log.RetainingLogHandler; import ch.njol.skript.log.SkriptLogger; @@ -38,9 +33,12 @@ import ch.njol.util.Kleenean; import org.bukkit.event.Event; import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.UnknownNullability; + +import java.util.List; @Name("Return") -@Description("Makes a trigger (e.g. a function) return a value") +@Description("Makes a trigger or a section (e.g. a function) return a value") @Examples({ "function double(i: number) :: number:", "\treturn 2 * {_i}", @@ -56,15 +54,16 @@ public class EffReturn extends Effect { ParserInstance.registerData(ReturnHandlerStack.class, ReturnHandlerStack::new); } - @SuppressWarnings("NotNullFieldNotInitialized") - private ReturnHandler handler; - @SuppressWarnings("NotNullFieldNotInitialized") - private Expression value; + private @UnknownNullability ReturnHandler handler; + private @UnknownNullability Expression value; + private @UnknownNullability List sectionsToExit; + private int breakLevels; @Override @SuppressWarnings("unchecked") public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { - handler = getParser().getData(ReturnHandlerStack.class).getCurrentHandler(); + ParserInstance parser = getParser(); + handler = parser.getData(ReturnHandlerStack.class).getCurrentHandler(); if (handler == null) { Skript.error("The return statement cannot be used here"); return false; @@ -102,28 +101,26 @@ public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelaye } value = convertedExpr; + List innerSections = parser.getSectionsUntil((TriggerSection) handler); + innerSections.add(0, (TriggerSection) handler); + breakLevels = innerSections.size(); + sectionsToExit = innerSections.stream() + .filter(SectionExitHandler.class::isInstance) + .map(SectionExitHandler.class::cast) + .toList(); return true; } @Override - @Nullable - protected TriggerItem walk(Event event) { + protected @Nullable TriggerItem walk(Event event) { debug(event, false); //noinspection rawtypes,unchecked ((ReturnHandler) handler).returnValues(event, value); - TriggerSection parent = getParent(); - while (parent != null && parent != handler) { - if (parent instanceof SectionExitHandler) - ((SectionExitHandler) parent).exit(event); - - parent = parent.getParent(); - } - - if (handler instanceof SectionExitHandler) - ((SectionExitHandler) handler).exit(event); + for (SectionExitHandler section : sectionsToExit) + section.exit(event); - return null; + return ((TriggerSection) handler).getNext(); } @Override @@ -131,6 +128,11 @@ protected void execute(Event event) { assert false; } + @Override + public ExecutionIntent executionIntent() { + return ExecutionIntent.stopSections(breakLevels); + } + @Override public String toString(@Nullable Event event, boolean debug) { return "return " + value.toString(event, debug); diff --git a/src/main/java/ch/njol/skript/effects/EffSuppressWarnings.java b/src/main/java/ch/njol/skript/effects/EffSuppressWarnings.java index 2c56e83587d..7fadcb0b33d 100644 --- a/src/main/java/ch/njol/skript/effects/EffSuppressWarnings.java +++ b/src/main/java/ch/njol/skript/effects/EffSuppressWarnings.java @@ -29,6 +29,7 @@ import ch.njol.util.Kleenean; import org.bukkit.event.Event; import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.UnknownNullability; import org.skriptlang.skript.lang.script.ScriptWarning; @Name("Locally Suppress Warning") @@ -41,13 +42,17 @@ public class EffSuppressWarnings extends Effect { static { - Skript.registerEffect(EffSuppressWarnings.class, - "[local[ly]] suppress [the] (1:conflict|2:variable save|3:[missing] conjunction[s]|4:starting [with] expression[s]|5:deprecated syntax) warning[s]" - ); + StringBuilder warnings = new StringBuilder(); + ScriptWarning[] values = ScriptWarning.values(); + for (int i = 0; i < values.length; i++) { + if (i != 0) + warnings.append('|'); + warnings.append(values[i].ordinal()).append(':').append(values[i].getPattern()); + } + Skript.registerEffect(EffSuppressWarnings.class, "[local[ly]] suppress [the] (" + warnings + ") warning[s]"); } - private static final int CONFLICT = 1, INSTANCE = 2, CONJUNCTION = 3, START_EXPR = 4, DEPRECATED = 5; - private int mark = 0; + private @UnknownNullability ScriptWarning warning; @Override public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, SkriptParser.ParseResult parseResult) { @@ -56,11 +61,11 @@ public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelaye return false; } - mark = parseResult.mark; - if (mark == 1) { - Skript.warning("Variable conflict warnings no longer need suppression, as they have been removed altogether"); + warning = ScriptWarning.values()[parseResult.mark]; + if (warning.isDeprecated()) { + Skript.warning(warning.getDeprecationMessage()); } else { - getParser().getCurrentScript().suppressWarning(ScriptWarning.values()[mark - 2]); + getParser().getCurrentScript().suppressWarning(warning); } return true; } @@ -70,27 +75,7 @@ protected void execute(Event event) { } @Override public String toString(@Nullable Event event, boolean debug) { - String word; - switch (mark) { - case CONFLICT: - word = "conflict"; - break; - case INSTANCE: - word = "variable save"; - break; - case CONJUNCTION: - word = "missing conjunction"; - break; - case START_EXPR: - word = "starting expression"; - break; - case DEPRECATED: - word = "deprecated syntax"; - break; - default: - throw new IllegalStateException(); - } - return "suppress " + word + " warnings"; + return "suppress " + warning.getWarningName() + " warnings"; } } diff --git a/src/main/java/ch/njol/skript/lang/ExecutionIntent.java b/src/main/java/ch/njol/skript/lang/ExecutionIntent.java new file mode 100644 index 00000000000..da67e862f44 --- /dev/null +++ b/src/main/java/ch/njol/skript/lang/ExecutionIntent.java @@ -0,0 +1,141 @@ +package ch.njol.skript.lang; + +import com.google.common.base.MoreObjects; +import com.google.common.base.Preconditions; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; + +/** + * Used to describe the intention of a {@link TriggerItem}. + * As of now, it is only used to tell whether the item halts the execution or not and print the appropriate warnings. + * + * @see TriggerItem#executionIntent() + */ +public sealed interface ExecutionIntent extends Comparable + permits ExecutionIntent.StopTrigger, ExecutionIntent.StopSections { + + /** + * Creates a new stop trigger intent. + * + * @return a new stop trigger intent. + */ + @Contract(value = " -> new", pure = true) + static StopTrigger stopTrigger() { + return new StopTrigger(); + } + + /** + * Creates a new stop sections intent. + * + * @param levels the number of levels to stop. + * @return a new stop sections intent. + * @throws IllegalArgumentException if the depth is less than 1. + */ + @Contract(value = "_ -> new", pure = true) + static StopSections stopSections(int levels) { + Preconditions.checkArgument(levels > 0, "Depth must be at least 1"); + return new StopSections(levels); + } + + /** + * Creates a new stop sections intent with a depth of 1. + * + * @return a new stop sections intent. + */ + @Contract(value = " -> new", pure = true) + static StopSections stopSection() { + return new StopSections(1); + } + + /** + * Uses the current ExecutionIntent. + * + * @return a new ExecutionIntent, or null if it's exhausted. + */ + @Nullable ExecutionIntent use(); + + /** + * Represents a stop trigger intent. This intent stops the execution of the trigger. + */ + final class StopTrigger implements ExecutionIntent { + + private StopTrigger() {} + + @Override + public StopTrigger use() { + return new StopTrigger(); + } + + @Override + @SuppressWarnings("ComparatorMethodParameterNotUsed") + public int compareTo(@NotNull ExecutionIntent other) { + return other instanceof StopTrigger ? 0 : 1; + } + + @Override + public String toString() { + return "StopTrigger"; + } + + } + + /** + * Represents a stop sections intent. + * This intent stops the execution of the current section and the specified number of levels. + */ + final class StopSections implements ExecutionIntent { + + private final int levels; + + private StopSections(int levels) { + this.levels = levels; + } + + public @Nullable ExecutionIntent.StopSections use() { + return levels > 1 ? new StopSections(levels - 1) : null; + } + + @Override + public int compareTo(@NotNull ExecutionIntent other) { + if (!(other instanceof StopSections)) + return other.compareTo(this) * -1; + int levels = ((StopSections) other).levels; + return Integer.compare(this.levels, levels); + } + + /** + * Returns the number of levels to stop. + * + * @return the number of levels to stop. + */ + public int levels() { + return levels; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + if (obj == null || obj.getClass() != this.getClass()) + return false; + return this.levels == ((StopSections) obj).levels; + } + + @Override + public int hashCode() { + return Objects.hash(levels); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("levels", levels) + .toString(); + } + + } + +} diff --git a/src/main/java/ch/njol/skript/lang/ExpressionList.java b/src/main/java/ch/njol/skript/lang/ExpressionList.java index 9c89358431d..e4b68bd880c 100644 --- a/src/main/java/ch/njol/skript/lang/ExpressionList.java +++ b/src/main/java/ch/njol/skript/lang/ExpressionList.java @@ -28,13 +28,10 @@ import ch.njol.util.coll.CollectionUtils; import org.bukkit.event.Event; import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Unmodifiable; import java.lang.reflect.Array; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Iterator; -import java.util.List; -import java.util.NoSuchElementException; +import java.util.*; /** * A list of expressions. @@ -302,6 +299,23 @@ public Expression[] getExpressions() { return expressions; } + /** + * Retrieves all expressions, including those nested within any {@code ExpressionList}s. + * + * @return A list of all expressions. + */ + public List> getAllExpressions() { + List> expressions = new ArrayList<>(); + for (Expression expression : this.expressions) { + if (expression instanceof ExpressionList innerList) { + expressions.addAll(innerList.getAllExpressions()); + continue; + } + expressions.add(expression); + } + return expressions; + } + @Override @SuppressWarnings("unchecked") public Expression simplify() { diff --git a/src/main/java/ch/njol/skript/lang/TriggerItem.java b/src/main/java/ch/njol/skript/lang/TriggerItem.java index eb192be11f4..6a517a6c601 100644 --- a/src/main/java/ch/njol/skript/lang/TriggerItem.java +++ b/src/main/java/ch/njol/skript/lang/TriggerItem.java @@ -110,6 +110,20 @@ public static boolean walk(TriggerItem start, Event event) { return false; } + /** + * Returns whether this item stops the execution of the current trigger or section(s). + *
+ * If present, and there are statement(s) after this one, the parser will print a warning + * to the user. + *

+ * Note: This method is used purely to print warnings and doesn't affect parsing, execution or anything else. + * + * @return whether this item stops the execution of the current trigger or section. + */ + public @Nullable ExecutionIntent executionIntent() { + return null; + } + /** * how much to indent each level */ @@ -167,4 +181,15 @@ public TriggerItem setNext(@Nullable TriggerItem next) { return next; } + /** + * This method guarantees to return next {@link TriggerItem} after this item. + * This is not always the case for {@link #getNext()}, for example, {@code getNext()} + * of a {@link ch.njol.skript.sections.SecLoop loop section} usually returns itself. + * + * @return The next {@link TriggerItem}. + */ + public @Nullable TriggerItem getActualNext() { + return next; + } + } diff --git a/src/main/java/ch/njol/skript/lang/TriggerSection.java b/src/main/java/ch/njol/skript/lang/TriggerSection.java index c861805525a..2f479f88791 100644 --- a/src/main/java/ch/njol/skript/lang/TriggerSection.java +++ b/src/main/java/ch/njol/skript/lang/TriggerSection.java @@ -113,4 +113,20 @@ protected final boolean run(Event event) { } } + /** + * @return The execution intent of the section's trigger. + */ + protected @Nullable ExecutionIntent triggerExecutionIntent() { + TriggerItem current = first; + while (current != null) { + ExecutionIntent executionIntent = current.executionIntent(); + if (executionIntent != null) + return executionIntent.use(); + if (current == last) + break; + current = current.getActualNext(); + } + return null; + } + } diff --git a/src/main/java/ch/njol/skript/lang/parser/ParserInstance.java b/src/main/java/ch/njol/skript/lang/parser/ParserInstance.java index e2acd362443..3e05612c0a7 100644 --- a/src/main/java/ch/njol/skript/lang/parser/ParserInstance.java +++ b/src/main/java/ch/njol/skript/lang/parser/ParserInstance.java @@ -31,6 +31,7 @@ import ch.njol.skript.structures.StructOptions.OptionsData; import ch.njol.util.Kleenean; import ch.njol.util.coll.CollectionUtils; +import com.google.common.base.Preconditions; import org.bukkit.event.Event; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -323,6 +324,68 @@ public List getCurrentSections() { return list; } + /** + * Returns the sections from the current section (inclusive) until the specified section (exclusive). + *

+ * If we have the following sections: + *

{@code
+	 * Section1
+	 *   └ Section2
+	 *       └ Section3} (we are here)
+ * And we call {@code getSectionsUntil(Section1)}, the result will be {@code [Section2, Section3]}. + * + * @param section The section to stop at. (exclusive) + * @return A list of sections from the current section (inclusive) until the specified section (exclusive). + */ + public List getSectionsUntil(TriggerSection section) { + return new ArrayList<>(currentSections.subList(currentSections.indexOf(section) + 1, currentSections.size())); + } + + /** + * Returns a list of sections up to the specified number of levels from the current section. + *

+ * If we have the following sections: + *

{@code
+	 * Section1
+	 *   └ Section2
+	 *       └ Section3} (we are here)
+ * And we call {@code getSections(2)}, the result will be {@code [Section2, Section3]}. + * + * @param levels The number of levels to retrieve from the current section upwards. Must be greater than 0. + * @return A list of sections up to the specified number of levels. + * @throws IllegalArgumentException if the levels is less than 1. + */ + public List getSections(int levels) { + Preconditions.checkArgument(levels > 0, "Depth must be at least 1"); + return new ArrayList<>(currentSections.subList(Math.max(currentSections.size() - levels, 0), currentSections.size())); + } + + /** + * Returns a list of sections to the specified number of levels from the current section. + * Only counting sections of the specified type. + *

+ * If we have the following sections: + *

{@code
+	 * Section1
+	 *   └ LoopSection2
+	 *       └ Section3
+	 *           └ LoopSection4} (we are here)
+ * And we call {@code getSections(2, LoopSection.class)}, the result will be {@code [LoopSection2, Section3, LoopSection4]}. + * + * @param levels The number of levels to retrieve from the current section upwards. Must be greater than 0. + * @param type The class type of the sections to count. + * @return A list of sections of the specified type up to the specified number of levels. + * @throws IllegalArgumentException if the levels is less than 1. + */ + public List getSections(int levels, Class type) { + Preconditions.checkArgument(levels > 0, "Depth must be at least 1"); + List sections = getCurrentSections(type); + if (sections.isEmpty()) + return new ArrayList<>(); + TriggerSection section = sections.get(Math.max(sections.size() - levels, 0)); + return new ArrayList<>(currentSections.subList(currentSections.indexOf(section), currentSections.size())); + } + /** * @return Whether {@link #getCurrentSections()} contains * a section instance of the given class (or subclass). diff --git a/src/main/java/ch/njol/skript/sections/SecConditional.java b/src/main/java/ch/njol/skript/sections/SecConditional.java index c74730854a9..01aac81bf50 100644 --- a/src/main/java/ch/njol/skript/sections/SecConditional.java +++ b/src/main/java/ch/njol/skript/sections/SecConditional.java @@ -26,11 +26,8 @@ import ch.njol.skript.doc.Examples; import ch.njol.skript.doc.Name; import ch.njol.skript.doc.Since; -import ch.njol.skript.lang.Condition; -import ch.njol.skript.lang.Expression; -import ch.njol.skript.lang.Section; +import ch.njol.skript.lang.*; import ch.njol.skript.lang.SkriptParser.ParseResult; -import ch.njol.skript.lang.TriggerItem; import ch.njol.skript.lang.parser.ParserInstance; import ch.njol.skript.lang.util.ContextlessEvent; import ch.njol.skript.patterns.PatternCompiler; @@ -103,6 +100,7 @@ private enum ConditionalType { private boolean multiline; private Kleenean hasDelayAfter; + private @Nullable ExecutionIntent executionIntent; @Override public boolean init(Expression[] exprs, @@ -115,6 +113,7 @@ public boolean init(Expression[] exprs, ifAny = parseResult.hasTag("any"); parseIf = parseResult.hasTag("parse"); multiline = parseResult.regexes.size() == 0 && type != ConditionalType.ELSE; + ParserInstance parser = getParser(); // ensure this conditional is chained correctly (e.g. an else must have an if) if (type != ConditionalType.IF) { @@ -149,7 +148,7 @@ public boolean init(Expression[] exprs, } else { // if this is a multiline if, we need to check if there is a "then" section after this if (multiline) { - Node nextNode = getNextNode(sectionNode, getParser()); + Node nextNode = getNextNode(sectionNode, parser); String error = (ifAny ? "'if any'" : "'if all'") + " has to be placed just before a 'then' section"; if (nextNode instanceof SectionNode && nextNode.getKey() != null) { String nextNodeKey = ScriptLoader.replaceOptions(nextNode.getKey()); @@ -166,7 +165,6 @@ public boolean init(Expression[] exprs, // if this an "if" or "else if", let's try to parse the conditions right away if (type == ConditionalType.IF || type == ConditionalType.ELSE_IF) { - ParserInstance parser = getParser(); Class[] currentEvents = parser.getCurrentEvents(); String currentEventName = parser.getCurrentEventName(); @@ -228,10 +226,37 @@ public boolean init(Expression[] exprs, parseIfPassed = true; } - Kleenean hadDelayBefore = getParser().getHasDelayBefore(); + Kleenean hadDelayBefore = parser.getHasDelayBefore(); if (!multiline || type == ConditionalType.THEN) loadCode(sectionNode); - hasDelayAfter = getParser().getHasDelayBefore(); + + // Get the execution intent of the entire conditional chain. + if (type == ConditionalType.ELSE) { + List conditionals = getPrecedingConditionals(triggerItems); + conditionals.add(0, this); + for (SecConditional conditional : conditionals) { + // Continue if the current conditional doesn't have executable code (the 'if' section of a multiline). + if (conditional.multiline && conditional.type != ConditionalType.THEN) + continue; + + // If the current conditional doesn't have an execution intent, + // then there is a possibility of the chain not stopping the execution. + // Therefore, we can't assume anything about the intention of the chain, + // so we just set it to null and break out of the loop. + ExecutionIntent triggerIntent = conditional.triggerExecutionIntent(); + if (triggerIntent == null) { + executionIntent = null; + break; + } + + // If the current trigger's execution intent has a lower value than the chain's execution intent, + // then set the chain's intent to the trigger's + if (executionIntent == null || triggerIntent.compareTo(executionIntent) < 0) + executionIntent = triggerIntent; + } + } + + hasDelayAfter = parser.getHasDelayBefore(); // If the code definitely has a delay before this section, or if the section did not alter the delayed Kleenean, // there's no need to change the Kleenean. @@ -247,16 +272,16 @@ public boolean init(Expression[] exprs, && getElseIfs(triggerItems).stream().map(SecConditional::getHasDelayAfter).allMatch(Kleenean::isTrue)) { // ... if the if section, all else-if sections and the else section have definite delays, // mark delayed as TRUE. - getParser().setHasDelayBefore(Kleenean.TRUE); + parser.setHasDelayBefore(Kleenean.TRUE); } else { // ... otherwise mark delayed as UNKNOWN. - getParser().setHasDelayBefore(Kleenean.UNKNOWN); + parser.setHasDelayBefore(Kleenean.UNKNOWN); } } else { if (!hasDelayAfter.isFalse()) { // If an if section or else-if section has some delay (definite or possible) in it, // set the delayed Kleenean to UNKNOWN. - getParser().setHasDelayBefore(Kleenean.UNKNOWN); + parser.setHasDelayBefore(Kleenean.UNKNOWN); } } @@ -269,33 +294,41 @@ public TriggerItem getNext() { return getSkippedNext(); } - @Nullable - public TriggerItem getNormalNext() { - return super.getNext(); - } - @Nullable @Override protected TriggerItem walk(Event event) { if (type == ConditionalType.THEN || (parseIf && !parseIfPassed)) { - return getNormalNext(); + return getActualNext(); } else if (parseIf || checkConditions(event)) { // if this is a multiline if, we need to run the "then" section instead - SecConditional sectionToRun = multiline ? (SecConditional) getNormalNext() : this; + SecConditional sectionToRun = multiline ? (SecConditional) getActualNext() : this; TriggerItem skippedNext = getSkippedNext(); if (sectionToRun.last != null) sectionToRun.last.setNext(skippedNext); return sectionToRun.first != null ? sectionToRun.first : skippedNext; } else { - return getNormalNext(); + return getActualNext(); } } + @Override + public @Nullable ExecutionIntent executionIntent() { + return executionIntent; + } + + @Override + public ExecutionIntent triggerExecutionIntent() { + if (multiline && type != ConditionalType.THEN) + // Handled in the 'then' section + return null; + return super.triggerExecutionIntent(); + } + @Nullable private TriggerItem getSkippedNext() { - TriggerItem next = getNormalNext(); + TriggerItem next = getActualNext(); while (next instanceof SecConditional && ((SecConditional) next).type != ConditionalType.IF) - next = ((SecConditional) next).getNormalNext(); + next = next.getActualNext(); return next; } @@ -335,9 +368,7 @@ private static SecConditional getPrecedingConditional(List triggerI // loop through the triggerItems in reverse order so that we find the most recent items first for (int i = triggerItems.size() - 1; i >= 0; i--) { TriggerItem triggerItem = triggerItems.get(i); - if (triggerItem instanceof SecConditional) { - SecConditional conditionalSection = (SecConditional) triggerItem; - + if (triggerItem instanceof SecConditional conditionalSection) { if (conditionalSection.type == ConditionalType.ELSE) { // if the conditional is an else, return null because it belongs to a different condition and ends // this one @@ -353,17 +384,28 @@ private static SecConditional getPrecedingConditional(List triggerI return null; } + private static List getPrecedingConditionals(List triggerItems) { + List conditionals = new ArrayList<>(); + // loop through the triggerItems in reverse order so that we find the most recent items first + for (int i = triggerItems.size() - 1; i >= 0; i--) { + TriggerItem triggerItem = triggerItems.get(i); + if (!(triggerItem instanceof SecConditional conditional)) + break; + if (conditional.type == ConditionalType.ELSE) + // if the conditional is an else, break because it belongs to a different condition and ends + // this one + break; + conditionals.add(conditional); + } + return conditionals; + } + private static List getElseIfs(List triggerItems) { List list = new ArrayList<>(); for (int i = triggerItems.size() - 1; i >= 0; i--) { TriggerItem triggerItem = triggerItems.get(i); - if (triggerItem instanceof SecConditional) { - SecConditional secConditional = (SecConditional) triggerItem; - - if (secConditional.type == ConditionalType.ELSE_IF) - list.add(secConditional); - else - break; + if (triggerItem instanceof SecConditional secConditional && secConditional.type == ConditionalType.ELSE_IF) { + list.add(secConditional); } else { break; } diff --git a/src/main/java/ch/njol/skript/sections/SecLoop.java b/src/main/java/ch/njol/skript/sections/SecLoop.java index ef3728ddc9e..419583c6eb3 100644 --- a/src/main/java/ch/njol/skript/sections/SecLoop.java +++ b/src/main/java/ch/njol/skript/sections/SecLoop.java @@ -25,11 +25,8 @@ import ch.njol.skript.doc.Examples; import ch.njol.skript.doc.Name; import ch.njol.skript.doc.Since; -import ch.njol.skript.lang.Expression; -import ch.njol.skript.lang.LoopSection; +import ch.njol.skript.lang.*; import ch.njol.skript.lang.SkriptParser.ParseResult; -import ch.njol.skript.lang.TriggerItem; -import ch.njol.skript.lang.Variable; import ch.njol.skript.lang.util.ContainerExpression; import ch.njol.skript.util.Container; import ch.njol.skript.util.Container.ContainerType; @@ -92,8 +89,8 @@ public class SecLoop extends LoopSection { private final transient Map current = new WeakHashMap<>(); private final transient Map> currentIter = new WeakHashMap<>(); - @Nullable - private TriggerItem actualNext; + private @Nullable TriggerItem actualNext; + private boolean guaranteedToLoop; @Override @SuppressWarnings("unchecked") @@ -121,6 +118,7 @@ public boolean init(Expression[] exprs, return false; } + guaranteedToLoop = guaranteedToLoop(expr); loadOptionalCode(sectionNode); super.setNext(this); @@ -132,7 +130,7 @@ public boolean init(Expression[] exprs, protected TriggerItem walk(Event event) { Iterator iter = currentIter.get(event); if (iter == null) { - iter = expr instanceof Variable ? ((Variable) expr).variablesIterator(event) : expr.iterator(event); + iter = expr instanceof Variable variable ? variable.variablesIterator(event) : expr.iterator(event); if (iter != null) { if (iter.hasNext()) currentIter.put(event, iter); @@ -151,6 +149,11 @@ protected TriggerItem walk(Event event) { } } + @Override + public @Nullable ExecutionIntent executionIntent() { + return guaranteedToLoop ? triggerExecutionIntent() : null; + } + @Override public String toString(@Nullable Event event, boolean debug) { return "loop " + expr.toString(event, debug); @@ -184,4 +187,32 @@ public void exit(Event event) { super.exit(event); } + private static boolean guaranteedToLoop(Expression expression) { + // If the expression is a literal, it's guaranteed to loop if it has at least one value + if (expression instanceof Literal literal) + return literal.getAll().length > 0; + + // If the expression isn't a list, then we can't guarantee that it will loop + if (!(expression instanceof ExpressionList list)) + return false; + + // If the list is an OR list (a, b or c), then it's guaranteed to loop iff all its values are guaranteed to loop + if (!list.getAnd()) { + for (Expression expr : list.getExpressions()) { + if (!guaranteedToLoop(expr)) + return false; + } + return true; + } + + // If the list is an AND list (a, b and c), then it's guaranteed to loop if any of its values are guaranteed to loop + for (Expression expr : list.getExpressions()) { + if (guaranteedToLoop(expr)) + return true; + } + + // Otherwise, we can't guarantee that it will loop + return false; + } + } diff --git a/src/main/java/ch/njol/skript/sections/SecWhile.java b/src/main/java/ch/njol/skript/sections/SecWhile.java index 7a04f6d0ca5..25e295c56ac 100644 --- a/src/main/java/ch/njol/skript/sections/SecWhile.java +++ b/src/main/java/ch/njol/skript/sections/SecWhile.java @@ -24,18 +24,13 @@ import ch.njol.skript.doc.Examples; import ch.njol.skript.doc.Name; import ch.njol.skript.doc.Since; -import ch.njol.skript.lang.Condition; -import ch.njol.skript.lang.Expression; -import ch.njol.skript.lang.LoopSection; +import ch.njol.skript.lang.*; import ch.njol.skript.lang.SkriptParser.ParseResult; -import ch.njol.skript.lang.TriggerItem; import ch.njol.util.Kleenean; import org.bukkit.event.Event; import org.jetbrains.annotations.Nullable; import java.util.List; -import java.util.Map; -import java.util.WeakHashMap; @Name("While Loop") @Description("While Loop sections are loops that will just keep repeating as long as a condition is met.") @@ -59,7 +54,7 @@ public class SecWhile extends LoopSection { static { - Skript.registerSection(SecWhile.class, "[(:do)] while <.+>"); + Skript.registerSection(SecWhile.class, "[:do] while <.+>"); } @SuppressWarnings("NotNullFieldNotInitialized") @@ -104,6 +99,11 @@ protected TriggerItem walk(Event event) { } } + @Override + public @Nullable ExecutionIntent executionIntent() { + return doWhile ? triggerExecutionIntent() : null; + } + @Override public SecWhile setNext(@Nullable TriggerItem next) { actualNext = next; diff --git a/src/main/java/org/skriptlang/skript/lang/script/ScriptWarning.java b/src/main/java/org/skriptlang/skript/lang/script/ScriptWarning.java index 438f2ccfb70..10e917ad88b 100644 --- a/src/main/java/org/skriptlang/skript/lang/script/ScriptWarning.java +++ b/src/main/java/org/skriptlang/skript/lang/script/ScriptWarning.java @@ -23,31 +23,83 @@ import ch.njol.skript.lang.SkriptParser; import ch.njol.skript.lang.parser.ParserInstance; import ch.njol.util.Kleenean; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.UnknownNullability; /** * An enum containing {@link Script} warnings that can be suppressed. */ public enum ScriptWarning { + /** + * Possible variable conflict (Deprecated) + */ + @Deprecated + VARIABLE_CONFLICT("conflict", "conflict", "Variable conflict warnings no longer need suppression, as they have been removed altogether"), + /** * Variable cannot be saved (the ClassInfo is not serializable) */ - VARIABLE_SAVE, + VARIABLE_SAVE("variable save"), /** * Missing "and" or "or" */ - MISSING_CONJUNCTION, + MISSING_CONJUNCTION("missing conjunction", "[missing] conjunction"), /** * Variable starts with an Expression */ - VARIABLE_STARTS_WITH_EXPRESSION, + VARIABLE_STARTS_WITH_EXPRESSION("starting expression", "starting [with] expression[s]"), /** * This syntax is deprecated and scheduled for future removal */ - DEPRECATED_SYNTAX; + DEPRECATED_SYNTAX("deprecated syntax"), + + /** + * The code cannot be reached due to a previous statement stopping further execution + */ + UNREACHABLE_CODE("unreachable code"); + + private final String warningName; + private final String pattern; + private final @UnknownNullability String deprecationMessage; + + ScriptWarning(String warningName) { + this(warningName, warningName); + } + + ScriptWarning(String warningName, String pattern) { + this(warningName, pattern, null); + } + + ScriptWarning(String warningName, String pattern, @Nullable String deprecationMessage) { + this.warningName = warningName; + this.pattern = pattern; + this.deprecationMessage = deprecationMessage; + } + + public String getWarningName() { + return warningName; + } + + public String getPattern() { + return pattern; + } + + public boolean isDeprecated() { + return deprecationMessage != null; + } + + /** + * Returns the deprecation message of this warning, or null if the warning isn't deprecated. + * @return The deprecation message. + * @see #isDeprecated() + */ + public @UnknownNullability String getDeprecationMessage() { + return deprecationMessage; + } /** * Prints the given message using {@link Skript#warning(String)} iff the current script does not suppress deprecation warnings. diff --git a/src/main/resources/config.sk b/src/main/resources/config.sk index 2c0611ca5b0..1461b3becc6 100644 --- a/src/main/resources/config.sk +++ b/src/main/resources/config.sk @@ -172,6 +172,9 @@ disable variable missing and/or warnings: false disable starting a variable's name with an expression warnings: false # Disables the "Starting a variable's name with an expression is discouraged..." warnings +disable unreachable code warnings: false +# Disables the "Unreachable code. The previous statement stops further execution." warnings. + soft api exceptions: false # Allows Skript to ignore certain actions which would normally result in thrown exceptions. # If everything works correctly, you should keep this option disabled. It might cause problems in some cases. diff --git a/src/test/skript/tests/syntaxes/effects/EffContinue.sk b/src/test/skript/tests/syntaxes/effects/EffContinue.sk index 2400e7441e0..b63bc3c8603 100644 --- a/src/test/skript/tests/syntaxes/effects/EffContinue.sk +++ b/src/test/skript/tests/syntaxes/effects/EffContinue.sk @@ -4,16 +4,18 @@ test "continue effect": continue assert loop-value is not 5 with "continue in loop failed" set {_i} to 0 + while {_i} is smaller than 10: increase {_i} by 1 if {_i} is equal to 5: continue assert {_i} is not 5 with "continue in while failed" + loop integers from 1 to 10: continue this loop if loop-value is 5 - assert loop-value is not 5 with "leveled continue failed" + assert loop-value is not 5 with "leveled continue failed #1" loop integers from 11 to 20: continue 2nd loop if loop-value-2 is 15 - assert loop-value-2 is not 15 with "leveled continue failed" + assert loop-value-2 is not 15 with "leveled continue failed #2" continue 1st loop if loop-value-1 is 10 - assert loop-value is not 10 with "leveled continue failed" + assert loop-value is not 10 with "leveled continue failed #3"