feat(annotations): add command containers (#364)
This is the first part of the introduction of annotation processing to cloud. A new `@CommandContainer` annotation has been introduced, which can be placed on classes to have the annotation parser automatically construct & parse the classes when `AnnotationParser.parseContainers()` is invoked. A future PR will introduce another processor that will scan for `@CommandMethod` annotations and verify the integrity of the annotated methods (visibility, argument annotations, etc.).
This commit is contained in:
parent
d613fd0208
commit
f1582fb64e
15 changed files with 543 additions and 2 deletions
|
|
@ -30,6 +30,7 @@ import cloud.commandframework.annotations.injection.ParameterInjectorRegistry;
|
|||
import cloud.commandframework.annotations.injection.RawArgs;
|
||||
import cloud.commandframework.annotations.parsers.MethodArgumentParser;
|
||||
import cloud.commandframework.annotations.parsers.Parser;
|
||||
import cloud.commandframework.annotations.processing.CommandContainerProcessor;
|
||||
import cloud.commandframework.annotations.specifier.Completions;
|
||||
import cloud.commandframework.annotations.suggestions.MethodSuggestionsProvider;
|
||||
import cloud.commandframework.annotations.suggestions.Suggestions;
|
||||
|
|
@ -48,16 +49,21 @@ import cloud.commandframework.extra.confirmation.CommandConfirmationManager;
|
|||
import cloud.commandframework.meta.CommandMeta;
|
||||
import cloud.commandframework.meta.SimpleCommandMeta;
|
||||
import io.leangen.geantyref.TypeToken;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.lang.reflect.Parameter;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
|
@ -66,6 +72,8 @@ import java.util.Set;
|
|||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
|
||||
|
|
@ -318,13 +326,64 @@ public final class AnnotationParser<C> {
|
|||
return Arrays.stream(strings).map(this::processString).toArray(String[]::new);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses all known {@link cloud.commandframework.annotations.processing.CommandContainer command containers}.
|
||||
*
|
||||
* @return Collection of parsed commands
|
||||
* @throws Exception re-throws all encountered exceptions.
|
||||
* @since 1.7.0
|
||||
* @see cloud.commandframework.annotations.processing.CommandContainer CommandContainer for more information.
|
||||
*/
|
||||
public @NonNull Collection<@NonNull Command<C>> parseContainers() throws Exception {
|
||||
final List<Command<C>> commands = new LinkedList<>();
|
||||
|
||||
final List<String> classes;
|
||||
try (InputStream stream = this.getClass().getClassLoader().getResourceAsStream(CommandContainerProcessor.PATH)) {
|
||||
if (stream == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8))) {
|
||||
classes = reader.lines().distinct().collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
||||
for (final String className : classes) {
|
||||
final Class<?> commandContainer = Class.forName(className);
|
||||
|
||||
// We now have the class, and we now just need to decide what constructor to invoke.
|
||||
// We first try to find a constructor which takes in the parser.
|
||||
@MonotonicNonNull Object instance;
|
||||
try {
|
||||
instance = commandContainer.getConstructor(AnnotationParser.class).newInstance(this);
|
||||
} catch (final NoSuchMethodException ignored) {
|
||||
try {
|
||||
// Then we try to find a no-arg constructor.
|
||||
instance = commandContainer.getConstructor().newInstance();
|
||||
} catch (final NoSuchMethodException e) {
|
||||
// If neither are found, we panic!
|
||||
throw new IllegalStateException(
|
||||
String.format(
|
||||
"Command container %s has no valid constructors",
|
||||
commandContainer
|
||||
),
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
commands.addAll(this.parse(instance));
|
||||
}
|
||||
|
||||
return Collections.unmodifiableList(commands);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan a class instance of {@link CommandMethod} annotations and attempt to
|
||||
* compile them into {@link Command} instances
|
||||
* compile them into {@link Command} instances.
|
||||
*
|
||||
* @param instance Instance to scan
|
||||
* @param <T> Type of the instance
|
||||
* @return Collection of parsed annotations
|
||||
* @return Collection of parsed commands
|
||||
*/
|
||||
@SuppressWarnings({"deprecation", "unchecked", "rawtypes"})
|
||||
public <T> @NonNull Collection<@NonNull Command<C>> parse(final @NonNull T instance) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
//
|
||||
// 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.AnnotationParser;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Indicates that the class contains
|
||||
* {@link cloud.commandframework.annotations.CommandMethod command metods}.
|
||||
* <p>
|
||||
* If using <i>cloud-annotations</i> as an annotation processor, then the class will
|
||||
* be listed in a special file under META-INF. These containers can be collectively
|
||||
* parsed using {@link AnnotationParser#parseContainers()}, which will create instances
|
||||
* of the containers and then call {@link AnnotationParser#parse(Object)} with the created instance.
|
||||
* <p>
|
||||
* Every class annotated with {@link CommandContainer} needs to be {@code public}, and it
|
||||
* also needs to have one of the following:
|
||||
* <ul>
|
||||
* <li>A {@code public} no-arg constructor</li>
|
||||
* <li>A {@code public} constructor with {@link AnnotationParser} as the sole parameter</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* <b>NOTE:</b> For container parsing to work, you need to make sure that <i>cloud-annotations</i> is added
|
||||
* as an annotation processor.
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface CommandContainer {
|
||||
String ANNOTATION_PATH = "cloud.commandframework.annotations.processing.CommandContainer";
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
//
|
||||
// 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 java.io.BufferedWriter;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
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;
|
||||
import javax.tools.Diagnostic;
|
||||
import javax.tools.StandardLocation;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
|
||||
@SupportedAnnotationTypes(CommandContainer.ANNOTATION_PATH)
|
||||
public final class CommandContainerProcessor extends AbstractProcessor {
|
||||
|
||||
/**
|
||||
* The file in which all command container names are stored.
|
||||
*/
|
||||
public static final String PATH = "META-INF/commands/cloud.commandframework.annotations.processing.CommandContainer";
|
||||
|
||||
@Override
|
||||
public boolean process(
|
||||
final @NonNull Set<? extends TypeElement> annotations,
|
||||
final @NonNull RoundEnvironment roundEnv
|
||||
) {
|
||||
final List<String> validTypes = new ArrayList<>();
|
||||
|
||||
final Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(CommandContainer.class);
|
||||
if (elements.isEmpty()) {
|
||||
return false; // Nothing to process...
|
||||
}
|
||||
|
||||
for (final Element element : elements) {
|
||||
if (element.getKind() != ElementKind.CLASS) {
|
||||
this.processingEnv.getMessager().printMessage(
|
||||
Diagnostic.Kind.ERROR,
|
||||
String.format(
|
||||
"@CommandMethod found on unsupported element type '%s' (%s)",
|
||||
element.getKind().name(),
|
||||
element.getSimpleName().toString()
|
||||
),
|
||||
element
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
element.accept(new CommandContainerVisitor(this.processingEnv, validTypes), null);
|
||||
}
|
||||
|
||||
for (final String type : validTypes) {
|
||||
this.processingEnv.getMessager().printMessage(
|
||||
Diagnostic.Kind.NOTE,
|
||||
String.format(
|
||||
"Found valid @CommandMethod annotated class: %s",
|
||||
type
|
||||
)
|
||||
);
|
||||
}
|
||||
this.writeCommandFile(validTypes);
|
||||
|
||||
// https://errorprone.info/bugpattern/DoNotClaimAnnotations
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SourceVersion getSupportedSourceVersion() {
|
||||
return SourceVersion.latest();
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unused", "try"})
|
||||
private void writeCommandFile(final @NonNull List<String> types) {
|
||||
try (BufferedWriter writer = new BufferedWriter(this.processingEnv.getFiler().createResource(
|
||||
StandardLocation.CLASS_OUTPUT,
|
||||
"",
|
||||
PATH
|
||||
).openWriter())) {
|
||||
for (final String t : types) {
|
||||
writer.write(t);
|
||||
writer.newLine();
|
||||
}
|
||||
writer.flush();
|
||||
} catch (final IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
//
|
||||
// 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 java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import javax.annotation.processing.ProcessingEnvironment;
|
||||
import javax.lang.model.element.Element;
|
||||
import javax.lang.model.element.ElementKind;
|
||||
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;
|
||||
|
||||
final class CommandContainerVisitor implements ElementVisitor<Void, Void> {
|
||||
|
||||
private static final Collection<String> PERMITTED_CONSTRUCTOR_PARAMETER_TYPES = Arrays.asList(
|
||||
"cloud.commandframework.annotations.AnnotationParser"
|
||||
);
|
||||
|
||||
private final ProcessingEnvironment processingEnvironment;
|
||||
private final Collection<String> validTypes;
|
||||
|
||||
private boolean suitableConstructorFound;
|
||||
|
||||
CommandContainerVisitor(
|
||||
final @NonNull ProcessingEnvironment processingEnvironment,
|
||||
final @NonNull Collection<@NonNull String> validTypes
|
||||
) {
|
||||
this.processingEnvironment = processingEnvironment;
|
||||
this.validTypes = validTypes;
|
||||
this.suitableConstructorFound = false;
|
||||
}
|
||||
|
||||
@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) {
|
||||
if (!e.getModifiers().contains(Modifier.PUBLIC)) {
|
||||
this.processingEnvironment.getMessager().printMessage(
|
||||
Diagnostic.Kind.ERROR,
|
||||
String.format("@CommandMethod annotated class must be public (%s)", e.getSimpleName()),
|
||||
e
|
||||
);
|
||||
}
|
||||
|
||||
for (final Element enclosedElement : e.getEnclosedElements()) {
|
||||
if (enclosedElement.getKind() != ElementKind.CONSTRUCTOR) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Visit the constructor.
|
||||
enclosedElement.accept(this, null);
|
||||
|
||||
// If we've already found a suitable constructor, there's no
|
||||
// need to search for more.
|
||||
if (this.suitableConstructorFound) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// When we've visited every constructor, we verify that we found
|
||||
// at least one suitable constructor.
|
||||
if (!this.suitableConstructorFound) {
|
||||
this.processingEnvironment.getMessager().printMessage(
|
||||
Diagnostic.Kind.ERROR,
|
||||
String.format("@CommandMethod must have a suitable constructor (%s)", e.getSimpleName()),
|
||||
e
|
||||
);
|
||||
}
|
||||
|
||||
// We know it's valid, so we'll add it to the list of valid types.
|
||||
this.validTypes.add(e.asType().toString());
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void visitVariable(final VariableElement e, final Void unused) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void visitExecutable(final ExecutableElement e, final Void unused) {
|
||||
// We only want to process public constructors.
|
||||
if (!e.getModifiers().contains(Modifier.PUBLIC)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Now we need to verify that the paramters are correct.
|
||||
final boolean containsIllegalParameter = e.getParameters()
|
||||
.stream()
|
||||
.map(parameter -> parameter.asType().toString())
|
||||
// Ignore annotations.
|
||||
.map(typeString -> typeString.split(" "))
|
||||
.map(parts -> parts[parts.length - 1])
|
||||
// Ignore type parameters.
|
||||
.map(part -> part.split("<")[0])
|
||||
.anyMatch(type -> !PERMITTED_CONSTRUCTOR_PARAMETER_TYPES.contains(type));
|
||||
if (containsIllegalParameter) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// We now know that there's a constructor which accepts the permitted types,
|
||||
// and is public - Yay.
|
||||
this.suitableConstructorFound = true;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
/**
|
||||
* Annotation-processing related classes.
|
||||
*
|
||||
* @since 1.7.0
|
||||
*/
|
||||
package cloud.commandframework.annotations.processing;
|
||||
|
|
@ -0,0 +1 @@
|
|||
cloud.commandframework.annotations.processing.CommandContainerProcessor
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
//
|
||||
// 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.common.truth.StringSubject;
|
||||
import com.google.testing.compile.Compilation;
|
||||
import com.google.testing.compile.Compiler;
|
||||
import com.google.testing.compile.JavaFileObjects;
|
||||
import javax.tools.StandardLocation;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static com.google.testing.compile.CompilationSubject.assertThat;
|
||||
import static com.google.testing.compile.Compiler.javac;
|
||||
|
||||
class CommandContainerProcessorTest {
|
||||
|
||||
@Test
|
||||
void testCommandContainerParsing() {
|
||||
// Arrange
|
||||
final Compiler compiler = javac().withProcessors(new CommandContainerProcessor());
|
||||
|
||||
// Act
|
||||
final Compilation compilation = compiler.compile(
|
||||
JavaFileObjects.forResource("TestCommandContainer.java"),
|
||||
JavaFileObjects.forResource("TestCommandContainer2.java")
|
||||
);
|
||||
|
||||
// Assert
|
||||
assertThat(compilation).succeeded();
|
||||
|
||||
final StringSubject contentSubject = assertThat(compilation).generatedFile(
|
||||
StandardLocation.CLASS_OUTPUT,
|
||||
"" /* package */,
|
||||
CommandContainerProcessor.PATH
|
||||
).contentsAsUtf8String();
|
||||
contentSubject.contains("TestCommandContainer");
|
||||
contentSubject.contains("TestCommandContainer2");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import cloud.commandframework.annotations.processing.CommandContainer;
|
||||
|
||||
@CommandContainer
|
||||
public class TestCommandContainer {
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import cloud.commandframework.annotations.AnnotationParser;
|
||||
import cloud.commandframework.annotations.processing.CommandContainer;
|
||||
|
||||
@CommandContainer
|
||||
public class TestCommandContainer2 {
|
||||
|
||||
public TestCommandContainer2(final AnnotationParser parser) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue