8000 Remake argument-exceptions branch · CommandAPI/CommandAPI@da16444 · GitHub
[go: up one dir, main page]

Skip to content

Commit da16444

Browse files
committed
Remake argument-exceptions branch
Add API to use custom error handling when Argument parsing fails See #370 for the basis of these changes New changes here: - Arguments can only have an ArgumentParseExceptionHandler attached if they implement ArgumentParseExceptionArgument - The substitute value from ArgumentParseExceptionHandler doesn't have to be returned directly - ExceptionInformation can be provided by arguments - New NMS method to extract translation keys from CommandSyntaxExceptions
1 parent 5e546d7 commit da16444

File tree

59 files changed

+2488
-580
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+2488
-580
lines changed

commandapi-core/src/main/java/dev/jorel/commandapi/CommandAPIHandler.java

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import java.util.function.Predicate;
3939

4040
import com.mojang.brigadier.Command;
41+
import com.mojang.brigadier.arguments.ArgumentType;
4142
import com.mojang.brigadier.builder.ArgumentBuilder;
4243
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
4344
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
@@ -50,13 +51,7 @@
5051
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
5152
import com.mojang.brigadier.tree.LiteralCommandNode;
5253

53-
import dev.jorel.commandapi.arguments.AbstractArgument;
54-
import dev.jorel.commandapi.arguments.ArgumentSuggestions;
55-
import dev.jorel.commandapi.arguments.CustomProvidedArgument;
56-
import dev.jorel.commandapi.arguments.Literal;
57-
import dev.jorel.commandapi.arguments.MultiLiteral;
58-
import dev.jorel.commandapi.arguments.PreviewInfo;
59-
import dev.jorel.commandapi.arguments.Previewable;
54+
import dev.jorel.commandapi.arguments.*;
6055
import dev.jorel.commandapi.commandsenders.AbstractCommandSender;
6156
import dev.jorel.commandapi.executors.CommandArguments;
6257
import dev.jorel.commandapi.executors.ExecutionInfo;
@@ -162,9 +157,10 @@ private static void resetInstance() {
162157
CommandAPIHandler.instance = null;
163158
}
164159

