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

feat(annotations): add @CommandMethod annotation processing #366

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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