Skip to content

Commit

Permalink
Remake argument-exceptions branch
Browse files Browse the repository at this point in the history
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
  • Loading branch information
willkroboth committed Aug 17, 2023
1 parent 5e546d7 commit da16444
Show file tree
Hide file tree
Showing 59 changed files with 2,488 additions and 580 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import java.util.function.Predicate;

import com.mojang.brigadier.Command;
import com.mojang.brigadier.arguments.ArgumentType;
import com.mojang.brigadier.builder.ArgumentBuilder;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
Expand All @@ -50,13 +51,7 @@
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
import com.mojang.brigadier.tree.LiteralCommandNode;

import dev.jorel.commandapi.arguments.AbstractArgument;
import dev.jorel.commandapi.arguments.ArgumentSuggestions;
import dev.jorel.commandapi.arguments.CustomProvidedArgument;
import dev.jorel.commandapi.arguments.Literal;
import dev.jorel.commandapi.arguments.MultiLiteral;
import dev.jorel.commandapi.arguments.PreviewInfo;
import dev.jorel.commandapi.arguments.Previewable;
import dev.jorel.commandapi.arguments.*;
import dev.jorel.commandapi.commandsenders.AbstractCommandSender;
import dev.jorel.commandapi.executors.CommandArguments;
import dev.jorel.commandapi.executors.ExecutionInfo;
Expand Down Expand Up @@ -162,9 +157,10 @@ private static void resetInstance() {
CommandAPIHandler.instance = null;
}

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

RequiredArgumentBuilder<Source, ?> requiredArgumentBuilder = RequiredArgumentBuilder
.argument(argument.getNodeName(), argument.getRawType());
.argument(argument.getNodeName(), wrapArgumentType(argument, argument.getRawType()));

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

<T, EI> ArgumentType<T> wrapArgumentType(Argument argument, ArgumentType<T> rawType) {
if (argument instanceof WrapperArgument) {
// A WrapperArgument should set its raw type to baseArgument's raw type, so that is already correct
return wrapArgumentType(((WrapperArgument<Argument>) argument).getBaseArgument(), rawType);
}

if (!(argument instanceof InitialParseExceptionArgument)) return rawType;

InitialParseExceptionArgument<T, ArgumentType<T>, EI, ?> iPEA =
(InitialParseExceptionArgument<T, ArgumentType<T>, EI, ?>) argument.instance();

Optional<InitialParseExceptionHandler<T, EI>> handler = iPEA.getInitialParseExceptionHandler();
if (handler.isEmpty()) return rawType;
return new ExceptionHandlingArgumentType<>(rawType, handler.get(), iPEA::parseInitialParseException);
}