165-
public static CommandAPIHandler<?, ?, ?> getInstance() {
160+
public static <Argument extends AbstractArgument<?, ?, Argument, CommandSender>, CommandSender, Source>
161+
CommandAPIHandler<Argument, CommandSender, Source> getInstance() {
166162
if(CommandAPIHandler.instance != null) {
167-
return CommandAPIHandler.instance;
163+
return (CommandAPIHandler<Argument, CommandSender, Source>) CommandAPIHandler.instance;
168164
} else {
169165
throw new IllegalStateException("Tried to access CommandAPIHandler instance, but it was null! Are you using CommandAPI features before calling CommandAPI#onLoad?");
170166
}
@@ -813,12 +809,28 @@ LiteralArgumentBuilder<Source> getLiteralArgumentBuilderArgument(String commandN
813809
}
814810

815811
RequiredArgumentBuilder<Source, ?> requiredArgumentBuilder = RequiredArgumentBuilder
816-
.argument(argument.getNodeName(), argument.getRawType());
812+
.argument(argument.getNodeName(), wrapArgumentType(argument, argument.getRawType()));
817813

818814
return requiredArgumentBuilder.requires(css -> permissionCheck(platform.getCommandSenderFromCommandSource(css),
819815
argument.getArgumentPermission(), argument.getRequirements())).suggests(newSuggestionsProvider);
820816
}
821817

818+
<T, EI> ArgumentType<T> wrapArgumentType(Argument argument, ArgumentType<T> rawType) {
819+
if (argument instanceof WrapperArgument) {
820+
// A WrapperArgument should set its raw type to baseArgument's raw type, so that is already correct
821+
return wrapArgumentType(((WrapperArgument<Argument>) argument).getBaseArgument(), rawType);
822+
}
823+
824+
if (!(argument instanceof InitialParseExceptionArgument)) return rawType;
825+
826+
InitialParseExceptionArgument<T, ArgumentType<T>, EI, ?> iPEA =
827+
(InitialParseExceptionArgument<T, ArgumentType<T>, EI, ?>) argument.instance();
828+
829+
Optional<InitialParseExceptionHandler<T, EI>> handler = iPEA.getInitialParseExceptionHandler();
830+
if (handler.isEmpty()) return rawType;
831+
return new ExceptionHandlingArgumentType<>(rawType, handler.get(), iPEA::parseInitialParseException);
832+
}
833+
822834
CommandArguments generatePreviousArguments(CommandContext<Source> context, Argument[] args, String nodeName)
823835
throws CommandSyntaxException {
824836
// Populate Object[], which is our previously filled arguments
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package dev.jorel.commandapi;
2+
3+
import java.lang.invoke.MethodHandle;
4+
import java.lang.invoke.MethodHandles;
5+
import java.lang.invoke.MethodType;
6+
7+
/**
8+
* A wrapper around MethodHandle with better type safety using generics and a
9+
* toggleable underlying implementation depending on whether we're using mojang
10+
* mappings or non-mojang mappings. This implementation only works for static
11+
* methods that have one parameter.
12+
*
13+
* @param <ReturnType>
14+
* @param <ParameterType>
15+
*/
16+
public class SafeStaticOneParameterMethodHandle<ReturnType, ParameterType> {
17+
18+
private final MethodHandle handle;
19+
20+
private SafeStaticOneParameterMethodHandle(MethodHandle handle) {
21+
this.handle = handle;
22+
}
23+
24+
private static <ReturnType, ParameterType> SafeStaticOneParameterMethodHandle<ReturnType, ParameterType> of(
25+
Class<?> classType,
26+
String methodName, String mojangMappedMethodName,
27+
Class<? super ReturnType> returnType,
28+
Class<? super ParameterType> parameterType
29+
) throws ReflectiveOperationException {
30+
return new SafeStaticOneParameterMethodHandle<>(MethodHandles.privateLookupIn(classType, MethodHandles.lookup()).findStatic(classType, SafeVarHandle.USING_MOJANG_MAPPINGS ? mojangMappedMethodName : methodName, MethodType.methodType(returnType, parameterType)));
31+
}
32+
33+
public static <ReturnType, ParameterType> SafeStaticOneParameterMethodHandle<ReturnType, ParameterType> ofOrNull(
34+
Class<?> classType,
35+
String methodName, String mojangMappedMethodName,
36+
Class<? super ReturnType> returnType,
37+
Class<? super ParameterType> parameterType
38+
) {
39+
try {
40+
return of(classType, methodName, mojangMappedMethodName, returnType, parameterType);
41+
} catch (ReflectiveOperationException e) {
42+
e.printStackTrace();
43+
return null;
44+
}
45+
}
46+
47+
public ReturnType invoke(ParameterType parameter) throws Throwable {
48+
return (ReturnType) handle.invoke(parameter);
49+
}
50+
51+
public ReturnType invokeOrNull(ParameterType parameter) {
52+
try {
53+
return invoke(parameter);
54+
} catch (Throwable e) {
55+
e.printStackTrace();
56+
return null;
57+
}
58+
}
59+
}

commandapi-core/src/main/java/dev/jorel/commandapi/SafeVarHandle.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public class SafeVarHandle<Type, FieldType> {
1515

1616
public static boolean USING_MOJANG_MAPPINGS = false; // This should only be set to true in testing.
1717

18-
private VarHandle handle;
18+
private final VarHandle handle;
1919

2020
private SafeVarHandle(VarHandle handle) {
2121
this.handle = handle;
@@ -40,6 +40,10 @@ public FieldType get(Type instance) {
4040
return (FieldType) handle.get(instance);
4141
}
4242

43+
public FieldType getUnknownInstanceType(Object instance) {
44+
return (FieldType) handle.get(instance);
45+
}
46+
4347
public FieldType getStatic() {
4448
return (FieldType) handle.get(null);
4549
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package dev.jorel.commandapi.arguments;
2+
3+
import com.mojang.brigadier.context.CommandContext;
4+
import com.mojang.brigadier.exceptions.CommandSyntaxException;
5+
import dev.jorel.commandapi.ChainableBuilder;
6+
import dev.jorel.commandapi.CommandAPIHandler;
7+
import dev.jorel.commandapi.exceptions.WrapperCommandSyntaxException;
8+
import dev.jorel.commandapi.executors.CommandArguments;
9+
10+
import java.util.HashMap;
11+
import java.util.Map;
12+
import java.util.Optional;
13+
14+
/**
15+
* An interface that indicates an argument can have an {@link ArgumentParseExceptionHandler} attached to it.
16+
*
17+
* @param <T> The class of the object that can be substituted instead of an exception when the Argument fails to parse.
18+
* @param <Raw> The class of the object returned by the initial Brigadier parse for the Argument.
19+
* @param <ExceptionInformation> The class that holds information about the exception.
20+
* @param <Impl> The class extending this class, used as the return type in chained calls.
21+
* @param <CommandSender> The CommandSender class used by the class extending this class.
22+
*/
23+
public interface ArgumentParseExceptionArgument<T, Raw, ExceptionInformation, Impl extends AbstractArgument<?, Impl, ?, CommandSender>, CommandSender> extends ChainableBuilder<Impl> {
24+
/**
25+
* A map that links Arguments to their ExceptionHandlers. This is basically
26+
* equivalent to putting one instance variable in this interface, but Java
27+
* doesn't let you put instance variables in interfaces, so we have to do
28+
* this instead if we want to provide default implementations of the methods,
29+
* overall avoiding the code duplication that comes from implementing these
30+
* methods in the inheriting classes.
31+
*/
32+
// TODO: Maybe this can be a WeakHashMap, so once the Argument objects aren't being used anywhere else we can forget
33+
// about them and not store them anymore. I'm not entirely sure that is what WeakHashMap does though. Are Arguments
34+
// ever GC'd anyway, or do they stick around somewhere?
35+
Map<ArgumentParseExceptionArgument<?, ?, ?, ?, ?>, ArgumentParseExceptionHandler<?, ?, ?, ?>> exceptionHandlers = new HashMap<>();
36+
37+
/**
38+
* Sets the {@link ArgumentParseExceptionHandler} this Argument should use when it fails to parse.
39+
*
40+
* @param exceptionHandler The new {@link ArgumentParseExceptionHandler} this argument should use
41+
* @return this current argument
42+
*/
43+
default Impl withArgumentParseExceptionHandler(
44+
ArgumentParseExceptionHandler<T, Raw, ExceptionInformation, CommandSender> exceptionHandler
45+
) {
46+
exceptionHandlers.put(this, exceptionHandler);
47+
return instance();
48+
}
49+
50+
/**
51+
* Returns the {@link ArgumentParseExceptionHandler} this argument is using
52+
* @return The {@link ArgumentParseExceptionHandler} this argument is using
53+
*/
54+
default Optional<ArgumentParseExceptionHandler<T, Raw, ExceptionInformation, CommandSender>> getArgumentParseExceptionHandler() {
55+
return Optional.ofNullable(
56+
(ArgumentParseExceptionHandler<T, Raw, ExceptionInformation, CommandSender>) exceptionHandlers.get(this)
57+
);
58+
}
59+
60+
default <Source, A extends AbstractArgument<?, ?, A, CommandSender>>
61+
T handleArgumentParseException(
62+
CommandContext<Source> cmdCtx, String key, CommandArguments previousArgs,
63+
CommandSyntaxException original, ExceptionInformation exceptionInformation
64+
) throws CommandSyntaxException {
65+
ArgumentParseExceptionHandler<T, Raw, ExceptionInformation, CommandSender> exceptionHandler =
66+
getArgumentParseExceptionHandler().orElseThrow(() -> original);
67+
68+
try {
69+
return exceptionHandler.handleException(new ArgumentParseExceptionContext<>(
70+
new WrapperCommandSyntaxException(original),
71+
exceptionInformation,
72+
CommandAPIHandler.<A, CommandSender, Source>getInstance().getPlatform()
73+
.getCommandSenderFromCommandSource(cmdCtx.getSource()).getSource(),
74+
(Raw) cmdCtx.getArgument(key, Object.class),
75+
previousArgs
76+
));
77+
} catch (WrapperCommandSyntaxException newException) {
78+
throw newException.getException();
79+
}
80+
}
81+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package dev.jorel.commandapi.arguments;
2+
3+
import dev.jorel.commandapi.exceptions.WrapperCommandSyntaxException;
4+
import dev.jorel.commandapi.executors.CommandArguments;
5+
6+
/**
7+
* A record containing information on why an Argument failed to parse.
8+
*
9+
* @param exception The CommandSyntaxException that was thrown when the Argument failed to parse.
10+
*
11+
* @param <ExceptionInformation> The class that holds information about the exception.
12+
* @param exceptionInformation Extra information about the exception.
13+
*
14+
* @param <CommandSender> The CommandSender class being used.
15+
* @param sender The CommandSender who sent the command that caused the exception.
16+
*
17+
* @param <Raw> The class that is returned by the initial Brigadier parse for the Argument.
18+
* @param input The raw object returned by the initial Brigadier parse for the Argument.
19+
*
20+
* @param previousArguments A {@link CommandArguments} object holding previously declared (and parsed) arguments. This can
21+
* be used as if it were arguments in a command executor method.
22+
*/
23+
public record ArgumentParseExceptionContext<Raw, ExceptionInformation, CommandSender>(
24+
/**
25+
* @param exception The CommandSyntaxException that was thrown when the Argument failed to parse.
26+
*/
27+
WrapperCommandSyntaxException exception,
28+
/**
29+
* @param exceptionInformation Extra information about the exception.
30+
*/
31+
ExceptionInformation exceptionInformation,
32+
/**
33+
* @param sender The CommandSender who sent the command that caused the exception.
34+
*/
35+
CommandSender sender,
36+
/**
37+
* @param input The raw object returned by the initial Brigadier parse for the Argument.
38+
*/
39+
Raw input,
40+
/**
41+
* @param previousArguments A {@link CommandArguments} object holding previously declared (and parsed) arguments.
42+
* This can be used as if it were arguments in a command executor method.
43+
*/
44+
CommandArguments previousArguments) {
45+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package dev.jorel.commandapi.arguments;
2+
3+
import dev.jorel.commandapi.exceptions.WrapperCommandSyntaxException;
4+
5+
/**
6+
* A FunctionalInterface for defining custom behavior when an Argument fails to parse.
7+
* See {@link ArgumentParseExceptionHandler#handleException(ArgumentParseExceptionContext)}.
8+
*
9+
* @param <T> The class of the object that can be substituted instead of an exception when the Argument fails to parse.
10+
* @param <Raw> The class of the object returned by the initial Brigadier parse for the Argument.
11+
* @param <ExceptionInformation> The class that holds information about the exception.
12+
* @param <CommandSender> The CommandSender class being used.
13+
*/
14+
@FunctionalInterface
15+
public interface ArgumentParseExceptionHandler<T, Raw, ExceptionInformation, CommandSender> {
16+
/**
17+
* A method that handles when an Argument fails to parse.
18+
* It can either return an object or throw a different exception.
19+
*
20+
* @param context a {@link ArgumentParseExceptionContext} record that holds information
21+
* about why and when the Argument failed to parse
22+
* @return A new object in place of the failed parse
23+
* @throws WrapperCommandSyntaxException A new exception to pass on
24+
*/
25+
T handleException(ArgumentParseExceptionContext<Raw, ExceptionInformation, CommandSender> context) throws WrapperCommandSyntaxException;
26+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package dev.jorel.commandapi.arguments;
2+
3+
import com.mojang.brigadier.StringReader;
4+
import com.mojang.brigadier.arguments.ArgumentType;
5+
import com.mojang.brigadier.context.CommandContext;
6+
import com.mojang.brigadier.exceptions.CommandSyntaxException;
7+
import com.mojang.brigadier.suggestion.Suggestions;
8+
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
9+
import dev.jorel.commandapi.exceptions.WrapperCommandSyntaxException;
10+
import dev.jorel.commandapi.wrappers.WrapperStringReader;
11+
import org.apache.commons.lang3.function.TriFunction;
12+
13+
import java.util.Collection;
14+
import java.util.concurrent.CompletableFuture;
15+
import java.util.function.BiFunction;
16+
17+
/**
18+
* An {@link ArgumentType} that wraps another {@link ArgumentType} and intercepts any
19+
* {@link CommandSyntaxException} to send to a developer-specified {@link InitialParseExceptionHandler}
20+
*
21+
* @param baseType The {@link ArgumentType} this object is wrapping.
22+
* @param exceptionHandler The {@link InitialParseExceptionHandler} that handles intercepted {@link CommandSyntaxException}.
23+
* @param exceptionParser A function that parses the information in a {@link CommandSyntaxException} to create an
24+
* {@link ExceptionInformation} object.
25+
*
26+
* @param <T> The object returned when the wrapped {@link ArgumentType} is parsed.
27+
* @param <BaseType> The class of the {@link ArgumentType} this object is wrapping.
28+
* @param <ExceptionInformation> The class that holds information about the exception.
29+
*/
30+
public record ExceptionHandlingArgumentType<T, BaseType extends ArgumentType<T>, ExceptionInformation>(
31+
/**
32+
* @param baseType The {@link ArgumentType} this object is wrapping
33+
*/
34+
BaseType baseType,
35+
/**
36+
* @param exceptionHandler The {@link InitialParseExceptionHandler} that handles intercepted {@link CommandSyntaxException}
37+
*/
38+
InitialParseExceptionHandler<T, ExceptionInformation> exceptionHandler,
39+
/**
40+
* @param exceptionParser A function that parses the information in a {@link CommandSyntaxException} to create an
41+
* {@link ExceptionInformation} object.
42+
*/
43+
TriFunction<CommandSyntaxException, StringReader, BaseType, ExceptionInformation> exceptionParser
44+
) implements ArgumentType<T> {
45+
46+
@Override
47+
public T parse(StringReader stringReader) throws CommandSyntaxException {
48+
try {
49+
return baseType.parse(stringReader);
50+
} catch (CommandSyntaxException original) {
51+
try {
52+
return exceptionHandler.handleException(new InitialParseExceptionContext<>(
53+
new WrapperCommandSyntaxException(original),
54+
exceptionParser.apply(original, stringReader, baseType),
55+
new WrapperStringReader(stringReader)
56+
));
57+
} catch (WrapperCommandSyntaxException newException) {
58+
throw newException.getException();
59+
}
60+
}
61+
}
62+
63+
@Override
64+
public Collection<String> getExamples() {
65+
return baseType.getExamples();
66+
}
67+
68+
@Override
69+
public <S> CompletableFuture<Suggestions> listSuggestions(CommandContext<S> context, SuggestionsBuilder builder) {
70+
return baseType.listSuggestions(context, builder);
71+
}
72+
}

0 commit comments

Comments
 (0)
0