Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Argument exceptions2 #476

Draft
wants to merge 28 commits into
base: dev/dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
da16444
Remake argument-exceptions branch
willkroboth Jul 30, 2023
bb78f3e
Create InitialParseExceptionParser because TriFunction was being weird
willkroboth Jul 31, 2023
10cd400
Fix implementation of NMS_Common#extractTranslationKey for pre 1.19
willkroboth Jul 31, 2023
17543b6
Fix method reflection in ExceptionHandlingArgumentSerializers
willkroboth Jul 31, 2023
7191cd9
Fix Brigadier exposure when using IntegerArgument
willkroboth Jul 31, 2023
a1bf7b2
Address some SonarCloud code smells on `argument-exceptions2`
willkroboth Jul 31, 2023
e6d987b
Refactor negated pattern variables
willkroboth Aug 14, 2023
ba7c67e
Add javadocs to WrapperStringReader methods
willkroboth Aug 14, 2023
05d01f3
Add `InitialParse` to the name of `ExceptionHandlingArgumentType` and…
willkroboth Aug 14, 2023
a426196
Fix `Initial`ParseExceptionHandlingArgumentType accidentally being na…
willkroboth Aug 14, 2023
c1ff4d3
Create InitialParseExceptionNumberArgument to share ExceptionInformat…
willkroboth Aug 14, 2023
11a24b8
Clean up InitialParseExceptionNumberArgument
willkroboth Aug 14, 2023
62587ec
Move parse exception related classes in `dev.jorel.commandapi.argumen…
willkroboth Aug 14, 2023
1974b30
Fix javadoc link to IntegerArgument in InitialParseExceptionNumberArg…
willkroboth Aug 14, 2023
e2b8e99
Fix variable name capitalization in InitialParseExceptionHandlingArgu…
willkroboth Aug 14, 2023
149d4d7
Add InitialParseExceptionTextArgument for Arguments that use `StringA…
willkroboth Aug 14, 2023
0fac123
Add cursorStart to InitialParseExceptionContext
willkroboth Aug 14, 2023
e3f65d3
Add rawItem to ListArgumentCommon.ArgumentParseExceptionInformation
willkroboth Aug 14, 2023
5861f00
Add test for substituting values when using an ArgumentParseException…
willkroboth Aug 14, 2023
ae0fac4
Add package for `parseexceptions` tests
willkroboth Aug 14, 2023
5a1a16e
Fix javadocs in InitialParseExceptionNumberArgument that refer to `in…
willkroboth Aug 16, 2023
c41f567
Add tests for InitialParseExceptionNumberArguments usage
willkroboth Aug 16, 2023
d18e3d4
Remove public modifier from new tests
willkroboth Aug 16, 2023
f9ff517
Add `InitialParseExceptionContextVerifier` and `ArgumentParseExceptio…
willkroboth Aug 16, 2023
412a0ef
Add `assertCorrectContext` method to ArgumentParseExceptionContextVer…
willkroboth Aug 17, 2023
f7cc6d8
Add `argumentParseExceptionTestWithListArgument`
willkroboth Aug 17, 2023
3c72dde
Add `initialParseExceptionTestWithListTextArgument`
willkroboth Aug 17, 2023
8d40f57
Change `exceptionHandlers` maps to `WeakHashMaps`
willkroboth Aug 18, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,10 @@
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.arguments.parseexceptions.InitialParseExceptionArgument;
import dev.jorel.commandapi.arguments.parseexceptions.InitialParseExceptionHandler;
import dev.jorel.commandapi.arguments.parseexceptions.InitialParseExceptionHandlingArgumentType;
import dev.jorel.commandapi.commandsenders.AbstractCommandSender;
import dev.jorel.commandapi.executors.CommandArguments;
import dev.jorel.commandapi.executors.ExecutionInfo;
Expand Down Expand Up @@ -162,9 +160,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 +812,32 @@ 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) {
InitialParseExceptionArgument<T, EI, ?> iPEA = (InitialParseExceptionArgument<T, EI, ?>) argument.instance();
Optional<InitialParseExceptionHandler<T, EI>> handler = iPEA.getInitialParseExceptionHandler();

if (handler.isPresent()) {
return new InitialParseExceptionHandlingArgumentType<>(
rawType, handler.get(), iPEA::parseInitialParseException
);
}
}

return rawType;
}

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,18 @@
package dev.jorel.commandapi.arguments;

import com.mojang.brigadier.arguments.ArgumentType;

/**
* An interface that indicates this Argument wraps another Argument. For example, a CustomArgument wraps another
* Argument, with the purpose of adding additional parsing onto it's 'base Argument'.
* <p>
* Note: A WrapperArgument should set its {@link ArgumentType} to that of its base Argument.
*
* @param <Argument> The Argument class this Argument is wrapping
*/
public interface WrapperArgument<Argument> {
/**
* @return The base Argument that this Argument has wrapped.
*/
Argument getBaseArgument();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package dev.jorel.commandapi.arguments.parseexceptions;

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.arguments.AbstractArgument;
import dev.jorel.commandapi.exceptions.WrapperCommandSyntaxException;
import dev.jorel.commandapi.executors.CommandArguments;

import java.util.Map;
import java.util.Optional;
import java.util.WeakHashMap;

/**
* 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.
*/
Map<ArgumentParseExceptionArgument<?, ?, ?, ?, ?>, ArgumentParseExceptionHandler<?, ?, ?, ?>> exceptionHandlers = new WeakHashMap<>();

/**
* 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.parseexceptions;

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.parseexceptions;

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,62 @@
package dev.jorel.commandapi.arguments.parseexceptions;

import com.mojang.brigadier.StringReader;
import com.mojang.brigadier.arguments.ArgumentType;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import dev.jorel.commandapi.ChainableBuilder;
import dev.jorel.commandapi.arguments.AbstractArgument;

import java.util.Map;
import java.util.Optional;
import java.util.WeakHashMap;

/**
* An interface that indicates an argument can have an {@link InitialParseExceptionHandler} attached to it.
*
* @param <T> The class of the object returned when the {@link ArgumentType}
* used by this Argument parses its raw input.
* @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.
*/
public interface InitialParseExceptionArgument<T, ExceptionInformation, Impl extends AbstractArgument<?, Impl, ?, ?>> 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.
*/
Map<InitialParseExceptionArgument<?, ?, ?>, InitialParseExceptionHandler<?, ?>> exceptionHandlers = new WeakHashMap<>();

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

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

/**
* Extracts information about an exception that was thrown when the {@link ArgumentType} for this Argument
* failed to parse.
*
* @param exception The {@link CommandSyntaxException} that was thrown.
* @param reader The {@link StringReader} that was reading this Argument.
* @return An {@link ExceptionInformation} object that holds information about the given exception.
*/
ExceptionInformation parseInitialParseException(CommandSyntaxException exception, StringReader reader);
}
Loading
Loading