Skip to content

Commit 1ff3b7a

Browse files
authored
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
1 parent 8a7dcd2 commit 1ff3b7a

File tree

17 files changed

+500
-14
lines changed

17 files changed

+500
-14
lines changed

.checkstyle/checkstyle-suppressions.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@
55
<suppressions>
66
<suppress checks="(?:(?:Member|Method)Name|DesignForExtension|Javadoc.*)" files=".*[\\/]mixin[\\/].*"/>
77
<suppress checks="(?:Javadoc.*)" files=".*[\\/]bukkit[\\/]internal[\\/].*"/>
8+
<suppress checks="(?:Javadoc.*)" files=".*[\\/]example-.*[\\/].*"/>
89
</suppressions>

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
- Core: Add delegating command execution handlers ([#363](https://github.com/Incendo/cloud/pull/363))
1313
- Core: Add `builder()` getter to `Command.Builder` ([#363](https://github.com/Incendo/cloud/pull/363))
1414
- Annotations: Annotation string processors ([#353](https://github.com/Incendo/cloud/pull/353))
15-
- Annotations: `@CommandContainer` annotation processing
15+
- Annotations: `@CommandContainer` annotation processing ([#364](https://github.com/Incendo/cloud/pull/364))
16+
- Annotations: `@CommandMethod` annotation processing for compile-time validation ([#365](https://github.com/Incendo/cloud/pull/365))
1617

1718
### Fixed
1819
- Core: Fix missing caption registration for the regex caption ([#351](https://github.com/Incendo/cloud/pull/351))

cloud-annotations/src/main/java/cloud/commandframework/annotations/ArgumentMode.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,12 @@
2323
//
2424
package cloud.commandframework.annotations;
2525

26-
enum ArgumentMode {
26+
/**
27+
* The mode of an argument.
28+
* <p>
29+
* Public since 1.7.0.
30+
*/
31+
public enum ArgumentMode {
2732
LITERAL,
2833
OPTIONAL,
2934
REQUIRED

cloud-annotations/src/main/java/cloud/commandframework/annotations/CommandMethod.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
@Retention(RetentionPolicy.RUNTIME)
3636
@Target({ElementType.METHOD, ElementType.TYPE})
3737
public @interface CommandMethod {
38+
String ANNOTATION_PATH = "cloud.commandframework.annotations.CommandMethod";
3839

3940
/**
4041
* Command syntax

cloud-annotations/src/main/java/cloud/commandframework/annotations/SyntaxFragment.java

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@
2626
import java.util.List;
2727
import org.checkerframework.checker.nullness.qual.NonNull;
2828

29-
final class SyntaxFragment {
29+
/**
30+
* Public since 1.7.0.
31+
*/
32+
public final class SyntaxFragment {
3033

3134
private final String major;
3235
private final List<String> minor;
@@ -42,15 +45,34 @@ final class SyntaxFragment {
4245
this.argumentMode = argumentMode;
4346
}
4447

45-
@NonNull String getMajor() {
48+
/**
49+
* Returns the major portion of the fragment.
50+
* <p>
51+
* This is likely the name of an argument, or a string literal.
52+
*
53+
* @return the major part of the fragment
54+
*/
55+
public @NonNull String getMajor() {
4656
return this.major;
4757
}
4858

49-
@NonNull List<@NonNull String> getMinor() {
59+
/**
60+
* Returns the minor part of the fragment.
61+
* <p>
62+
* This is likely a list of aliases.
63+
*
64+
* @return the minor part of the fragment.
65+
*/
66+
public @NonNull List<@NonNull String> getMinor() {
5067
return this.minor;
5168
}
5269

53-
@NonNull ArgumentMode getArgumentMode() {
70+
/**
71+
* Returns the argument mode.
72+
*
73+
* @return the argument mode
74+
*/
75+
public @NonNull ArgumentMode getArgumentMode() {
5476
return this.argumentMode;
5577
}
5678

cloud-annotations/src/main/java/cloud/commandframework/annotations/SyntaxParser.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,11 @@
3333
import org.checkerframework.checker.nullness.qual.NonNull;
3434

3535
/**
36-
* Parses command syntax into syntax fragments
36+
* Parses command syntax into syntax fragments.
37+
* <p>
38+
* Public since 1.7.0.
3739
*/
38-
final class SyntaxParser implements Function<@NonNull String, @NonNull List<@NonNull SyntaxFragment>> {
40+
public final class SyntaxParser implements Function<@NonNull String, @NonNull List<@NonNull SyntaxFragment>> {
3941

4042
private static final Predicate<String> PATTERN_ARGUMENT_LITERAL = Pattern.compile("([A-Za-z0-9\\-_]+)(|([A-Za-z0-9\\-_]+))*")
4143
.asPredicate();
@@ -72,5 +74,4 @@ final class SyntaxParser implements Function<@NonNull String, @NonNull List<@Non
7274
}
7375
return syntaxFragments;
7476
}
75-
7677
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
//
2+
// MIT License
3+
//
4+
// Copyright (c) 2021 Alexander Söderberg & Contributors
5+
//
6+
// Permission is hereby granted, free of charge, to any person obtaining a copy
7+
// of this software and associated documentation files (the "Software"), to deal
8+
// in the Software without restriction, including without limitation the rights
9+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
// copies of the Software, and to permit persons to whom the Software is
11+
// furnished to do so, subject to the following conditions:
12+
//
13+
// The above copyright notice and this permission notice shall be included in all
14+
// copies or substantial portions of the Software.
15+
//
16+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
// SOFTWARE.
23+
//
24+
package cloud.commandframework.annotations.processing;
25+
26+
import cloud.commandframework.annotations.CommandMethod;
27+
import java.util.Set;
28+
import javax.annotation.processing.AbstractProcessor;
29+
import javax.annotation.processing.RoundEnvironment;
30+
import javax.annotation.processing.SupportedAnnotationTypes;
31+
import javax.lang.model.SourceVersion;
32+
import javax.lang.model.element.Element;
33+
import javax.lang.model.element.ElementKind;
34+
import javax.lang.model.element.TypeElement;
35+
36+
@SupportedAnnotationTypes(CommandMethod.ANNOTATION_PATH)
37+
public final class CommandMethodProcessor extends AbstractProcessor {
38+
39+
@Override
40+
public boolean process(
41+
final Set<? extends TypeElement> annotations,
42+
final RoundEnvironment roundEnv
43+
) {
44+
final Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(CommandMethod.class);
45+
if (elements.isEmpty()) {
46+
return false; // Nothing to process...
47+
}
48+
49+
for (final Element element : elements) {
50+
if (element.getKind() != ElementKind.METHOD) {
51+
// @CommandMethod can also be used on classes, but there's
52+
// essentially nothing to process there...
53+
continue;
54+
}
55+
56+
element.accept(new CommandMethodVisitor(this.processingEnv), null);
57+
}
58+
59+
// https://errorprone.info/bugpattern/DoNotClaimAnnotations
60+
return false;
61+
}
62+
63+
@Override
64+
public SourceVersion getSupportedSourceVersion() {
65+
return SourceVersion.latest();
66+
}
67+
}
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
//
2+
// MIT License
3+
//
4+
// Copyright (c) 2021 Alexander Söderberg & Contributors
5+
//
6+
// Permission is hereby granted, free of charge, to any person obtaining a copy
7+
// of this software and associated documentation files (the "Software"), to deal
8+
// in the Software without restriction, including without limitation the rights
9+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
// copies of the Software, and to permit persons to whom the Software is
11+
// furnished to do so, subject to the following conditions:
12+
//
13+
// The above copyright notice and this permission notice shall be included in all
14+
// copies or substantial portions of the Software.
15+
//
16+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
// SOFTWARE.
23+
//
24+
package cloud.commandframework.annotations.processing;
25+
26+
import cloud.commandframework.annotations.Argument;
27+
import cloud.commandframework.annotations.ArgumentMode;
28+
import cloud.commandframework.annotations.CommandMethod;
29+
import cloud.commandframework.annotations.SyntaxFragment;
30+
import cloud.commandframework.annotations.SyntaxParser;
31+
import java.util.ArrayList;
32+
import java.util.List;
33+
import java.util.Objects;
34+
import java.util.stream.Collectors;
35+
import javax.annotation.processing.ProcessingEnvironment;
36+
import javax.lang.model.element.Element;
37+
import javax.lang.model.element.ElementVisitor;
38+
import javax.lang.model.element.ExecutableElement;
39+
import javax.lang.model.element.Modifier;
40+
import javax.lang.model.element.PackageElement;
41+
import javax.lang.model.element.TypeElement;
42+
import javax.lang.model.element.TypeParameterElement;
43+
import javax.lang.model.element.VariableElement;
44+
import javax.tools.Diagnostic;
45+
import org.checkerframework.checker.nullness.qual.NonNull;
46+
47+
class CommandMethodVisitor implements ElementVisitor<Void, Void> {
48+
49+
private final ProcessingEnvironment processingEnvironment;
50+
private final SyntaxParser syntaxParser;
51+
52+
CommandMethodVisitor(final @NonNull ProcessingEnvironment processingEnvironment) {
53+
this.processingEnvironment = processingEnvironment;
54+
this.syntaxParser = new SyntaxParser();
55+
}
56+
57+
@Override
58+
public Void visit(final Element e) {
59+
return this.visit(e, null);
60+
}
61+
62+
@Override
63+
public Void visit(final Element e, final Void unused) {
64+
return null;
65+
}
66+
67+
@Override
68+
public Void visitPackage(final PackageElement e, final Void unused) {
69+
return null;
70+
}
71+
72+
@Override
73+
public Void visitType(final TypeElement e, final Void unused) {
74+
return null;
75+
}
76+
77+
@Override
78+
public Void visitVariable(final VariableElement e, final Void unused) {
79+
return null;
80+
}
81+
82+
@Override
83+
public Void visitExecutable(final ExecutableElement e, final Void unused) {
84+
if (!e.getModifiers().contains(Modifier.PUBLIC)) {
85+
this.processingEnvironment.getMessager().printMessage(
86+
Diagnostic.Kind.WARNING,
87+
String.format(
88+
"@CommandMethod annotated methods should be public (%s)",
89+
e.getSimpleName()
90+
),
91+
e
92+
);
93+
}
94+
95+
if (e.getModifiers().contains(Modifier.STATIC)) {
96+
this.processingEnvironment.getMessager().printMessage(
97+
Diagnostic.Kind.ERROR,
98+
String.format(
99+
"@CommandMethod annotated methods should be non-static (%s)",
100+
e.getSimpleName()
101+
),
102+
e
103+
);
104+
}
105+
106+
if (e.getReturnType().toString().equals("Void")) {
107+
this.processingEnvironment.getMessager().printMessage(
108+
Diagnostic.Kind.ERROR,
109+
String.format(
110+
"@CommandMethod annotated methods should return void (%s)",
111+
e.getSimpleName()
112+
),
113+
e
114+
);
115+
}
116+
117+
final CommandMethod commandMethod = e.getAnnotation(CommandMethod.class);
118+
final List<String> parameterArgumentNames = e.getParameters()
119+
.stream()
120+
.map(parameter -> parameter.getAnnotation(Argument.class))
121+
.filter(Objects::nonNull)
122+
.map(Argument::value)
123+
.collect(Collectors.toList());
124+
final List<String> parsedArgumentNames = new ArrayList<>(parameterArgumentNames.size());
125+
126+
final List<SyntaxFragment> syntaxFragments = this.syntaxParser.apply(commandMethod.value());
127+
128+
boolean foundOptional = false;
129+
for (final SyntaxFragment fragment : syntaxFragments) {
130+
if (fragment.getArgumentMode() == ArgumentMode.LITERAL) {
131+
continue;
132+
}
133+
134+
if (!parameterArgumentNames.contains(fragment.getMajor())) {
135+
this.processingEnvironment.getMessager().printMessage(
136+
Diagnostic.Kind.ERROR,
137+
String.format(
138+
"@Argument(\"%s\") is missing from @CommandMethod (%s)",
139+
fragment.getMajor(),
140+
e.getSimpleName()
141+
),
142+
e
143+
);
144+
}
145+
146+
if (fragment.getArgumentMode() == ArgumentMode.REQUIRED) {
147+
if (foundOptional) {
148+
this.processingEnvironment.getMessager().printMessage(
149+
Diagnostic.Kind.ERROR,
150+
String.format(
151+
"Required argument '%s' cannot succeed an optional argument (%s)",
152+
fragment.getMajor(),
153+
e.getSimpleName()
154+
),
155+
e
156+
);
157+
}
158+
} else {
159+
foundOptional = true;
160+
}
161+
162+
parsedArgumentNames.add(fragment.getMajor());
163+
}
164+
165+
for (final String argument : parameterArgumentNames) {
166+
if (!parsedArgumentNames.contains(argument)) {
167+
this.processingEnvironment.getMessager().printMessage(
168+
Diagnostic.Kind.ERROR,
169+
String.format(
170+
"Argument '%s' is missing from the @CommandMethod syntax (%s)",
171+
argument,
172+
e.getSimpleName()
173+
),
174+
e
175+
);
176+
}
177+
}
178+
179+
return null;
180+
}
181+
182+
@Override
183+
public Void visitTypeParameter(final TypeParameterElement e, final Void unused) {
184+
return null;
185+
}
186+
187+
@Override
188+
public Void visitUnknown(final Element e, final Void unused) {
189+
return null;
190+
}
191+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
cloud.commandframework.annotations.processing.CommandContainerProcessor
2+
cloud.commandframework.annotations.processing.CommandMethodProcessor

0 commit comments

Comments
 (0)