diff --git a/README.md b/README.md
index b7d579c..6cfd33a 100644
--- a/README.md
+++ b/README.md
@@ -80,6 +80,43 @@ dependencies {
---
+## 🏷️ Annotations Addon
+
+The `annotations-addon` module provides annotation-based command registration. When using `@Arg` implicitly (without annotation), parameter names are used as argument names. This requires the `-parameters` compiler flag.
+
+### Maven
+
+```xml
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.13.0
+
+ true
+
+
+```
+
+### Gradle (Groovy)
+
+```groovy
+tasks.withType(JavaCompile).configureEach {
+ options.compilerArgs.add('-parameters')
+}
+```
+
+### Gradle (Kotlin DSL)
+
+```kotlin
+tasks.withType().configureEach {
+ options.compilerArgs.add("-parameters")
+}
+```
+
+> 💡 Without this flag, parameter names default to `arg0`, `arg1`, etc. You can always use `@Arg("name")` explicitly to avoid this requirement.
+
+---
+
## 💡 Example (Spigot)
Be sure to extends all the classes from the platform you are using (Spigot, Velocity, etc.):
diff --git a/annotations-addon/src/main/java/fr/traqueur/commands/annotations/AnnotationCommandProcessor.java b/annotations-addon/src/main/java/fr/traqueur/commands/annotations/AnnotationCommandProcessor.java
index 7bc2e83..a2185ce 100644
--- a/annotations-addon/src/main/java/fr/traqueur/commands/annotations/AnnotationCommandProcessor.java
+++ b/annotations-addon/src/main/java/fr/traqueur/commands/annotations/AnnotationCommandProcessor.java
@@ -6,6 +6,7 @@
import fr.traqueur.commands.api.models.Command;
import fr.traqueur.commands.api.models.CommandBuilder;
import fr.traqueur.commands.api.resolver.SenderResolver;
+import fr.traqueur.commands.api.utils.Patterns;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
@@ -31,30 +32,47 @@ public AnnotationCommandProcessor(CommandManager manager) {
this.senderResolver = manager.getPlatform().getSenderResolver();
}
- public void register(Object... handlers) {
+ public List> register(Object... handlers) {
+ List> allCommands = new ArrayList<>();
for (Object handler : handlers) {
- processHandler(handler);
+ allCommands.addAll(processHandler(handler));
}
+ return allCommands;
}
- private void processHandler(Object handler) {
+ private List> processHandler(Object handler) {
Class> clazz = handler.getClass();
+ validateCommandContainer(clazz);
+ collectTabCompleters(handler, clazz);
+
+ List commandMethods = collectCommandMethods(handler, clazz);
+ Set allPaths = extractAllPaths(commandMethods);
+
+ Map> builtCommands = buildAllCommands(commandMethods, allPaths);
+ Set rootCommands = organizeHierarchy(commandMethods, allPaths, builtCommands);
+
+ return registerRootCommands(rootCommands, builtCommands);
+ }
+
+ private void validateCommandContainer(Class> clazz) {
if (!clazz.isAnnotationPresent(CommandContainer.class)) {
throw new IllegalArgumentException(
"Class must be annotated with @CommandContainer: " + clazz.getName()
);
}
+ }
- // First pass: collect all @TabComplete methods
+ private void collectTabCompleters(Object handler, Class> clazz) {
tabCompleters.clear();
for (Method method : clazz.getDeclaredMethods()) {
if (method.isAnnotationPresent(TabComplete.class)) {
processTabCompleter(handler, method);
}
}
+ }
- // Second pass: collect all @Command methods and sort by depth
+ private List collectCommandMethods(Object handler, Class> clazz) {
List commandMethods = new ArrayList<>();
for (Method method : clazz.getDeclaredMethods()) {
if (method.isAnnotationPresent(fr.traqueur.commands.annotations.Command.class)) {
@@ -63,46 +81,51 @@ private void processHandler(Object handler) {
commandMethods.add(new CommandMethodInfo(handler, method, annotation.name()));
}
}
+ commandMethods.sort(Comparator.comparingInt(info -> Patterns.DOT.split(info.name).length));
+ return commandMethods;
+ }
- // Sort by depth (parents first)
- commandMethods.sort(Comparator.comparingInt(info -> info.name.split("\\.").length));
-
- // Collect all command paths to determine which have parents defined
+ private Set extractAllPaths(List commandMethods) {
Set allPaths = new HashSet<>();
for (CommandMethodInfo info : commandMethods) {
allPaths.add(info.name);
}
+ return allPaths;
+ }
- // Third pass: build ALL commands first
+ private Map> buildAllCommands(List commandMethods, Set allPaths) {
Map> builtCommands = new LinkedHashMap<>();
- Set rootCommands = new LinkedHashSet<>();
-
for (CommandMethodInfo info : commandMethods) {
String parentPath = getParentPath(info.name);
boolean hasParentInBatch = parentPath != null && allPaths.contains(parentPath);
-
Command command = buildCommand(info.handler, info.method, info.name, hasParentInBatch);
builtCommands.put(info.name, command);
}
+ return builtCommands;
+ }
- // Fourth pass: organize hierarchy (add subcommands to parents)
+ private Set organizeHierarchy(List commandMethods, Set allPaths,
+ Map> builtCommands) {
+ Set rootCommands = new LinkedHashSet<>();
for (CommandMethodInfo info : commandMethods) {
String parentPath = getParentPath(info.name);
-
if (parentPath != null && allPaths.contains(parentPath)) {
- Command parent = builtCommands.get(parentPath);
- Command child = builtCommands.get(info.name);
- parent.addSubCommand(child);
+ builtCommands.get(parentPath).addSubCommand(builtCommands.get(info.name));
} else {
rootCommands.add(info.name);
}
}
+ return rootCommands;
+ }
- // Fifth pass: register only root commands
+ private List> registerRootCommands(Set rootCommands, Map> builtCommands) {
+ List> registeredCommands = new ArrayList<>();
for (String rootPath : rootCommands) {
- Command rootCommand = builtCommands.get(rootPath);
- manager.registerCommand(rootCommand);
+ Command command = builtCommands.get(rootPath);
+ manager.registerCommand(command);
+ registeredCommands.add(command);
}
+ return registeredCommands;
}
private String getParentPath(String path) {
@@ -167,67 +190,48 @@ private void processTabCompleter(Object handler, Method method) {
private void processParameters(CommandBuilder builder, Method method, String commandPath) {
Parameter[] params = method.getParameters();
- Type[] genericTypes = method.getGenericParameterTypes();
for (int i = 0; i < params.length; i++) {
Parameter param = params[i];
- Class> paramType = param.getType();
-
- // First parameter is sender (skip it for args)
- if (i == 0) {
- Class> senderType = paramType;
- if (paramType == Optional.class) {
- senderType = extractOptionalType(param);
- }
- if (senderResolver.canResolve(senderType)) {
- continue;
- }
- }
- // Must have @Arg annotation
- Arg argAnnotation = param.getAnnotation(Arg.class);
- if (argAnnotation == null) {
- throw new IllegalArgumentException(
- "Parameter '" + param.getName() + "' in method '" + method.getName() +
- "' must be annotated with @Arg or be the sender type"
- );
+ if (i == 0 && isSenderParameter(param)) {
+ continue;
}
- String argName = argAnnotation.value();
- boolean isOptional = paramType == Optional.class;
- boolean isInfinite = param.isAnnotationPresent(Infinite.class);
+ registerArgument(builder, param, commandPath);
+ }
+ }
- // Determine the actual argument type
- Class> argType;
- if (isOptional) {
- argType = extractOptionalType(param);
- } else {
- argType = paramType;
- }
+ private boolean isSenderParameter(Parameter param) {
+ Class> paramType = param.getType();
+ Class> senderType = (paramType == Optional.class) ? extractOptionalType(param) : paramType;
+ return senderResolver.canResolve(senderType);
+ }
- // If @Infinite, use Infinite.class as the type
- if (isInfinite) {
- argType = fr.traqueur.commands.api.arguments.Infinite.class;
- }
+ private void registerArgument(CommandBuilder builder, Parameter param, String commandPath) {
+ String argName = getArgumentName(param);
+ Class> argType = resolveArgumentType(param);
+ boolean isOptional = param.getType() == Optional.class;
+ TabCompleter completer = getTabCompleter(commandPath, argName);
- // Get tab completer if exists
- TabCompleter completer = getTabCompleter(commandPath, argName);
+ if (isOptional) {
+ builder.optionalArg(argName, argType, completer);
+ } else {
+ builder.arg(argName, argType, completer);
+ }
+ }
- // Add argument to builder
- if (isOptional) {
- if (completer != null) {
- builder.optionalArg(argName, argType, completer);
- } else {
- builder.optionalArg(argName, argType);
- }
- } else {
- if (completer != null) {
- builder.arg(argName, argType, completer);
- } else {
- builder.arg(argName, argType);
- }
- }
+ private String getArgumentName(Parameter param) {
+ Arg argAnnotation = param.getAnnotation(Arg.class);
+ return (argAnnotation != null) ? argAnnotation.value() : param.getName();
+ }
+
+ private Class> resolveArgumentType(Parameter param) {
+ if (param.isAnnotationPresent(Infinite.class)) {
+ return fr.traqueur.commands.api.arguments.Infinite.class;
}
+ Class> paramType = param.getType();
+ return (paramType == Optional.class) ? extractOptionalType(param) : paramType;
}
/**
@@ -257,67 +261,71 @@ private TabCompleter getTabCompleter(String commandPath, String argName) {
return (sender, args) -> {
try {
- Object result;
- Parameter[] params = tcMethod.method.getParameters();
-
- if (params.length == 0) {
- result = tcMethod.method.invoke(tcMethod.handler);
- } else if (params.length == 1) {
- Object resolvedSender = senderResolver.resolve(sender, params[0].getType());
- result = tcMethod.method.invoke(tcMethod.handler, resolvedSender);
- } else {
- Object resolvedSender = senderResolver.resolve(sender, params[0].getType());
- String current = !args.isEmpty() ? args.getLast() : "";
- result = tcMethod.method.invoke(tcMethod.handler, resolvedSender, current);
- }
-
+ Object result = invokeTabCompleter(tcMethod, sender, args);
return (List) result;
} catch (Exception e) {
- throw new RuntimeException("Failed to invoke tab completer", e);
+ throw new RuntimeException(
+ "Failed to invoke tab completer for command '" + commandPath +
+ "', argument '" + argName + "', method '" + tcMethod.method.getName() + "'", e);
}
};
}
+ private Object invokeTabCompleter(TabCompleterMethod tcMethod, S sender, List args) throws Exception {
+ Parameter[] params = tcMethod.method.getParameters();
+
+ if (params.length == 0) {
+ return tcMethod.method.invoke(tcMethod.handler);
+ }
+
+ Object resolvedSender = senderResolver.resolve(sender, params[0].getType());
+ if (params.length == 1) {
+ return tcMethod.method.invoke(tcMethod.handler, resolvedSender);
+ }
+
+ String current = !args.isEmpty() ? args.getLast() : "";
+ return tcMethod.method.invoke(tcMethod.handler, resolvedSender, current);
+ }
+
private void invokeMethod(Object handler, Method method, S sender, Arguments args) {
try {
Parameter[] params = method.getParameters();
- Object[] invokeArgs = new Object[params.length];
-
- for (int i = 0; i < params.length; i++) {
- Parameter param = params[i];
- Class> paramType = param.getType();
- boolean isOptional = paramType == Optional.class;
-
- // First param: sender
- if (i == 0) {
- Class> senderType = isOptional ? extractOptionalType(param) : paramType;
- if (senderResolver.canResolve(senderType)) {
- Object resolved = senderResolver.resolve(sender, senderType);
- invokeArgs[i] = isOptional ? Optional.ofNullable(resolved) : resolved;
- continue;
- }
- }
-
- // Other params: @Arg
- Arg argAnnotation = param.getAnnotation(Arg.class);
- if (argAnnotation != null) {
- String argName = argAnnotation.value();
-
- if (isOptional) {
- invokeArgs[i] = args.getOptional(argName);
- } else {
- invokeArgs[i] = args.get(argName);
- }
- }
- }
-
+ Object[] invokeArgs = buildInvokeArgs(params, sender, args);
method.invoke(handler, invokeArgs);
-
} catch (Exception e) {
throw new RuntimeException("Failed to invoke command method: " + method.getName(), e);
}
}
+ private Object[] buildInvokeArgs(Parameter[] params, S sender, Arguments args) {
+ Object[] invokeArgs = new Object[params.length];
+
+ for (int i = 0; i < params.length; i++) {
+ Parameter param = params[i];
+
+ if (i == 0 && isSenderParameter(param)) {
+ invokeArgs[i] = resolveSender(param, sender);
+ } else {
+ invokeArgs[i] = resolveArgument(param, args);
+ }
+ }
+ return invokeArgs;
+ }
+
+ private Object resolveSender(Parameter param, S sender) {
+ Class> paramType = param.getType();
+ boolean isOptional = paramType == Optional.class;
+ Class> senderType = isOptional ? extractOptionalType(param) : paramType;
+ Object resolved = senderResolver.resolve(sender, senderType);
+ return isOptional ? Optional.ofNullable(resolved) : resolved;
+ }
+
+ private Object resolveArgument(Parameter param, Arguments args) {
+ String argName = getArgumentName(param);
+ boolean isOptional = param.getType() == Optional.class;
+ return isOptional ? args.getOptional(argName) : args.get(argName);
+ }
+
private record CommandMethodInfo(Object handler, Method method, String name) {}
private record TabCompleterMethod(Object handler, Method method) {}
}
\ No newline at end of file
diff --git a/annotations-addon/src/test/java/fr/traqueur/commands/annotations/AnnotationCommandProcessorTest.java b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/AnnotationCommandProcessorTest.java
index cd3ddc9..2314706 100644
--- a/annotations-addon/src/test/java/fr/traqueur/commands/annotations/AnnotationCommandProcessorTest.java
+++ b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/AnnotationCommandProcessorTest.java
@@ -363,16 +363,17 @@ void shouldThrowWhenMissingCommandContainer() {
}
@Test
- @DisplayName("should throw when parameter is missing @Arg annotation")
- void shouldThrowWhenMissingArgAnnotation() {
- InvalidContainerMissingArg invalid = new InvalidContainerMissingArg();
-
- IllegalArgumentException ex = assertThrows(
- IllegalArgumentException.class,
- () -> processor.register(invalid)
- );
-
- assertTrue(ex.getMessage().contains("@Arg"));
+ @DisplayName("should use parameter name when @Arg is missing")
+ void shouldUseParameterNameWhenArgMissing() {
+ InvalidContainerMissingArg container = new InvalidContainerMissingArg();
+
+ // Should not throw - @Arg is optional, uses parameter name instead
+ List> commands = processor.register(container);
+
+ assertEquals(1, commands.size());
+ Command