Skip to content

Commit

Permalink
feat(annotations): add @CommandMethod annotation processing (#366)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Citymonstret committed Jun 3, 2022
1 parent ab849ff commit 8cfc80e
Show file tree
Hide file tree
Showing 17 changed files with 500 additions and 14 deletions.
1 change: 1 addition & 0 deletions .checkstyle/checkstyle-suppressions.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@
<suppressions>
<suppress checks="(?:(?:Member|Method)Name|DesignForExtension|Javadoc.*)" files=".*[\\/]mixin[\\/].*"/>
<suppress checks="(?:Javadoc.*)" files=".*[\\/]bukkit[\\/]internal[\\/].*"/>
<suppress checks="(?:Javadoc.*)" files=".*[\\/]example-.*[\\/].*"/>
</suppressions>
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,12 @@
//
package cloud.commandframework.annotations;

enum ArgumentMode {
/**
* The mode of an argument.
* <p>
* Public since 1.7.0.
*/
public enum ArgumentMode {
LITERAL,
OPTIONAL,
REQUIRED
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface CommandMethod {
String ANNOTATION_PATH = "cloud.commandframework.annotations.CommandMethod";

/**
* Command syntax
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> minor;
Expand All @@ -42,15 +45,34 @@ final class SyntaxFragment {
this.argumentMode = argumentMode;
}

@NonNull String getMajor() {
/**
* Returns the major portion of the fragment.
* <p>
* 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.
* <p>
* 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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,11 @@
import org.checkerframework.checker.nullness.qual.NonNull;

/**
* Parses command syntax into syntax fragments
* Parses command syntax into syntax fragments.
* <p>
* 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<String> PATTERN_ARGUMENT_LITERAL = Pattern.compile("([A-Za-z0-9\\-_]+)(|([A-Za-z0-9\\-_]+))*")
.asPredicate();
Expand Down Expand Up @@ -72,5 +74,4 @@ final class SyntaxParser implements Function<@NonNull String, @NonNull List<@Non
}
return syntaxFragments;
}

}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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<Void, Void> {

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<String> parameterArgumentNames = e.getParameters()
.stream()
.map(parameter -> parameter.getAnnotation(Argument.class))
.filter(Objects::nonNull)
.map(Argument::value)
.collect(Collectors.toList());
final List<String> parsedArgumentNames = new ArrayList<>(parameterArgumentNames.size());

final List<SyntaxFragment> 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;
}
}
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
cloud.commandframework.annotations.processing.CommandContainerProcessor
cloud.commandframework.annotations.processing.CommandMethodProcessor
Loading

0 comments on commit 8cfc80e

Please sign in to comment.