From 1a6d37ed4233c04da7335427650529e36784cf9f Mon Sep 17 00:00:00 2001 From: sovdee <10354869+sovdeeth@users.noreply.github.com> Date: Sat, 20 Sep 2025 18:42:25 -0700 Subject: [PATCH] Roughly 70% refactor of SkriptParser --- .../skript/lang/EventRestrictedSyntax.java | 27 + .../njol/skript/lang/ParsingConstraints.java | 203 ++ .../ch/njol/skript/lang/SkriptParser.java | 1945 ++++------------- .../java/ch/njol/skript/lang/Variable.java | 34 + .../njol/skript/lang/function/Functions.java | 12 +- .../skript/lang/parser/ExpressionParser.java | 526 +++++ .../skript/lang/parser/FunctionParser.java | 133 ++ src/main/java/ch/njol/util/StringUtils.java | 102 + 8 files changed, 1430 insertions(+), 1552 deletions(-) create mode 100644 src/main/java/ch/njol/skript/lang/ParsingConstraints.java create mode 100644 src/main/java/ch/njol/skript/lang/parser/ExpressionParser.java create mode 100644 src/main/java/ch/njol/skript/lang/parser/FunctionParser.java 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/ParsingConstraints.java b/src/main/java/ch/njol/skript/lang/ParsingConstraints.java new file mode 100644 index 00000000000..bd80a171ae2 --- /dev/null +++ b/src/main/java/ch/njol/skript/lang/ParsingConstraints.java @@ -0,0 +1,203 @@ +package ch.njol.skript.lang; + +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 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.getElementClass(); + 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 void applyParseFlags(int flags) { + allowNonLiterals = (flags & SkriptParser.PARSE_EXPRESSIONS) != 0; + allowLiterals = (flags & SkriptParser.PARSE_LITERALS) != 0; + } + + 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/ch/njol/skript/lang/SkriptParser.java b/src/main/java/ch/njol/skript/lang/SkriptParser.java index a8426bd55d1..c288099f12e 100644 --- a/src/main/java/ch/njol/skript/lang/SkriptParser.java +++ b/src/main/java/ch/njol/skript/lang/SkriptParser.java @@ -1,30 +1,18 @@ + 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; -import ch.njol.skript.command.Argument; -import ch.njol.skript.command.Commands; -import ch.njol.skript.command.ScriptCommand; -import ch.njol.skript.command.ScriptCommandEvent; -import ch.njol.skript.expressions.ExprParse; -import ch.njol.skript.lang.DefaultExpressionUtils.DefaultExpressionError; import ch.njol.skript.lang.function.ExprFunctionCall; import ch.njol.skript.lang.function.FunctionReference; -import ch.njol.skript.lang.function.FunctionRegistry; -import ch.njol.skript.lang.function.Functions; -import ch.njol.skript.lang.function.Signature; import ch.njol.skript.lang.parser.DefaultValueData; import ch.njol.skript.lang.parser.ParseStackOverflowException; import ch.njol.skript.lang.parser.ParserInstance; import ch.njol.skript.lang.parser.ParsingStack; import ch.njol.skript.lang.simplification.Simplifiable; -import ch.njol.skript.lang.util.SimpleLiteral; import ch.njol.skript.localization.Language; -import ch.njol.skript.localization.Message; -import ch.njol.skript.localization.Noun; import ch.njol.skript.log.ErrorQuality; import ch.njol.skript.log.LogEntry; import ch.njol.skript.log.ParseLogHandler; @@ -34,71 +22,78 @@ 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; -import ch.njol.util.NonNullPair; import ch.njol.util.StringUtils; import ch.njol.util.coll.CollectionUtils; -import ch.njol.util.coll.iterator.CheckedIterator; -import com.google.common.base.Preconditions; -import com.google.common.primitives.Booleans; import org.bukkit.event.Event; import org.bukkit.plugin.java.JavaPlugin; +import org.jetbrains.annotations.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.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 org.skriptlang.skript.registration.SyntaxRegistry.Key; -import java.lang.reflect.Array; -import java.util.ArrayList; -import java.util.Deque; -import java.util.EnumMap; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Map.Entry; +import java.lang.reflect.InvocationTargetException; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Supplier; import java.util.regex.MatchResult; -import java.util.regex.Matcher; import java.util.regex.Pattern; -import java.util.regex.PatternSyntaxException; -import java.util.stream.Stream; -/** - * Used for parsing my custom patterns.
- *
- * Note: All parse methods print one error at most xor any amount of warnings and lower level log messages. If the given string doesn't match any pattern then nothing is printed. - * - * @author Peter Güttinger - */ -public final class SkriptParser { +public class SkriptParser { + + @Deprecated(since = "INSERT VERSION", forRemoval = true) + public static final int PARSE_EXPRESSIONS = 1; + @Deprecated(since = "INSERT VERSION", forRemoval = true) + public static final int PARSE_LITERALS = 2; + @Deprecated(since = "INSERT VERSION", forRemoval = true) + public static final int ALL_FLAGS = PARSE_EXPRESSIONS | PARSE_LITERALS; - private final String expr; + private static final Map patterns = new ConcurrentHashMap<>(); - public final static int PARSE_EXPRESSIONS = 1; - public final static int PARSE_LITERALS = 2; - public final static int ALL_FLAGS = PARSE_EXPRESSIONS | PARSE_LITERALS; - private final int flags; - public final boolean doSimplification = SkriptConfig.simplifySyntaxesOnParse.value(); + /** + * 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 SkriptParser(String expr) { - this(expr, ALL_FLAGS); + 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()); } - public SkriptParser(String expr, int flags) { - this(expr, flags, ParseContext.DEFAULT); + /** + * 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); } /** @@ -106,77 +101,110 @@ public SkriptParser(String expr, int flags) { *

* 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 expr The expression to parse - * @param flags Some parse flags ({@link #PARSE_EXPRESSIONS}, {@link #PARSE_LITERALS}) - * @param context The parse context + * @param input The text to parse. + * @param constraints The constraints under which to parse. + * @param context The parse context. */ - public SkriptParser(String expr, int flags, ParseContext context) { - assert expr != null; - assert (flags & ALL_FLAGS) != 0; - this.expr = "" + expr.trim(); - this.flags = flags; + protected SkriptParser(@NotNull String input, ParsingConstraints constraints, ParseContext context) { + this.input = input.trim(); + this.parsingConstraints = constraints; this.context = context; } - public SkriptParser(SkriptParser other, String expr) { - this(expr, other.flags, other.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; } - public static final String WILDCARD = "[^\"]*?(?:\"[^\"]*?\"[^\"]*?)*?"; + protected SkriptParser(@NotNull SkriptParser other, String input) { + this(input, other.parsingConstraints, other.context); + this.suppressMissingAndOrWarnings = other.suppressMissingAndOrWarnings; + } - public static class ParseResult { - public @Nullable SkriptPattern source; - public Expression[] exprs; - public List regexes = new ArrayList<>(1); - public String expr; - /** - * Defaults to 0. Any marks encountered in the pattern will be XORed with the existing value, in particular if only one mark is encountered this value will be set to that - * mark. - */ - public int mark = 0; - public List tags = new ArrayList<>(); + /** + * 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 = new Iterator<>() { - public ParseResult(SkriptParser parser, String pattern) { - expr = parser.expr; - exprs = new Expression[countUnescaped(pattern, '%') / 2]; - } + private final Iterator iterator = Skript.instance().syntaxRegistry().syntaxes(expectedTypeKey).iterator(); - public ParseResult(String expr, Expression[] expressions) { - this.expr = expr; - this.exprs = expressions; - } + @Override + public boolean hasNext() { + return iterator.hasNext(); + } - public boolean hasTag(String tag) { - return tags.contains(tag); - } - } + @Override + @Contract(" -> new") + public @NotNull SyntaxElementInfo next() { + return SyntaxElementInfo.fromModern(iterator.next()); + } + }; - /** - * Parses a single literal, i.e. not lists of literals. - *

- * Prints errors. - */ - public static @Nullable Literal parseLiteral(String expr, Class expectedClass, ParseContext context) { - expr = "" + expr.trim(); - if (expr.isEmpty()) - return null; - //noinspection ReassignedVariable,unchecked - return new UnparsedLiteral(expr).getConvertedExpression(context, expectedClass); + return SkriptParser.parse( + input, + parsingConstraints, + uncheckedIterator, + context, + defaultError + ); } /** * Parses a string as one of the given syntax elements. *

* Can print an error. - */ - public static @Nullable T parse(String expr, Iterator> source, @Nullable String defaultError) { - expr = "" + expr.trim(); - if (expr.isEmpty()) { + * + * @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()) { - T element = new SkriptParser(expr).parse(source); + E element = new SkriptParser(input, parsingConstraints, context).parse(allowedSyntaxes); if (element != null) { log.printLog(); return element; @@ -186,165 +214,201 @@ public boolean hasTag(String tag) { } } - public static @Nullable T parseStatic(String expr, Iterator> source, @Nullable String defaultError) { - return parseStatic(expr, source, ParseContext.DEFAULT, defaultError); - } - - public static @Nullable T parseStatic(String expr, Iterator> source, ParseContext parseContext, @Nullable String defaultError) { - expr = expr.trim(); - if (expr.isEmpty()) { - Skript.error(defaultError); - return null; - } - - T element; + /** + * 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()) { - element = new SkriptParser(expr, PARSE_LITERALS, parseContext).parse(source); - if (element != null) { - log.printLog(); - return element; + // for each allowed syntax + while (allowedSyntaxes.hasNext()) { + SyntaxElementInfo info = allowedSyntaxes.next(); + // check each of its patterns + for (int patternIndex = 0; patternIndex < info.patterns.length; patternIndex++) { + log.clear(); + E element = parse(info, patternIndex); + // return if this pattern parsed successfully + if (element != null) { + log.printLog(); + return element; + } + } } - log.printError(defaultError); + log.printError(); return null; } } - 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(); + /** + * 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<>() { - if (!checkRestrictedEvents(element, parseResult)) - continue; + private final Iterator iterator = Skript.instance().syntaxRegistry().syntaxes(expectedTypeKey).iterator(); - if (!checkExperimentalSyntax(element)) - continue; + @Override + public boolean hasNext() { + return iterator.hasNext(); + } - 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; - } - } + @Override + @Contract(" -> new") + public @NotNull SyntaxElementInfo next() { + return SyntaxElementInfo.fromModern(iterator.next()); } + }; - // No successful syntax elements parsed, print errors and return - log.printError(); + 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 SyntaxElementInfo info, int patternIndex) { + ParsingStack parsingStack = getParser().getParsingStack(); + ParseResult parseResult; + String pattern = info.getPatterns()[patternIndex]; + 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.getElementClass().getName(); + try { + JavaPlugin providingPlugin = JavaPlugin.getProvidingPlugin(info.getElementClass()); + 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.getElementClass().getDeclaredConstructor().newInstance(); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new RuntimeException("Failed to create instance of " + info.getElementClass().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; } /** - * Checks whether the given element is restricted to specific events, and if so, whether the current event is allowed. - * Prints errors. - * @param element The syntax element to check. - * @param parseResult The parse result for error information. - * @return True if the element is allowed in the current event, false otherwise. - */ - private static boolean checkRestrictedEvents(SyntaxElement element, ParseResult parseResult) { - if (element instanceof EventRestrictedSyntax eventRestrictedSyntax) { - Class[] supportedEvents = eventRestrictedSyntax.supportedEvents(); - if (!getParser().isCurrentEvent(supportedEvents)) { - Skript.error("'" + parseResult.expr + "' can only be used in " + supportedEventsNames(supportedEvents)); - return false; + * 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 true; + return false; } /** - * 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. + * 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(SyntaxElementInfo, int) */ - 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())); + 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 StringUtils.join(names, ", ", " or "); + 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}. @@ -361,7 +425,7 @@ private static boolean checkExperimentalSyntax(T eleme 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; @@ -384,7 +448,7 @@ private static boolean checkExperimentalSyntax(T eleme 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]; @@ -393,7 +457,7 @@ private static boolean checkExperimentalSyntax(T eleme 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); @@ -406,1085 +470,122 @@ private static boolean checkExperimentalSyntax(T eleme return passed; List errors = new ArrayList<>(); - for (Entry> entry : failed.entrySet()) { + for (Map.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); - /** - * Prints errors + * 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 @Nullable Variable parseVariable(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; + 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 Variable.newInstance(variableName, returnTypes); } - return null; + return true; } - private static @Nullable Expression parseExpression(Class[] types, String expr) {; - if (expr.startsWith("“") || expr.startsWith("”") || expr.endsWith("”") || expr.endsWith("“")) { - 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) { - return VariableString.newInstance("" + expr.substring(1, expr.length() - 1)); - } else { - var iterator = new CheckedIterator<>(Skript.instance().syntaxRegistry().syntaxes(SyntaxRegistry.EXPRESSION).iterator(), info -> { - if (info == null || info.returnType() == Object.class) - return true; - for (Class returnType : types) { - assert returnType != null; - if (Converters.converterExists(info.returnType(), returnType)) - return true; - } - return false; - }); - //noinspection unchecked,rawtypes - return (Expression) parse(expr, (Iterator) iterator, null); - } + /** + * 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(); + } - @SuppressWarnings({"unchecked"}) - private @Nullable Expression parseSingleExpr(boolean allowUnparsedLiteral, @Nullable LogEntry error, Class... types) { - assert types.length > 0; - assert types.length == 1 || !CollectionUtils.contains(types, Object.class); - if (expr.isEmpty()) - return null; - if (context != ParseContext.COMMAND && - context != ParseContext.PARSE && - expr.startsWith("(") && expr.endsWith(")") && - next(expr, 0, context) == expr.length()) - return new SkriptParser(this, "" + expr.substring(1, expr.length() - 1)).parseSingleExpr(allowUnparsedLiteral, error, types); - try (ParseLogHandler log = SkriptLogger.startParseLogHandler()) { - if (context == ParseContext.DEFAULT || context == ParseContext.EVENT) { - Variable parsedVariable = parseVariable(expr, types); - if (parsedVariable != null) { - if ((flags & PARSE_EXPRESSIONS) == 0) { - Skript.error("Variables cannot be used here."); - log.printError(); - return null; - } - log.printLog(); - return parsedVariable; - } else if (log.hasError()) { - log.printError(); - return null; - } - FunctionReference functionReference = parseFunction(types); - if (functionReference != null) { - log.printLog(); - //noinspection rawtypes - return new ExprFunctionCall(functionReference); - } else if (log.hasError()) { - log.printError(); - return null; - } - } - log.clear(); - if ((flags & PARSE_EXPRESSIONS) != 0) { - Expression parsedExpression = parseExpression(types, expr); - if (parsedExpression != null) { // Expression/VariableString parsing success - Class parsedReturnType = parsedExpression.getReturnType(); - for (Class type : types) { - if (type.isAssignableFrom(parsedReturnType)) { - log.printLog(); - return (Expression) parsedExpression; - } - } + @Contract("-> this") + public SkriptParser suppressMissingAndOrWarnings() { + suppressMissingAndOrWarnings = true; + return this; + } - // No directly same type found - Class[] objTypes = (Class[]) types; // Java generics... ? - Expression convertedExpression = parsedExpression.getConvertedExpression(objTypes); - if (convertedExpression != null) { - log.printLog(); - return convertedExpression; - } - // Print errors, if we couldn't get the correct type - log.printError(parsedExpression.toString(null, false) + " " + Language.get("is") + " " + notOfType(types), ErrorQuality.NOT_AN_EXPRESSION); - return null; - } - log.clear(); - } - if ((flags & PARSE_LITERALS) == 0) { - log.printError(); - return null; - } - return parseAsLiteral(allowUnparsedLiteral, log, error, types); + public static class ExprInfo { + public ExprInfo(int length) { + classes = new ClassInfo[length]; + isPlural = new boolean[length]; } - } - private static final String INVALID_LSPEC_CHARS = "[^,():/\"'\\[\\]}{]"; - private static final Pattern LITERAL_SPECIFICATION_PATTERN = Pattern.compile("(?" + INVALID_LSPEC_CHARS + "+) \\((?[\\w\\p{L} ]+)\\)"); + public final ClassInfo[] classes; + public final boolean[] isPlural; + public boolean isOptional; + public int flagMask = ~0; + public int time = 0; + } - private @Nullable Expression parseSingleExpr(boolean allowUnparsedLiteral, @Nullable LogEntry error, ExprInfo exprInfo) { - if (expr.isEmpty()) // Empty expressions return nothing, obviously - return null; + /** + * A class that contains information about the parsing of a pattern and what was matched. + */ + public static class ParseResult { + /** + * The pattern that was matched against. + */ + public @Nullable SkriptPattern source; - // Command special parsing - if (context != ParseContext.COMMAND && - context != ParseContext.PARSE && - expr.startsWith("(") && expr.endsWith(")") && - next(expr, 0, context) == expr.length()) - return new SkriptParser(this, "" + expr.substring(1, expr.length() - 1)).parseSingleExpr(allowUnparsedLiteral, error, exprInfo); - try (ParseLogHandler log = SkriptLogger.startParseLogHandler()) { - // Construct types array which contains all potential classes - Class[] types = new Class[exprInfo.classes.length]; // This may contain nulls! - boolean hasSingular = false; - boolean hasPlural = false; + /** + * The parsed sub-expressions that this pattern contains. + * This is an array of {@link Expression} objects, which may include nulls if the expression 'slot' was nullable and not occupied. + */ + public Expression[] exprs; + /** + * Any regexes that were matched in the pattern. + */ + public List regexes = new ArrayList<>(1); + /** + * The raw input string that was parsed. + */ + public String expr; + /** + * Defaults to 0. Any marks encountered in the pattern will be XORed with the existing value, in particular if only one mark is encountered this value will be set to that + * mark. + */ + public int mark = 0; + /** + * Any tags that were encountered in the pattern. + */ + public List tags = new ArrayList<>(); - // Another array for all potential types, but this time without any nulls - // (indexes do not align with other data in ExprInfo) - Class[] nonNullTypes = new Class[exprInfo.classes.length]; + public ParseResult(@NotNull SkriptParser parser, String pattern) { + expr = parser.input; + exprs = new Expression[StringUtils.countUnescaped(pattern, '%') / 2]; + } - int nonNullIndex = 0; - for (int i = 0; i < types.length; i++) { - if ((flags & exprInfo.flagMask) == 0) { // Flag mask invalidates this, skip it - continue; - } + public ParseResult(String expr, Expression[] expressions) { + this.expr = expr; + this.exprs = expressions; + } - // Plural/singular checks - // TODO move them elsewhere, this method needs to be as fast as possible - if (exprInfo.isPlural[i]) - hasPlural = true; - else - hasSingular = true; + /** + * Whether a specific tag was encountered in the parsed pattern. + * @param tag The tag to check for. + * @return Whether the tag was encountered. + */ + public boolean hasTag(String tag) { + return tags.contains(tag); + } - // Actually put class to types[i] - types[i] = exprInfo.classes[i].getC(); - - // Handle nonNullTypes data fill - nonNullTypes[nonNullIndex] = types[i]; - nonNullIndex++; - } - - boolean onlyPlural = !hasSingular && hasPlural; - boolean onlySingular = hasSingular && !hasPlural; - - if (context == ParseContext.DEFAULT || context == ParseContext.EVENT) { - // Attempt to parse variable first - if (onlySingular || onlyPlural) { // No mixed plurals/singulars possible - Variable parsedVariable = parseVariable(expr, nonNullTypes); - if (parsedVariable != null) { // Parsing succeeded, we have a variable - // If variables cannot be used here, it is now allowed - if ((flags & PARSE_EXPRESSIONS) == 0) { - Skript.error("Variables cannot be used here."); - log.printError(); - return null; - } - - // Plural/singular sanity check - if (hasSingular && !parsedVariable.isSingle()) { - Skript.error("'" + expr + "' can only be a single " - + Classes.toString(Stream.of(exprInfo.classes).map(classInfo -> classInfo.getName().toString()).toArray(), false) - + ", not more."); - log.printError(); - return null; - } - - log.printLog(); - return parsedVariable; - } else if (log.hasError()) { - log.printError(); - return null; - } - } else { // Mixed plurals/singulars - Variable parsedVariable = parseVariable(expr, types); - if (parsedVariable != null) { // Parsing succeeded, we have a variable - // If variables cannot be used here, it is now allowed - if ((flags & PARSE_EXPRESSIONS) == 0) { - Skript.error("Variables cannot be used here."); - log.printError(); - return null; - } - - // Plural/singular sanity check - // - // It's (currently?) not possible to detect this at parse time when there are multiple - // acceptable types and only some of them are single, since variables, global especially, - // can hold any possible type, and the type used can only be 100% known at runtime - // - // TODO: - // despite of that, we should probably implement a runtime check for this somewhere - // before executing the syntax element (perhaps even exceptionally with a console warning, - // otherwise users may have some hard time debugging the plurality issues) - currently an - // improper use in a script would result in an exception - if (((exprInfo.classes.length == 1 && !exprInfo.isPlural[0]) || Booleans.contains(exprInfo.isPlural, true)) - && !parsedVariable.isSingle()) { - Skript.error("'" + expr + "' can only be a single " - + Classes.toString(Stream.of(exprInfo.classes).map(classInfo -> classInfo.getName().toString()).toArray(), false) - + ", not more."); - log.printError(); - return null; - } - - log.printLog(); - return parsedVariable; - } else if (log.hasError()) { - log.printError(); - return null; - } - } - - // If it wasn't variable, do same for function call - FunctionReference functionReference = parseFunction(types); - if (functionReference != null) { - - if (onlySingular && !functionReference.isSingle()) { - Skript.error("'" + expr + "' can only be a single " - + Classes.toString(Stream.of(exprInfo.classes).map(classInfo -> classInfo.getName().toString()).toArray(), false) - + ", not more."); - log.printError(); - return null; - } - - log.printLog(); - return new ExprFunctionCall<>(functionReference); - } else if (log.hasError()) { - log.printError(); - return null; - } - } - log.clear(); - if ((flags & PARSE_EXPRESSIONS) != 0) { - Expression parsedExpression = parseExpression(types, expr); - if (parsedExpression != null) { // Expression/VariableString parsing success - Class parsedReturnType = parsedExpression.getReturnType(); - for (int i = 0; i < types.length; i++) { - Class type = types[i]; - if (type == null) // Ignore invalid (null) types - continue; - - // Check return type against the expression's return type - if (type.isAssignableFrom(parsedReturnType)) { - if (!exprInfo.isPlural[i] && !parsedExpression.isSingle()) { // Wrong number of arguments - if (context == ParseContext.COMMAND) { - Skript.error(Commands.m_too_many_arguments.toString(exprInfo.classes[i].getName().getIndefiniteArticle(), exprInfo.classes[i].getName().toString()), ErrorQuality.SEMANTIC_ERROR); - } else { - Skript.error("'" + expr + "' can only be a single " - + Classes.toString(Stream.of(exprInfo.classes).map(classInfo -> classInfo.getName().toString()).toArray(), false) - + ", not more."); - } - log.printError(); - return null; - } - - log.printLog(); - return parsedExpression; - } - } - - if (onlySingular && !parsedExpression.isSingle()) { - Skript.error("'" + expr + "' can only be a single " - + Classes.toString(Stream.of(exprInfo.classes).map(classInfo -> classInfo.getName().toString()).toArray(), false) - + ", not more."); - log.printError(); - return null; - } - - // No directly same type found - Expression convertedExpression = parsedExpression.getConvertedExpression((Class[]) types); - if (convertedExpression != null) { - log.printLog(); - return convertedExpression; - } - - // Print errors, if we couldn't get the correct type - log.printError(parsedExpression.toString(null, false) + " " + Language.get("is") + " " + notOfType(types), ErrorQuality.NOT_AN_EXPRESSION); - return null; - } - log.clear(); - } - if ((flags & PARSE_LITERALS) == 0) { - log.printError(); - return null; - } - return parseAsLiteral(allowUnparsedLiteral, log, error, nonNullTypes); - } - } - - /** - * Helper method for {@link #parseSingleExpr(boolean, LogEntry, Class[])} and {@link #parseSingleExpr(boolean, LogEntry, ExprInfo)}. - * Attempts to parse {@link #expr} as a literal. Prints errors. - * - * @param allowUnparsedLiteral If {@code true}, will allow unparsed literals to be returned. - * @param log The current {@link ParseLogHandler} to use for logging. - * @param error A {@link LogEntry} containing a default error to be printed if failed to parse. - * @param types The valid types to parse the literal as. - * @return {@link Expression} of type {@code T} if successful, otherwise {@code null}.
- * @param The type of the literal to parse.
- */ - @SafeVarargs - private @Nullable Expression parseAsLiteral( - boolean allowUnparsedLiteral, - ParseLogHandler log, - @Nullable LogEntry error, - Class... types - ) { - if (expr.endsWith(")") && expr.contains("(")) { - Matcher classInfoMatcher = LITERAL_SPECIFICATION_PATTERN.matcher(expr); - if (classInfoMatcher.matches()) { - String literalString = classInfoMatcher.group("literal"); - String unparsedClassInfo = Noun.stripDefiniteArticle(classInfoMatcher.group("classinfo")); - Expression result = parseSpecifiedLiteral(literalString, unparsedClassInfo, types); - if (result != null) { - log.printLog(); - return result; - } - } - } - if (types.length == 1 && types[0] == Object.class) { - if (!allowUnparsedLiteral) { - log.printError(); - return null; - } - //noinspection unchecked - return (Expression) getUnparsedLiteral(log, error); - } - 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(expr, type, context); - if (parsedObject != null) { - log.printLog(); - return new SimpleLiteral<>(parsedObject, false, new UnparsedLiteral(expr)); - } - } - if (allowUnparsedLiteral && containsObjectClass) - //noinspection unchecked - return (Expression) getUnparsedLiteral(log, error); - if (expr.startsWith("\"") && expr.endsWith("\"") && expr.length() > 1) { - for (Class type : types) { - if (!type.isAssignableFrom(String.class)) - continue; - VariableString string = VariableString.newInstance(expr.substring(1, expr.length() - 1)); - if (string instanceof LiteralString) - //noinspection unchecked - return (Expression) string; - break; - } - } - log.printError(); - return null; - } - - /** - * If {@link #expr} 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(expr, Object.class, context) == null) { - log.printError(); - return null; - } - log.clear(); - LogEntry logError = log.getError(); - return new UnparsedLiteral(expr, 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 - * @param types An {@link Array} of the acceptable {@link Class}es - * @return {@link SimpleLiteral} or {@code null} if any checks fail - */ - @SafeVarargs - private @Nullable Expression parseSpecifiedLiteral( - String literalString, - String unparsedClassInfo, - Class ... types - ) { - 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(), types)) { - Skript.error(expr + " " + Language.get("is") + " " + notOfType(types)); - 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; - } - - /** - * 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); - - 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'"; - - private boolean suppressMissingAndOrWarnings = SkriptConfig.disableMissingAndOrWarnings.value(); - - private SkriptParser suppressMissingAndOrWarnings() { - suppressMissingAndOrWarnings = true; - return this; - } - - @SafeVarargs - public final @Nullable Expression parseExpression(Class... types) { - if (expr.isEmpty()) { - return null; - } - - assert types.length > 0; - assert types.length == 1 || !CollectionUtils.contains(types, Object.class); - - try (ParseLogHandler log = SkriptLogger.startParseLogHandler()) { - Expression parsedExpression = parseSingleExpr(true, null, types); - if (parsedExpression != null) { - log.printLog(); - return parsedExpression; - } - log.clear(); - - return parseExpressionList(log, types); - } - } - - public @Nullable Expression parseExpression(ExprInfo exprInfo) { - if (expr.isEmpty()) { - return null; - } - - try (ParseLogHandler log = SkriptLogger.startParseLogHandler()) { - Expression parsedExpression = parseSingleExpr(true, null, exprInfo); - if (parsedExpression != null) { - log.printLog(); - return parsedExpression; - } - log.clear(); - - return parseExpressionList(log, exprInfo); - } - } - - /* - * 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) { - OrderedExprInfo orderedExprInfo = data instanceof OrderedExprInfo info ? info : null; - ExprInfo exprInfo = data instanceof ExprInfo info ? info : null; - Class[] types = orderedExprInfo == null && exprInfo == null ? (Class[]) data : null; - boolean isObject; - if (orderedExprInfo != null) { - isObject = orderedExprInfo.infos.length == 1 && orderedExprInfo.infos[0].classes[0].getC() == Object.class; - } else if (exprInfo != null) { - isObject = exprInfo.classes.length == 1 && exprInfo.classes[0].getC() == Object.class; - } else { - isObject = types.length == 1 && types[0] == Object.class; - } - - List pieces = new ArrayList<>(); - Matcher matcher = LIST_SPLIT_PATTERN.matcher(expr); - int currentPosition = 0; - int lastPosition = currentPosition; - while (currentPosition >= 0 && currentPosition <= expr.length()) { - if (currentPosition == expr.length() || matcher.region(currentPosition, expr.length()).lookingAt()) { - pieces.add(new int[]{lastPosition, currentPosition}); - if (currentPosition == expr.length()) { - break; - } - currentPosition = matcher.end(); - lastPosition = currentPosition; - } - currentPosition = next(expr, currentPosition, context); - } - if (currentPosition != expr.length()) { - assert currentPosition == -1 && context != ParseContext.COMMAND && context != ParseContext.PARSE : currentPosition + "; " + expr; - log.printError("Invalid brackets/variables/text in '" + expr + "'", 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 (expr.startsWith("(") && expr.endsWith(")") && next(expr, 0, context) == expr.length()) { - log.clear(); - // parse again without parentheses - SkriptParser parser = new SkriptParser(this, expr.substring(1, expr.length() - 1)); - if (exprInfo != null) { - return parser.parseExpression(exprInfo); - } else { - return parser.parseExpression(types); - } - } - if (isObject && (flags & PARSE_LITERALS) != 0) { // single expression, can return an UnparsedLiteral now - log.clear(); - return new UnparsedLiteral(expr, log.getError()); - } - log.printError(); - return null; - } - - // early check whether this can be parsed as an 'or' list - // if it cannot, and the output is expected to be single, we can return early - if (exprInfo != null && !Booleans.contains(exprInfo.isPlural, true) && !OR_PATTERN.matcher(expr).find()) { - log.printError(); - return null; - } - - List> parsedExpressions = new ArrayList<>(); - boolean isLiteralList = true; - Kleenean and = Kleenean.UNKNOWN; - // given "a, b, c" try "a, ab, ac" when starting with "a" - outer: for (int first = 0; first < pieces.size(); ) { - for (int last = first; last < pieces.size(); last++) { - if (first == 0 && last == pieces.size() - 1) { // this is the whole expression, which would have already been tried - continue; - } - - int start = pieces.get(first)[0]; - int end = pieces.get(last)[1]; - String subExpr = expr.substring(start, end); - - // allow parsing as a list only if subExpr is wrapped with parentheses - SkriptParser parser = new SkriptParser(this, subExpr); - Expression parsedExpression; - if (subExpr.startsWith("(") && subExpr.endsWith(")") && next(subExpr, 0, context) == subExpr.length()) { - if (orderedExprInfo != null) { - int infoIndex = parsedExpressions.size(); - if (infoIndex >= orderedExprInfo.infos.length) { - log.printError(); - return null; - } - parsedExpression = parser.parseExpression(orderedExprInfo.infos[infoIndex]); - } else if (exprInfo != null) { - parsedExpression = parser.parseExpression(exprInfo); - } else { - parsedExpression = parser.parseExpression(types); - } - } else { - if (orderedExprInfo != null) { - int infoIndex = parsedExpressions.size(); - if (infoIndex >= orderedExprInfo.infos.length) { - log.printError(); - return null; - } - parsedExpression = parser.parseSingleExpr(last == first, log.getError(), orderedExprInfo.infos[infoIndex]); - } else if (exprInfo != null) { - parsedExpression = parser.parseSingleExpr(last == first, log.getError(), exprInfo); - } else { - parsedExpression = parser.parseSingleExpr(last == first, log.getError(), types); - } - } - - if (parsedExpression == null) { // try again with expanded subExpr - continue; - } - - isLiteralList &= parsedExpression instanceof Literal; - parsedExpressions.add(parsedExpression); - if (first != 0) { - String delimiter = expr.substring(pieces.get(first - 1)[1], start).trim().toLowerCase(Locale.ENGLISH); - if (!delimiter.equals(",")) { - boolean or = !delimiter.endsWith("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: " + expr); - and = Kleenean.TRUE; - } - } - } - - first = last + 1; - continue outer; - } - // could not parse successfully with the piece starting from "first" - log.printError(); - return null; - } - - // determine return types - Class[] returnTypes; - Class superReturnType; - if (parsedExpressions.size() == 1) { - returnTypes = null; - superReturnType = parsedExpressions.get(0).getReturnType(); - } else { - returnTypes = new Class[parsedExpressions.size()]; - for (int i = 0; i < parsedExpressions.size(); i++) { - returnTypes[i] = parsedExpressions.get(i).getReturnType(); - } - superReturnType = Classes.getSuperClassInfo(returnTypes).getC(); - } - - // this could be an 'and' list, and the expected list should be an 'or' list - if (exprInfo != null && !and.isFalse()) { - boolean canBePlural = false; - - // quick check for direct super type match - for (int typeIndex = 0; typeIndex < exprInfo.classes.length; typeIndex++) { - if (exprInfo.isPlural[typeIndex] && exprInfo.classes[typeIndex].getC().isAssignableFrom(superReturnType)) { - canBePlural = true; - break; - } - } - - // long check against return types for each expression - if (!canBePlural) { - for (var parsedExpression : parsedExpressions) { // ensure each expression is of a plural type - canBePlural = false; // reset for each iteration - for (int typeIndex = 0; typeIndex < exprInfo.classes.length; typeIndex++) { - if (!exprInfo.isPlural[typeIndex]) { - continue; - } - if (parsedExpression.canReturn(exprInfo.classes[typeIndex].getC())) { - canBePlural = true; - break; - } - } - if (!canBePlural) { // expression could not return a plural type - break; - } - } - } - - if (!canBePlural) { - // List cannot be used in place of a single value here - log.printError(); - return null; - } - } - - if (returnTypes == null) { // only parsed one expression out of the pieces - return parsedExpressions.get(0); - } - - log.printLog(false); - - if (and.isUnknown() && !suppressMissingAndOrWarnings) { - ParserInstance parser = getParser(); - if (parser.isActive() && !parser.getCurrentScript().suppressesWarning(ScriptWarning.MISSING_CONJUNCTION)) { - Skript.warning(MISSING_AND_OR + ": " + expr); - } - } - - if (isLiteralList) { - //noinspection SuspiciousToArrayCall - Literal[] literals = parsedExpressions.toArray(new Literal[0]); - //noinspection unchecked, rawtypes - return new LiteralList(literals, superReturnType, returnTypes, !and.isFalse()); - } else { - Expression[] expressions = parsedExpressions.toArray(new Expression[0]); - //noinspection unchecked, rawtypes - return new ExpressionList(expressions, superReturnType, returnTypes, !and.isFalse()); - } - } - - /* - * Function parsing - */ - - private final static Pattern FUNCTION_CALL_PATTERN = Pattern.compile("(" + Functions.functionNamePattern + ")\\((.*)\\)"); - - /** - * @param types The required return type or null if it is not used (e.g. when calling a void function) - * @return The parsed function, or null if the given expression is not a function call or is an invalid function call (check for an error to differentiate these two) - */ - @SuppressWarnings("unchecked") - public @Nullable FunctionReference parseFunction(@Nullable Class... types) { - if (context != ParseContext.DEFAULT && context != ParseContext.EVENT) - return null; - try (ParseLogHandler log = SkriptLogger.startParseLogHandler()) { - Matcher matcher = FUNCTION_CALL_PATTERN.matcher(expr); - if (!matcher.matches()) { - log.printLog(); - return null; - } - - String functionName = matcher.group(1); - String args = matcher.group(2); - - // 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 = next(args, i, context)) { - if (i == -1) { - log.printLog(); - return null; - } - } - - if ((flags & PARSE_EXPRESSIONS) == 0) { - Skript.error("Functions cannot be used here (or there is a problem with your arguments)."); - log.printError(); - return null; - } - - SkriptParser skriptParser = new SkriptParser(args, flags | PARSE_LITERALS, context) - .suppressMissingAndOrWarnings(); - Expression[] params = args.isEmpty() ? new Expression[0] : null; - - String namespace = null; - ParserInstance parser = getParser(); - if (parser.isActive()) { - namespace = parser.getCurrentScript().getConfig().getFileName(); - } - - if (params == null) { // there are arguments to parse - // determine signatures that could match - var signatures = FunctionRegistry.getRegistry().getSignatures(namespace, functionName).stream() - .filter(signature -> { - if (signature.getMaxParameters() == 0) { // we have arguments, but this function doesn't - return false; - } - if (types != null) { // filter signatures based on expected return type - if (signature.getReturnType() == null) { - return false; - } - Class signatureType = signature.getReturnType().getC(); - for (Class type : types) { - //noinspection DataFlowIssue - individual elements won't be null - if (Converters.converterExists(signatureType, type)) { - return true; - } - } - return false; - } - return true; - }) - .toList(); - - // here, we map all signatures into type/plurality collections - // for example, all possible types (and whether they are plural) for the first parameter - // will be mapped into the 0-index of both collections - record SignatureData(ClassInfo classInfo, boolean plural) { } - List> signatureDatas = new ArrayList<>(); - boolean trySingle = false; - boolean trySinglePlural = false; - for (var signature : signatures) { - trySingle |= signature.getMinParameters() == 1 || signature.getMaxParameters() == 1; - trySinglePlural |= trySingle && !signature.getParameter(0).isSingleValue(); - for (int i = 0; i < signature.getMaxParameters(); i++) { - if (signatureDatas.size() <= i) { - signatureDatas.add(new ArrayList<>()); - } - var parameter = signature.getParameter(i); - signatureDatas.get(i).add(new SignatureData(parameter.getType(), !parameter.isSingleValue())); - } - } - ExprInfo[] signatureInfos = new ExprInfo[signatureDatas.size()]; - for (int infoIndex = 0; infoIndex < signatureInfos.length; infoIndex++) { - List datas = signatureDatas.get(infoIndex); - ClassInfo[] infos = new ClassInfo[datas.size()]; - boolean[] isPlural = new boolean[infos.length]; - for (int dataIndex = 0; dataIndex < infos.length; dataIndex++) { - SignatureData data = datas.get(dataIndex); - infos[dataIndex] = data.classInfo; - isPlural[dataIndex] = data.plural; - } - signatureInfos[infoIndex] = new ExprInfo(infos, isPlural); - } - OrderedExprInfo orderedExprInfo = new OrderedExprInfo(signatureInfos); - - if (trySingle) { - params = this.getFunctionArguments( - () -> skriptParser.parseSingleExpr(true, null, orderedExprInfo.infos[0]), - args); - if (params == null && trySinglePlural) { - log.clear(); - log.clearError(); - try (ParseLogHandler listLog = SkriptLogger.startParseLogHandler()) { - params = this.getFunctionArguments( - () -> skriptParser.parseExpressionList(listLog, orderedExprInfo.infos[0]), - args); - } - } - } - if (params == null) { - log.clear(); - log.clearError(); - try (ParseLogHandler listLog = SkriptLogger.startParseLogHandler()) { - params = this.getFunctionArguments( - () -> skriptParser.parseExpressionList(listLog, orderedExprInfo), - args); - } - } - if (params == null) { - log.printError(); - return null; - } - } - - FunctionReference functionReference = new FunctionReference<>(functionName, SkriptLogger.getNode(), namespace, types, params); - if (!functionReference.validateFunction(true)) { - log.printError(); - return null; - } - log.printLog(); - return functionReference; - } - } - - private Expression @Nullable [] getFunctionArguments(Supplier> parsing, String args) { - if (args.isEmpty()) { - return new Expression[0]; - } - - Expression parsedExpression = parsing.get(); - if (parsedExpression == null) { - return null; - } - - Expression[] params; - if (parsedExpression instanceof 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) parsedExpression).getExpressions(); - } else { - params = new Expression[] {parsedExpression}; - } - - return params; - } - - /* - * Command parsing - */ - - /** - * Prints parse errors (i.e. must start a ParseLog before calling this method) - */ - public static boolean parseArguments(String args, ScriptCommand command, ScriptCommandEvent event) { - SkriptParser parser = new SkriptParser(args, PARSE_LITERALS, ParseContext.COMMAND); - ParseResult parseResult = parser.parse_i(command.getPattern()); - if (parseResult == null) - return false; - - List> arguments = command.getArguments(); - assert arguments.size() == parseResult.exprs.length; - for (int i = 0; i < parseResult.exprs.length; i++) { - if (parseResult.exprs[i] == null) - arguments.get(i).setToDefault(event); - else - arguments.get(i).set(event, parseResult.exprs[i].getArray(event)); - } - return true; - } - - /* - * Utility methods - */ - - /** - * Parses the text as the given pattern as {@link ParseContext#COMMAND}. - *

- * Prints parse errors (i.e. must start a ParseLog before calling this method) - */ - public static @Nullable ParseResult parse(String text, String pattern) { - return new SkriptParser(text, PARSE_LITERALS, ParseContext.COMMAND).parse_i(pattern); - } - - /** - * Parses the text as the given pattern with the given parse context and parse flags. - *

- * Prints parse errors (i.e. must start a ParseLog before calling this method) - */ - public static @Nullable ParseResult parse(String text, String pattern, int parseFlags, ParseContext parseContext) { - return new SkriptParser(text, parseFlags, parseContext).parse_i(pattern); - } - - /** - * Parses the text as the given pattern with the given parse context and parse flags. - *

- * Prints parse errors (i.e. must start a ParseLog before calling this method) - */ - public static @Nullable ParseResult parse(String text, SkriptPattern pattern, int parseFlags, ParseContext parseContext) { - return parse(text, pattern.toString(), parseFlags, parseContext); - } - - /** - * Finds the closing bracket of the group at start (i.e. start has to be in a group). - * - * @param pattern The string to search in - * @param closingBracket The bracket to look for, e.g. ')' - * @param openingBracket A bracket that opens another group, e.g. '(' - * @param start This must not be the index of the opening bracket! - * @param isGroup Whether start is assumed to be in a group (will print an error if this is not the case, otherwise it returns pattern.length()) - * @return The index of the next bracket - * @throws MalformedPatternException If the group is not closed - */ - public static int nextBracket(String pattern, char closingBracket, char openingBracket, int start, boolean isGroup) throws MalformedPatternException { - int index = 0; - for (int i = start; i < pattern.length(); i++) { - if (pattern.charAt(i) == '\\') { - i++; - } else if (pattern.charAt(i) == closingBracket) { - if (index == 0) { - if (!isGroup) - throw new MalformedPatternException(pattern, "Unexpected closing bracket '" + closingBracket + "'"); - return i; - } - index--; - } else if (pattern.charAt(i) == openingBracket) { - index++; - } - } - if (isGroup) - throw new MalformedPatternException(pattern, "Missing closing bracket '" + closingBracket + "'"); - 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 @@ -1540,262 +641,4 @@ public static String notOfType(ClassInfo... types) { } } - /** - * Returns the next character in the expression, skipping strings, - * variables and parentheses - * (unless {@code context} is {@link ParseContext#COMMAND} or {@link ParseContext#PARSE}). - * - * @param expr The expression to traverse. - * @param startIndex The index to start at. - * @return The next index (can be expr.length()), or -1 if - * an invalid string, variable or bracket is found - * or if {@code startIndex >= expr.length()}. - * @throws StringIndexOutOfBoundsException if {@code startIndex < 0}. - */ - public static int next(String expr, int startIndex, ParseContext context) { - if (startIndex < 0) - throw new StringIndexOutOfBoundsException(startIndex); - - int exprLength = expr.length(); - if (startIndex >= exprLength) - return -1; - - if (context == ParseContext.COMMAND || context == ParseContext.PARSE) - return startIndex + 1; - - int index; - switch (expr.charAt(startIndex)) { - case '"': - index = nextQuote(expr, startIndex + 1); - return index < 0 ? -1 : index + 1; - case '{': - index = VariableString.nextVariableBracket(expr, startIndex + 1); - return index < 0 ? -1 : index + 1; - case '(': - for (index = startIndex + 1; index >= 0 && index < exprLength; index = next(expr, index, context)) { - if (expr.charAt(index) == ')') - return index + 1; - } - return -1; - default: - return startIndex + 1; - } - } - - /** - * Returns the next occurrence of the needle in the haystack. - * Similar to {@link #next(String, int, ParseContext)}, this method skips - * strings, variables and parentheses (unless context is {@link ParseContext#COMMAND} - * or {@link ParseContext#PARSE}). - * - * @param haystack The string to search in. - * @param needle The string to search for. - * @param startIndex The index to start in within the haystack. - * @param caseSensitive Whether this search will be case-sensitive. - * @return The next index representing the first character of the needle. - * May return -1 if an invalid string, variable or bracket is found or if startIndex >= hatsack.length(). - * @throws StringIndexOutOfBoundsException if startIndex < 0. - */ - public static int nextOccurrence(String haystack, String needle, int startIndex, ParseContext parseContext, boolean caseSensitive) { - if (startIndex < 0) - throw new StringIndexOutOfBoundsException(startIndex); - if (parseContext == ParseContext.COMMAND || parseContext == ParseContext.PARSE) - return haystack.indexOf(needle, startIndex); - - int haystackLength = haystack.length(); - if (startIndex >= haystackLength) - return -1; - - int needleLength = needle.length(); - - char firstChar = needle.charAt(0); - boolean startsWithSpecialChar = firstChar == '"' || firstChar == '{' || firstChar == '('; - - while (startIndex < haystackLength) { - - char character = haystack.charAt(startIndex); - - if ( // Early check before special character handling - startsWithSpecialChar && - haystack.regionMatches(!caseSensitive, startIndex, needle, 0, needleLength) - ) { - return startIndex; - } - - switch (character) { - case '"': - startIndex = nextQuote(haystack, startIndex + 1); - if (startIndex < 0) - return -1; - break; - case '{': - startIndex = VariableString.nextVariableBracket(haystack, startIndex + 1); - if (startIndex < 0) - return -1; - break; - case '(': - startIndex = next(haystack, startIndex, parseContext); // Use other function to skip to right after closing parentheses - if (startIndex < 0) - return -1; - break; - } - - if (haystack.regionMatches(!caseSensitive, startIndex, needle, 0, needleLength)) - return startIndex; - - startIndex++; - } - - return -1; - } - - private static final Map patterns = new ConcurrentHashMap<>(); - - private @Nullable ParseResult parse_i(String pattern) { - SkriptPattern skriptPattern = patterns.computeIfAbsent(pattern, PatternCompiler::compile); - ch.njol.skript.patterns.MatchResult matchResult = skriptPattern.match(expr, flags, context); - if (matchResult == null) - return null; - return matchResult.toParseResult(); - } - - /** - * Validates a user-defined pattern (used in {@link ExprParse}). - * - * @param pattern The pattern string to validate - * @return The pattern with %codenames% and a boolean array that contains whether the expressions are plural or not - */ - public static @Nullable NonNullPair, Boolean>[]> validatePattern(String pattern) { - List, Boolean>> pairs = new ArrayList<>(); - int groupLevel = 0, optionalLevel = 0; - Deque groups = new LinkedList<>(); - StringBuilder stringBuilder = new StringBuilder(pattern.length()); - int last = 0; - for (int i = 0; i < pattern.length(); i++) { - char character = pattern.charAt(i); - if (character == '(') { - groupLevel++; - groups.addLast(character); - } else if (character == '|') { - if (groupLevel == 0 || groups.peekLast() != '(' && groups.peekLast() != '|') - return error("Cannot use the pipe character '|' outside of groups. Escape it if you want to match a literal pipe: '\\|'"); - groups.removeLast(); - groups.addLast(character); - } else if (character == ')') { - if (groupLevel == 0 || groups.peekLast() != '(' && groups.peekLast() != '|') - return error("Unexpected closing group bracket ')'. Escape it if you want to match a literal bracket: '\\)'"); - if (groups.peekLast() == '(') - return error("(...|...) groups have to contain at least one pipe character '|' to separate it into parts. Escape the brackets if you want to match literal brackets: \"\\(not a group\\)\""); - groupLevel--; - groups.removeLast(); - } else if (character == '[') { - optionalLevel++; - groups.addLast(character); - } else if (character == ']') { - if (optionalLevel == 0 || groups.peekLast() != '[') - return error("Unexpected closing optional bracket ']'. Escape it if you want to match a literal bracket: '\\]'"); - optionalLevel--; - groups.removeLast(); - } else if (character == '<') { - int j = pattern.indexOf('>', i + 1); - if (j == -1) - return error("Missing closing regex bracket '>'. Escape the '<' if you want to match a literal bracket: '\\<'"); - try { - Pattern.compile(pattern.substring(i + 1, j)); - } catch (PatternSyntaxException e) { - return error("Invalid Regular Expression '" + pattern.substring(i + 1, j) + "': " + e.getLocalizedMessage()); - } - i = j; - } else if (character == '>') { - return error("Unexpected closing regex bracket '>'. Escape it if you want to match a literal bracket: '\\>'"); - } else if (character == '%') { - int j = pattern.indexOf('%', i + 1); - if (j == -1) - return error("Missing end sign '%' of expression. Escape the percent sign to match a literal '%': '\\%'"); - NonNullPair pair = Utils.getEnglishPlural("" + pattern.substring(i + 1, j)); - ClassInfo classInfo = Classes.getClassInfoFromUserInput(pair.getFirst()); - if (classInfo == null) - return error("The type '" + pair.getFirst() + "' could not be found. Please check your spelling or escape the percent signs if you want to match literal %s: \"\\%not an expression\\%\""); - pairs.add(new NonNullPair<>(classInfo, pair.getSecond())); - stringBuilder.append(pattern, last, i + 1); - stringBuilder.append(Utils.toEnglishPlural(classInfo.getCodeName(), pair.getSecond())); - last = j; - i = j; - } else if (character == '\\') { - if (i == pattern.length() - 1) - return error("Pattern must not end in an unescaped backslash. Add another backslash to escape it, or remove it altogether."); - i++; - } - } - stringBuilder.append(pattern.substring(last)); - //noinspection unchecked - return new NonNullPair<>(stringBuilder.toString(), pairs.toArray(new NonNullPair[0])); - } - - private static @Nullable NonNullPair, Boolean>[]> error(final String error) { - Skript.error("Invalid pattern: " + error); - return null; - } - - private final static Message M_QUOTES_ERROR = new Message("skript.quotes error"); - private final static Message M_BRACKETS_ERROR = new Message("skript.brackets error"); - - public static boolean validateLine(String line) { - if (StringUtils.count(line, '"') % 2 != 0) { - Skript.error(M_QUOTES_ERROR.toString()); - return false; - } - for (int i = 0; i < line.length(); i = next(line, i, ParseContext.DEFAULT)) { - if (i == -1) { - Skript.error(M_BRACKETS_ERROR.toString()); - return false; - } - } - return true; - } - - public static class ExprInfo { - public ExprInfo(int length) { - this(new ClassInfo[length], new boolean[length]); - } - - public ExprInfo(ClassInfo[] classes, boolean[] isPlural) { - Preconditions.checkState(classes.length == isPlural.length, "classes and isPlural must be the same length"); - this.classes = classes; - this.isPlural = isPlural; - } - - public final ClassInfo[] classes; - public final boolean[] isPlural; - public boolean isOptional; - public int flagMask = ~0; - public int time = 0; - } - - /** - * @see ParserInstance#get() - */ - private static ParserInstance getParser() { - return ParserInstance.get(); - } - - // register default value data when the parser class is loaded. - static { - ParserInstance.registerData(DefaultValueData.class, DefaultValueData::new); - } - - /** - * @deprecated due to bad naming conventions, - * use {@link #LIST_SPLIT_PATTERN} instead. - */ - @Deprecated(since = "2.7.0", forRemoval = true) - public final static Pattern listSplitPattern = LIST_SPLIT_PATTERN; - - /** - * @deprecated due to bad naming conventions, - * use {@link #WILDCARD} instead. - */ - @Deprecated(since = "2.8.0", forRemoval = true) - public final static String wildcard = WILDCARD; - } 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/skript/lang/function/Functions.java b/src/main/java/ch/njol/skript/lang/function/Functions.java index 624a6f0e878..6db2ab67acb 100644 --- a/src/main/java/ch/njol/skript/lang/function/Functions.java +++ b/src/main/java/ch/njol/skript/lang/function/Functions.java @@ -7,6 +7,9 @@ import ch.njol.skript.config.SectionNode; import ch.njol.skript.lang.function.FunctionRegistry.Retrieval; import ch.njol.skript.lang.function.FunctionRegistry.RetrievalResult; +import ch.njol.skript.lang.ParseContext; +import ch.njol.skript.lang.ParsingConstraints; +import ch.njol.skript.lang.parser.FunctionParser; import ch.njol.skript.registrations.Classes; import ch.njol.skript.util.Utils; import ch.njol.util.NonNullPair; @@ -444,7 +447,14 @@ public static void enableFunctionEvents(SkriptAddon addon) { if (addon == null) { throw new SkriptAPIException("enabling function events requires addon instance"); } + } - callFunctionEvents = true; + @SafeVarargs + public static FunctionReference parse(String input, Class... types) { + return new FunctionParser( + input, + ParsingConstraints.all().constrainReturnTypes(types), + ParseContext.DEFAULT) + .parse(); } } diff --git a/src/main/java/ch/njol/skript/lang/parser/ExpressionParser.java b/src/main/java/ch/njol/skript/lang/parser/ExpressionParser.java new file mode 100644 index 00000000000..4fc5999b841 --- /dev/null +++ b/src/main/java/ch/njol/skript/lang/parser/ExpressionParser.java @@ -0,0 +1,526 @@ +package ch.njol.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.Kleenean; +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.lang.script.Script; +import org.skriptlang.skript.lang.script.ScriptWarning; +import org.skriptlang.skript.registration.SyntaxRegistry; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +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(")") + && StringUtils.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()); + } + } + + /** + * 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/ch/njol/skript/lang/parser/FunctionParser.java b/src/main/java/ch/njol/skript/lang/parser/FunctionParser.java new file mode 100644 index 00000000000..82bba8f1acc --- /dev/null +++ b/src/main/java/ch/njol/skript/lang/parser/FunctionParser.java @@ -0,0 +1,133 @@ +package ch.njol.skript.lang.parser; + +import ch.njol.skript.Skript; +import ch.njol.skript.lang.*; +import ch.njol.skript.lang.function.FunctionReference; +import ch.njol.skript.lang.function.Functions; +import ch.njol.skript.log.ParseLogHandler; +import ch.njol.skript.log.SkriptLogger; +import ch.njol.util.StringUtils; +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 = StringUtils.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/ch/njol/util/StringUtils.java b/src/main/java/ch/njol/util/StringUtils.java index c68d344636d..a47d19a99b7 100644 --- a/src/main/java/ch/njol/util/StringUtils.java +++ b/src/main/java/ch/njol/util/StringUtils.java @@ -1,5 +1,7 @@ package ch.njol.util; +import ch.njol.skript.lang.ParseContext; +import ch.njol.skript.lang.VariableString; import org.jetbrains.annotations.Nullable; import java.util.Iterator; @@ -490,4 +492,104 @@ 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; + } + + /** + * Returns the next character in the expression, skipping strings, + * variables and parentheses + * (unless {@code context} is {@link ParseContext#COMMAND} or {@link ParseContext#PARSE}). + * + * @param expr The expression to traverse. + * @param startIndex The index to start at. + * @return The next index (can be expr.length()), or -1 if + * an invalid string, variable or bracket is found + * or if {@code startIndex >= expr.length()}. + * @throws StringIndexOutOfBoundsException if {@code startIndex < 0}. + */ + public static int next(String expr, int startIndex, ParseContext context) { + if (startIndex < 0) + throw new StringIndexOutOfBoundsException(startIndex); + + int exprLength = expr.length(); + if (startIndex >= exprLength) + return -1; + + if (context == ParseContext.COMMAND || context == ParseContext.PARSE) + return startIndex + 1; + + int index; + switch (expr.charAt(startIndex)) { + case '"': + index = nextQuote(expr, startIndex + 1); + return index < 0 ? -1 : index + 1; + case '{': + index = VariableString.nextVariableBracket(expr, startIndex + 1); + return index < 0 ? -1 : index + 1; + case '(': + for (index = startIndex + 1; index >= 0 && index < exprLength; index = next(expr, index, context)) { + if (expr.charAt(index) == ')') + return index + 1; + } + return -1; + default: + return startIndex + 1; + } + } + }