From 8db995b549c02f8a62b38110c4b4ca824273959d Mon Sep 17 00:00:00 2001 From: Citymonstret Date: Tue, 31 May 2022 20:16:57 +0200 Subject: [PATCH] feat(annotations): add `@CommandMethod` annotation processing 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 annotations, + final RoundEnvironment roundEnv + ) { + final Set 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 ) {