Skip to content

Commit

Permalink
api: Introduce KSP variant of the annotation processor
Browse files Browse the repository at this point in the history
This should improve the experience for Kotlin users
  • Loading branch information
zml2008 committed Jan 20, 2024
1 parent 46d018c commit f945607
Show file tree
Hide file tree
Showing 9 changed files with 656 additions and 68 deletions.
66 changes: 54 additions & 12 deletions api/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,66 @@ plugins {
`java-library`
`maven-publish`
id("velocity-publish")
kotlin("jvm") version "1.9.22"
}

java {
withJavadocJar()
withSourcesJar()
val apKotlinOnly by configurations.creating
val apAndMain by configurations.creating

val ap by sourceSets.creating

tasks.named("compileApKotlin", org.jetbrains.kotlin.gradle.tasks.KotlinCompile::class) {
libraries.from(apKotlinOnly)
}

sourceSets["main"].java {
srcDir("src/ap/java")
// Make IDEA happy -- eclipse doesn't handle Kotlin anyways
if (System.getProperty("idea.sync.active").toBoolean()) {
configurations.named("apImplementation") {
extendsFrom(apKotlinOnly)
}
}

configurations {
api { extendsFrom(apAndMain) }
named(ap.apiConfigurationName) { extendsFrom(apAndMain) }

sourceSets["main"].resources {
srcDir("src/ap/resources")
// Expose AP to other subprojects
sequenceOf(apiElements, runtimeElements).forEach {
it {
outgoing.variants.named("classes") {
val classesDirs = ap.output.classesDirs
classesDirs.forEach { dir ->
artifact(dir) {
type = ArtifactTypeDefinition.JVM_CLASS_DIRECTORY
builtBy(classesDirs.buildDependencies)
}
}
}
}
}
}

kotlin {
val minimumKotlin = "1.7"
target.compilations.configureEach {
kotlinOptions {
apiVersion = minimumKotlin
languageVersion = minimumKotlin
jvmTarget = "17"
}
}
}

java {
withJavadocJar()
withSourcesJar()
}

dependencies {
compileOnlyApi(libs.jspecify)

api(libs.gson)
api(libs.guava)
apAndMain(libs.gson)
apAndMain(libs.guava)

// DEPRECATED: Will be removed in Velocity Polymer
api("com.moandjiezana.toml:toml4j:0.7.2")
Expand All @@ -39,21 +79,23 @@ dependencies {

api(libs.slf4j)
api(libs.guice)
api(libs.checker.qual)
apAndMain(libs.checker.qual)
api(libs.brigadier)
api(libs.bundles.configurate4)
api(libs.caffeine)
implementation(ap.output)
apKotlinOnly(libs.kspApi)
apKotlinOnly(kotlin("stdlib-jdk8", "1.7.22"))
}

tasks {
jar {
manifest {
attributes["Automatic-Module-Name"] = "com.velocitypowered.api"
}
from(ap.output)
}
withType<Javadoc> {
exclude("com/velocitypowered/api/plugin/ap/**")

val o = options as StandardJavadocDocletOptions
o.encoding = "UTF-8"
o.source = "8"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,39 +7,48 @@

package com.velocitypowered.api.plugin.ap;

import com.google.gson.Gson;
import com.velocitypowered.api.plugin.Plugin;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.Writer;
import java.util.Objects;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.Name;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.tools.Diagnostic;
import javax.tools.FileObject;
import javax.tools.StandardLocation;
import org.checkerframework.checker.nullness.qual.Nullable;

/**
* Annotation processor for Velocity.
*/
@SupportedAnnotationTypes({"com.velocitypowered.api.plugin.Plugin"})
@SupportedAnnotationTypes({PluginProcessingEnvironment.PLUGIN_CLASS_NAME})
public class PluginAnnotationProcessor extends AbstractProcessor {

private ProcessingEnvironment environment;
private String pluginClassFound;
private boolean warnedAboutMultiplePlugins;
private PluginProcessingEnvironment<Element> pluginEnv;

@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
this.environment = processingEnv;
public synchronized void init(final ProcessingEnvironment processingEnv) {
super.init(processingEnv);
this.pluginEnv = new JavaProcessingEnvironment(processingEnv);
}

@Override
Expand All @@ -54,52 +63,200 @@ public synchronized boolean process(Set<? extends TypeElement> annotations,
return false;
}

for (Element element : roundEnv.getElementsAnnotatedWith(Plugin.class)) {
if (element.getKind() != ElementKind.CLASS) {
environment.getMessager()
.printMessage(Diagnostic.Kind.ERROR, "Only classes can be annotated with "
+ Plugin.class.getCanonicalName());
final TypeElement pluginAnnElement = this.processingEnv.getElementUtils()
.getTypeElement(PluginProcessingEnvironment.PLUGIN_CLASS_NAME);
if (pluginAnnElement == null) {
this.processingEnv.getMessager().printMessage(
Diagnostic.Kind.ERROR,
"Unable to find an element of type "
+ PluginProcessingEnvironment.PLUGIN_CLASS_NAME + " on classpath");
return false;
}

for (final Element element : roundEnv.getElementsAnnotatedWith(pluginAnnElement)) {
if (!this.pluginEnv.process(element)) {
return false;
}
}

return false;
}

static final class JavaProcessingEnvironment extends PluginProcessingEnvironment<Element> {
private final Messager messager;
private final Filer filer;
private final Elements elements;

JavaProcessingEnvironment(final ProcessingEnvironment env) {
this.messager = env.getMessager();
this.filer = env.getFiler();
this.elements = env.getElementUtils();
}

@Override
void logNotice(final String message, final Element element) {
this.messager.printMessage(Diagnostic.Kind.NOTE, message, element);
}

@Override
void logWarning(final String message, final Element element) {
this.messager.printMessage(Diagnostic.Kind.WARNING, message, element);
}

Name qualifiedName = ((TypeElement) element).getQualifiedName();
@Override
void logError(final String message, final Element element) {
this.messager.printMessage(Diagnostic.Kind.ERROR, message, element);
}

@Override
void logError(final String message, final Element element, final Exception ex) {
this.messager.printMessage(Diagnostic.Kind.ERROR, message, element);
final StringWriter writer = new StringWriter();
ex.printStackTrace(new PrintWriter(writer));
this.messager.printMessage(Diagnostic.Kind.ERROR, writer.getBuffer());

}

@Override
boolean isClass(final Element element) {
return element.getKind().isClass();
}

@Override
String getQualifiedName(final Element element) {
return ((TypeElement) element).getQualifiedName().toString();
}

if (Objects.equals(pluginClassFound, qualifiedName.toString())) {
if (!warnedAboutMultiplePlugins) {
environment.getMessager()
.printMessage(Diagnostic.Kind.WARNING, "Velocity does not yet currently support "
+ "multiple plugins. We are using " + pluginClassFound
+ " for your plugin's main class.");
warnedAboutMultiplePlugins = true;
@Override
@Nullable AnnotationWrapper getPluginAnnotation(final Element element) {
for (final AnnotationMirror mirror : element.getAnnotationMirrors()) {
final Element typeElement = mirror.getAnnotationType().asElement();
if (typeElement.getSimpleName().contentEquals("Plugin")
&& (typeElement.getKind() == ElementKind.ANNOTATION_TYPE)
&& ((TypeElement) typeElement).getQualifiedName().contentEquals(PLUGIN_CLASS_NAME)) {
return new JavaAnnotationWrapper(this.elements, mirror);
}
return false;
}
return null;
}

Plugin plugin = element.getAnnotation(Plugin.class);
if (!SerializedPluginDescription.ID_PATTERN.matcher(plugin.id()).matches()) {
environment.getMessager().printMessage(Diagnostic.Kind.ERROR, "Invalid ID for plugin "
+ qualifiedName
+ ". IDs must start alphabetically, have lowercase alphanumeric characters, and "
+ "can contain dashes or underscores.");
return false;
@Override
BufferedWriter openWriter(
final String pkg,
final String name,
final Element sourceElement
) throws IOException {
final FileObject file = this.filer.createResource(
StandardLocation.CLASS_OUTPUT, pkg, name, sourceElement);
return new BufferedWriter(file.openWriter());
}

static final class JavaAnnotationWrapper implements AnnotationWrapper {
private final Elements elements;
private final Map<String, Object> explicitValues;
private final Map<String, Object> defaultValues;

JavaAnnotationWrapper(final Elements elements, final AnnotationMirror annotation) {
final Map<String, Object> explicitVals = new HashMap<>();
for (final var methodToValue : annotation.getElementValues().entrySet()) {
final String name = methodToValue.getKey().getSimpleName().toString();
final Object value = methodToValue.getValue().getValue();

explicitVals.put(name, value);
}

final Map<String, Object> defaultVals = new HashMap<>();
final var elementsWithDefaults = elements.getElementValuesWithDefaults(annotation);
for (final var methodToValue : elementsWithDefaults.entrySet()) {
final String name = methodToValue.getKey().getSimpleName().toString();
if (explicitVals.containsKey(name)) {
continue;
}

final Object value = methodToValue.getValue().getValue();
defaultVals.put(name, value);
}


this.elements = elements;
this.explicitValues = explicitVals;
this.defaultValues = defaultVals;
}

// All good, generate the velocity-plugin.json.
SerializedPluginDescription description = SerializedPluginDescription
.from(plugin, qualifiedName.toString());
try {
FileObject object = environment.getFiler()
.createResource(StandardLocation.CLASS_OUTPUT, "", "velocity-plugin.json");
try (Writer writer = new BufferedWriter(object.openWriter())) {
new Gson().toJson(description, writer);
private Object unbox(final Object boxed) {
if (boxed instanceof TypeMirror) { // .class reference
// TODO: not used for Velocity
return boxed;
} else if (boxed instanceof VariableElement) { // enum constant
return ((VariableElement) boxed).getSimpleName();
} else if (boxed instanceof AnnotationMirror) { // nested annotation
return new JavaAnnotationWrapper(this.elements, (AnnotationMirror) boxed);
} else if (boxed instanceof List<?>) {
@SuppressWarnings("unchecked")
final List<AnnotationValue> boxedList = (List<AnnotationValue>) boxed;
final List<Object> val = new ArrayList<>(boxedList.size());
for (final AnnotationValue o : boxedList) {
val.add(this.unbox(o.getValue()));
}
return val;
} else if (boxed instanceof CharSequence) {
return boxed.toString();
} else {
return boxed;
}
pluginClassFound = qualifiedName.toString();
} catch (IOException e) {
environment.getMessager()
.printMessage(Diagnostic.Kind.ERROR, "Unable to generate plugin file");
}
}

return false;
private <T> T unboxAndCast(final Object o, final Class<T> expectedType) {
final Object value = this.unbox(o);
if (!expectedType.isInstance(value)) {
throw new IllegalArgumentException("Expected '" + value + "' to be a "
+ expectedType + ", but it was a " + value.getClass() + " instead");
}
return expectedType.cast(value);
}

@Override
public <T> @Nullable T get(final String key, final Class<T> expectedType) {
Object value = this.explicitValues.get(key);
if (value == null) {
value = this.defaultValues.get(key);
}

if (value == null) {
return null;
}

return this.unboxAndCast(value, expectedType);
}

@Override
public @Nullable <T> List<T> getList(final String key, final Class<T> expectedType) {
Object value = this.explicitValues.get(key);
if (value == null) {
value = this.defaultValues.get(key);
}

if (value == null) {
return null;
}

if (value instanceof List<?>) {
@SuppressWarnings("unchecked")
final List<AnnotationValue> boxedList = (List<AnnotationValue>) value;
final List<T> val = new ArrayList<>(boxedList.size());
for (final AnnotationValue o : boxedList) {
val.add(this.unboxAndCast(o.getValue(), expectedType));
}
return val;
} else {
return Collections.singletonList(this.unboxAndCast(value, expectedType));
}
}

@Override
public boolean isExplicit(final String key) {
return this.explicitValues.containsKey(key);
}
}
}
}
Loading

0 comments on commit f945607

Please sign in to comment.