diff --git a/src/main/java/ch/njol/skript/lang/DefaultExpressionUtils.java b/src/main/java/ch/njol/skript/lang/DefaultExpressionUtils.java index 5de7eebcbab..e7a53b7f9f6 100644 --- a/src/main/java/ch/njol/skript/lang/DefaultExpressionUtils.java +++ b/src/main/java/ch/njol/skript/lang/DefaultExpressionUtils.java @@ -3,6 +3,7 @@ import ch.njol.skript.classes.ClassInfo; import ch.njol.skript.lang.SkriptParser.ExprInfo; import ch.njol.util.StringUtils; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; import java.util.List; @@ -10,7 +11,8 @@ /** * Utility class for {@link DefaultExpression}. */ -final class DefaultExpressionUtils { +@ApiStatus.Internal +public final class DefaultExpressionUtils { /** * Check if {@code expr} is valid with the settings from {@code exprInfo}. @@ -20,7 +22,8 @@ final class DefaultExpressionUtils { * @param index The index of the {@link ClassInfo} in {@code exprInfo} used to grab {@code expr}. * @return {@link DefaultExpressionError} if it's not valid, otherwise {@code null}. */ - static @Nullable DefaultExpressionError isValid(DefaultExpression expr, ExprInfo exprInfo, int index) { + @ApiStatus.Internal + public static @Nullable DefaultExpressionError isValid(DefaultExpression expr, ExprInfo exprInfo, int index) { if (expr == null) { return DefaultExpressionError.NOT_FOUND; } else if (!(expr instanceof Literal) && (exprInfo.flagMask & SkriptParser.PARSE_EXPRESSIONS) == 0) { @@ -35,7 +38,8 @@ final class DefaultExpressionUtils { return null; } - enum DefaultExpressionError { + @ApiStatus.Internal + public enum DefaultExpressionError { /** * Error type for when a {@link DefaultExpression} can not be found for a {@link Class}. */ @@ -132,6 +136,7 @@ public String getError(List codeNames, String pattern) { * @param pattern The pattern to include in the error message. * @return error message. */ + @ApiStatus.Internal public abstract String getError(List codeNames, String pattern); /** diff --git a/src/main/java/ch/njol/skript/lang/EventRestrictedSyntax.java b/src/main/java/ch/njol/skript/lang/EventRestrictedSyntax.java index a8357f47175..45d728f8cdb 100644 --- a/src/main/java/ch/njol/skript/lang/EventRestrictedSyntax.java +++ b/src/main/java/ch/njol/skript/lang/EventRestrictedSyntax.java @@ -1,8 +1,14 @@ package ch.njol.skript.lang; +import ch.njol.skript.Skript; import ch.njol.util.Kleenean; +import ch.njol.util.StringUtils; import ch.njol.util.coll.CollectionUtils; import org.bukkit.event.Event; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; /** * A syntax element that restricts the events it can be used in. @@ -23,4 +29,25 @@ public interface EventRestrictedSyntax { */ Class[] supportedEvents(); + /** + * Creates a readable list of the user-facing names of the given event classes. + * @param supportedEvents The classes of the events to list. + * @return A string containing the names of the events as a list: {@code "the on death event, the on explosion event, or the on player join event"}. + */ + static @NotNull String supportedEventsNames(Class[] supportedEvents) { + List names = new ArrayList<>(); + + for (SkriptEventInfo eventInfo : Skript.getEvents()) { + for (Class eventClass : supportedEvents) { + for (Class event : eventInfo.events) { + if (event.isAssignableFrom(eventClass)) { + names.add("the %s event".formatted(eventInfo.getName().toLowerCase())); + } + } + } + } + + return StringUtils.join(names, ", ", " or "); + } + } diff --git a/src/main/java/ch/njol/skript/lang/SkriptParser.java b/src/main/java/ch/njol/skript/lang/SkriptParser.java index 713fc52f674..77c71e78056 100644 --- a/src/main/java/ch/njol/skript/lang/SkriptParser.java +++ b/src/main/java/ch/njol/skript/lang/SkriptParser.java @@ -1,7 +1,6 @@ package ch.njol.skript.lang; import ch.njol.skript.Skript; -import ch.njol.skript.SkriptAPIException; import ch.njol.skript.SkriptConfig; import ch.njol.skript.classes.ClassInfo; import ch.njol.skript.classes.Parser; @@ -10,17 +9,12 @@ import ch.njol.skript.command.ScriptCommand; import ch.njol.skript.command.ScriptCommandEvent; import ch.njol.skript.expressions.ExprParse; -import ch.njol.skript.lang.DefaultExpressionUtils.DefaultExpressionError; import ch.njol.skript.lang.function.ExprFunctionCall; import ch.njol.skript.lang.function.FunctionReference; import ch.njol.skript.lang.function.FunctionRegistry; import ch.njol.skript.lang.function.Functions; -import ch.njol.skript.lang.function.Signature; import ch.njol.skript.lang.parser.DefaultValueData; -import ch.njol.skript.lang.parser.ParseStackOverflowException; import ch.njol.skript.lang.parser.ParserInstance; -import ch.njol.skript.lang.parser.ParsingStack; -import ch.njol.skript.lang.simplification.Simplifiable; import ch.njol.skript.lang.util.SimpleLiteral; import ch.njol.skript.localization.Language; import ch.njol.skript.localization.Message; @@ -32,7 +26,6 @@ import ch.njol.skript.patterns.MalformedPatternException; import ch.njol.skript.patterns.PatternCompiler; import ch.njol.skript.patterns.SkriptPattern; -import ch.njol.skript.patterns.TypePatternElement; import ch.njol.skript.registrations.Classes; import ch.njol.skript.util.Utils; import ch.njol.util.Kleenean; @@ -42,30 +35,17 @@ import ch.njol.util.coll.iterator.CheckedIterator; import com.google.common.base.Preconditions; import com.google.common.primitives.Booleans; -import org.bukkit.event.Event; -import org.bukkit.plugin.java.JavaPlugin; -import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.skriptlang.skript.lang.converter.Converters; -import org.skriptlang.skript.lang.experiment.ExperimentSet; -import org.skriptlang.skript.lang.experiment.ExperimentalSyntax; -import org.skriptlang.skript.lang.script.Script; +import org.skriptlang.skript.lang.parser.ParsingConstraints; +import org.skriptlang.skript.lang.parser.SyntaxParser; import org.skriptlang.skript.lang.script.ScriptWarning; import org.skriptlang.skript.registration.SyntaxInfo; import org.skriptlang.skript.registration.SyntaxRegistry; import java.lang.reflect.Array; -import java.util.ArrayList; -import java.util.Deque; -import java.util.EnumMap; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Map.Entry; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Supplier; import java.util.regex.MatchResult; import java.util.regex.Matcher; @@ -93,6 +73,8 @@ public final class SkriptParser { public final ParseContext context; + private final SyntaxParser newParser; + public SkriptParser(String expr) { this(expr, ALL_FLAGS); } @@ -116,6 +98,12 @@ public SkriptParser(String expr, int flags, ParseContext context) { this.expr = "" + expr.trim(); this.flags = flags; this.context = context; + + ParsingConstraints constraints = ParsingConstraints.all().applyParseFlags(flags); + this.newParser = SyntaxParser.from(Skript.instance()) + .input(this.expr) + .parseContext(this.context) + .constraints(constraints); } public SkriptParser(SkriptParser other, String expr) { @@ -138,7 +126,7 @@ public static class ParseResult { public ParseResult(SkriptParser parser, String pattern) { expr = parser.expr; - exprs = new Expression[countUnescaped(pattern, '%') / 2]; + exprs = new Expression[StringUtils.countUnescaped(pattern, '%') / 2]; } public ParseResult(String expr, Expression[] expressions) { @@ -210,207 +198,7 @@ public boolean hasTag(String tag) { } private @Nullable T parse(Iterator> source) { - ParsingStack parsingStack = getParser().getParsingStack(); - try (ParseLogHandler log = SkriptLogger.startParseLogHandler()) { - while (source.hasNext()) { - SyntaxInfo info = source.next(); - int matchedPattern = -1; // will increment at the start of each iteration - patternsLoop: for (String pattern : info.patterns()) { - matchedPattern++; - log.clear(); - ParseResult parseResult; - - try { - parsingStack.push(new ParsingStack.Element(info, matchedPattern)); - parseResult = parse_i(pattern); - } catch (MalformedPatternException e) { - String message = "pattern compiling exception, element class: " + info.type().getName(); - try { - JavaPlugin providingPlugin = JavaPlugin.getProvidingPlugin(info.type()); - message += " (provided by " + providingPlugin.getName() + ")"; - } catch (IllegalArgumentException | IllegalStateException ignored) { } - throw new RuntimeException(message, e); - } catch (StackOverflowError e) { - // Parsing caused a stack overflow, possibly due to too long lines - throw new ParseStackOverflowException(e, new ParsingStack(parsingStack)); - } finally { - // Recursive parsing call done, pop the element from the parsing stack - ParsingStack.Element stackElement = parsingStack.pop(); - assert stackElement.syntaxElementInfo() == info && stackElement.patternIndex() == matchedPattern; - } - - if (parseResult == null) - continue; - - assert parseResult.source != null; // parse results from parse_i have a source - List types = null; - for (int i = 0; i < parseResult.exprs.length; i++) { - if (parseResult.exprs[i] == null) { - if (types == null) - types = parseResult.source.getElements(TypePatternElement.class);; - ExprInfo exprInfo = types.get(i).getExprInfo(); - if (!exprInfo.isOptional) { - List> exprs = getDefaultExpressions(exprInfo, pattern); - DefaultExpression matchedExpr = null; - for (DefaultExpression expr : exprs) { - if (expr.init()) { - matchedExpr = expr; - break; - } - } - if (matchedExpr == null) - continue patternsLoop; - parseResult.exprs[i] = matchedExpr; - } - } - } - T element = info.instance(); - - if (!checkRestrictedEvents(element, parseResult)) - continue; - - if (!checkExperimentalSyntax(element)) - continue; - - boolean success = element.preInit() && element.init(parseResult.exprs, matchedPattern, getParser().getHasDelayBefore(), parseResult); - if (success) { - // Check if any expressions are 'UnparsedLiterals' and if applicable for multiple info warning. - for (Expression expr : parseResult.exprs) { - if (expr instanceof UnparsedLiteral unparsedLiteral && unparsedLiteral.multipleWarning()) - break; - } - log.printLog(); - if (doSimplification && element instanceof Simplifiable simplifiable) - //noinspection unchecked - return (T) simplifiable.simplify(); - return element; - } - } - } - - // No successful syntax elements parsed, print errors and return - log.printError(); - return null; - } - } - - /** - * Checks whether the given element is restricted to specific events, and if so, whether the current event is allowed. - * Prints errors. - * @param element The syntax element to check. - * @param parseResult The parse result for error information. - * @return True if the element is allowed in the current event, false otherwise. - */ - private static boolean checkRestrictedEvents(SyntaxElement element, ParseResult parseResult) { - if (element instanceof EventRestrictedSyntax eventRestrictedSyntax) { - Class[] supportedEvents = eventRestrictedSyntax.supportedEvents(); - if (!getParser().isCurrentEvent(supportedEvents)) { - Skript.error("'" + parseResult.expr + "' can only be used in " + supportedEventsNames(supportedEvents)); - return false; - } - } - return true; - } - - /** - * Returns a string with the names of the supported skript events for the given class array. - * If no events are found, returns an empty string. - * @param supportedEvents The array of supported event classes. - * @return A string with the names of the supported skript events, or an empty string if none are found. - */ - private static @NotNull String supportedEventsNames(Class[] supportedEvents) { - List names = new ArrayList<>(); - - for (SkriptEventInfo eventInfo : Skript.getEvents()) { - for (Class eventClass : supportedEvents) { - for (Class event : eventInfo.events) { - if (event.isAssignableFrom(eventClass)) { - names.add("the %s event".formatted(eventInfo.getName().toLowerCase())); - } - } - } - } - - return StringUtils.join(names, ", ", " or "); - } - - /** - * Checks that {@code element} is an {@link ExperimentalSyntax} and, if so, ensures that its requirements are satisfied by the current {@link ExperimentSet}. - * @param element The {@link SyntaxElement} to check. - * @return {@code True} if the {@link SyntaxElement} is not an {@link ExperimentalSyntax} or is satisfied. - */ - private static boolean checkExperimentalSyntax(T element) { - if (!(element instanceof ExperimentalSyntax experimentalSyntax)) - return true; - ExperimentSet experiments = getParser().getExperimentSet(); - return experimentalSyntax.isSatisfiedBy(experiments); - } - - /** - * Returns the {@link DefaultExpression} from the first {@link ClassInfo} stored in {@code exprInfo}. - * - * @param exprInfo The {@link ExprInfo} to check for {@link DefaultExpression}. - * @param pattern The pattern used to create {@link ExprInfo}. - * @return {@link DefaultExpression}. - * @throws SkriptAPIException If the {@link DefaultExpression} is not valid, produces an error message for the reasoning of failure. - */ - private static @NotNull DefaultExpression getDefaultExpression(ExprInfo exprInfo, String pattern) { - DefaultValueData data = getParser().getData(DefaultValueData.class); - ClassInfo classInfo = exprInfo.classes[0]; - DefaultExpression expr = data.getDefaultValue(classInfo.getC()); - if (expr == null) - expr = classInfo.getDefaultExpression(); - - DefaultExpressionError errorType = DefaultExpressionUtils.isValid(expr, exprInfo, 0); - if (errorType == null) { - assert expr != null; - return expr; - } - - throw new SkriptAPIException(errorType.getError(List.of(classInfo.getCodeName()), pattern)); - } - - /** - * Returns all {@link DefaultExpression}s from all the {@link ClassInfo}s embedded in {@code exprInfo} that are valid. - * - * @param exprInfo The {@link ExprInfo} to check for {@link DefaultExpression}s. - * @param pattern The pattern used to create {@link ExprInfo}. - * @return All available {@link DefaultExpression}s. - * @throws SkriptAPIException If no {@link DefaultExpression}s are valid, produces an error message for the reasoning of failure. - */ - static @NotNull List> getDefaultExpressions(ExprInfo exprInfo, String pattern) { - if (exprInfo.classes.length == 1) - return new ArrayList<>(List.of(getDefaultExpression(exprInfo, pattern))); - - DefaultValueData data = getParser().getData(DefaultValueData.class); - - EnumMap> failed = new EnumMap<>(DefaultExpressionError.class); - List> passed = new ArrayList<>(); - for (int i = 0; i < exprInfo.classes.length; i++) { - ClassInfo classInfo = exprInfo.classes[i]; - DefaultExpression expr = data.getDefaultValue(classInfo.getC()); - if (expr == null) - expr = classInfo.getDefaultExpression(); - - String codeName = classInfo.getCodeName(); - DefaultExpressionError errorType = DefaultExpressionUtils.isValid(expr, exprInfo, i); - - if (errorType != null) { - failed.computeIfAbsent(errorType, list -> new ArrayList<>()).add(codeName); - } else { - passed.add(expr); - } - } - - if (!passed.isEmpty()) - return passed; - - List errors = new ArrayList<>(); - for (Entry> entry : failed.entrySet()) { - String error = entry.getKey().getError(entry.getValue(), pattern); - errors.add(error); - } - throw new SkriptAPIException(StringUtils.join(errors, "\n")); + return newParser.parse(source); } private static final Pattern VARIABLE_PATTERN = Pattern.compile("((the )?var(iable)? )?\\{.+\\}", Pattern.CASE_INSENSITIVE); @@ -446,7 +234,7 @@ private static boolean checkExperimentalSyntax(T eleme Skript.error("Pretty quotes are not allowed, change to regular quotes (\")"); return null; } - if (expr.startsWith("\"") && expr.length() != 1 && nextQuote(expr, 1) == expr.length() - 1) { + if (expr.startsWith("\"") && expr.length() != 1 && StringUtils.nextQuote(expr, 1) == expr.length() - 1) { return VariableString.newInstance("" + expr.substring(1, expr.length() - 1)); } else { var iterator = new CheckedIterator<>(Skript.instance().syntaxRegistry().syntaxes(SyntaxRegistry.EXPRESSION).iterator(), info -> { @@ -1409,83 +1197,6 @@ public static int nextBracket(String pattern, char closingBracket, char openingB return -1; } - /** - * Gets the next occurrence of a character in a string that is not escaped with a preceding backslash. - * - * @param pattern The string to search in - * @param character The character to search for - * @param from The index to start searching from - * @return The next index where the character occurs unescaped or -1 if it doesn't occur. - */ - private static int nextUnescaped(String pattern, char character, int from) { - for (int i = from; i < pattern.length(); i++) { - if (pattern.charAt(i) == '\\') { - i++; - } else if (pattern.charAt(i) == character) { - return i; - } - } - return -1; - } - - /** - * Counts how often the given character occurs in the given string, ignoring any escaped occurrences of the character. - * - * @param haystack The string to search in - * @param needle The character to search for - * @return The number of unescaped occurrences of the given character - */ - static int countUnescaped(String haystack, char needle) { - return countUnescaped(haystack, needle, 0, haystack.length()); - } - - /** - * Counts how often the given character occurs between the given indices in the given string, - * ignoring any escaped occurrences of the character. - * - * @param haystack The string to search in - * @param needle The character to search for - * @param start The index to start searching from (inclusive) - * @param end The index to stop searching at (exclusive) - * @return The number of unescaped occurrences of the given character - */ - static int countUnescaped(String haystack, char needle, int start, int end) { - assert start >= 0 && start <= end && end <= haystack.length() : start + ", " + end + "; " + haystack.length(); - int count = 0; - for (int i = start; i < end; i++) { - char character = haystack.charAt(i); - if (character == '\\') { - i++; - } else if (character == needle) { - count++; - } - } - return count; - } - - /** - * Find the next unescaped (i.e. single) double quote in the string. - * - * @param string The string to search in - * @param start Index after the starting quote - * @return Index of the end quote - */ - private static int nextQuote(String string, int start) { - boolean inExpression = false; - int length = string.length(); - for (int i = start; i < length; i++) { - char character = string.charAt(i); - if (character == '"' && !inExpression) { - if (i == length - 1 || string.charAt(i + 1) != '"') - return i; - i++; - } else if (character == '%') { - inExpression = !inExpression; - } - } - return -1; - } - /** * @param types The types to include in the message * @return "not an x" or "neither an x, a y nor a z" @@ -1566,7 +1277,7 @@ public static int next(String expr, int startIndex, ParseContext context) { int index; switch (expr.charAt(startIndex)) { case '"': - index = nextQuote(expr, startIndex + 1); + index = StringUtils.nextQuote(expr, startIndex + 1); return index < 0 ? -1 : index + 1; case '{': index = VariableString.nextVariableBracket(expr, startIndex + 1); @@ -1624,7 +1335,7 @@ public static int nextOccurrence(String haystack, String needle, int startIndex, switch (character) { case '"': - startIndex = nextQuote(haystack, startIndex + 1); + startIndex = StringUtils.nextQuote(haystack, startIndex + 1); if (startIndex < 0) return -1; break; diff --git a/src/main/java/ch/njol/skript/lang/Variable.java b/src/main/java/ch/njol/skript/lang/Variable.java index 9d41e9e2deb..e5b29e792bf 100644 --- a/src/main/java/ch/njol/skript/lang/Variable.java +++ b/src/main/java/ch/njol/skript/lang/Variable.java @@ -26,6 +26,7 @@ import org.bukkit.Bukkit; import org.bukkit.entity.Player; import org.bukkit.event.Event; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.skriptlang.skript.lang.arithmetic.Arithmetics; @@ -42,6 +43,7 @@ import java.util.Map.Entry; import java.util.function.Function; import java.util.function.Predicate; +import java.util.regex.Pattern; public class Variable implements Expression, KeyReceiverExpression, KeyProviderExpression { @@ -50,6 +52,7 @@ public class Variable implements Expression, KeyReceiverExpression, Key public final static String LOCAL_VARIABLE_TOKEN = "_"; public static final String EPHEMERAL_VARIABLE_TOKEN = "-"; private static final char[] reservedTokens = {'~', '.', '+', '$', '!', '&', '^', '*'}; + private static final Pattern VARIABLE_PATTERN = Pattern.compile("((the )?var(iable)? )?\\{.+}", Pattern.CASE_INSENSITIVE); /** * Script this variable was created in. @@ -159,6 +162,37 @@ else if (character == '%') return true; } + /** + * Parses a variable from a string. This is used to parse variables from strings in the form of "{%variable%}". + * + * @param expr The string to parse + * @param returnTypes The types to return + * @return The parsed variable, or null if the string is not a valid variable + */ + @ApiStatus.Internal + public static @Nullable Variable parse(String expr, Class[] returnTypes) { + if (VARIABLE_PATTERN.matcher(expr).matches()) { + String variableName = expr.substring(expr.indexOf('{') + 1, expr.lastIndexOf('}')); + boolean inExpression = false; + int variableDepth = 0; + for (char character : variableName.toCharArray()) { + if (character == '%' && variableDepth == 0) + inExpression = !inExpression; + if (inExpression) { + if (character == '{') { + variableDepth++; + } else if (character == '}') + variableDepth--; + } + + if (!inExpression && (character == '{' || character == '}')) + return null; + } + return Variable.newInstance(variableName, returnTypes); + } + return null; + } + /** * Creates a new variable instance with the given name and types. Prints errors. * @param name The raw name of the variable. diff --git a/src/main/java/ch/njol/util/StringUtils.java b/src/main/java/ch/njol/util/StringUtils.java index c68d344636d..16fb227dbeb 100644 --- a/src/main/java/ch/njol/util/StringUtils.java +++ b/src/main/java/ch/njol/util/StringUtils.java @@ -490,4 +490,62 @@ public static int indexOfOutsideGroup(String string, char find, char groupOpen, return -1; } + /** + * Counts how often the given character occurs in the given string, ignoring any escaped occurrences of the character. + * + * @param haystack The string to search in + * @param needle The character to search for + * @return The number of unescaped occurrences of the given character + */ + public static int countUnescaped(String haystack, char needle) { + return countUnescaped(haystack, needle, 0, haystack.length()); + } + + /** + * Counts how often the given character occurs between the given indices in the given string, + * ignoring any escaped occurrences of the character. + * + * @param haystack The string to search in + * @param needle The character to search for + * @param start The index to start searching from (inclusive) + * @param end The index to stop searching at (exclusive) + * @return The number of unescaped occurrences of the given character + */ + public static int countUnescaped(String haystack, char needle, int start, int end) { + assert start >= 0 && start <= end && end <= haystack.length() : start + ", " + end + "; " + haystack.length(); + int count = 0; + for (int i = start; i < end; i++) { + char character = haystack.charAt(i); + if (character == '\\') { + i++; + } else if (character == needle) { + count++; + } + } + return count; + } + + /** + * Find the next unescaped (i.e. single) double quote in the string. + * + * @param string The string to search in + * @param start Index after the starting quote + * @return Index of the end quote + */ + public static int nextQuote(String string, int start) { + boolean inExpression = false; + int length = string.length(); + for (int i = start; i < length; i++) { + char character = string.charAt(i); + if (character == '"' && !inExpression) { + if (i == length - 1 || string.charAt(i + 1) != '"') + return i; + i++; + } else if (character == '%') { + inExpression = !inExpression; + } + } + return -1; + } + } diff --git a/src/main/java/org/skriptlang/skript/lang/parser/ExpressionParser.java b/src/main/java/org/skriptlang/skript/lang/parser/ExpressionParser.java new file mode 100644 index 00000000000..a780f25c18e --- /dev/null +++ b/src/main/java/org/skriptlang/skript/lang/parser/ExpressionParser.java @@ -0,0 +1,21 @@ +package org.skriptlang.skript.lang.parser; + +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.skriptlang.skript.Skript; + +public interface ExpressionParser

> extends SyntaxParser

{ + + + @Contract("_ -> new") + static @NotNull ExpressionParser from(Skript skript) { + return new ExpressionParserImpl(skript); + } + + @Contract("_ -> new") + static @NotNull ExpressionParser from(SyntaxParser other) { + return new ExpressionParserImpl(other); + } + + +} diff --git a/src/main/java/org/skriptlang/skript/lang/parser/ExpressionParserImpl.java b/src/main/java/org/skriptlang/skript/lang/parser/ExpressionParserImpl.java new file mode 100644 index 00000000000..0dfc2e0d7dc --- /dev/null +++ b/src/main/java/org/skriptlang/skript/lang/parser/ExpressionParserImpl.java @@ -0,0 +1,393 @@ +package org.skriptlang.skript.lang.parser; + +import ch.njol.skript.Skript; +import ch.njol.skript.classes.ClassInfo; +import ch.njol.skript.lang.*; +import ch.njol.skript.lang.SkriptParser.ExprInfo; +import ch.njol.skript.lang.function.ExprFunctionCall; +import ch.njol.skript.lang.function.FunctionReference; +import ch.njol.skript.lang.util.SimpleLiteral; +import ch.njol.skript.localization.Language; +import ch.njol.skript.localization.Noun; +import ch.njol.skript.log.ErrorQuality; +import ch.njol.skript.log.LogEntry; +import ch.njol.skript.log.ParseLogHandler; +import ch.njol.skript.log.SkriptLogger; +import ch.njol.skript.registrations.Classes; +import ch.njol.util.StringUtils; +import ch.njol.util.coll.CollectionUtils; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.registration.SyntaxInfo; +import org.skriptlang.skript.registration.SyntaxRegistry; + +import java.util.Iterator; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static ch.njol.skript.lang.SkriptParser.notOfType; + +class ExpressionParserImpl extends SyntaxParserImpl implements ExpressionParser { + + private final static String MULTIPLE_AND_OR = "List has multiple 'and' or 'or', will default to 'and'. Use brackets if you want to define multiple lists."; + private final static String MISSING_AND_OR = "List is missing 'and' or 'or', defaulting to 'and'"; + + protected ExpressionParserImpl(org.skriptlang.skript.Skript skript) { + super(skript); + } + + public ExpressionParserImpl(SyntaxParser other) { + super(other); + } + + + public final @Nullable Expression parse() { + return null; + } + + public final @Nullable Expression parse(ExprInfo info) { + if (input.isEmpty()) + return null; + + var types = constraints.getValidReturnTypes(); + + assert types != null; + assert types.length > 0; + assert types.length == 1 || !CollectionUtils.contains(types, Object.class); + + try (ParseLogHandler log = SkriptLogger.startParseLogHandler()) { + Expression parsedExpression = parseSingleExpr(true, null); + if (parsedExpression != null) { + log.printLog(); + return parsedExpression; + } + log.clear(); + + return null; // this.parseExpressionList(log); + } + } + + /** + * Helper method to parse the input as a variable, taking into account flags and context + * @param log The log handler to use for logging errors + * @return A {@link Result} object containing the parsed variable or null if parsing failed, + * as well as a boolean indicating whether an error occurred + */ + @Contract("_ -> new") + private @NotNull Result> parseAsVariable(ParseLogHandler log) { + // check if the context is valid for variable parsing + if (context != ParseContext.DEFAULT && context != ParseContext.EVENT) + return new Result<>(false, null); + + //noinspection unchecked + Variable parsedVariable = (Variable) Variable.parse(input, constraints.getValidReturnTypes()); + if (parsedVariable != null) { + if (!constraints.allowsNonLiterals()) { + // TODO: this error pops up a lot when it isn't relevant, improve this + Skript.error("Variables cannot be used here."); + log.printError(); + return new Result<>(true, null); + } + log.printLog(); + return new Result<>(false, parsedVariable); + } else if (log.hasError()) { + log.printError(); + return new Result<>(true, null); + } + return new Result<>(false, null); + } + + /** + * Helper method to parse the input as a function, taking into account flags and context + * @param log The log handler to use for logging errors + * @return A {@link Result} object containing the parsed function or null if parsing failed, + * as well as a boolean indicating whether an error occurred + * @param The supertype that the function is expected to return + */ + @Contract("_ -> new") + private @NotNull Result> parseAsFunction(ParseLogHandler log) { + // check if the context is valid for function parsing + if (!constraints.allowsFunctionCalls() || context != ParseContext.DEFAULT && context != ParseContext.EVENT) + return new Result<>(false, null); + + FunctionReference functionReference = new FunctionParserImpl(this).parse(); + if (functionReference != null) { + log.printLog(); + return new Result<>(false, new ExprFunctionCall<>(functionReference)); + } else if (log.hasError()) { + log.printError(); + return new Result<>(true, null); + } + return new Result<>(false, null); + } + + /** + * Helper method to parse the input as a non-literal expression, taking into account flags and context + * @param log The log handler to use for logging errors + * @return A {@link Result} object containing the parsed expression or null if parsing failed, + * as well as a boolean indicating whether an error occurred + * @param The supertype that the expression is expected to return + */ + @Contract("_ -> new") + private @NotNull Result> parseAsNonLiteral(ParseLogHandler log) { + if (!constraints.allowsNonLiterals()) + return new Result<>(false, null); + + Expression parsedExpression; + if (input.startsWith("“") || input.startsWith("”") || input.endsWith("”") || input.endsWith("“")) { + Skript.error("Pretty quotes are not allowed, change to regular quotes (\")"); + return new Result<>(true, null); + } + // quoted string, strip quotes and parse as VariableString + if (input.startsWith("\"") && input.length() != 1 && StringUtils.nextQuote(input, 1) == input.length() - 1) { + //noinspection unchecked + return new Result<>(false, (Expression) VariableString.newInstance(input.substring(1, input.length() - 1))); + } else { + //noinspection unchecked + parsedExpression = (Expression) parse(SyntaxRegistry.EXPRESSION); + } + + if (parsedExpression != null) { // Expression/VariableString parsing success + Class parsedReturnType = parsedExpression.getReturnType(); + for (Class type : constraints.getValidReturnTypes()) { + if (type.isAssignableFrom(parsedReturnType)) { + log.printLog(); + return new Result<>(false, parsedExpression); + } + } + + // No directly same type found + //noinspection unchecked + Class[] objTypes = (Class[]) constraints.getValidReturnTypes(); + Expression convertedExpression = parsedExpression.getConvertedExpression(objTypes); + if (convertedExpression != null) { + log.printLog(); + return new Result<>(false, convertedExpression); + } + // Print errors, if we couldn't get the correct type + log.printError(parsedExpression.toString(null, false) + " " + Language.get("is") + " " + + notOfType(constraints.getValidReturnTypes()), ErrorQuality.NOT_AN_EXPRESSION); + return new Result<>(true, null); + } + return new Result<>(false, null); + } + + private static final String INVALID_LSPEC_CHARS = "[^,():/\"'\\[\\]}{]"; + private static final Pattern LITERAL_SPECIFICATION_PATTERN = Pattern.compile("(?" + INVALID_LSPEC_CHARS + "+) \\((?[\\w\\p{L} ]+)\\)"); + + /** + * Helper method to parse the input as a literal expression, taking into account flags and context + * @param log The log handler to use for logging errors + * @return A {@link Result} object containing the parsed expression or null if parsing failed, + * as well as a boolean indicating whether an error occurred + * @param The supertype that the expression is expected to return + */ + @Contract("_,_,_ -> new") + private @NotNull Result> parseAsLiteral(ParseLogHandler log, boolean allowUnparsedLiteral, @Nullable LogEntry error) { + if (!constraints.allowsLiterals()) + return new Result<>(false, null); + + // specified literal + if (input.endsWith(")") && input.contains("(")) { + Matcher classInfoMatcher = LITERAL_SPECIFICATION_PATTERN.matcher(input); + if (classInfoMatcher.matches()) { + String literalString = classInfoMatcher.group("literal"); + String unparsedClassInfo = Noun.stripDefiniteArticle(classInfoMatcher.group("classinfo")); + Expression result = parseSpecifiedLiteral(literalString, unparsedClassInfo); + if (result != null) { + log.printLog(); + return new Result<>(false, result); + } + } + } + // if target is just Object.class, we can use unparsed literal. + Class[] types = constraints.getValidReturnTypes(); + if (types.length == 1 && types[0] == Object.class) { + if (!allowUnparsedLiteral) { + log.printError(); + return new Result<>(true, null); + } + //noinspection unchecked + return new Result<>(false, (Expression) getUnparsedLiteral(log, error)); + } + + // attempt more specific parsing + boolean containsObjectClass = false; + for (Class type : types) { + log.clear(); + if (type == Object.class) { + // If 'Object.class' is an option, needs to be treated as previous behavior + // But we also want to be sure every other 'ClassInfo' is attempted to be parsed beforehand + containsObjectClass = true; + continue; + } + //noinspection unchecked + T parsedObject = (T) Classes.parse(input, type, context); + if (parsedObject != null) { + log.printLog(); + return new Result<>(false, new SimpleLiteral<>(parsedObject, false)); + } + } + if (allowUnparsedLiteral && containsObjectClass) + //noinspection unchecked + return new Result<>(false, (Expression) getUnparsedLiteral(log, error)); + + // literal string + if (input.startsWith("\"") && input.endsWith("\"") && input.length() > 1) { + for (Class type : types) { + if (!type.isAssignableFrom(String.class)) + continue; + VariableString string = VariableString.newInstance(input.substring(1, input.length() - 1)); + if (string instanceof LiteralString) + //noinspection unchecked + return new Result<>(false, (Expression) string); + break; + } + } + log.printError(); + return new Result<>(false, null); + } + + /** + * If {@link #input} is a valid literal expression, will return {@link UnparsedLiteral}. + * @param log The current {@link ParseLogHandler}. + * @param error A {@link LogEntry} containing a default error to be printed if failed to retrieve. + * @return {@link UnparsedLiteral} or {@code null}. + */ + private @Nullable UnparsedLiteral getUnparsedLiteral( + ParseLogHandler log, + @Nullable LogEntry error + ) { + // Do check if a literal with this name actually exists before returning an UnparsedLiteral + if (Classes.parseSimple(input, Object.class, context) == null) { + log.printError(); + return null; + } + log.clear(); + LogEntry logError = log.getError(); + return new UnparsedLiteral(input, logError != null && (error == null || logError.quality > error.quality) ? logError : error); + } + + /** + *

+ * With ambiguous literals being used in multiple {@link ClassInfo}s, users can specify which one they want + * in the format of 'literal (classinfo)'; Example: black (wolf variant) + * This checks to ensure the given 'classinfo' exists, is parseable, and is of the accepted types that is required. + * If so, the literal section of the input is parsed as the given classinfo and the result returned. + *

+ * @param literalString A {@link String} representing a literal + * @param unparsedClassInfo A {@link String} representing a class info + * @return {@link SimpleLiteral} or {@code null} if any checks fail + */ + private @Nullable Expression parseSpecifiedLiteral( + String literalString, + String unparsedClassInfo + ) { + ClassInfo classInfo = Classes.parse(unparsedClassInfo, ClassInfo.class, context); + if (classInfo == null) { + Skript.error("A " + unparsedClassInfo + " is not a valid type."); + return null; + } + ch.njol.skript.classes.Parser classInfoParser = classInfo.getParser(); + if (classInfoParser == null || !classInfoParser.canParse(context)) { + Skript.error("A " + unparsedClassInfo + " cannot be parsed."); + return null; + } + if (!checkAcceptedType(classInfo.getC(), constraints.getValidReturnTypes())) { + Skript.error(input + " " + Language.get("is") + " " + notOfType(constraints.getValidReturnTypes())); + return null; + } + //noinspection unchecked + T parsedObject = (T) classInfoParser.parse(literalString, context); + if (parsedObject != null) + return new SimpleLiteral<>(parsedObject, false, new UnparsedLiteral(literalString)); + return null; + } + + /** + * Check if the provided {@code clazz} is an accepted type from any class of {@code types}. + * @param clazz The {@link Class} to check + * @param types The {@link Class}es that are accepted + * @return true if {@code clazz} is of a {@link Class} from {@code types} + */ + private boolean checkAcceptedType(Class clazz, Class ... types) { + for (Class targetType : types) { + if (targetType.isAssignableFrom(clazz)) + return true; + } + return false; + } + + /** + * Parses the input as a singular expression that has a return type matching one of the given types. + * @param allowUnparsedLiteral Whether to allow unparsed literals to be returned + * @param defaultError The default error to log if the expression cannot be parsed + * @return The parsed expression, or null if the given input could not be parsed as an expression + * @param The return supertype of the expression + */ + private @Nullable Expression parseSingleExpr( + boolean allowUnparsedLiteral, + @Nullable LogEntry defaultError + ) { + if (input.isEmpty()) + return null; + + // strip "(" and ")" from the input if the input is properly enclosed + // do not do this for COMMAND or PARSE context for some reason + if (context != ParseContext.COMMAND + && context != ParseContext.PARSE + && input.startsWith("(") && input.endsWith(")") + && ch.njol.skript.lang.SkriptParser.next(input, 0, context) == input.length() + ) { + return this.input(input.substring(1, input.length() - 1)) + .parseSingleExpr(allowUnparsedLiteral, defaultError); + } + + try (ParseLogHandler log = SkriptLogger.startParseLogHandler()) { + // attempt to parse the input as a variable + Result> variableResult = parseAsVariable(log); + if (variableResult.error() || variableResult.value() != null) + return variableResult.value(); + log.clear(); + + // attempt to parse the input as a function + Result> functionResult = parseAsFunction(log); + if (functionResult.error() || functionResult.value() != null) + return functionResult.value(); + log.clear(); + + // attempt to parse the input as a non-literal expression + Result> expressionResult = parseAsNonLiteral(log); + if (expressionResult.error() || expressionResult.value() != null) + return expressionResult.value(); + log.clear(); + + // attempt to parse the input as a literal expression + Result> literalResult = parseAsLiteral(log, allowUnparsedLiteral, defaultError); + if (literalResult.error() || literalResult.value() != null) + return literalResult.value(); + log.clear(); + + // if all parsing attempts failed, return null + log.printLog(); + return null; + } + } + + @Override + public @Nullable E parse(@NotNull Iterator> candidateSyntaxes) { + return null; + } + + // todo: list parsing + + /** + * A record that contains internal information about the success of a single parsing operation, to facilitate helper methods. + * Not to be confused with {@link ch.njol.skript.lang.SkriptParser.ParseResult}, which contains information about the parsing itself. + * @param error Whether the parsing encountered an error and should exit. + * @param value The value that was parsed, or null if the parsing failed. + * @param The type of the value that was parsed. + */ + protected record Result(boolean error, @Nullable T value) { } +} diff --git a/src/main/java/org/skriptlang/skript/lang/parser/FunctionParser.java b/src/main/java/org/skriptlang/skript/lang/parser/FunctionParser.java new file mode 100644 index 00000000000..945cb78b600 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/lang/parser/FunctionParser.java @@ -0,0 +1,19 @@ +package org.skriptlang.skript.lang.parser; + +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.skriptlang.skript.Skript; + +public interface FunctionParser

> extends SyntaxParser

{ + + @Contract("_ -> new") + static @NotNull FunctionParser from(Skript skript) { + return new FunctionParserImpl(skript); + } + + @Contract("_ -> new") + static @NotNull FunctionParser from(SyntaxParser other) { + return new FunctionParserImpl(other); + } + +} diff --git a/src/main/java/org/skriptlang/skript/lang/parser/FunctionParserImpl.java b/src/main/java/org/skriptlang/skript/lang/parser/FunctionParserImpl.java new file mode 100644 index 00000000000..0ee21393a3a --- /dev/null +++ b/src/main/java/org/skriptlang/skript/lang/parser/FunctionParserImpl.java @@ -0,0 +1,20 @@ +package org.skriptlang.skript.lang.parser; + +import ch.njol.skript.lang.function.FunctionReference; +import org.jetbrains.annotations.NotNull; +import org.skriptlang.skript.Skript; + +class FunctionParserImpl extends SyntaxParserImpl implements FunctionParser { + + protected FunctionParserImpl(Skript skript) { + super(skript); + } + + public FunctionParserImpl(@NotNull SyntaxParser other) { + super(other); + } + + public FunctionReference parse() { + return null; + } +} diff --git a/src/main/java/org/skriptlang/skript/lang/parser/ParsingConstraints.java b/src/main/java/org/skriptlang/skript/lang/parser/ParsingConstraints.java new file mode 100644 index 00000000000..55c003ed7b6 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/lang/parser/ParsingConstraints.java @@ -0,0 +1,219 @@ +package org.skriptlang.skript.lang.parser; + +import ch.njol.skript.lang.ExpressionInfo; +import ch.njol.skript.lang.Literal; +import ch.njol.skript.lang.SyntaxElement; +import ch.njol.skript.lang.parser.ParserInstance; +import ch.njol.util.coll.iterator.CheckedIterator; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.lang.converter.Converters; +import org.skriptlang.skript.registration.SyntaxInfo; + +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import static ch.njol.skript.lang.SkriptParser.PARSE_EXPRESSIONS; +import static ch.njol.skript.lang.SkriptParser.PARSE_LITERALS; + +public class ParsingConstraints { + + private enum ExceptionMode { + UNUSED, + EXCLUDE, + INCLUDE + } + + private Set> exceptions = Set.of(); + private ExceptionMode exceptionMode; + + private boolean allowFunctionCalls; + + private boolean allowNonLiterals; + private boolean allowLiterals; + + private Class @Nullable [] validReturnTypes; + + @Contract("-> new") + public static @NotNull ParsingConstraints empty() { + return new ParsingConstraints() + .allowFunctionCalls(false) + .include() + .allowLiterals(false) + .allowNonLiterals(false); + } + + @Contract(" -> new") + public static @NotNull ParsingConstraints all() { + return new ParsingConstraints(); + } + + private ParsingConstraints() { + exceptionMode = ExceptionMode.UNUSED; + allowFunctionCalls = true; + allowNonLiterals = true; + allowLiterals = true; + validReturnTypes = new Class[]{Object.class}; + } + + public @NotNull Iterator> constrainIterator(Iterator> uncheckedIterator) { + return new CheckedIterator<>(uncheckedIterator, info -> { + assert info != null; + Class elementClass = info.type(); + if (elementClass == null) { + return false; + } + + if (info instanceof ExpressionInfo) { + // check literals + if (!allowsLiterals() && Literal.class.isAssignableFrom(elementClass)) { + return false; + } + // check non-literals + // TODO: allow simplification + if (!allowsNonLiterals() && !Literal.class.isAssignableFrom(elementClass)) { + return false; + } + } + + // check exceptions + if (exceptionMode == ExceptionMode.INCLUDE && !exceptions.contains(elementClass)) { + return false; + } else if (exceptionMode == ExceptionMode.EXCLUDE && exceptions.contains(elementClass)) { + return false; + } + + // check return types + if (info instanceof ExpressionInfo expressionInfo) { + if (validReturnTypes == null || expressionInfo.returnType == Object.class) + return true; + + for (Class returnType : validReturnTypes) { + if (Converters.converterExists(expressionInfo.returnType, returnType)) + return true; + } + return false; + } + return true; + }); + } + + public ParsingConstraints include(Class... exceptions) { + if (exceptionMode != ExceptionMode.INCLUDE) { + this.exceptions = new HashSet<>(); + } + this.exceptions.addAll(Set.of(exceptions)); + exceptionMode = ExceptionMode.INCLUDE; + return this; + } + + public ParsingConstraints exclude(Class... exceptions) { + if (exceptionMode != ExceptionMode.EXCLUDE) { + this.exceptions = new HashSet<>(); + } + this.exceptions.addAll(Set.of(exceptions)); + exceptionMode = ExceptionMode.EXCLUDE; + return this; + } + + public ParsingConstraints clearExceptions() { + exceptions = Set.of(); + exceptionMode = ExceptionMode.UNUSED; + return this; + } + + public boolean allowsFunctionCalls() { return allowFunctionCalls; } + + public ParsingConstraints allowFunctionCalls(boolean allow) { + allowFunctionCalls = allow; + return this; + } + + public Class[] getValidReturnTypes() { + return validReturnTypes; + } + + public ParsingConstraints constrainReturnTypes(Class... validReturnTypes) { + if (validReturnTypes == null || validReturnTypes.length == 0) { + this.validReturnTypes = null; + } else { + this.validReturnTypes = validReturnTypes; + } + return this; + } + + public boolean allowsNonLiterals() { return allowNonLiterals; } + + public ParsingConstraints allowNonLiterals(boolean allow) { + allowNonLiterals = allow; + return this; + } + + public boolean allowsLiterals() { return allowLiterals; } + + public ParsingConstraints allowLiterals(boolean allow) { + allowLiterals = allow; + return this; + } + + @ApiStatus.Internal + public int asParseFlags() { + int flags = 0; + if (allowNonLiterals) { + flags |= PARSE_EXPRESSIONS; + } + if (allowLiterals) { + flags |= PARSE_LITERALS; + } + return flags; + } + + @ApiStatus.Internal + public ParsingConstraints applyParseFlags(int flags) { + allowNonLiterals = (flags & PARSE_EXPRESSIONS) != 0; + allowLiterals = (flags & PARSE_LITERALS) != 0; + return this; + } + + @ApiStatus.Internal + public ParsingConstraints andParseFlags(int flags) { + applyParseFlags(asParseFlags() & flags); + return this; + } + + public ParsingConstraints copy() { + ParsingConstraints copy = new ParsingConstraints(); + copy.exceptions = new HashSet<>(exceptions); + copy.exceptionMode = exceptionMode; + copy.validReturnTypes = validReturnTypes; + copy.allowFunctionCalls = allowFunctionCalls; + copy.allowNonLiterals = allowNonLiterals; + copy.allowLiterals = allowLiterals; + return copy; + } + + static { + ParserInstance.registerData(ConstraintData.class, ConstraintData::new); + } + + public static class ConstraintData extends ParserInstance.Data { + private ParsingConstraints parsingConstraints = ParsingConstraints.all(); + + public ConstraintData(ParserInstance parserInstance) { + super(parserInstance); + } + + public ParsingConstraints getConstraints() { + return parsingConstraints; + } + + public void setConstraints(ParsingConstraints parsingConstraints) { + this.parsingConstraints = parsingConstraints; + } + + } + +} diff --git a/src/main/java/org/skriptlang/skript/lang/parser/SyntaxParser.java b/src/main/java/org/skriptlang/skript/lang/parser/SyntaxParser.java new file mode 100644 index 00000000000..c1f0da653c2 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/lang/parser/SyntaxParser.java @@ -0,0 +1,131 @@ +package org.skriptlang.skript.lang.parser; + +import ch.njol.skript.lang.*; +import ch.njol.util.Kleenean; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.Skript; +import org.skriptlang.skript.registration.SyntaxInfo; +import org.skriptlang.skript.registration.SyntaxRegistry; + +import java.util.Iterator; + +/** + * A parser with configurable constraints and context for parsing Skript syntax elements. + * Implementations should use {@link SyntaxParserImpl} as a base class, which provides default implementations for this interface. + *

+ * This is generally not intended to be used directly for parsing; instead, specialized parsers such as {@link ExpressionParser} + * or {@link FunctionParser} should be used for specific parsing tasks. This parser serves as a common foundation + * for those specialized parsers, though it can also be used directly for general parsing if needed. + *

+ * Implementing interfaces should provide static factory methods similar to those in this interface, allowing parsers + * to transition between different parser types while retaining configuration. + * + * @param

the type of the parser subclass + * @see SyntaxParserImpl + * @see ExpressionParser + */ +public interface SyntaxParser

> { + + @Contract("_ -> new") + static @NotNull SyntaxParser from(Skript skript) { + return new SyntaxParserImpl<>(skript); + } + + @Contract("_ -> new") + static @NotNull SyntaxParser from(SyntaxParser other) { + return new SyntaxParserImpl<>(other); + } + + /** + * Attempts to parse this parser's input against the given syntaxes. + * Prints parse errors (i.e. must start a ParseLog before calling this method) + * {@link #parse(SyntaxRegistry.Key)} is preferred for parsing against a specific syntax. + *

+ * Implementations should throw an exception if the parser is not ready to parse. + * + * @param candidateSyntaxes The iterator of {@link SyntaxElementInfo} objects to parse against. + * @return A parsed {@link SyntaxElement} with its {@link SyntaxElement#init(Expression[], int, Kleenean, ch.njol.skript.lang.SkriptParser.ParseResult)} + * method having been run and returned true. If no successful parse can be made, null is returned. + * @param The type of {@link SyntaxElement} that will be returned. + */ + @Contract(pure = true) + @Nullable E parse(@NotNull Iterator> candidateSyntaxes); + + /** + * Attempts to parse this parser's input against the given syntax type. + * Prints parse errors (i.e. must start a ParseLog before calling this method). + * + * @param expectedTypeKey A {@link SyntaxRegistry.Key} that determines what + * kind of syntax is expected as a result of the parsing. + * @return A parsed {@link SyntaxElement} with its {@link SyntaxElement#init(Expression[], int, Kleenean, ch.njol.skript.lang.SkriptParser.ParseResult)} + * method having been run and returned true. If no successful parse can be made, null is returned. + * @param The type of {@link SyntaxElement} that will be returned. + */ + @Contract(pure = true) + > @Nullable E parse(SyntaxRegistry.Key expectedTypeKey); + + /** + * @return the input string to be parsed + */ + @Contract(pure = true) + String input(); + + /** + * Sets the input string to be parsed. + * @param input the new input string + * @return this parser instance + */ + @Contract("_ -> this") + P input(@NotNull String input); + + /** + * @return the current parsing constraints + */ + @Contract(pure = true) + ParsingConstraints constraints(); + + /** + * Sets the parsing constraints. + * @param constraints the new parsing constraints + * @return this parser instance + */ + @Contract("_ -> this") + P constraints(@NotNull ParsingConstraints constraints); + + /** + * @return the current parse context + */ + @Contract(pure = true) + ParseContext parseContext(); + + /** + * Sets the parse context. + * @param context the new parse context + * @return this parser instance + */ + @Contract("_ -> this") + P parseContext(@NotNull ParseContext context); + + /** + * @return the default error message to use if parsing fails + */ + @Contract(pure = true) + @Nullable String defaultError(); + + /** + * Sets the default error message to use if parsing fails. + * @param defaultError the new default error message + * @return this parser instance + */ + @Contract("_ -> this") + P defaultError(@Nullable String defaultError); + + /** + * @return the Skript instance associated with this parser + */ + @Contract(pure = true) + Skript skript(); + +} diff --git a/src/main/java/org/skriptlang/skript/lang/parser/SyntaxParserImpl.java b/src/main/java/org/skriptlang/skript/lang/parser/SyntaxParserImpl.java new file mode 100644 index 00000000000..55987cd3c03 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/lang/parser/SyntaxParserImpl.java @@ -0,0 +1,439 @@ +package org.skriptlang.skript.lang.parser; + +import ch.njol.skript.SkriptAPIException; +import ch.njol.skript.SkriptConfig; +import ch.njol.skript.classes.ClassInfo; +import ch.njol.skript.lang.*; +import ch.njol.skript.lang.parser.DefaultValueData; +import ch.njol.skript.lang.parser.ParseStackOverflowException; +import ch.njol.skript.lang.parser.ParserInstance; +import ch.njol.skript.lang.parser.ParsingStack; +import ch.njol.skript.lang.simplification.Simplifiable; +import ch.njol.skript.log.ParseLogHandler; +import ch.njol.skript.log.SkriptLogger; +import ch.njol.skript.patterns.MalformedPatternException; +import ch.njol.skript.patterns.PatternCompiler; +import ch.njol.skript.patterns.SkriptPattern; +import ch.njol.skript.patterns.TypePatternElement; +import ch.njol.util.Kleenean; +import ch.njol.util.StringUtils; +import com.google.common.base.Preconditions; +import org.bukkit.event.Event; +import org.bukkit.plugin.java.JavaPlugin; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.Skript; +import org.skriptlang.skript.lang.experiment.ExperimentSet; +import org.skriptlang.skript.lang.experiment.ExperimentalSyntax; +import org.skriptlang.skript.registration.SyntaxInfo; +import org.skriptlang.skript.registration.SyntaxRegistry; + +import java.lang.reflect.InvocationTargetException; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +/** + * A parser with configurable constraints and context for parsing Skript syntax elements. + * Implementations should use {@link SyntaxParserImpl} as a base class, which provides default implementations for {@link SyntaxParser}. + * @param

the type of the parser subclass + */ +public class SyntaxParserImpl

> implements SyntaxParser

{ + + private static final Map patterns = new ConcurrentHashMap<>(); + + /** + * Matches ',', 'and', 'or', etc. as well as surrounding whitespace. + *

+ * group 1 is null for ',', otherwise it's one of and/or/nor (not necessarily lowercase). + */ + protected boolean suppressMissingAndOrWarnings = SkriptConfig.disableMissingAndOrWarnings.value(); + public final boolean doSimplification = SkriptConfig.simplifySyntaxesOnParse.value(); + + protected ParsingConstraints constraints; + protected ParseContext context; + protected String input; + protected String defaultError; + protected final Skript skript; + + protected SyntaxParserImpl(Skript skript) { + this.constraints = ParsingConstraints.all(); + this.context = ParseContext.DEFAULT; + this.skript = skript; + } + + public SyntaxParserImpl(@NotNull SyntaxParser other) { + this.constraints = other.constraints(); + this.context = other.parseContext(); + this.input = other.input(); + this.defaultError = other.defaultError(); + this.skript = other.skript(); + if (other instanceof SyntaxParserImpl otherImpl) + this.suppressMissingAndOrWarnings = otherImpl.suppressMissingAndOrWarnings; + } + + @Override + public @Nullable E parse(@NotNull Iterator> allowedSyntaxes) { + allowedSyntaxes = constraints.constrainIterator(allowedSyntaxes); + try (ParseLogHandler log = SkriptLogger.startParseLogHandler()) { + // for each allowed syntax + while (allowedSyntaxes.hasNext()) { + SyntaxInfo info = allowedSyntaxes.next(); + // check each of its patterns + int patternIndex = 0; + for (String pattern : info.patterns()) { + log.clear(); + E element = parse(info, pattern, patternIndex); + // return if this pattern parsed successfully + if (element != null) { + log.printLog(); + return element; + } + patternIndex++; + } + } + log.printError(); + return null; + } + } + + @Override + public > @Nullable E parse(SyntaxRegistry.Key expectedTypeKey) { + Iterator candidateSyntaxes = skript.syntaxRegistry().syntaxes(expectedTypeKey).iterator(); + return parse(candidateSyntaxes); + } + + /** + * Asserts that the parser is ready to parse. Implementations should call this method before attempting to parse. + */ + protected void assertReadyToParse() { + Preconditions.checkNotNull(input, "Input string must be set before parsing."); + Preconditions.checkNotNull(constraints, "Parsing constraints must be set before parsing."); + Preconditions.checkNotNull(context, "Parse context must be set before parsing."); + } + + /** + * @return the input string to be parsed + */ + @Contract(pure = true) + public String input() { + return input; + } + + /** + * Sets the input string to be parsed. + * @param input the new input string + * @return this parser instance + */ + @Contract("_ -> this") + @SuppressWarnings("unchecked") + public P input(@NotNull String input) { + this.input = input; + return (P) this; + } + + /** + * @return the current parsing constraints + */ + @Contract(pure = true) + public ParsingConstraints constraints() { + return constraints; + } + + /** + * Sets the parsing constraints. + * @param constraints the new parsing constraints + * @return this parser instance + */ + @Contract("_ -> this") + @SuppressWarnings("unchecked") + public P constraints(@NotNull ParsingConstraints constraints) { + this.constraints = constraints; + return (P) this; + } + + /** + * @return the current parse context + */ + @Contract(pure = true) + public ParseContext parseContext() { + return context; + } + + /** + * Sets the parse context. + * @param context the new parse context + * @return this parser instance + */ + @Contract("_ -> this") + @SuppressWarnings("unchecked") + public P parseContext(@NotNull ParseContext context) { + this.context = context; + return (P) this; + } + + /** + * @return the default error message to use if parsing fails + */ + @Contract(pure = true) + public @Nullable String defaultError() { + return defaultError; + } + + /** + * Sets the default error message to use if parsing fails. + * @param defaultError the new default error message + * @return this parser instance + */ + @Contract("_ -> this") + @SuppressWarnings("unchecked") + public P defaultError(@Nullable String defaultError) { + this.defaultError = defaultError; + return (P) this; + } + + @Override + public Skript skript() { + return skript; + } + + // -------------------------------------------------------------------------------- + // PARSING LOGIC + // -------------------------------------------------------------------------------- + + /** + * Attempts to parse this parser's input against the given pattern. + * Prints parse errors (i.e. must start a ParseLog before calling this method). + * @return A parsed {@link SyntaxElement} with its {@link SyntaxElement#init(Expression[], int, Kleenean, SkriptParser.ParseResult)} + * method having been run and returned true. If no successful parse can be made, null is returned. + * @param The type of {@link SyntaxElement} that will be returned. + */ + private @Nullable E parse(@NotNull SyntaxInfo info, String pattern, int patternIndex) { + ParsingStack parsingStack = getParser().getParsingStack(); + SkriptParser.ParseResult parseResult; + try { + // attempt to parse with the given pattern + parsingStack.push(new ParsingStack.Element(info, patternIndex)); + parseResult = parseAgainstPattern(pattern); + } catch (MalformedPatternException exception) { + // if the pattern failed to compile: + String message = "pattern compiling exception, element class: " + info.type().getName(); + try { + JavaPlugin providingPlugin = JavaPlugin.getProvidingPlugin(info.type()); + message += " (provided by " + providingPlugin.getName() + ")"; + } catch (IllegalArgumentException | IllegalStateException ignored) {} + + throw new RuntimeException(message, exception); + } catch (StackOverflowError e) { + // Parsing caused a stack overflow, possibly due to too long lines + throw new ParseStackOverflowException(e, new ParsingStack(parsingStack)); + } finally { + // Recursive parsing call done, pop the element from the parsing stack + ParsingStack.Element stackElement = parsingStack.pop(); + assert stackElement.syntaxElementInfo() == info && stackElement.patternIndex() == patternIndex; + } + + // if parsing was successful, attempt to populate default expressions + if (parseResult == null || !populateDefaultExpressions(parseResult, pattern)) + return null; + + E element; + // construct instance + try { + element = info.type().getDeclaredConstructor().newInstance(); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new RuntimeException("Failed to create instance of " + info.type().getName(), e); + } + + // if default expr population succeeded, try to init element. + if (initializeElement(element, patternIndex, parseResult)) { + if (doSimplification && element instanceof Simplifiable simplifiable) + //noinspection unchecked + return (E) simplifiable.simplify(); + return element; + } + + return null; + } + + /** + * Runs through all the initialization checks and steps for the given element, finalizing in a call to {@link SyntaxElement#init(Expression[], int, Kleenean, SkriptParser.ParseResult)}. + * @param element The element to initialize. + * @param patternIndex The index of the pattern that was matched. + * @param parseResult The parse result from parsing this element. + * @return Whether the element was successfully initialized. + */ + private boolean initializeElement(SyntaxElement element, int patternIndex, SkriptParser.ParseResult parseResult) { + if (!checkRestrictedEvents(element, parseResult)) + return false; + + if (!checkExperimentalSyntax(element)) + return false; + + // try to initialize the element + boolean success = element.preInit() && element.init(parseResult.exprs, patternIndex, getParser().getHasDelayBefore(), parseResult); + if (success) { + // Check if any expressions are 'UnparsedLiterals' and if applicable for multiple info warning. + for (Expression expr : parseResult.exprs) { + if (expr instanceof UnparsedLiteral unparsedLiteral && unparsedLiteral.multipleWarning()) + break; + } + return true; + } + return false; + } + + /** + * Attempts to match this parser's input against the given pattern. Any sub-elements (expressions) will be + * parsed and initialized. Default values will not be populated. + * Prints parse errors (i.e. must start a ParseLog before calling this method). + * @return A {@link SkriptParser.ParseResult} containing the results of the parsing, if successful. Null otherwise. + * @see #parse(SyntaxInfo, String, int) + */ + private @Nullable SkriptParser.ParseResult parseAgainstPattern(String pattern) throws MalformedPatternException { + SkriptPattern skriptPattern = patterns.computeIfAbsent(pattern, PatternCompiler::compile); + ch.njol.skript.patterns.MatchResult matchResult = skriptPattern.match(input, constraints.asParseFlags(), context); + if (matchResult == null) + return null; + return matchResult.toParseResult(); + } + + /** + * Given a parseResult, populates any default expressions that need to be filled. + * If no such default expression can be found, false will be returned. + * @param parseResult The parse result to populate. + * @param pattern The pattern to use to locate required default expressions. + * @return true if population was successful, false otherwise. + */ + private boolean populateDefaultExpressions(@NotNull SkriptParser.ParseResult parseResult, String pattern) { + assert parseResult.source != null; // parse results from parseAgainstPattern have a source + List types = null; + for (int i = 0; i < parseResult.exprs.length; i++) { + if (parseResult.exprs[i] == null) { + if (types == null) + types = parseResult.source.getElements(TypePatternElement.class); + SkriptParser.ExprInfo exprInfo = types.get(i).getExprInfo(); + if (!exprInfo.isOptional) { + List> exprs = getDefaultExpressions(exprInfo, pattern); + DefaultExpression matchedExpr = null; + for (DefaultExpression expr : exprs) { + if (expr.init()) { + matchedExpr = expr; + break; + } + } + if (matchedExpr == null) + return false; + parseResult.exprs[i] = matchedExpr; + } + } + } + return true; + } + + /** + * Returns the {@link DefaultExpression} from the first {@link ClassInfo} stored in {@code exprInfo}. + * + * @param exprInfo The {@link SkriptParser.ExprInfo} to check for {@link DefaultExpression}. + * @param pattern The pattern used to create {@link SkriptParser.ExprInfo}. + * @return {@link DefaultExpression}. + * @throws SkriptAPIException If the {@link DefaultExpression} is not valid, produces an error message for the reasoning of failure. + */ + private static @NotNull DefaultExpression getDefaultExpression(SkriptParser.ExprInfo exprInfo, String pattern) { + DefaultValueData data = getParser().getData(DefaultValueData.class); + ClassInfo classInfo = exprInfo.classes[0]; + DefaultExpression expr = data.getDefaultValue(classInfo.getC()); + if (expr == null) + expr = classInfo.getDefaultExpression(); + + DefaultExpressionUtils.DefaultExpressionError errorType = DefaultExpressionUtils.isValid(expr, exprInfo, 0); + if (errorType == null) { + assert expr != null; + return expr; + } + + throw new SkriptAPIException(errorType.getError(List.of(classInfo.getCodeName()), pattern)); + } + + /** + * Returns all {@link DefaultExpression}s from all the {@link ClassInfo}s embedded in {@code exprInfo} that are valid. + * + * @param exprInfo The {@link SkriptParser.ExprInfo} to check for {@link DefaultExpression}s. + * @param pattern The pattern used to create {@link SkriptParser.ExprInfo}. + * @return All available {@link DefaultExpression}s. + * @throws SkriptAPIException If no {@link DefaultExpression}s are valid, produces an error message for the reasoning of failure. + */ + static @NotNull List> getDefaultExpressions(SkriptParser.ExprInfo exprInfo, String pattern) { + if (exprInfo.classes.length == 1) + return new ArrayList<>(List.of(getDefaultExpression(exprInfo, pattern))); + + DefaultValueData data = getParser().getData(DefaultValueData.class); + + EnumMap> failed = new EnumMap<>(DefaultExpressionUtils.DefaultExpressionError.class); + List> passed = new ArrayList<>(); + for (int i = 0; i < exprInfo.classes.length; i++) { + ClassInfo classInfo = exprInfo.classes[i]; + DefaultExpression expr = data.getDefaultValue(classInfo.getC()); + if (expr == null) + expr = classInfo.getDefaultExpression(); + + String codeName = classInfo.getCodeName(); + DefaultExpressionUtils.DefaultExpressionError errorType = DefaultExpressionUtils.isValid(expr, exprInfo, i); + + if (errorType != null) { + failed.computeIfAbsent(errorType, list -> new ArrayList<>()).add(codeName); + } else { + passed.add(expr); + } + } + + if (!passed.isEmpty()) + return passed; + + List errors = new ArrayList<>(); + for (Map.Entry> entry : failed.entrySet()) { + String error = entry.getKey().getError(entry.getValue(), pattern); + errors.add(error); + } + throw new SkriptAPIException(StringUtils.join(errors, "\n")); + } + + /** + * Checks whether the given element is restricted to specific events, and if so, whether the current event is allowed. + * Prints errors. + * @param element The syntax element to check. + * @param parseResult The parse result for error information. + * @return True if the element is allowed in the current event, false otherwise. + */ + private static boolean checkRestrictedEvents(SyntaxElement element, SkriptParser.ParseResult parseResult) { + if (element instanceof EventRestrictedSyntax eventRestrictedSyntax) { + Class[] supportedEvents = eventRestrictedSyntax.supportedEvents(); + if (!getParser().isCurrentEvent(supportedEvents)) { + ch.njol.skript.Skript.error("'" + parseResult.expr + "' can only be used in " + + EventRestrictedSyntax.supportedEventsNames(supportedEvents)); + return false; + } + } + return true; + } + + /** + * Checks that {@code element} is an {@link ExperimentalSyntax} and, if so, ensures that its requirements are satisfied by the current {@link ExperimentSet}. + * @param element The {@link SyntaxElement} to check. + * @return {@code True} if the {@link SyntaxElement} is not an {@link ExperimentalSyntax} or is satisfied. + */ + private static boolean checkExperimentalSyntax(T element) { + if (!(element instanceof ExperimentalSyntax experimentalSyntax)) + return true; + ExperimentSet experiments = getParser().getExperimentSet(); + return experimentalSyntax.isSatisfiedBy(experiments); + } + + /** + * @see ParserInstance#get() + */ + protected static ParserInstance getParser() { + return ParserInstance.get(); + } + +} diff --git a/src/test/java/ch/njol/skript/lang/DefaultExpressionErrorTest.java b/src/test/java/org/skriptlang/skript/lang/parser/DefaultExpressionErrorTest.java similarity index 99% rename from src/test/java/ch/njol/skript/lang/DefaultExpressionErrorTest.java rename to src/test/java/org/skriptlang/skript/lang/parser/DefaultExpressionErrorTest.java index 43c9647f370..ddf62d662ff 100644 --- a/src/test/java/ch/njol/skript/lang/DefaultExpressionErrorTest.java +++ b/src/test/java/org/skriptlang/skript/lang/parser/DefaultExpressionErrorTest.java @@ -1,4 +1,4 @@ -package ch.njol.skript.lang; +package org.skriptlang.skript.lang.parser; import ch.njol.skript.lang.DefaultExpressionUtils.DefaultExpressionError; import ch.njol.skript.test.runner.SkriptJUnitTest; diff --git a/src/test/java/ch/njol/skript/lang/GetDefaultExpressionsTest.java b/src/test/java/org/skriptlang/skript/lang/parser/GetDefaultExpressionsTest.java similarity index 99% rename from src/test/java/ch/njol/skript/lang/GetDefaultExpressionsTest.java rename to src/test/java/org/skriptlang/skript/lang/parser/GetDefaultExpressionsTest.java index ce0a8bbdeae..d5abb29832f 100644 --- a/src/test/java/ch/njol/skript/lang/GetDefaultExpressionsTest.java +++ b/src/test/java/org/skriptlang/skript/lang/parser/GetDefaultExpressionsTest.java @@ -1,4 +1,4 @@ -package ch.njol.skript.lang; +package org.skriptlang.skript.lang.parser; import ch.njol.skript.SkriptAPIException; import ch.njol.skript.classes.ClassInfo; @@ -14,7 +14,7 @@ import java.util.List; import java.util.function.BiConsumer; -import static ch.njol.skript.lang.SkriptParser.getDefaultExpressions; +import static org.skriptlang.skript.lang.parser.SyntaxParserImpl.getDefaultExpressions; public class GetDefaultExpressionsTest extends SkriptJUnitTest {