Skip to content

Commit 23a00b2

Browse files
Match Any Default Expression (#8089)
1 parent 334ced2 commit 23a00b2

File tree

5 files changed

+751
-23
lines changed

5 files changed

+751
-23
lines changed
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
package ch.njol.skript.lang;
2+
3+
import ch.njol.skript.classes.ClassInfo;
4+
import ch.njol.skript.lang.SkriptParser.ExprInfo;
5+
import ch.njol.util.StringUtils;
6+
import org.jetbrains.annotations.Nullable;
7+
8+
import java.util.List;
9+
10+
/**
11+
* Utility class for {@link DefaultExpression}.
12+
*/
13+
final class DefaultExpressionUtils {
14+
15+
/**
16+
* Check if {@code expr} is valid with the settings from {@code exprInfo}.
17+
*
18+
* @param expr The {@link DefaultExpression} to check.
19+
* @param exprInfo The {@link ExprInfo} to check {@code expr} against its settings.
20+
* @param index The index of the {@link ClassInfo} in {@code exprInfo} used to grab {@code expr}.
21+
* @return {@link DefaultExpressionError} if it's not valid, otherwise {@code null}.
22+
*/
23+
static @Nullable DefaultExpressionError isValid(DefaultExpression<?> expr, ExprInfo exprInfo, int index) {
24+
if (expr == null) {
25+
return DefaultExpressionError.NOT_FOUND;
26+
} else if (!(expr instanceof Literal<?>) && (exprInfo.flagMask & SkriptParser.PARSE_EXPRESSIONS) == 0) {
27+
return DefaultExpressionError.NOT_LITERAL;
28+
} else if (expr instanceof Literal<?> && (exprInfo.flagMask & SkriptParser.PARSE_LITERALS) == 0) {
29+
return DefaultExpressionError.LITERAL;
30+
} else if (!exprInfo.isPlural[index] && !expr.isSingle()) {
31+
return DefaultExpressionError.NOT_SINGLE;
32+
} else if (exprInfo.time != 0 && !expr.setTime(exprInfo.time)) {
33+
return DefaultExpressionError.TIME_STATE;
34+
}
35+
return null;
36+
}
37+
38+
enum DefaultExpressionError {
39+
/**
40+
* Error type for when a {@link DefaultExpression} can not be found for a {@link Class}.
41+
*/
42+
NOT_FOUND {
43+
@Override
44+
public String getError(List<String> codeNames, String pattern) {
45+
StringBuilder builder = new StringBuilder();
46+
String combinedComma = getCombinedComma(codeNames);
47+
String combinedSlash = StringUtils.join(codeNames, "/");
48+
builder.append(plurality(codeNames, "The class '", "The classes '"));
49+
builder.append(combinedComma)
50+
.append("'")
51+
.append(plurality(codeNames, " does ", " do "))
52+
.append("not provide a default expression. Either allow null (with %-")
53+
.append(combinedSlash)
54+
.append("%) or make it mandatory [pattern: ")
55+
.append(pattern)
56+
.append("]");
57+
return builder.toString();
58+
}
59+
},
60+
61+
/**
62+
* Error type for when the {@link DefaultExpression} for a {@link Class} is not a {@link Literal}
63+
* and the pattern only accepts {@link Literal}s.
64+
*/
65+
NOT_LITERAL {
66+
@Override
67+
public String getError(List<String> codeNames, String pattern) {
68+
StringBuilder builder = new StringBuilder();
69+
builder.append(defaultExpression(codeNames, " is not a literal. ", " are not literals. "))
70+
.append("Either allow null (with %-*")
71+
.append(StringUtils.join(codeNames, "/"))
72+
.append("%) or make it mandatory [pattern: ")
73+
.append(pattern)
74+
.append("]");
75+
return builder.toString();
76+
}
77+
},
78+
79+
/**
80+
* Error type for when the {@link DefaultExpression} for a {@link Class} is a {@link Literal}
81+
* and the pattern does not accept {@link Literal}s.
82+
*/
83+
LITERAL {
84+
@Override
85+
public String getError(List<String> codeNames, String pattern) {
86+
StringBuilder builder = new StringBuilder();
87+
builder.append(defaultExpression(codeNames, " is a literal. ", " are literals. "))
88+
.append("Either allow null (with %-~")
89+
.append(StringUtils.join(codeNames, "/"))
90+
.append("%) or make it mandatory [pattern: ")
91+
.append(pattern)
92+
.append("]");
93+
return builder.toString();
94+
}
95+
},
96+
97+
/**
98+
* Error type for when the {@link DefaultExpression} for a {@link Class} is plural
99+
* but the pattern only accepts single.
100+
*/
101+
NOT_SINGLE {
102+
@Override
103+
public String getError(List<String> codeNames, String pattern) {
104+
StringBuilder builder = new StringBuilder();
105+
builder.append(defaultExpression(codeNames, " is not a single-element expression. ", " are not single-element expressions. "))
106+
.append("Change your pattern to allow multiple elements or make the expression mandatory [pattern: ")
107+
.append(pattern)
108+
.append("]");
109+
return builder.toString();
110+
}
111+
},
112+
113+
/**
114+
* Error type for when the {@link DefaultExpression} for a {@link Class} does not accept time states
115+
* but the pattern infers it.
116+
*/
117+
TIME_STATE {
118+
@Override
119+
public String getError(List<String> codeNames, String pattern) {
120+
StringBuilder builder = new StringBuilder(defaultExpression(codeNames, " does ", " do "));
121+
builder.append("not have distinct time states. [pattern: ")
122+
.append(pattern)
123+
.append("]");
124+
return builder.toString();
125+
}
126+
};
127+
128+
/**
129+
* Returns an error message for the given type.
130+
*
131+
* @param codeNames The codeNames of {@link ClassInfo}s to include in the error message.
132+
* @param pattern The pattern to include in the error message.
133+
* @return error message.
134+
*/
135+
public abstract String getError(List<String> codeNames, String pattern);
136+
137+
/**
138+
* Utility method for constructing error messages in the format of
139+
* <code>
140+
* The default expression(s) of (codenames) (single/plural)
141+
* single -> The default expression of item type is
142+
* plural -> the default expressions of item type and entity are
143+
* </code>
144+
*
145+
* @param codeNames The list of codenames to be included in the error message.
146+
* @param single The string to be formatted at the end if there is only one codename.
147+
* @param plural The string to be formatted at the end if there is more than one codename.
148+
* @return The formatted error message.
149+
*/
150+
private static String defaultExpression(List<String> codeNames, String single, String plural) {
151+
StringBuilder builder = new StringBuilder();
152+
String combinedComma = getCombinedComma(codeNames);
153+
builder.append("The default ")
154+
.append(plurality(codeNames, "expression ", "expressions "))
155+
.append("of '")
156+
.append(combinedComma)
157+
.append("'")
158+
.append(plurality(codeNames, single, plural));
159+
return builder.toString();
160+
}
161+
162+
/**
163+
* Utility method for grabbing {@code single} if {@code codeNames} is singular, otherwise {@code plural}.
164+
*
165+
* @param codeNames The list of codenames to be checked.
166+
* @param single The string to be used if there is only one codename.
167+
* @param plural The string to be used if there is more than one codename.
168+
* @return {@code single} or {@code plural}.
169+
*/
170+
private static String plurality(List<String> codeNames, String single, String plural) {
171+
return codeNames.size() > 1 ? plural : single;
172+
}
173+
174+
/**
175+
* Utility method for combining {@code codeNames} into one string following this format.
176+
* <p>
177+
* 1: x
178+
* 2: x and y
179+
* 3 or more: x, y, and z
180+
* </p>
181+
* @param codeNames {@link List} of codenames to combine.
182+
* @return The combined string.
183+
*/
184+
private static String getCombinedComma(List<String> codeNames) {
185+
assert !codeNames.isEmpty();
186+
if (codeNames.size() == 1) {
187+
return codeNames.get(0);
188+
} else if (codeNames.size() == 2) {
189+
return StringUtils.join(codeNames, " and ");
190+
} else {
191+
return StringUtils.join(codeNames, ", ", ", and ");
192+
}
193+
}
194+
}
195+
196+
}

src/main/java/ch/njol/skript/lang/SkriptParser.java

Lines changed: 82 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@
1010
import ch.njol.skript.command.ScriptCommand;
1111
import ch.njol.skript.command.ScriptCommandEvent;
1212
import ch.njol.skript.expressions.ExprParse;
13+
import ch.njol.skript.lang.DefaultExpressionUtils.DefaultExpressionError;
1314
import ch.njol.skript.lang.function.ExprFunctionCall;
1415
import ch.njol.skript.lang.function.FunctionReference;
1516
import ch.njol.skript.lang.function.Functions;
1617
import ch.njol.skript.lang.parser.DefaultValueData;
1718
import ch.njol.skript.lang.parser.ParseStackOverflowException;
1819
import ch.njol.skript.lang.parser.ParserInstance;
1920
import ch.njol.skript.lang.parser.ParsingStack;
21+
import ch.njol.skript.lang.simplification.Simplifiable;
2022
import ch.njol.skript.lang.util.SimpleLiteral;
2123
import ch.njol.skript.localization.Language;
2224
import ch.njol.skript.localization.Message;
@@ -48,10 +50,17 @@
4850
import org.skriptlang.skript.lang.script.ScriptWarning;
4951
import org.skriptlang.skript.registration.SyntaxInfo;
5052
import org.skriptlang.skript.registration.SyntaxRegistry;
51-
import ch.njol.skript.lang.simplification.Simplifiable;
5253

5354
import java.lang.reflect.Array;
54-
import java.util.*;
55+
import java.util.ArrayList;
56+
import java.util.Deque;
57+
import java.util.EnumMap;
58+
import java.util.Iterator;
59+
import java.util.LinkedList;
60+
import java.util.List;
61+
import java.util.Locale;
62+
import java.util.Map;
63+
import java.util.Map.Entry;
5564
import java.util.concurrent.ConcurrentHashMap;
5665
import java.util.concurrent.atomic.AtomicBoolean;
5766
import java.util.function.Supplier;
@@ -238,10 +247,17 @@ public boolean hasTag(String tag) {
238247
types = parseResult.source.getElements(TypePatternElement.class);;
239248
ExprInfo exprInfo = types.get(i).getExprInfo();
240249
if (!exprInfo.isOptional) {
241-
DefaultExpression<?> expr = getDefaultExpression(exprInfo, pattern);
242-
if (!expr.init())
250+
List<DefaultExpression<?>> exprs = getDefaultExpressions(exprInfo, pattern);
251+
DefaultExpression<?> matchedExpr = null;
252+
for (DefaultExpression<?> expr : exprs) {
253+
if (expr.init()) {
254+
matchedExpr = expr;
255+
break;
256+
}
257+
}
258+
if (matchedExpr == null)
243259
continue patternsLoop;
244-
parseResult.exprs[i] = expr;
260+
parseResult.exprs[i] = matchedExpr;
245261
}
246262
}
247263
}
@@ -327,27 +343,71 @@ private static <T extends SyntaxElement> boolean checkExperimentalSyntax(T eleme
327343
return experimentalSyntax.isSatisfiedBy(experiments);
328344
}
329345