CommandArguments generatePreviousArguments(CommandContext<Source> context, Argument[] args, String nodeName)
throws CommandSyntaxException {
// Populate Object[], which is our previously filled arguments
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package dev.jorel.commandapi;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;

/**
* A wrapper around MethodHandle with better type safety using generics and a
* toggleable underlying implementation depending on whether we're using mojang
* mappings or non-mojang mappings. This implementation only works for static
* methods that have one parameter.
*
* @param <ReturnType>
* @param <ParameterType>
*/
public class SafeStaticOneParameterMethodHandle<ReturnType, ParameterType> {

private final MethodHandle handle;

private SafeStaticOneParameterMethodHandle(MethodHandle handle) {
this.handle = handle;
}

private static <ReturnType, ParameterType> SafeStaticOneParameterMethodHandle<ReturnType, ParameterType> of(
Class<?> classType,
String methodName, String mojangMappedMethodName,
Class<? super ReturnType> returnType,
Class<? super ParameterType> parameterType
) throws ReflectiveOperationException {
return new SafeStaticOneParameterMethodHandle<>(MethodHandles.privateLookupIn(classType, MethodHandles.lookup()).findStatic(classType, SafeVarHandle.USING_MOJANG_MAPPINGS ? mojangMappedMethodName : methodName, MethodType.methodType(returnType, parameterType)));
}

public static <ReturnType, ParameterType> SafeStaticOneParameterMethodHandle<ReturnType, ParameterType> ofOrNull(
Class<?> classType,
String methodName, String mojangMappedMethodName,
Class<? super ReturnType> returnType,
Class<? super ParameterType> parameterType
) {
try {
return of(classType, methodName, mojangMappedMethodName, returnType, parameterType);
} catch (ReflectiveOperationException e) {
e.printStackTrace();
return null;
}
}

public ReturnType invoke(ParameterType parameter) throws Throwable {
return (ReturnType) handle.invoke(parameter);
}

public ReturnType invokeOrNull(ParameterType parameter) {
try {
return invoke(parameter);
} catch (Throwable e) {
e.printStackTrace();
return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public class SafeVarHandle<Type, FieldType> {

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

private VarHandle handle;
private final VarHandle handle;

private SafeVarHandle(VarHandle handle) {
this.handle = handle;
Expand All @@ -40,6 +40,10 @@ public FieldType get(Type instance) {
return (FieldType) handle.get(instance);
}

public FieldType getUnknownInstanceType(Object instance) {
return (FieldType) handle.get(instance);
}

public FieldType getStatic() {
return (FieldType) handle.get(null);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package dev.jorel.commandapi.arguments;

import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import dev.jorel.commandapi.ChainableBuilder;
import dev.jorel.commandapi.CommandAPIHandler;
import dev.jorel.commandapi.exceptions.WrapperCommandSyntaxException;
import dev.jorel.commandapi.executors.CommandArguments;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

/**
* An interface that indicates an argument can have an {@link ArgumentParseExceptionHandler} attached to it.
*
* @param <T> The class of the object that can be substituted instead of an exception when the Argument fails to parse.
* @param <Raw> The class of the object returned by the initial Brigadier parse for the Argument.
* @param <ExceptionInformation> The class that holds information about the exception.
* @param <Impl> The class extending this class, used as the return type in chained calls.
* @param <CommandSender> The CommandSender class used by the class extending this class.
*/
public interface ArgumentParseExceptionArgument<T, Raw, ExceptionInformation, Impl extends AbstractArgument<?, Impl, ?, CommandSender>, CommandSender> extends ChainableBuilder<Impl> {
/**
* A map that links Arguments to their ExceptionHandlers. This is basically
* equivalent to putting one instance variable in this interface, but Java
* doesn't let you put instance variables in interfaces, so we have to do
* this instead if we want to provide default implementations of the methods,
* overall avoiding the code duplication that comes from implementing these
* methods in the inheriting classes.
*/
// TODO: Maybe this can be a WeakHashMap, so once the Argument objects aren't being used anywhere else we can forget
// about them and not store them anymore. I'm not entirely sure that is what WeakHashMap does though. Are Arguments
// ever GC'd anyway, or do they stick around somewhere?
Map<ArgumentParseExceptionArgument<?, ?, ?, ?, ?>, ArgumentParseExceptionHandler<?, ?, ?, ?>> exceptionHandlers = new HashMap<>();

/**
* Sets the {@link ArgumentParseExceptionHandler} this Argument should use when it fails to parse.
*
* @param exceptionHandler The new {@link ArgumentParseExceptionHandler} this argument should use
* @return this current argument
*/
default Impl withArgumentParseExceptionHandler(
ArgumentParseExceptionHandler<T, Raw, ExceptionInformation, CommandSender> exceptionHandler
) {
exceptionHandlers.put(this, exceptionHandler);
return instance();
}

/**
* Returns the {@link ArgumentParseExceptionHandler} this argument is using
* @return The {@link ArgumentParseExceptionHandler} this argument is using
*/
default Optional<ArgumentParseExceptionHandler<T, Raw, ExceptionInformation, CommandSender>> getArgumentParseExceptionHandler() {
return Optional.ofNullable(
(ArgumentParseExceptionHandler<T, Raw, ExceptionInformation, CommandSender>) exceptionHandlers.get(this)
);
}

default <Source, A extends AbstractArgument<?, ?, A, CommandSender>>
T handleArgumentParseException(
CommandContext<Source> cmdCtx, String key, CommandArguments previousArgs,
CommandSyntaxException original, ExceptionInformation exceptionInformation
) throws CommandSyntaxException {
ArgumentParseExceptionHandler<T, Raw, ExceptionInformation, CommandSender> exceptionHandler =
getArgumentParseExceptionHandler().orElseThrow(() -> original);

try {
return exceptionHandler.handleException(new ArgumentParseExceptionContext<>(
new WrapperCommandSyntaxException(original),
exceptionInformation,
CommandAPIHandler.<A, CommandSender, Source>getInstance().getPlatform()
.getCommandSenderFromCommandSource(cmdCtx.getSource()).getSource(),
(Raw) cmdCtx.getArgument(key, Object.class),
previousArgs
));
} catch (WrapperCommandSyntaxException newException) {
throw newException.getException();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package dev.jorel.commandapi.arguments;

import dev.jorel.commandapi.exceptions.WrapperCommandSyntaxException;
import dev.jorel.commandapi.executors.CommandArguments;

/**
* A record containing information on why an Argument failed to parse.
*
* @param exception The CommandSyntaxException that was thrown when the Argument failed to parse.
*
* @param <ExceptionInformation> The class that holds information about the exception.
* @param exceptionInformation Extra information about the exception.
*
* @param <CommandSender> The CommandSender class being used.
* @param sender The CommandSender who sent the command that caused the exception.
*
* @param <Raw> The class that is returned by the initial Brigadier parse for the Argument.
* @param input The raw object returned by the initial Brigadier parse for the Argument.
*
* @param previousArguments A {@link CommandArguments} object holding previously declared (and parsed) arguments. This can
* be used as if it were arguments in a command executor method.
*/
public record ArgumentParseExceptionContext<Raw, ExceptionInformation, CommandSender>(
/**
* @param exception The CommandSyntaxException that was thrown when the Argument failed to parse.
*/
WrapperCommandSyntaxException exception,
/**
* @param exceptionInformation Extra information about the exception.
*/
ExceptionInformation exceptionInformation,
/**
* @param sender The CommandSender who sent the command that caused the exception.
*/
CommandSender sender,
/**
* @param input The raw object returned by the initial Brigadier parse for the Argument.
*/
Raw input,
/**
* @param previousArguments A {@link CommandArguments} object holding previously declared (and parsed) arguments.
* This can be used as if it were arguments in a command executor method.
*/
CommandArguments previousArguments) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package dev.jorel.commandapi.arguments;

import dev.jorel.commandapi.exceptions.WrapperCommandSyntaxException;

/**
* A FunctionalInterface for defining custom behavior when an Argument fails to parse.
* See {@link ArgumentParseExceptionHandler#handleException(ArgumentParseExceptionContext)}.
*
* @param <T> The class of the object that can be substituted instead of an exception when the Argument fails to parse.
* @param <Raw> The class of the object returned by the initial Brigadier parse for the Argument.
* @param <ExceptionInformation> The class that holds information about the exception.
* @param <CommandSender> The CommandSender class being used.
*/
@FunctionalInterface
public interface ArgumentParseExceptionHandler<T, Raw, ExceptionInformation, CommandSender> {
/**
* A method that handles when an Argument fails to parse.
* It can either return an object or throw a different exception.
*
* @param context a {@link ArgumentParseExceptionContext} record that holds information
* about why and when the Argument failed to parse
* @return A new object in place of the failed parse
* @throws WrapperCommandSyntaxException A new exception to pass on
*/
T handleException(ArgumentParseExceptionContext<Raw, ExceptionInformation, CommandSender> context) throws WrapperCommandSyntaxException;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package dev.jorel.commandapi.arguments;

import com.mojang.brigadier.StringReader;
import com.mojang.brigadier.arguments.ArgumentType;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.suggestion.Suggestions;
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
import dev.jorel.commandapi.exceptions.WrapperCommandSyntaxException;
import dev.jorel.commandapi.wrappers.WrapperStringReader;
import org.apache.commons.lang3.function.TriFunction;

import java.util.Collection;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiFunction;

/**
* An {@link ArgumentType} that wraps another {@link ArgumentType} and intercepts any
* {@link CommandSyntaxException} to send to a developer-specified {@link InitialParseExceptionHandler}
*
* @param baseType The {@link ArgumentType} this object is wrapping.
* @param exceptionHandler The {@link InitialParseExceptionHandler} that handles intercepted {@link CommandSyntaxException}.
* @param exceptionParser A function that parses the information in a {@link CommandSyntaxException} to create an
* {@link ExceptionInformation} object.
*
* @param <T> The object returned when the wrapped {@link ArgumentType} is parsed.
* @param <BaseType> The class of the {@link ArgumentType} this object is wrapping.
* @param <ExceptionInformation> The class that holds information about the exception.
*/
public record ExceptionHandlingArgumentType<T, BaseType extends ArgumentType<T>, ExceptionInformation>(
/**
* @param baseType The {@link ArgumentType} this object is wrapping
*/
BaseType baseType,
/**
* @param exceptionHandler The {@link InitialParseExceptionHandler} that handles intercepted {@link CommandSyntaxException}
*/
InitialParseExceptionHandler<T, ExceptionInformation> exceptionHandler,
/**
* @param exceptionParser A function that parses the information in a {@link CommandSyntaxException} to create an
* {@link ExceptionInformation} object.
*/
TriFunction<CommandSyntaxException, StringReader, BaseType, ExceptionInformation> exceptionParser
) implements ArgumentType<T> {

@Override
public T parse(StringReader stringReader) throws CommandSyntaxException {
try {
return baseType.parse(stringReader);
} catch (CommandSyntaxException original) {
try {
return exceptionHandler.handleException(new InitialParseExceptionContext<>(
new WrapperCommandSyntaxException(original),
exceptionParser.apply(original, stringReader, baseType),
new WrapperStringReader(stringReader)
));
} catch (WrapperCommandSyntaxException newException) {
throw newException.getException();
}
}
}

@Override
public Collection<String> getExamples() {
return baseType.getExamples();
}

@Override
public <S> CompletableFuture<Suggestions> listSuggestions(CommandContext<S> context, SuggestionsBuilder builder) {
return baseType.listSuggestions(context, builder);
}
}
Loading

0 comments on commit da16444

Please sign in to comment.