From 80dbe6e160a2bc8b6420f09b14c7859987117072 Mon Sep 17 00:00:00 2001 From: sovdee <10354869+sovdeeth@users.noreply.github.com> Date: Thu, 23 Oct 2025 12:19:12 -0700 Subject: [PATCH 1/7] begin refactor by moving to skriptlang and pulling out some helpers to more appropriate locations. --- .../skript/lang/DefaultExpressionUtils.java | 11 +- .../skript/lang/EventRestrictedSyntax.java | 27 + .../ch/njol/skript/lang/SkriptParser.java | 121 +--- .../java/ch/njol/skript/lang/Variable.java | 34 + src/main/java/ch/njol/util/StringUtils.java | 58 ++ .../skript/lang/parser/ExpressionParser.java | 521 ++++++++++++++++ .../skript/lang/parser/FunctionParser.java | 135 ++++ .../lang/parser/ParsingConstraints.java | 214 +++++++ .../skript/lang/parser/SkriptParser.java | 586 ++++++++++++++++++ .../skript/lang/parser/StatementParser.java | 4 + 10 files changed, 1593 insertions(+), 118 deletions(-) create mode 100644 src/main/java/org/skriptlang/skript/lang/parser/ExpressionParser.java create mode 100644 src/main/java/org/skriptlang/skript/lang/parser/FunctionParser.java create mode 100644 src/main/java/org/skriptlang/skript/lang/parser/ParsingConstraints.java create mode 100644 src/main/java/org/skriptlang/skript/lang/parser/SkriptParser.java create mode 100644 src/main/java/org/skriptlang/skript/lang/parser/StatementParser.java 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..70a03471bc9 100644 --- a/src/main/java/ch/njol/skript/lang/SkriptParser.java +++ b/src/main/java/ch/njol/skript/lang/SkriptParser.java @@ -15,7 +15,6 @@ 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; @@ -49,23 +48,14 @@ 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.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.*; import java.util.Map.Entry; 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; @@ -138,7 +128,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) { @@ -305,35 +295,13 @@ private static boolean checkRestrictedEvents(SyntaxElement element, 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)); + Skript.error("'" + parseResult.expr + "' can only be used in " + EventRestrictedSyntax.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. @@ -446,7 +414,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 +1377,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 +1457,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 +1515,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..cf03ce5301a --- /dev/null +++ b/src/main/java/org/skriptlang/skript/lang/parser/ExpressionParser.java @@ -0,0 +1,521 @@ +package org.skriptlang.skript.lang.parser; + +import ch.njol.skript.Skript; +import ch.njol.skript.classes.ClassInfo; +import ch.njol.skript.classes.Parser; +import ch.njol.skript.lang.*; +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.SyntaxRegistry; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class ExpressionParser extends SkriptParser { + + 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'"; + + public ExpressionParser(String input) { + super(input); + } + + public ExpressionParser(String input, ParsingConstraints constraints) { + super(input, constraints); + } + + public ExpressionParser(@NotNull String input, ParsingConstraints constraints, ParseContext context) { + super(input, constraints, context); + } + + public ExpressionParser(SkriptParser skriptParser, String input) { + super(skriptParser, input); + } + + public final @Nullable Expression parse() { + if (input.isEmpty()) + return null; + + var types = parsingConstraints.getValidReturnTypes(); + + assert types != null; + assert types.length > 0; + assert types.length == 1 || !CollectionUtils.contains(types, Object.class); + + ParseLogHandler log = SkriptLogger.startParseLogHandler(); + try { + Expression parsedExpression = parseSingleExpr(true, null); + if (parsedExpression != null) { + log.printLog(); + return parsedExpression; + } + log.clear(); + + return this.parseExpressionList(log); + } finally { + log.stop(); + } + } + + /** + * 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, parsingConstraints.getValidReturnTypes()); + if (parsedVariable != null) { + if (!parsingConstraints.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 (!parsingConstraints.allowsFunctionCalls() || context != ParseContext.DEFAULT && context != ParseContext.EVENT) + return new Result<>(false, null); + + FunctionReference functionReference = new FunctionParser(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 (!parsingConstraints.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 : parsingConstraints.getValidReturnTypes()) { + if (type.isAssignableFrom(parsedReturnType)) { + log.printLog(); + return new Result<>(false, parsedExpression); + } + } + + // No directly same type found + //noinspection unchecked + Class[] objTypes = (Class[]) parsingConstraints.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(parsingConstraints.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 (!parsingConstraints.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 = parsingConstraints.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; + } + Parser classInfoParser = classInfo.getParser(); + if (classInfoParser == null || !classInfoParser.canParse(context)) { + Skript.error("A " + unparsedClassInfo + " cannot be parsed."); + return null; + } + if (!checkAcceptedType(classInfo.getC(), parsingConstraints.getValidReturnTypes())) { + Skript.error(input + " " + Language.get("is") + " " + notOfType(parsingConstraints.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 new ExpressionParser(this, 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; + } + } + + /* + + * List parsing + + private record OrderedExprInfo(ExprInfo[] infos) { } + + @SafeVarargs + private @Nullable Expression parseExpressionList(ParseLogHandler log, Class... types) { + //noinspection unchecked + return (Expression) parseExpressionList_i(log, types); + } + + private @Nullable Expression parseExpressionList(ParseLogHandler log, ExprInfo info) { + return parseExpressionList_i(log, info); + } + + private @Nullable Expression parseExpressionList(ParseLogHandler log, OrderedExprInfo info) { + return parseExpressionList_i(log, info); + } + + private @Nullable Expression parseExpressionList_i(ParseLogHandler log, Object data) { + */ + + @Nullable Expression parseExpressionList(ParseLogHandler log) { +// var types = parsingConstraints.getValidReturnTypes(); +// boolean isObject = types.length == 1 && types[0] == Object.class; +// List> parsedExpressions = new ArrayList<>(); +// Kleenean and = Kleenean.UNKNOWN; +// boolean isLiteralList = true; +// Expression parsedExpression; +// +// List pieces = new ArrayList<>(); +// { +// Matcher matcher = LIST_SPLIT_PATTERN.matcher(input); +// int i = 0, j = 0; +// for (; i >= 0 && i <= input.length(); i = StringUtils.next(input, i, context)) { +// if (i == input.length() || matcher.region(i, input.length()).lookingAt()) { +// pieces.add(new int[] {j, i}); +// if (i == input.length()) +// break; +// j = i = matcher.end(); +// } +// } +// if (i != input.length()) { +// assert i == -1 && context != ParseContext.COMMAND && context != ParseContext.PARSE : i + "; " + input; +// log.printError("Invalid brackets/variables/text in '" + input + "'", ErrorQuality.NOT_AN_EXPRESSION); +// return null; +// } +// } +// +// if (pieces.size() == 1) { // not a list of expressions, and a single one has failed to parse above +// if (input.startsWith("(") && input.endsWith(")") && StringUtils.next(input, 0, context) == input.length()) { +// log.clear(); +// return new ExpressionParser(this, input.substring(1, input.length() - 1)).parse(); +// } +// if (isObject && parsingConstraints.allowsLiterals()) { // single expression - can return an UnparsedLiteral now +// log.clear(); +// //noinspection unchecked +// return (Expression) new UnparsedLiteral(input, log.getError()); +// } +// // results in useless errors most of the time +//// log.printError("'" + input + "' " + Language.get("is") + " " + notOfType(types), ErrorQuality.NOT_AN_EXPRESSION); +// log.printError(); +// return null; +// } +// +// outer: for (int first = 0; first < pieces.size();) { +// for (int last = 1; last <= pieces.size() - first; last++) { +// if (first == 0 && last == pieces.size()) // i.e. the whole expression - already tried to parse above +// continue; +// int start = pieces.get(first)[0], end = pieces.get(first + last - 1)[1]; +// String subExpr = input.substring(start, end).trim(); +// assert subExpr.length() < input.length() : subExpr; +// +// if (subExpr.startsWith("(") && subExpr.endsWith(")") && StringUtils.next(subExpr, 0, context) == subExpr.length()) +// parsedExpression = new ExpressionParser(this, subExpr).parse(); // only parse as possible expression list if its surrounded by brackets +// else +// parsedExpression = new ExpressionParser(this, subExpr).parseSingleExpr(last == 1, log.getError()); // otherwise parse as a single expression only +// if (parsedExpression != null) { +// isLiteralList &= parsedExpression instanceof Literal; +// parsedExpressions.add(parsedExpression); +// if (first != 0) { +// String delimiter = input.substring(pieces.get(first - 1)[1], start).trim().toLowerCase(Locale.ENGLISH); +// if (!delimiter.equals(",")) { +// boolean or = !delimiter.contains("nor") && delimiter.endsWith("or"); +// if (and.isUnknown()) { +// and = Kleenean.get(!or); // nor is and +// } else { +// if (and != Kleenean.get(!or)) { +// Skript.warning(MULTIPLE_AND_OR + " List: " + input); +// and = Kleenean.TRUE; +// } +// } +// } +// } +// first += last; +// continue outer; +// } +// } +// log.printError(); +// return null; +// } +// +// log.printLog(false); +// +// if (parsedExpressions.size() == 1) +// return parsedExpressions.get(0); +// +// if (and.isUnknown() && !suppressMissingAndOrWarnings) { +// ParserInstance parser = getParser(); +// Script currentScript = parser.isActive() ? parser.getCurrentScript() : null; +// if (currentScript == null || !currentScript.suppressesWarning(ScriptWarning.MISSING_CONJUNCTION)) +// Skript.warning(MISSING_AND_OR + ": " + input); +// } +// +// Class[] exprReturnTypes = new Class[parsedExpressions.size()]; +// for (int i = 0; i < parsedExpressions.size(); i++) +// exprReturnTypes[i] = parsedExpressions.get(i).getReturnType(); +// +// if (isLiteralList) { +// //noinspection unchecked,SuspiciousToArrayCall +// Literal[] literals = parsedExpressions.toArray(new Literal[0]); +// //noinspection unchecked +// return new LiteralList<>(literals, (Class) Classes.getSuperClassInfo(exprReturnTypes).getC(), exprReturnTypes, !and.isFalse()); +// } else { +// //noinspection unchecked +// Expression[] expressions = parsedExpressions.toArray(new Expression[0]); +// //noinspection unchecked +// return new ExpressionList<>(expressions, (Class) Classes.getSuperClassInfo(exprReturnTypes).getC(), exprReturnTypes, !and.isFalse()); +// } + return null; + } + + /** + * A record that contains internal information about the success of a single parsing operation, to facilitate helper methods. + * Not to be confused with {@link 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..19d49a851ce --- /dev/null +++ b/src/main/java/org/skriptlang/skript/lang/parser/FunctionParser.java @@ -0,0 +1,135 @@ +package org.skriptlang.skript.lang.parser; + +import ch.njol.skript.Skript; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.ExpressionList; +import ch.njol.skript.lang.ParseContext; +import ch.njol.skript.lang.function.FunctionReference; +import ch.njol.skript.lang.function.Functions; +import ch.njol.skript.lang.parser.ParserInstance; +import ch.njol.skript.log.ParseLogHandler; +import ch.njol.skript.log.SkriptLogger; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.lang.script.Script; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Supplier; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class FunctionParser extends SkriptParser { + + private final static Pattern FUNCTION_CALL_PATTERN = Pattern.compile("(" + Functions.functionNamePattern + ")\\((.*)\\)"); + + public FunctionParser(String input) { + super(input); + } + + public FunctionParser(String input, ParsingConstraints constraints) { + super(input, constraints); + } + + public FunctionParser(@NotNull String input, ParsingConstraints constraints, ParseContext context) { + super(input, constraints, context); + } + + public FunctionParser(@NotNull SkriptParser other) { + super(other); + } + + public final @Nullable FunctionReference parse() { + if (context != ParseContext.DEFAULT && context != ParseContext.EVENT) + return null; + var returnTypes = parsingConstraints.getValidReturnTypes(); + AtomicBoolean unaryArgument = new AtomicBoolean(false); + try (ParseLogHandler log = SkriptLogger.startParseLogHandler()) { + Matcher matcher = FUNCTION_CALL_PATTERN.matcher(input); + if (!matcher.matches()) { + log.printLog(); + return null; + } + + String functionName = matcher.group(1); + String args = matcher.group(2); + Expression[] params; + + // Check for incorrect quotes, e.g. "myFunction() + otherFunction()" being parsed as one function + // See https://github.com/SkriptLang/Skript/issues/1532 + for (int i = 0; i < args.length(); i = ch.njol.skript.lang.SkriptParser.next(args, i, context)) { + if (i == -1) { + log.printLog(); + return null; + } + } + + if (!parsingConstraints.allowsNonLiterals()) { + Skript.error("Functions cannot be used here (or there is a problem with your arguments)."); + log.printError(); + return null; + } + ExpressionParser exprParser = new ExpressionParser(args, + parsingConstraints.copy() + .allowLiterals(true) + .constrainReturnTypes(Object.class), + context); + exprParser.suppressMissingAndOrWarnings(); + + params = this.getFunctionArguments(exprParser::parse, args, unaryArgument); + if (params == null) { + log.printError(); + return null; + } + + ParserInstance parser = getParser(); + Script currentScript = parser.isActive() ? parser.getCurrentScript() : null; + //noinspection unchecked + FunctionReference functionReference = (FunctionReference) new FunctionReference<>(functionName, SkriptLogger.getNode(), + currentScript != null ? currentScript.getConfig().getFileName() : null, returnTypes, params);//.toArray(new Expression[params.size()])); + attempt_list_parse: + if (unaryArgument.get() && !functionReference.validateParameterArity(true)) { + try (ParseLogHandler ignored = SkriptLogger.startParseLogHandler()) { + + exprParser.suppressMissingAndOrWarnings(); + params = this.getFunctionArguments(() -> exprParser.parseExpressionList(ignored), args, unaryArgument); + ignored.clear(); + if (params == null) + break attempt_list_parse; + } + //noinspection unchecked + functionReference = (FunctionReference) new FunctionReference<>(functionName, SkriptLogger.getNode(), + currentScript != null ? currentScript.getConfig().getFileName() : null, returnTypes, params); + } + if (!functionReference.validateFunction(true)) { + log.printError(); + return null; + } + log.printLog(); + return functionReference; + } + } + + private Expression @Nullable [] getFunctionArguments(Supplier> parsing, @NotNull String args, AtomicBoolean unary) { + Expression[] params; + if (!args.isEmpty()) { + Expression parsedExpression = parsing.get(); + if (parsedExpression == null) + return null; + if (parsedExpression instanceof ExpressionList expressionList) { + if (!parsedExpression.getAnd()) { + Skript.error("Function arguments must be separated by commas and optionally an 'and', but not an 'or'." + + " Put the 'or' into a second set of parentheses if you want to make it a single parameter, e.g. 'give(player, (sword or axe))'"); + return null; + } + params = expressionList.getExpressions(); + } else { + unary.set(true); + params = new Expression[] {parsedExpression}; + } + } else { + params = new Expression[0]; + } + return params; + } + +} 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..35a32b1651b --- /dev/null +++ b/src/main/java/org/skriptlang/skript/lang/parser/ParsingConstraints.java @@ -0,0 +1,214 @@ +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; + +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; + } + + // 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 |= SkriptParser.PARSE_EXPRESSIONS; + } + if (allowLiterals) { + flags |= SkriptParser.PARSE_LITERALS; + } + return flags; + } + + @ApiStatus.Internal + public ParsingConstraints applyParseFlags(int flags) { + allowNonLiterals = (flags & SkriptParser.PARSE_EXPRESSIONS) != 0; + allowLiterals = (flags & SkriptParser.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/SkriptParser.java b/src/main/java/org/skriptlang/skript/lang/parser/SkriptParser.java new file mode 100644 index 00000000000..f4cda0846ad --- /dev/null +++ b/src/main/java/org/skriptlang/skript/lang/parser/SkriptParser.java @@ -0,0 +1,586 @@ + +package org.skriptlang.skript.lang.parser; + +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.lang.*; +import ch.njol.skript.lang.DefaultExpressionUtils.DefaultExpressionError; +import ch.njol.skript.lang.SkriptParser.ExprInfo; +import ch.njol.skript.lang.SkriptParser.ParseResult; +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.localization.Language; +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.skript.registrations.Classes; +import ch.njol.util.Kleenean; +import ch.njol.util.StringUtils; +import org.bukkit.event.Event; +import org.bukkit.plugin.java.JavaPlugin; +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.experiment.ExperimentSet; +import org.skriptlang.skript.lang.experiment.ExperimentalSyntax; +import org.skriptlang.skript.registration.SyntaxInfo; +import org.skriptlang.skript.registration.SyntaxRegistry.Key; + +import java.lang.reflect.InvocationTargetException; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Pattern; + +public class SkriptParser { + + @Deprecated(since = "INSERT VERSION") + public static final int PARSE_EXPRESSIONS = 1; + @Deprecated(since = "INSERT VERSION") + public static final int PARSE_LITERALS = 2; + @Deprecated(since = "INSERT VERSION") + public static final int ALL_FLAGS = PARSE_EXPRESSIONS | PARSE_LITERALS; + + 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). + */ + public static final Pattern LIST_SPLIT_PATTERN = Pattern.compile("\\s*,?\\s+(and|n?or)\\s+|\\s*,\\s*", Pattern.CASE_INSENSITIVE); + public static final Pattern OR_PATTERN = Pattern.compile("\\sor\\s", Pattern.CASE_INSENSITIVE); + protected boolean suppressMissingAndOrWarnings = SkriptConfig.disableMissingAndOrWarnings.value(); + + protected ParsingConstraints parsingConstraints; + protected final String input; + public final ParseContext context; + + public final boolean doSimplification = SkriptConfig.simplifySyntaxesOnParse.value(); + + + /** + * Constructs a new SkriptParser object that can be used to parse the given expression. + * Parses expressions and literals using {@link ParseContext#DEFAULT}. + *

+ * A SkriptParser can be re-used indefinitely for the given expression, but to parse a new expression a new SkriptParser has to be created. + * + * @param input The text to parse. + */ + protected SkriptParser(String input) { + this(input, ParsingConstraints.all()); + } + + /** + * Constructs a new SkriptParser object that can be used to parse the given expression. + * Parses using {@link ParseContext#DEFAULT}. + *

+ * A SkriptParser can be re-used indefinitely for the given expression, but to parse a new expression a new SkriptParser has to be created. + * + * @param constraints The constraints under which to parse. + * @param input The text to parse. + */ + protected SkriptParser(String input, ParsingConstraints constraints) { + this(input, constraints, ParseContext.DEFAULT); + } + + /** + * Constructs a new SkriptParser object that can be used to parse the given expression. + *

+ * A SkriptParser can be re-used indefinitely for the given expression, but to parse a new expression a new SkriptParser has to be created. + * + * @param input The text to parse. + * @param constraints The constraints under which to parse. + * @param context The parse context. + */ + protected SkriptParser(@NotNull String input, ParsingConstraints constraints, ParseContext context) { + this.input = input.trim(); + this.parsingConstraints = constraints; + this.context = context; + } + + /** + * Constructs a new SkriptParser object that can be used to parse the given expression. + *

+ * A SkriptParser can be re-used indefinitely for the given expression, but to parse a new expression a new SkriptParser has to be created. + * + * @param other The other SkriptParser to copy input, constraints, and context from. + */ + protected SkriptParser(@NotNull SkriptParser other) { + this(other.input, other.parsingConstraints, other.context); + this.suppressMissingAndOrWarnings = other.suppressMissingAndOrWarnings; + } + + protected SkriptParser(@NotNull SkriptParser other, String input) { + this(input, other.parsingConstraints, other.context); + this.suppressMissingAndOrWarnings = other.suppressMissingAndOrWarnings; + } + + /** + * Parses a string as one of the given syntax elements. + *

+ * Can print an error. + * + * @param The {@link SyntaxInfo} type associated with the given + * {@link Key}. + * @param The type of the returned {@link SyntaxElement}, which should be equivalent to the class + * returned by {@link SyntaxInfo#type()}. + * @param input The raw string input to be parsed. + * @param parsingConstraints A {@link ParsingConstraints} object containing all the allowed syntaxes. + * @param expectedTypeKey A {@link Key} that determines what + * kind of syntax is expected as a result of the parsing. + * @param context The context under which to parse this string. + * @param defaultError The default error to use if no other error is encountered during parsing. + * @return A parsed, initialized {@link SyntaxElement}, or null if parsing failed. + */ + public static > @Nullable E parse( + String input, + @NotNull ParsingConstraints parsingConstraints, + Key expectedTypeKey, + ParseContext context, + @Nullable String defaultError + ) { + Iterator uncheckedIterator = Skript.instance().syntaxRegistry().syntaxes(expectedTypeKey).iterator(); + + return SkriptParser.parse( + input, + parsingConstraints, + uncheckedIterator, + context, + defaultError + ); + } + + /** + * Parses a string as one of the given syntax elements. + *

+ * Can print an error. + * + * @param The type of the returned {@link SyntaxElement}, which should be equivalent to the class + * returned by {@link SyntaxInfo#type()}. + * @param input The raw string input to be parsed. + * @param parsingConstraints A {@link ParsingConstraints} object containing all the allowed syntaxes. + * @param allowedSyntaxes An {@link Iterator} over {@link SyntaxElementInfo} objects that represent the allowed syntaxes. + * @param context The context under which to parse this string. + * @param defaultError The default error to use if no other error is encountered during parsing. + * @return A parsed, initialized {@link SyntaxElement}, or null if parsing failed. + */ + public static @Nullable E parse( + String input, + @NotNull ParsingConstraints parsingConstraints, + Iterator> allowedSyntaxes, + ParseContext context, + @Nullable String defaultError + ) { + input = input.trim(); + if (input.isEmpty()) { + Skript.error(defaultError); + return null; + } + try (ParseLogHandler log = SkriptLogger.startParseLogHandler()) { + E element = new SkriptParser(input, parsingConstraints, context).parse(allowedSyntaxes); + if (element != null) { + log.printLog(); + return element; + } + log.printError(defaultError); + return null; + } + } + + /** + * @deprecated use {@link #parse(String, ParsingConstraints, Iterator, ParseContext, String)} with + * {@link ParsingConstraints#allowLiterals(boolean)} set to false and {@link ParseContext#DEFAULT}. + */ + @Deprecated + public static @Nullable T parseStatic( + String expr, + Iterator> source, + @Nullable String defaultError + ) { + return parse(expr, ParsingConstraints.all().allowNonLiterals(false), source, ParseContext.DEFAULT, defaultError); + } + + @Deprecated + public static @Nullable T parseStatic( + String expr, + Iterator> source, + ParseContext parseContext, + @Nullable String defaultError + ) { + return parse(expr, ParsingConstraints.all().allowNonLiterals(false), source, parseContext, defaultError); + } + + + /** + * Attempts to parse this parser's input against the given syntax. + * Prints parse errors (i.e. must start a ParseLog before calling this method) + * {@link #parse(Key)} is preferred for parsing against a specific syntax. + * + * @param allowedSyntaxes The iterator of {@link SyntaxElementInfo} objects to parse against. + * @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. + */ + @ApiStatus.Internal + public @Nullable E parse(@NotNull Iterator> allowedSyntaxes) { + allowedSyntaxes = parsingConstraints.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; + } + } + + /** + * 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 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, 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. + */ + @ApiStatus.Internal + public > @Nullable E parse(Key expectedTypeKey) { + Iterator> uncheckedIterator = new Iterator<>() { + + private final Iterator iterator = Skript.instance().syntaxRegistry().syntaxes(expectedTypeKey).iterator(); + + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + @Override + @Contract(" -> new") + public @NotNull SyntaxElementInfo next() { + return SyntaxElementInfo.fromModern(iterator.next()); + } + }; + + return parse(uncheckedIterator); + } + + /** + * 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, 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(); + 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, 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, 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 ParseResult} containing the results of the parsing, if successful. Null otherwise. + * @see #parse(SyntaxInfo, String, int) + */ + private @Nullable ParseResult parseAgainstPattern(String pattern) throws MalformedPatternException { + SkriptPattern skriptPattern = patterns.computeIfAbsent(pattern, PatternCompiler::compile); + ch.njol.skript.patterns.MatchResult matchResult = skriptPattern.match(input, parsingConstraints.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 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); + 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 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 (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, ParseResult parseResult) { + if (element instanceof EventRestrictedSyntax eventRestrictedSyntax) { + Class[] supportedEvents = eventRestrictedSyntax.supportedEvents(); + if (!getParser().isCurrentEvent(supportedEvents)) { + 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(); + } + + @Contract("-> this") + public SkriptParser suppressMissingAndOrWarnings() { + suppressMissingAndOrWarnings = true; + return this; + } + + /** + * @param types The types to include in the message + * @return "not an x" or "neither an x, a y nor a z" + */ + public static String notOfType(Class... types) { + if (types.length == 1) { + Class type = types[0]; + assert type != null; + return Language.get("not") + " " + Classes.getSuperClassInfo(type).getName().withIndefiniteArticle(); + } else { + StringBuilder message = new StringBuilder(Language.get("neither") + " "); + for (int i = 0; i < types.length; i++) { + if (i != 0) { + if (i != types.length - 1) { + message.append(", "); + } else { + message.append(" ").append(Language.get("nor")).append(" "); + } + } + Class c = types[i]; + assert c != null; + ClassInfo classInfo = Classes.getSuperClassInfo(c); + // if there's a registered class info, + if (classInfo != null) { + // use the article, + message.append(classInfo.getName().withIndefiniteArticle()); + } else { + // otherwise fallback to class name + message.append(c.getName()); + } + } + return message.toString(); + } + } + + public static String notOfType(ClassInfo... types) { + if (types.length == 1) { + return Language.get("not") + " " + types[0].getName().withIndefiniteArticle(); + } else { + StringBuilder message = new StringBuilder(Language.get("neither") + " "); + for (int i = 0; i < types.length; i++) { + if (i != 0) { + if (i != types.length - 1) { + message.append(", "); + } else { + message.append(" ").append(Language.get("nor")).append(" "); + } + } + message.append(types[i].getName().withIndefiniteArticle()); + } + return message.toString(); + } + } + +} diff --git a/src/main/java/org/skriptlang/skript/lang/parser/StatementParser.java b/src/main/java/org/skriptlang/skript/lang/parser/StatementParser.java new file mode 100644 index 00000000000..73e13004a5b --- /dev/null +++ b/src/main/java/org/skriptlang/skript/lang/parser/StatementParser.java @@ -0,0 +1,4 @@ +package org.skriptlang.skript.lang.parser; + +public class StatementParser { +} From 5704b80dae5090de1de6f81a8fbe0bf0477f4a66 Mon Sep 17 00:00:00 2001 From: sovdee <10354869+sovdeeth@users.noreply.github.com> Date: Thu, 23 Oct 2025 12:25:10 -0700 Subject: [PATCH 2/7] clear out previous attempt code --- .../skript/lang/parser/ExpressionParser.java | 515 +----------------- .../skript/lang/parser/FunctionParser.java | 129 +---- .../skript/lang/parser/SkriptParser.java | 4 +- 3 files changed, 4 insertions(+), 644 deletions(-) diff --git a/src/main/java/org/skriptlang/skript/lang/parser/ExpressionParser.java b/src/main/java/org/skriptlang/skript/lang/parser/ExpressionParser.java index cf03ce5301a..a660b7f2f44 100644 --- a/src/main/java/org/skriptlang/skript/lang/parser/ExpressionParser.java +++ b/src/main/java/org/skriptlang/skript/lang/parser/ExpressionParser.java @@ -1,521 +1,8 @@ package org.skriptlang.skript.lang.parser; -import ch.njol.skript.Skript; -import ch.njol.skript.classes.ClassInfo; -import ch.njol.skript.classes.Parser; -import ch.njol.skript.lang.*; -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.SyntaxRegistry; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - public class ExpressionParser extends SkriptParser { - 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'"; - - public ExpressionParser(String input) { + protected ExpressionParser(String input) { super(input); } - - public ExpressionParser(String input, ParsingConstraints constraints) { - super(input, constraints); - } - - public ExpressionParser(@NotNull String input, ParsingConstraints constraints, ParseContext context) { - super(input, constraints, context); - } - - public ExpressionParser(SkriptParser skriptParser, String input) { - super(skriptParser, input); - } - - public final @Nullable Expression parse() { - if (input.isEmpty()) - return null; - - var types = parsingConstraints.getValidReturnTypes(); - - assert types != null; - assert types.length > 0; - assert types.length == 1 || !CollectionUtils.contains(types, Object.class); - - ParseLogHandler log = SkriptLogger.startParseLogHandler(); - try { - Expression parsedExpression = parseSingleExpr(true, null); - if (parsedExpression != null) { - log.printLog(); - return parsedExpression; - } - log.clear(); - - return this.parseExpressionList(log); - } finally { - log.stop(); - } - } - - /** - * 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, parsingConstraints.getValidReturnTypes()); - if (parsedVariable != null) { - if (!parsingConstraints.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 (!parsingConstraints.allowsFunctionCalls() || context != ParseContext.DEFAULT && context != ParseContext.EVENT) - return new Result<>(false, null); - - FunctionReference functionReference = new FunctionParser(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 (!parsingConstraints.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 : parsingConstraints.getValidReturnTypes()) { - if (type.isAssignableFrom(parsedReturnType)) { - log.printLog(); - return new Result<>(false, parsedExpression); - } - } - - // No directly same type found - //noinspection unchecked - Class[] objTypes = (Class[]) parsingConstraints.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(parsingConstraints.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 (!parsingConstraints.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 = parsingConstraints.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; - } - Parser classInfoParser = classInfo.getParser(); - if (classInfoParser == null || !classInfoParser.canParse(context)) { - Skript.error("A " + unparsedClassInfo + " cannot be parsed."); - return null; - } - if (!checkAcceptedType(classInfo.getC(), parsingConstraints.getValidReturnTypes())) { - Skript.error(input + " " + Language.get("is") + " " + notOfType(parsingConstraints.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 new ExpressionParser(this, 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; - } - } - - /* - - * List parsing - - private record OrderedExprInfo(ExprInfo[] infos) { } - - @SafeVarargs - private @Nullable Expression parseExpressionList(ParseLogHandler log, Class... types) { - //noinspection unchecked - return (Expression) parseExpressionList_i(log, types); - } - - private @Nullable Expression parseExpressionList(ParseLogHandler log, ExprInfo info) { - return parseExpressionList_i(log, info); - } - - private @Nullable Expression parseExpressionList(ParseLogHandler log, OrderedExprInfo info) { - return parseExpressionList_i(log, info); - } - - private @Nullable Expression parseExpressionList_i(ParseLogHandler log, Object data) { - */ - - @Nullable Expression parseExpressionList(ParseLogHandler log) { -// var types = parsingConstraints.getValidReturnTypes(); -// boolean isObject = types.length == 1 && types[0] == Object.class; -// List> parsedExpressions = new ArrayList<>(); -// Kleenean and = Kleenean.UNKNOWN; -// boolean isLiteralList = true; -// Expression parsedExpression; -// -// List pieces = new ArrayList<>(); -// { -// Matcher matcher = LIST_SPLIT_PATTERN.matcher(input); -// int i = 0, j = 0; -// for (; i >= 0 && i <= input.length(); i = StringUtils.next(input, i, context)) { -// if (i == input.length() || matcher.region(i, input.length()).lookingAt()) { -// pieces.add(new int[] {j, i}); -// if (i == input.length()) -// break; -// j = i = matcher.end(); -// } -// } -// if (i != input.length()) { -// assert i == -1 && context != ParseContext.COMMAND && context != ParseContext.PARSE : i + "; " + input; -// log.printError("Invalid brackets/variables/text in '" + input + "'", ErrorQuality.NOT_AN_EXPRESSION); -// return null; -// } -// } -// -// if (pieces.size() == 1) { // not a list of expressions, and a single one has failed to parse above -// if (input.startsWith("(") && input.endsWith(")") && StringUtils.next(input, 0, context) == input.length()) { -// log.clear(); -// return new ExpressionParser(this, input.substring(1, input.length() - 1)).parse(); -// } -// if (isObject && parsingConstraints.allowsLiterals()) { // single expression - can return an UnparsedLiteral now -// log.clear(); -// //noinspection unchecked -// return (Expression) new UnparsedLiteral(input, log.getError()); -// } -// // results in useless errors most of the time -//// log.printError("'" + input + "' " + Language.get("is") + " " + notOfType(types), ErrorQuality.NOT_AN_EXPRESSION); -// log.printError(); -// return null; -// } -// -// outer: for (int first = 0; first < pieces.size();) { -// for (int last = 1; last <= pieces.size() - first; last++) { -// if (first == 0 && last == pieces.size()) // i.e. the whole expression - already tried to parse above -// continue; -// int start = pieces.get(first)[0], end = pieces.get(first + last - 1)[1]; -// String subExpr = input.substring(start, end).trim(); -// assert subExpr.length() < input.length() : subExpr; -// -// if (subExpr.startsWith("(") && subExpr.endsWith(")") && StringUtils.next(subExpr, 0, context) == subExpr.length()) -// parsedExpression = new ExpressionParser(this, subExpr).parse(); // only parse as possible expression list if its surrounded by brackets -// else -// parsedExpression = new ExpressionParser(this, subExpr).parseSingleExpr(last == 1, log.getError()); // otherwise parse as a single expression only -// if (parsedExpression != null) { -// isLiteralList &= parsedExpression instanceof Literal; -// parsedExpressions.add(parsedExpression); -// if (first != 0) { -// String delimiter = input.substring(pieces.get(first - 1)[1], start).trim().toLowerCase(Locale.ENGLISH); -// if (!delimiter.equals(",")) { -// boolean or = !delimiter.contains("nor") && delimiter.endsWith("or"); -// if (and.isUnknown()) { -// and = Kleenean.get(!or); // nor is and -// } else { -// if (and != Kleenean.get(!or)) { -// Skript.warning(MULTIPLE_AND_OR + " List: " + input); -// and = Kleenean.TRUE; -// } -// } -// } -// } -// first += last; -// continue outer; -// } -// } -// log.printError(); -// return null; -// } -// -// log.printLog(false); -// -// if (parsedExpressions.size() == 1) -// return parsedExpressions.get(0); -// -// if (and.isUnknown() && !suppressMissingAndOrWarnings) { -// ParserInstance parser = getParser(); -// Script currentScript = parser.isActive() ? parser.getCurrentScript() : null; -// if (currentScript == null || !currentScript.suppressesWarning(ScriptWarning.MISSING_CONJUNCTION)) -// Skript.warning(MISSING_AND_OR + ": " + input); -// } -// -// Class[] exprReturnTypes = new Class[parsedExpressions.size()]; -// for (int i = 0; i < parsedExpressions.size(); i++) -// exprReturnTypes[i] = parsedExpressions.get(i).getReturnType(); -// -// if (isLiteralList) { -// //noinspection unchecked,SuspiciousToArrayCall -// Literal[] literals = parsedExpressions.toArray(new Literal[0]); -// //noinspection unchecked -// return new LiteralList<>(literals, (Class) Classes.getSuperClassInfo(exprReturnTypes).getC(), exprReturnTypes, !and.isFalse()); -// } else { -// //noinspection unchecked -// Expression[] expressions = parsedExpressions.toArray(new Expression[0]); -// //noinspection unchecked -// return new ExpressionList<>(expressions, (Class) Classes.getSuperClassInfo(exprReturnTypes).getC(), exprReturnTypes, !and.isFalse()); -// } - return null; - } - - /** - * A record that contains internal information about the success of a single parsing operation, to facilitate helper methods. - * Not to be confused with {@link 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 index 19d49a851ce..94259017b3b 100644 --- a/src/main/java/org/skriptlang/skript/lang/parser/FunctionParser.java +++ b/src/main/java/org/skriptlang/skript/lang/parser/FunctionParser.java @@ -1,135 +1,8 @@ package org.skriptlang.skript.lang.parser; -import ch.njol.skript.Skript; -import ch.njol.skript.lang.Expression; -import ch.njol.skript.lang.ExpressionList; -import ch.njol.skript.lang.ParseContext; -import ch.njol.skript.lang.function.FunctionReference; -import ch.njol.skript.lang.function.Functions; -import ch.njol.skript.lang.parser.ParserInstance; -import ch.njol.skript.log.ParseLogHandler; -import ch.njol.skript.log.SkriptLogger; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.skriptlang.skript.lang.script.Script; - -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Supplier; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - public class FunctionParser extends SkriptParser { - private final static Pattern FUNCTION_CALL_PATTERN = Pattern.compile("(" + Functions.functionNamePattern + ")\\((.*)\\)"); - - public FunctionParser(String input) { + protected FunctionParser(String input) { super(input); } - - public FunctionParser(String input, ParsingConstraints constraints) { - super(input, constraints); - } - - public FunctionParser(@NotNull String input, ParsingConstraints constraints, ParseContext context) { - super(input, constraints, context); - } - - public FunctionParser(@NotNull SkriptParser other) { - super(other); - } - - public final @Nullable FunctionReference parse() { - if (context != ParseContext.DEFAULT && context != ParseContext.EVENT) - return null; - var returnTypes = parsingConstraints.getValidReturnTypes(); - AtomicBoolean unaryArgument = new AtomicBoolean(false); - try (ParseLogHandler log = SkriptLogger.startParseLogHandler()) { - Matcher matcher = FUNCTION_CALL_PATTERN.matcher(input); - if (!matcher.matches()) { - log.printLog(); - return null; - } - - String functionName = matcher.group(1); - String args = matcher.group(2); - Expression[] params; - - // Check for incorrect quotes, e.g. "myFunction() + otherFunction()" being parsed as one function - // See https://github.com/SkriptLang/Skript/issues/1532 - for (int i = 0; i < args.length(); i = ch.njol.skript.lang.SkriptParser.next(args, i, context)) { - if (i == -1) { - log.printLog(); - return null; - } - } - - if (!parsingConstraints.allowsNonLiterals()) { - Skript.error("Functions cannot be used here (or there is a problem with your arguments)."); - log.printError(); - return null; - } - ExpressionParser exprParser = new ExpressionParser(args, - parsingConstraints.copy() - .allowLiterals(true) - .constrainReturnTypes(Object.class), - context); - exprParser.suppressMissingAndOrWarnings(); - - params = this.getFunctionArguments(exprParser::parse, args, unaryArgument); - if (params == null) { - log.printError(); - return null; - } - - ParserInstance parser = getParser(); - Script currentScript = parser.isActive() ? parser.getCurrentScript() : null; - //noinspection unchecked - FunctionReference functionReference = (FunctionReference) new FunctionReference<>(functionName, SkriptLogger.getNode(), - currentScript != null ? currentScript.getConfig().getFileName() : null, returnTypes, params);//.toArray(new Expression[params.size()])); - attempt_list_parse: - if (unaryArgument.get() && !functionReference.validateParameterArity(true)) { - try (ParseLogHandler ignored = SkriptLogger.startParseLogHandler()) { - - exprParser.suppressMissingAndOrWarnings(); - params = this.getFunctionArguments(() -> exprParser.parseExpressionList(ignored), args, unaryArgument); - ignored.clear(); - if (params == null) - break attempt_list_parse; - } - //noinspection unchecked - functionReference = (FunctionReference) new FunctionReference<>(functionName, SkriptLogger.getNode(), - currentScript != null ? currentScript.getConfig().getFileName() : null, returnTypes, params); - } - if (!functionReference.validateFunction(true)) { - log.printError(); - return null; - } - log.printLog(); - return functionReference; - } - } - - private Expression @Nullable [] getFunctionArguments(Supplier> parsing, @NotNull String args, AtomicBoolean unary) { - Expression[] params; - if (!args.isEmpty()) { - Expression parsedExpression = parsing.get(); - if (parsedExpression == null) - return null; - if (parsedExpression instanceof ExpressionList expressionList) { - if (!parsedExpression.getAnd()) { - Skript.error("Function arguments must be separated by commas and optionally an 'and', but not an 'or'." - + " Put the 'or' into a second set of parentheses if you want to make it a single parameter, e.g. 'give(player, (sword or axe))'"); - return null; - } - params = expressionList.getExpressions(); - } else { - unary.set(true); - params = new Expression[] {parsedExpression}; - } - } else { - params = new Expression[0]; - } - return params; - } - } diff --git a/src/main/java/org/skriptlang/skript/lang/parser/SkriptParser.java b/src/main/java/org/skriptlang/skript/lang/parser/SkriptParser.java index f4cda0846ad..8780eab6bbb 100644 --- a/src/main/java/org/skriptlang/skript/lang/parser/SkriptParser.java +++ b/src/main/java/org/skriptlang/skript/lang/parser/SkriptParser.java @@ -227,7 +227,7 @@ protected SkriptParser(@NotNull SkriptParser other, String input) { * {@link #parse(Key)} is preferred for parsing against a specific syntax. * * @param allowedSyntaxes The iterator of {@link SyntaxElementInfo} objects to parse against. - * @return A parsed {@link SyntaxElement} with its {@link SyntaxElement#init(Expression[], int, Kleenean, SkriptParser.ParseResult)} + * @return A parsed {@link SyntaxElement} with its {@link SyntaxElement#init(Expression[], int, Kleenean, 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. */ @@ -262,7 +262,7 @@ protected SkriptParser(@NotNull SkriptParser other, String input) { * * @param expectedTypeKey A {@link 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, SkriptParser.ParseResult)} + * @return A parsed {@link SyntaxElement} with its {@link SyntaxElement#init(Expression[], int, Kleenean, 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. */ From 87a6bf5c2c27438ea9271d35966592f0acb547de Mon Sep 17 00:00:00 2001 From: sovdee <10354869+sovdeeth@users.noreply.github.com> Date: Thu, 23 Oct 2025 12:27:58 -0700 Subject: [PATCH 3/7] Update SkriptParser.java --- .../skript/lang/parser/SkriptParser.java | 96 ------------------- 1 file changed, 96 deletions(-) diff --git a/src/main/java/org/skriptlang/skript/lang/parser/SkriptParser.java b/src/main/java/org/skriptlang/skript/lang/parser/SkriptParser.java index 8780eab6bbb..6ef07645703 100644 --- a/src/main/java/org/skriptlang/skript/lang/parser/SkriptParser.java +++ b/src/main/java/org/skriptlang/skript/lang/parser/SkriptParser.java @@ -14,14 +14,12 @@ 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.localization.Language; 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.skript.registrations.Classes; import ch.njol.util.Kleenean; import ch.njol.util.StringUtils; import org.bukkit.event.Event; @@ -38,17 +36,9 @@ import java.lang.reflect.InvocationTargetException; import java.util.*; import java.util.concurrent.ConcurrentHashMap; -import java.util.regex.Pattern; public class SkriptParser { - @Deprecated(since = "INSERT VERSION") - public static final int PARSE_EXPRESSIONS = 1; - @Deprecated(since = "INSERT VERSION") - public static final int PARSE_LITERALS = 2; - @Deprecated(since = "INSERT VERSION") - public static final int ALL_FLAGS = PARSE_EXPRESSIONS | PARSE_LITERALS; - private static final Map patterns = new ConcurrentHashMap<>(); @@ -57,8 +47,6 @@ public class SkriptParser { *

* group 1 is null for ',', otherwise it's one of and/or/nor (not necessarily lowercase). */ - public static final Pattern LIST_SPLIT_PATTERN = Pattern.compile("\\s*,?\\s+(and|n?or)\\s+|\\s*,\\s*", Pattern.CASE_INSENSITIVE); - public static final Pattern OR_PATTERN = Pattern.compile("\\sor\\s", Pattern.CASE_INSENSITIVE); protected boolean suppressMissingAndOrWarnings = SkriptConfig.disableMissingAndOrWarnings.value(); protected ParsingConstraints parsingConstraints; @@ -197,30 +185,6 @@ protected SkriptParser(@NotNull SkriptParser other, String input) { } } - /** - * @deprecated use {@link #parse(String, ParsingConstraints, Iterator, ParseContext, String)} with - * {@link ParsingConstraints#allowLiterals(boolean)} set to false and {@link ParseContext#DEFAULT}. - */ - @Deprecated - public static @Nullable T parseStatic( - String expr, - Iterator> source, - @Nullable String defaultError - ) { - return parse(expr, ParsingConstraints.all().allowNonLiterals(false), source, ParseContext.DEFAULT, defaultError); - } - - @Deprecated - public static @Nullable T parseStatic( - String expr, - Iterator> source, - ParseContext parseContext, - @Nullable String defaultError - ) { - return parse(expr, ParsingConstraints.all().allowNonLiterals(false), source, parseContext, defaultError); - } - - /** * Attempts to parse this parser's input against the given syntax. * Prints parse errors (i.e. must start a ParseLog before calling this method) @@ -523,64 +487,4 @@ protected static ParserInstance getParser() { return ParserInstance.get(); } - @Contract("-> this") - public SkriptParser suppressMissingAndOrWarnings() { - suppressMissingAndOrWarnings = true; - return this; - } - - /** - * @param types The types to include in the message - * @return "not an x" or "neither an x, a y nor a z" - */ - public static String notOfType(Class... types) { - if (types.length == 1) { - Class type = types[0]; - assert type != null; - return Language.get("not") + " " + Classes.getSuperClassInfo(type).getName().withIndefiniteArticle(); - } else { - StringBuilder message = new StringBuilder(Language.get("neither") + " "); - for (int i = 0; i < types.length; i++) { - if (i != 0) { - if (i != types.length - 1) { - message.append(", "); - } else { - message.append(" ").append(Language.get("nor")).append(" "); - } - } - Class c = types[i]; - assert c != null; - ClassInfo classInfo = Classes.getSuperClassInfo(c); - // if there's a registered class info, - if (classInfo != null) { - // use the article, - message.append(classInfo.getName().withIndefiniteArticle()); - } else { - // otherwise fallback to class name - message.append(c.getName()); - } - } - return message.toString(); - } - } - - public static String notOfType(ClassInfo... types) { - if (types.length == 1) { - return Language.get("not") + " " + types[0].getName().withIndefiniteArticle(); - } else { - StringBuilder message = new StringBuilder(Language.get("neither") + " "); - for (int i = 0; i < types.length; i++) { - if (i != 0) { - if (i != types.length - 1) { - message.append(", "); - } else { - message.append(" ").append(Language.get("nor")).append(" "); - } - } - message.append(types[i].getName().withIndefiniteArticle()); - } - return message.toString(); - } - } - } From b839cda4390402b346794afdc1c569efe0a7b083 Mon Sep 17 00:00:00 2001 From: sovdee <10354869+sovdeeth@users.noreply.github.com> Date: Thu, 23 Oct 2025 12:29:55 -0700 Subject: [PATCH 4/7] Update ParsingConstraints.java --- .../skript/lang/parser/ParsingConstraints.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/skriptlang/skript/lang/parser/ParsingConstraints.java b/src/main/java/org/skriptlang/skript/lang/parser/ParsingConstraints.java index 35a32b1651b..bdfab8f1e2d 100644 --- a/src/main/java/org/skriptlang/skript/lang/parser/ParsingConstraints.java +++ b/src/main/java/org/skriptlang/skript/lang/parser/ParsingConstraints.java @@ -16,6 +16,9 @@ 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 { @@ -158,18 +161,18 @@ public ParsingConstraints allowLiterals(boolean allow) { public int asParseFlags() { int flags = 0; if (allowNonLiterals) { - flags |= SkriptParser.PARSE_EXPRESSIONS; + flags |= PARSE_EXPRESSIONS; } if (allowLiterals) { - flags |= SkriptParser.PARSE_LITERALS; + flags |= PARSE_LITERALS; } return flags; } @ApiStatus.Internal public ParsingConstraints applyParseFlags(int flags) { - allowNonLiterals = (flags & SkriptParser.PARSE_EXPRESSIONS) != 0; - allowLiterals = (flags & SkriptParser.PARSE_LITERALS) != 0; + allowNonLiterals = (flags & PARSE_EXPRESSIONS) != 0; + allowLiterals = (flags & PARSE_LITERALS) != 0; return this; } From 6707edde152afbc0d3da2bf6586479f8178e0716 Mon Sep 17 00:00:00 2001 From: sovdee <10354869+sovdeeth@users.noreply.github.com> Date: Thu, 23 Oct 2025 12:47:57 -0700 Subject: [PATCH 5/7] use new parser parse() method --- .../ch/njol/skript/lang/SkriptParser.java | 94 ++----------------- .../lang/parser/ParsingConstraints.java | 18 ++-- .../skript/lang/parser/SkriptParser.java | 6 +- 3 files changed, 20 insertions(+), 98 deletions(-) diff --git a/src/main/java/ch/njol/skript/lang/SkriptParser.java b/src/main/java/ch/njol/skript/lang/SkriptParser.java index 70a03471bc9..2d63dfa27ae 100644 --- a/src/main/java/ch/njol/skript/lang/SkriptParser.java +++ b/src/main/java/ch/njol/skript/lang/SkriptParser.java @@ -16,10 +16,7 @@ import ch.njol.skript.lang.function.FunctionRegistry; import ch.njol.skript.lang.function.Functions; 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; @@ -31,7 +28,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,12 +38,12 @@ 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.parser.ParsingConstraints; import org.skriptlang.skript.lang.script.ScriptWarning; import org.skriptlang.skript.registration.SyntaxInfo; import org.skriptlang.skript.registration.SyntaxRegistry; @@ -83,6 +79,8 @@ public final class SkriptParser { public final ParseContext context; + private final org.skriptlang.skript.lang.parser.SkriptParser newParser; + public SkriptParser(String expr) { this(expr, ALL_FLAGS); } @@ -106,6 +104,9 @@ 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 = new org.skriptlang.skript.lang.parser.SkriptParser(this.expr, constraints, context); } public SkriptParser(SkriptParser other, String expr) { @@ -200,88 +201,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; - } + return newParser.parse(source); } /** diff --git a/src/main/java/org/skriptlang/skript/lang/parser/ParsingConstraints.java b/src/main/java/org/skriptlang/skript/lang/parser/ParsingConstraints.java index bdfab8f1e2d..55c003ed7b6 100644 --- a/src/main/java/org/skriptlang/skript/lang/parser/ParsingConstraints.java +++ b/src/main/java/org/skriptlang/skript/lang/parser/ParsingConstraints.java @@ -67,14 +67,16 @@ private ParsingConstraints() { return false; } - // check literals - if (!allowsLiterals() && Literal.class.isAssignableFrom(elementClass)) { - return false; - } - // check non-literals - // TODO: allow simplification - if (!allowsNonLiterals() && !Literal.class.isAssignableFrom(elementClass)) { - 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 diff --git a/src/main/java/org/skriptlang/skript/lang/parser/SkriptParser.java b/src/main/java/org/skriptlang/skript/lang/parser/SkriptParser.java index 6ef07645703..c849da6399b 100644 --- a/src/main/java/org/skriptlang/skript/lang/parser/SkriptParser.java +++ b/src/main/java/org/skriptlang/skript/lang/parser/SkriptParser.java @@ -64,7 +64,7 @@ public class SkriptParser { * * @param input The text to parse. */ - protected SkriptParser(String input) { + public SkriptParser(String input) { this(input, ParsingConstraints.all()); } @@ -77,7 +77,7 @@ protected SkriptParser(String input) { * @param constraints The constraints under which to parse. * @param input The text to parse. */ - protected SkriptParser(String input, ParsingConstraints constraints) { + public SkriptParser(String input, ParsingConstraints constraints) { this(input, constraints, ParseContext.DEFAULT); } @@ -90,7 +90,7 @@ protected SkriptParser(String input, ParsingConstraints constraints) { * @param constraints The constraints under which to parse. * @param context The parse context. */ - protected SkriptParser(@NotNull String input, ParsingConstraints constraints, ParseContext context) { + public SkriptParser(@NotNull String input, ParsingConstraints constraints, ParseContext context) { this.input = input.trim(); this.parsingConstraints = constraints; this.context = context; From f3f85a844da54c5df2716c66edb8f5b922c12bb6 Mon Sep 17 00:00:00 2001 From: sovdee <10354869+sovdeeth@users.noreply.github.com> Date: Thu, 23 Oct 2025 13:09:49 -0700 Subject: [PATCH 6/7] remove unused code, move junit tests --- .../ch/njol/skript/lang/SkriptParser.java | 104 ------------------ .../parser}/DefaultExpressionErrorTest.java | 2 +- .../parser}/GetDefaultExpressionsTest.java | 4 +- 3 files changed, 3 insertions(+), 107 deletions(-) rename src/test/java/{ch/njol/skript/lang => org/skriptlang/skript/lang/parser}/DefaultExpressionErrorTest.java (99%) rename src/test/java/{ch/njol/skript/lang => org/skriptlang/skript/lang/parser}/GetDefaultExpressionsTest.java (99%) diff --git a/src/main/java/ch/njol/skript/lang/SkriptParser.java b/src/main/java/ch/njol/skript/lang/SkriptParser.java index 2d63dfa27ae..06c4fe43d29 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,7 +9,6 @@ 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; @@ -37,12 +35,8 @@ 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.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.parser.ParsingConstraints; import org.skriptlang.skript.lang.script.ScriptWarning; import org.skriptlang.skript.registration.SyntaxInfo; @@ -50,7 +44,6 @@ import java.lang.reflect.Array; import java.util.*; -import java.util.Map.Entry; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Supplier; import java.util.regex.MatchResult; @@ -204,103 +197,6 @@ public boolean hasTag(String tag) { return newParser.parse(source); } - /** - * 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 " + 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); - } - - /** - * 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")); - } - private static final Pattern VARIABLE_PATTERN = Pattern.compile("((the )?var(iable)? )?\\{.+\\}", Pattern.CASE_INSENSITIVE); /** 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..9f69877ff44 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.SkriptParser.getDefaultExpressions; public class GetDefaultExpressionsTest extends SkriptJUnitTest { From 09dd859d5daa106a215d8ee0334ee772df187a09 Mon Sep 17 00:00:00 2001 From: sovdee <10354869+sovdeeth@users.noreply.github.com> Date: Thu, 23 Oct 2025 21:15:28 -0700 Subject: [PATCH 7/7] rename and create interfaces --- .../ch/njol/skript/lang/SkriptParser.java | 8 +- .../skript/lang/parser/ExpressionParser.java | 21 +- .../lang/parser/ExpressionParserImpl.java | 393 ++++++++++++++++++ .../skript/lang/parser/FunctionParser.java | 17 +- .../lang/parser/FunctionParserImpl.java | 20 + .../skript/lang/parser/StatementParser.java | 4 - .../skript/lang/parser/SyntaxParser.java | 131 ++++++ ...kriptParser.java => SyntaxParserImpl.java} | 341 +++++++-------- .../parser/GetDefaultExpressionsTest.java | 2 +- 9 files changed, 727 insertions(+), 210 deletions(-) create mode 100644 src/main/java/org/skriptlang/skript/lang/parser/ExpressionParserImpl.java create mode 100644 src/main/java/org/skriptlang/skript/lang/parser/FunctionParserImpl.java delete mode 100644 src/main/java/org/skriptlang/skript/lang/parser/StatementParser.java create mode 100644 src/main/java/org/skriptlang/skript/lang/parser/SyntaxParser.java rename src/main/java/org/skriptlang/skript/lang/parser/{SkriptParser.java => SyntaxParserImpl.java} (56%) diff --git a/src/main/java/ch/njol/skript/lang/SkriptParser.java b/src/main/java/ch/njol/skript/lang/SkriptParser.java index 06c4fe43d29..77c71e78056 100644 --- a/src/main/java/ch/njol/skript/lang/SkriptParser.java +++ b/src/main/java/ch/njol/skript/lang/SkriptParser.java @@ -38,6 +38,7 @@ import org.jetbrains.annotations.Nullable; import org.skriptlang.skript.lang.converter.Converters; 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; @@ -72,7 +73,7 @@ public final class SkriptParser { public final ParseContext context; - private final org.skriptlang.skript.lang.parser.SkriptParser newParser; + private final SyntaxParser newParser; public SkriptParser(String expr) { this(expr, ALL_FLAGS); @@ -99,7 +100,10 @@ public SkriptParser(String expr, int flags, ParseContext context) { this.context = context; ParsingConstraints constraints = ParsingConstraints.all().applyParseFlags(flags); - this.newParser = new org.skriptlang.skript.lang.parser.SkriptParser(this.expr, constraints, context); + this.newParser = SyntaxParser.from(Skript.instance()) + .input(this.expr) + .parseContext(this.context) + .constraints(constraints); } public SkriptParser(SkriptParser other, String expr) { diff --git a/src/main/java/org/skriptlang/skript/lang/parser/ExpressionParser.java b/src/main/java/org/skriptlang/skript/lang/parser/ExpressionParser.java index a660b7f2f44..a780f25c18e 100644 --- a/src/main/java/org/skriptlang/skript/lang/parser/ExpressionParser.java +++ b/src/main/java/org/skriptlang/skript/lang/parser/ExpressionParser.java @@ -1,8 +1,21 @@ package org.skriptlang.skript.lang.parser; -public class ExpressionParser extends SkriptParser { +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); + } + - protected ExpressionParser(String input) { - super(input); - } } 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 index 94259017b3b..945cb78b600 100644 --- a/src/main/java/org/skriptlang/skript/lang/parser/FunctionParser.java +++ b/src/main/java/org/skriptlang/skript/lang/parser/FunctionParser.java @@ -1,8 +1,19 @@ package org.skriptlang.skript.lang.parser; -public class FunctionParser extends SkriptParser { +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.skriptlang.skript.Skript; - protected FunctionParser(String input) { - super(input); +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/StatementParser.java b/src/main/java/org/skriptlang/skript/lang/parser/StatementParser.java deleted file mode 100644 index 73e13004a5b..00000000000 --- a/src/main/java/org/skriptlang/skript/lang/parser/StatementParser.java +++ /dev/null @@ -1,4 +0,0 @@ -package org.skriptlang.skript.lang.parser; - -public class StatementParser { -} 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/SkriptParser.java b/src/main/java/org/skriptlang/skript/lang/parser/SyntaxParserImpl.java similarity index 56% rename from src/main/java/org/skriptlang/skript/lang/parser/SkriptParser.java rename to src/main/java/org/skriptlang/skript/lang/parser/SyntaxParserImpl.java index c849da6399b..55987cd3c03 100644 --- a/src/main/java/org/skriptlang/skript/lang/parser/SkriptParser.java +++ b/src/main/java/org/skriptlang/skript/lang/parser/SyntaxParserImpl.java @@ -1,14 +1,9 @@ - package org.skriptlang.skript.lang.parser; -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.lang.*; -import ch.njol.skript.lang.DefaultExpressionUtils.DefaultExpressionError; -import ch.njol.skript.lang.SkriptParser.ExprInfo; -import ch.njol.skript.lang.SkriptParser.ParseResult; import ch.njol.skript.lang.parser.DefaultValueData; import ch.njol.skript.lang.parser.ParseStackOverflowException; import ch.njol.skript.lang.parser.ParserInstance; @@ -22,245 +17,200 @@ 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.ApiStatus; 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.Key; +import org.skriptlang.skript.registration.SyntaxRegistry; import java.lang.reflect.InvocationTargetException; import java.util.*; import java.util.concurrent.ConcurrentHashMap; -public class SkriptParser { +/** + * 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 parsingConstraints; - protected final String input; - public final ParseContext context; + protected ParsingConstraints constraints; + protected ParseContext context; + protected String input; + protected String defaultError; + protected final Skript skript; - public final boolean doSimplification = SkriptConfig.simplifySyntaxesOnParse.value(); + 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); + } /** - * Constructs a new SkriptParser object that can be used to parse the given expression. - * Parses expressions and literals using {@link ParseContext#DEFAULT}. - *

- * A SkriptParser can be re-used indefinitely for the given expression, but to parse a new expression a new SkriptParser has to be created. - * - * @param input The text to parse. + * Asserts that the parser is ready to parse. Implementations should call this method before attempting to parse. */ - public SkriptParser(String input) { - this(input, ParsingConstraints.all()); + 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."); } /** - * Constructs a new SkriptParser object that can be used to parse the given expression. - * Parses using {@link ParseContext#DEFAULT}. - *

- * A SkriptParser can be re-used indefinitely for the given expression, but to parse a new expression a new SkriptParser has to be created. - * - * @param constraints The constraints under which to parse. - * @param input The text to parse. + * @return the input string to be parsed */ - public SkriptParser(String input, ParsingConstraints constraints) { - this(input, constraints, ParseContext.DEFAULT); + @Contract(pure = true) + public String input() { + return input; } /** - * Constructs a new SkriptParser object that can be used to parse the given expression. - *

- * A SkriptParser can be re-used indefinitely for the given expression, but to parse a new expression a new SkriptParser has to be created. - * - * @param input The text to parse. - * @param constraints The constraints under which to parse. - * @param context The parse context. + * Sets the input string to be parsed. + * @param input the new input string + * @return this parser instance */ - public SkriptParser(@NotNull String input, ParsingConstraints constraints, ParseContext context) { - this.input = input.trim(); - this.parsingConstraints = constraints; - this.context = context; + @Contract("_ -> this") + @SuppressWarnings("unchecked") + public P input(@NotNull String input) { + this.input = input; + return (P) this; } /** - * Constructs a new SkriptParser object that can be used to parse the given expression. - *

- * A SkriptParser can be re-used indefinitely for the given expression, but to parse a new expression a new SkriptParser has to be created. - * - * @param other The other SkriptParser to copy input, constraints, and context from. + * @return the current parsing constraints */ - protected SkriptParser(@NotNull SkriptParser other) { - this(other.input, other.parsingConstraints, other.context); - this.suppressMissingAndOrWarnings = other.suppressMissingAndOrWarnings; + @Contract(pure = true) + public ParsingConstraints constraints() { + return constraints; } - protected SkriptParser(@NotNull SkriptParser other, String input) { - this(input, other.parsingConstraints, other.context); - this.suppressMissingAndOrWarnings = other.suppressMissingAndOrWarnings; + /** + * 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; } /** - * Parses a string as one of the given syntax elements. - *

- * Can print an error. - * - * @param The {@link SyntaxInfo} type associated with the given - * {@link Key}. - * @param The type of the returned {@link SyntaxElement}, which should be equivalent to the class - * returned by {@link SyntaxInfo#type()}. - * @param input The raw string input to be parsed. - * @param parsingConstraints A {@link ParsingConstraints} object containing all the allowed syntaxes. - * @param expectedTypeKey A {@link Key} that determines what - * kind of syntax is expected as a result of the parsing. - * @param context The context under which to parse this string. - * @param defaultError The default error to use if no other error is encountered during parsing. - * @return A parsed, initialized {@link SyntaxElement}, or null if parsing failed. + * @return the current parse context */ - public static > @Nullable E parse( - String input, - @NotNull ParsingConstraints parsingConstraints, - Key expectedTypeKey, - ParseContext context, - @Nullable String defaultError - ) { - Iterator uncheckedIterator = Skript.instance().syntaxRegistry().syntaxes(expectedTypeKey).iterator(); - - return SkriptParser.parse( - input, - parsingConstraints, - uncheckedIterator, - context, - defaultError - ); + @Contract(pure = true) + public ParseContext parseContext() { + return context; } /** - * Parses a string as one of the given syntax elements. - *

- * Can print an error. - * - * @param The type of the returned {@link SyntaxElement}, which should be equivalent to the class - * returned by {@link SyntaxInfo#type()}. - * @param input The raw string input to be parsed. - * @param parsingConstraints A {@link ParsingConstraints} object containing all the allowed syntaxes. - * @param allowedSyntaxes An {@link Iterator} over {@link SyntaxElementInfo} objects that represent the allowed syntaxes. - * @param context The context under which to parse this string. - * @param defaultError The default error to use if no other error is encountered during parsing. - * @return A parsed, initialized {@link SyntaxElement}, or null if parsing failed. + * Sets the parse context. + * @param context the new parse context + * @return this parser instance */ - public static @Nullable E parse( - String input, - @NotNull ParsingConstraints parsingConstraints, - Iterator> allowedSyntaxes, - ParseContext context, - @Nullable String defaultError - ) { - input = input.trim(); - if (input.isEmpty()) { - Skript.error(defaultError); - return null; - } - try (ParseLogHandler log = SkriptLogger.startParseLogHandler()) { - E element = new SkriptParser(input, parsingConstraints, context).parse(allowedSyntaxes); - if (element != null) { - log.printLog(); - return element; - } - log.printError(defaultError); - return null; - } + @Contract("_ -> this") + @SuppressWarnings("unchecked") + public P parseContext(@NotNull ParseContext context) { + this.context = context; + return (P) this; } /** - * Attempts to parse this parser's input against the given syntax. - * Prints parse errors (i.e. must start a ParseLog before calling this method) - * {@link #parse(Key)} is preferred for parsing against a specific syntax. - * - * @param allowedSyntaxes The iterator of {@link SyntaxElementInfo} objects to parse against. - * @return A parsed {@link SyntaxElement} with its {@link SyntaxElement#init(Expression[], int, Kleenean, 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. + * @return the default error message to use if parsing fails */ - @ApiStatus.Internal - public @Nullable E parse(@NotNull Iterator> allowedSyntaxes) { - allowedSyntaxes = parsingConstraints.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; - } + @Contract(pure = true) + public @Nullable String defaultError() { + return defaultError; } /** - * 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 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, 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. + * Sets the default error message to use if parsing fails. + * @param defaultError the new default error message + * @return this parser instance */ - @ApiStatus.Internal - public > @Nullable E parse(Key expectedTypeKey) { - Iterator> uncheckedIterator = new Iterator<>() { - - private final Iterator iterator = Skript.instance().syntaxRegistry().syntaxes(expectedTypeKey).iterator(); - - @Override - public boolean hasNext() { - return iterator.hasNext(); - } - - @Override - @Contract(" -> new") - public @NotNull SyntaxElementInfo next() { - return SyntaxElementInfo.fromModern(iterator.next()); - } - }; + @Contract("_ -> this") + @SuppressWarnings("unchecked") + public P defaultError(@Nullable String defaultError) { + this.defaultError = defaultError; + return (P) this; + } - return parse(uncheckedIterator); + @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, ParseResult)} + * @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(); - ParseResult parseResult; + SkriptParser.ParseResult parseResult; try { // attempt to parse with the given pattern parsingStack.push(new ParsingStack.Element(info, patternIndex)); @@ -307,13 +257,13 @@ public boolean hasNext() { } /** - * Runs through all the initialization checks and steps for the given element, finalizing in a call to {@link SyntaxElement#init(Expression[], int, Kleenean, ParseResult)}. + * 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, ParseResult parseResult) { + private boolean initializeElement(SyntaxElement element, int patternIndex, SkriptParser.ParseResult parseResult) { if (!checkRestrictedEvents(element, parseResult)) return false; @@ -337,12 +287,12 @@ private boolean initializeElement(SyntaxElement element, int patternIndex, Parse * 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 ParseResult} containing the results of the parsing, if successful. Null otherwise. + * @return A {@link SkriptParser.ParseResult} containing the results of the parsing, if successful. Null otherwise. * @see #parse(SyntaxInfo, String, int) */ - private @Nullable ParseResult parseAgainstPattern(String pattern) throws MalformedPatternException { + 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, parsingConstraints.asParseFlags(), context); + ch.njol.skript.patterns.MatchResult matchResult = skriptPattern.match(input, constraints.asParseFlags(), context); if (matchResult == null) return null; return matchResult.toParseResult(); @@ -355,14 +305,14 @@ private boolean initializeElement(SyntaxElement element, int patternIndex, Parse * @param pattern The pattern to use to locate required default expressions. * @return true if population was successful, false otherwise. */ - private boolean populateDefaultExpressions(@NotNull ParseResult parseResult, String pattern) { + 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); - ExprInfo exprInfo = types.get(i).getExprInfo(); + SkriptParser.ExprInfo exprInfo = types.get(i).getExprInfo(); if (!exprInfo.isOptional) { List> exprs = getDefaultExpressions(exprInfo, pattern); DefaultExpression matchedExpr = null; @@ -381,23 +331,22 @@ private boolean populateDefaultExpressions(@NotNull ParseResult parseResult, Str return true; } - /** * 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}. + * @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(ExprInfo exprInfo, String pattern) { + 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(); - DefaultExpressionError errorType = DefaultExpressionUtils.isValid(expr, exprInfo, 0); + DefaultExpressionUtils.DefaultExpressionError errorType = DefaultExpressionUtils.isValid(expr, exprInfo, 0); if (errorType == null) { assert expr != null; return expr; @@ -409,18 +358,18 @@ private boolean populateDefaultExpressions(@NotNull ParseResult parseResult, Str /** * 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}. + * @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(ExprInfo exprInfo, String pattern) { + 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<>(DefaultExpressionError.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]; @@ -429,7 +378,7 @@ private boolean populateDefaultExpressions(@NotNull ParseResult parseResult, Str expr = classInfo.getDefaultExpression(); String codeName = classInfo.getCodeName(); - DefaultExpressionError errorType = DefaultExpressionUtils.isValid(expr, exprInfo, i); + DefaultExpressionUtils.DefaultExpressionError errorType = DefaultExpressionUtils.isValid(expr, exprInfo, i); if (errorType != null) { failed.computeIfAbsent(errorType, list -> new ArrayList<>()).add(codeName); @@ -442,7 +391,7 @@ private boolean populateDefaultExpressions(@NotNull ParseResult parseResult, Str return passed; List errors = new ArrayList<>(); - for (Map.Entry> entry : failed.entrySet()) { + for (Map.Entry> entry : failed.entrySet()) { String error = entry.getKey().getError(entry.getValue(), pattern); errors.add(error); } @@ -456,12 +405,12 @@ private boolean populateDefaultExpressions(@NotNull ParseResult parseResult, Str * @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) { + private static boolean checkRestrictedEvents(SyntaxElement element, SkriptParser.ParseResult parseResult) { if (element instanceof EventRestrictedSyntax eventRestrictedSyntax) { Class[] supportedEvents = eventRestrictedSyntax.supportedEvents(); if (!getParser().isCurrentEvent(supportedEvents)) { - Skript.error("'" + parseResult.expr + "' can only be used in " - + EventRestrictedSyntax.supportedEventsNames(supportedEvents)); + ch.njol.skript.Skript.error("'" + parseResult.expr + "' can only be used in " + + EventRestrictedSyntax.supportedEventsNames(supportedEvents)); return false; } } diff --git a/src/test/java/org/skriptlang/skript/lang/parser/GetDefaultExpressionsTest.java b/src/test/java/org/skriptlang/skript/lang/parser/GetDefaultExpressionsTest.java index 9f69877ff44..d5abb29832f 100644 --- a/src/test/java/org/skriptlang/skript/lang/parser/GetDefaultExpressionsTest.java +++ b/src/test/java/org/skriptlang/skript/lang/parser/GetDefaultExpressionsTest.java @@ -14,7 +14,7 @@ import java.util.List; import java.util.function.BiConsumer; -import static org.skriptlang.skript.lang.parser.SkriptParser.getDefaultExpressions; +import static org.skriptlang.skript.lang.parser.SyntaxParserImpl.getDefaultExpressions; public class GetDefaultExpressionsTest extends SkriptJUnitTest {