346+
/**
347+
* Returns the {@link DefaultExpression} from the first {@link ClassInfo} stored in {@code exprInfo}.
348+
*
349+
* @param exprInfo The {@link ExprInfo} to check for {@link DefaultExpression}.
350+
* @param pattern The pattern used to create {@link ExprInfo}.
351+
* @return {@link DefaultExpression}.
352+
* @throws SkriptAPIException If the {@link DefaultExpression} is not valid, produces an error message for the reasoning of failure.
353+
*/
330354
private static @NotNull DefaultExpression<?> getDefaultExpression(ExprInfo exprInfo, String pattern) {
331-
DefaultExpression<?> expr;
332-
// check custom default values first.
333355
DefaultValueData data = getParser().getData(DefaultValueData.class);
334-
expr = data.getDefaultValue(exprInfo.classes[0].getC());
335-
336-
// then check classinfo
356+
ClassInfo<?> classInfo = exprInfo.classes[0];
357+
DefaultExpression<?> expr = data.getDefaultValue(classInfo.getC());
337358
if (expr == null)
338-
expr = exprInfo.classes[0].getDefaultExpression();
359+
expr = classInfo.getDefaultExpression();
339360

340-
if (expr == null)
341-
throw new SkriptAPIException("The class '" + exprInfo.classes[0].getCodeName() + "' does not provide a default expression. Either allow null (with %-" + exprInfo.classes[0].getCodeName() + "%) or make it mandatory [pattern: " + pattern + "]");
342-
if (!(expr instanceof Literal) && (exprInfo.flagMask & PARSE_EXPRESSIONS) == 0)
343-
throw new SkriptAPIException("The default expression of '" + exprInfo.classes[0].getCodeName() + "' is not a literal. Either allow null (with %-*" + exprInfo.classes[0].getCodeName() + "%) or make it mandatory [pattern: " + pattern + "]");
344-
if (expr instanceof Literal && (exprInfo.flagMask & PARSE_LITERALS) == 0)
345-
throw new SkriptAPIException("The default expression of '" + exprInfo.classes[0].getCodeName() + "' is a literal. Either allow null (with %-~" + exprInfo.classes[0].getCodeName() + "%) or make it mandatory [pattern: " + pattern + "]");
346-
if (!exprInfo.isPlural[0] && !expr.isSingle())
347-
throw new SkriptAPIException("The default expression of '" + exprInfo.classes[0].getCodeName() + "' is not a single-element expression. Change your pattern to allow multiple elements or make the expression mandatory [pattern: " + pattern + "]");
348-
if (exprInfo.time != 0 && !expr.setTime(exprInfo.time))
349-
throw new SkriptAPIException("The default expression of '" + exprInfo.classes[0].getCodeName() + "' does not have distinct time states. [pattern: " + pattern + "]");
350-
return expr;
361+
DefaultExpressionError errorType = DefaultExpressionUtils.isValid(expr, exprInfo, 0);
362+
if (errorType == null) {
363+
assert expr != null;
364+
return expr;
365+
}
366+
367+
throw new SkriptAPIException(errorType.getError(List.of(classInfo.getCodeName()), pattern));
368+
}
369+
370+
/**
371+
* Returns all {@link DefaultExpression}s from all the {@link ClassInfo}s embedded in {@code exprInfo} that are valid.
372+
*
373+
* @param exprInfo The {@link ExprInfo} to check for {@link DefaultExpression}s.
374+
* @param pattern The pattern used to create {@link ExprInfo}.
375+
* @return All available {@link DefaultExpression}s.
376+
* @throws SkriptAPIException If no {@link DefaultExpression}s are valid, produces an error message for the reasoning of failure.
377+
*/
378+
static @NotNull List<DefaultExpression<?>> getDefaultExpressions(ExprInfo exprInfo, String pattern) {
379+
if (exprInfo.classes.length == 1)
380+
return new ArrayList<>(List.of(getDefaultExpression(exprInfo, pattern)));
381+
382+
DefaultValueData data = getParser().getData(DefaultValueData.class);
383+
384+
EnumMap<DefaultExpressionError, List<String>> failed = new EnumMap<>(DefaultExpressionError.class);
385+
List<DefaultExpression<?>> passed = new ArrayList<>();
386+
for (int i = 0; i < exprInfo.classes.length; i++) {
387+
ClassInfo<?> classInfo = exprInfo.classes[i];
388+
DefaultExpression<?> expr = data.getDefaultValue(classInfo.getC());
389+
if (expr == null)
390+
expr = classInfo.getDefaultExpression();
391+
392+
String codeName = classInfo.getCodeName();
393+
DefaultExpressionError errorType = DefaultExpressionUtils.isValid(expr, exprInfo, i);
394+
395+
if (errorType != null) {
396+
failed.computeIfAbsent(errorType, list -> new ArrayList<>()).add(codeName);
397+
} else {
398+
passed.add(expr);
399+
}
400+
}
401+
402+
if (!passed.isEmpty())
403+
return passed;
404+
405+
List<String> errors = new ArrayList<>();
406+
for (Entry<DefaultExpressionError, List<String>> entry : failed.entrySet()) {
407+
String error = entry.getKey().getError(entry.getValue(), pattern);
408+
errors.add(error);
409+
}
410+
throw new SkriptAPIException(StringUtils.join(errors, "\n"));
351411
}
352412

353413
private static final Pattern VARIABLE_PATTERN = Pattern.compile("((the )?var(iable)? )?\\{.+\\}", Pattern.CASE_INSENSITIVE);

src/main/java/ch/njol/skript/lang/util/SimpleLiteral.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,16 @@ public SimpleLiteral(T[] data, Class<T> type, boolean and) {
4646
}
4747

4848
public SimpleLiteral(T[] data, Class<T> type, boolean and, @Nullable Expression<?> source) {
49+
this(data, type, and, false, source);
50+
}
51+
52+
public SimpleLiteral(T[] data, Class<T> type, boolean and, boolean isDefault, @Nullable Expression<?> source) {
4953
assert data != null;
5054
assert type != null;
5155
this.data = data;
5256
this.type = type;
5357
this.and = data.length <= 1 || and;
54-
this.isDefault = false;
58+
this.isDefault = isDefault;
5559
this.source = source == null ? this : source;
5660
}
5761

0 commit comments

Comments
 (0)