From 8cfc80e525667b4e87ade18cb1cb3adc80d54387 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Alexander=20S=C3=B6derberg?=
<4096670+Citymonstret@users.noreply.github.com>
Date: Tue, 31 May 2022 21:06:15 +0200
Subject: [PATCH] feat(annotations): add `@CommandMethod` annotation processing
(#366)
We now verify the following at compile time:
- That `@CommandMethod` annotated methods are non-static (error)
- That `@CommandMethod` annotated methods are public (warning)
- That the `@CommandMethod` syntax and specified `@Argument`s match
- That no optional argument precedes a required argument
---
.checkstyle/checkstyle-suppressions.xml | 1 +
CHANGELOG.md | 3 +-
.../annotations/ArgumentMode.java | 7 +-
.../annotations/CommandMethod.java | 1 +
.../annotations/SyntaxFragment.java | 30 ++-
.../annotations/SyntaxParser.java | 7 +-
.../processing/CommandMethodProcessor.java | 67 ++++++
.../processing/CommandMethodVisitor.java | 191 ++++++++++++++++++
.../javax.annotation.processing.Processor | 1 +
.../CommandMethodProcessorTest.java | 124 ++++++++++++
.../src/test/resources/TestCommandMethod.java | 13 ++
.../TestCommandMethodMissingArgument.java | 11 +
.../TestCommandMethodMissingSyntax.java | 12 ++
...stCommandMethodOptionalBeforeRequired.java | 12 ++
.../resources/TestCommandMethodPrivate.java | 12 ++
.../resources/TestCommandMethodStatic.java | 12 ++
.../examples/bukkit/ExamplePlugin.java | 10 +-
17 files changed, 500 insertions(+), 14 deletions(-)
create mode 100644 cloud-annotations/src/main/java/cloud/commandframework/annotations/processing/CommandMethodProcessor.java
create mode 100644 cloud-annotations/src/main/java/cloud/commandframework/annotations/processing/CommandMethodVisitor.java
create mode 100644 cloud-annotations/src/test/java/cloud/commandframework/annotations/processing/CommandMethodProcessorTest.java
create mode 100644 cloud-annotations/src/test/resources/TestCommandMethod.java
create mode 100644 cloud-annotations/src/test/resources/TestCommandMethodMissingArgument.java
create mode 100644 cloud-annotations/src/test/resources/TestCommandMethodMissingSyntax.java
create mode 100644 cloud-annotations/src/test/resources/TestCommandMethodOptionalBeforeRequired.java
create mode 100644 cloud-annotations/src/test/resources/TestCommandMethodPrivate.java
create mode 100644 cloud-annotations/src/test/resources/TestCommandMethodStatic.java
diff --git a/.checkstyle/checkstyle-suppressions.xml b/.checkstyle/checkstyle-suppressions.xml
index ebc790d1f..ab9f5b2e8 100644
--- a/.checkstyle/checkstyle-suppressions.xml
+++ b/.checkstyle/checkstyle-suppressions.xml
@@ -5,4 +5,5 @@
+
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2985a03ee..608975ba4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,7 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Core: Add delegating command execution handlers ([#363](https://github.com/Incendo/cloud/pull/363))
- Core: Add `builder()` getter to `Command.Builder` ([#363](https://github.com/Incendo/cloud/pull/363))
- Annotations: Annotation string processors ([#353](https://github.com/Incendo/cloud/pull/353))
-- Annotations: `@CommandContainer` annotation processing
+- Annotations: `@CommandContainer` annotation processing ([#364](https://github.com/Incendo/cloud/pull/364))
+- Annotations: `@CommandMethod` annotation processing for compile-time validation ([#365](https://github.com/Incendo/cloud/pull/365))
### Fixed
- Core: Fix missing caption registration for the regex caption ([#351](https://github.com/Incendo/cloud/pull/351))
diff --git a/cloud-annotations/src/main/java/cloud/commandframework/annotations/ArgumentMode.java b/cloud-annotations/src/main/java/cloud/commandframework/annotations/ArgumentMode.java
index d3cdcac85..38ed2f311 100644
--- a/cloud-annotations/src/main/java/cloud/commandframework/annotations/ArgumentMode.java
+++ b/cloud-annotations/src/main/java/cloud/commandframework/annotations/ArgumentMode.java
@@ -23,7 +23,12 @@
//
package cloud.commandframework.annotations;
-enum ArgumentMode {
+/**
+ * The mode of an argument.
+ *
+ * Public since 1.7.0.
+ */
+public enum ArgumentMode {
LITERAL,
OPTIONAL,
REQUIRED
diff --git a/cloud-annotations/src/main/java/cloud/commandframework/annotations/CommandMethod.java b/cloud-annotations/src/main/java/cloud/commandframework/annotations/CommandMethod.java
index 5aedca26a..ca8313312 100644
--- a/cloud-annotations/src/main/java/cloud/commandframework/annotations/CommandMethod.java
+++ b/cloud-annotations/src/main/java/cloud/commandframework/annotations/CommandMethod.java
@@ -35,6 +35,7 @@
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface CommandMethod {
+ String ANNOTATION_PATH = "cloud.commandframework.annotations.CommandMethod";
/**
* Command syntax
diff --git a/cloud-annotations/src/main/java/cloud/commandframework/annotations/SyntaxFragment.java b/cloud-annotations/src/main/java/cloud/commandframework/annotations/SyntaxFragment.java
index 96498825e..3252204d5 100644
--- a/cloud-annotations/src/main/java/cloud/commandframework/annotations/SyntaxFragment.java
+++ b/cloud-annotations/src/main/java/cloud/commandframework/annotations/SyntaxFragment.java
@@ -26,7 +26,10 @@
import java.util.List;
import org.checkerframework.checker.nullness.qual.NonNull;
-final class SyntaxFragment {
+/**
+ * Public since 1.7.0.
+ */
+public final class SyntaxFragment {
private final String major;
private final List minor;
@@ -42,15 +45,34 @@ final class SyntaxFragment {
this.argumentMode = argumentMode;
}
- @NonNull String getMajor() {
+ /**
+ * Returns the major portion of the fragment.
+ *
+ * This is likely the name of an argument, or a string literal.
+ *
+ * @return the major part of the fragment
+ */
+ public @NonNull String getMajor() {
return this.major;
}
- @NonNull List<@NonNull String> getMinor() {
+ /**
+ * Returns the minor part of the fragment.
+ *
+ * This is likely a list of aliases.
+ *
+ * @return the minor part of the fragment.
+ */
+ public @NonNull List<@NonNull String> getMinor() {
return this.minor;
}
- @NonNull ArgumentMode getArgumentMode() {
+ /**
+ * Returns the argument mode.
+ *
+ * @return the argument mode
+ */
+ public @NonNull ArgumentMode getArgumentMode() {
return this.argumentMode;
}
diff --git a/cloud-annotations/src/main/java/cloud/commandframework/annotations/SyntaxParser.java b/cloud-annotations/src/main/java/cloud/commandframework/annotations/SyntaxParser.java
index db35638b6..a6a26e5cf 100644
--- a/cloud-annotations/src/main/java/cloud/commandframework/annotations/SyntaxParser.java
+++ b/cloud-annotations/src/main/java/cloud/commandframework/annotations/SyntaxParser.java
@@ -33,9 +33,11 @@
import org.checkerframework.checker.nullness.qual.NonNull;
/**
- * Parses command syntax into syntax fragments
+ * Parses command syntax into syntax fragments.
+ *
+ * Public since 1.7.0.
*/
-final class SyntaxParser implements Function<@NonNull String, @NonNull List<@NonNull SyntaxFragment>> {
+public final class SyntaxParser implements Function<@NonNull String, @NonNull List<@NonNull SyntaxFragment>> {
private static final Predicate PATTERN_ARGUMENT_LITERAL = Pattern.compile("([A-Za-z0-9\\-_]+)(|([A-Za-z0-9\\-_]+))*")
.asPredicate();
@@ -72,5 +74,4 @@ final class SyntaxParser implements Function<@NonNull String, @NonNull List<@Non
}
return syntaxFragments;
}
-
}
diff --git a/cloud-annotations/src/main/java/cloud/commandframework/annotations/processing/CommandMethodProcessor.java b/cloud-annotations/src/main/java/cloud/commandframework/annotations/processing/CommandMethodProcessor.java
new file mode 100644
index 000000000..042a6f518
--- /dev/null
+++ b/cloud-annotations/src/main/java/cloud/commandframework/annotations/processing/CommandMethodProcessor.java
@@ -0,0 +1,67 @@
+//
+// MIT License
+//
+// Copyright (c) 2021 Alexander Söderberg & Contributors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+package cloud.commandframework.annotations.processing;
+
+import cloud.commandframework.annotations.CommandMethod;
+import java.util.Set;
+import javax.annotation.processing.AbstractProcessor;
+import javax.annotation.processing.RoundEnvironment;
+import javax.annotation.processing.SupportedAnnotationTypes;
+import javax.lang.model.SourceVersion;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.ElementKind;
+import javax.lang.model.element.TypeElement;
+
+@SupportedAnnotationTypes(CommandMethod.ANNOTATION_PATH)
+public final class CommandMethodProcessor extends AbstractProcessor {
+
+ @Override
+ public boolean process(
+ final Set extends TypeElement> annotations,
+ final RoundEnvironment roundEnv
+ ) {
+ final Set extends Element> elements = roundEnv.getElementsAnnotatedWith(CommandMethod.class);
+ if (elements.isEmpty()) {
+ return false; // Nothing to process...
+ }
+
+ for (final Element element : elements) {
+ if (element.getKind() != ElementKind.METHOD) {
+ // @CommandMethod can also be used on classes, but there's
+ // essentially nothing to process there...
+ continue;
+ }
+
+ element.accept(new CommandMethodVisitor(this.processingEnv), null);
+ }
+
+ // https://errorprone.info/bugpattern/DoNotClaimAnnotations
+ return false;
+ }
+
+ @Override
+ public SourceVersion getSupportedSourceVersion() {
+ return SourceVersion.latest();
+ }
+}
diff --git a/cloud-annotations/src/main/java/cloud/commandframework/annotations/processing/CommandMethodVisitor.java b/cloud-annotations/src/main/java/cloud/commandframework/annotations/processing/CommandMethodVisitor.java
new file mode 100644
index 000000000..a659857df
--- /dev/null
+++ b/cloud-annotations/src/main/java/cloud/commandframework/annotations/processing/CommandMethodVisitor.java
@@ -0,0 +1,191 @@
+//
+// MIT License
+//
+// Copyright (c) 2021 Alexander Söderberg & Contributors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+package cloud.commandframework.annotations.processing;
+
+import cloud.commandframework.annotations.Argument;
+import cloud.commandframework.annotations.ArgumentMode;
+import cloud.commandframework.annotations.CommandMethod;
+import cloud.commandframework.annotations.SyntaxFragment;
+import cloud.commandframework.annotations.SyntaxParser;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+import javax.annotation.processing.ProcessingEnvironment;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.ElementVisitor;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.Modifier;
+import javax.lang.model.element.PackageElement;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.element.TypeParameterElement;
+import javax.lang.model.element.VariableElement;
+import javax.tools.Diagnostic;
+import org.checkerframework.checker.nullness.qual.NonNull;
+
+class CommandMethodVisitor implements ElementVisitor {
+
+ private final ProcessingEnvironment processingEnvironment;
+ private final SyntaxParser syntaxParser;
+
+ CommandMethodVisitor(final @NonNull ProcessingEnvironment processingEnvironment) {
+ this.processingEnvironment = processingEnvironment;
+ this.syntaxParser = new SyntaxParser();
+ }
+
+ @Override
+ public Void visit(final Element e) {
+ return this.visit(e, null);
+ }
+
+ @Override
+ public Void visit(final Element e, final Void unused) {
+ return null;
+ }
+
+ @Override
+ public Void visitPackage(final PackageElement e, final Void unused) {
+ return null;
+ }
+
+ @Override
+ public Void visitType(final TypeElement e, final Void unused) {
+ return null;
+ }
+
+ @Override
+ public Void visitVariable(final VariableElement e, final Void unused) {
+ return null;
+ }
+
+ @Override
+ public Void visitExecutable(final ExecutableElement e, final Void unused) {
+ if (!e.getModifiers().contains(Modifier.PUBLIC)) {
+ this.processingEnvironment.getMessager().printMessage(
+ Diagnostic.Kind.WARNING,
+ String.format(
+ "@CommandMethod annotated methods should be public (%s)",
+ e.getSimpleName()
+ ),
+ e
+ );
+ }
+
+ if (e.getModifiers().contains(Modifier.STATIC)) {
+ this.processingEnvironment.getMessager().printMessage(
+ Diagnostic.Kind.ERROR,
+ String.format(
+ "@CommandMethod annotated methods should be non-static (%s)",
+ e.getSimpleName()
+ ),
+ e
+ );
+ }
+
+ if (e.getReturnType().toString().equals("Void")) {
+ this.processingEnvironment.getMessager().printMessage(
+ Diagnostic.Kind.ERROR,
+ String.format(
+ "@CommandMethod annotated methods should return void (%s)",
+ e.getSimpleName()
+ ),
+ e
+ );
+ }
+
+ final CommandMethod commandMethod = e.getAnnotation(CommandMethod.class);
+ final List parameterArgumentNames = e.getParameters()
+ .stream()
+ .map(parameter -> parameter.getAnnotation(Argument.class))
+ .filter(Objects::nonNull)
+ .map(Argument::value)
+ .collect(Collectors.toList());
+ final List parsedArgumentNames = new ArrayList<>(parameterArgumentNames.size());
+
+ final List syntaxFragments = this.syntaxParser.apply(commandMethod.value());
+
+ boolean foundOptional = false;
+ for (final SyntaxFragment fragment : syntaxFragments) {
+ if (fragment.getArgumentMode() == ArgumentMode.LITERAL) {
+ continue;
+ }
+
+ if (!parameterArgumentNames.contains(fragment.getMajor())) {
+ this.processingEnvironment.getMessager().printMessage(
+ Diagnostic.Kind.ERROR,
+ String.format(
+ "@Argument(\"%s\") is missing from @CommandMethod (%s)",
+ fragment.getMajor(),
+ e.getSimpleName()
+ ),
+ e
+ );
+ }
+
+ if (fragment.getArgumentMode() == ArgumentMode.REQUIRED) {
+ if (foundOptional) {
+ this.processingEnvironment.getMessager().printMessage(
+ Diagnostic.Kind.ERROR,
+ String.format(
+ "Required argument '%s' cannot succeed an optional argument (%s)",
+ fragment.getMajor(),
+ e.getSimpleName()
+ ),
+ e
+ );
+ }
+ } else {
+ foundOptional = true;
+ }
+
+ parsedArgumentNames.add(fragment.getMajor());
+ }
+
+ for (final String argument : parameterArgumentNames) {
+ if (!parsedArgumentNames.contains(argument)) {
+ this.processingEnvironment.getMessager().printMessage(
+ Diagnostic.Kind.ERROR,
+ String.format(
+ "Argument '%s' is missing from the @CommandMethod syntax (%s)",
+ argument,
+ e.getSimpleName()
+ ),
+ e
+ );
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ public Void visitTypeParameter(final TypeParameterElement e, final Void unused) {
+ return null;
+ }
+
+ @Override
+ public Void visitUnknown(final Element e, final Void unused) {
+ return null;
+ }
+}
diff --git a/cloud-annotations/src/main/resources/META-INF/services/javax.annotation.processing.Processor b/cloud-annotations/src/main/resources/META-INF/services/javax.annotation.processing.Processor
index 1210878b9..30727d06c 100644
--- a/cloud-annotations/src/main/resources/META-INF/services/javax.annotation.processing.Processor
+++ b/cloud-annotations/src/main/resources/META-INF/services/javax.annotation.processing.Processor
@@ -1 +1,2 @@
cloud.commandframework.annotations.processing.CommandContainerProcessor
+cloud.commandframework.annotations.processing.CommandMethodProcessor
diff --git a/cloud-annotations/src/test/java/cloud/commandframework/annotations/processing/CommandMethodProcessorTest.java b/cloud-annotations/src/test/java/cloud/commandframework/annotations/processing/CommandMethodProcessorTest.java
new file mode 100644
index 000000000..1ca25bcc4
--- /dev/null
+++ b/cloud-annotations/src/test/java/cloud/commandframework/annotations/processing/CommandMethodProcessorTest.java
@@ -0,0 +1,124 @@
+//
+// MIT License
+//
+// Copyright (c) 2021 Alexander Söderberg & Contributors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+package cloud.commandframework.annotations.processing;
+
+import com.google.testing.compile.Compilation;
+import com.google.testing.compile.Compiler;
+import com.google.testing.compile.JavaFileObjects;
+import org.junit.jupiter.api.Test;
+
+import static com.google.testing.compile.CompilationSubject.assertThat;
+import static com.google.testing.compile.Compiler.javac;
+
+public class CommandMethodProcessorTest {
+
+ @Test
+ void testValidCommandMethodParsing() {
+ // Arrange
+ final Compiler compiler = javac().withProcessors(new CommandMethodProcessor());
+
+ // Act
+ final Compilation compilation = compiler.compile(
+ JavaFileObjects.forResource("TestCommandMethod.java")
+ );
+
+ // Assert
+ assertThat(compilation).succeededWithoutWarnings();
+ }
+
+ @Test
+ void testStaticCommandMethodParsing() {
+ // Arrange
+ final Compiler compiler = javac().withProcessors(new CommandMethodProcessor());
+
+ // Act
+ final Compilation compilation = compiler.compile(
+ JavaFileObjects.forResource("TestCommandMethodStatic.java")
+ );
+
+ // Assert
+ assertThat(compilation).failed();
+ assertThat(compilation).hadErrorContaining("@CommandMethod annotated methods should be non-static (commandMethod)");
+ }
+
+ @Test
+ void testOptionalBeforeRequiredParsing() {
+ // Arrange
+ final Compiler compiler = javac().withProcessors(new CommandMethodProcessor());
+
+ // Act
+ final Compilation compilation = compiler.compile(
+ JavaFileObjects.forResource("TestCommandMethodOptionalBeforeRequired.java")
+ );
+
+ // Assert
+ assertThat(compilation).failed();
+ assertThat(compilation).hadErrorContaining("Required argument 'required' cannot succeed an optional argument (commandMethod)");
+ }
+
+ @Test
+ void testPrivateCommandMethodParsing() {
+ // Arrange
+ final Compiler compiler = javac().withProcessors(new CommandMethodProcessor());
+
+ // Act
+ final Compilation compilation = compiler.compile(
+ JavaFileObjects.forResource("TestCommandMethodPrivate.java")
+ );
+
+ // Assert
+ assertThat(compilation).succeeded();
+ assertThat(compilation).hadWarningContaining("@CommandMethod annotated methods should be public (commandMethod)");
+ }
+
+ @Test
+ void testCommandMethodMissingArgumentParsing() {
+ // Arrange
+ final Compiler compiler = javac().withProcessors(new CommandMethodProcessor());
+
+ // Act
+ final Compilation compilation = compiler.compile(
+ JavaFileObjects.forResource("TestCommandMethodMissingArgument.java")
+ );
+
+ // Assert
+ assertThat(compilation).failed();
+ assertThat(compilation).hadErrorContaining("@Argument(\"required\") is missing from @CommandMethod (commandMethod)");
+ }
+
+ @Test
+ void testCommandMethodMissingSyntaxParsing() {
+ // Arrange
+ final Compiler compiler = javac().withProcessors(new CommandMethodProcessor());
+
+ // Act
+ final Compilation compilation = compiler.compile(
+ JavaFileObjects.forResource("TestCommandMethodMissingSyntax.java")
+ );
+
+ // Assert
+ assertThat(compilation).failed();
+ assertThat(compilation).hadErrorContaining("Argument 'optional' is missing from the @CommandMethod syntax (commandMethod)");
+ }
+}
diff --git a/cloud-annotations/src/test/resources/TestCommandMethod.java b/cloud-annotations/src/test/resources/TestCommandMethod.java
new file mode 100644
index 000000000..183f1dc11
--- /dev/null
+++ b/cloud-annotations/src/test/resources/TestCommandMethod.java
@@ -0,0 +1,13 @@
+import cloud.commandframework.annotations.Argument;
+import cloud.commandframework.annotations.CommandMethod;
+
+public class TestCommandMethod {
+
+ @CommandMethod("command [optional]")
+ public void commandMethod(
+ final Object sender,
+ @Argument("required") final String required,
+ @Argument("optional") final String optional
+ ) {
+ }
+}
diff --git a/cloud-annotations/src/test/resources/TestCommandMethodMissingArgument.java b/cloud-annotations/src/test/resources/TestCommandMethodMissingArgument.java
new file mode 100644
index 000000000..577a40be3
--- /dev/null
+++ b/cloud-annotations/src/test/resources/TestCommandMethodMissingArgument.java
@@ -0,0 +1,11 @@
+import cloud.commandframework.annotations.Argument;
+import cloud.commandframework.annotations.CommandMethod;
+
+public class TestCommandMethodMissingArgument {
+
+ @CommandMethod("command [optional]")
+ public void commandMethod(
+ @Argument("optional") final String optional
+ ) {
+ }
+}
diff --git a/cloud-annotations/src/test/resources/TestCommandMethodMissingSyntax.java b/cloud-annotations/src/test/resources/TestCommandMethodMissingSyntax.java
new file mode 100644
index 000000000..8853c47a4
--- /dev/null
+++ b/cloud-annotations/src/test/resources/TestCommandMethodMissingSyntax.java
@@ -0,0 +1,12 @@
+import cloud.commandframework.annotations.Argument;
+import cloud.commandframework.annotations.CommandMethod;
+
+public class TestCommandMethodMissingSyntax {
+
+ @CommandMethod("command ")
+ public void commandMethod(
+ @Argument("required") final String required,
+ @Argument("optional") final String optional
+ ) {
+ }
+}
diff --git a/cloud-annotations/src/test/resources/TestCommandMethodOptionalBeforeRequired.java b/cloud-annotations/src/test/resources/TestCommandMethodOptionalBeforeRequired.java
new file mode 100644
index 000000000..2890dd68c
--- /dev/null
+++ b/cloud-annotations/src/test/resources/TestCommandMethodOptionalBeforeRequired.java
@@ -0,0 +1,12 @@
+import cloud.commandframework.annotations.Argument;
+import cloud.commandframework.annotations.CommandMethod;
+
+public class TestCommandMethodOptionalBeforeRequired {
+
+ @CommandMethod("command [optional] ")
+ public void commandMethod(
+ @Argument("required") final String required,
+ @Argument("optional") final String optional
+ ) {
+ }
+}
diff --git a/cloud-annotations/src/test/resources/TestCommandMethodPrivate.java b/cloud-annotations/src/test/resources/TestCommandMethodPrivate.java
new file mode 100644
index 000000000..1ae616930
--- /dev/null
+++ b/cloud-annotations/src/test/resources/TestCommandMethodPrivate.java
@@ -0,0 +1,12 @@
+import cloud.commandframework.annotations.Argument;
+import cloud.commandframework.annotations.CommandMethod;
+
+public class TestCommandMethodPrivate {
+
+ @CommandMethod("command [optional]")
+ private void commandMethod(
+ @Argument("required") final String required,
+ @Argument("optional") final String optional
+ ) {
+ }
+}
diff --git a/cloud-annotations/src/test/resources/TestCommandMethodStatic.java b/cloud-annotations/src/test/resources/TestCommandMethodStatic.java
new file mode 100644
index 000000000..e1efbf3d2
--- /dev/null
+++ b/cloud-annotations/src/test/resources/TestCommandMethodStatic.java
@@ -0,0 +1,12 @@
+import cloud.commandframework.annotations.Argument;
+import cloud.commandframework.annotations.CommandMethod;
+
+public class TestCommandMethodStatic {
+
+ @CommandMethod("command [optional]")
+ public static void commandMethod(
+ @Argument("required") final String required,
+ @Argument("optional") final String optional
+ ) {
+ }
+}
diff --git a/examples/example-bukkit/src/main/java/cloud/commandframework/examples/bukkit/ExamplePlugin.java b/examples/example-bukkit/src/main/java/cloud/commandframework/examples/bukkit/ExamplePlugin.java
index ac7fdcd14..4ab5bed04 100644
--- a/examples/example-bukkit/src/main/java/cloud/commandframework/examples/bukkit/ExamplePlugin.java
+++ b/examples/example-bukkit/src/main/java/cloud/commandframework/examples/bukkit/ExamplePlugin.java
@@ -456,7 +456,7 @@ private void constructCommands() {
@CommandMethod("example help [query]")
@CommandDescription("Help menu")
- private void commandHelp(
+ public void commandHelp(
final @NonNull CommandSender sender,
final @Argument("query") @Greedy String query
) {
@@ -467,7 +467,7 @@ private void commandHelp(
@CommandMethod("example clear")
@CommandDescription("Clear your inventory")
@CommandPermission("example.clear")
- private void commandClear(final @NonNull Player player) {
+ public void commandClear(final @NonNull Player player) {
player.getInventory().clear();
this.bukkitAudiences.player(player)
.sendMessage(Identity.nil(), text("Your inventory has been cleared", NamedTextColor.GOLD));
@@ -475,7 +475,7 @@ private void commandClear(final @NonNull Player player) {
@CommandMethod("example give ")
@CommandDescription("Give yourself an item")
- private void commandGive(
+ public void commandGive(
final @NonNull Player player,
final @NonNull @Argument("material") Material material,
final @Argument("amount") int number,
@@ -507,7 +507,7 @@ private void commandGive(
@CommandMethod("example pay ")
@CommandDescription("Command to test the preprocessing system")
- private void commandPay(
+ public void commandPay(
final @NonNull CommandSender sender,
final @Argument("money") @Regex(value = "(?=.*?\\d)^\\$?(([1-9]\\d{0,2}(,\\d{3})*)|\\d+)?(\\.\\d{1,2})?$",
failureCaption = "regex.money") String money
@@ -520,7 +520,7 @@ private void commandPay(
}
@CommandMethod("example teleport complex ")
- private void teleportComplex(
+ public void teleportComplex(
final @NonNull Player sender,
final @NonNull @Argument("location") Location location
) {